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
13 changes: 13 additions & 0 deletions common/libnetwork/internal/rootlessnetns/netns_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,19 @@ func (n *Netns) setupPasta(nsPath string) error {
extraOpts = append(extraOpts, "-c", n.getPath(pestoSocketFile))
}

// For the rootless netns, hard-code IPv6 --map-guest-addr, --address, and
// --gateway so pasta uses routable addresses for inbound IPv6 forwarding
// (instead of link-local which is not routable across bridges).
// The v4 --map-guest-addr must also be listed here because providing any
// --map-guest-addr suppresses the default one added by createPastaArgs.
// See: https://bugs.passt.top/show_bug.cgi?id=217
extraOpts = append(extraOpts,
"--map-guest-addr", pasta.MapGuestAddrIpv4,
"--map-guest-addr", pasta.MapGuestAddrIpv6,
"--address", pasta.GuestAddrIpv6,
"--gateway", pasta.GatewayIpv6,
)

pastaOpts := pasta.SetupOptions{
Config: n.config,
Netns: nsPath,
Expand Down
10 changes: 7 additions & 3 deletions common/libnetwork/netavark/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,13 @@ type netavarkNetwork struct {
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.
// containers.conf. When set to config.RootlessPortForwarderPasta, pesto
// handles port forwarding directly and netavark DNAT rules are skipped.
rootlessPortForwarder string

// config is the containers.conf config, needed for FindHelperBinary
// when invoking pesto for dynamic port forwarding.
config *config.Config

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.

I missed this is the first round of review but so far I tried to keep the full config out of the network interface as we lookup the netavark path once at the beginning and then store it as string instead.

I guess the is not strict reason to not have it so I am fine to add this here but it would allow us some cleanup/removal of fields above in theory. (anyhow lets not make this PR more complicated I guess, we can do that as some cleanup in the future)

}

type InitConfig struct {
Expand Down Expand Up @@ -167,6 +170,7 @@ func NewNetworkInterface(conf *InitConfig) (types.ContainerNetwork, error) {
syslog: conf.Syslog,
rootlessNetns: netns,
rootlessPortForwarder: conf.Config.Network.RootlessPortForwarder,
config: conf.Config,
}

return n, nil
Expand Down
152 changes: 140 additions & 12 deletions common/libnetwork/netavark/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"

"github.com/sirupsen/logrus"
"go.podman.io/common/libnetwork/pasta"
"go.podman.io/common/libnetwork/types"
"go.podman.io/common/pkg/config"
)
Expand Down Expand Up @@ -143,9 +144,107 @@ func (n *netavarkNetwork) Setup(namespacePath string, options types.SetupOptions
return nil, fmt.Errorf("unexpected netavark result length, want (%d), got (%d) networks", len(options.Networks), len(result))
}

if n.rootlessPortForwarder == config.RootlessPortForwarderPasta && n.networkRootless && len(options.NetworkOptions.PortMappings) > 0 {
// If NetworkStatus was already populated by the caller, pesto setup
// already happened on a prior network connect — skip to avoid duplicate rules.
if options.NetworkOptions.NetworkStatus != nil {
return result, nil
}
opts := options.NetworkOptions
opts.NetworkStatus = result
if len(opts.NetworkOrder) == 0 {
opts.NetworkOrder = make([]string, 0, len(opts.Networks))
for _, net := range opts.Networks {
opts.NetworkOrder = append(opts.NetworkOrder, net.Name)
}
}
if err := n.pestoSetup(opts); err != nil {
return nil, err
}
}

return result, err
}

// pestoSetup publishes port mappings via pesto with target address mapping.
// PestoAddPorts is idempotent so this is safe to call on every Setup.
func (n *netavarkNetwork) pestoSetup(opts types.NetworkOptions) error {
if n.rootlessNetns == nil {
return nil
}

ipv4, _, ipv6, _ := firstIPsFromStatus(opts.NetworkStatus, opts.NetworkOrder)
if ipv4 == "" && ipv6 == "" {
return nil
}
return pasta.PestoAddPorts(n.config, n.PestoSocketPath(), opts.PortMappings, ipv4, ipv6)
}

// pestoTeardown handles pesto port mapping removal on network disconnect.
// It derives the active forwarding IPs from the caller-provided NetworkStatus
// (the podman db state) rather than maintaining a separate state file.
// - If NetworkStatus is empty: nothing to do.
// - If this is the last network: delete all mappings.
// - If the disconnected network supplied the active IP: delete old mappings,
// pick a replacement IP from the remaining networks, and re-add.
// - Otherwise: no port changes needed.
func (n *netavarkNetwork) pestoTeardown(opts types.NetworkOptions) error {
if n.rootlessNetns == nil {
return nil
}

if len(opts.NetworkStatus) == 0 {
return nil
}

// Determine current active IPs from the full set of connected networks,
// respecting connection-time ordering from NetworkOrder.
activeIPv4, activeIPv4Net, activeIPv6, activeIPv6Net := firstIPsFromStatus(opts.NetworkStatus, opts.NetworkOrder)

// Build the set of networks being disconnected and the remaining status.
remaining := make(map[string]types.StatusBlock, len(opts.NetworkStatus))
disconnectedNets := make(map[string]struct{}, len(opts.Networks))
for _, namedNet := range opts.Networks {
disconnectedNets[namedNet.Name] = struct{}{}
}
for name, sb := range opts.NetworkStatus {
if _, removed := disconnectedNets[name]; !removed {
remaining[name] = sb
}
}

// Last network: remove all port mappings.
if len(remaining) == 0 {
return pasta.PestoDeletePorts(n.config, n.PestoSocketPath(), opts.PortMappings,
activeIPv4, activeIPv6)
}

// Check whether any active IP came from a network being disconnected.
_, v4Lost := disconnectedNets[activeIPv4Net]
v4Lost = v4Lost && activeIPv4Net != ""
_, v6Lost := disconnectedNets[activeIPv6Net]
v6Lost = v6Lost && activeIPv6Net != ""

if !v4Lost && !v6Lost {
return nil
}

// Remap: delete old mappings, pick new IPs from remaining networks, re-add.
if err := pasta.PestoDeletePorts(n.config, n.PestoSocketPath(), opts.PortMappings,
activeIPv4, activeIPv6); err != nil {
return fmt.Errorf("deleting port mappings for remap: %w", err)
}

newIPv4, _, newIPv6, _ := firstIPsFromStatus(remaining, opts.NetworkOrder)
if newIPv4 != "" || newIPv6 != "" {
if err := pasta.PestoAddPorts(n.config, n.PestoSocketPath(), opts.PortMappings,
newIPv4, newIPv6); err != nil {
return fmt.Errorf("re-adding port mappings after remap: %w", err)
}
}
return nil
}

// Teardown will teardown the container network namespace.
func (n *netavarkNetwork) Teardown(namespacePath string, options types.TeardownOptions) error {
n.lock.Lock()
Expand All @@ -163,6 +262,19 @@ func (n *netavarkNetwork) Teardown(namespacePath string, options types.TeardownO
logrus.Error(err)
}

if n.rootlessPortForwarder == config.RootlessPortForwarderPasta && n.networkRootless && len(options.NetworkOptions.PortMappings) > 0 {
opts := options.NetworkOptions
if len(opts.NetworkOrder) == 0 {
opts.NetworkOrder = make([]string, 0, len(opts.Networks))
for _, net := range opts.Networks {
opts.NetworkOrder = append(opts.NetworkOrder, net.Name)
}
}
if err := n.pestoTeardown(opts); err != nil {
logrus.Errorf("pesto: %v", err)
}
}

netavarkOpts, needPlugin, err := n.convertNetOpts(options.NetworkOptions)
if err != nil {
return fmt.Errorf("failed to convert net opts: %w", err)
Expand Down Expand Up @@ -206,18 +318,6 @@ 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 Expand Up @@ -266,3 +366,31 @@ func (n *netavarkNetwork) PestoSocketPath() string {
}
return n.rootlessNetns.PestoSocketPath()
}

// firstIPsFromStatus picks the first IPv4 and IPv6 container addresses from a
// set of network StatusBlocks, iterating in the order given by networkOrder
// (connection time). Returns the IP strings and the owning network name.
func firstIPsFromStatus(status map[string]types.StatusBlock, networkOrder []string) (ipv4, ipv4Net, ipv6, ipv6Net string) {
for _, name := range networkOrder {
sb, ok := status[name]
if !ok {
continue
}
for _, netInt := range sb.Interfaces {
for _, netAddr := range netInt.Subnets {
ip := netAddr.IPNet.IP
if ip.To4() != nil && ipv4 == "" {
ipv4 = ip.String()
ipv4Net = name
} else if ip.To4() == nil && ipv6 == "" {
ipv6 = ip.String()
ipv6Net = name
}
}
}
if ipv4 != "" && ipv6 != "" {
break
}
}
return
}
74 changes: 38 additions & 36 deletions common/libnetwork/pasta/pasta_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,26 @@ const (
// mapGuestAddrIpv4 static ip used as forwarder address inside the netns to reach the host,
// given this is a "link local" ip it should be very unlikely that it causes conflicts.
mapGuestAddrIpv4 = "169.254.1.2"

// mapGuestAddrIpv6 static ip used as IPv6 forwarder address inside the netns to reach the host.
mapGuestAddrIpv6 = "fc00::2"

// gatewayIpv6 is the IPv6 default gateway for the guest. pasta uses this as the source
// address for inbound IPv6 forwarding. Must differ from mapGuestAddrIpv6 to avoid
// --map-guest-addr intercepting reply traffic.
// See: https://bugs.passt.top/show_bug.cgi?id=217
gatewayIpv6 = "fc00::1"

// guestAddrIpv6 is assigned to the guest interface so the IPv6 gateway is reachable.
guestAddrIpv6 = "fc00::3"
)

// Exported IPv6 address constants for use by the rootless netns setup.
const (
MapGuestAddrIpv4 = mapGuestAddrIpv4
MapGuestAddrIpv6 = mapGuestAddrIpv6
GatewayIpv6 = gatewayIpv6
GuestAddrIpv6 = guestAddrIpv6

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.

I suppose you might as well just export the variable without a second reassignment but that is just a minor style thing not worth blocking on.

)

type SetupOptions struct {
Expand Down Expand Up @@ -67,39 +87,24 @@ func Setup(opts *SetupOptions) (*SetupResult, error) {

logrus.Debugf("pasta arguments: %s", strings.Join(cmdArgs, " "))

for {
// pasta forks once ready, and quits once we delete the target namespace
out, err := exec.Command(path, cmdArgs...).CombinedOutput()
if err != nil {
exitErr := &exec.ExitError{}
if errors.As(err, &exitErr) {
// special backwards compat check, --map-guest-addr was added in pasta version 20240814 so we
// cannot hard require it yet. Once we are confident that the update is most distros we can remove it.
if exitErr.ExitCode() == 1 &&
strings.Contains(string(out), "unrecognized option '"+mapGuestAddrOpt) &&
len(mapGuestAddrIPs) == 1 && mapGuestAddrIPs[0] == mapGuestAddrIpv4 {
// we did add the default --map-guest-addr option, if users set something different we want
// to get to the error below. We have to unset mapGuestAddrIPs here to avoid a infinite loop.
mapGuestAddrIPs = nil
// Trim off last two args which are --map-guest-addr 169.254.1.2.
cmdArgs = cmdArgs[:len(cmdArgs)-2]
continue
}
return nil, fmt.Errorf("pasta failed with exit code %d:\n%s",
exitErr.ExitCode(), string(out))
}
return nil, fmt.Errorf("failed to start pasta: %w", err)
// pasta forks once ready, and quits once we delete the target namespace
out, err := exec.Command(path, cmdArgs...).CombinedOutput()
if err != nil {
exitErr := &exec.ExitError{}
if errors.As(err, &exitErr) {
return nil, fmt.Errorf("pasta failed with exit code %d:\n%s",
exitErr.ExitCode(), string(out))
}
return nil, fmt.Errorf("failed to start pasta: %w", err)
}

if len(out) > 0 {
// TODO: This should be warning but as of August 2024 pasta still prints
// things with --quiet that we do not care about. In podman CI I still see
// "Couldn't get any nameserver address" so until this is fixed we cannot
// enable it. For now info is fine and we can bump it up later, it is only a
// nice to have.
logrus.Infof("pasta logged warnings: %q", strings.TrimSpace(string(out)))
}
break
if len(out) > 0 {
// TODO: This should be warning but as of August 2024 pasta still prints
// things with --quiet that we do not care about. In podman CI I still see
// "Couldn't get any nameserver address" so until this is fixed we cannot
// enable it. For now info is fine and we can bump it up later, it is only a
// nice to have.
logrus.Infof("pasta logged warnings: %q", strings.TrimSpace(string(out)))
}

var ipv4, ipv6 bool
Expand Down Expand Up @@ -265,15 +270,12 @@ func createPastaArgs(opts *SetupOptions) ([]string, []string, []string, error) {
cmdArgs = append(cmdArgs, "--quiet")
}

cmdArgs = append(cmdArgs, "--netns", opts.Netns)

// do this as last arg so we can easily trim them off in the error case when we have an older version
if len(mapGuestAddrIPs) == 0 {
// the user did not request custom --map-guest-addr so add our own so that we can use this
// for our own host.containers.internal host entry.
cmdArgs = append(cmdArgs, mapGuestAddrOpt, mapGuestAddrIpv4)
mapGuestAddrIPs = append(mapGuestAddrIPs, mapGuestAddrIpv4)
}

cmdArgs = append(cmdArgs, "--netns", opts.Netns)

return cmdArgs, dnsForwardIPs, mapGuestAddrIPs, nil
}
Loading
Loading