From 0ff6ac85f0713b96d1ed7fee08b0626a44167ec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Rod=C3=A1k?= Date: Thu, 9 Apr 2026 13:33:07 +0200 Subject: [PATCH 1/3] Vendor storage@main, image/v5@main, common@main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Used command: go get go.podman.io/storage@main && go get go.podman.io/image/v5@main && go get go.podman.io/common@main && make vendor Signed-off-by: Jan Rodák --- go.mod | 6 +- go.sum | 16 +-- .../internal/rootlessnetns/netns_linux.go | 22 ++- .../common/libnetwork/netavark/network.go | 37 ++--- .../common/libnetwork/netavark/run.go | 13 ++ .../common/libnetwork/pasta/pesto_linux.go | 127 ++++++++++++++++++ .../common/libnetwork/types/network.go | 3 + .../go.podman.io/common/pkg/config/config.go | 11 ++ .../common/pkg/config/containers.conf | 43 ++---- .../common/pkg/config/containers.conf-freebsd | 31 ----- .../go.podman.io/common/pkg/config/default.go | 1 + vendor/go.podman.io/storage/VERSION | 2 +- .../storage/pkg/chunked/dump/dump.go | 5 +- vendor/modules.txt | 6 +- 14 files changed, 224 insertions(+), 99 deletions(-) create mode 100644 vendor/go.podman.io/common/libnetwork/pasta/pesto_linux.go diff --git a/go.mod b/go.mod index ea09a2bb4a..86d744ee36 100644 --- a/go.mod +++ b/go.mod @@ -65,9 +65,9 @@ require ( github.com/vishvananda/netlink v1.3.1 go.etcd.io/bbolt v1.4.3 go.podman.io/buildah v1.42.1-0.20260501153811-377cf64e213b - go.podman.io/common v0.67.2-0.20260515151312-e2c14667a598 - go.podman.io/image/v5 v5.39.3-0.20260515151312-e2c14667a598 - go.podman.io/storage v1.62.1-0.20260515151312-e2c14667a598 + go.podman.io/common v0.67.2-0.20260519201413-7e9ee2072844 + go.podman.io/image/v5 v5.39.3-0.20260519201413-7e9ee2072844 + go.podman.io/storage v1.63.1-0.20260519201413-7e9ee2072844 golang.org/x/crypto v0.51.0 golang.org/x/net v0.54.0 golang.org/x/sync v0.20.0 diff --git a/go.sum b/go.sum index dbf5e08c11..899faf05f0 100644 --- a/go.sum +++ b/go.sum @@ -98,8 +98,8 @@ github.com/disiqueira/gotree/v3 v3.0.2 h1:ik5iuLQQoufZBNPY518dXhiO5056hyNBIK9lWh github.com/disiqueira/gotree/v3 v3.0.2/go.mod h1:ZuyjE4+mUQZlbpkI24AmruZKhg3VHEgPLDY8Qk+uUu8= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v29.5.0+incompatible h1:FPUvKJoKpeP4Njz8NrQdeUN8o247P7ndTiILtaP5/l4= -github.com/docker/cli v29.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.5.1+incompatible h1:NiufLAJoRcPauFoBNYthfuM4REFwM8H2h9xnLABNHGs= +github.com/docker/cli v29.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker-credential-helpers v0.9.7 h1:jaPIxEIDz5bQeghNAdzz0ETwMMnM4vzjZlxz3pWP4JA= @@ -431,12 +431,12 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.podman.io/buildah v1.42.1-0.20260501153811-377cf64e213b h1:i8ntFzITajbJA3ojnA0ZdpbC+I+ccweZvZaGIhQb4i8= go.podman.io/buildah v1.42.1-0.20260501153811-377cf64e213b/go.mod h1:hPvgsjBU09C+15fKoIZJvKvNaxR+c0QvMg/n4NgBS7A= -go.podman.io/common v0.67.2-0.20260515151312-e2c14667a598 h1:siUEt+Q/qN+1TI4/dem6kaI4x4hnanj0CPKIYLAA5x8= -go.podman.io/common v0.67.2-0.20260515151312-e2c14667a598/go.mod h1:Zw3/xJw/PY5FERQykTNIWFAJzL9ism2LaWFrmvFlBjo= -go.podman.io/image/v5 v5.39.3-0.20260515151312-e2c14667a598 h1:nHqODDlrwo33mxXf5qH7b7rjcc2ZVmR8xSTdvAApOrg= -go.podman.io/image/v5 v5.39.3-0.20260515151312-e2c14667a598/go.mod h1:PehFFa+D1wGt+GL5PCm0BupwarWcFFnOpZyQkF0eGdw= -go.podman.io/storage v1.62.1-0.20260515151312-e2c14667a598 h1:HtvnEpmKdzedYRDIBFIrVJummv3WnflKYEhK+3twZs0= -go.podman.io/storage v1.62.1-0.20260515151312-e2c14667a598/go.mod h1:z4Z9K+7GhKjWL/Y1O17+4f8a1KGijVeC9hr3tymhSOs= +go.podman.io/common v0.67.2-0.20260519201413-7e9ee2072844 h1:Un2Wz6Ni/QmkVC528gXOVvVKAM/vOi/c7kLK8gWvmvI= +go.podman.io/common v0.67.2-0.20260519201413-7e9ee2072844/go.mod h1:bhfqGXJ/cMC6CYubcmENInEQlemgClO0ea9+e1Mz/8k= +go.podman.io/image/v5 v5.39.3-0.20260519201413-7e9ee2072844 h1:wbh4wP38Ba13JjvHtNS2RZrI+L2/n2pIhQY36dUaebw= +go.podman.io/image/v5 v5.39.3-0.20260519201413-7e9ee2072844/go.mod h1:Jg91tpyyYgFAQLG1YjWbaZs4NHRzLWpDgCZyiOvDZyY= +go.podman.io/storage v1.63.1-0.20260519201413-7e9ee2072844 h1:s3bBIQVRqZ4uusgEMaVmUfXXphj4tEsg7BwiQB4COkQ= +go.podman.io/storage v1.63.1-0.20260519201413-7e9ee2072844/go.mod h1:z4Z9K+7GhKjWL/Y1O17+4f8a1KGijVeC9hr3tymhSOs= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= diff --git a/vendor/go.podman.io/common/libnetwork/internal/rootlessnetns/netns_linux.go b/vendor/go.podman.io/common/libnetwork/internal/rootlessnetns/netns_linux.go index 687b421029..c11dc07d7c 100644 --- a/vendor/go.podman.io/common/libnetwork/internal/rootlessnetns/netns_linux.go +++ b/vendor/go.podman.io/common/libnetwork/internal/rootlessnetns/netns_linux.go @@ -40,6 +40,11 @@ const ( // rootlessNetNsConnPidFile is the name of the rootless netns slirp4netns/pasta pid file. rootlessNetNsConnPidFile = "rootless-netns-conn.pid" + // pestoSocketFile is the name of the UNIX domain socket file used by + // pesto to communicate with the running pasta instance. Pasta is started + // with "-c " to enable this control channel. + pestoSocketFile = "pasta.sock" + tmpfs = "tmpfs" none = "none" resolvConfName = "resolv.conf" @@ -198,10 +203,18 @@ func (n *Netns) cleanup() error { func (n *Netns) setupPasta(nsPath string) error { pidPath := n.getPath(rootlessNetNsConnPidFile) + extraOpts := []string{"--pid", pidPath} + + var socketPath string + if n.config.Network.RootlessPortForwarder == config.RootlessPortForwarderPasta { + socketPath = n.getPath(pestoSocketFile) + extraOpts = append(extraOpts, "-c", socketPath) + } + pastaOpts := pasta.SetupOptions{ Config: n.config, Netns: nsPath, - ExtraOptions: []string{"--pid", pidPath}, + ExtraOptions: extraOpts, } res, err := pasta.Setup(&pastaOpts) if err != nil { @@ -235,9 +248,10 @@ func (n *Netns) setupPasta(nsPath string) error { } n.info = &types.RootlessNetnsInfo{ - IPAddresses: res.IPAddresses, - DnsForwardIps: res.DNSForwardIPs, - MapGuestIps: res.MapGuestAddrIPs, + IPAddresses: res.IPAddresses, + DnsForwardIps: res.DNSForwardIPs, + MapGuestIps: res.MapGuestAddrIPs, + PestoSocketPath: socketPath, } if err := n.serializeInfo(); err != nil { return wrapError("serialize info", err) diff --git a/vendor/go.podman.io/common/libnetwork/netavark/network.go b/vendor/go.podman.io/common/libnetwork/netavark/network.go index e1021309a2..c06d0486f2 100644 --- a/vendor/go.podman.io/common/libnetwork/netavark/network.go +++ b/vendor/go.podman.io/common/libnetwork/netavark/network.go @@ -71,6 +71,12 @@ type netavarkNetwork struct { // rootlessNetns is used for the rootless network setup/teardown rootlessNetns *rootlessnetns.Netns + + // rootlessPortForwarder is the value of config.RootlessPortForwarder from + // containers.conf. When set to config.RootlessPortForwarderPasta, HostIP + // is stripped from port mappings before passing to netavark because pasta's + // splice changes the destination IP. + rootlessPortForwarder string } type InitConfig struct { @@ -145,21 +151,22 @@ func NewNetworkInterface(conf *InitConfig) (types.ContainerNetwork, error) { } n := &netavarkNetwork{ - networkConfigDir: conf.NetworkConfigDir, - networkRunDir: conf.NetworkRunDir, - netavarkBinary: conf.NetavarkBinary, - aardvarkBinary: conf.AardvarkBinary, - networkRootless: useRootlessNetns, - ipamDBPath: filepath.Join(conf.NetworkRunDir, "ipam.db"), - firewallDriver: conf.Config.Network.FirewallDriver, - defaultNetwork: defaultNetworkName, - defaultSubnet: defaultNet, - defaultsubnetPools: defaultSubnetPools, - dnsBindPort: conf.Config.Network.DNSBindPort, - pluginDirs: conf.Config.Network.NetavarkPluginDirs.Get(), - lock: lock, - syslog: conf.Syslog, - rootlessNetns: netns, + networkConfigDir: conf.NetworkConfigDir, + networkRunDir: conf.NetworkRunDir, + netavarkBinary: conf.NetavarkBinary, + aardvarkBinary: conf.AardvarkBinary, + networkRootless: useRootlessNetns, + ipamDBPath: filepath.Join(conf.NetworkRunDir, "ipam.db"), + firewallDriver: conf.Config.Network.FirewallDriver, + defaultNetwork: defaultNetworkName, + defaultSubnet: defaultNet, + defaultsubnetPools: defaultSubnetPools, + dnsBindPort: conf.Config.Network.DNSBindPort, + pluginDirs: conf.Config.Network.NetavarkPluginDirs.Get(), + lock: lock, + syslog: conf.Syslog, + rootlessNetns: netns, + rootlessPortForwarder: conf.Config.Network.RootlessPortForwarder, } return n, nil diff --git a/vendor/go.podman.io/common/libnetwork/netavark/run.go b/vendor/go.podman.io/common/libnetwork/netavark/run.go index b172c2e654..0a385edc5d 100644 --- a/vendor/go.podman.io/common/libnetwork/netavark/run.go +++ b/vendor/go.podman.io/common/libnetwork/netavark/run.go @@ -12,6 +12,7 @@ import ( "github.com/sirupsen/logrus" "go.podman.io/common/libnetwork/internal/util" "go.podman.io/common/libnetwork/types" + "go.podman.io/common/pkg/config" ) type netavarkOptions struct { @@ -162,6 +163,18 @@ func (n *netavarkNetwork) getCommonNetavarkOptions(needPlugin bool) []string { } func (n *netavarkNetwork) convertNetOpts(opts types.NetworkOptions) (*netavarkOptions, bool, error) { + // In pasta mode, strip HostIP from port mappings. Pasta handles host-side + // address binding; netavark only needs DNAT rules inside the netns without + // "ip daddr" constraints (pasta's splice changes the destination IP). + if n.rootlessPortForwarder == config.RootlessPortForwarderPasta && n.networkRootless && len(opts.PortMappings) > 0 { + stripped := make([]types.PortMapping, len(opts.PortMappings)) + copy(stripped, opts.PortMappings) + for i := range stripped { + stripped[i].HostIP = "" + } + opts.PortMappings = stripped + } + netavarkOptions := netavarkOptions{ NetworkOptions: opts, Networks: make(map[string]*types.Network, len(opts.Networks)), diff --git a/vendor/go.podman.io/common/libnetwork/pasta/pesto_linux.go b/vendor/go.podman.io/common/libnetwork/pasta/pesto_linux.go new file mode 100644 index 0000000000..bd8f4b6639 --- /dev/null +++ b/vendor/go.podman.io/common/libnetwork/pasta/pesto_linux.go @@ -0,0 +1,127 @@ +// Pesto client for dynamic port forwarding on a running pasta instance. +// +// Pesto updates pasta's forwarding table via a UNIX domain socket (-c). +// Used by rootless bridge networking: pesto incrementally adds or deletes +// port forwarding rules for individual containers. +// +// Passt only forwards traffic from the host into the rootless netns. +// Netavark handles the final DNAT to the container IP:ContainerPort +// inside the netns. Each mapping uses HostPort as both source and +// destination so traffic arrives at the port netavark expects. +// +// When no HostIP is specified, pesto binds both IPv4 (0.0.0.0) and +// IPv6 ([::]) so dual-stack networks work out of the box. +// +// Limitations: +// - TCP and UDP only (SCTP is silently skipped) + +package pasta + +import ( + "errors" + "fmt" + "os/exec" + "strings" + + "github.com/sirupsen/logrus" + "go.podman.io/common/libnetwork/types" + "go.podman.io/common/pkg/config" +) + +const PestoBinaryName = "pesto" + +// PestoAddPorts adds port forwarding rules to the running pasta instance +// via -A/--add. Idempotent: adding already-active ports is a no-op. +func PestoAddPorts(conf *config.Config, socketPath string, ports []types.PortMapping) error { + if socketPath == "" { + return errors.New("pesto control socket not available") + } + logrus.Debugf("pesto: adding %d port mappings", len(ports)) + return pestoModifyPorts(conf, socketPath, ports, "--add") +} + +// PestoDeletePorts removes port forwarding rules from the running pasta +// instance via -D/--delete. +func PestoDeletePorts(conf *config.Config, socketPath string, ports []types.PortMapping) error { + if socketPath == "" { + return nil + } + logrus.Debugf("pesto: deleting %d port mappings", len(ports)) + return pestoModifyPorts(conf, socketPath, ports, "--delete") +} + +func pestoModifyPorts(conf *config.Config, socketPath string, ports []types.PortMapping, mode string) error { + pestoPath, err := conf.FindHelperBinary(PestoBinaryName, true) + if err != nil { + return fmt.Errorf("could not find pesto binary: %w", err) + } + + pestoArgs, err := portMappingsToPestoArgs(ports) + if err != nil { + return err + } + args := make([]string, 0, len(pestoArgs)+2) // +2 for mode and socket path + args = append(args, mode) + args = append(args, pestoArgs...) + args = append(args, socketPath) + + logrus.Debugf("pesto arguments: %s", strings.Join(args, " ")) + + out, err := exec.Command(pestoPath, args...).CombinedOutput() + if err != nil { + return fmt.Errorf("pesto failed: %w\noutput: %s", err, string(out)) + } + if len(out) > 0 { + logrus.Debugf("pesto output: %s", strings.TrimSpace(string(out))) + } + return nil +} + +// portMappingsToPestoArgs converts PortMappings into pesto CLI arguments. +// +// When HostIP is set, a single binding is created (e.g. "-t 127.0.0.1/8080"). +// When HostIP is empty, both IPv4 and IPv6 bindings are created so that +// dual-stack networks work: "-t 0.0.0.0/8080 -t [::]/8080". +func portMappingsToPestoArgs(ports []types.PortMapping) ([]string, error) { + var args []string + + for _, p := range ports { + var addrs []string + switch { + case p.HostIP == "": + addrs = []string{"0.0.0.0/", "[::]/"} + case strings.Contains(p.HostIP, ":"): + addrs = []string{"[" + p.HostIP + "]/"} + default: + addrs = []string{p.HostIP + "/"} + } + + for protocol := range strings.SplitSeq(p.Protocol, ",") { + var flag string + switch protocol { + case "tcp": + flag = "-t" + case "udp": + flag = "-u" + default: + return nil, fmt.Errorf("pesto: unsupported protocol %s", protocol) + } + + portRange := p.Range + if portRange == 0 { + portRange = 1 + } + + for _, addr := range addrs { + var arg string + if portRange == 1 { + arg = fmt.Sprintf("%s%d", addr, p.HostPort) + } else { + arg = fmt.Sprintf("%s%d-%d", addr, p.HostPort, p.HostPort+portRange-1) + } + args = append(args, flag, arg) + } + } + } + return args, nil +} diff --git a/vendor/go.podman.io/common/libnetwork/types/network.go b/vendor/go.podman.io/common/libnetwork/types/network.go index 11ba341355..85159bad2b 100644 --- a/vendor/go.podman.io/common/libnetwork/types/network.go +++ b/vendor/go.podman.io/common/libnetwork/types/network.go @@ -377,6 +377,9 @@ type RootlessNetnsInfo struct { DnsForwardIps []string // MapGuestIps should be used for the host.containers.internal entry when set MapGuestIps []string + // PestoSocketPath is the path to the pasta control socket for dynamic + // port forwarding via pesto. Empty when pasta was started without -c. + PestoSocketPath string } // FilterFunc can be passed to NetworkList to filter the networks. diff --git a/vendor/go.podman.io/common/pkg/config/config.go b/vendor/go.podman.io/common/pkg/config/config.go index eb21a3895a..e53dcce63b 100644 --- a/vendor/go.podman.io/common/pkg/config/config.go +++ b/vendor/go.podman.io/common/pkg/config/config.go @@ -630,8 +630,19 @@ type NetworkConfig struct { // If multiple IPs are specified, separate port mapping for each of the specified // IP would be created. DefaultHostIPs configfile.Slice `toml:"default_host_ips,omitempty"` + + // RootlessPortForwarder selects the port forwarding mechanism for rootless + // bridge networks. Valid values are RootlessPortForwarderRootlessport + // (default, userspace TCP/UDP proxy) and RootlessPortForwarderPasta + // (experimental, pasta's kernel splice preserving the original source IP). + RootlessPortForwarder string `toml:"rootless_port_forwarder,omitempty"` } +const ( + RootlessPortForwarderRootlessport = "rootlessport" + RootlessPortForwarderPasta = "pasta" +) + type SubnetPool struct { // Base is a bigger subnet which will be used to allocate a subnet with // the given size. diff --git a/vendor/go.podman.io/common/pkg/config/containers.conf b/vendor/go.podman.io/common/pkg/config/containers.conf index 9a6f38bfd2..1725909fc7 100644 --- a/vendor/go.podman.io/common/pkg/config/containers.conf +++ b/vendor/go.podman.io/common/pkg/config/containers.conf @@ -405,11 +405,19 @@ default_sysctls = [ -# Configure which rootless network program to use by default. Valid options are -# `slirp4netns` and `pasta` (default). +# Configure which rootless network program to use by default. +# The only valid option is `pasta` (default). # #default_rootless_network_cmd = "pasta" +# Select the port forwarding mechanism for rootless bridge networks. +# "rootlessport" (default) uses a userspace TCP/UDP proxy. +# "pasta" (experimental) uses pasta's control socket to add port forwarding rules +# via kernel splice, which preserves the original source IP address inside the +# container. This option is experimental and subject to change. +# +#rootless_port_forwarder = "rootlessport" + # Path to the directory where network configuration files are located. # The default is "/etc/containers/networks" as root # and "$graphroot/networks" as rootless. @@ -510,15 +518,6 @@ default_sysctls = [ # short-name aliases defined in containers-registries.conf(5). #compat_api_enforce_docker_hub = true -# The database backend of Podman. Supported values are "" (default), "boltdb" -# and "sqlite". An empty value means it will check whenever a boltdb already -# exists and use it when it does, otherwise it will use sqlite as default -# (e.g. new installs). This allows for backwards compatibility with older versions. -# Please run `podman-system-reset` prior to changing the database -# backend of an existing deployment, to make sure Podman can operate correctly. -# -#database_backend = "" - # Specify the keys sequence used to detach a container. # Format is a single character [a-Z] or a comma separated sequence of # `ctrl-`, where `` is one of: @@ -663,28 +662,6 @@ default_sysctls = [ # #namespace = "" -# Default options to pass to the slirp4netns binary. -# Valid options values are: -# -# - allow_host_loopback=true|false: Allow the slirp4netns to reach the host loopback IP (`10.0.2.2`). -# Default is false. -# - mtu=MTU: Specify the MTU to use for this network. (Default is `65520`). -# - cidr=CIDR: Specify ip range to use for this network. (Default is `10.0.2.0/24`). -# - enable_ipv6=true|false: Enable IPv6. Default is true. (Required for `outbound_addr6`). -# - outbound_addr=INTERFACE: Specify the outbound interface slirp should bind to (ipv4 traffic only). -# - outbound_addr=IPv4: Specify the outbound ipv4 address slirp should bind to. -# - outbound_addr6=INTERFACE: Specify the outbound interface slirp should bind to (ipv6 traffic only). -# - outbound_addr6=IPv6: Specify the outbound ipv6 address slirp should bind to. -# - port_handler=rootlesskit: Use rootlesskit for port forwarding. Default. -# Note: Rootlesskit changes the source IP address of incoming packets to a IP address in the container -# network namespace, usually `10.0.2.100`. If your application requires the real source IP address, -# e.g. web server logs, use the slirp4netns port handler. The rootlesskit port handler is also used for -# rootless containers when connected to user-defined networks. -# - port_handler=slirp4netns: Use the slirp4netns port forwarding, it is slower than rootlesskit but -# preserves the correct source IP address. This port handler cannot be used for user-defined networks. -# -#network_cmd_options = [] - # Whether to use chroot instead of pivot_root in the runtime # #no_pivot_root = false diff --git a/vendor/go.podman.io/common/pkg/config/containers.conf-freebsd b/vendor/go.podman.io/common/pkg/config/containers.conf-freebsd index 620bd0ca4f..2f7713bd0c 100644 --- a/vendor/go.podman.io/common/pkg/config/containers.conf-freebsd +++ b/vendor/go.podman.io/common/pkg/config/containers.conf-freebsd @@ -35,15 +35,6 @@ # #container_name_as_hostname = false -# The database backend of Podman. Supported values are "" (default), "boltdb" -# and "sqlite". An empty value means it will check whenever a boltdb already -# exists and use it when it does, otherwise it will use sqlite as default -# (e.g. new installs). This allows for backwards compatibility with older versions. -# Please run `podman-system-reset` prior to changing the database -# backend of an existing deployment, to make sure Podman can operate correctly. -# -#database_backend = "" - # List of default capabilities for containers. If it is empty or commented out, # the default capabilities defined in the container engine will be added. # @@ -496,28 +487,6 @@ default_sysctls = [ # #namespace = "" -# Default options to pass to the slirp4netns binary. -# Valid options values are: -# -# - allow_host_loopback=true|false: Allow the slirp4netns to reach the host loopback IP (`10.0.2.2`). -# Default is false. -# - mtu=MTU: Specify the MTU to use for this network. (Default is `65520`). -# - cidr=CIDR: Specify ip range to use for this network. (Default is `10.0.2.0/24`). -# - enable_ipv6=true|false: Enable IPv6. Default is true. (Required for `outbound_addr6`). -# - outbound_addr=INTERFACE: Specify the outbound interface slirp should bind to (ipv4 traffic only). -# - outbound_addr=IPv4: Specify the outbound ipv4 address slirp should bind to. -# - outbound_addr6=INTERFACE: Specify the outbound interface slirp should bind to (ipv6 traffic only). -# - outbound_addr6=IPv6: Specify the outbound ipv6 address slirp should bind to. -# - port_handler=rootlesskit: Use rootlesskit for port forwarding. Default. -# Note: Rootlesskit changes the source IP address of incoming packets to a IP address in the container -# network namespace, usually `10.0.2.100`. If your application requires the real source IP address, -# e.g. web server logs, use the slirp4netns port handler. The rootlesskit port handler is also used for -# rootless containers when connected to user-defined networks. -# - port_handler=slirp4netns: Use the slirp4netns port forwarding, it is slower than rootlesskit but -# preserves the correct source IP address. This port handler cannot be used for user-defined networks. -# -#network_cmd_options = [] - # Whether to use chroot instead of pivot_root in the runtime # #no_pivot_root = false diff --git a/vendor/go.podman.io/common/pkg/config/default.go b/vendor/go.podman.io/common/pkg/config/default.go index a4dca5ebcd..1fd30e21cf 100644 --- a/vendor/go.podman.io/common/pkg/config/default.go +++ b/vendor/go.podman.io/common/pkg/config/default.go @@ -249,6 +249,7 @@ func defaultConfig() (*Config, error) { DefaultRootlessNetworkCmd: "pasta", DNSBindPort: 0, NetavarkPluginDirs: configfile.NewSlice(DefaultNetavarkPluginDirs), + RootlessPortForwarder: RootlessPortForwarderRootlessport, }, Engine: *defaultEngineConfig, Secrets: defaultSecretConfig(), diff --git a/vendor/go.podman.io/storage/VERSION b/vendor/go.podman.io/storage/VERSION index d73f2062b2..af92bdd9f5 100644 --- a/vendor/go.podman.io/storage/VERSION +++ b/vendor/go.podman.io/storage/VERSION @@ -1 +1 @@ -1.63.0-dev +1.63.0 diff --git a/vendor/go.podman.io/storage/pkg/chunked/dump/dump.go b/vendor/go.podman.io/storage/pkg/chunked/dump/dump.go index 1d004023b8..11b5a120ef 100644 --- a/vendor/go.podman.io/storage/pkg/chunked/dump/dump.go +++ b/vendor/go.podman.io/storage/pkg/chunked/dump/dump.go @@ -191,7 +191,10 @@ func dumpNode(out io.Writer, added map[string]*minimal.FileMetadata, links map[s if _, err := fmt.Fprint(out, " "); err != nil { return err } - digest := verityDigests[payload] + digest := "" + if entry.Type == minimal.TypeReg { + digest = verityDigests["/"+payload] + } if _, err := fmt.Fprint(out, escapedOptional([]byte(digest), ESCAPE_LONE_DASH)); err != nil { return err } diff --git a/vendor/modules.txt b/vendor/modules.txt index d11c251eb3..1b12269918 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -735,7 +735,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.67.2-0.20260515151312-e2c14667a598 +# go.podman.io/common v0.67.2-0.20260519201413-7e9ee2072844 ## explicit; go 1.25.6 go.podman.io/common/internal go.podman.io/common/libimage @@ -801,7 +801,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.39.3-0.20260515151312-e2c14667a598 +# go.podman.io/image/v5 v5.39.3-0.20260519201413-7e9ee2072844 ## explicit; go 1.25.6 go.podman.io/image/v5/copy go.podman.io/image/v5/directory @@ -878,7 +878,7 @@ go.podman.io/image/v5/transports go.podman.io/image/v5/transports/alltransports go.podman.io/image/v5/types go.podman.io/image/v5/version -# go.podman.io/storage v1.62.1-0.20260515151312-e2c14667a598 +# go.podman.io/storage v1.63.1-0.20260519201413-7e9ee2072844 ## explicit; go 1.25.0 go.podman.io/storage go.podman.io/storage/drivers From e5986572441b9b6e9b8dca1ceffde0858a019b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Rod=C3=A1k?= Date: Thu, 9 Apr 2026 13:55:18 +0200 Subject: [PATCH 2/3] Add pasta-based port forwarding for rootless bridge networks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add rootless_port_forwarder="pasta" option that uses pesto to update pasta's forwarding table via UNIX socket, preserving source IPs that rootlessport's userspace proxy masks. HostIP is stripped from port mappings in the netavark wrapper when pasta forwarding is active because pesto handles host-side binding while pasta's splice changes the destination IP that netavark DNAT expects. Pesto binds both 0.0.0.0 and [::] for dual-stack support. Fixes: https://redhat.atlassian.net/browse/RUN-2214 Fixes: https://github.com/containers/podman/issues/8193 Fixes: https://redhat.atlassian.net/browse/RUN-3587 Signed-off-by: Jan Rodák --- libpod/define/info.go | 7 +- libpod/info.go | 41 +++--- libpod/networking_common.go | 28 +++- libpod/networking_freebsd.go | 4 + libpod/networking_linux.go | 23 ++-- libpod/networking_pesto_linux.go | 52 +++++++ libpod/oci_conmon_common.go | 7 +- test/e2e/common_test.go | 29 ++++ test/e2e/run_networking_test.go | 215 +++++++++++++++++++++++++++++ test/system/500-networking.bats | 223 +++++++++++++++++++++---------- 10 files changed, 519 insertions(+), 110 deletions(-) create mode 100644 libpod/networking_pesto_linux.go diff --git a/libpod/define/info.go b/libpod/define/info.go index e03c39289f..2dacf09c92 100644 --- a/libpod/define/info.go +++ b/libpod/define/info.go @@ -53,8 +53,11 @@ type HostInfo struct { // RemoteSocket returns the UNIX domain socket the Podman service is listening on RemoteSocket *RemoteSocket `json:"remoteSocket,omitempty"` // RootlessNetworkCmd returns the default rootless network command (pasta) - RootlessNetworkCmd string `json:"rootlessNetworkCmd"` - RuntimeInfo map[string]any `json:"runtimeInfo,omitempty"` + RootlessNetworkCmd string `json:"rootlessNetworkCmd"` + // RootlessPortForwarder returns the port forwarding mechanism for rootless + // bridge networks: "rootlessport" (default) or "pasta" (experimental) + RootlessPortForwarder string `json:"rootlessPortForwarder"` + RuntimeInfo map[string]any `json:"runtimeInfo,omitempty"` // ServiceIsRemote is true when the podman/libpod service is remote to the client ServiceIsRemote bool `json:"serviceIsRemote"` Security SecurityInfo `json:"security"` diff --git a/libpod/info.go b/libpod/info.go index 8140fb12e0..5aab80902d 100644 --- a/libpod/info.go +++ b/libpod/info.go @@ -108,26 +108,27 @@ func (r *Runtime) hostInfo() (*define.HostInfo, error) { } info := define.HostInfo{ - Arch: runtime.GOARCH, - BuildahVersion: buildah.Version, - DatabaseBackend: r.state.Name(), - Linkmode: linkmode.Linkmode(), - CPUs: runtime.NumCPU(), - CPUUtilization: cpuUtil, - Distribution: hostDistributionInfo, - LogDriver: r.config.Containers.LogDriver, - EventLogger: r.eventer.String(), - FreeLocks: locksFree, - Hostname: host, - Kernel: kv, - MemFree: mi.MemFree, - MemTotal: mi.MemTotal, - NetworkBackend: r.config.Network.NetworkBackend, - NetworkBackendInfo: r.network.NetworkInfo(), - OS: runtime.GOOS, - RootlessNetworkCmd: r.config.Network.DefaultRootlessNetworkCmd, - SwapFree: mi.SwapFree, - SwapTotal: mi.SwapTotal, + Arch: runtime.GOARCH, + BuildahVersion: buildah.Version, + DatabaseBackend: r.state.Name(), + Linkmode: linkmode.Linkmode(), + CPUs: runtime.NumCPU(), + CPUUtilization: cpuUtil, + Distribution: hostDistributionInfo, + LogDriver: r.config.Containers.LogDriver, + EventLogger: r.eventer.String(), + FreeLocks: locksFree, + Hostname: host, + Kernel: kv, + MemFree: mi.MemFree, + MemTotal: mi.MemTotal, + NetworkBackend: r.config.Network.NetworkBackend, + NetworkBackendInfo: r.network.NetworkInfo(), + OS: runtime.GOOS, + RootlessNetworkCmd: r.config.Network.DefaultRootlessNetworkCmd, + RootlessPortForwarder: r.config.Network.RootlessPortForwarder, + SwapFree: mi.SwapFree, + SwapTotal: mi.SwapTotal, } platform := parse.DefaultPlatform() pArr := strings.Split(platform, "/") diff --git a/libpod/networking_common.go b/libpod/networking_common.go index 03bd8d99d0..25d50256d8 100644 --- a/libpod/networking_common.go +++ b/libpod/networking_common.go @@ -110,11 +110,27 @@ func (r *Runtime) teardownNetwork(ctr *Container) error { return err } - if !ctr.config.NetMode.IsPasta() && len(networks) > 0 { - netOpts := ctr.getNetworkOptions(networks) - return r.teardownNetworkBackend(ctr.state.NetNS, netOpts) + if len(networks) == 0 { + return nil } - return nil + + // --net=pasta: per-container pasta cleans up when it exits, nothing to tear down. + if ctr.config.NetMode.IsPasta() { + return nil + } + + // Pasta forwarding mode: remove port forwarding rules (via pesto) before + // netavark tears down bridge/nftables so pasta stops forwarding first. + // Rootlessport mode: no explicit teardown needed (exits with conmon). + if rootless.IsRootless() && ctr.config.NetMode.IsBridge() && len(ctr.config.PortMappings) > 0 && + r.config.Network.RootlessPortForwarder == config.RootlessPortForwarderPasta { + if err := r.teardownRootlessPortMappingViaPesto(ctr); err != nil { + logrus.Warnf("pesto port cleanup failed for container %s: %v", ctr.ID(), err) + } + } + + netOpts := ctr.getNetworkOptions(networks) + return r.teardownNetworkBackend(ctr.state.NetNS, netOpts) } // isBridgeNetMode checks if the given network mode is bridge. @@ -439,7 +455,7 @@ func (c *Container) NetworkDisconnect(nameOrID, netName string, _ bool) error { // Reload ports when there are still connected networks, maybe we removed the network interface with the child ip. // Reloading without connected networks does not make sense, so we can skip this step. - if rootless.IsRootless() && len(networkStatus) > 0 { + if rootless.IsRootless() && c.runtime.config.Network.RootlessPortForwarder == config.RootlessPortForwarderRootlessport && len(networkStatus) > 0 { if err := c.reloadRootlessRLKPortMapping(); err != nil { return err } @@ -595,7 +611,7 @@ func (c *Container) NetworkConnect(nameOrID, netName string, netOpts types.PerNe // The first network needs a port reload to set the correct child ip for the rootlessport process. // Adding a second network does not require a port reload because the child ip is still valid. - if rootless.IsRootless() && len(networks) == 0 { + if rootless.IsRootless() && c.runtime.config.Network.RootlessPortForwarder == config.RootlessPortForwarderRootlessport && len(networks) == 0 { if err := c.reloadRootlessRLKPortMapping(); err != nil { return err } diff --git a/libpod/networking_freebsd.go b/libpod/networking_freebsd.go index e296f996f0..561e91fa1c 100644 --- a/libpod/networking_freebsd.go +++ b/libpod/networking_freebsd.go @@ -224,3 +224,7 @@ func (c *Container) inspectJoinedNetworkNS(_ string) (q types.StatusBlock, retEr func (c *Container) reloadRootlessRLKPortMapping() error { return errors.New("unsupported (*Container).reloadRootlessRLKPortMapping") } + +func (r *Runtime) teardownRootlessPortMappingViaPesto(_ *Container) error { + return errors.New("unsupported teardownRootlessPortMappingViaPesto on FreeBSD") +} diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go index f16b95504c..d764b82965 100644 --- a/libpod/networking_linux.go +++ b/libpod/networking_linux.go @@ -11,6 +11,7 @@ import ( "github.com/sirupsen/logrus" "github.com/vishvananda/netlink" "go.podman.io/common/libnetwork/types" + "go.podman.io/common/pkg/config" "go.podman.io/common/pkg/netns" "go.podman.io/podman/v6/libpod/define" "go.podman.io/podman/v6/pkg/rootless" @@ -59,15 +60,19 @@ func (r *Runtime) configureNetNS(ctr *Container, ctrNS string) (status map[strin } }() - // set up rootless port forwarder when rootless with ports and the network status is empty, - // if this is called from network reload the network status will not be empty and we should - // not set up port because they are still active - if rootless.IsRootless() && len(ctr.config.PortMappings) > 0 && ctr.getNetworkStatus() == nil { - // set up port forwarder for rootless netns - // make sure to fix this in container.handleRestartPolicy() as well - // Important we have to call this after r.setUpNetwork() so that - // we can use the proper netStatus - err = r.setupRootlessPortMappingViaRLK(ctr, ctrNS, netStatus) + // Set up port forwarding for rootless bridge networks. + if rootless.IsRootless() && len(ctr.config.PortMappings) > 0 { + switch r.config.Network.RootlessPortForwarder { + case config.RootlessPortForwarderPasta: + err = r.setupRootlessPortMappingViaPesto(ctr) + case config.RootlessPortForwarderRootlessport, "": + if ctr.getNetworkStatus() == nil { + err = r.setupRootlessPortMappingViaRLK(ctr, ctrNS, netStatus) + } + default: + err = fmt.Errorf("invalid rootless_port_forwarder value %q, must be %q or %q", + r.config.Network.RootlessPortForwarder, config.RootlessPortForwarderRootlessport, config.RootlessPortForwarderPasta) + } } return netStatus, err } diff --git a/libpod/networking_pesto_linux.go b/libpod/networking_pesto_linux.go new file mode 100644 index 0000000000..873647e139 --- /dev/null +++ b/libpod/networking_pesto_linux.go @@ -0,0 +1,52 @@ +//go:build !remote + +// Pesto integration for rootless bridge network port forwarding. +// +// A shared pasta instance in the rootless netns (-c pasta.sock) handles +// host-side port forwarding. On container start/stop, pesto incrementally +// adds or deletes port forwarding rules for that container. Pasta forwards +// via kernel splice (localhost) or TAP (external), preserving source IPs. +// The container sees the real client's address instead of a proxy or bridge +// gateway address. +// +// Container start: +// - netavark sets up bridge + DNAT +// - pesto --add: adds this container's ports to pasta +// +// Container stop: +// - pesto --delete: removes this container's ports from pasta +// - netavark tears down bridge/DNAT + +package libpod + +import ( + "go.podman.io/common/libnetwork/pasta" +) + +func (r *Runtime) pestoSocketPath() string { + info, err := r.network.RootlessNetnsInfo() + if err != nil || info == nil { + return "" + } + return info.PestoSocketPath +} + +// setupRootlessPortMappingViaPesto adds this container's port forwarding +// rules to the shared pasta instance. +func (r *Runtime) setupRootlessPortMappingViaPesto(ctr *Container) error { + ports := ctr.convertPortMappings() + if len(ports) == 0 { + return nil + } + return pasta.PestoAddPorts(r.config, r.pestoSocketPath(), ports) +} + +// teardownRootlessPortMappingViaPesto removes this container's port +// forwarding rules from the shared pasta instance. +func (r *Runtime) teardownRootlessPortMappingViaPesto(ctr *Container) error { + ports := ctr.convertPortMappings() + if len(ports) == 0 { + return nil + } + return pasta.PestoDeletePorts(r.config, r.pestoSocketPath(), ports) +} diff --git a/libpod/oci_conmon_common.go b/libpod/oci_conmon_common.go index 91028d8c35..730e2bded0 100644 --- a/libpod/oci_conmon_common.go +++ b/libpod/oci_conmon_common.go @@ -1188,8 +1188,11 @@ func (r *ConmonOCIRuntime) createOCIContainer(ctr *Container, restoreOptions *Co // process cannot use them. cmd.ExtraFiles = append(cmd.ExtraFiles, ports...) - // For rootless port forwarding, create sync pipe and leak write end to conmon - if rootless.IsRootless() && len(ctr.config.PortMappings) > 0 { + // For rootless port forwarding via rootlessport, create sync pipe and + // leak write end to conmon. Pasta forwarding mode does not use + // rootlessport, so no pipe is needed. + if rootless.IsRootless() && len(ctr.config.PortMappings) > 0 && + ctr.runtime.config.Network.RootlessPortForwarder == config.RootlessPortForwarderRootlessport { ctr.rootlessPortSyncR, ctr.rootlessPortSyncW, err = os.Pipe() if err != nil { return 0, fmt.Errorf("failed to create rootless port sync pipe: %w", err) diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go index 53d8c1d8e7..667c394e1e 100644 --- a/test/e2e/common_test.go +++ b/test/e2e/common_test.go @@ -1629,6 +1629,35 @@ func testPortConnection(port int) { Expect(err).ToNot(HaveOccurred()) } +// startNCContainer starts a detached container running nc (netcat) listening +// on the given port, waits for it to be ready, and returns the container name. +// +//nolint:unused,nolintlint // only called from linux-only test files, unused on freebsd +func (p *PodmanTestIntegration) startNCContainer(name string, listenPort int, extraArgs ...string) string { + GinkgoHelper() + portStr := strconv.Itoa(listenPort) + args := append([]string{"run", "-d", "--name", name}, extraArgs...) + args = append(args, ALPINE, "sh", "-c", "nc -l -n -v -p "+portStr+" 2>&1") + p.PodmanExitCleanly(args...) + p.WaitForContainerLog(name, "listening") + return name +} + +// WaitForContainerLog polls container logs until the given substring appears +// in either stdout or stderr. Fails the test if not found within the timeout. +func (p *PodmanTestIntegration) WaitForContainerLog(ctrName string, substr string) { + GinkgoHelper() + for range 10 { + logs := p.Podman([]string{"logs", ctrName}) + logs.WaitWithDefaultTimeout() + if strings.Contains(logs.ErrorToString(), substr) || strings.Contains(logs.OutputToString(), substr) { + return + } + time.Sleep(500 * time.Millisecond) + } + Fail(fmt.Sprintf("timed out waiting for %q in logs of container %s", substr, ctrName)) +} + func createNetworkName(name string) string { return name + stringid.GenerateRandomID()[:10] } diff --git a/test/e2e/run_networking_test.go b/test/e2e/run_networking_test.go index 7ef3d9ef35..5906bd5adf 100644 --- a/test/e2e/run_networking_test.go +++ b/test/e2e/run_networking_test.go @@ -4,11 +4,15 @@ package integration import ( "fmt" + "io" "math/rand" "net" "os" + "os/exec" + "path/filepath" "strings" "syscall" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -1049,6 +1053,198 @@ EXPOSE 2004-2005/tcp`, ALPINE) Expect(session).Should(ExitCleanly()) }) + // configurePortForwarder sets rootless_port_forwarder via + // CONTAINERS_CONF_OVERRIDE for the duration of this test. + // For "pasta": requires rootless and pesto binary, skips otherwise. + // For "rootlessport": no-op (it's the default). + configurePortForwarder := func(forwarder string) { + GinkgoHelper() + if forwarder == "pasta" { + if !isRootless() { + Skip("pasta port forwarding requires rootless") + } + if _, err := exec.LookPath("pesto"); err != nil { + Skip("pesto binary not found (requires passt >= passt-0^20260507.g1afd4ed)") + } + } + conffile := filepath.Join(podmanTest.TempDir, forwarder+"-forwarder.conf") + err := os.WriteFile(conffile, []byte(fmt.Sprintf("[network]\nrootless_port_forwarder=\"%s\"\n", forwarder)), 0o755) + Expect(err).ToNot(HaveOccurred()) + GinkgoT().Setenv("CONTAINERS_CONF_OVERRIDE", conffile) + if IsRemote() { + podmanTest.RestartRemoteService() + } + } + + for _, forwarder := range []string{"rootlessport", "pasta"} { + for i, tc := range []struct { + name string + ipv6 bool + hostIP string + diffPorts bool + }{ + {name: "IPv4"}, + {name: "IPv4 explicit HostIP", hostIP: "127.0.0.1"}, + {name: "IPv4 different ports", diffPorts: true}, + {name: "IPv6", ipv6: true}, + {name: "IPv6 explicit HostIP", ipv6: true, hostIP: "[::1]"}, + {name: "IPv6 different ports", ipv6: true, diffPorts: true}, + } { + It(fmt.Sprintf("podman run bridge source IP %s %s", forwarder, tc.name), func() { + if tc.ipv6 { + SkipIfNotRootless("netavark does not support IPv6 port forwarding") + if forwarder == "rootlessport" { + Skip("rootlessport does not support native IPv6 port forwarding") + } + } + configurePortForwarder(forwarder) + + // Each forwarder+test combination needs a unique subnet + // to avoid collisions when running in parallel. + pastaOffset := 0 + if forwarder == "pasta" { + pastaOffset = 10 + } + var subnet, subnetMatch, connectAddr string + if tc.ipv6 { + subnet = fmt.Sprintf("fd00:%x::/64", 0x42+i+pastaOffset) + subnetMatch = fmt.Sprintf("fd00:%x:", 0x42+i+pastaOffset) + connectAddr = "[::1]" + } else { + subnet = fmt.Sprintf("172.%d.0.0/24", 30+i+pastaOffset) + subnetMatch = fmt.Sprintf(`172\.%d\.`, 30+i+pastaOffset) + connectAddr = "127.0.0.1" + } + + createArgs := []string{"network", "create"} + if tc.ipv6 { + createArgs = append(createArgs, "--ipv6") + } + createArgs = append(createArgs, "--subnet", subnet) + netName := createNetworkName("srcip") + createArgs = append(createArgs, netName) + podmanTest.PodmanExitCleanly(createArgs...) + defer podmanTest.removeNetwork(netName) + + hostPort := GetPort() + ctrPort := hostPort + if tc.diffPorts { + ctrPort = GetPort() + } + portFlag := fmt.Sprintf("%d:%d", hostPort, ctrPort) + if tc.hostIP != "" { + portFlag = fmt.Sprintf("%s:%d:%d", tc.hostIP, hostPort, ctrPort) + } + ctr := podmanTest.startNCContainer( + "srcip-ctr", ctrPort, + "--network", netName, + "-p", portFlag, + ) + + msg := RandomString(20) + sendMessageToAddr(fmt.Sprintf("%s:%d", connectAddr, hostPort), msg) + podmanTest.WaitForContainerLog(ctr, msg) + + logs := podmanTest.PodmanExitCleanly("logs", ctr) + output := logs.OutputToString() + Expect(output).To(MatchRegexp(`connect to .* from`)) + + if forwarder == "rootlessport" { + Expect(output).To(MatchRegexp(`connect to .* from .*` + subnetMatch)) + } else { + Expect(output).ToNot(MatchRegexp(`connect to .* from .*` + subnetMatch)) + } + + podmanTest.PodmanExitCleanly("rm", "-f", ctr) + podmanTest.PodmanExitCleanly("rm", "-f", netName) + }) + } + + It(fmt.Sprintf("podman run bridge network port cleanup on container stop with %s", forwarder), func() { + configurePortForwarder(forwarder) + netName := createNetworkName("cleanup") + podmanTest.PodmanExitCleanly("network", "create", netName) + defer podmanTest.removeNetwork(netName) + + port := GetPort() + podmanTest.PodmanExitCleanly( + "run", "-d", + "--name", "cleanup-ctr", + "--network", netName, + "-p", fmt.Sprintf("127.0.0.1:%d:80", port), + NGINX_IMAGE, + ) + + testPortConnection(port) + podmanTest.PodmanExitCleanly("rm", "-f", "cleanup-ctr") + + podmanTest.PodmanExitCleanly( + "run", "-d", + "--name", "cleanup-ctr2", + "--network", netName, + "-p", fmt.Sprintf("127.0.0.1:%d:80", port), + NGINX_IMAGE, + ) + testPortConnection(port) + + podmanTest.PodmanExitCleanly("rm", "-f", "cleanup-ctr2") + }) + + It(fmt.Sprintf("podman run bridge dual-stack network IPv4 and IPv6 port forwarding with %s", forwarder), func() { + SkipIfNotRootless("netavark does not support IPv6 port forwarding") + configurePortForwarder(forwarder) + + netName := createNetworkName("dual-stack") + podmanTest.PodmanExitCleanly("network", "create", "--ipv6", + "--subnet", "fd00:42::/64", "--subnet", "172.42.0.0/24", netName) + defer podmanTest.removeNetwork(netName) + + port6 := GetPort() + ctr6 := podmanTest.startNCContainer( + "c-ipv6", port6, + "--network", netName, + "-p", fmt.Sprintf("%d:%d", port6, port6), + ) + msg6 := RandomString(20) + sendMessageToAddr(fmt.Sprintf("[::1]:%d", port6), msg6) + podmanTest.WaitForContainerLog(ctr6, msg6) + + port4 := GetPort() + ctr4 := podmanTest.startNCContainer( + "c-ipv4", port4, + "--network", netName, + "-p", fmt.Sprintf("%d:%d", port4, port4), + ) + msg4 := RandomString(20) + sendMessageToAddr(fmt.Sprintf("127.0.0.1:%d", port4), msg4) + podmanTest.WaitForContainerLog(ctr4, msg4) + }) + } + + It("podman run pasta network preserves source IP", func() { + SkipIfNotRootless("pasta network mode is only supported rootless") + port := GetPort() + ctrName := podmanTest.startNCContainer( + "srcip-pasta-ctr", port, + "--net=pasta", + "-p", fmt.Sprintf("%d:%d", port, port), + ) + + msg := RandomString(20) + sendMessageToAddr(fmt.Sprintf("127.0.0.1:%d", port), msg) + podmanTest.WaitForContainerLog(ctrName, msg) + + logs := podmanTest.PodmanExitCleanly("logs", ctrName) + output := logs.OutputToString() + // With --net=pasta, pasta handles port forwarding directly without + // a bridge or netavark. The source IP is the host address (not a + // bridge gateway), confirming pasta's native source preservation. + Expect(output).To(MatchRegexp(`connect to .* from`)) + Expect(output).ToNot(MatchRegexp(`connect to .* from .*127\.0\.0\.`)) + + podmanTest.PodmanExitCleanly("rm", "-f", "srcip-pasta-ctr") + }) + It("Rootless podman run with --net=bridge works and connects to default network", func() { // This is harmless when run as root, so we'll just let it run. ctrName := "testctr" @@ -1465,3 +1661,22 @@ options ndots:1 }) } }) + +// sendMessageToAddr sends a message to the given tcp address (host:port). +func sendMessageToAddr(addr string, message string) { + GinkgoHelper() + conn, err := net.DialTimeout("tcp", addr, 5*time.Second) + Expect(err).ToNot(HaveOccurred(), "should connect to %s", addr) + + tcpConn := conn.(*net.TCPConn) + _, err = tcpConn.Write([]byte(message)) + Expect(err).ToNot(HaveOccurred()) + + err = tcpConn.CloseWrite() + Expect(err).ToNot(HaveOccurred()) + + err = tcpConn.SetReadDeadline(time.Now().Add(5 * time.Second)) + Expect(err).ToNot(HaveOccurred()) + _, _ = io.Copy(io.Discard, tcpConn) + tcpConn.Close() +} diff --git a/test/system/500-networking.bats b/test/system/500-networking.bats index 97e7e6275a..a0aa3a6d55 100644 --- a/test/system/500-networking.bats +++ b/test/system/500-networking.bats @@ -300,9 +300,21 @@ function run_pod_etc_hosts_test(){ run_podman 1 network rm $mynetname } -# CANNOT BE PARALLELIZED due to nft commands -@test "podman network reload" { - skip_if_remote "podman network reload does not have remote support" +# _test_network_reload: run network reload test with the given port forwarder +# $1: port forwarder name ("rootlessport" or "pasta") +function _test_network_reload() { + local forwarder="$1" + + # Set up pasta forwarder config if requested + unset CONTAINERS_CONF_OVERRIDE + if [[ "$forwarder" == "pasta" ]]; then + local conffile=$PODMAN_TMPDIR/pasta-forwarder.conf + cat >$conffile </dev/null || skip "pesto not available" + _test_network_reload pasta } # bats test_tags=ci:parallel @@ -470,9 +499,21 @@ function run_pod_etc_hosts_test(){ run_podman network rm -t 0 -f $netname } -# Test for https://github.com/containers/podman/issues/10052 -# bats test_tags=ci:parallel -@test "podman network connect/disconnect with port forwarding" { +# _test_network_connect_disconnect: Test for https://github.com/containers/podman/issues/10052 +# $1: port forwarder name ("rootlessport" or "pasta") +function _test_network_connect_disconnect() { + local forwarder="$1" + + unset CONTAINERS_CONF_OVERRIDE + if [[ "$forwarder" == "pasta" ]]; then + local conffile=$PODMAN_TMPDIR/pasta-forwarder.conf + cat >$conffile </dev/null || skip "pesto not available" + _test_network_connect_disconnect pasta +} + +# _test_network_after_restart: run network restart test with the given port forwarder +# $1: port forwarder name ("rootlessport" or "pasta") +function _test_network_after_restart() { + local forwarder="$1" + + unset CONTAINERS_CONF_OVERRIDE + if [[ "$forwarder" == "pasta" ]]; then + local conffile=$PODMAN_TMPDIR/pasta-forwarder.conf + cat >$conffile </dev/null || skip "pesto not available" + _test_network_after_restart pasta } # FIXME: random_rfc1918_subnet is not parallel-safe From baf8fa6223db7fddb9707da6dddfffd3f21bc6fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Rod=C3=A1k?= Date: Mon, 11 May 2026 20:17:35 +0200 Subject: [PATCH 3/3] Document pasta forwarding mode for rootless bridge networks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan Rodák --- docs/source/markdown/options/network.md | 2 ++ docs/source/markdown/options/publish.md | 8 ++++++++ docs/source/markdown/podman-info.1.md | 2 ++ docs/tutorials/basic_networking.md | 8 ++++++++ rootless.md | 1 + 5 files changed, 21 insertions(+) diff --git a/docs/source/markdown/options/network.md b/docs/source/markdown/options/network.md index 9234289da3..3a33d9253c 100644 --- a/docs/source/markdown/options/network.md +++ b/docs/source/markdown/options/network.md @@ -35,6 +35,8 @@ Valid _mode_ values are: Any other options will be passed through to netavark without validation. This can be useful to pass arguments to netavark plugins. + For rootless bridge networks, port forwarding uses `rootlessport` by default. Setting `rootless_port_forwarder="pasta"` in the `[network]` section of **[containers.conf(5)](https://github.com/containers/container-libs/blob/main/common/docs/containers.conf.5.md)** switches to pasta's kernel-level forwarding (via `pesto`), which preserves the original client source IP address inside the container. This option is experimental and its behavior is subject to change. + For example, to set a static IPv4 address and a static mac address, use `--network bridge:ip=10.88.0.10,mac=44:33:22:11:00:99`. - _\_**[:OPTIONS,...]**: Connect to a user-defined network; this is the network name or ID from a network created by **[podman network create](podman-network-create.1.md)**. It is possible to specify the same options described under the bridge mode above. Use the **--network** option multiple times to specify additional networks. \ diff --git a/docs/source/markdown/options/publish.md b/docs/source/markdown/options/publish.md index 07cc360f35..493e886088 100644 --- a/docs/source/markdown/options/publish.md +++ b/docs/source/markdown/options/publish.md @@ -28,3 +28,11 @@ Use **podman port** to see the actual mapping: `podman port $CONTAINER $CONTAINE Port publishing is only supported for containers utilizing their own network namespace through `bridge` networks, or the `pasta` network mode. + +For rootless bridge networks, port forwarding uses `rootlessport` by default, which +is a userspace proxy that does not preserve client source IPs. Setting +`rootless_port_forwarder="pasta"` in the `[network]` section of +**[containers.conf(5)](https://github.com/containers/container-libs/blob/main/common/docs/containers.conf.5.md)** +switches to pasta's kernel-level forwarding via `pesto`, preserving the original +client IP address inside the container. This option is experimental and its +behavior is subject to change. diff --git a/docs/source/markdown/podman-info.1.md b/docs/source/markdown/podman-info.1.md index f9a784b2c6..cb7151d940 100644 --- a/docs/source/markdown/podman-info.1.md +++ b/docs/source/markdown/podman-info.1.md @@ -96,6 +96,7 @@ host: spec: 1.0.0 +SYSTEMD +SELINUX +APPARMOR +CAP +SECCOMP +EBPF +CRIU +YAJL os: linux + rootlessPortForwarder: rootlessport pasta: executable: /usr/bin/passt package: passt-0^20221116.gace074c-1.fc34.x86_64 @@ -237,6 +238,7 @@ $ podman info --format json "version": "crun version 1.0\ncommit: 139dc6971e2f1d931af520188763e984d6cdfbf8\nspec: 1.0.0\n+SYSTEMD +SELINUX +APPARMOR +CAP +SECCOMP +EBPF +CRIU +YAJL" }, "os": "linux", + "rootlessPortForwarder": "rootlessport", "remoteSocket": { "path": "/run/user/3267/podman/podman.sock" }, diff --git a/docs/tutorials/basic_networking.md b/docs/tutorials/basic_networking.md index 151e2db184..112fecfdc6 100644 --- a/docs/tutorials/basic_networking.md +++ b/docs/tutorials/basic_networking.md @@ -80,6 +80,14 @@ The user experience of rootless netavark is very akin to a rootful netavark, exc there is no default network configuration provided. You simply need to create a network, and the one will be created as a bridge network. +By default, rootless bridge networks use `rootlessport` for port forwarding, which +is a userspace proxy that does not preserve client source IPs. To preserve source +IPs, set `rootless_port_forwarder="pasta"` in the `[network]` section of +`containers.conf`. This uses `pesto` to configure pasta's kernel-level forwarding, +so containers see the real client IP address instead of the bridge gateway. +This option is experimental and its behavior is subject to change. +It requires a version of passt (`>= passt-0^20260507.g1afd4ed`) that ships the `pesto` binary. + ``` $ podman network create ``` diff --git a/rootless.md b/rootless.md index e8970c56bd..ab07d56563 100644 --- a/rootless.md +++ b/rootless.md @@ -8,6 +8,7 @@ The following list categorizes the known issues and irregularities with running * A proxy server, kernel firewall rule, or redirection tool such as [redir](https://github.com/troglobit/redir) may be used to redirect traffic from a privileged port to an unprivileged one (where a podman pod is bound) in a server scenario - where a user has access to the root account (or setuid on the binary would be an acceptable risk), but wants to run the containers as an unprivileged user for enhanced security and for a limited number of pre-known ports. * As of Podman 5.0, pasta is the default networking tool. Since pasta copies the IP address of the main interface, connections to that IP from containers do not work. This means that unless you have more than one interface, inter-container connections cannot be made without explicitly passing a pasta network configuration, either in `containers.conf` or at runtime. * If you previously had port forwards (ex. via `-p 80:80`) that other containers could access, you can use the solution (setting pasta options with `10.0.2.x` IPs) posted [here](https://blog.podman.io/2024/03/podman-5-0-breaking-changes-in-detail/). +* Rootless bridge networks use `rootlessport` for port forwarding by default, which is a userspace proxy that does not preserve client source IPs. Setting `rootless_port_forwarder="pasta"` in the `[network]` section of `containers.conf` switches to pasta's kernel-level forwarding (via `pesto`), preserving the original client IP. This option is experimental and its behavior is subject to change. It requires a version of passt (`>= passt-0^20260507.g1afd4ed`) that ships the `pesto` binary. * If /etc/subuid and /etc/subgid are not set up for a user, then podman commands can easily fail * Some identity providers (e.g. FreeIPA) have integrated subuid/subgid support, but many have not.