Skip to content
Open
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
15 changes: 11 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 @@ -197,11 +202,12 @@ func (n *Netns) cleanup() error {

func (n *Netns) setupPasta(nsPath string) error {
pidPath := n.getPath(rootlessNetNsConnPidFile)
socketPath := n.getPath(pestoSocketFile)

pastaOpts := pasta.SetupOptions{
Config: n.config,
Netns: nsPath,
ExtraOptions: []string{"--pid", pidPath},
ExtraOptions: []string{"--pid", pidPath, "-c", socketPath},
}
res, err := pasta.Setup(&pastaOpts)
if err != nil {
Expand Down Expand Up @@ -235,9 +241,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
120 changes: 120 additions & 0 deletions common/libnetwork/pasta/pesto_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// 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.
//
// Limitations:
// - IPv4 only (netavark DNAT is IPv4; IPv6 bindings cause RST)
// - 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 := portMappingsToPestoArgs(ports)
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.
//
// Pesto only forwards traffic from the host into the rootless netns. This
// does NOT perform DNAT to the container. Netavark handles that inside the
// netns. Therefore each mapping uses HostPort as both source and destination
// (e.g. "-t 0.0.0.0/8080") so traffic arrives at the port netavark expects.
func portMappingsToPestoArgs(ports []types.PortMapping) []string {
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.

does this func make sense if ports is empty? i think you will get something in args that is not expected?

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.

I think this makes sense. If no ports are specified, it will set -t none -u none, meaning no ports will be forwarded.

var args []string

for _, p := range ports {
// Netavark's DNAT rules use "dnat ip to" which only matches IPv4.
// Restrict pesto to the correct address family so pasta doesn't
// accept IPv6 connections that can't be DNAT'd (which causes RST).
addr := "0.0.0.0/"
if p.HostIP != "" {
if strings.Contains(p.HostIP, ":") {
addr = "[" + p.HostIP + "]/"
} else {
addr = p.HostIP + "/"
}
}

for protocol := range strings.SplitSeq(p.Protocol, ",") {
var flag string
switch protocol {
case "tcp":
flag = "-t"
case "udp":
flag = "-u"
default:
logrus.Warnf("pesto: unsupported protocol %q, skipping", protocol)
continue
}

portRange := p.Range
if portRange == 0 {
portRange = 1
}

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
}
156 changes: 156 additions & 0 deletions common/libnetwork/pasta/pesto_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package pasta

import (
"testing"

"github.com/stretchr/testify/assert"
"go.podman.io/common/libnetwork/types"
)

func Test_portMappingsToPestoArgs(t *testing.T) {
tests := []struct {
name string
ports []types.PortMapping
want []string
}{
{
name: "no ports returns nil",
ports: nil,
want: nil,
},
{
name: "empty slice same as nil",
ports: []types.PortMapping{},
want: nil,
},
{
name: "single tcp port defaults to 0.0.0.0",
ports: []types.PortMapping{
{HostPort: 8080, ContainerPort: 80, Protocol: "tcp", Range: 1},
},
want: []string{"-t", "0.0.0.0/8080"},
},
{
name: "single udp port",
ports: []types.PortMapping{
{HostPort: 53, ContainerPort: 53, Protocol: "udp", Range: 1},
},
want: []string{"-u", "0.0.0.0/53"},
},
{
name: "tcp and udp port",
ports: []types.PortMapping{
{HostPort: 80, ContainerPort: 80, Protocol: "tcp", Range: 1},
{HostPort: 53, ContainerPort: 53, Protocol: "udp", Range: 1},
},
want: []string{"-t", "0.0.0.0/80", "-u", "0.0.0.0/53"},
},
{
name: "dual protocol on single mapping",
ports: []types.PortMapping{
{HostPort: 80, ContainerPort: 80, Protocol: "tcp,udp", Range: 1},
},
want: []string{"-t", "0.0.0.0/80", "-u", "0.0.0.0/80"},
},
{
name: "port range expands to host port range",
ports: []types.PortMapping{
{HostPort: 8000, ContainerPort: 80, Protocol: "tcp", Range: 5},
},
want: []string{"-t", "0.0.0.0/8000-8004"},
},
{
name: "range of zero treated as single port",
ports: []types.PortMapping{
{HostPort: 80, ContainerPort: 80, Protocol: "tcp", Range: 0},
},
want: []string{"-t", "0.0.0.0/80"},
},
{
name: "range of two",
ports: []types.PortMapping{
{HostPort: 3000, ContainerPort: 3000, Protocol: "tcp", Range: 2},
},
want: []string{"-t", "0.0.0.0/3000-3001"},
},
{
name: "explicit IPv4 host IP",
ports: []types.PortMapping{
{HostIP: "127.0.0.1", HostPort: 443, ContainerPort: 443, Protocol: "tcp", Range: 1},
},
want: []string{"-t", "127.0.0.1/443"},
},
{
name: "IPv6 host IP gets brackets",
ports: []types.PortMapping{
{HostIP: "::1", HostPort: 8080, ContainerPort: 80, Protocol: "tcp", Range: 1},
},
want: []string{"-t", "[::1]/8080"},
},
{
name: "full-form IPv6 host IP",
ports: []types.PortMapping{
{HostIP: "fd00::1", HostPort: 80, ContainerPort: 80, Protocol: "udp", Range: 1},
},
want: []string{"-u", "[fd00::1]/80"},
},
{
name: "multiple tcp ports",
ports: []types.PortMapping{
{HostPort: 80, ContainerPort: 80, Protocol: "tcp", Range: 1},
{HostPort: 443, ContainerPort: 443, Protocol: "tcp", Range: 1},
},
want: []string{"-t", "0.0.0.0/80", "-t", "0.0.0.0/443"},
},
{
name: "unsupported protocol is skipped",
ports: []types.PortMapping{
{HostPort: 80, ContainerPort: 80, Protocol: "sctp", Range: 1},
},
want: nil,
},
{
name: "unsupported protocol mixed with valid",
ports: []types.PortMapping{
{HostPort: 80, ContainerPort: 80, Protocol: "tcp", Range: 1},
{HostPort: 90, ContainerPort: 90, Protocol: "sctp", Range: 1},
},
want: []string{"-t", "0.0.0.0/80"},
},
{
name: "explicit host IP on udp",
ports: []types.PortMapping{
{HostIP: "10.0.0.1", HostPort: 3000, ContainerPort: 3000, Protocol: "udp", Range: 1},
},
want: []string{"-u", "10.0.0.1/3000"},
},
{
name: "container port does not appear in args",
ports: []types.PortMapping{
{HostPort: 9090, ContainerPort: 3000, Protocol: "tcp", Range: 1},
},
want: []string{"-t", "0.0.0.0/9090"},
},
{
name: "host IP with range",
ports: []types.PortMapping{
{HostIP: "10.0.0.1", HostPort: 3000, ContainerPort: 3000, Protocol: "udp", Range: 3},
},
want: []string{"-u", "10.0.0.1/3000-3002"},
},
{
name: "range with dual protocol",
ports: []types.PortMapping{
{HostPort: 5000, ContainerPort: 5000, Protocol: "tcp,udp", Range: 3},
},
want: []string{"-t", "0.0.0.0/5000-5002", "-u", "0.0.0.0/5000-5002"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := portMappingsToPestoArgs(tt.ports)
assert.Equal(t, tt.want, got)
})
}
}
3 changes: 3 additions & 0 deletions common/libnetwork/types/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading