Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions common/docs/containers.conf.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,15 @@ default_subnet_pools = [
Configure which rootless network program to use by default. Valid options are
`slirp4netns` and `pasta` (default).

**rootless_port_forwarder**="rootlessport"

Select the port forwarding mechanism for rootless bridge networks.
Valid options are `rootlessport` (default) and `pasta`.
`rootlessport` uses a userspace TCP/UDP proxy.
`pasta` uses pasta's control socket to add port forwarding rules via kernel splice,
which preserves the original source IP address inside the container.
The `pasta` option is **experimental** and subject to change.

**network_config_dir**="/etc/containers/networks"

Path to the directory where network configuration files are located.
Expand Down
22 changes: 18 additions & 4 deletions common/libnetwork/internal/rootlessnetns/netns_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <socketPath>" to enable this control channel.
pestoSocketFile = "pasta.sock"

tmpfs = "tmpfs"
none = "none"
resolvConfName = "resolv.conf"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
37 changes: 22 additions & 15 deletions common/libnetwork/netavark/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a change for this PR, but it would be nice to have a follow-up that puts these in alpha order.

}

return n, nil
Expand Down
13 changes: 13 additions & 0 deletions common/libnetwork/netavark/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)),
Expand Down
127 changes: 127 additions & 0 deletions common/libnetwork/pasta/pesto_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Pesto client for dynamic port forwarding on a running pasta instance.
Comment thread
Honny1 marked this conversation as resolved.
//
// 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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can socketPath == ""

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it cannot be. It should be checked by the caller. Should I add a check to be sure?


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
}
Loading
Loading