Skip to content

Commit e598657

Browse files
committed
Add pasta-based port forwarding for rootless bridge networks
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: #8193 Fixes: https://redhat.atlassian.net/browse/RUN-3587 Signed-off-by: Jan Rodák <hony.com@seznam.cz>
1 parent 0ff6ac8 commit e598657

10 files changed

Lines changed: 517 additions & 108 deletions

libpod/define/info.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,11 @@ type HostInfo struct {
5353
// RemoteSocket returns the UNIX domain socket the Podman service is listening on
5454
RemoteSocket *RemoteSocket `json:"remoteSocket,omitempty"`
5555
// RootlessNetworkCmd returns the default rootless network command (pasta)
56-
RootlessNetworkCmd string `json:"rootlessNetworkCmd"`
57-
RuntimeInfo map[string]any `json:"runtimeInfo,omitempty"`
56+
RootlessNetworkCmd string `json:"rootlessNetworkCmd"`
57+
// RootlessPortForwarder returns the port forwarding mechanism for rootless
58+
// bridge networks: "rootlessport" (default) or "pasta" (experimental)
59+
RootlessPortForwarder string `json:"rootlessPortForwarder"`
60+
RuntimeInfo map[string]any `json:"runtimeInfo,omitempty"`
5861
// ServiceIsRemote is true when the podman/libpod service is remote to the client
5962
ServiceIsRemote bool `json:"serviceIsRemote"`
6063
Security SecurityInfo `json:"security"`

libpod/info.go

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -108,26 +108,27 @@ func (r *Runtime) hostInfo() (*define.HostInfo, error) {
108108
}
109109

110110
info := define.HostInfo{
111-
Arch: runtime.GOARCH,
112-
BuildahVersion: buildah.Version,
113-
DatabaseBackend: r.state.Name(),
114-
Linkmode: linkmode.Linkmode(),
115-
CPUs: runtime.NumCPU(),
116-
CPUUtilization: cpuUtil,
117-
Distribution: hostDistributionInfo,
118-
LogDriver: r.config.Containers.LogDriver,
119-
EventLogger: r.eventer.String(),
120-
FreeLocks: locksFree,
121-
Hostname: host,
122-
Kernel: kv,
123-
MemFree: mi.MemFree,
124-
MemTotal: mi.MemTotal,
125-
NetworkBackend: r.config.Network.NetworkBackend,
126-
NetworkBackendInfo: r.network.NetworkInfo(),
127-
OS: runtime.GOOS,
128-
RootlessNetworkCmd: r.config.Network.DefaultRootlessNetworkCmd,
129-
SwapFree: mi.SwapFree,
130-
SwapTotal: mi.SwapTotal,
111+
Arch: runtime.GOARCH,
112+
BuildahVersion: buildah.Version,
113+
DatabaseBackend: r.state.Name(),
114+
Linkmode: linkmode.Linkmode(),
115+
CPUs: runtime.NumCPU(),
116+
CPUUtilization: cpuUtil,
117+
Distribution: hostDistributionInfo,
118+
LogDriver: r.config.Containers.LogDriver,
119+
EventLogger: r.eventer.String(),
120+
FreeLocks: locksFree,
121+
Hostname: host,
122+
Kernel: kv,
123+
MemFree: mi.MemFree,
124+
MemTotal: mi.MemTotal,
125+
NetworkBackend: r.config.Network.NetworkBackend,
126+
NetworkBackendInfo: r.network.NetworkInfo(),
127+
OS: runtime.GOOS,
128+
RootlessNetworkCmd: r.config.Network.DefaultRootlessNetworkCmd,
129+
RootlessPortForwarder: r.config.Network.RootlessPortForwarder,
130+
SwapFree: mi.SwapFree,
131+
SwapTotal: mi.SwapTotal,
131132
}
132133
platform := parse.DefaultPlatform()
133134
pArr := strings.Split(platform, "/")

libpod/networking_common.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,27 @@ func (r *Runtime) teardownNetwork(ctr *Container) error {
110110
return err
111111
}
112112

113-
if !ctr.config.NetMode.IsPasta() && len(networks) > 0 {
114-
netOpts := ctr.getNetworkOptions(networks)
115-
return r.teardownNetworkBackend(ctr.state.NetNS, netOpts)
113+
if len(networks) == 0 {
114+
return nil
116115
}
117-
return nil
116+
117+
// --net=pasta: per-container pasta cleans up when it exits, nothing to tear down.
118+
if ctr.config.NetMode.IsPasta() {
119+
return nil
120+
}
121+
122+
// Pasta forwarding mode: remove port forwarding rules (via pesto) before
123+
// netavark tears down bridge/nftables so pasta stops forwarding first.
124+
// Rootlessport mode: no explicit teardown needed (exits with conmon).
125+
if rootless.IsRootless() && ctr.config.NetMode.IsBridge() && len(ctr.config.PortMappings) > 0 &&
126+
r.config.Network.RootlessPortForwarder == config.RootlessPortForwarderPasta {
127+
if err := r.teardownRootlessPortMappingViaPesto(ctr); err != nil {
128+
logrus.Warnf("pesto port cleanup failed for container %s: %v", ctr.ID(), err)
129+
}
130+
}
131+
132+
netOpts := ctr.getNetworkOptions(networks)
133+
return r.teardownNetworkBackend(ctr.state.NetNS, netOpts)
118134
}
119135

120136
// isBridgeNetMode checks if the given network mode is bridge.
@@ -439,7 +455,7 @@ func (c *Container) NetworkDisconnect(nameOrID, netName string, _ bool) error {
439455

440456
// Reload ports when there are still connected networks, maybe we removed the network interface with the child ip.
441457
// Reloading without connected networks does not make sense, so we can skip this step.
442-
if rootless.IsRootless() && len(networkStatus) > 0 {
458+
if rootless.IsRootless() && c.runtime.config.Network.RootlessPortForwarder == config.RootlessPortForwarderRootlessport && len(networkStatus) > 0 {
443459
if err := c.reloadRootlessRLKPortMapping(); err != nil {
444460
return err
445461
}
@@ -595,7 +611,7 @@ func (c *Container) NetworkConnect(nameOrID, netName string, netOpts types.PerNe
595611

596612
// The first network needs a port reload to set the correct child ip for the rootlessport process.
597613
// Adding a second network does not require a port reload because the child ip is still valid.
598-
if rootless.IsRootless() && len(networks) == 0 {
614+
if rootless.IsRootless() && c.runtime.config.Network.RootlessPortForwarder == config.RootlessPortForwarderRootlessport && len(networks) == 0 {
599615
if err := c.reloadRootlessRLKPortMapping(); err != nil {
600616
return err
601617
}

libpod/networking_freebsd.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,7 @@ func (c *Container) inspectJoinedNetworkNS(_ string) (q types.StatusBlock, retEr
224224
func (c *Container) reloadRootlessRLKPortMapping() error {
225225
return errors.New("unsupported (*Container).reloadRootlessRLKPortMapping")
226226
}
227+
228+
func (r *Runtime) teardownRootlessPortMappingViaPesto(_ *Container) error {
229+
return errors.New("unsupported teardownRootlessPortMappingViaPesto on FreeBSD")
230+
}

libpod/networking_linux.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/sirupsen/logrus"
1212
"github.com/vishvananda/netlink"
1313
"go.podman.io/common/libnetwork/types"
14+
"go.podman.io/common/pkg/config"
1415
"go.podman.io/common/pkg/netns"
1516
"go.podman.io/podman/v6/libpod/define"
1617
"go.podman.io/podman/v6/pkg/rootless"
@@ -59,15 +60,19 @@ func (r *Runtime) configureNetNS(ctr *Container, ctrNS string) (status map[strin
5960
}
6061
}()
6162

62-
// set up rootless port forwarder when rootless with ports and the network status is empty,
63-
// if this is called from network reload the network status will not be empty and we should
64-
// not set up port because they are still active
65-
if rootless.IsRootless() && len(ctr.config.PortMappings) > 0 && ctr.getNetworkStatus() == nil {
66-
// set up port forwarder for rootless netns
67-
// make sure to fix this in container.handleRestartPolicy() as well
68-
// Important we have to call this after r.setUpNetwork() so that
69-
// we can use the proper netStatus
70-
err = r.setupRootlessPortMappingViaRLK(ctr, ctrNS, netStatus)
63+
// Set up port forwarding for rootless bridge networks.
64+
if rootless.IsRootless() && len(ctr.config.PortMappings) > 0 {
65+
switch r.config.Network.RootlessPortForwarder {
66+
case config.RootlessPortForwarderPasta:
67+
err = r.setupRootlessPortMappingViaPesto(ctr)
68+
case config.RootlessPortForwarderRootlessport, "":
69+
if ctr.getNetworkStatus() == nil {
70+
err = r.setupRootlessPortMappingViaRLK(ctr, ctrNS, netStatus)
71+
}
72+
default:
73+
err = fmt.Errorf("invalid rootless_port_forwarder value %q, must be %q or %q",
74+
r.config.Network.RootlessPortForwarder, config.RootlessPortForwarderRootlessport, config.RootlessPortForwarderPasta)
75+
}
7176
}
7277
return netStatus, err
7378
}

libpod/networking_pesto_linux.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//go:build !remote
2+
3+
// Pesto integration for rootless bridge network port forwarding.
4+
//
5+
// A shared pasta instance in the rootless netns (-c pasta.sock) handles
6+
// host-side port forwarding. On container start/stop, pesto incrementally
7+
// adds or deletes port forwarding rules for that container. Pasta forwards
8+
// via kernel splice (localhost) or TAP (external), preserving source IPs.
9+
// The container sees the real client's address instead of a proxy or bridge
10+
// gateway address.
11+
//
12+
// Container start:
13+
// - netavark sets up bridge + DNAT
14+
// - pesto --add: adds this container's ports to pasta
15+
//
16+
// Container stop:
17+
// - pesto --delete: removes this container's ports from pasta
18+
// - netavark tears down bridge/DNAT
19+
20+
package libpod
21+
22+
import (
23+
"go.podman.io/common/libnetwork/pasta"
24+
)
25+
26+
func (r *Runtime) pestoSocketPath() string {
27+
info, err := r.network.RootlessNetnsInfo()
28+
if err != nil || info == nil {
29+
return ""
30+
}
31+
return info.PestoSocketPath
32+
}
33+
34+
// setupRootlessPortMappingViaPesto adds this container's port forwarding
35+
// rules to the shared pasta instance.
36+
func (r *Runtime) setupRootlessPortMappingViaPesto(ctr *Container) error {
37+
ports := ctr.convertPortMappings()
38+
if len(ports) == 0 {
39+
return nil
40+
}
41+
return pasta.PestoAddPorts(r.config, r.pestoSocketPath(), ports)
42+
}
43+
44+
// teardownRootlessPortMappingViaPesto removes this container's port
45+
// forwarding rules from the shared pasta instance.
46+
func (r *Runtime) teardownRootlessPortMappingViaPesto(ctr *Container) error {
47+
ports := ctr.convertPortMappings()
48+
if len(ports) == 0 {
49+
return nil
50+
}
51+
return pasta.PestoDeletePorts(r.config, r.pestoSocketPath(), ports)
52+
}

libpod/oci_conmon_common.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,8 +1188,11 @@ func (r *ConmonOCIRuntime) createOCIContainer(ctr *Container, restoreOptions *Co
11881188
// process cannot use them.
11891189
cmd.ExtraFiles = append(cmd.ExtraFiles, ports...)
11901190

1191-
// For rootless port forwarding, create sync pipe and leak write end to conmon
1192-
if rootless.IsRootless() && len(ctr.config.PortMappings) > 0 {
1191+
// For rootless port forwarding via rootlessport, create sync pipe and
1192+
// leak write end to conmon. Pasta forwarding mode does not use
1193+
// rootlessport, so no pipe is needed.
1194+
if rootless.IsRootless() && len(ctr.config.PortMappings) > 0 &&
1195+
ctr.runtime.config.Network.RootlessPortForwarder == config.RootlessPortForwarderRootlessport {
11931196
ctr.rootlessPortSyncR, ctr.rootlessPortSyncW, err = os.Pipe()
11941197
if err != nil {
11951198
return 0, fmt.Errorf("failed to create rootless port sync pipe: %w", err)

test/e2e/common_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1629,6 +1629,35 @@ func testPortConnection(port int) {
16291629
Expect(err).ToNot(HaveOccurred())
16301630
}
16311631

1632+
// startNCContainer starts a detached container running nc (netcat) listening
1633+
// on the given port, waits for it to be ready, and returns the container name.
1634+
//
1635+
//nolint:unused,nolintlint // only called from linux-only test files, unused on freebsd
1636+
func (p *PodmanTestIntegration) startNCContainer(name string, listenPort int, extraArgs ...string) string {
1637+
GinkgoHelper()
1638+
portStr := strconv.Itoa(listenPort)
1639+
args := append([]string{"run", "-d", "--name", name}, extraArgs...)
1640+
args = append(args, ALPINE, "sh", "-c", "nc -l -n -v -p "+portStr+" 2>&1")
1641+
p.PodmanExitCleanly(args...)
1642+
p.WaitForContainerLog(name, "listening")
1643+
return name
1644+
}
1645+
1646+
// WaitForContainerLog polls container logs until the given substring appears
1647+
// in either stdout or stderr. Fails the test if not found within the timeout.
1648+
func (p *PodmanTestIntegration) WaitForContainerLog(ctrName string, substr string) {
1649+
GinkgoHelper()
1650+
for range 10 {
1651+
logs := p.Podman([]string{"logs", ctrName})
1652+
logs.WaitWithDefaultTimeout()
1653+
if strings.Contains(logs.ErrorToString(), substr) || strings.Contains(logs.OutputToString(), substr) {
1654+
return
1655+
}
1656+
time.Sleep(500 * time.Millisecond)
1657+
}
1658+
Fail(fmt.Sprintf("timed out waiting for %q in logs of container %s", substr, ctrName))
1659+
}
1660+
16321661
func createNetworkName(name string) string {
16331662
return name + stringid.GenerateRandomID()[:10]
16341663
}

0 commit comments

Comments
 (0)