diff --git a/go.mod b/go.mod index 198427a6fa..8dc017dbc3 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 79ff921e1c..a09d24032a 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/libpod/networking_common.go b/libpod/networking_common.go index 0da0a84413..d51b7b82b5 100644 --- a/libpod/networking_common.go +++ b/libpod/networking_common.go @@ -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. diff --git a/libpod/oci_util.go b/libpod/oci_util.go index bc316b5a74..8129c365bb 100644 --- a/libpod/oci_util.go +++ b/libpod/oci_util.go @@ -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": diff --git a/test/system/500-networking.bats b/test/system/500-networking.bats index 6652fdeaf9..abe7abb0cb 100644 --- a/test/system/500-networking.bats +++ b/test/system/500-networking.bats @@ -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 < 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 blob’s 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 400–499. 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. diff --git a/vendor/go.podman.io/image/v5/version/version.go b/vendor/go.podman.io/image/v5/version/version.go index adb2667d4f..40852fed62 100644 --- a/vendor/go.podman.io/image/v5/version/version.go +++ b/vendor/go.podman.io/image/v5/version/version.go @@ -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 = "" diff --git a/vendor/modules.txt b/vendor/modules.txt index 230a004cc2..3119db6f75 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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