Skip to content
This repository was archived by the owner on Oct 13, 2023. It is now read-only.

Commit 427c7cc

Browse files
Anca IordachethaJeztah
Anca Iordache
authored andcommitted
Add http(s) proxy properties to daemon configuration
This allows configuring the daemon's proxy server through the daemon.json con- figuration file or command-line flags configuration file, in addition to the existing option (through environment variables). Configuring environment variables on Windows to configure a service is more complicated than on Linux, and adding alternatives for this to the daemon con- figuration makes the configuration more transparent and easier to use. The configuration as set through command-line flags or through the daemon.json configuration file takes precedence over env-vars in the daemon's environment, which allows the daemon to use a different proxy. If both command-line flags and a daemon.json configuration option is set, an error is produced when starting the daemon. Note that this configuration is not "live reloadable" due to Golang's use of `sync.Once()` for proxy configuration, which means that changing the proxy configuration requires a restart of the daemon (reload / SIGHUP will not update the configuration. With this patch: cat /etc/docker/daemon.json { "http-proxy": "http://proxytest.example.com:80", "https-proxy": "https://proxytest.example.com:443" } docker pull busybox Using default tag: latest Error response from daemon: Get "https://registry-1.docker.io/v2/": proxyconnect tcp: dial tcp: lookup proxytest.example.com on 127.0.0.11:53: no such host docker build . Sending build context to Docker daemon 89.28MB Step 1/3 : FROM golang:1.16-alpine AS base Get "https://registry-1.docker.io/v2/": proxyconnect tcp: dial tcp: lookup proxytest.example.com on 127.0.0.11:53: no such host Integration tests were added to test the behavior: - verify that the configuration through all means are used (env-var, command-line flags, damon.json), and used in the expected order of preference. - verify that conflicting options produce an error. Signed-off-by: Anca Iordache <[email protected]> Signed-off-by: Sebastiaan van Stijn <[email protected]>
1 parent a6ce7ef commit 427c7cc

File tree

5 files changed

+203
-3
lines changed

5 files changed

+203
-3
lines changed

cmd/dockerd/config.go

+4
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) error {
101101

102102
flags.StringVar(&conf.DefaultRuntime, "default-runtime", config.StockRuntimeName, "Default OCI runtime for containers")
103103

104+
flags.StringVar(&conf.HTTPProxy, "http-proxy", "", "HTTP proxy URL to use for outgoing traffic")
105+
flags.StringVar(&conf.HTTPSProxy, "https-proxy", "", "HTTPS proxy URL to use for outgoing traffic")
106+
flags.StringVar(&conf.NoProxy, "no-proxy", "", "Comma-separated list of hosts or IP addresses for which the proxy is skipped")
107+
104108
return nil
105109
}
106110

cmd/dockerd/daemon.go

+28
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
8787
return nil
8888
}
8989

90+
configureProxyEnv(cli.Config)
91+
9092
warnOnDeprecatedConfigOptions(cli.Config)
9193

9294
if err := configureDaemonLogs(cli.Config); err != nil {
@@ -779,3 +781,29 @@ func configureDaemonLogs(conf *config.Config) error {
779781
})
780782
return nil
781783
}
784+
785+
func configureProxyEnv(conf *config.Config) {
786+
if p := conf.HTTPProxy; p != "" {
787+
overrideProxyEnv("HTTP_PROXY", p)
788+
overrideProxyEnv("http_proxy", p)
789+
}
790+
if p := conf.HTTPSProxy; p != "" {
791+
overrideProxyEnv("HTTPS_PROXY", p)
792+
overrideProxyEnv("https_proxy", p)
793+
}
794+
if p := conf.NoProxy; p != "" {
795+
overrideProxyEnv("NO_PROXY", p)
796+
overrideProxyEnv("no_proxy", p)
797+
}
798+
}
799+
800+
func overrideProxyEnv(name, val string) {
801+
if oldVal := os.Getenv(name); oldVal != "" && oldVal != val {
802+
logrus.WithFields(logrus.Fields{
803+
"name": name,
804+
"old-value": oldVal,
805+
"new-value": val,
806+
}).Warn("overriding existing proxy variable with value from configuration")
807+
}
808+
_ = os.Setenv(name, val)
809+
}

daemon/config/config.go

+8
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ type CommonConfig struct {
166166
ExecRoot string `json:"exec-root,omitempty"`
167167
SocketGroup string `json:"group,omitempty"`
168168
CorsHeaders string `json:"api-cors-header,omitempty"`
169+
ProxyConfig
169170

170171
// TrustKeyPath is used to generate the daemon ID and for signing schema 1 manifests
171172
// when pushing to a registry which does not support schema 2. This field is marked as
@@ -276,6 +277,13 @@ type CommonConfig struct {
276277
DefaultRuntime string `json:"default-runtime,omitempty"`
277278
}
278279

280+
// ProxyConfig holds the proxy-configuration for the daemon.
281+
type ProxyConfig struct {
282+
HTTPProxy string `json:"http-proxy,omitempty"`
283+
HTTPSProxy string `json:"https-proxy,omitempty"`
284+
NoProxy string `json:"no-proxy,omitempty"`
285+
}
286+
279287
// IsValueSet returns true if a configuration value
280288
// was explicitly set in the configuration file.
281289
func (conf *Config) IsValueSet(name string) bool {

daemon/info.go

+10-3
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ func (daemon *Daemon) SystemInfo() *types.Info {
6363
Labels: daemon.configStore.Labels,
6464
ExperimentalBuild: daemon.configStore.Experimental,
6565
ServerVersion: dockerversion.Version,
66-
HTTPProxy: config.MaskCredentials(getEnvAny("HTTP_PROXY", "http_proxy")),
67-
HTTPSProxy: config.MaskCredentials(getEnvAny("HTTPS_PROXY", "https_proxy")),
68-
NoProxy: getEnvAny("NO_PROXY", "no_proxy"),
66+
HTTPProxy: config.MaskCredentials(getConfigOrEnv(daemon.configStore.HTTPProxy, "HTTP_PROXY", "http_proxy")),
67+
HTTPSProxy: config.MaskCredentials(getConfigOrEnv(daemon.configStore.HTTPSProxy, "HTTPS_PROXY", "https_proxy")),
68+
NoProxy: getConfigOrEnv(daemon.configStore.NoProxy, "NO_PROXY", "no_proxy"),
6969
LiveRestoreEnabled: daemon.configStore.LiveRestoreEnabled,
7070
Isolation: daemon.defaultIsolation,
7171
}
@@ -296,3 +296,10 @@ func getEnvAny(names ...string) string {
296296
}
297297
return ""
298298
}
299+
300+
func getConfigOrEnv(config string, env ...string) string {
301+
if config != "" {
302+
return config
303+
}
304+
return getEnvAny(env...)
305+
}

integration/daemon/daemon_test.go

+153
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
package daemon // import "github.com/docker/docker/integration/daemon"
22

33
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"net/http/httptest"
48
"os"
59
"os/exec"
610
"path/filepath"
711
"runtime"
812
"testing"
913

14+
"github.com/docker/docker/api/types"
1015
"github.com/docker/docker/daemon/config"
1116
"github.com/docker/docker/testutil/daemon"
1217
"gotest.tools/v3/assert"
1318
is "gotest.tools/v3/assert/cmp"
19+
"gotest.tools/v3/env"
1420
"gotest.tools/v3/skip"
1521
)
1622

@@ -146,3 +152,150 @@ func TestConfigDaemonSeccompProfiles(t *testing.T) {
146152
})
147153
}
148154
}
155+
156+
func TestDaemonProxy(t *testing.T) {
157+
skip.If(t, runtime.GOOS == "windows", "cannot start multiple daemons on windows")
158+
159+
var received string
160+
proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
161+
received = r.Host
162+
w.Header().Set("Content-Type", "application/json")
163+
_, _ = w.Write([]byte("OK"))
164+
}))
165+
defer proxyServer.Close()
166+
167+
// Configure proxy through env-vars
168+
t.Run("environment variables", func(t *testing.T) {
169+
defer env.Patch(t, "HTTP_PROXY", proxyServer.URL)()
170+
defer env.Patch(t, "HTTPS_PROXY", proxyServer.URL)()
171+
defer env.Patch(t, "NO_PROXY", "example.com")()
172+
173+
d := daemon.New(t)
174+
c := d.NewClientT(t)
175+
defer func() { _ = c.Close() }()
176+
ctx := context.Background()
177+
d.Start(t)
178+
179+
_, err := c.ImagePull(ctx, "example.org:5000/some/image:latest", types.ImagePullOptions{})
180+
assert.ErrorContains(t, err, "", "pulling should have failed")
181+
assert.Equal(t, received, "example.org:5000")
182+
183+
// Test NoProxy: example.com should not hit the proxy, and "received" variable should not be changed.
184+
_, err = c.ImagePull(ctx, "example.com/some/image:latest", types.ImagePullOptions{})
185+
assert.ErrorContains(t, err, "", "pulling should have failed")
186+
assert.Equal(t, received, "example.org:5000", "should not have used proxy")
187+
188+
info := d.Info(t)
189+
assert.Equal(t, info.HTTPProxy, proxyServer.URL)
190+
assert.Equal(t, info.HTTPSProxy, proxyServer.URL)
191+
assert.Equal(t, info.NoProxy, "example.com")
192+
d.Stop(t)
193+
})
194+
195+
// Configure proxy through command-line flags
196+
t.Run("command-line options", func(t *testing.T) {
197+
defer env.Patch(t, "HTTP_PROXY", "http://from-env-http.invalid")()
198+
defer env.Patch(t, "http_proxy", "http://from-env-http.invalid")()
199+
defer env.Patch(t, "HTTPS_PROXY", "https://from-env-https.invalid")()
200+
defer env.Patch(t, "https_proxy", "https://from-env-http.invalid")()
201+
defer env.Patch(t, "NO_PROXY", "ignore.invalid")()
202+
defer env.Patch(t, "no_proxy", "ignore.invalid")()
203+
204+
d := daemon.New(t)
205+
d.Start(t, "--http-proxy", proxyServer.URL, "--https-proxy", proxyServer.URL, "--no-proxy", "example.com")
206+
207+
logs, err := d.ReadLogFile()
208+
assert.NilError(t, err)
209+
assert.Assert(t, is.Contains(string(logs), "overriding existing proxy variable with value from configuration"))
210+
for _, v := range []string{"http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY", "no_proxy", "NO_PROXY"} {
211+
assert.Assert(t, is.Contains(string(logs), "name="+v))
212+
}
213+
214+
c := d.NewClientT(t)
215+
defer func() { _ = c.Close() }()
216+
ctx := context.Background()
217+
218+
_, err = c.ImagePull(ctx, "example.org:5001/some/image:latest", types.ImagePullOptions{})
219+
assert.ErrorContains(t, err, "", "pulling should have failed")
220+
assert.Equal(t, received, "example.org:5001")
221+
222+
// Test NoProxy: example.com should not hit the proxy, and "received" variable should not be changed.
223+
_, err = c.ImagePull(ctx, "example.com/some/image:latest", types.ImagePullOptions{})
224+
assert.ErrorContains(t, err, "", "pulling should have failed")
225+
assert.Equal(t, received, "example.org:5001", "should not have used proxy")
226+
227+
info := d.Info(t)
228+
assert.Equal(t, info.HTTPProxy, proxyServer.URL)
229+
assert.Equal(t, info.HTTPSProxy, proxyServer.URL)
230+
assert.Equal(t, info.NoProxy, "example.com")
231+
232+
d.Stop(t)
233+
})
234+
235+
// Configure proxy through configuration file
236+
t.Run("configuration file", func(t *testing.T) {
237+
defer env.Patch(t, "HTTP_PROXY", "http://from-env-http.invalid")()
238+
defer env.Patch(t, "http_proxy", "http://from-env-http.invalid")()
239+
defer env.Patch(t, "HTTPS_PROXY", "https://from-env-https.invalid")()
240+
defer env.Patch(t, "https_proxy", "https://from-env-http.invalid")()
241+
defer env.Patch(t, "NO_PROXY", "ignore.invalid")()
242+
defer env.Patch(t, "no_proxy", "ignore.invalid")()
243+
244+
d := daemon.New(t)
245+
c := d.NewClientT(t)
246+
defer func() { _ = c.Close() }()
247+
ctx := context.Background()
248+
249+
configFile := filepath.Join(d.RootDir(), "daemon.json")
250+
configJSON := fmt.Sprintf(`{"http-proxy":%[1]q, "https-proxy": %[1]q, "no-proxy": "example.com"}`, proxyServer.URL)
251+
assert.NilError(t, os.WriteFile(configFile, []byte(configJSON), 0644))
252+
253+
d.Start(t, "--config-file", configFile)
254+
255+
logs, err := d.ReadLogFile()
256+
assert.NilError(t, err)
257+
assert.Assert(t, is.Contains(string(logs), "overriding existing proxy variable with value from configuration"))
258+
for _, v := range []string{"http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY", "no_proxy", "NO_PROXY"} {
259+
assert.Assert(t, is.Contains(string(logs), "name="+v))
260+
}
261+
262+
_, err = c.ImagePull(ctx, "example.org:5002/some/image:latest", types.ImagePullOptions{})
263+
assert.ErrorContains(t, err, "", "pulling should have failed")
264+
assert.Equal(t, received, "example.org:5002")
265+
266+
// Test NoProxy: example.com should not hit the proxy, and "received" variable should not be changed.
267+
_, err = c.ImagePull(ctx, "example.com/some/image:latest", types.ImagePullOptions{})
268+
assert.ErrorContains(t, err, "", "pulling should have failed")
269+
assert.Equal(t, received, "example.org:5002", "should not have used proxy")
270+
271+
info := d.Info(t)
272+
assert.Equal(t, info.HTTPProxy, proxyServer.URL)
273+
assert.Equal(t, info.HTTPSProxy, proxyServer.URL)
274+
assert.Equal(t, info.NoProxy, "example.com")
275+
276+
d.Stop(t)
277+
})
278+
279+
// Conflicting options (passed both through command-line options and config file)
280+
t.Run("conflicting options", func(t *testing.T) {
281+
const (
282+
proxyRawURL = "https://myuser:[email protected]"
283+
)
284+
285+
d := daemon.New(t)
286+
287+
configFile := filepath.Join(d.RootDir(), "daemon.json")
288+
configJSON := fmt.Sprintf(`{"http-proxy":%[1]q, "https-proxy": %[1]q, "no-proxy": "example.com"}`, proxyRawURL)
289+
assert.NilError(t, os.WriteFile(configFile, []byte(configJSON), 0644))
290+
291+
err := d.StartWithError("--http-proxy", proxyRawURL, "--https-proxy", proxyRawURL, "--no-proxy", "example.com", "--config-file", configFile, "--validate")
292+
assert.ErrorContains(t, err, "daemon exited during startup")
293+
logs, err := d.ReadLogFile()
294+
assert.NilError(t, err)
295+
expected := fmt.Sprintf(
296+
`the following directives are specified both as a flag and in the configuration file: http-proxy: (from flag: %[1]s, from file: %[1]s), https-proxy: (from flag: %[1]s, from file: %[1]s), no-proxy: (from flag: example.com, from file: example.com)`,
297+
proxyRawURL,
298+
)
299+
assert.Assert(t, is.Contains(string(logs), expected))
300+
})
301+
}

0 commit comments

Comments
 (0)