From e0d2064862b2681784529ac8910c0cf09525ad4a Mon Sep 17 00:00:00 2001 From: adisivaprasad Date: Fri, 21 Aug 2026 12:28:00 -0700 Subject: [PATCH] connhelper: include ssh remote host in dummy client URL When connecting to a daemon over ssh, ConnectionHelper.Host is used as a dummy URL for the API client, since Go's stdlib requires a valid hostname in requests. This dummy URL (http://docker.example.com) also showed up in connection error messages, which made users believe the CLI was trying to connect to docker.example.com instead of the remote host they configured: Error: Cannot connect to the Docker daemon at http://docker.example.com. Use the ssh target's hostname (and non-default port) in the dummy URL so that connection errors point at the host the user configured, e.g.: Error: Cannot connect to the Docker daemon at http://myserver.local. The URL remains a dummy; the actual connection is established via the ssh dialer, so this does not change how requests are transported. Fixes: docker/cli#5604 Signed-off-by: adisivaprasad --- cli/connhelper/connhelper.go | 14 +++++++++-- cli/connhelper/connhelper_test.go | 42 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/cli/connhelper/connhelper.go b/cli/connhelper/connhelper.go index 8d97a2a40b7f..efd4462e5e6a 100644 --- a/cli/connhelper/connhelper.go +++ b/cli/connhelper/connhelper.go @@ -19,7 +19,7 @@ import ( // ConnectionHelper allows to connect to a remote host with custom stream provider binary. type ConnectionHelper struct { Dialer func(ctx context.Context, network, addr string) (net.Conn, error) - Host string // dummy URL used for HTTP requests. e.g. "http://docker" + Host string // dummy URL used for HTTP requests. e.g. "http://docker". For ssh connections, the dummy URL includes the remote hostname so that connection error messages refer to the host the user intends to connect to. } // GetConnectionHelper returns Docker-specific connection helper for the given URL. @@ -61,11 +61,21 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*ConnectionHelper if err != nil { return nil, err } + + // The Host is a dummy URL used for the API client to build requests; + // the actual connection is made through the Dialer above. Using the + // remote hostname in the dummy URL makes sure that error messages + // (e.g. "Cannot connect to the Docker daemon at ...") mention the + // host the user configured, instead of a non-descriptive dummy domain. + host := sp.Host + if sp.Port != "" && sp.Port != "22" { + host = net.JoinHostPort(host, sp.Port) + } return &ConnectionHelper{ Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { return commandconn.New(ctx, "ssh", sshArgs...) }, - Host: "http://docker.example.com", + Host: "http://" + host, }, nil } // Future version may support plugins via ~/.docker/config.json. e.g. "dind" diff --git a/cli/connhelper/connhelper_test.go b/cli/connhelper/connhelper_test.go index 66d2487c3423..eb76128ab3b5 100644 --- a/cli/connhelper/connhelper_test.go +++ b/cli/connhelper/connhelper_test.go @@ -31,6 +31,48 @@ func TestSSHFlags(t *testing.T) { } } +func TestConnectionHelperHostIncludesRemoteHost(t *testing.T) { + testCases := []struct { + name string + daemonURL string + expected string + }{ + { + name: "ssh with user and default port", + daemonURL: "ssh://user@myserver.local", + expected: "http://myserver.local", + }, + { + name: "ssh with custom port", + daemonURL: "ssh://user@myserver.local:2222", + expected: "http://myserver.local:2222", + }, + { + name: "ssh with explicit default port", + daemonURL: "ssh://myserver.local:22", + expected: "http://myserver.local", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + helper, err := GetConnectionHelper(tc.daemonURL) + assert.NilError(t, err) + assert.Assert(t, helper != nil) + assert.Equal(t, helper.Host, tc.expected, + "dummy Host URL must include the remote hostname so connection errors point to the daemon the user configured (docker/cli#5604)") + assert.Assert(t, helper.Dialer != nil) + }) + } +} + +func TestGetCommandConnectionHelperHost(t *testing.T) { + helper, err := GetCommandConnectionHelper("fake-command") + assert.NilError(t, err) + assert.Assert(t, helper != nil) + assert.Equal(t, helper.Host, "http://docker.example.com") +} + func TestDisablePseudoTerminalAllocation(t *testing.T) { testCases := []struct { name string