Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions image/docker/docker_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ 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 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
registryToken string
Expand Down Expand Up @@ -262,18 +268,21 @@ 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
// 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)
}
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))
}
skipVerify = reg.Insecure
registryProxy = reg.Proxy
}
tlsClientConfig.InsecureSkipVerify = skipVerify

Expand All @@ -287,6 +296,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
Expand Down Expand Up @@ -968,6 +978,11 @@ func (c *dockerClient) detectPropertiesHelper(ctx context.Context) error {
}
tr := tlsclientconfig.NewTransport()
tr.TLSClientConfig = c.tlsClientConfig
// Set registry-specific proxy.
// This has a narrower scope so should take precedence over globally-scoped environment variables.
if 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 {
tr.Proxy = http.ProxyURL(c.sys.DockerProxyURL)
Expand Down
43 changes: 43 additions & 0 deletions image/docker/docker_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,3 +445,46 @@ func TestIsManifestUnknownError(t *testing.T) {
assert.True(t, res, "%s: %#v", c.name, err)
}
}

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,
}

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) {
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)

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)

if c.expectedProxy == "" {
require.Nil(t, proxyURL)
} else {
require.NotNil(t, proxyURL)
assert.Equal(t, c.expectedProxy, proxyURL.String())
}
})
}
}
1 change: 1 addition & 0 deletions image/docker/docker_image_src.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ func newImageSourceAttempt(ctx context.Context, sys *types.SystemContext, logica
return nil, err
}
client.tlsClientConfig.InsecureSkipVerify = pullSource.Endpoint.Insecure
client.registryProxy = pullSource.Endpoint.Proxy

s := &dockerImageSource{
PropertyMethodsInitialize: impl.PropertyMethods(impl.Properties{
Expand Down
15 changes: 13 additions & 2 deletions image/docs/containers-registries.conf.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ By default, container runtimes require TLS when retrieving images from a registr
If `insecure` is set to `true`, unencrypted HTTP as well as TLS connections with untrusted
certificates are allowed.

`proxy`
: Sets the forwarding proxy to be used specifically for connections to this registry.
This setting takes precedence over globally-scoped proxies set using environment variables.
Accepts a URL to the proxy in the format of _scheme_`://`_host_[`:`_port_][`/`_path_]
where _scheme_ is one of `http`, `https`, `socks5`, or `socks5h`. See CURLOPT_PROXY(3).
Note that both `socks5` and `socks5h` behave like `socks5h` in curl,
i.e. name resolution always happens remotely.

`blocked`
: `true` or `false`.
If `true`, pulling images with matching names is forbidden.
Expand Down Expand Up @@ -94,14 +102,14 @@ With a `prefix` containing a wildcard in the format: "*.example.com" for subdoma
the location can be empty. In such a case,
prefix matching will occur, but no reference rewrite will occur. The
original requested image string will be used as-is. But other settings like
`insecure` / `blocked` / `mirrors` will be applied to matching images.
`insecure` / `proxy` / `blocked` / `mirrors` will be applied to matching images.

Example: Given
```
prefix = "*.example.com"
```
requests for the image `blah.example.com/foo/myimage:latest` will be used
as-is. But other settings like insecure/blocked/mirrors will be applied to matching images
as-is. But other settings like insecure/proxy/blocked/mirrors will be applied to matching images

`mirror`
: An array of TOML tables specifying (possibly-partial) mirrors for the
Expand All @@ -117,6 +125,8 @@ Each TOML table in the `mirror` array can contain the following fields:
as specified in the `[[registry]]` TOML table
- `insecure`: same semantics
as specified in the `[[registry]]` TOML table
- `proxy`: same semantics
as specified in the `[[registry]]` TOML table
- `pull-from-mirror`: `all`, `digest-only` or `tag-only`. If "digest-only", mirrors will only be used for digest pulls. Pulling images by tag can potentially yield different images, depending on which endpoint we pull from. Restricting mirrors to pulls by digest avoids that issue. If "tag-only", mirrors will only be used for tag pulls. For a more up-to-date and expensive mirror that it is less likely to be out of sync if tags move, it should not be unnecessarily used for digest references. Default is "all" (or left empty), mirrors will be used for both digest pulls and tag pulls unless the mirror-by-digest-only is set for the primary registry.
Note that this per-mirror setting is allowed only when `mirror-by-digest-only` is not configured for the primary registry.

Expand Down Expand Up @@ -239,6 +249,7 @@ location = "internal-registry-for-example.com/bar"

[[registry.mirror]]
location = "example-mirror-0.local/mirror-for-foo"
proxy = "http://proxy.example.com:8000"

[[registry.mirror]]
location = "example-mirror-1.local/mirrors/foo"
Expand Down
43 changes: 43 additions & 0 deletions image/pkg/sysregistriesv2/system_registries_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io/fs"
"maps"
"net/url"
"os"
"path/filepath"
"reflect"
Expand Down Expand Up @@ -59,6 +60,12 @@ 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.
// 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
Expand Down Expand Up @@ -341,6 +348,27 @@ 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
}

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
}

// ConvertToV2 returns a v2 config corresponding to a v1 one.
func (config *V1RegistriesConf) ConvertToV2() (*V2RegistriesConf, error) {
regMap := make(map[string]*Registry)
Expand Down Expand Up @@ -426,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)
Expand All @@ -445,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)}
}
Expand Down Expand Up @@ -483,6 +521,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}
Expand Down
52 changes: 52 additions & 0 deletions image/pkg/sysregistriesv2/system_registries_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -983,3 +1005,33 @@ func TestCredentialHelpers(t *testing.T) {
require.Equal(t, test.helpers, helpers, "%v", test)
}
}

func TestProxyConfiguration(t *testing.T) {
ctx := &types.SystemContext{
SystemRegistriesConfPath: "testdata/proxy.conf",
SystemRegistriesConfDirPath: "testdata/this-does-not-exist",
}

InvalidateCache()
_, err := TryUpdatingCache(ctx)
require.NoError(t, err)

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)
require.Nil(t, mirror1.Proxy)

mirror2 := reg1.Mirrors[1]
assert.Equal(t, "mirror-2.registry-1.test", mirror2.Location)
require.NotNil(t, mirror2.Proxy)
assert.Equal(t, "http://proxy-1.example.test", mirror2.Proxy.String())

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())
}
13 changes: 13 additions & 0 deletions image/pkg/sysregistriesv2/testdata/proxy.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[[registry]]
location = "registry-1.test"

[[registry.mirror]]
location = "mirror-1.registry-1.test"

[[registry.mirror]]
location = "mirror-2.registry-1.test"
proxy = "http://proxy-1.example.test"

[[registry]]
location = "registry-2.test"
proxy = "https://proxy-2.example.test"