Merge pull request #29475 from Luap99/v6.1

[v6.1] bump common and image and backport WSL port forwarding fix
This commit is contained in:
Matt Heon 2026-08-12 12:59:47 -04:00 committed by GitHub
commit 80bd5deabf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 459 additions and 84 deletions

4
go.mod
View file

@ -65,8 +65,8 @@ require (
github.com/vishvananda/netlink v1.3.1
go.etcd.io/bbolt v1.5.0
go.podman.io/buildah v1.45.0
go.podman.io/common v0.69.0
go.podman.io/image/v5 v5.41.0
go.podman.io/common v0.69.1
go.podman.io/image/v5 v5.41.1
go.podman.io/storage v1.64.0
golang.org/x/crypto v0.54.0
golang.org/x/net v0.57.0

8
go.sum
View file

@ -435,10 +435,10 @@ go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.podman.io/buildah v1.45.0 h1:/LAS2Sgf+xmRoLVo5j52HjEPL+g5HKjomnCvCk42iAU=
go.podman.io/buildah v1.45.0/go.mod h1:cTVrqG7Hu24euHntdC31/sU2nvqPoLmHmpkz+JIl2Lw=
go.podman.io/common v0.69.0 h1:tAM8gJRCgoPmyfXnYP+6VRfOQ6JySgafpY28OhDZpWc=
go.podman.io/common v0.69.0/go.mod h1:g1iVXP+Xn/8He+96qpJw75pu054FRDQMiDGf2VKKspg=
go.podman.io/image/v5 v5.41.0 h1:xOan4jlwbT4R5qhe3ruDAGJLnOC2z0eR61WhZjHbe4E=
go.podman.io/image/v5 v5.41.0/go.mod h1:wfgAlfczPK4ZZw/wn/Av2iJfb3z9Vv+RKWgjHNShzZk=
go.podman.io/common v0.69.1 h1:hTIv8wzHJfHAMMZRocjv1849TiWrf5HDhH0eD74qJqY=
go.podman.io/common v0.69.1/go.mod h1:g1iVXP+Xn/8He+96qpJw75pu054FRDQMiDGf2VKKspg=
go.podman.io/image/v5 v5.41.1 h1:iPrhIt7/aNfRBMuzoxbMUVpQmR7Q75ahnrzkIC4JWLQ=
go.podman.io/image/v5 v5.41.1/go.mod h1:wfgAlfczPK4ZZw/wn/Av2iJfb3z9Vv+RKWgjHNShzZk=
go.podman.io/storage v1.64.0 h1:ryHCZO+Zl0ZG7OTHF+Qc4C0zLNuoe9W35zgE7mh2pYw=
go.podman.io/storage v1.64.0/go.mod h1:ft0DzCRMZs1yn0rszmLYXEEwgd7IIc66JRwSRPUDwwc=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=

View file

@ -28,7 +28,7 @@ func (c *Container) bindPorts() ([]*os.File, error) {
if !c.runtime.config.Engine.EnablePortReservation || rootless.IsRootless() || !c.config.NetMode.IsBridge() {
return nil, nil
}
return bindPorts(c.convertPortMappings())
return bindPorts(c.convertPortMappings(), c.runtime.config.Engine.ForcePortListen)
}
// convertPortMappings will remove the HostIP part from the ports when running inside podman machine.

View file

@ -31,7 +31,7 @@ type ociError struct {
}
// Bind ports to keep them closed on the host
func bindPorts(ports []types.PortMapping) ([]*os.File, error) {
func bindPorts(ports []types.PortMapping, forceListen bool) ([]*os.File, error) {
var files []*os.File
sctpWarning := true
for _, port := range ports {
@ -42,7 +42,7 @@ func bindPorts(ports []types.PortMapping) ([]*os.File, error) {
protocols := strings.SplitSeq(port.Protocol, ",")
for protocol := range protocols {
for i := uint16(0); i < port.Range; i++ {
f, err := bindPort(protocol, port.HostIP, port.HostPort+i, isV6, &sctpWarning)
f, err := bindPort(protocol, port.HostIP, port.HostPort+i, isV6, &sctpWarning, forceListen)
if err != nil {
// close all open ports in case of early error so we do not
// rely on the garbage collector to close them
@ -60,9 +60,9 @@ func bindPorts(ports []types.PortMapping) ([]*os.File, error) {
return files, nil
}
// bindPort reserves a port on the host using socket+bind without listen.
// bindPort reserves a port on the host using socket+bind, listen() only when listen is set to true
// Dual-stack bind by default unless hostIP is specified.
func bindPort(protocol, hostIP string, port uint16, isV6 bool, sctpWarning *bool) (*os.File, error) {
func bindPort(protocol, hostIP string, port uint16, isV6 bool, sctpWarning *bool, forceListen bool) (*os.File, error) {
switch protocol {
case "tcp", "udp":
sockType := unix.SOCK_STREAM
@ -94,6 +94,13 @@ func bindPort(protocol, hostIP string, port uint16, isV6 bool, sctpWarning *bool
return nil, fmt.Errorf("cannot bind %s port %s: %w", protocol, net.JoinHostPort(hostIP, strconv.FormatUint(uint64(port), 10)), err)
}
if forceListen && sockType == unix.SOCK_STREAM {
if err := unix.Listen(fd, 0); err != nil {
unix.Close(fd)
return nil, fmt.Errorf("cannot listen on %s port %s: %w", protocol, net.JoinHostPort(hostIP, strconv.FormatUint(uint64(port), 10)), err)
}
}
return os.NewFile(uintptr(fd), fmt.Sprintf("reservation-%s-%d", protocol, port)), nil
case "sctp":

View file

@ -1257,4 +1257,25 @@ EOF
run_podman rm -f -t0 $cid1
}
# bats test_tags=ci:parallel
@test "podman run - test force_port_listen containers.conf option" {
skip_if_rootless "force_port_listen is only used as root"
skip_if_remote "force_port_listen would need to be set on the server side"
myport=$(random_free_port)
cname="c1-$(safename)"
containersconf=$PODMAN_TMPDIR/containers.conf
cat >$containersconf <<EOF
[engine]
force_port_listen = true
EOF
CONTAINERS_CONF_OVERRIDE=$containersconf run_podman run -d --name $cname -p $myport:8080 $IMAGE sleep inf
run -0 ss -Hlpn sport = $myport
assert "$output" =~ "tcp[[:blank:]]LISTEN.*\*:$myport" "tcp port should be in LISTEN state"
run_podman rm -f -t0 $cname
}
# vim: filetype=sh

View file

@ -300,9 +300,12 @@ type EngineConfig struct {
// host when they are forwarded to containers. When enabled, when ports are
// forwarded to containers, they are held open by conmon as long as the
// container is running, ensuring that they cannot be reused by other
// programs on the host. However, this can cause significant memory usage if
// a container has many ports forwarded to it. Disabling this can save
// memory.
// programs on the host. However, this can cause increased memory usage
// if a container has many ports forwarded to it. Disabling this can save
// memory but is not recommended as port conflicts are not noticed.
// Note this option is only used for rootful container when ports are
// forwarded via firewall rules. Rootless containers using pasta(1)
// always have to bind each port.
EnablePortReservation bool `toml:"enable_port_reservation,omitempty"`
// Environment variables to be used when running the container engine (e.g., Podman, Buildah). For example "http_proxy=internal.proxy.company.com"
@ -323,6 +326,18 @@ type EngineConfig struct {
// information about the container.
EventsContainerCreateInspectData bool `toml:"events_container_create_inspect_data,omitempty"`
// ForcePortListen forces the port reservation to use listen().
// For rootful containers when reserving the ports Podman will not use listen()
// on TCP ports by default as connections are forwarded via firewall rules.
// This is done to ensure if the firewall rule is bypassed, i.e. a connection
// to "::1", the connection will be correctly refused and not hang forever as we
// never accept() them.
// For applications which only monitor the listening state of a socket it means they
// will not see the bound port then. In that case this option can be set to true in
// order to force Podman to also call listen() on the ports.
// Also see the **enable_port_reservation** option.
ForcePortListen bool `toml:"force_port_listen,omitempty"`
// graphRoot internal stores the location of the graphroot
graphRoot string

View file

@ -529,8 +529,10 @@ default_sysctls = [
# forwarded to containers. When enabled, when ports are forwarded to containers,
# ports are held open by as long as the container is running, ensuring that
# they cannot be reused by other programs on the host. However, this can cause
# significant memory usage if a container has many ports forwarded to it.
# Disabling this can save memory.
# increased memory usage if a container has many ports forwarded to it.
# Disabling this can save memory but is not recommended as port conflicts are not noticed.
# Note this option is only used for rootful container when ports are forwarded via firewall rules.
# Rootless containers using pasta(1) always have to bind each port.
#
#enable_port_reservation = true
@ -566,6 +568,18 @@ default_sysctls = [
# with detailed information about the container.
#events_container_create_inspect_data = false
# For rootful containers when reserving the ports Podman will not use listen()
# on TCP ports by default as connections are forwarded via firewall rules.
# This is done to ensure if the firewall rule is bypassed, i.e. a connection
# to "::1", the connection will be correctly refused and not hang forever as we
# never accept() them.
# For applications which only monitor the listening state of a socket it means they
# will not see the bound port then. In that case this option can be set to true in
# order to force Podman to also call listen() on the ports.
# Also see the enable_port_reservation option.
#
#force_port_listen=false
# Whenever Podman should log healthcheck events.
# With many running healthcheck on short interval Podman will spam the event
# log a lot as it generates a event for each single healthcheck run. Because

View file

@ -372,8 +372,10 @@ default_sysctls = [
# forwarded to containers. When enabled, when ports are forwarded to containers,
# ports are held open by as long as the container is running, ensuring that
# they cannot be reused by other programs on the host. However, this can cause
# significant memory usage if a container has many ports forwarded to it.
# Disabling this can save memory.
# increased memory usage if a container has many ports forwarded to it.
# Disabling this can save memory but is not recommended as port conflicts are not noticed.
# Note this option is only used for rootful container when ports are forwarded via firewall rules.
# Rootless containers using pasta(1) always have to bind each port.
#
#enable_port_reservation = true
@ -405,6 +407,18 @@ default_sysctls = [
#
#events_logger = "file"
# For rootful containers when reserving the ports Podman will not use listen()
# on TCP ports by default as connections are forwarded via firewall rules.
# This is done to ensure if the firewall rule is bypassed, i.e. a connection
# to "::1", the connection will be correctly refused and not hang forever as we
# never accept() them.
# For applications which only monitor the listening state of a socket it means they
# will not see the bound port then. In that case this option can be set to true in
# order to force Podman to also call listen() on the ports.
# Also see the enable_port_reservation option.
#
#force_port_listen=false
# Whenever Podman should log healthcheck events.
# With many running healthcheck on short interval Podman will spam the event
# log a lot as it generates a event for each single healthcheck run. Because

View file

@ -1,4 +1,4 @@
package version
// Version is the version of the build.
const Version = "0.69.0"
const Version = "0.69.1"

View file

@ -8,15 +8,20 @@ import (
"fmt"
"io"
"math"
"math/rand/v2"
"mime"
"mime/multipart"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"strings"
"sync"
"time"
"github.com/docker/distribution/registry/api/errcode"
v2 "github.com/docker/distribution/registry/api/v2"
digest "github.com/opencontainers/go-digest"
"github.com/sirupsen/logrus"
"go.podman.io/image/v5/docker/reference"
@ -48,6 +53,31 @@ type dockerImageSource struct {
// State
cachedManifest []byte // nil if not loaded yet
cachedManifestMIMEType string // Only valid if cachedManifest != nil
// Mirror fallback: when a blob fetch fails with a fallback-worthy error,
// try remaining pull sources before giving up. Protected by mirrorMu.
mirrorMu sync.Mutex
mirrorOverride *mirrorSource // If non-nil, this is the mirror and physicalRef for the override client
prevOverrides []*dockerClient // mirror clients replaced by a newer mirrorOverride; kept open because other goroutines may still be reading blobs through them, closed in Close()
remainingSources []sysregistriesv2.PullSource // later pull sources not yet tried, consumed front-to-back during fallback; nil when selected source is already the original source.
fallbackSys *types.SystemContext
fallbackRegConfig *registryConfiguration
}
type mirrorSource struct {
client *dockerClient
ref dockerReference
}
type pullEndpoint struct {
client *dockerClient
ref dockerReference
endpointSys *types.SystemContext
}
type sourceAttempt struct {
ref reference.Named
err error
}
// newImageSource creates a new ImageSource for the specified image reference.
@ -86,12 +116,8 @@ func newImageSource(ctx context.Context, sys *types.SystemContext, ref dockerRef
if err != nil {
return nil, err
}
type attempt struct {
ref reference.Named
err error
}
attempts := []attempt{}
for _, pullSource := range pullSources {
attempts := []sourceAttempt{}
for i, pullSource := range pullSources {
if sys != nil && sys.DockerLogMirrorChoice {
logrus.Infof("Trying to access %q", pullSource.Reference)
} else {
@ -99,10 +125,15 @@ func newImageSource(ctx context.Context, sys *types.SystemContext, ref dockerRef
}
s, err := newImageSourceAttempt(ctx, sys, ref, pullSource, registryConfig)
if err == nil {
if i+1 < len(pullSources) {
s.remainingSources = pullSources[i+1:]
s.fallbackSys = sys
s.fallbackRegConfig = registryConfig
}
return s, nil
}
logrus.Debugf("Accessing %q failed: %v", pullSource.Reference, err)
attempts = append(attempts, attempt{
attempts = append(attempts, sourceAttempt{
ref: pullSource.Reference,
err: err,
})
@ -131,6 +162,63 @@ func newImageSource(ctx context.Context, sys *types.SystemContext, ref dockerRef
func newImageSourceAttempt(ctx context.Context, sys *types.SystemContext, logicalRef dockerReference, pullSource sysregistriesv2.PullSource,
registryConfig *registryConfiguration,
) (*dockerImageSource, error) {
endpoint, err := newPullClient(sys, logicalRef, pullSource, registryConfig)
if err != nil {
return nil, err
}
s := &dockerImageSource{
PropertyMethodsInitialize: impl.PropertyMethods(impl.Properties{
HasThreadSafeGetBlob: true,
}),
logicalRef: logicalRef,
physicalRef: endpoint.ref,
c: endpoint.client,
}
s.Compat = impl.AddCompat(s)
if err := s.ensureManifestIsLoaded(ctx); err != nil {
endpoint.client.Close()
return nil, err
}
if h, err := sysregistriesv2.AdditionalLayerStoreAuthHelper(endpoint.endpointSys); err == nil && h != "" {
acf := map[string]struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
IdentityToken string `json:"identityToken,omitempty"`
}{
endpoint.ref.ref.String(): {
Username: endpoint.client.auth.Username,
Password: endpoint.client.auth.Password,
IdentityToken: endpoint.client.auth.IdentityToken,
},
}
acfD, err := json.Marshal(acf)
if err != nil {
logrus.Warnf("failed to marshal auth config: %v", err)
} else {
cmd := exec.Command(h)
cmd.Stdin = bytes.NewReader(acfD)
if err := cmd.Run(); err != nil {
var stderr string
if ee, ok := err.(*exec.ExitError); ok {
stderr = string(ee.Stderr)
}
logrus.Warnf("Failed to call additional-layer-store-auth-helper (stderr:%s): %v", stderr, err)
}
}
}
return s, nil
}
// newPullClient creates a dockerClient, reference, and endpoint-specific SystemContext
// for the given pull source.
// The caller must call client.Close() when done.
func newPullClient(sys *types.SystemContext, logicalRef dockerReference, pullSource sysregistriesv2.PullSource,
registryConfig *registryConfiguration,
) (*pullEndpoint, error) {
physicalRef, err := newReference(pullSource.Reference, false)
if err != nil {
return nil, err
@ -152,50 +240,7 @@ func newImageSourceAttempt(ctx context.Context, sys *types.SystemContext, logica
client.tlsClientConfig.InsecureSkipVerify = pullSource.Endpoint.Insecure
client.namespaceProxy = pullSource.Endpoint.NamespaceProxy
s := &dockerImageSource{
PropertyMethodsInitialize: impl.PropertyMethods(impl.Properties{
HasThreadSafeGetBlob: true,
}),
logicalRef: logicalRef,
physicalRef: physicalRef,
c: client,
}
s.Compat = impl.AddCompat(s)
if err := s.ensureManifestIsLoaded(ctx); err != nil {
client.Close()
return nil, err
}
if h, err := sysregistriesv2.AdditionalLayerStoreAuthHelper(endpointSys); err == nil && h != "" {
acf := map[string]struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
IdentityToken string `json:"identityToken,omitempty"`
}{
physicalRef.ref.String(): {
Username: client.auth.Username,
Password: client.auth.Password,
IdentityToken: client.auth.IdentityToken,
},
}
acfD, err := json.Marshal(acf)
if err != nil {
logrus.Warnf("failed to marshal auth config: %v", err)
} else {
cmd := exec.Command(h)
cmd.Stdin = bytes.NewReader(acfD)
if err := cmd.Run(); err != nil {
var stderr string
if ee, ok := err.(*exec.ExitError); ok {
stderr = string(ee.Stderr)
}
logrus.Warnf("Failed to call additional-layer-store-auth-helper (stderr:%s): %v", stderr, err)
}
}
}
return s, nil
return &pullEndpoint{client: client, ref: physicalRef, endpointSys: endpointSys}, nil
}
// Reference returns the reference used to set up this source, _as specified by the user_
@ -206,6 +251,18 @@ func (s *dockerImageSource) Reference() types.ImageReference {
// Close removes resources associated with an initialized ImageSource, if any.
func (s *dockerImageSource) Close() error {
s.mirrorMu.Lock()
prev := s.prevOverrides
override := s.mirrorOverride
s.prevOverrides = nil
s.mirrorOverride = nil
s.mirrorMu.Unlock()
for _, m := range prev {
m.Close()
}
if override != nil {
override.client.Close()
}
return s.c.Close()
}
@ -416,9 +473,88 @@ func (s *dockerImageSource) GetBlobAt(ctx context.Context, info types.BlobInfo,
if err := info.Digest.Validate(); err != nil { // Make sure info.Digest.String() does not contain any unexpected characters
return nil, nil, err
}
path := fmt.Sprintf(blobsPath, reference.Path(s.physicalRef.ref), info.Digest.String())
client, physRef, hasRemaining := s.getActiveSource()
streams, errs, err := tryGetBlobAt(ctx, client, physRef, info, chunks, headers, hasRemaining)
if err == nil || !hasRemaining {
return streams, errs, err
}
if isMirrorTransientError(err) || isMirrorFallbackError(err) {
logrus.Debugf("Partial blob %s fetch from %q failed (%v), trying fallback sources", info.Digest, physRef.ref, err)
return s.getBlobAtWithMirrorFallback(ctx, info, chunks, headers, err, client)
}
return streams, errs, err
}
// GetBlob returns a stream for the specified blob, and the blobs size (or -1 if unknown).
// The Digest field in BlobInfo is guaranteed to be provided, Size may be -1 and MediaType may be optionally provided.
// May update BlobInfoCache, preferably after it knows for certain that a blob truly exists at a specific location.
func (s *dockerImageSource) GetBlob(ctx context.Context, info types.BlobInfo, cache types.BlobInfoCache) (io.ReadCloser, int64, error) {
client, physRef, hasRemaining := s.getActiveSource()
reader, size, err := tryGetBlob(ctx, client, physRef, info, cache, hasRemaining)
if err == nil || !hasRemaining {
return reader, size, err
}
if isMirrorTransientError(err) || isMirrorFallbackError(err) {
logrus.Debugf("Blob %s fetch from %q failed (%v), trying fallback sources", info.Digest, physRef.ref, err)
return s.getBlobWithMirrorFallback(ctx, info, cache, err, client)
}
return reader, size, err
}
func (s *dockerImageSource) getActiveSource() (*dockerClient, dockerReference, bool) {
s.mirrorMu.Lock()
defer s.mirrorMu.Unlock()
hasRemaining := len(s.remainingSources) > 0
if s.mirrorOverride != nil {
return s.mirrorOverride.client, s.mirrorOverride.ref, hasRemaining
}
return s.c, s.physicalRef, hasRemaining
}
// tryGetBlob attempts to fetch a blob, retrying once on transient errors if fallback mirrors remain.
func tryGetBlob(ctx context.Context, client *dockerClient, physRef dockerReference,
info types.BlobInfo, cache types.BlobInfoCache, hasRemainingSources bool,
) (io.ReadCloser, int64, error) {
reader, size, err := client.getBlob(ctx, physRef, info, cache)
if err != nil && hasRemainingSources && isMirrorTransientError(err) {
logrus.Debugf("Transient error fetching blob %s from %q, retrying: %v", info.Digest, physRef.ref, err)
delay := time.Second + rand.N(time.Second/10) // same as retry.IfNecessary first attempt: 1s + 10% jitter
select {
case <-ctx.Done():
return nil, 0, fmt.Errorf("%w (while retrying after: %v)", ctx.Err(), err)
case <-time.After(delay):
}
reader, size, err = client.getBlob(ctx, physRef, info, cache)
}
return reader, size, err
}
// tryGetBlobAt attempts to fetch a partial blob (range request), retrying once on transient errors
// if fallback mirrors remain. Mirrors tryGetBlob for the partial-pull (zstd:chunked) path.
func tryGetBlobAt(ctx context.Context, client *dockerClient, physRef dockerReference,
info types.BlobInfo, chunks []private.ImageSourceChunk, headers map[string][]string, hasRemainingSources bool,
) (chan io.ReadCloser, chan error, error) {
streams, errs, err := fetchBlobAt(ctx, client, physRef, info, chunks, headers)
if err != nil && hasRemainingSources && isMirrorTransientError(err) {
logrus.Debugf("Transient error fetching partial blob %s from %q, retrying: %v", info.Digest, physRef.ref, err)
delay := time.Second + rand.N(time.Second/10)
select {
case <-ctx.Done():
return nil, nil, fmt.Errorf("%w (while retrying after: %v)", ctx.Err(), err)
case <-time.After(delay):
}
streams, errs, err = fetchBlobAt(ctx, client, physRef, info, chunks, headers)
}
return streams, errs, err
}
func fetchBlobAt(ctx context.Context, client *dockerClient, physRef dockerReference,
info types.BlobInfo, chunks []private.ImageSourceChunk, headers map[string][]string,
) (chan io.ReadCloser, chan error, error) {
path := fmt.Sprintf(blobsPath, reference.Path(physRef.ref), info.Digest.String())
logrus.Debugf("Downloading %s", path)
res, err := s.c.makeRequest(ctx, http.MethodGet, path, headers, nil, v2Auth, nil)
res, err := client.makeRequest(ctx, http.MethodGet, path, headers, nil, v2Auth, nil)
if err != nil {
return nil, nil, err
}
@ -436,27 +572,195 @@ func (s *dockerImageSource) GetBlobAt(ctx context.Context, info types.BlobInfo,
if err != nil {
return nil, nil, err
}
streams := make(chan io.ReadCloser)
errs := make(chan error)
go handle206Response(streams, errs, res.Body, chunks, mediaType, params)
return streams, errs, nil
case http.StatusBadRequest:
res.Body.Close()
return nil, nil, private.BadPartialRequestError{Status: res.Status}
default:
err := registryHTTPResponseToError(res)
httpErr := registryHTTPResponseToError(res)
res.Body.Close()
return nil, nil, fmt.Errorf("fetching partial blob: %w", err)
return nil, nil, fmt.Errorf("fetching partial blob: %w", httpErr)
}
}
// GetBlob returns a stream for the specified blob, and the blobs size (or -1 if unknown).
// The Digest field in BlobInfo is guaranteed to be provided, Size may be -1 and MediaType may be optionally provided.
// May update BlobInfoCache, preferably after it knows for certain that a blob truly exists at a specific location.
func (s *dockerImageSource) GetBlob(ctx context.Context, info types.BlobInfo, cache types.BlobInfoCache) (io.ReadCloser, int64, error) {
return s.c.getBlob(ctx, s.physicalRef, info, cache)
// isMirrorFallbackError returns true for errors where the blob is not present
// on this source but may be available from another — warranting a fallback attempt.
// All tested registries (docker.io, registry.redhat.io, quay.io, registry.access.redhat.com)
// return BLOB_UNKNOWN for blob 404s, matching the OCI distribution spec.
func isMirrorFallbackError(err error) bool {
var ec errcode.ErrorCoder
if errors.As(err, &ec) {
switch ec.ErrorCode() {
case v2.ErrorCodeBlobUnknown:
// Blob endpoint returns HTTP 404 with errcode JSON body
// {"errors":[{"code":"BLOB_UNKNOWN",...}]}.
// registryHTTPResponseToError calls handleErrorResponse → parseHTTPErrorResponse,
// then unwraps errcode.Errors to the first errcode.Error.
return true
case errcode.ErrorCodeTooManyRequests:
// makeRequestToResolvedURL already retried 5 times with exponential
// backoff. This source's rate limit is exhausted — skip straight to
// the next mirror which has its own rate limit budget.
return true
}
}
// UnexpectedHTTPStatusError (capital U) is NOT matched here — for regular blobs,
// handleErrorResponse never produces it for 4xx (only for 5xx). For external blobs,
// getExternalBlob() produces it via newUnexpectedHTTPStatusError() directly for any
// non-200 including 404, but external URLs are fixed and mirror-independent —
// a different registry mirror cannot serve them.
return false
}
// isMirrorTransientError returns true for errors that are transient; the source
// probably has the blob but temporarily cannot serve it.
func isMirrorTransientError(err error) bool {
// HTTP 5xx: handleErrorResponse returns UnexpectedHTTPStatusError for status
// codes outside 400499. Server-side error, another mirror may succeed.
var httpErr UnexpectedHTTPStatusError
if errors.As(err, &httpErr) && httpErr.StatusCode >= 500 {
return true
}
// Network timeout: makeRequest returns net.Error with Timeout() == true.
// The mirror is reachable but slow — worth retrying on another.
var netErr net.Error
return errors.As(err, &netErr) && netErr.Timeout()
}
// retireStaleOverride checks if the current mirrorOverride was set by another
// goroutine and differs from failedClient. If so, it returns the override's
// client and ref for the caller to retry. If the override is the same client
// that already failed, or is nil, it returns nil. Must be called with mirrorMu held.
func (s *dockerImageSource) retireStaleOverride(failedClient *dockerClient) (*dockerClient, dockerReference, bool) {
if s.mirrorOverride != nil && s.mirrorOverride.client != failedClient {
return s.mirrorOverride.client, s.mirrorOverride.ref, true
}
return nil, dockerReference{}, false
}
// retireCurrentOverride moves the current mirrorOverride to prevOverrides.
// Must be called with mirrorMu held.
func (s *dockerImageSource) retireCurrentOverride() {
if s.mirrorOverride != nil {
s.prevOverrides = append(s.prevOverrides, s.mirrorOverride.client)
s.mirrorOverride = nil
}
}
func formatFallbackError(attempts []sourceAttempt, originalErr error) error {
if len(attempts) == 0 {
return originalErr
}
extras := make([]string, 0, len(attempts))
for _, a := range attempts {
extras = append(extras, fmt.Sprintf("[%s: %v]", a.ref.String(), a.err))
}
return fmt.Errorf("(Fallback sources also failed: %s): %w", strings.Join(extras, "\n"), originalErr)
}
func (s *dockerImageSource) getBlobWithMirrorFallback(ctx context.Context, info types.BlobInfo, cache types.BlobInfoCache, originalErr error, failedClient *dockerClient) (io.ReadCloser, int64, error) {
// Held for the full fallback loop, including network I/O. This serializes
// concurrent blob fetchers during fallback — only one goroutine probes
// sources while the rest block on getActiveSource(). Acceptable trade-off:
// fallback is rare and serialization prevents thundering-herd probing.
s.mirrorMu.Lock()
defer s.mirrorMu.Unlock()
if client, physRef, ok := s.retireStaleOverride(failedClient); ok {
reader, size, err := tryGetBlob(ctx, client, physRef, info, cache, len(s.remainingSources) > 0)
if err == nil {
return reader, size, nil
}
s.retireCurrentOverride()
}
attempts := []sourceAttempt{}
for len(s.remainingSources) > 0 {
pullSource := s.remainingSources[0]
s.remainingSources = s.remainingSources[1:]
logrus.Debugf("Trying to access %q", pullSource.Reference)
fallbackEndpoint, clientErr := newPullClient(s.fallbackSys, s.logicalRef, pullSource, s.fallbackRegConfig)
if clientErr != nil {
logrus.Debugf("Accessing %q failed: %v", pullSource.Reference, clientErr)
attempts = append(attempts, sourceAttempt{ref: pullSource.Reference, err: clientErr})
continue
}
reader, size, err := tryGetBlob(ctx, fallbackEndpoint.client, fallbackEndpoint.ref, info, cache, len(s.remainingSources) > 0)
if err != nil {
fallbackEndpoint.client.Close()
logrus.Debugf("Accessing %q failed: %v", pullSource.Reference, err)
attempts = append(attempts, sourceAttempt{ref: pullSource.Reference, err: err})
if !isMirrorTransientError(err) && !isMirrorFallbackError(err) {
// non-transient errors are unlikely to be resolved by trying another source: stop probing further.
// break here to log the attempts and return originalErr below.
break
}
continue
}
s.retireCurrentOverride()
s.mirrorOverride = &mirrorSource{client: fallbackEndpoint.client, ref: fallbackEndpoint.ref}
logrus.Debugf("Blob fetch succeeded from fallback source %q, switching to it for future requests", pullSource.Reference)
return reader, size, nil
}
return nil, 0, formatFallbackError(attempts, originalErr)
}
func (s *dockerImageSource) getBlobAtWithMirrorFallback(ctx context.Context, info types.BlobInfo, chunks []private.ImageSourceChunk, headers map[string][]string, originalErr error, failedClient *dockerClient) (chan io.ReadCloser, chan error, error) {
s.mirrorMu.Lock()
defer s.mirrorMu.Unlock()
if client, physRef, ok := s.retireStaleOverride(failedClient); ok {
streams, errs, err := tryGetBlobAt(ctx, client, physRef, info, chunks, headers, len(s.remainingSources) > 0)
if err == nil {
return streams, errs, nil
}
s.retireCurrentOverride()
}
attempts := []sourceAttempt{}
for len(s.remainingSources) > 0 {
pullSource := s.remainingSources[0]
s.remainingSources = s.remainingSources[1:]
logrus.Debugf("Trying to access %q", pullSource.Reference)
fallbackEndpoint, clientErr := newPullClient(s.fallbackSys, s.logicalRef, pullSource, s.fallbackRegConfig)
if clientErr != nil {
logrus.Debugf("Accessing %q failed: %v", pullSource.Reference, clientErr)
attempts = append(attempts, sourceAttempt{ref: pullSource.Reference, err: clientErr})
continue
}
streams, errs, err := tryGetBlobAt(ctx, fallbackEndpoint.client, fallbackEndpoint.ref, info, chunks, headers, len(s.remainingSources) > 0)
if err != nil {
fallbackEndpoint.client.Close()
logrus.Debugf("Accessing %q failed: %v", pullSource.Reference, err)
attempts = append(attempts, sourceAttempt{ref: pullSource.Reference, err: err})
if !isMirrorTransientError(err) && !isMirrorFallbackError(err) {
// non-transient errors are unlikely to be resolved by trying another source: stop probing further.
// break here to log the attempts and return originalErr below.
break
}
continue
}
s.retireCurrentOverride()
s.mirrorOverride = &mirrorSource{client: fallbackEndpoint.client, ref: fallbackEndpoint.ref}
logrus.Debugf("Partial blob fetch succeeded from fallback source %q, switching to it for future requests", pullSource.Reference)
return streams, errs, nil
}
return nil, nil, formatFallbackError(attempts, originalErr)
}
// GetSignaturesWithFormat returns the image's signatures. It may use a remote (= slow) service.

View file

@ -8,7 +8,7 @@ const (
// VersionMinor is for functionality in a backwards-compatible manner
VersionMinor = 41
// VersionPatch is for backwards-compatible bug fixes
VersionPatch = 0
VersionPatch = 1
// VersionDev indicates development branch. Releases will be empty string.
VersionDev = ""

4
vendor/modules.txt vendored
View file

@ -777,7 +777,7 @@ go.podman.io/buildah/pkg/sshagent
go.podman.io/buildah/pkg/util
go.podman.io/buildah/pkg/volumes
go.podman.io/buildah/util
# go.podman.io/common v0.69.0
# go.podman.io/common v0.69.1
## explicit; go 1.25.7
go.podman.io/common/internal
go.podman.io/common/libimage
@ -843,7 +843,7 @@ go.podman.io/common/pkg/umask
go.podman.io/common/pkg/util
go.podman.io/common/pkg/version
go.podman.io/common/version
# go.podman.io/image/v5 v5.41.0
# go.podman.io/image/v5 v5.41.1
## explicit; go 1.25.7
go.podman.io/image/v5/copy
go.podman.io/image/v5/directory