diff --git a/common/libnetwork/internal/rootlessnetns/netns_linux.go b/common/libnetwork/internal/rootlessnetns/netns_linux.go index 750cb3810f..d8e77f35f1 100644 --- a/common/libnetwork/internal/rootlessnetns/netns_linux.go +++ b/common/libnetwork/internal/rootlessnetns/netns_linux.go @@ -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, diff --git a/common/libnetwork/netavark/network.go b/common/libnetwork/netavark/network.go index c06d0486f2..aa55ab2008 100644 --- a/common/libnetwork/netavark/network.go +++ b/common/libnetwork/netavark/network.go @@ -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 } type InitConfig struct { @@ -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 diff --git a/common/libnetwork/netavark/run.go b/common/libnetwork/netavark/run.go index db5e18e911..c667ecbe60 100644 --- a/common/libnetwork/netavark/run.go +++ b/common/libnetwork/netavark/run.go @@ -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" ) @@ -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() @@ -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) @@ -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)), @@ -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 +} diff --git a/common/libnetwork/pasta/pasta_linux.go b/common/libnetwork/pasta/pasta_linux.go index 91bbb16534..8618b6e4e3 100644 --- a/common/libnetwork/pasta/pasta_linux.go +++ b/common/libnetwork/pasta/pasta_linux.go @@ -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 ) type SetupOptions struct { @@ -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 @@ -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 } diff --git a/common/libnetwork/pasta/pasta_linux_test.go b/common/libnetwork/pasta/pasta_linux_test.go index 96d7c568b9..f08d62e8c4 100644 --- a/common/libnetwork/pasta/pasta_linux_test.go +++ b/common/libnetwork/pasta/pasta_linux_test.go @@ -36,11 +36,12 @@ func Test_createPastaArgs(t *testing.T) { ), wantArgs: []string{ "--config-net", "--dns-forward", dnsForwardIpv4, "-t", "none", "-u", "none", - "-T", "none", "-U", "none", "--no-map-gw", "--quiet", "--netns", "netns123", + "-T", "none", "-U", "none", "--no-map-gw", "--quiet", mapGuestAddrOpt, mapGuestAddrIpv4, + "--netns", "netns123", }, wantDNSForward: []string{dnsForwardIpv4}, - wantMapGuestAddr: []string{mapGuestAddrIpv4}, + wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6}, }, { name: "basic port", @@ -51,11 +52,12 @@ func Test_createPastaArgs(t *testing.T) { ), wantArgs: []string{ "--config-net", "-t", "80-80:80-80", "--dns-forward", dnsForwardIpv4, "-u", "none", - "-T", "none", "-U", "none", "--no-map-gw", "--quiet", "--netns", "netns123", + "-T", "none", "-U", "none", "--no-map-gw", "--quiet", mapGuestAddrOpt, mapGuestAddrIpv4, + "--netns", "netns123", }, wantDNSForward: []string{dnsForwardIpv4}, - wantMapGuestAddr: []string{mapGuestAddrIpv4}, + wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6}, }, { name: "port range", @@ -66,11 +68,12 @@ func Test_createPastaArgs(t *testing.T) { ), wantArgs: []string{ "--config-net", "-t", "80-82:80-82", "--dns-forward", dnsForwardIpv4, "-u", "none", - "-T", "none", "-U", "none", "--no-map-gw", "--quiet", "--netns", "netns123", + "-T", "none", "-U", "none", "--no-map-gw", "--quiet", mapGuestAddrOpt, mapGuestAddrIpv4, + "--netns", "netns123", }, wantDNSForward: []string{dnsForwardIpv4}, - wantMapGuestAddr: []string{mapGuestAddrIpv4}, + wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6}, }, { name: "different host and container port", @@ -81,11 +84,12 @@ func Test_createPastaArgs(t *testing.T) { ), wantArgs: []string{ "--config-net", "-t", "80-80:60-60", "--dns-forward", dnsForwardIpv4, "-u", "none", - "-T", "none", "-U", "none", "--no-map-gw", "--quiet", "--netns", "netns123", + "-T", "none", "-U", "none", "--no-map-gw", "--quiet", mapGuestAddrOpt, mapGuestAddrIpv4, + "--netns", "netns123", }, wantDNSForward: []string{dnsForwardIpv4}, - wantMapGuestAddr: []string{mapGuestAddrIpv4}, + wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6}, }, { name: "tcp and udp port", @@ -99,11 +103,12 @@ func Test_createPastaArgs(t *testing.T) { ), wantArgs: []string{ "--config-net", "-t", "80-80:60-60", "-u", "100-100:100-100", "--dns-forward", - dnsForwardIpv4, "-T", "none", "-U", "none", "--no-map-gw", "--quiet", "--netns", "netns123", + dnsForwardIpv4, "-T", "none", "-U", "none", "--no-map-gw", "--quiet", mapGuestAddrOpt, mapGuestAddrIpv4, + "--netns", "netns123", }, wantDNSForward: []string{dnsForwardIpv4}, - wantMapGuestAddr: []string{mapGuestAddrIpv4}, + wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6}, }, { name: "two tcp ports", @@ -117,11 +122,12 @@ func Test_createPastaArgs(t *testing.T) { ), wantArgs: []string{ "--config-net", "-t", "80-80:60-60", "-t", "100-100:100-100", "--dns-forward", - dnsForwardIpv4, "-u", "none", "-T", "none", "-U", "none", "--no-map-gw", "--quiet", "--netns", "netns123", + dnsForwardIpv4, "-u", "none", "-T", "none", "-U", "none", "--no-map-gw", "--quiet", mapGuestAddrOpt, mapGuestAddrIpv4, + "--netns", "netns123", }, wantDNSForward: []string{dnsForwardIpv4}, - wantMapGuestAddr: []string{mapGuestAddrIpv4}, + wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6}, }, { name: "invalid port", @@ -143,11 +149,12 @@ func Test_createPastaArgs(t *testing.T) { ), wantArgs: []string{ "--config-net", "-i", "eth0", "-n", "24", "--dns-forward", dnsForwardIpv4, - "-t", "none", "-u", "none", "-T", "none", "-U", "none", "--no-map-gw", "--quiet", "--netns", "netns123", + "-t", "none", "-u", "none", "-T", "none", "-U", "none", "--no-map-gw", "--quiet", mapGuestAddrOpt, mapGuestAddrIpv4, + "--netns", "netns123", }, wantDNSForward: []string{dnsForwardIpv4}, - wantMapGuestAddr: []string{mapGuestAddrIpv4}, + wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6}, }, { name: "config options before extra options", @@ -158,11 +165,12 @@ func Test_createPastaArgs(t *testing.T) { ), wantArgs: []string{ "--config-net", "-i", "eth0", "-n", "24", "--dns-forward", dnsForwardIpv4, - "-t", "none", "-u", "none", "-T", "none", "-U", "none", "--no-map-gw", "--quiet", "--netns", "netns123", + "-t", "none", "-u", "none", "-T", "none", "-U", "none", "--no-map-gw", "--quiet", mapGuestAddrOpt, mapGuestAddrIpv4, + "--netns", "netns123", }, wantDNSForward: []string{dnsForwardIpv4}, - wantMapGuestAddr: []string{mapGuestAddrIpv4}, + wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6}, }, { name: "-T option", @@ -173,11 +181,12 @@ func Test_createPastaArgs(t *testing.T) { ), wantArgs: []string{ "--config-net", "-T", "80", "--dns-forward", dnsForwardIpv4, - "-t", "none", "-u", "none", "-U", "none", "--no-map-gw", "--quiet", "--netns", "netns123", + "-t", "none", "-u", "none", "-U", "none", "--no-map-gw", "--quiet", mapGuestAddrOpt, mapGuestAddrIpv4, + "--netns", "netns123", }, wantDNSForward: []string{dnsForwardIpv4}, - wantMapGuestAddr: []string{mapGuestAddrIpv4}, + wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6}, }, { name: "--tcp-ns option", @@ -188,11 +197,12 @@ func Test_createPastaArgs(t *testing.T) { ), wantArgs: []string{ "--config-net", "--tcp-ns", "80", "--dns-forward", dnsForwardIpv4, - "-t", "none", "-u", "none", "-U", "none", "--no-map-gw", "--quiet", "--netns", "netns123", + "-t", "none", "-u", "none", "-U", "none", "--no-map-gw", "--quiet", mapGuestAddrOpt, mapGuestAddrIpv4, + "--netns", "netns123", }, wantDNSForward: []string{dnsForwardIpv4}, - wantMapGuestAddr: []string{mapGuestAddrIpv4}, + wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6}, }, { name: "--map-gw option", @@ -203,11 +213,12 @@ func Test_createPastaArgs(t *testing.T) { ), wantArgs: []string{ "--config-net", "--dns-forward", dnsForwardIpv4, "-t", "none", - "-u", "none", "-T", "none", "-U", "none", "--quiet", "--netns", "netns123", + "-u", "none", "-T", "none", "-U", "none", "--quiet", mapGuestAddrOpt, mapGuestAddrIpv4, + "--netns", "netns123", }, wantDNSForward: []string{dnsForwardIpv4}, - wantMapGuestAddr: []string{mapGuestAddrIpv4}, + wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6}, }, { // https://github.com/containers/podman/issues/22477 @@ -218,11 +229,12 @@ func Test_createPastaArgs(t *testing.T) { ), wantArgs: []string{ "--config-net", "-T", "80", "--dns-forward", dnsForwardIpv4, - "-t", "none", "-u", "none", "-U", "none", "--quiet", "--netns", "netns123", + "-t", "none", "-u", "none", "-U", "none", "--quiet", mapGuestAddrOpt, mapGuestAddrIpv4, + "--netns", "netns123", }, wantDNSForward: []string{dnsForwardIpv4}, - wantMapGuestAddr: []string{mapGuestAddrIpv4}, + wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6}, }, { name: "two --map-gw", @@ -233,11 +245,12 @@ func Test_createPastaArgs(t *testing.T) { ), wantArgs: []string{ "--config-net", "-T", "80", "--dns-forward", dnsForwardIpv4, - "-t", "none", "-u", "none", "-U", "none", "--quiet", "--netns", "netns123", + "-t", "none", "-u", "none", "-U", "none", "--quiet", mapGuestAddrOpt, mapGuestAddrIpv4, + "--netns", "netns123", }, wantDNSForward: []string{dnsForwardIpv4}, - wantMapGuestAddr: []string{mapGuestAddrIpv4}, + wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6}, }, { name: "--dns-forward option", @@ -248,11 +261,12 @@ func Test_createPastaArgs(t *testing.T) { ), wantArgs: []string{ "--config-net", "--dns-forward", "192.168.255.255", "-t", "none", - "-u", "none", "-T", "none", "-U", "none", "--no-map-gw", "--quiet", "--netns", "netns123", + "-u", "none", "-T", "none", "-U", "none", "--no-map-gw", "--quiet", mapGuestAddrOpt, mapGuestAddrIpv4, + "--netns", "netns123", }, wantDNSForward: []string{"192.168.255.255"}, - wantMapGuestAddr: []string{mapGuestAddrIpv4}, + wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6}, }, { name: "two --dns-forward options", @@ -263,11 +277,12 @@ func Test_createPastaArgs(t *testing.T) { ), wantArgs: []string{ "--config-net", "--dns-forward", "192.168.255.255", "--dns-forward", "::1", "-t", "none", - "-u", "none", "-T", "none", "-U", "none", "--no-map-gw", "--quiet", "--netns", "netns123", + "-u", "none", "-T", "none", "-U", "none", "--no-map-gw", "--quiet", mapGuestAddrOpt, mapGuestAddrIpv4, + "--netns", "netns123", }, wantDNSForward: []string{"192.168.255.255", "::1"}, - wantMapGuestAddr: []string{mapGuestAddrIpv4}, + wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6}, }, { name: "port and custom opt", @@ -278,11 +293,12 @@ func Test_createPastaArgs(t *testing.T) { ), wantArgs: []string{ "--config-net", "-i", "eth0", "-t", "80-80:80-80", "--dns-forward", dnsForwardIpv4, - "-u", "none", "-T", "none", "-U", "none", "--no-map-gw", "--quiet", "--netns", "netns123", + "-u", "none", "-T", "none", "-U", "none", "--no-map-gw", "--quiet", mapGuestAddrOpt, mapGuestAddrIpv4, + "--netns", "netns123", }, wantDNSForward: []string{dnsForwardIpv4}, - wantMapGuestAddr: []string{mapGuestAddrIpv4}, + wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6}, }, { name: "Add verbose logging", @@ -294,10 +310,12 @@ func Test_createPastaArgs(t *testing.T) { wantArgs: []string{ "--config-net", "--log-file=/tmp/log", "--trace", "--debug", "--dns-forward", dnsForwardIpv4, "-t", "none", "-u", "none", "-T", "none", "-U", "none", - "--no-map-gw", "--netns", "netns123", mapGuestAddrOpt, mapGuestAddrIpv4, + "--no-map-gw", + mapGuestAddrOpt, mapGuestAddrIpv4, + "--netns", "netns123", }, wantDNSForward: []string{dnsForwardIpv4}, - wantMapGuestAddr: []string{mapGuestAddrIpv4}, + wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6}, }, { name: "--map-guest-addr option", @@ -324,11 +342,38 @@ func Test_createPastaArgs(t *testing.T) { wantArgs: []string{ "--config-net", mapGuestAddrOpt, "192.168.255.255", mapGuestAddrOpt, "::1", dnsForwardOpt, dnsForwardIpv4, "-t", "none", "-u", "none", "-T", "none", - "-U", "none", "--no-map-gw", "--quiet", "--netns", "netns123", + "-U", "none", "--no-map-gw", "--quiet", + "--netns", "netns123", }, wantDNSForward: []string{dnsForwardIpv4}, wantMapGuestAddr: []string{"192.168.255.255", "::1"}, }, + { + name: "rootless netns extra options (v4+v6 map-guest-addr, address, gateway)", + input: makeSetupOptions( + nil, + []string{ + "--pid", "/run/pasta.pid", + mapGuestAddrOpt, mapGuestAddrIpv4, + mapGuestAddrOpt, mapGuestAddrIpv6, + "--address", guestAddrIpv6, + "--gateway", gatewayIpv6, + }, + nil, + ), + wantArgs: []string{ + "--config-net", "--pid", "/run/pasta.pid", + mapGuestAddrOpt, mapGuestAddrIpv4, + mapGuestAddrOpt, mapGuestAddrIpv6, + "--address", guestAddrIpv6, + "--gateway", gatewayIpv6, + dnsForwardOpt, dnsForwardIpv4, + "-t", "none", "-u", "none", "-T", "none", "-U", "none", "--no-map-gw", "--quiet", + "--netns", "netns123", + }, + wantDNSForward: []string{dnsForwardIpv4}, + wantMapGuestAddr: []string{mapGuestAddrIpv4, mapGuestAddrIpv6}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/common/libnetwork/pasta/pesto_freebsd.go b/common/libnetwork/pasta/pesto_freebsd.go new file mode 100644 index 0000000000..66994e1956 --- /dev/null +++ b/common/libnetwork/pasta/pesto_freebsd.go @@ -0,0 +1,18 @@ +package pasta + +import ( + "errors" + + "go.podman.io/common/libnetwork/types" + "go.podman.io/common/pkg/config" +) + +var errPestoNotSupported = errors.New("pesto is not supported on FreeBSD") + +func PestoAddPorts(_ *config.Config, _ string, _ []types.PortMapping, _, _ string) error { + return errPestoNotSupported +} + +func PestoDeletePorts(_ *config.Config, _ string, _ []types.PortMapping, _, _ string) error { + return errPestoNotSupported +} diff --git a/common/libnetwork/pasta/pesto_linux.go b/common/libnetwork/pasta/pesto_linux.go index bd8f4b6639..216f5ae3a6 100644 --- a/common/libnetwork/pasta/pesto_linux.go +++ b/common/libnetwork/pasta/pesto_linux.go @@ -4,16 +4,15 @@ // 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. +// Each mapping specifies both the host binding and container target +// address, so pasta forwards traffic directly to the correct +// container IP:ContainerPort. // // 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) +// - TCP and UDP only (unsupported protocols return an error) package pasta @@ -32,31 +31,35 @@ 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 { +// containerIPv4 and containerIPv6 are the container's addresses inside the +// network namespace; they are embedded in the target side of each mapping. +func PestoAddPorts(conf *config.Config, socketPath string, ports []types.PortMapping, containerIPv4, containerIPv6 string) 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") + return pestoModifyPorts(conf, socketPath, ports, "--add", containerIPv4, containerIPv6) } // PestoDeletePorts removes port forwarding rules from the running pasta // instance via -D/--delete. -func PestoDeletePorts(conf *config.Config, socketPath string, ports []types.PortMapping) error { +// containerIPv4 and containerIPv6 are the container's addresses inside the +// network namespace; they are embedded in the target side of each mapping. +func PestoDeletePorts(conf *config.Config, socketPath string, ports []types.PortMapping, containerIPv4, containerIPv6 string) error { if socketPath == "" { return nil } logrus.Debugf("pesto: deleting %d port mappings", len(ports)) - return pestoModifyPorts(conf, socketPath, ports, "--delete") + return pestoModifyPorts(conf, socketPath, ports, "--delete", containerIPv4, containerIPv6) } -func pestoModifyPorts(conf *config.Config, socketPath string, ports []types.PortMapping, mode string) error { +func pestoModifyPorts(conf *config.Config, socketPath string, ports []types.PortMapping, mode, containerIPv4, containerIPv6 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) + pestoArgs, err := portMappingsToPestoArgs(ports, containerIPv4, containerIPv6) if err != nil { return err } @@ -77,23 +80,42 @@ func pestoModifyPorts(conf *config.Config, socketPath string, ports []types.Port return nil } -// portMappingsToPestoArgs converts PortMappings into pesto CLI arguments. +// portMappingsToPestoArgs converts PortMappings into pesto CLI arguments +// using the target mapping syntax: -t hostIP/hostPort:containerIP/containerPort. // -// 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) { +// IPv6 host addresses are bracketed (e.g. [::]) to disambiguate colons +// from the mapping ":" separator; container addresses are never bracketed +// because they appear after the ":" where there is no ambiguity. +// +// When HostIP is empty, dual-stack bindings are created using 0.0.0.0 with +// containerIPv4, and (if containerIPv6 is non-empty) [::] with containerIPv6. +// When HostIP is set, the address-family-matched container IP is used. +func portMappingsToPestoArgs(ports []types.PortMapping, containerIPv4, containerIPv6 string) ([]string, error) { + type addrPair struct { + host, container string + } + var args []string for _, p := range ports { - var addrs []string + var pairs []addrPair + switch { case p.HostIP == "": - addrs = []string{"0.0.0.0/", "[::]/"} + if containerIPv4 != "" { + pairs = append(pairs, addrPair{"0.0.0.0", containerIPv4}) + } + if containerIPv6 != "" { + pairs = append(pairs, addrPair{"[::]", containerIPv6}) + } case strings.Contains(p.HostIP, ":"): - addrs = []string{"[" + p.HostIP + "]/"} + if containerIPv6 != "" { + pairs = append(pairs, addrPair{"[" + p.HostIP + "]", containerIPv6}) + } default: - addrs = []string{p.HostIP + "/"} + if containerIPv4 != "" { + pairs = append(pairs, addrPair{p.HostIP, containerIPv4}) + } } for protocol := range strings.SplitSeq(p.Protocol, ",") { @@ -112,12 +134,14 @@ func portMappingsToPestoArgs(ports []types.PortMapping) ([]string, error) { portRange = 1 } - for _, addr := range addrs { + for _, pair := range pairs { var arg string if portRange == 1 { - arg = fmt.Sprintf("%s%d", addr, p.HostPort) + arg = fmt.Sprintf("%s/%d:%s/%d", pair.host, p.HostPort, pair.container, p.ContainerPort) } else { - arg = fmt.Sprintf("%s%d-%d", addr, p.HostPort, p.HostPort+portRange-1) + arg = fmt.Sprintf("%s/%d-%d:%s/%d-%d", + pair.host, p.HostPort, p.HostPort+portRange-1, + pair.container, p.ContainerPort, p.ContainerPort+portRange-1) } args = append(args, flag, arg) } diff --git a/common/libnetwork/pasta/pesto_linux_test.go b/common/libnetwork/pasta/pesto_linux_test.go index 45700ded10..121d567891 100644 --- a/common/libnetwork/pasta/pesto_linux_test.go +++ b/common/libnetwork/pasta/pesto_linux_test.go @@ -9,10 +9,12 @@ import ( func Test_portMappingsToPestoArgs(t *testing.T) { tests := []struct { - name string - ports []types.PortMapping - want []string - wantErr string + name string + ports []types.PortMapping + containerIPv4 string + containerIPv6 string + want []string + wantErr string }{ { name: "no ports returns nil", @@ -29,14 +31,18 @@ func Test_portMappingsToPestoArgs(t *testing.T) { ports: []types.PortMapping{ {HostPort: 8080, ContainerPort: 80, Protocol: "tcp", Range: 1}, }, - want: []string{"-t", "0.0.0.0/8080", "-t", "[::]/8080"}, + containerIPv4: "10.0.0.2", + containerIPv6: "fd00::2", + want: []string{"-t", "0.0.0.0/8080:10.0.0.2/80", "-t", "[::]/8080:fd00::2/80"}, }, { name: "single udp port dual-stack", ports: []types.PortMapping{ {HostPort: 53, ContainerPort: 53, Protocol: "udp", Range: 1}, }, - want: []string{"-u", "0.0.0.0/53", "-u", "[::]/53"}, + containerIPv4: "10.0.0.2", + containerIPv6: "fd00::2", + want: []string{"-u", "0.0.0.0/53:10.0.0.2/53", "-u", "[::]/53:fd00::2/53"}, }, { name: "tcp and udp port dual-stack", @@ -44,56 +50,69 @@ func Test_portMappingsToPestoArgs(t *testing.T) { {HostPort: 80, ContainerPort: 80, Protocol: "tcp", Range: 1}, {HostPort: 53, ContainerPort: 53, Protocol: "udp", Range: 1}, }, - want: []string{"-t", "0.0.0.0/80", "-t", "[::]/80", "-u", "0.0.0.0/53", "-u", "[::]/53"}, + containerIPv4: "10.0.0.2", + containerIPv6: "fd00::2", + want: []string{"-t", "0.0.0.0/80:10.0.0.2/80", "-t", "[::]/80:fd00::2/80", "-u", "0.0.0.0/53:10.0.0.2/53", "-u", "[::]/53:fd00::2/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", "-t", "[::]/80", "-u", "0.0.0.0/80", "-u", "[::]/80"}, + containerIPv4: "10.0.0.2", + containerIPv6: "fd00::2", + want: []string{"-t", "0.0.0.0/80:10.0.0.2/80", "-t", "[::]/80:fd00::2/80", "-u", "0.0.0.0/80:10.0.0.2/80", "-u", "[::]/80:fd00::2/80"}, }, { - name: "port range expands to host port range", + name: "port range maps both host and container ranges", ports: []types.PortMapping{ {HostPort: 8000, ContainerPort: 80, Protocol: "tcp", Range: 5}, }, - want: []string{"-t", "0.0.0.0/8000-8004", "-t", "[::]/8000-8004"}, + containerIPv4: "10.0.0.2", + containerIPv6: "fd00::2", + want: []string{"-t", "0.0.0.0/8000-8004:10.0.0.2/80-84", "-t", "[::]/8000-8004:fd00::2/80-84"}, }, { 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", "-t", "[::]/80"}, + containerIPv4: "10.0.0.2", + containerIPv6: "fd00::2", + want: []string{"-t", "0.0.0.0/80:10.0.0.2/80", "-t", "[::]/80:fd00::2/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", "-t", "[::]/3000-3001"}, + containerIPv4: "10.0.0.2", + containerIPv6: "fd00::2", + want: []string{"-t", "0.0.0.0/3000-3001:10.0.0.2/3000-3001", "-t", "[::]/3000-3001:fd00::2/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"}, + containerIPv4: "10.0.0.2", + want: []string{"-t", "127.0.0.1/443:10.0.0.2/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"}, + containerIPv6: "fd00::2", + want: []string{"-t", "[::1]/8080:fd00::2/80"}, }, { 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"}, + containerIPv6: "fd00::2", + want: []string{"-u", "[fd00::1]/80:fd00::2/80"}, }, { name: "multiple tcp ports dual-stack", @@ -101,7 +120,9 @@ func Test_portMappingsToPestoArgs(t *testing.T) { {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", "[::]/80", "-t", "0.0.0.0/443", "-t", "[::]/443"}, + containerIPv4: "10.0.0.2", + containerIPv6: "fd00::2", + want: []string{"-t", "0.0.0.0/80:10.0.0.2/80", "-t", "[::]/80:fd00::2/80", "-t", "0.0.0.0/443:10.0.0.2/443", "-t", "[::]/443:fd00::2/443"}, }, { name: "unsupported protocol returns error", @@ -123,35 +144,42 @@ func Test_portMappingsToPestoArgs(t *testing.T) { ports: []types.PortMapping{ {HostIP: "10.0.0.1", HostPort: 3000, ContainerPort: 3000, Protocol: "udp", Range: 1}, }, - want: []string{"-u", "10.0.0.1/3000"}, + containerIPv4: "10.0.0.2", + want: []string{"-u", "10.0.0.1/3000:10.0.0.2/3000"}, }, { - name: "container port does not appear in args", + name: "host and container ports differ", ports: []types.PortMapping{ {HostPort: 9090, ContainerPort: 3000, Protocol: "tcp", Range: 1}, }, - want: []string{"-t", "0.0.0.0/9090", "-t", "[::]/9090"}, + containerIPv4: "10.0.0.2", + containerIPv6: "fd00::2", + want: []string{"-t", "0.0.0.0/9090:10.0.0.2/3000", "-t", "[::]/9090:fd00::2/3000"}, }, { 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"}, + containerIPv4: "10.0.0.2", + want: []string{"-u", "10.0.0.1/3000-3002:10.0.0.2/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", "-t", "[::]/5000-5002", "-u", "0.0.0.0/5000-5002", "-u", "[::]/5000-5002"}, + containerIPv4: "10.0.0.2", + containerIPv6: "fd00::2", + want: []string{"-t", "0.0.0.0/5000-5002:10.0.0.2/5000-5002", "-t", "[::]/5000-5002:fd00::2/5000-5002", "-u", "0.0.0.0/5000-5002:10.0.0.2/5000-5002", "-u", "[::]/5000-5002:fd00::2/5000-5002"}, }, { name: "IPv6 host IP with range", ports: []types.PortMapping{ {HostIP: "::1", HostPort: 5000, ContainerPort: 5000, Protocol: "tcp", Range: 4}, }, - want: []string{"-t", "[::1]/5000-5003"}, + containerIPv6: "fd00::2", + want: []string{"-t", "[::1]/5000-5003:fd00::2/5000-5003"}, }, { name: "mixed explicit and default host IPs", @@ -159,7 +187,9 @@ func Test_portMappingsToPestoArgs(t *testing.T) { {HostIP: "10.0.0.1", HostPort: 80, ContainerPort: 80, Protocol: "tcp", Range: 1}, {HostPort: 443, ContainerPort: 443, Protocol: "tcp", Range: 1}, }, - want: []string{"-t", "10.0.0.1/80", "-t", "0.0.0.0/443", "-t", "[::]/443"}, + containerIPv4: "10.0.0.2", + containerIPv6: "fd00::2", + want: []string{"-t", "10.0.0.1/80:10.0.0.2/80", "-t", "0.0.0.0/443:10.0.0.2/443", "-t", "[::]/443:fd00::2/443"}, }, { name: "triple protocol with unsupported in middle returns error", @@ -173,14 +203,16 @@ func Test_portMappingsToPestoArgs(t *testing.T) { ports: []types.PortMapping{ {HostIP: "192.168.1.1", HostPort: 80, ContainerPort: 80, Protocol: "tcp,udp", Range: 1}, }, - want: []string{"-t", "192.168.1.1/80", "-u", "192.168.1.1/80"}, + containerIPv4: "10.0.0.2", + want: []string{"-t", "192.168.1.1/80:10.0.0.2/80", "-u", "192.168.1.1/80:10.0.0.2/80"}, }, { name: "dual protocol with explicit IPv6", ports: []types.PortMapping{ {HostIP: "fd00::1", HostPort: 80, ContainerPort: 80, Protocol: "tcp,udp", Range: 1}, }, - want: []string{"-t", "[fd00::1]/80", "-u", "[fd00::1]/80"}, + containerIPv6: "fd00::2", + want: []string{"-t", "[fd00::1]/80:fd00::2/80", "-u", "[fd00::1]/80:fd00::2/80"}, }, { name: "all unsupported protocols returns error", @@ -189,11 +221,44 @@ func Test_portMappingsToPestoArgs(t *testing.T) { }, wantErr: "pesto: unsupported protocol sctp", }, + { + name: "IPv4 only when no IPv6 container address", + ports: []types.PortMapping{ + {HostPort: 8080, ContainerPort: 80, Protocol: "tcp", Range: 1}, + }, + containerIPv4: "10.0.0.2", + want: []string{"-t", "0.0.0.0/8080:10.0.0.2/80"}, + }, + { + name: "IPv6 host IP skipped when no container IPv6", + ports: []types.PortMapping{ + {HostIP: "::1", HostPort: 9999, ContainerPort: 9999, Protocol: "tcp", Range: 1}, + }, + containerIPv4: "10.0.0.2", + want: nil, + }, + { + name: "IPv4 host IP skipped when no container IPv4", + ports: []types.PortMapping{ + {HostIP: "127.0.0.1", HostPort: 9999, ContainerPort: 9999, Protocol: "tcp", Range: 1}, + }, + containerIPv6: "fd00::2", + want: nil, + }, + { + name: "different host and container port ranges", + ports: []types.PortMapping{ + {HostPort: 9000, ContainerPort: 3000, Protocol: "tcp", Range: 3}, + }, + containerIPv4: "10.0.0.2", + containerIPv6: "fd00::2", + want: []string{"-t", "0.0.0.0/9000-9002:10.0.0.2/3000-3002", "-t", "[::]/9000-9002:fd00::2/3000-3002"}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := portMappingsToPestoArgs(tt.ports) + got, err := portMappingsToPestoArgs(tt.ports, tt.containerIPv4, tt.containerIPv6) if tt.wantErr != "" { assert.EqualError(t, err, tt.wantErr) return diff --git a/common/libnetwork/types/network.go b/common/libnetwork/types/network.go index 21260f1059..30b34ecc78 100644 --- a/common/libnetwork/types/network.go +++ b/common/libnetwork/types/network.go @@ -313,6 +313,19 @@ type NetworkOptions struct { // Each NamedPerNetworkOptions must have a unique Name. // The list should contain at least one element. Networks []NamedPerNetworkOptions `json:"networks"` + // NetworkStatus contains the previously stored network results from the + // container's database, keyed by network name. Populated by the caller + // (e.g. podman) on network connect/disconnect so that pesto can determine + // which container IPs are already forwarded without maintaining a separate + // state file. Nil or empty on the very first Setup call. + // Not part of the netavark JSON format. + NetworkStatus map[string]StatusBlock `json:"-"` + // NetworkOrder lists network names in the order they were connected + // to the container (earliest first). Used by pesto to pick which + // container IP to forward traffic to: the first-connected network wins. + // Populated by the caller alongside NetworkStatus. + // Not part of the netavark JSON format. + NetworkOrder []string `json:"-"` // List of custom DNS server for podman's DNS resolver. // Priority order will be kept as defined by user in the configuration. DNSServers []string `json:"dns_servers,omitempty"`