diff --git a/libnetwork/drivers/bridge/bridge_linux.go b/libnetwork/drivers/bridge/bridge_linux.go index 05e8d103fd..350a5f7ef4 100644 --- a/libnetwork/drivers/bridge/bridge_linux.go +++ b/libnetwork/drivers/bridge/bridge_linux.go @@ -1448,7 +1448,7 @@ func (d *driver) ProgramExternalConnectivity(ctx context.Context, nid, eid strin // Program any required port mapping and store them in the endpoint if endpoint.extConnConfig != nil && endpoint.extConnConfig.PortBindings != nil { endpoint.portMapping, err = network.addPortMappings( - ctx, + log.WithLogger(ctx, log.G(ctx).WithFields(log.Fields{"nid": nid, "eid": eid})), endpoint.addr, endpoint.addrv6, endpoint.extConnConfig.PortBindings, diff --git a/libnetwork/drivers/bridge/port_mapping_linux.go b/libnetwork/drivers/bridge/port_mapping_linux.go index 34806c7a52..b1129e94b6 100644 --- a/libnetwork/drivers/bridge/port_mapping_linux.go +++ b/libnetwork/drivers/bridge/port_mapping_linux.go @@ -96,11 +96,6 @@ func (n *bridgeNetwork) addPortMappings( containerIPv6 = epAddrV6.IP } - disableNAT4, disableNAT6 := n.getNATDisabled() - if err := validatePortBindings(cfg, !disableNAT4, !disableNAT6, containerIPv6); err != nil { - return nil, err - } - bindings := make([]portBinding, 0, len(cfg)*2) defer func() { @@ -116,6 +111,7 @@ func (n *bridgeNetwork) addPortMappings( proxyPath := n.userlandProxyPath() pdc := n.getPortDriverClient() + disableNAT4, disableNAT6 := n.getNATDisabled() // toBind accumulates port bindings that should be allocated the same host port // (if required by NAT config). If the host address is unspecified, and defHostIP @@ -128,23 +124,36 @@ func (n *bridgeNetwork) addPortMappings( // bindings to collect, they're applied and toBind is reset. var toBind []portBindingReq for i, c := range sortedCfg { - if bindingIPv4, ok := configurePortBindingIPv4(pdc, disableNAT4, c, containerIPv4, defHostIP); ok { + if bindingIPv4, ok := configurePortBindingIPv4(ctx, pdc, disableNAT4, c, containerIPv4, defHostIP); ok { toBind = append(toBind, bindingIPv4) } // If the container has no IPv6 address, allow proxying host IPv6 traffic to it // by setting up the binding with the IPv4 interface if the userland proxy is enabled // This change was added to keep backward compatibility - // TODO(robmry) - this will silently ignore port bindings with an explicit IPv6 - // host address, when docker-proxy is disabled, and the container is IPv4-only. - // If there's no proxying and the container has no IPv6, should probably error if ... - // - the mapping's host address is IPv6, or - // - the mapping has no host address, but the default address is IPv6. containerIP := containerIPv6 - if proxyPath != "" && (containerIPv6 == nil) { - containerIP = containerIPv4 + if containerIPv6 == nil { + if proxyPath == "" { + // There's no way to map from host-IPv6 to container-IPv4 with the userland proxy + // disabled. + // If that is required, don't treat it as an error because, as networks are + // connected/disconnected, the container's gateway endpoint might change to a + // network where this config makes more sense. + if len(c.HostIP) > 0 && c.HostIP.To4() == nil { + log.G(ctx).WithFields(log.Fields{"mapping": c}).Info( + "Cannot map from IPv6 to an IPv4-only container because the userland proxy is disabled") + } + if len(c.HostIP) == 0 && defHostIP.To4() == nil { + log.G(ctx).WithFields(log.Fields{ + "mapping": c, + "default": defHostIP, + }).Info("Cannot map from default host binding address to an IPv4-only container because the userland proxy is disabled") + } + } else { + containerIP = containerIPv4 + } } - if bindingIPv6, ok := configurePortBindingIPv6(pdc, disableNAT6, c, containerIP, defHostIP); ok { + if bindingIPv6, ok := configurePortBindingIPv6(ctx, pdc, disableNAT6, c, containerIP, defHostIP); ok { toBind = append(toBind, bindingIPv6) } @@ -237,65 +246,6 @@ func (n *bridgeNetwork) addPortMappings( return bindings, nil } -// Limit the number of errors reported, because there may be a lot of port -// bindings (host port ranges are expanded by the CLI). -const validationErrLimit = 6 - -// validatePortBindings checks that, if NAT is disabled for all uses of a -// PortBinding, no HostPort, or non-zero HostIP, is specified - because they have -// no meaning. A zero HostIP is allowed, as it's used to determine the address -// family. -// -// The default binding IP is not considered, meaning that no error is raised if -// there is a default binding address that is not used but could have been. -// -// For example, the default is an IPv6 interface address, no HostIP is specified, -// and NAT6 is disabled; the default is ignored and no error will be raised. (Note -// that this example may be valid if the container has no IPv6 address, and -// docker-proxy is used to forward between the default IPv6 address and the -// container's IPv4. So, simply disallowing a non-zero IPv6 default when NAT6 -// is disabled for the network would be incorrect.) -func validatePortBindings(pbs []types.PortBinding, nat4, nat6 bool, cIPv6 net.IP) error { - var errs []error - for i := range pbs { - pb := &pbs[i] - disallowHostPort := false - if !nat4 && len(pb.HostIP) > 0 && pb.HostIP.To4() != nil && !pb.HostIP.Equal(net.IPv4zero) { - // There's no NAT4, so don't allow a nonzero IPv4 host address in the mapping. The port will - // be accessible via any host interface. - errs = append(errs, - fmt.Errorf("NAT is disabled, omit host address in port mapping %s, or use 0.0.0.0::%d to open port %d for IPv4-only", - pb, pb.Port, pb.Port)) - // The mapping is IPv4-specific but there's no NAT4, so a host port would make no sense. - disallowHostPort = true - } else if !nat6 && len(pb.HostIP) > 0 && pb.HostIP.To4() == nil && !pb.HostIP.Equal(net.IPv6zero) { - // If the container has no IPv6 address, the userland proxy will proxy between the - // host's IPv6 address and the container's IPv4. So, even with no NAT6, it's ok for - // an IPv6 port mapping to include a specific host address or port. - if len(cIPv6) > 0 { - // There's no NAT6, so don't allow an IPv6 host address in the mapping. The port will - // accessible via any host interface. - errs = append(errs, - fmt.Errorf("NAT is disabled, omit host address in port mapping %s, or use [::]::%d to open port %d for IPv6-only", - pb, pb.Port, pb.Port)) - // The mapping is IPv6-specific but there's no NAT6, so a host port would make no sense. - disallowHostPort = true - } - } else if !nat4 && !nat6 { - // There's no NAT, so it would make no sense to specify a host port. - disallowHostPort = true - } - if disallowHostPort && pb.HostPort != 0 { - errs = append(errs, - fmt.Errorf("host port must not be specified in mapping %s because NAT is disabled", pb)) - } - if len(errs) >= validationErrLimit { - break - } - } - return errors.Join(errs...) -} - // sortAndNormPBs normalises cfg by making HostPortEnd=HostPort (rather than 0) if the // host port isn't a range - and sorts it into the ordering defined by cmpPortBinding. func sortAndNormPBs(cfg []types.PortBinding) { @@ -360,9 +310,17 @@ func needSamePort(a, b types.PortBinding) bool { a.IP.Equal(b.IP) } -// configurePortBindingIPv4 returns a new port binding with the HostIP field populated -// if a binding is required, else nil. -func configurePortBindingIPv4(pdc portDriverClient, disableNAT bool, bnd types.PortBinding, containerIPv4, defHostIP net.IP) (portBindingReq, bool) { +// configurePortBindingIPv4 returns a new port binding with the HostIP field +// populated and true, if a binding is required. Else, false and an empty +// binding. +func configurePortBindingIPv4( + ctx context.Context, + pdc portDriverClient, + disableNAT bool, + bnd types.PortBinding, + containerIPv4, + defHostIP net.IP, +) (portBindingReq, bool) { if len(containerIPv4) == 0 { return portBindingReq{}, false } @@ -376,8 +334,27 @@ func configurePortBindingIPv4(pdc portDriverClient, disableNAT bool, bnd types.P // The default binding address is IPv6. return portBindingReq{}, false } - bnd.HostIP = defHostIP + // The default binding IP is an IPv4 address, use it - unless NAT is disabled, + // in which case it's not possible to bind to a specific host address (the port + // mapping only opens the container's port for direct routing). + if disableNAT { + bnd.HostIP = net.IPv4zero + } else { + bnd.HostIP = defHostIP + } } + + if disableNAT && len(bnd.HostIP) != 0 && !bnd.HostIP.Equal(net.IPv4zero) { + // Ignore the default binding when nat is disabled - it may have been set + // up for IPv6 if nat is enabled there. + // Don't treat this as an error because, as networks are connected/disconnected, + // the container's gateway endpoint might change to a network where this config + // makes more sense. + log.G(ctx).WithFields(log.Fields{"mapping": bnd}).Info( + "Using address 0.0.0.0 because NAT is disabled") + bnd.HostIP = net.IPv4zero + } + // Unmap the addresses if they're IPv4-mapped IPv6. bnd.HostIP = bnd.HostIP.To4() bnd.IP = containerIPv4.To4() @@ -387,9 +364,16 @@ func configurePortBindingIPv4(pdc portDriverClient, disableNAT bool, bnd types.P }), true } -// configurePortBindingIPv6 returns a new port binding with the HostIP field populated -// if a binding is required, else nil. -func configurePortBindingIPv6(pdc portDriverClient, disableNAT bool, bnd types.PortBinding, containerIP, defHostIP net.IP) (portBindingReq, bool) { +// configurePortBindingIPv6 returns a new port binding with the HostIP field +// populated and true, if a binding is required. Else, false and an empty +// binding. +func configurePortBindingIPv6( + ctx context.Context, + pdc portDriverClient, + disableNAT bool, + bnd types.PortBinding, + containerIP, defHostIP net.IP, +) (portBindingReq, bool) { if containerIP == nil { return portBindingReq{}, false } @@ -408,13 +392,31 @@ func configurePortBindingIPv6(pdc portDriverClient, disableNAT bool, bnd types.P // Implicit binding to "::", no explicit HostIP and the default is 0.0.0.0 bnd.HostIP = net.IPv6zero } else if defHostIP.To4() == nil { - // The default binding IP is an IPv6 address, use it. - bnd.HostIP = defHostIP + // The default binding IP is an IPv6 address, use it - unless NAT is disabled, in + // which case it's not possible to bind to a specific host address (the port + // mapping only opens the container's port for direct routing). + if disableNAT { + bnd.HostIP = net.IPv6zero + } else { + bnd.HostIP = defHostIP + } } else { // The default binding IP is an IPv4 address, nothing to do here. return portBindingReq{}, false } } + + if disableNAT && len(bnd.HostIP) != 0 && !bnd.HostIP.Equal(net.IPv6zero) { + // Ignore the default binding when nat is disabled - it may have been set + // up for IPv4 if nat is enabled there. + // Don't treat this as an error because, as networks are connected/disconnected, + // the container's gateway endpoint might change to a network where this config + // makes more sense. + log.G(ctx).WithFields(log.Fields{"mapping": bnd}).Info( + "Using address [::] because NAT is disabled") + bnd.HostIP = net.IPv6zero + } + bnd.IP = containerIP return setChildHostIP(pdc, portBindingReq{ PortBinding: bnd, @@ -480,7 +482,7 @@ func bindHostPorts( // successfully reserved, a portBinding is returned for each mapping. // // If NAT is disabled for any of the bindings, no host port reservation is -// needed. These bindings are included in results, as the container port itself +// needed. Include these bindings in results, as the container port itself // needs to be opened in the firewall. func attemptBindHostPorts( ctx context.Context, @@ -539,8 +541,12 @@ func attemptBindHostPorts( var pb portBinding if c.disableNAT { pb = portBinding{PortBinding: c.GetCopy()} - pb.HostPort = 0 - pb.HostPortEnd = 0 + if pb.HostPort != 0 || pb.HostPortEnd != 0 { + log.G(ctx).WithFields(log.Fields{"mapping": pb}).Infof( + "Host port ignored, because NAT is disabled") + pb.HostPort = 0 + pb.HostPortEnd = 0 + } } else { switch proto { case "tcp": diff --git a/libnetwork/drivers/bridge/port_mapping_linux_test.go b/libnetwork/drivers/bridge/port_mapping_linux_test.go index bec4b22bde..d1e2110c2d 100644 --- a/libnetwork/drivers/bridge/port_mapping_linux_test.go +++ b/libnetwork/drivers/bridge/port_mapping_linux_test.go @@ -12,12 +12,14 @@ import ( "syscall" "testing" + "github.com/containerd/log" "github.com/docker/docker/internal/testutils/netnsutils" "github.com/docker/docker/libnetwork/iptables" "github.com/docker/docker/libnetwork/netlabel" "github.com/docker/docker/libnetwork/ns" "github.com/docker/docker/libnetwork/portallocator" "github.com/docker/docker/libnetwork/types" + "github.com/sirupsen/logrus" "github.com/vishvananda/netlink" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" @@ -186,146 +188,6 @@ func loopbackUp() error { return nlHandle.LinkSetUp(iface) } -func TestValidatePortBindings(t *testing.T) { - testcases := []struct { - name string - nat4 bool - nat6 bool - ctrIPv6 net.IP - pbs []types.PortBinding - expErrs []string - }{ - { - name: "no nat or addrs or ports", - pbs: []types.PortBinding{ - {Proto: types.TCP, Port: 80}, - }, - }, - { - name: "no nat with addrs", - ctrIPv6: newIPNet(t, "fd2c:b48c:69fb::2/128").IP, - pbs: []types.PortBinding{ - {Proto: types.TCP, HostIP: newIPNet(t, "233.252.0.2/24").IP, Port: 80}, - {Proto: types.TCP, HostIP: newIPNet(t, "2001:db8::2/64").IP, Port: 80}, - }, - expErrs: []string{ - "NAT is disabled, omit host address in port mapping 233.252.0.2::80/tcp, or use 0.0.0.0::80 to open port 80 for IPv4-only", - "NAT is disabled, omit host address in port mapping [2001:db8::2]::80/tcp, or use [::]::80 to open port 80 for IPv6-only", - }, - }, - { - name: "no nat with zero addrs", - pbs: []types.PortBinding{ - {Proto: types.TCP, HostIP: newIPNet(t, "0.0.0.0/0").IP, Port: 80}, - {Proto: types.TCP, HostIP: newIPNet(t, "::/0").IP, Port: 80}, - }, - }, - { - name: "no nat with host port", - pbs: []types.PortBinding{ - {Proto: types.TCP, HostPort: 8080, Port: 80}, - }, - expErrs: []string{ - "host port must not be specified in mapping 8080:80/tcp because NAT is disabled", - }, - }, - { - name: "nat4 any addr with host port", - nat4: true, - pbs: []types.PortBinding{ - {Proto: types.TCP, HostPort: 8080, Port: 80}, - }, - }, - { - name: "nat6 any addr with host port", - nat6: true, - pbs: []types.PortBinding{ - {Proto: types.TCP, HostPort: 8080, Port: 80}, - }, - }, - { - name: "nat and addrs and ports", - nat4: true, - nat6: true, - pbs: []types.PortBinding{ - {Proto: types.TCP, HostIP: newIPNet(t, "233.252.0.2/24").IP, HostPort: 8080, Port: 80}, - {Proto: types.TCP, HostIP: newIPNet(t, "2001:db8::2/64").IP, HostPort: 8080, Port: 80}, - }, - }, - { - name: "nat4 and addrs and ports", - nat4: true, - pbs: []types.PortBinding{ - {Proto: types.TCP, HostIP: newIPNet(t, "233.252.0.2/24").IP, HostPort: 8080, Port: 80}, - {Proto: types.TCP, HostIP: newIPNet(t, "2001:db8::2/64").IP, HostPort: 8080, Port: 80}, - }, - }, - { - name: "no nat and addrs and ports", - ctrIPv6: newIPNet(t, "fd2c:b48c:69fb::2/128").IP, - pbs: []types.PortBinding{ - {Proto: types.TCP, HostIP: newIPNet(t, "233.252.0.2/24").IP, HostPort: 8080, Port: 80}, - {Proto: types.TCP, HostIP: newIPNet(t, "2001:db8::2/64").IP, HostPort: 8080, Port: 80}, - }, - expErrs: []string{ - "NAT is disabled, omit host address in port mapping 233.252.0.2:8080:80/tcp, or use 0.0.0.0::80 to open port 80 for IPv4-only", - "NAT is disabled, omit host address in port mapping [2001:db8::2]:8080:80/tcp, or use [::]::80 to open port 80 for IPv6-only", - "host port must not be specified in mapping 233.252.0.2:8080:80/tcp because NAT is disabled", - "host port must not be specified in mapping [2001:db8::2]:8080:80/tcp because NAT is disabled", - }, - }, - { - name: "no nat no ctrIPv6 and addrs and ports", - pbs: []types.PortBinding{ - {Proto: types.TCP, HostIP: newIPNet(t, "233.252.0.2/24").IP, HostPort: 8080, Port: 80}, - {Proto: types.TCP, HostIP: newIPNet(t, "2001:db8::2/64").IP, HostPort: 8080, Port: 80}, - }, - expErrs: []string{ - "NAT is disabled, omit host address in port mapping 233.252.0.2:8080:80/tcp, or use 0.0.0.0::80 to open port 80 for IPv4-only", - "host port must not be specified in mapping 233.252.0.2:8080:80/tcp because NAT is disabled", - }, - }, - { - name: "max errs reached", - pbs: []types.PortBinding{ - {Proto: types.TCP, HostPort: 8080, Port: 80}, - {Proto: types.TCP, HostPort: 8081, Port: 80}, - {Proto: types.TCP, HostPort: 8082, Port: 80}, - {Proto: types.TCP, HostPort: 8083, Port: 80}, - {Proto: types.TCP, HostPort: 8084, Port: 80}, - {Proto: types.TCP, HostPort: 8085, Port: 80}, - {Proto: types.TCP, HostPort: 8086, Port: 80}, - }, - expErrs: []string{ - "host port must not be specified in mapping 8080:80/tcp because NAT is disabled", - "host port must not be specified in mapping 8081:80/tcp because NAT is disabled", - "host port must not be specified in mapping 8082:80/tcp because NAT is disabled", - "host port must not be specified in mapping 8083:80/tcp because NAT is disabled", - "host port must not be specified in mapping 8084:80/tcp because NAT is disabled", - "host port must not be specified in mapping 8085:80/tcp because NAT is disabled", - }, - }, - } - - for _, tc := range testcases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - err := validatePortBindings(tc.pbs, tc.nat4, tc.nat6, tc.ctrIPv6) - if tc.expErrs == nil { - assert.Check(t, err) - } else { - assert.Assert(t, err != nil) - for _, e := range tc.expErrs { - assert.Check(t, is.ErrorContains(err, e)) - } - numErrs := len(err.(interface{ Unwrap() []error }).Unwrap()) - assert.Check(t, is.Equal(numErrs, len(tc.expErrs)), - fmt.Sprintf("expected %d errors, got %d in %s", len(tc.expErrs), numErrs, err.Error())) - } - }) - } -} - func TestCmpPortBindings(t *testing.T) { pb := types.PortBinding{ Proto: types.TCP, @@ -429,6 +291,7 @@ func TestAddPortMappings(t *testing.T) { hostAddrs []string expErr string + expLogs []string expPBs []types.PortBinding expProxyRunning bool expReleaseErr string @@ -681,6 +544,22 @@ func TestAddPortMappings(t *testing.T) { {Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero}, }, }, + { + name: "disable nat6 with ipv6 default binding", + epAddrV4: ctrIP4, + epAddrV6: ctrIP6, + cfg: []types.PortBinding{ + {Proto: types.TCP, Port: 22}, + {Proto: types.TCP, Port: 80}, + }, + proxyPath: "/dummy/path/to/proxy", + gwMode6: gwModeRouted, + defHostIP: net.IPv6loopback, + expPBs: []types.PortBinding{ + {Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero}, + {Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero}, + }, + }, { name: "disable nat4", epAddrV4: ctrIP4, @@ -716,6 +595,90 @@ func TestAddPortMappings(t *testing.T) { {Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero}, }, }, + { + name: "ipv6 mapping to ipv4 container no proxy", + epAddrV4: ctrIP4, + cfg: []types.PortBinding{ + {Proto: types.TCP, Port: 22, HostIP: net.IPv6loopback}, + }, + expLogs: []string{"Cannot map from IPv6 to an IPv4-only container because the userland proxy is disabled"}, + }, + { + name: "ipv6 default mapping to ipv4 container no proxy", + epAddrV4: ctrIP4, + defHostIP: net.IPv6loopback, + cfg: []types.PortBinding{ + {Proto: types.TCP, Port: 22}, + }, + expLogs: []string{"Cannot map from default host binding address to an IPv4-only container because the userland proxy is disabled"}, + }, + { + name: "routed mode specific address", + epAddrV4: ctrIP4, + epAddrV6: ctrIP6, + gwMode4: gwModeRouted, + gwMode6: gwModeRouted, + proxyPath: "/dummy/path/to/proxy", + cfg: []types.PortBinding{ + {Proto: types.TCP, Port: 22, HostIP: newIPNet(t, "127.0.0.1/8").IP}, + {Proto: types.TCP, Port: 22, HostIP: net.IPv6loopback}, + }, + expLogs: []string{ + "Using address 0.0.0.0 because NAT is disabled", + "Using address [::] because NAT is disabled", + }, + expPBs: []types.PortBinding{ + {Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero}, + {Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero}, + }, + }, + { + name: "routed4 nat6 with ipv4 default binding", + epAddrV4: ctrIP4, + epAddrV6: ctrIP6, + gwMode4: gwModeRouted, + defHostIP: newIPNet(t, "127.0.0.1/8").IP, + proxyPath: "/dummy/path/to/proxy", + cfg: []types.PortBinding{ + {Proto: types.TCP, Port: 22}, + }, + expPBs: []types.PortBinding{ + {Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero}, + }, + }, + { + name: "routed4 nat6 with ipv6 default binding", + epAddrV4: ctrIP4, + epAddrV6: ctrIP6, + gwMode4: gwModeRouted, + defHostIP: net.IPv6loopback, + proxyPath: "/dummy/path/to/proxy", + cfg: []types.PortBinding{ + {Proto: types.TCP, Port: 22}, + }, + expPBs: []types.PortBinding{ + {Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6loopback, HostPort: firstEphemPort}, + }, + }, + { + name: "routed with host port", + epAddrV4: ctrIP4, + epAddrV6: ctrIP6, + gwMode4: gwModeRouted, + gwMode6: gwModeRouted, + cfg: []types.PortBinding{ + {Proto: types.TCP, Port: 22, HostPort: 2222}, + }, + expPBs: []types.PortBinding{ + {Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero}, + {Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero}, + }, + expLogs: []string{ + "Host port ignored, because NAT is disabled", + "0.0.0.0:2222:172.19.0.2:22/tcp", + "[::]:2222:[fdf8:b88e:bb5c:3483::2]:22/tcp", + }, + }, { name: "same ports for matching mappings with different host addresses", epAddrV4: ctrIP4, @@ -878,12 +841,26 @@ func TestAddPortMappings(t *testing.T) { portallocator.Get().ReleaseAll() - pbs, err := n.addPortMappings(context.Background(), tc.epAddrV4, tc.epAddrV6, tc.cfg, tc.defHostIP) + // Capture logs by stashing a new logger in the context. + var sb strings.Builder + logger := logrus.New() + logger.Out = &sb + ctx := log.WithLogger(context.Background(), &log.Entry{Logger: logger}) + t.Cleanup(func() { + if t.Failed() { + t.Logf("Daemon logs:\n%s", sb.String()) + } + }) + + pbs, err := n.addPortMappings(ctx, tc.epAddrV4, tc.epAddrV6, tc.cfg, tc.defHostIP) if tc.expErr != "" { assert.ErrorContains(t, err, tc.expErr) return } assert.NilError(t, err) + for _, expLog := range tc.expLogs { + assert.Check(t, is.Contains(sb.String(), expLog)) + } assert.Assert(t, is.Len(pbs, len(tc.expPBs))) // Check the iptables rules.