mirror of
https://github.com/containers/podman.git
synced 2026-05-24 16:40:44 -04:00
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: https://github.com/containers/podman/issues/8193 Fixes: https://redhat.atlassian.net/browse/RUN-3587 Signed-off-by: Jan Rodák <hony.com@seznam.cz>
This commit is contained in:
@@ -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"`
|
||||
|
||||
@@ -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, "/")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
52
libpod/networking_pesto_linux.go
Normal file
52
libpod/networking_pesto_linux.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 <<EOCONF
|
||||
[network]
|
||||
rootless_port_forwarder="pasta"
|
||||
EOCONF
|
||||
export CONTAINERS_CONF_OVERRIDE=$conffile
|
||||
fi
|
||||
|
||||
random_1=$(random_string 30)
|
||||
HOST_PORT=$(random_free_port)
|
||||
@@ -409,6 +421,23 @@ function run_pod_etc_hosts_test(){
|
||||
is "$output" "Error: default network $netname cannot be removed" "Remove default network"
|
||||
|
||||
run_podman network rm -t 0 -f $netname2
|
||||
|
||||
unset CONTAINERS_CONF_OVERRIDE
|
||||
}
|
||||
|
||||
# CANNOT BE PARALLELIZED due to nft commands
|
||||
# Uses rootlessport forwarder for rootless, no forwarder for rootful.
|
||||
@test "podman network reload" {
|
||||
skip_if_remote "podman network reload does not have remote support"
|
||||
_test_network_reload rootlessport
|
||||
}
|
||||
|
||||
# bats test_tags=ci:parallel
|
||||
@test "podman network reload - pasta forwarder" {
|
||||
skip_if_remote "podman network reload does not have remote support"
|
||||
is_rootless || skip "pasta port forwarder requires rootless"
|
||||
type -P pesto >/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 <<EOCONF
|
||||
[network]
|
||||
rootless_port_forwarder="pasta"
|
||||
EOCONF
|
||||
export CONTAINERS_CONF_OVERRIDE=$conffile
|
||||
fi
|
||||
|
||||
random_1=$(random_string 30)
|
||||
HOST_PORT=$(random_free_port)
|
||||
SERVER=http://127.0.0.1:$HOST_PORT
|
||||
@@ -592,10 +633,38 @@ function run_pod_etc_hosts_test(){
|
||||
# clean up
|
||||
run_podman rm -t 0 -f $cid $background_cid
|
||||
run_podman network rm -t 0 -f $netname $netname2
|
||||
|
||||
unset CONTAINERS_CONF_OVERRIDE
|
||||
}
|
||||
|
||||
# bats test_tags=ci:parallel
|
||||
@test "podman network after restart" {
|
||||
# Uses rootlessport forwarder for rootless, no forwarder for rootful.
|
||||
@test "podman network connect/disconnect with port forwarding" {
|
||||
_test_network_connect_disconnect rootlessport
|
||||
}
|
||||
|
||||
# bats test_tags=ci:parallel
|
||||
@test "podman network connect/disconnect with port forwarding - pasta forwarder" {
|
||||
is_rootless || skip "pasta port forwarder requires rootless"
|
||||
type -P pesto >/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 <<EOCONF
|
||||
[network]
|
||||
rootless_port_forwarder="pasta"
|
||||
EOCONF
|
||||
export CONTAINERS_CONF_OVERRIDE=$conffile
|
||||
fi
|
||||
|
||||
random_1=$(random_string 30)
|
||||
|
||||
HOST_PORT=$(random_free_port)
|
||||
@@ -609,76 +678,88 @@ function run_pod_etc_hosts_test(){
|
||||
run_podman network create $netname
|
||||
is "$output" "$netname" "output of 'network create'"
|
||||
|
||||
local -a networks=("$netname")
|
||||
for network in "${networks[@]}"; do
|
||||
# Start container with the restart always policy
|
||||
local cname=c-$(safename)
|
||||
run_podman run -d --name $cname -p "$HOST_PORT:80" \
|
||||
--restart always \
|
||||
--network $network \
|
||||
-v $INDEX1:/var/www/index.txt:Z \
|
||||
-w /var/www \
|
||||
$IMAGE /bin/busybox-extras httpd -f -p 80
|
||||
cid=$output
|
||||
# Start container with the restart always policy
|
||||
local cname=c-$(safename)
|
||||
run_podman run -d --name $cname -p "$HOST_PORT:80" \
|
||||
--restart always \
|
||||
--network $netname \
|
||||
-v $INDEX1:/var/www/index.txt:Z \
|
||||
-w /var/www \
|
||||
$IMAGE /bin/busybox-extras httpd -f -p 80
|
||||
cid=$output
|
||||
|
||||
# Tests #10310: podman will restart network on container restart
|
||||
run_podman container inspect --format "{{.State.Pid}}" $cid
|
||||
pid=$output
|
||||
# Tests #10310: podman will restart network on container restart
|
||||
run_podman container inspect --format "{{.State.Pid}}" $cid
|
||||
pid=$output
|
||||
|
||||
# Kill the process; podman restart policy will bring up a new container.
|
||||
# -9 is crucial: busybox httpd ignores all other signals.
|
||||
kill -9 $pid
|
||||
# Wait for process to exit
|
||||
retries=30
|
||||
while kill -0 $pid; do
|
||||
sleep 0.5
|
||||
retries=$((retries - 1))
|
||||
assert $retries -gt 0 "Process $pid (container $cid) refused to die"
|
||||
done
|
||||
|
||||
# Wait for container to restart
|
||||
retries=20
|
||||
while :;do
|
||||
run_podman container inspect --format "{{.State.Pid}}" $cid
|
||||
# pid is 0 as long as the container is not running
|
||||
if [[ $output -ne 0 ]]; then
|
||||
assert "$output" != "$pid" \
|
||||
"This should never happen! Restarted container has same PID as killed one!"
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
retries=$((retries - 1))
|
||||
assert $retries -gt 0 "Timed out waiting for container to restart"
|
||||
done
|
||||
|
||||
# Verify http contents again: curl from localhost
|
||||
# Use retry since it can take a moment until the new container is ready
|
||||
local curlcmd="curl --retry 2 --retry-connrefused -s $SERVER/index.txt"
|
||||
echo "$_LOG_PROMPT $curlcmd"
|
||||
run $curlcmd
|
||||
echo "$output"
|
||||
assert "$status" == 0 "curl exit status"
|
||||
assert "$output" = "$random_1" "curl $SERVER/index.txt after auto restart"
|
||||
|
||||
run_podman 0+w restart -t1 $cid
|
||||
if ! is_remote; then
|
||||
require_warning "StopSignal SIGTERM failed to stop container .* in 1 seconds, resorting to SIGKILL" \
|
||||
"podman restart issues warning"
|
||||
fi
|
||||
|
||||
# Verify http contents again: curl from localhost
|
||||
# Use retry since it can take a moment until the new container is ready
|
||||
echo "$_LOG_PROMPT $curlcmd"
|
||||
run $curlcmd
|
||||
echo "$output"
|
||||
assert "$status" == 0 "curl exit status"
|
||||
assert "$output" = "$random_1" "curl $SERVER/index.txt after podman restart"
|
||||
|
||||
run_podman rm -t 0 -f $cid
|
||||
# Kill the process; podman restart policy will bring up a new container.
|
||||
# -9 is crucial: busybox httpd ignores all other signals.
|
||||
kill -9 $pid
|
||||
# Wait for process to exit
|
||||
retries=30
|
||||
while kill -0 $pid; do
|
||||
sleep 0.5
|
||||
retries=$((retries - 1))
|
||||
assert $retries -gt 0 "Process $pid (container $cid) refused to die"
|
||||
done
|
||||
|
||||
# Wait for container to restart
|
||||
retries=20
|
||||
while :;do
|
||||
run_podman container inspect --format "{{.State.Pid}}" $cid
|
||||
# pid is 0 as long as the container is not running
|
||||
if [[ $output -ne 0 ]]; then
|
||||
assert "$output" != "$pid" \
|
||||
"This should never happen! Restarted container has same PID as killed one!"
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
retries=$((retries - 1))
|
||||
assert $retries -gt 0 "Timed out waiting for container to restart"
|
||||
done
|
||||
|
||||
# Verify http contents again: curl from localhost
|
||||
# Use retry since it can take a moment until the new container is ready
|
||||
local curlcmd="curl --retry 2 --retry-connrefused -s $SERVER/index.txt"
|
||||
echo "$_LOG_PROMPT $curlcmd"
|
||||
run $curlcmd
|
||||
echo "$output"
|
||||
assert "$status" == 0 "curl exit status"
|
||||
assert "$output" = "$random_1" "curl $SERVER/index.txt after auto restart"
|
||||
|
||||
run_podman 0+w restart -t1 $cid
|
||||
if ! is_remote; then
|
||||
require_warning "StopSignal SIGTERM failed to stop container .* in 1 seconds, resorting to SIGKILL" \
|
||||
"podman restart issues warning"
|
||||
fi
|
||||
|
||||
# Verify http contents again: curl from localhost
|
||||
# Use retry since it can take a moment until the new container is ready
|
||||
echo "$_LOG_PROMPT $curlcmd"
|
||||
run $curlcmd
|
||||
echo "$output"
|
||||
assert "$status" == 0 "curl exit status"
|
||||
assert "$output" = "$random_1" "curl $SERVER/index.txt after podman restart"
|
||||
|
||||
run_podman rm -t 0 -f $cid
|
||||
|
||||
# Clean up network
|
||||
run_podman network rm -t 0 -f $netname
|
||||
|
||||
unset CONTAINERS_CONF_OVERRIDE
|
||||
}
|
||||
|
||||
# bats test_tags=ci:parallel
|
||||
# Uses rootlessport forwarder for rootless, no forwarder for rootful.
|
||||
@test "podman network after restart" {
|
||||
_test_network_after_restart rootlessport
|
||||
}
|
||||
|
||||
# bats test_tags=ci:parallel
|
||||
@test "podman network after restart - pasta forwarder" {
|
||||
is_rootless || skip "pasta port forwarder requires rootless"
|
||||
type -P pesto >/dev/null || skip "pesto not available"
|
||||
_test_network_after_restart pasta
|
||||
}
|
||||
|
||||
# FIXME: random_rfc1918_subnet is not parallel-safe
|
||||
|
||||
Reference in New Issue
Block a user