From 4278ca83bc5a260847697e37aafdccb66917f0bc Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:58:17 +0800 Subject: [PATCH 1/8] Registries proxy config: initial impl Signed-off-by: cyqsimon <28627918+cyqsimon@users.noreply.github.com> --- image/docker/docker_client.go | 26 ++++++++++-- image/docker/docker_image_src.go | 5 +++ .../sysregistriesv2/system_registries_v2.go | 42 +++++++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/image/docker/docker_client.go b/image/docker/docker_client.go index 2f257076f5..9f2f811b39 100644 --- a/image/docker/docker_client.go +++ b/image/docker/docker_client.go @@ -114,6 +114,10 @@ type dockerClient struct { // tlsClientConfig is setup by newDockerClient and will be used and updated // by detectProperties(). Callers can edit tlsClientConfig.InsecureSkipVerify in the meantime. tlsClientConfig *tls.Config + // registryProxy is the proxy URL from the registry configuration, if any. + // It has the lowest priority and can be overridden by either DockerProxyURL or environment variables. + // When pulling, this value could be overwritten by a mirror-specific proxy. See docker_client_src.go. + registryProxy *url.URL // The following members are not set by newDockerClient and must be set by callers if needed. auth types.DockerAuthConfig registryToken string @@ -262,18 +266,24 @@ func newDockerClient(sys *types.SystemContext, registry, reference string) (*doc return nil, err } - // Check if TLS verification shall be skipped (default=false) which can - // be specified in the sysregistriesv2 configuration. - skipVerify := false + // Fetch and load sysregistriesv2 configurations. reg, err := sysregistriesv2.FindRegistry(sys, reference) if err != nil { return nil, fmt.Errorf("loading registries: %w", err) } + skipVerify := false + var registryProxy *url.URL if reg != nil { if reg.Blocked { return nil, fmt.Errorf("registry %s is blocked in %s or %s", reg.Prefix, sysregistriesv2.ConfigPath(sys), sysregistriesv2.ConfigDirPath(sys)) } + // Check if TLS verification shall be skipped (default=false). skipVerify = reg.Insecure + // Set registry proxy. + registryProxy, err = sysregistriesv2.ParseProxy(reg.Proxy) + if err != nil { + return nil, err + } } tlsClientConfig.InsecureSkipVerify = skipVerify @@ -287,6 +297,7 @@ func newDockerClient(sys *types.SystemContext, registry, reference string) (*doc registry: registry, userAgent: userAgent, tlsClientConfig: tlsClientConfig, + registryProxy: registryProxy, tokenCache: map[string]*bearerToken{}, reportedWarnings: set.New[string](), }, nil @@ -968,6 +979,15 @@ func (c *dockerClient) detectPropertiesHelper(ctx context.Context) error { } tr := tlsclientconfig.NewTransport() tr.TLSClientConfig = c.tlsClientConfig + // Set registry-specific proxy with lowest priority, which can be overridden by environment variables. + if c.registryProxy != nil { + tr.Proxy = func(req *http.Request) (*url.URL, error) { + if envProxy, err := http.ProxyFromEnvironment(req); err != nil || envProxy != nil { + return envProxy, err + } + return c.registryProxy, nil + } + } // if set DockerProxyURL explicitly, use the DockerProxyURL instead of system proxy if c.sys != nil && c.sys.DockerProxyURL != nil { tr.Proxy = http.ProxyURL(c.sys.DockerProxyURL) diff --git a/image/docker/docker_image_src.go b/image/docker/docker_image_src.go index 4003af5d27..3ba69d39c4 100644 --- a/image/docker/docker_image_src.go +++ b/image/docker/docker_image_src.go @@ -150,6 +150,11 @@ func newImageSourceAttempt(ctx context.Context, sys *types.SystemContext, logica return nil, err } client.tlsClientConfig.InsecureSkipVerify = pullSource.Endpoint.Insecure + registryProxy, err := sysregistriesv2.ParseProxy(pullSource.Endpoint.Proxy) + if err != nil { + return nil, err + } + client.registryProxy = registryProxy s := &dockerImageSource{ PropertyMethodsInitialize: impl.PropertyMethods(impl.Properties{ diff --git a/image/pkg/sysregistriesv2/system_registries_v2.go b/image/pkg/sysregistriesv2/system_registries_v2.go index 0cf44571d3..849be7c182 100644 --- a/image/pkg/sysregistriesv2/system_registries_v2.go +++ b/image/pkg/sysregistriesv2/system_registries_v2.go @@ -5,6 +5,7 @@ import ( "fmt" "io/fs" "maps" + "net/url" "os" "path/filepath" "reflect" @@ -59,6 +60,8 @@ type Endpoint struct { // If true, certs verification will be skipped and HTTP (non-TLS) // connections will be allowed. Insecure bool `toml:"insecure,omitempty"` + // The forwarding proxy to be used for accessing this endpoint. + Proxy string `toml:"proxy,omitempty"` // PullFromMirror is used for adding restrictions to image pull through the mirror. // Set to "all", "digest-only", or "tag-only". // If "digest-only", mirrors will only be used for digest pulls. Pulling images by @@ -341,6 +344,32 @@ func parseLocation(input string) (string, error) { return trimmed, nil } +// ParseProxy parses the input string for a proxy configuration. +// Errors if a scheme is unsupported or unspecified, or if the input is not a valid URL. +func ParseProxy(input string) (*url.URL, error) { + if input == "" { + return nil, nil + } + + var hasSupportedScheme bool + for _, scheme := range []string{"http://", "https://", "socks5://", "socks5h://"} { + if strings.HasPrefix(input, scheme) { + hasSupportedScheme = true + break + } + } + if !hasSupportedScheme { + return nil, &InvalidRegistries{s: "invalid proxy: proxy URL must specify one of the supported schemes: http://, https://, socks5://, socks5h://"} + } + + parsed, err := url.Parse(input) + if err != nil { + return nil, fmt.Errorf("parsing proxy URL %q: %w", input, err) + } + + return parsed, nil +} + // ConvertToV2 returns a v2 config corresponding to a v1 one. func (config *V1RegistriesConf) ConvertToV2() (*V2RegistriesConf, error) { regMap := make(map[string]*Registry) @@ -409,6 +438,10 @@ func (config *V2RegistriesConf) postProcessRegistries() error { return err } + if _, err = ParseProxy(reg.Proxy); err != nil { + return err + } + if reg.Prefix == "" { if reg.Location == "" { return &InvalidRegistries{s: "invalid condition: both location and prefix are unset"} @@ -438,6 +471,10 @@ func (config *V2RegistriesConf) postProcessRegistries() error { return err } + if _, err = ParseProxy(mir.Proxy); err != nil { + return err + } + // FIXME: unqualifiedSearchRegistries now also accepts empty values // and shouldn't // https://github.com/containers/image/pull/1191#discussion_r610623216 @@ -483,6 +520,11 @@ func (config *V2RegistriesConf) postProcessRegistries() error { return &InvalidRegistries{s: msg} } + if reg.Proxy != other.Proxy { + msg := fmt.Sprintf("registry '%s' is defined multiple times with conflicting 'proxy' setting", reg.Location) + return &InvalidRegistries{s: msg} + } + if reg.Blocked != other.Blocked { msg := fmt.Sprintf("registry '%s' is defined multiple times with conflicting 'blocked' setting", reg.Location) return &InvalidRegistries{s: msg} From bc60b0770029633cbf12e3810dbd9d4108aa4849 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Wed, 11 Feb 2026 15:52:12 +0800 Subject: [PATCH 2/8] Registries proxy config: add tests Signed-off-by: cyqsimon <28627918+cyqsimon@users.noreply.github.com> --- image/docker/docker_client_test.go | 46 +++++++++++++++++ .../system_registries_v2_test.go | 50 +++++++++++++++++++ image/pkg/sysregistriesv2/testdata/proxy.conf | 13 +++++ 3 files changed, 109 insertions(+) create mode 100644 image/pkg/sysregistriesv2/testdata/proxy.conf diff --git a/image/docker/docker_client_test.go b/image/docker/docker_client_test.go index 229fea332f..65307b0d96 100644 --- a/image/docker/docker_client_test.go +++ b/image/docker/docker_client_test.go @@ -445,3 +445,49 @@ func TestIsManifestUnknownError(t *testing.T) { assert.True(t, res, "%s: %#v", c.name, err) } } + +// Helper function to test that the selected proxy for a registry matches expected. +func testProxyForRegistry(t *testing.T, ctx context.Context, sys *types.SystemContext, registry string, expectedProxy string) { + t.Run(fmt.Sprintf(`Expecting proxy "%s" for registry "%s"`, expectedProxy, registry), func(t *testing.T) { + req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/v2/", registry), nil) + require.NoError(t, err) + + // Proxy configured using environment variables have priority, so we skip if it's set. + envProxy, _ := http.ProxyFromEnvironment(req) + if envProxy != nil { + t.Skip("Skipping registry proxy test: proxy configured using environment variables") + } + + client, err := newDockerClient(sys, registry, registry) + require.NoError(t, err) + + // Ping will fail, but we only care about the side effect of setting the proxy. + _ = client.detectProperties(ctx) + + transport, ok := client.client.Transport.(*http.Transport) + require.True(t, ok) + require.NotNil(t, transport.Proxy) + + proxyURL, err := transport.Proxy(req) + require.NoError(t, err) + + if expectedProxy == "" { + require.Nil(t, proxyURL) + } else { + require.NotNil(t, proxyURL) + assert.Equal(t, expectedProxy, proxyURL.String()) + } + }) +} + +func TestRegistrySpecificProxy(t *testing.T) { + ctx := context.Background() + sys := &types.SystemContext{ + SystemRegistriesConfPath: "../pkg/sysregistriesv2/testdata/proxy.conf", + SystemRegistriesConfDirPath: "../pkg/sysregistriesv2/testdata/this-does-not-exist", + DockerInsecureSkipTLSVerify: types.OptionalBoolTrue, + } + + testProxyForRegistry(t, ctx, sys, "registry-1.com", "") + testProxyForRegistry(t, ctx, sys, "registry-2.com", "https://proxy-2.example.com") +} diff --git a/image/pkg/sysregistriesv2/system_registries_v2_test.go b/image/pkg/sysregistriesv2/system_registries_v2_test.go index b253714809..1222aa297a 100644 --- a/image/pkg/sysregistriesv2/system_registries_v2_test.go +++ b/image/pkg/sysregistriesv2/system_registries_v2_test.go @@ -107,6 +107,28 @@ func TestParseLocation(t *testing.T) { assert.Equal(t, "example.com:5000/with/path", location) } +func TestParseProxy(t *testing.T) { + for _, valid := range []string{ + "", + "http://proxy.example.com", + "https://proxy.example.com", + "socks5://proxy.example.com", + "socks5h://proxy.example.com:1080", + } { + _, err := ParseProxy(valid) + assert.Nil(t, err, valid) + } + + for _, invalid := range []string{ + "no-scheme.example.com", + "ftp://bad-scheme.example.com", + "ssh://bad-scheme.example.com:2222", + } { + _, err := ParseProxy(invalid) + assert.NotNil(t, err) + } +} + func TestEmptyConfig(t *testing.T) { registries, err := GetRegistries(&types.SystemContext{ SystemRegistriesConfPath: "testdata/empty.conf", @@ -983,3 +1005,31 @@ func TestCredentialHelpers(t *testing.T) { require.Equal(t, test.helpers, helpers, "%v", test) } } + +func TestProxyConfiguration(t *testing.T) { + sys := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/proxy.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + } + + registries, err := GetRegistries(sys) + require.NoError(t, err) + require.Equal(t, 2, len(registries)) + + reg1 := registries[0] + assert.Equal(t, "registry-1.com", reg1.Location) + assert.Equal(t, "", reg1.Proxy) + require.Equal(t, 2, len(reg1.Mirrors)) + + mirror1 := reg1.Mirrors[0] + assert.Equal(t, "mirror-1.registry-1.com", mirror1.Location) + assert.Equal(t, "", mirror1.Proxy) + + mirror2 := reg1.Mirrors[1] + assert.Equal(t, "mirror-2.registry-1.com", mirror2.Location) + assert.Equal(t, "http://proxy-1.example.com", mirror2.Proxy) + + reg2 := registries[1] + assert.Equal(t, "registry-2.com", reg2.Location) + assert.Equal(t, "https://proxy-2.example.com", reg2.Proxy) +} diff --git a/image/pkg/sysregistriesv2/testdata/proxy.conf b/image/pkg/sysregistriesv2/testdata/proxy.conf new file mode 100644 index 0000000000..3f02bf080b --- /dev/null +++ b/image/pkg/sysregistriesv2/testdata/proxy.conf @@ -0,0 +1,13 @@ +[[registry]] +location = "registry-1.com" + +[[registry.mirror]] +location = "mirror-1.registry-1.com" + +[[registry.mirror]] +location = "mirror-2.registry-1.com" +proxy = "http://proxy-1.example.com" + +[[registry]] +location = "registry-2.com" +proxy = "https://proxy-2.example.com" From 2baeda7707ef5e8874c9d706b9607512ed2b8795 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:57:33 +0800 Subject: [PATCH 3/8] Refactor ParseProxy Signed-off-by: cyqsimon <28627918+cyqsimon@users.noreply.github.com> --- .../pkg/sysregistriesv2/system_registries_v2.go | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/image/pkg/sysregistriesv2/system_registries_v2.go b/image/pkg/sysregistriesv2/system_registries_v2.go index 849be7c182..1d07f622ae 100644 --- a/image/pkg/sysregistriesv2/system_registries_v2.go +++ b/image/pkg/sysregistriesv2/system_registries_v2.go @@ -351,22 +351,17 @@ func ParseProxy(input string) (*url.URL, error) { return nil, nil } - var hasSupportedScheme bool - for _, scheme := range []string{"http://", "https://", "socks5://", "socks5h://"} { - if strings.HasPrefix(input, scheme) { - hasSupportedScheme = true - break - } - } - if !hasSupportedScheme { - return nil, &InvalidRegistries{s: "invalid proxy: proxy URL must specify one of the supported schemes: http://, https://, socks5://, socks5h://"} - } - parsed, err := url.Parse(input) if err != nil { return nil, fmt.Errorf("parsing proxy URL %q: %w", input, err) } + supportedSchemes := []string{"http", "https", "socks5", "socks5h"} + if !slices.Contains(supportedSchemes, parsed.Scheme) { + msg := fmt.Sprintf(`proxy URL scheme "%s" is not supported. Supported are http, https, socks5, socks5h`, parsed.Scheme) + return nil, &InvalidRegistries{s: msg} + } + return parsed, nil } From 8ace4f7c0704bc4083854dedfe8101510bd79dc1 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:37:57 +0800 Subject: [PATCH 4/8] Improve code comments - Rewrite comments for `registryProxy` to make it more appropriate for its layer - Make comments regarding loading registry config more substantive Signed-off-by: cyqsimon <28627918+cyqsimon@users.noreply.github.com> --- image/docker/docker_client.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/image/docker/docker_client.go b/image/docker/docker_client.go index 9f2f811b39..6b2f63f70a 100644 --- a/image/docker/docker_client.go +++ b/image/docker/docker_client.go @@ -114,9 +114,11 @@ type dockerClient struct { // tlsClientConfig is setup by newDockerClient and will be used and updated // by detectProperties(). Callers can edit tlsClientConfig.InsecureSkipVerify in the meantime. tlsClientConfig *tls.Config - // registryProxy is the proxy URL from the registry configuration, if any. - // It has the lowest priority and can be overridden by either DockerProxyURL or environment variables. - // When pulling, this value could be overwritten by a mirror-specific proxy. See docker_client_src.go. + // registryProxy is the forwarding proxy used for this client, + // read from the registry configuration and set by newDockerClient. + // detectProperties will set the proxy for the HTTP client using registryProxy, + // subject to overrides by DockerProxyURL and DockerProxy. + // Callers can edit this value before detectProperties is called. registryProxy *url.URL // The following members are not set by newDockerClient and must be set by callers if needed. auth types.DockerAuthConfig @@ -266,7 +268,9 @@ func newDockerClient(sys *types.SystemContext, registry, reference string) (*doc return nil, err } - // Fetch and load sysregistriesv2 configurations. + // Apply options from sysregistriesv2 configuration + // - Check if TLS verification shall be skipped (default=false) + // - Set registry-specific proxy reg, err := sysregistriesv2.FindRegistry(sys, reference) if err != nil { return nil, fmt.Errorf("loading registries: %w", err) @@ -277,9 +281,7 @@ func newDockerClient(sys *types.SystemContext, registry, reference string) (*doc if reg.Blocked { return nil, fmt.Errorf("registry %s is blocked in %s or %s", reg.Prefix, sysregistriesv2.ConfigPath(sys), sysregistriesv2.ConfigDirPath(sys)) } - // Check if TLS verification shall be skipped (default=false). skipVerify = reg.Insecure - // Set registry proxy. registryProxy, err = sysregistriesv2.ParseProxy(reg.Proxy) if err != nil { return nil, err From c5c234ea2c9f57d67d9dca460766415832cfed10 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:55:30 +0800 Subject: [PATCH 5/8] Use RFC2606 `*.test` domains for tests Signed-off-by: cyqsimon <28627918+cyqsimon@users.noreply.github.com> --- image/docker/docker_client_test.go | 4 ++-- .../pkg/sysregistriesv2/system_registries_v2_test.go | 12 ++++++------ image/pkg/sysregistriesv2/testdata/proxy.conf | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/image/docker/docker_client_test.go b/image/docker/docker_client_test.go index 65307b0d96..bf4a64c3e5 100644 --- a/image/docker/docker_client_test.go +++ b/image/docker/docker_client_test.go @@ -488,6 +488,6 @@ func TestRegistrySpecificProxy(t *testing.T) { DockerInsecureSkipTLSVerify: types.OptionalBoolTrue, } - testProxyForRegistry(t, ctx, sys, "registry-1.com", "") - testProxyForRegistry(t, ctx, sys, "registry-2.com", "https://proxy-2.example.com") + testProxyForRegistry(t, ctx, sys, "registry-1.test", "") + testProxyForRegistry(t, ctx, sys, "registry-2.test", "https://proxy-2.example.test") } diff --git a/image/pkg/sysregistriesv2/system_registries_v2_test.go b/image/pkg/sysregistriesv2/system_registries_v2_test.go index 1222aa297a..5bc4793844 100644 --- a/image/pkg/sysregistriesv2/system_registries_v2_test.go +++ b/image/pkg/sysregistriesv2/system_registries_v2_test.go @@ -1017,19 +1017,19 @@ func TestProxyConfiguration(t *testing.T) { require.Equal(t, 2, len(registries)) reg1 := registries[0] - assert.Equal(t, "registry-1.com", reg1.Location) + assert.Equal(t, "registry-1.test", reg1.Location) assert.Equal(t, "", reg1.Proxy) require.Equal(t, 2, len(reg1.Mirrors)) mirror1 := reg1.Mirrors[0] - assert.Equal(t, "mirror-1.registry-1.com", mirror1.Location) + assert.Equal(t, "mirror-1.registry-1.test", mirror1.Location) assert.Equal(t, "", mirror1.Proxy) mirror2 := reg1.Mirrors[1] - assert.Equal(t, "mirror-2.registry-1.com", mirror2.Location) - assert.Equal(t, "http://proxy-1.example.com", mirror2.Proxy) + assert.Equal(t, "mirror-2.registry-1.test", mirror2.Location) + assert.Equal(t, "http://proxy-1.example.test", mirror2.Proxy) reg2 := registries[1] - assert.Equal(t, "registry-2.com", reg2.Location) - assert.Equal(t, "https://proxy-2.example.com", reg2.Proxy) + assert.Equal(t, "registry-2.test", reg2.Location) + assert.Equal(t, "https://proxy-2.example.test", reg2.Proxy) } diff --git a/image/pkg/sysregistriesv2/testdata/proxy.conf b/image/pkg/sysregistriesv2/testdata/proxy.conf index 3f02bf080b..bde7375632 100644 --- a/image/pkg/sysregistriesv2/testdata/proxy.conf +++ b/image/pkg/sysregistriesv2/testdata/proxy.conf @@ -1,13 +1,13 @@ [[registry]] -location = "registry-1.com" +location = "registry-1.test" [[registry.mirror]] -location = "mirror-1.registry-1.com" +location = "mirror-1.registry-1.test" [[registry.mirror]] -location = "mirror-2.registry-1.com" -proxy = "http://proxy-1.example.com" +location = "mirror-2.registry-1.test" +proxy = "http://proxy-1.example.test" [[registry]] -location = "registry-2.com" -proxy = "https://proxy-2.example.com" +location = "registry-2.test" +proxy = "https://proxy-2.example.test" From 28a6cf0611cb67604187e2ca686222f3d438a498 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:33:58 +0800 Subject: [PATCH 6/8] Parse proxy URL during normalization; make `ParseProxy` private Signed-off-by: cyqsimon <28627918+cyqsimon@users.noreply.github.com> --- image/docker/docker_client.go | 5 +--- image/docker/docker_image_src.go | 6 +--- .../sysregistriesv2/system_registries_v2.go | 28 +++++++++++-------- .../system_registries_v2_test.go | 28 ++++++++++--------- 4 files changed, 34 insertions(+), 33 deletions(-) diff --git a/image/docker/docker_client.go b/image/docker/docker_client.go index 6b2f63f70a..d77eaff911 100644 --- a/image/docker/docker_client.go +++ b/image/docker/docker_client.go @@ -282,10 +282,7 @@ func newDockerClient(sys *types.SystemContext, registry, reference string) (*doc return nil, fmt.Errorf("registry %s is blocked in %s or %s", reg.Prefix, sysregistriesv2.ConfigPath(sys), sysregistriesv2.ConfigDirPath(sys)) } skipVerify = reg.Insecure - registryProxy, err = sysregistriesv2.ParseProxy(reg.Proxy) - if err != nil { - return nil, err - } + registryProxy = reg.Proxy } tlsClientConfig.InsecureSkipVerify = skipVerify diff --git a/image/docker/docker_image_src.go b/image/docker/docker_image_src.go index 3ba69d39c4..5e6cc80248 100644 --- a/image/docker/docker_image_src.go +++ b/image/docker/docker_image_src.go @@ -150,11 +150,7 @@ func newImageSourceAttempt(ctx context.Context, sys *types.SystemContext, logica return nil, err } client.tlsClientConfig.InsecureSkipVerify = pullSource.Endpoint.Insecure - registryProxy, err := sysregistriesv2.ParseProxy(pullSource.Endpoint.Proxy) - if err != nil { - return nil, err - } - client.registryProxy = registryProxy + client.registryProxy = pullSource.Endpoint.Proxy s := &dockerImageSource{ PropertyMethodsInitialize: impl.PropertyMethods(impl.Properties{ diff --git a/image/pkg/sysregistriesv2/system_registries_v2.go b/image/pkg/sysregistriesv2/system_registries_v2.go index 1d07f622ae..4fcae7e125 100644 --- a/image/pkg/sysregistriesv2/system_registries_v2.go +++ b/image/pkg/sysregistriesv2/system_registries_v2.go @@ -61,7 +61,11 @@ type Endpoint struct { // connections will be allowed. Insecure bool `toml:"insecure,omitempty"` // The forwarding proxy to be used for accessing this endpoint. - Proxy string `toml:"proxy,omitempty"` + // postProcessRegistries normalizes this field into the public Proxy field. + ProxyRaw string `toml:"proxy,omitempty"` + // The forwarding proxy to be used for accessing this endpoint. + // Parsed from ProxyRaw after normalization. + Proxy *url.URL `toml:"-"` // PullFromMirror is used for adding restrictions to image pull through the mirror. // Set to "all", "digest-only", or "tag-only". // If "digest-only", mirrors will only be used for digest pulls. Pulling images by @@ -344,9 +348,9 @@ func parseLocation(input string) (string, error) { return trimmed, nil } -// ParseProxy parses the input string for a proxy configuration. +// parseProxy parses the input string for a proxy configuration. // Errors if a scheme is unsupported or unspecified, or if the input is not a valid URL. -func ParseProxy(input string) (*url.URL, error) { +func parseProxy(input string) (*url.URL, error) { if input == "" { return nil, nil } @@ -433,10 +437,6 @@ func (config *V2RegistriesConf) postProcessRegistries() error { return err } - if _, err = ParseProxy(reg.Proxy); err != nil { - return err - } - if reg.Prefix == "" { if reg.Location == "" { return &InvalidRegistries{s: "invalid condition: both location and prefix are unset"} @@ -454,6 +454,11 @@ func (config *V2RegistriesConf) postProcessRegistries() error { } } + reg.Proxy, err = parseProxy(reg.ProxyRaw) + if err != nil { + return err + } + // validate the mirror usage settings does not apply to primary registry if reg.PullFromMirror != "" { return fmt.Errorf("pull-from-mirror must not be set for a non-mirror registry %q", reg.Prefix) @@ -466,10 +471,6 @@ func (config *V2RegistriesConf) postProcessRegistries() error { return err } - if _, err = ParseProxy(mir.Proxy); err != nil { - return err - } - // FIXME: unqualifiedSearchRegistries now also accepts empty values // and shouldn't // https://github.com/containers/image/pull/1191#discussion_r610623216 @@ -477,6 +478,11 @@ func (config *V2RegistriesConf) postProcessRegistries() error { return &InvalidRegistries{s: "invalid condition: mirror location is unset"} } + mir.Proxy, err = parseProxy(mir.ProxyRaw) + if err != nil { + return err + } + if reg.MirrorByDigestOnly && mir.PullFromMirror != "" { return &InvalidRegistries{s: fmt.Sprintf("cannot set mirror usage mirror-by-digest-only for the registry (%q) and pull-from-mirror for per-mirror (%q) at the same time", reg.Prefix, mir.Location)} } diff --git a/image/pkg/sysregistriesv2/system_registries_v2_test.go b/image/pkg/sysregistriesv2/system_registries_v2_test.go index 5bc4793844..3d6071fbcc 100644 --- a/image/pkg/sysregistriesv2/system_registries_v2_test.go +++ b/image/pkg/sysregistriesv2/system_registries_v2_test.go @@ -115,7 +115,7 @@ func TestParseProxy(t *testing.T) { "socks5://proxy.example.com", "socks5h://proxy.example.com:1080", } { - _, err := ParseProxy(valid) + _, err := parseProxy(valid) assert.Nil(t, err, valid) } @@ -124,7 +124,7 @@ func TestParseProxy(t *testing.T) { "ftp://bad-scheme.example.com", "ssh://bad-scheme.example.com:2222", } { - _, err := ParseProxy(invalid) + _, err := parseProxy(invalid) assert.NotNil(t, err) } } @@ -1007,29 +1007,31 @@ func TestCredentialHelpers(t *testing.T) { } func TestProxyConfiguration(t *testing.T) { - sys := &types.SystemContext{ + ctx := &types.SystemContext{ SystemRegistriesConfPath: "testdata/proxy.conf", SystemRegistriesConfDirPath: "testdata/this-does-not-exist", } - registries, err := GetRegistries(sys) + InvalidateCache() + _, err := TryUpdatingCache(ctx) require.NoError(t, err) - require.Equal(t, 2, len(registries)) - reg1 := registries[0] - assert.Equal(t, "registry-1.test", reg1.Location) - assert.Equal(t, "", reg1.Proxy) + reg1, err := FindRegistry(ctx, "registry-1.test") + require.NoError(t, err) + require.Nil(t, reg1.Proxy) require.Equal(t, 2, len(reg1.Mirrors)) mirror1 := reg1.Mirrors[0] assert.Equal(t, "mirror-1.registry-1.test", mirror1.Location) - assert.Equal(t, "", mirror1.Proxy) + require.Nil(t, mirror1.Proxy) mirror2 := reg1.Mirrors[1] assert.Equal(t, "mirror-2.registry-1.test", mirror2.Location) - assert.Equal(t, "http://proxy-1.example.test", mirror2.Proxy) + require.NotNil(t, mirror2.Proxy) + assert.Equal(t, "http://proxy-1.example.test", mirror2.Proxy.String()) - reg2 := registries[1] - assert.Equal(t, "registry-2.test", reg2.Location) - assert.Equal(t, "https://proxy-2.example.test", reg2.Proxy) + reg2, err := FindRegistry(ctx, "registry-2.test") + require.NoError(t, err) + require.NotNil(t, reg2.Proxy) + assert.Equal(t, "https://proxy-2.example.test", reg2.Proxy.String()) } From d606a578bfa5a83956a663b326595df95bb0ab22 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:26:43 +0800 Subject: [PATCH 7/8] Inline `testProxyForRegistry` in favour of a table-driven test Signed-off-by: cyqsimon <28627918+cyqsimon@users.noreply.github.com> --- image/docker/docker_client_test.go | 75 ++++++++++++++++-------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/image/docker/docker_client_test.go b/image/docker/docker_client_test.go index bf4a64c3e5..b5061ec07a 100644 --- a/image/docker/docker_client_test.go +++ b/image/docker/docker_client_test.go @@ -446,40 +446,6 @@ func TestIsManifestUnknownError(t *testing.T) { } } -// Helper function to test that the selected proxy for a registry matches expected. -func testProxyForRegistry(t *testing.T, ctx context.Context, sys *types.SystemContext, registry string, expectedProxy string) { - t.Run(fmt.Sprintf(`Expecting proxy "%s" for registry "%s"`, expectedProxy, registry), func(t *testing.T) { - req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/v2/", registry), nil) - require.NoError(t, err) - - // Proxy configured using environment variables have priority, so we skip if it's set. - envProxy, _ := http.ProxyFromEnvironment(req) - if envProxy != nil { - t.Skip("Skipping registry proxy test: proxy configured using environment variables") - } - - client, err := newDockerClient(sys, registry, registry) - require.NoError(t, err) - - // Ping will fail, but we only care about the side effect of setting the proxy. - _ = client.detectProperties(ctx) - - transport, ok := client.client.Transport.(*http.Transport) - require.True(t, ok) - require.NotNil(t, transport.Proxy) - - proxyURL, err := transport.Proxy(req) - require.NoError(t, err) - - if expectedProxy == "" { - require.Nil(t, proxyURL) - } else { - require.NotNil(t, proxyURL) - assert.Equal(t, expectedProxy, proxyURL.String()) - } - }) -} - func TestRegistrySpecificProxy(t *testing.T) { ctx := context.Background() sys := &types.SystemContext{ @@ -488,6 +454,43 @@ func TestRegistrySpecificProxy(t *testing.T) { DockerInsecureSkipTLSVerify: types.OptionalBoolTrue, } - testProxyForRegistry(t, ctx, sys, "registry-1.test", "") - testProxyForRegistry(t, ctx, sys, "registry-2.test", "https://proxy-2.example.test") + var cases = []struct { + registry string + expectedProxy string + }{ + {"registry-1.test", ""}, + {"registry-2.test", "https://proxy-2.example.test"}, + } + for _, c := range cases { + t.Run(fmt.Sprintf(`Expecting proxy "%s" for registry "%s"`, c.expectedProxy, c.registry), func(t *testing.T) { + req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/v2/", c.registry), nil) + require.NoError(t, err) + + // Proxy configured using environment variables have priority, so we skip if it's set. + envProxy, _ := http.ProxyFromEnvironment(req) + if envProxy != nil { + t.Skip("Skipping registry proxy test: proxy configured using environment variables") + } + + client, err := newDockerClient(sys, c.registry, c.registry) + require.NoError(t, err) + + // Ping will fail, but we only care about the side effect of setting the proxy. + _ = client.detectProperties(ctx) + + transport, ok := client.client.Transport.(*http.Transport) + require.True(t, ok) + require.NotNil(t, transport.Proxy) + + proxyURL, err := transport.Proxy(req) + require.NoError(t, err) + + if c.expectedProxy == "" { + require.Nil(t, proxyURL) + } else { + require.NotNil(t, proxyURL) + assert.Equal(t, c.expectedProxy, proxyURL.String()) + } + }) + } } From 5f58f1610cbf7431e4cf64f1a03c403525e07bc3 Mon Sep 17 00:00:00 2001 From: cyqsimon <28627918+cyqsimon@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:48:58 +0800 Subject: [PATCH 8/8] Make registry-specific proxy takes precedence over proxy env vars Because it has a narrower scope than the globally scoped env vars. Signed-off-by: cyqsimon <28627918+cyqsimon@users.noreply.github.com> --- image/docker/docker_client.go | 10 +++------- image/docker/docker_client_test.go | 12 +++--------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/image/docker/docker_client.go b/image/docker/docker_client.go index d77eaff911..b30106b880 100644 --- a/image/docker/docker_client.go +++ b/image/docker/docker_client.go @@ -978,14 +978,10 @@ func (c *dockerClient) detectPropertiesHelper(ctx context.Context) error { } tr := tlsclientconfig.NewTransport() tr.TLSClientConfig = c.tlsClientConfig - // Set registry-specific proxy with lowest priority, which can be overridden by environment variables. + // Set registry-specific proxy. + // This has a narrower scope so should take precedence over globally-scoped environment variables. if c.registryProxy != nil { - tr.Proxy = func(req *http.Request) (*url.URL, error) { - if envProxy, err := http.ProxyFromEnvironment(req); err != nil || envProxy != nil { - return envProxy, err - } - return c.registryProxy, nil - } + tr.Proxy = http.ProxyURL(c.registryProxy) } // if set DockerProxyURL explicitly, use the DockerProxyURL instead of system proxy if c.sys != nil && c.sys.DockerProxyURL != nil { diff --git a/image/docker/docker_client_test.go b/image/docker/docker_client_test.go index b5061ec07a..a42d0e2257 100644 --- a/image/docker/docker_client_test.go +++ b/image/docker/docker_client_test.go @@ -463,15 +463,6 @@ func TestRegistrySpecificProxy(t *testing.T) { } for _, c := range cases { t.Run(fmt.Sprintf(`Expecting proxy "%s" for registry "%s"`, c.expectedProxy, c.registry), func(t *testing.T) { - req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/v2/", c.registry), nil) - require.NoError(t, err) - - // Proxy configured using environment variables have priority, so we skip if it's set. - envProxy, _ := http.ProxyFromEnvironment(req) - if envProxy != nil { - t.Skip("Skipping registry proxy test: proxy configured using environment variables") - } - client, err := newDockerClient(sys, c.registry, c.registry) require.NoError(t, err) @@ -482,6 +473,9 @@ func TestRegistrySpecificProxy(t *testing.T) { require.True(t, ok) require.NotNil(t, transport.Proxy) + req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/v2/", c.registry), nil) + require.NoError(t, err) + proxyURL, err := transport.Proxy(req) require.NoError(t, err)