mirror of
https://github.com/moby/moby.git
synced 2026-08-08 00:52:17 +00:00
Because we set SO_REUSEADDR on sockets for host ports, if there are port mappings for INADDR_ANY (the default) as well as for specific host ports - bind() cannot be used to detect clashes. That means, for example, on daemon startup, if the port allocator returns the first port in its ephemeral range for a specific host adddress, and the next port mapping is for 0.0.0.0 - the same port is returned and both bind() calls succeed. Then, the container fails to start later when listen() spots the problem and it's too late to find another port. So, bind and listen to each set of ports as they're allocated instead of just binding. Signed-off-by: Rob Murray <rob.murray@docker.com>
1033 lines
35 KiB
Go
1033 lines
35 KiB
Go
package bridge
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
|
|
"github.com/containerd/log"
|
|
"github.com/docker/docker/internal/testutils/netnsutils"
|
|
"github.com/docker/docker/internal/testutils/storeutils"
|
|
"github.com/docker/docker/libnetwork/drivers/bridge/internal/firewaller"
|
|
"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"
|
|
)
|
|
|
|
func TestPortMappingConfig(t *testing.T) {
|
|
defer netnsutils.SetupTestOSContext(t)()
|
|
useStubFirewaller(t)
|
|
|
|
d := newDriver(storeutils.NewTempStore(t))
|
|
|
|
config := &configuration{
|
|
EnableIPTables: true,
|
|
}
|
|
genericOption := make(map[string]interface{})
|
|
genericOption[netlabel.GenericData] = config
|
|
|
|
if err := d.configure(genericOption); err != nil {
|
|
t.Fatalf("Failed to setup driver config: %v", err)
|
|
}
|
|
|
|
binding1 := types.PortBinding{Proto: types.SCTP, Port: 300, HostPort: 65000}
|
|
binding2 := types.PortBinding{Proto: types.UDP, Port: 400, HostPort: 54000}
|
|
binding3 := types.PortBinding{Proto: types.TCP, Port: 500, HostPort: 65000}
|
|
portBindings := []types.PortBinding{binding1, binding2, binding3}
|
|
|
|
sbOptions := make(map[string]interface{})
|
|
sbOptions[netlabel.PortMap] = portBindings
|
|
|
|
netOptions := map[string]interface{}{
|
|
netlabel.GenericData: &networkConfiguration{
|
|
BridgeName: DefaultBridgeName,
|
|
EnableIPv4: true,
|
|
},
|
|
}
|
|
|
|
ipdList4 := getIPv4Data(t)
|
|
err := d.CreateNetwork(context.Background(), "dummy", netOptions, nil, ipdList4, getIPv6Data(t))
|
|
if err != nil {
|
|
t.Fatalf("Failed to create bridge: %v", err)
|
|
}
|
|
|
|
te := newTestEndpoint(ipdList4[0].Pool, 11)
|
|
err = d.CreateEndpoint(context.Background(), "dummy", "ep1", te.Interface(), nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create the endpoint: %s", err.Error())
|
|
}
|
|
|
|
if err = d.Join(context.Background(), "dummy", "ep1", "sbox", te, nil, sbOptions); err != nil {
|
|
t.Fatalf("Failed to join the endpoint: %v", err)
|
|
}
|
|
|
|
if err = d.ProgramExternalConnectivity(context.Background(), "dummy", "ep1", sbOptions); err != nil {
|
|
t.Fatalf("Failed to program external connectivity: %v", err)
|
|
}
|
|
|
|
network, ok := d.networks["dummy"]
|
|
if !ok {
|
|
t.Fatalf("Cannot find network %s inside driver", "dummy")
|
|
}
|
|
ep := network.endpoints["ep1"]
|
|
if len(ep.portMapping) != 3 {
|
|
t.Fatalf("Failed to store the port bindings into the sandbox info. Found: %v", ep.portMapping)
|
|
}
|
|
if ep.portMapping[0].Proto != binding1.Proto || ep.portMapping[0].Port != binding1.Port ||
|
|
ep.portMapping[1].Proto != binding2.Proto || ep.portMapping[1].Port != binding2.Port ||
|
|
ep.portMapping[2].Proto != binding3.Proto || ep.portMapping[2].Port != binding3.Port {
|
|
t.Fatal("bridgeEndpoint has incorrect port mapping values")
|
|
}
|
|
if ep.portMapping[0].HostIP == nil || ep.portMapping[0].HostPort == 0 ||
|
|
ep.portMapping[1].HostIP == nil || ep.portMapping[1].HostPort == 0 ||
|
|
ep.portMapping[2].HostIP == nil || ep.portMapping[2].HostPort == 0 {
|
|
t.Fatal("operational port mapping data not found on bridgeEndpoint")
|
|
}
|
|
|
|
// release host mapped ports
|
|
err = d.Leave("dummy", "ep1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = d.RevokeExternalConnectivity("dummy", "ep1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestPortMappingV6Config(t *testing.T) {
|
|
defer netnsutils.SetupTestOSContext(t)()
|
|
if err := loopbackUp(); err != nil {
|
|
t.Fatalf("Could not bring loopback iface up: %v", err)
|
|
}
|
|
|
|
d := newDriver(storeutils.NewTempStore(t))
|
|
|
|
config := &configuration{
|
|
EnableIPTables: true,
|
|
EnableIP6Tables: true,
|
|
}
|
|
genericOption := make(map[string]interface{})
|
|
genericOption[netlabel.GenericData] = config
|
|
|
|
if err := d.configure(genericOption); err != nil {
|
|
t.Fatalf("Failed to setup driver config: %v", err)
|
|
}
|
|
|
|
portBindings := []types.PortBinding{
|
|
{Proto: types.UDP, Port: 400, HostPort: 54000},
|
|
{Proto: types.TCP, Port: 500, HostPort: 65000},
|
|
{Proto: types.SCTP, Port: 500, HostPort: 65000},
|
|
}
|
|
|
|
sbOptions := make(map[string]interface{})
|
|
sbOptions[netlabel.PortMap] = portBindings
|
|
netConfig := &networkConfiguration{
|
|
BridgeName: DefaultBridgeName,
|
|
EnableIPv6: true,
|
|
}
|
|
netOptions := make(map[string]interface{})
|
|
netOptions[netlabel.GenericData] = netConfig
|
|
|
|
ipdList4 := getIPv4Data(t)
|
|
ipdList6 := getIPv6Data(t)
|
|
err := d.CreateNetwork(context.Background(), "dummy", netOptions, nil, ipdList4, ipdList6)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create bridge: %v", err)
|
|
}
|
|
|
|
te := newTestEndpoint46(ipdList4[0].Pool, ipdList6[0].Pool, 11)
|
|
err = d.CreateEndpoint(context.Background(), "dummy", "ep1", te.Interface(), nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create the endpoint: %s", err.Error())
|
|
}
|
|
|
|
if err = d.Join(context.Background(), "dummy", "ep1", "sbox", te, nil, sbOptions); err != nil {
|
|
t.Fatalf("Failed to join the endpoint: %v", err)
|
|
}
|
|
|
|
if err = d.ProgramExternalConnectivity(context.Background(), "dummy", "ep1", sbOptions); err != nil {
|
|
t.Fatalf("Failed to program external connectivity: %v", err)
|
|
}
|
|
|
|
network, ok := d.networks["dummy"]
|
|
if !ok {
|
|
t.Fatalf("Cannot find network %s inside driver", "dummy")
|
|
}
|
|
ep := network.endpoints["ep1"]
|
|
if len(ep.portMapping) != 6 {
|
|
t.Fatalf("Failed to store the port bindings into the sandbox info. Found: %v", ep.portMapping)
|
|
}
|
|
|
|
// release host mapped ports
|
|
err = d.Leave("dummy", "ep1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = d.RevokeExternalConnectivity("dummy", "ep1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func loopbackUp() error {
|
|
nlHandle := ns.NlHandle()
|
|
iface, err := nlHandle.LinkByName("lo")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nlHandle.LinkSetUp(iface)
|
|
}
|
|
|
|
func TestCmpPortBindings(t *testing.T) {
|
|
pb := types.PortBinding{
|
|
Proto: types.TCP,
|
|
IP: net.ParseIP("172.17.0.2"),
|
|
Port: 80,
|
|
HostIP: net.ParseIP("192.168.1.2"),
|
|
HostPort: 8080,
|
|
HostPortEnd: 8080,
|
|
}
|
|
var pbA, pbB types.PortBinding
|
|
|
|
assert.Check(t, cmpPortBinding(pb, pb) == 0)
|
|
|
|
pbA, pbB = pb, pb
|
|
pbA.Port = 22
|
|
assert.Check(t, cmpPortBinding(pbA, pbB) < 0)
|
|
assert.Check(t, cmpPortBinding(pbB, pbA) > 0)
|
|
|
|
pbA, pbB = pb, pb
|
|
pbB.Proto = types.UDP
|
|
assert.Check(t, cmpPortBinding(pbA, pbB) < 0)
|
|
assert.Check(t, cmpPortBinding(pbB, pbA) > 0)
|
|
|
|
pbA, pbB = pb, pb
|
|
pbA.Port = 22
|
|
pbA.Proto = types.UDP
|
|
assert.Check(t, cmpPortBinding(pbA, pbB) < 0)
|
|
assert.Check(t, cmpPortBinding(pbB, pbA) > 0)
|
|
|
|
pbA, pbB = pb, pb
|
|
pbB.HostPort = 8081
|
|
assert.Check(t, cmpPortBinding(pbA, pbB) < 0)
|
|
assert.Check(t, cmpPortBinding(pbB, pbA) > 0)
|
|
|
|
pbA, pbB = pb, pb
|
|
pbB.HostPort, pbB.HostPortEnd = 0, 0
|
|
assert.Check(t, cmpPortBinding(pbA, pbB) < 0)
|
|
assert.Check(t, cmpPortBinding(pbB, pbA) > 0)
|
|
|
|
pbA, pbB = pb, pb
|
|
pbB.HostPortEnd = 8081
|
|
assert.Check(t, cmpPortBinding(pbA, pbB) < 0)
|
|
assert.Check(t, cmpPortBinding(pbB, pbA) > 0)
|
|
|
|
pbA, pbB = pb, pb
|
|
pbA.HostPortEnd = 8080
|
|
pbB.HostPortEnd = 8081
|
|
assert.Check(t, cmpPortBinding(pbA, pbB) < 0)
|
|
assert.Check(t, cmpPortBinding(pbB, pbA) > 0)
|
|
}
|
|
|
|
func TestBindHostPortsError(t *testing.T) {
|
|
cfg := []portBindingReq{
|
|
{
|
|
PortBinding: types.PortBinding{
|
|
Proto: types.TCP,
|
|
Port: 80,
|
|
HostPort: 8080,
|
|
HostPortEnd: 8080,
|
|
},
|
|
},
|
|
{
|
|
PortBinding: types.PortBinding{
|
|
Proto: types.TCP,
|
|
Port: 80,
|
|
HostPort: 8080,
|
|
HostPortEnd: 8081,
|
|
},
|
|
},
|
|
}
|
|
pbs, err := bindHostPorts(context.Background(), cfg, "", nil, nil)
|
|
assert.Check(t, is.Error(err, "port binding mismatch 80/tcp:8080-8080, 80/tcp:8080-8081"))
|
|
assert.Check(t, is.Nil(pbs))
|
|
}
|
|
|
|
func newIPNet(t *testing.T, cidr string) *net.IPNet {
|
|
t.Helper()
|
|
ip, ipNet, err := net.ParseCIDR(cidr)
|
|
assert.NilError(t, err)
|
|
ipNet.IP = ip
|
|
return ipNet
|
|
}
|
|
|
|
func TestAddPortMappings(t *testing.T) {
|
|
ctrIP4 := newIPNet(t, "172.19.0.2/16")
|
|
ctrIP4Mapped := newIPNet(t, "::ffff:172.19.0.2/112")
|
|
ctrIP6 := newIPNet(t, "fdf8:b88e:bb5c:3483::2/64")
|
|
firstEphemPort, _ := portallocator.GetPortRange()
|
|
|
|
testcases := []struct {
|
|
name string
|
|
epAddrV4 *net.IPNet
|
|
epAddrV6 *net.IPNet
|
|
gwMode4 gwMode
|
|
gwMode6 gwMode
|
|
cfg []types.PortBinding
|
|
defHostIP net.IP
|
|
proxyPath string
|
|
busyPortIPv4 int
|
|
rootless bool
|
|
hostAddrs []string
|
|
noProxy6To4 bool
|
|
|
|
expErr string
|
|
expLogs []string
|
|
expPBs []types.PortBinding
|
|
expProxyRunning bool
|
|
expReleaseErr string
|
|
expNAT4Rules []string
|
|
expFilter4Rules []string
|
|
expNAT6Rules []string
|
|
expFilter6Rules []string
|
|
}{
|
|
{
|
|
name: "defaults",
|
|
epAddrV4: ctrIP4,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{
|
|
{Proto: types.TCP, Port: 22},
|
|
{Proto: types.TCP, Port: 80},
|
|
},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero, HostPort: firstEphemPort},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero, HostPort: firstEphemPort},
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: firstEphemPort + 1},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: firstEphemPort + 1},
|
|
},
|
|
},
|
|
{
|
|
name: "specific host port",
|
|
epAddrV4: ctrIP4,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80, HostPort: 8080}},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080, HostPortEnd: 8080},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080, HostPortEnd: 8080},
|
|
},
|
|
},
|
|
{
|
|
name: "nat explicitly enabled",
|
|
epAddrV4: ctrIP4,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80, HostPort: 8080}},
|
|
gwMode4: gwModeNAT,
|
|
gwMode6: gwModeNAT,
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080, HostPortEnd: 8080},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080, HostPortEnd: 8080},
|
|
},
|
|
},
|
|
{
|
|
name: "specific host port in-use",
|
|
epAddrV4: ctrIP4,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80, HostPort: 8080}},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
busyPortIPv4: 8080,
|
|
expErr: "failed to bind host port for 0.0.0.0:8080:172.19.0.2:80/tcp: address already in use",
|
|
},
|
|
{
|
|
name: "ipv4 mapped container address with specific host port",
|
|
epAddrV4: ctrIP4Mapped,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80, HostPort: 8080}},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080, HostPortEnd: 8080},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080, HostPortEnd: 8080},
|
|
},
|
|
},
|
|
{
|
|
name: "ipv4 mapped host address with specific host port",
|
|
epAddrV4: ctrIP4,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80, HostIP: newIPNet(t, "::ffff:127.0.0.1/128").IP, HostPort: 8080}},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: newIPNet(t, "127.0.0.1/32").IP, HostPort: 8080, HostPortEnd: 8080},
|
|
},
|
|
},
|
|
{
|
|
name: "host port range with first port in-use",
|
|
epAddrV4: ctrIP4,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80, HostPort: 8080, HostPortEnd: 8081}},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
busyPortIPv4: 8080,
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8081, HostPortEnd: 8081},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8081, HostPortEnd: 8081},
|
|
},
|
|
},
|
|
{
|
|
name: "multi host ips with host port range and first port in-use",
|
|
epAddrV4: ctrIP4,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{
|
|
{Proto: types.TCP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080, HostPortEnd: 8081},
|
|
{Proto: types.TCP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080, HostPortEnd: 8081},
|
|
},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
busyPortIPv4: 8080,
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8081},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8081},
|
|
},
|
|
},
|
|
{
|
|
name: "host port range with busy port",
|
|
epAddrV4: ctrIP4,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{
|
|
{Proto: types.TCP, Port: 80, HostPort: 8080, HostPortEnd: 8083},
|
|
{Proto: types.TCP, Port: 81, HostPort: 8080, HostPortEnd: 8083},
|
|
{Proto: types.TCP, Port: 82, HostPort: 8080, HostPortEnd: 8083},
|
|
{Proto: types.UDP, Port: 80, HostPort: 8080, HostPortEnd: 8083},
|
|
{Proto: types.UDP, Port: 81, HostPort: 8080, HostPortEnd: 8083},
|
|
{Proto: types.UDP, Port: 82, HostPort: 8080, HostPortEnd: 8083},
|
|
},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
busyPortIPv4: 8082,
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080, HostPortEnd: 8080},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080, HostPortEnd: 8080},
|
|
{Proto: types.UDP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080, HostPortEnd: 8080},
|
|
{Proto: types.UDP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080, HostPortEnd: 8080},
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 81, HostIP: net.IPv4zero, HostPort: 8081, HostPortEnd: 8081},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 81, HostIP: net.IPv6zero, HostPort: 8081, HostPortEnd: 8081},
|
|
{Proto: types.UDP, IP: ctrIP4.IP, Port: 81, HostIP: net.IPv4zero, HostPort: 8081, HostPortEnd: 8081},
|
|
{Proto: types.UDP, IP: ctrIP6.IP, Port: 81, HostIP: net.IPv6zero, HostPort: 8081, HostPortEnd: 8081},
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 82, HostIP: net.IPv4zero, HostPort: 8083, HostPortEnd: 8083},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 82, HostIP: net.IPv6zero, HostPort: 8083, HostPortEnd: 8083},
|
|
{Proto: types.UDP, IP: ctrIP4.IP, Port: 82, HostIP: net.IPv4zero, HostPort: 8083, HostPortEnd: 8083},
|
|
{Proto: types.UDP, IP: ctrIP6.IP, Port: 82, HostIP: net.IPv6zero, HostPort: 8083, HostPortEnd: 8083},
|
|
},
|
|
},
|
|
{
|
|
name: "host port range exhausted",
|
|
epAddrV4: ctrIP4,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{
|
|
{Proto: types.TCP, Port: 80, HostPort: 8080, HostPortEnd: 8082},
|
|
{Proto: types.TCP, Port: 81, HostPort: 8080, HostPortEnd: 8082},
|
|
{Proto: types.TCP, Port: 82, HostPort: 8080, HostPortEnd: 8082},
|
|
},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
busyPortIPv4: 8081,
|
|
expErr: "failed to bind host port 8081 for 0.0.0.0:8080-8082:172.19.0.2:82/tcp",
|
|
},
|
|
{
|
|
name: "map host ipv6 to ipv4 container with proxy",
|
|
epAddrV4: ctrIP4,
|
|
cfg: []types.PortBinding{
|
|
{Proto: types.TCP, HostIP: net.IPv4zero, Port: 80},
|
|
{Proto: types.TCP, HostIP: net.IPv6zero, Port: 80},
|
|
},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: firstEphemPort},
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv6zero, HostPort: firstEphemPort},
|
|
},
|
|
},
|
|
{
|
|
name: "map to ipv4 container with proxy but noProxy6To4",
|
|
epAddrV4: ctrIP4,
|
|
cfg: []types.PortBinding{
|
|
{Proto: types.TCP, Port: 80},
|
|
},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
noProxy6To4: true,
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: firstEphemPort},
|
|
},
|
|
},
|
|
{
|
|
name: "map host ipv6 to ipv4 container without proxy",
|
|
epAddrV4: ctrIP4,
|
|
cfg: []types.PortBinding{
|
|
{Proto: types.TCP, HostIP: net.IPv4zero, Port: 80},
|
|
{Proto: types.TCP, HostIP: net.IPv6zero, Port: 80}, // silently ignored
|
|
},
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: firstEphemPort},
|
|
},
|
|
},
|
|
{
|
|
name: "default host ip is nonzero v4",
|
|
epAddrV4: ctrIP4,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80}},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
defHostIP: newIPNet(t, "127.0.0.1/8").IP,
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: newIPNet(t, "127.0.0.1/8").IP, HostPort: firstEphemPort},
|
|
},
|
|
},
|
|
{
|
|
name: "default host ip is nonzero IPv4-mapped IPv6",
|
|
epAddrV4: ctrIP4,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80}},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
defHostIP: newIPNet(t, "::ffff:127.0.0.1/72").IP,
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: newIPNet(t, "127.0.0.1/8").IP, HostPort: firstEphemPort},
|
|
},
|
|
},
|
|
{
|
|
name: "default host ip is v6",
|
|
epAddrV4: ctrIP4,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80}},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
defHostIP: net.IPv6zero,
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: firstEphemPort},
|
|
},
|
|
},
|
|
{
|
|
name: "default host ip is nonzero v6",
|
|
epAddrV4: ctrIP4,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{{Proto: types.TCP, Port: 80}},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
defHostIP: newIPNet(t, "::1/128").IP,
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: newIPNet(t, "::1/128").IP, HostPort: firstEphemPort},
|
|
},
|
|
},
|
|
{
|
|
name: "error releasing bindings",
|
|
epAddrV4: ctrIP4,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{
|
|
{Proto: types.TCP, Port: 80, HostPort: 8080},
|
|
{Proto: types.TCP, Port: 22, HostPort: 2222},
|
|
},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero, HostPort: 2222},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero, HostPort: 2222},
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: 8080},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: 8080},
|
|
},
|
|
expReleaseErr: "failed to stop userland proxy for port mapping 0.0.0.0:2222:172.19.0.2:22/tcp: can't stop now\n" +
|
|
"failed to stop userland proxy for port mapping [::]:2222:[fdf8:b88e:bb5c:3483::2]:22/tcp: can't stop now\n" +
|
|
"failed to stop userland proxy for port mapping 0.0.0.0:8080:172.19.0.2:80/tcp: can't stop now\n" +
|
|
"failed to stop userland proxy for port mapping [::]:8080:[fdf8:b88e:bb5c:3483::2]:80/tcp: can't stop now",
|
|
},
|
|
{
|
|
name: "disable nat6",
|
|
epAddrV4: ctrIP4,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{
|
|
{Proto: types.TCP, Port: 22},
|
|
{Proto: types.TCP, Port: 80},
|
|
},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
gwMode6: gwModeRouted,
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero, HostPort: firstEphemPort},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero},
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: firstEphemPort + 1},
|
|
{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,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{
|
|
{Proto: types.TCP, Port: 22},
|
|
{Proto: types.TCP, Port: 80},
|
|
},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
gwMode4: gwModeRouted,
|
|
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, HostPort: firstEphemPort},
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: firstEphemPort + 1},
|
|
},
|
|
},
|
|
{
|
|
name: "disable nat",
|
|
epAddrV4: ctrIP4,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{
|
|
{Proto: types.TCP, Port: 22},
|
|
{Proto: types.TCP, Port: 80},
|
|
},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
gwMode4: gwModeRouted,
|
|
gwMode6: gwModeRouted,
|
|
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},
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero},
|
|
{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,
|
|
epAddrV6: ctrIP6,
|
|
hostAddrs: []string{"192.168.1.2/24", "fd0c:9167:5b11::2/64", "fd0c:9167:5b11::3/64"},
|
|
cfg: []types.PortBinding{
|
|
// These two should both get the same host port.
|
|
{Proto: types.TCP, Port: 80, HostIP: newIPNet(t, "fd0c:9167:5b11::2/64").IP},
|
|
{Proto: types.TCP, Port: 80, HostIP: newIPNet(t, "192.168.1.2/24").IP},
|
|
// These three should all get the same host port.
|
|
{Proto: types.TCP, Port: 22, HostIP: newIPNet(t, "fd0c:9167:5b11::2/64").IP},
|
|
{Proto: types.TCP, Port: 22, HostIP: newIPNet(t, "fd0c:9167:5b11::3/64").IP},
|
|
{Proto: types.TCP, Port: 22, HostIP: newIPNet(t, "192.168.1.2/24").IP},
|
|
// These two should get different host ports, and the exact-port should be allocated
|
|
// before the range.
|
|
{Proto: types.TCP, Port: 12345, HostPort: 12345, HostPortEnd: 12346},
|
|
{Proto: types.TCP, Port: 12345, HostPort: 12345},
|
|
},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 12345, HostIP: net.IPv4zero, HostPort: 12345},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 12345, HostIP: net.IPv6zero, HostPort: 12345},
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: newIPNet(t, "192.168.1.2/24").IP, HostPort: firstEphemPort},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: newIPNet(t, "fd0c:9167:5b11::2/64").IP, HostPort: firstEphemPort},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: newIPNet(t, "fd0c:9167:5b11::3/64").IP, HostPort: firstEphemPort},
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: newIPNet(t, "192.168.1.2/24").IP, HostPort: firstEphemPort + 1},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: newIPNet(t, "fd0c:9167:5b11::2/64").IP, HostPort: firstEphemPort + 1},
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 12345, HostIP: net.IPv4zero, HostPort: 12346},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 12345, HostIP: net.IPv6zero, HostPort: 12346},
|
|
},
|
|
},
|
|
{
|
|
name: "rootless",
|
|
epAddrV4: ctrIP4,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{
|
|
{Proto: types.TCP, Port: 22},
|
|
{Proto: types.TCP, Port: 80},
|
|
},
|
|
proxyPath: "/dummy/path/to/proxy",
|
|
rootless: true,
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero, HostPort: firstEphemPort},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero, HostPort: firstEphemPort},
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: firstEphemPort + 1},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: firstEphemPort + 1},
|
|
},
|
|
},
|
|
{
|
|
name: "rootless without proxy",
|
|
epAddrV4: ctrIP4,
|
|
epAddrV6: ctrIP6,
|
|
cfg: []types.PortBinding{
|
|
{Proto: types.TCP, Port: 22},
|
|
{Proto: types.TCP, Port: 80},
|
|
},
|
|
rootless: true,
|
|
expPBs: []types.PortBinding{
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero, HostPort: firstEphemPort},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero, HostPort: firstEphemPort},
|
|
{Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: firstEphemPort + 1},
|
|
{Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: firstEphemPort + 1},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testcases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
defer netnsutils.SetupTestOSContext(t)()
|
|
useStubFirewaller(t)
|
|
|
|
// Mock the startProxy function used by the code under test.
|
|
origStartProxy := startProxy
|
|
defer func() { startProxy = origStartProxy }()
|
|
proxies := map[proxyCall]bool{} // proxy -> is not stopped
|
|
startProxy = func(pb types.PortBinding,
|
|
proxyPath string,
|
|
listenSock *os.File,
|
|
) (stop func() error, retErr error) {
|
|
if tc.busyPortIPv4 > 0 && tc.busyPortIPv4 == int(pb.HostPort) && pb.HostIP.To4() != nil {
|
|
return nil, errors.New("busy port")
|
|
}
|
|
c := newProxyCall(pb.Proto.String(), pb.HostIP, int(pb.HostPort), pb.IP, int(pb.Port), proxyPath)
|
|
if _, ok := proxies[c]; ok {
|
|
return nil, fmt.Errorf("duplicate proxy: %#v", c)
|
|
}
|
|
proxies[c] = true
|
|
return func() error {
|
|
if tc.expReleaseErr != "" {
|
|
return errors.New("can't stop now")
|
|
}
|
|
if !proxies[c] {
|
|
return errors.New("already stopped")
|
|
}
|
|
proxies[c] = false
|
|
return nil
|
|
}, nil
|
|
}
|
|
|
|
// Mock the RootlessKit port driver.
|
|
origNewPortDriverClient := newPortDriverClient
|
|
defer func() { newPortDriverClient = origNewPortDriverClient }()
|
|
newPortDriverClient = func(ctx context.Context) (portDriverClient, error) {
|
|
return newMockPortDriverClient(ctx)
|
|
}
|
|
|
|
if len(tc.hostAddrs) > 0 {
|
|
dummyLink := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: "br-dummy"}}
|
|
err := netlink.LinkAdd(dummyLink)
|
|
assert.NilError(t, err)
|
|
for _, addr := range tc.hostAddrs {
|
|
// Add with NODAD so that the address is available immediately.
|
|
err := netlink.AddrAdd(dummyLink,
|
|
&netlink.Addr{IPNet: newIPNet(t, addr), Flags: syscall.IFA_F_NODAD})
|
|
assert.NilError(t, err)
|
|
}
|
|
err = netlink.LinkSetUp(dummyLink)
|
|
assert.NilError(t, err)
|
|
}
|
|
if tc.busyPortIPv4 != 0 {
|
|
tl, err := net.ListenTCP("tcp4", &net.TCPAddr{IP: net.IPv4zero, Port: tc.busyPortIPv4})
|
|
assert.NilError(t, err)
|
|
defer tl.Close()
|
|
ul, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: tc.busyPortIPv4})
|
|
assert.NilError(t, err)
|
|
defer ul.Close()
|
|
}
|
|
|
|
n := &bridgeNetwork{
|
|
config: &networkConfiguration{
|
|
BridgeName: "dummybridge",
|
|
EnableIPv4: tc.epAddrV4 != nil,
|
|
EnableIPv6: tc.epAddrV6 != nil,
|
|
GwModeIPv4: tc.gwMode4,
|
|
GwModeIPv6: tc.gwMode6,
|
|
},
|
|
bridge: &bridgeInterface{},
|
|
driver: newDriver(storeutils.NewTempStore(t)),
|
|
}
|
|
genericOption := map[string]interface{}{
|
|
netlabel.GenericData: &configuration{
|
|
EnableIPTables: true,
|
|
EnableIP6Tables: true,
|
|
EnableUserlandProxy: tc.proxyPath != "",
|
|
UserlandProxyPath: tc.proxyPath,
|
|
Rootless: tc.rootless,
|
|
},
|
|
}
|
|
err := n.driver.configure(genericOption)
|
|
assert.NilError(t, err)
|
|
fwn, err := n.newFirewallerNetwork(context.Background())
|
|
assert.NilError(t, err)
|
|
assert.Check(t, fwn != nil, "no firewaller network")
|
|
n.firewallerNetwork = fwn
|
|
|
|
assert.Check(t, is.Equal(n.driver.portDriverClient == nil, !tc.rootless))
|
|
expChildIP := func(hostIP net.IP) net.IP {
|
|
if !tc.rootless {
|
|
return hostIP
|
|
}
|
|
if hostIP.To4() == nil {
|
|
return net.ParseIP("::1")
|
|
}
|
|
return net.ParseIP("127.0.0.1")
|
|
}
|
|
|
|
portallocator.Get().ReleaseAll()
|
|
|
|
// 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, tc.noProxy6To4)
|
|
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)))
|
|
|
|
fw := n.driver.firewaller.(*firewaller.StubFirewaller)
|
|
assert.Check(t, is.Equal(fw.Hairpin, tc.proxyPath == ""))
|
|
assert.Check(t, fw.IPv4)
|
|
assert.Check(t, fw.IPv6)
|
|
|
|
fnw := n.firewallerNetwork.(*firewaller.StubFirewallerNetwork)
|
|
assert.Check(t, !fnw.Internal)
|
|
assert.Check(t, !fnw.ICC)
|
|
assert.Check(t, !fnw.Masquerade)
|
|
|
|
if n.config.HostIPv4 == nil {
|
|
assert.Check(t, !fnw.Config4.HostIP.IsValid())
|
|
} else {
|
|
assert.Check(t, is.Equal(fnw.Config4.HostIP.String(), n.config.HostIPv4.String()))
|
|
}
|
|
assert.Check(t, is.Equal(fnw.Config4.Routed, tc.gwMode4.routed()))
|
|
assert.Check(t, !fnw.Config4.Unprotected)
|
|
|
|
if n.config.HostIPv6 == nil {
|
|
assert.Check(t, !fnw.Config6.HostIP.IsValid())
|
|
} else {
|
|
assert.Check(t, is.Equal(fnw.Config6.HostIP.String(), n.config.HostIPv6.String()))
|
|
}
|
|
assert.Check(t, is.Equal(fnw.Config6.Routed, tc.gwMode6.routed()))
|
|
assert.Check(t, !fnw.Config6.Unprotected)
|
|
|
|
assert.Check(t, is.Len(fnw.Ports, len(tc.expPBs)))
|
|
for _, expPB := range tc.expPBs {
|
|
expPBCopy := expPB
|
|
expPBCopy.HostPortEnd = expPB.HostPort
|
|
expPBCopy.HostIP = expChildIP(expPB.HostIP)
|
|
assert.Check(t, fnw.PortExists(expPBCopy),
|
|
"expected port mapping %v (%v)", expPBCopy, expChildIP(expPB.HostIP))
|
|
}
|
|
|
|
// Release anything that was allocated.
|
|
err = n.releasePorts(&bridgeEndpoint{portMapping: pbs})
|
|
if tc.expReleaseErr == "" {
|
|
assert.Check(t, err)
|
|
} else {
|
|
assert.Check(t, is.Error(err, tc.expReleaseErr))
|
|
}
|
|
|
|
// Check a docker-proxy was started and stopped for each expected port binding.
|
|
if tc.proxyPath != "" {
|
|
expProxies := map[proxyCall]bool{}
|
|
for _, expPB := range tc.expPBs {
|
|
hip := expChildIP(expPB.HostIP)
|
|
is4 := hip.To4() != nil
|
|
if (is4 && tc.gwMode4.routed()) || (!is4 && tc.gwMode6.routed()) {
|
|
continue
|
|
}
|
|
p := newProxyCall(expPB.Proto.String(),
|
|
hip, int(expPB.HostPort),
|
|
expPB.IP, int(expPB.Port), tc.proxyPath)
|
|
expProxies[p] = tc.expReleaseErr != ""
|
|
}
|
|
assert.Check(t, is.DeepEqual(expProxies, proxies))
|
|
}
|
|
|
|
// Check the port driver has seen the expected port mappings and no others,
|
|
// and that they have all been closed.
|
|
if n.driver.portDriverClient != nil {
|
|
pdc := n.driver.portDriverClient.(*mockPortDriverClient)
|
|
expPorts := map[mockPortDriverPort]bool{}
|
|
for _, expPB := range tc.expPBs {
|
|
if expPB.HostPort == 0 {
|
|
continue
|
|
}
|
|
pdp := mockPortDriverPort{
|
|
proto: expPB.Proto.String(),
|
|
hostIP: expPB.HostIP.String(),
|
|
childIP: expChildIP(expPB.HostIP).String(),
|
|
hostPort: int(expPB.HostPort),
|
|
}
|
|
expPorts[pdp] = false
|
|
}
|
|
assert.Check(t, is.DeepEqual(pdc.openPorts, expPorts))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Type for tracking calls to StartProxy.
|
|
type proxyCall struct{ proto, host, container, proxyPath string }
|
|
|
|
func newProxyCall(proto string,
|
|
hostIP net.IP, hostPort int,
|
|
containerIP net.IP, containerPort int,
|
|
proxyPath string,
|
|
) proxyCall {
|
|
return proxyCall{
|
|
proto: proto,
|
|
host: fmt.Sprintf("%v:%v", hostIP, hostPort),
|
|
container: fmt.Sprintf("%v:%v", containerIP, containerPort),
|
|
proxyPath: proxyPath,
|
|
}
|
|
}
|
|
|
|
// Types for tracking calls to the port driver client (mock for RootlessKit client).
|
|
|
|
type mockPortDriverPort struct {
|
|
proto string
|
|
hostIP string
|
|
childIP string
|
|
hostPort int
|
|
}
|
|
|
|
func (p mockPortDriverPort) String() string {
|
|
return p.hostIP + ":" + strconv.Itoa(p.hostPort) + "/" + p.proto
|
|
}
|
|
|
|
type mockPortDriverClient struct {
|
|
openPorts map[mockPortDriverPort]bool
|
|
}
|
|
|
|
func newMockPortDriverClient(_ context.Context) (*mockPortDriverClient, error) {
|
|
return &mockPortDriverClient{
|
|
openPorts: map[mockPortDriverPort]bool{},
|
|
}, nil
|
|
}
|
|
|
|
func (c *mockPortDriverClient) ChildHostIP(hostIP netip.Addr) netip.Addr {
|
|
if hostIP.Is6() {
|
|
return netip.IPv6Loopback()
|
|
}
|
|
return netip.MustParseAddr("127.0.0.1")
|
|
}
|
|
|
|
func (c *mockPortDriverClient) AddPort(_ context.Context, proto string, hostIP, childIP netip.Addr, hostPort int) (func() error, error) {
|
|
key := mockPortDriverPort{proto: proto, hostIP: hostIP.String(), childIP: childIP.String(), hostPort: hostPort}
|
|
if _, exists := c.openPorts[key]; exists {
|
|
return nil, fmt.Errorf("mockPortDriverClient: port %s is already open", key)
|
|
}
|
|
c.openPorts[key] = true
|
|
return func() error {
|
|
if !c.openPorts[key] {
|
|
return fmt.Errorf("mockPortDriverClient: port %s is not open", key)
|
|
}
|
|
c.openPorts[key] = false
|
|
return nil
|
|
}, nil
|
|
}
|