Log rather than error if port mapping is overspecified

Previously, if a port mapping specified a host IP or port that
could not be used because the endpoint's network was in routed
mode (so, there's no host binding), it'd be treated as an error.

However:
- the selected gateway endpoint may change over time, as networks
  are connected and disconnected - so the binding may make sense
  for some other endpoint.
- the validation was complicated, duplicated logic in order to
  fail early, and wasn't complete.

So, just log when fields are ignored, at the point where they're
ignored.

Signed-off-by: Rob Murray <rob.murray@docker.com>
This commit is contained in:
Rob Murray
2024-10-03 15:03:56 +01:00
parent 339592f59b
commit 98efe665a5
3 changed files with 210 additions and 227 deletions

View File

@@ -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,

View File

@@ -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":

View File

@@ -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.