diff --git a/integration/dockertestutil/network.go b/integration/dockertestutil/network.go index ab049abf..68d472f4 100644 --- a/integration/dockertestutil/network.go +++ b/integration/dockertestutil/network.go @@ -14,13 +14,33 @@ import ( var ErrContainerNotFound = errors.New("container not found") func GetFirstOrCreateNetwork(pool *dockertest.Pool, name string) (*dockertest.Network, error) { + return GetFirstOrCreateNetworkWithSubnet(pool, name, "") +} + +// GetFirstOrCreateNetworkWithSubnet creates a Docker network with an optional +// custom subnet. When subnet is empty, Docker auto-assigns from its default +// pool. Use RFC 5737 TEST-NET ranges (e.g. "198.51.100.0/24") for networks +// that need to be reachable through Tailscale exit nodes, since Tailscale's +// shrinkDefaultRoute strips RFC1918 ranges from exit node forwarding filters. +func GetFirstOrCreateNetworkWithSubnet(pool *dockertest.Pool, name, subnet string) (*dockertest.Network, error) { networks, err := pool.NetworksByName(name) if err != nil { return nil, fmt.Errorf("looking up network names: %w", err) } if len(networks) == 0 { - if _, err := pool.CreateNetwork(name); err == nil { //nolint:noinlineerr // intentional inline check + var opts []func(*docker.CreateNetworkOptions) + if subnet != "" { + opts = append(opts, func(config *docker.CreateNetworkOptions) { + config.IPAM = &docker.IPAMOptions{ + Config: []docker.IPAMConfig{ + {Subnet: subnet}, + }, + } + }) + } + + if _, err := pool.CreateNetwork(name, opts...); err == nil { //nolint:noinlineerr // intentional inline check // Create does not give us an updated version of the resource, so we need to // get it again. networks, err := pool.NetworksByName(name) diff --git a/integration/embedded_derp_test.go b/integration/embedded_derp_test.go index 406de323..f55de2cc 100644 --- a/integration/embedded_derp_test.go +++ b/integration/embedded_derp_test.go @@ -22,10 +22,10 @@ func TestDERPServerScenario(t *testing.T) { spec := ScenarioSpec{ NodesPerUser: 1, Users: []string{"user1", "user2", "user3"}, - Networks: map[string][]string{ - "usernet1": {"user1"}, - "usernet2": {"user2"}, - "usernet3": {"user3"}, + Networks: map[string]NetworkSpec{ + "usernet1": {Users: []string{"user1"}}, + "usernet2": {Users: []string{"user2"}}, + "usernet3": {Users: []string{"user3"}}, }, } @@ -72,10 +72,10 @@ func TestDERPServerWebsocketScenario(t *testing.T) { spec := ScenarioSpec{ NodesPerUser: 1, Users: []string{"user1", "user2", "user3"}, - Networks: map[string][]string{ - "usernet1": {"user1"}, - "usernet2": {"user2"}, - "usernet3": {"user3"}, + Networks: map[string]NetworkSpec{ + "usernet1": {Users: []string{"user1"}}, + "usernet2": {Users: []string{"user2"}}, + "usernet3": {Users: []string{"user3"}}, }, } diff --git a/integration/grant_cap_test.go b/integration/grant_cap_test.go index 147cda75..b81696ff 100644 --- a/integration/grant_cap_test.go +++ b/integration/grant_cap_test.go @@ -80,10 +80,10 @@ func TestGrantCapRelay(t *testing.T) { spec := ScenarioSpec{ NodesPerUser: 0, Users: []string{"relay", "clienta", "clientb"}, - Networks: map[string][]string{ - "usernet1": {"clienta"}, - "usernet2": {"clientb"}, - "usernet3": {"relay"}, + Networks: map[string]NetworkSpec{ + "usernet1": {Users: []string{"clienta"}}, + "usernet2": {Users: []string{"clientb"}}, + "usernet3": {Users: []string{"relay"}}, }, Versions: []string{"head"}, } @@ -535,8 +535,8 @@ func TestGrantCapDrive(t *testing.T) { spec := ScenarioSpec{ NodesPerUser: 0, Users: []string{"sharer", "rwclient", "roclient", "noaccess"}, - Networks: map[string][]string{ - "usernet1": {"sharer", "rwclient", "roclient", "noaccess"}, + Networks: map[string]NetworkSpec{ + "usernet1": {Users: []string{"sharer", "rwclient", "roclient", "noaccess"}}, }, Versions: []string{"head"}, } diff --git a/integration/route_test.go b/integration/route_test.go index 40b61a80..e8aef218 100644 --- a/integration/route_test.go +++ b/integration/route_test.go @@ -244,9 +244,9 @@ func TestHASubnetRouterFailover(t *testing.T) { spec := ScenarioSpec{ NodesPerUser: 3, Users: []string{"user1", "user2"}, - Networks: map[string][]string{ - "usernet1": {"user1"}, - "usernet2": {"user2"}, + Networks: map[string]NetworkSpec{ + "usernet1": {Users: []string{"user1"}}, + "usernet2": {Users: []string{"user2"}}, }, ExtraService: map[string][]extraServiceFunc{ "usernet1": {Webservice}, @@ -1713,9 +1713,9 @@ func TestSubnetRouterMultiNetwork(t *testing.T) { spec := ScenarioSpec{ NodesPerUser: 1, Users: []string{"user1", "user2"}, - Networks: map[string][]string{ - "usernet1": {"user1"}, - "usernet2": {"user2"}, + Networks: map[string]NetworkSpec{ + "usernet1": {Users: []string{"user1"}}, + "usernet2": {Users: []string{"user2"}}, }, ExtraService: map[string][]extraServiceFunc{ "usernet1": {Webservice}, @@ -1866,9 +1866,9 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) { spec := ScenarioSpec{ NodesPerUser: 1, Users: []string{"user1", "user2"}, - Networks: map[string][]string{ - "usernet1": {"user1"}, - "usernet2": {"user2"}, + Networks: map[string]NetworkSpec{ + "usernet1": {Users: []string{"user1"}}, + "usernet2": {Users: []string{"user2"}}, }, ExtraService: map[string][]extraServiceFunc{ "usernet1": {Webservice}, @@ -2073,9 +2073,9 @@ func TestAutoApproveMultiNetwork(t *testing.T) { spec: ScenarioSpec{ NodesPerUser: 3, Users: []string{"user1", "user2"}, - Networks: map[string][]string{ - "usernet1": {"user1"}, - "usernet2": {"user2"}, + Networks: map[string]NetworkSpec{ + "usernet1": {Users: []string{"user1"}}, + "usernet2": {Users: []string{"user2"}}, }, ExtraService: map[string][]extraServiceFunc{ "usernet1": {Webservice}, @@ -2108,9 +2108,9 @@ func TestAutoApproveMultiNetwork(t *testing.T) { spec: ScenarioSpec{ NodesPerUser: 3, Users: []string{"user1", "user2"}, - Networks: map[string][]string{ - "usernet1": {"user1"}, - "usernet2": {"user2"}, + Networks: map[string]NetworkSpec{ + "usernet1": {Users: []string{"user1"}}, + "usernet2": {Users: []string{"user2"}}, }, ExtraService: map[string][]extraServiceFunc{ "usernet1": {Webservice}, @@ -2146,9 +2146,9 @@ func TestAutoApproveMultiNetwork(t *testing.T) { spec: ScenarioSpec{ NodesPerUser: 3, Users: []string{"user1", "user2"}, - Networks: map[string][]string{ - "usernet1": {"user1"}, - "usernet2": {"user2"}, + Networks: map[string]NetworkSpec{ + "usernet1": {Users: []string{"user1"}}, + "usernet2": {Users: []string{"user2"}}, }, ExtraService: map[string][]extraServiceFunc{ "usernet1": {Webservice}, @@ -2181,9 +2181,9 @@ func TestAutoApproveMultiNetwork(t *testing.T) { spec: ScenarioSpec{ NodesPerUser: 3, Users: []string{"user1", "user2"}, - Networks: map[string][]string{ - "usernet1": {"user1"}, - "usernet2": {"user2"}, + Networks: map[string]NetworkSpec{ + "usernet1": {Users: []string{"user1"}}, + "usernet2": {Users: []string{"user2"}}, }, ExtraService: map[string][]extraServiceFunc{ "usernet1": {Webservice}, @@ -2220,9 +2220,9 @@ func TestAutoApproveMultiNetwork(t *testing.T) { spec: ScenarioSpec{ NodesPerUser: 3, Users: []string{"user1", "user2"}, - Networks: map[string][]string{ - "usernet1": {"user1"}, - "usernet2": {"user2"}, + Networks: map[string]NetworkSpec{ + "usernet1": {Users: []string{"user1"}}, + "usernet2": {Users: []string{"user2"}}, }, ExtraService: map[string][]extraServiceFunc{ "usernet1": {Webservice}, @@ -2259,9 +2259,9 @@ func TestAutoApproveMultiNetwork(t *testing.T) { spec: ScenarioSpec{ NodesPerUser: 3, Users: []string{"user1", "user2"}, - Networks: map[string][]string{ - "usernet1": {"user1"}, - "usernet2": {"user2"}, + Networks: map[string]NetworkSpec{ + "usernet1": {Users: []string{"user1"}}, + "usernet2": {Users: []string{"user2"}}, }, ExtraService: map[string][]extraServiceFunc{ "usernet1": {Webservice}, @@ -2952,8 +2952,8 @@ func TestSubnetRouteACLFiltering(t *testing.T) { spec := ScenarioSpec{ NodesPerUser: 1, Users: []string{routerUser, nodeUser}, - Networks: map[string][]string{ - "usernet1": {routerUser, nodeUser}, + Networks: map[string]NetworkSpec{ + "usernet1": {Users: []string{routerUser, nodeUser}}, }, ExtraService: map[string][]extraServiceFunc{ "usernet1": {Webservice}, @@ -3163,9 +3163,9 @@ func TestGrantViaSubnetSteering(t *testing.T) { spec := ScenarioSpec{ NodesPerUser: 0, Users: []string{"router", "client"}, - Networks: map[string][]string{ - "usernet1": {"router"}, - "usernet2": {"client"}, + Networks: map[string]NetworkSpec{ + "usernet1": {Users: []string{"router"}}, + "usernet2": {Users: []string{"client"}}, }, ExtraService: map[string][]extraServiceFunc{ "usernet1": {Webservice}, @@ -3485,9 +3485,9 @@ func TestGrantViaExitNodeSteering(t *testing.T) { spec := ScenarioSpec{ NodesPerUser: 0, Users: []string{"exit", "client"}, - Networks: map[string][]string{ - "usernet1": {"exit"}, - "usernet2": {"client"}, + Networks: map[string]NetworkSpec{ + "usernet1": {Users: []string{"exit"}}, + "usernet2": {Users: []string{"client"}}, }, ExtraService: map[string][]extraServiceFunc{ "usernet1": {Webservice}, @@ -3831,9 +3831,9 @@ func TestGrantViaMixedSteering(t *testing.T) { spec := ScenarioSpec{ NodesPerUser: 0, Users: []string{"server", "client"}, - Networks: map[string][]string{ - "usernet1": {"server"}, - "usernet2": {"client"}, + Networks: map[string]NetworkSpec{ + "usernet1": {Users: []string{"server"}}, + "usernet2": {Users: []string{"client"}}, "externet": {}, }, ExtraService: map[string][]extraServiceFunc{ diff --git a/integration/scenario.go b/integration/scenario.go index ba99a392..d503d174 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -115,6 +115,19 @@ type Scenario struct { testDefaultNetwork string } +// NetworkSpec describes a Docker network for the test scenario. +type NetworkSpec struct { + // Users is the list of usernames whose nodes will be placed on this network. + Users []string + + // Subnet, if set, is the CIDR for the Docker network (e.g. "198.51.100.0/24"). + // When empty, Docker auto-assigns a subnet from its default pool (RFC1918). + // Use RFC 5737 TEST-NET ranges for networks that must be reachable through + // Tailscale exit nodes, since Tailscale's shrinkDefaultRoute strips RFC1918 + // ranges from exit node forwarding filters. + Subnet string +} + // ScenarioSpec describes the users, nodes, and network topology to // set up for a given scenario. type ScenarioSpec struct { @@ -131,7 +144,7 @@ type ScenarioSpec struct { // added there. // Please note that Docker networks are not necessarily routable and // connections between them might fall back to DERP. - Networks map[string][]string + Networks map[string]NetworkSpec // ExtraService, if set, is additional a map of network to additional // container services that should be set up. These container services @@ -199,15 +212,15 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) { var userToNetwork map[string]*dockertest.Network if spec.Networks != nil || len(spec.Networks) != 0 { - for name, users := range s.spec.Networks { + for name, netSpec := range s.spec.Networks { networkName := testHashPrefix + "-" + name - network, err := s.AddNetwork(networkName) + network, err := s.AddNetworkWithSubnet(networkName, netSpec.Subnet) if err != nil { return nil, err } - for _, user := range users { + for _, user := range netSpec.Users { if n2, ok := userToNetwork[user]; ok { return nil, fmt.Errorf("users can only have nodes placed in one network: %s into %s but already in %s", user, network.Network.Name, n2.Network.Name) //nolint:err113 } @@ -251,7 +264,11 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) { } func (s *Scenario) AddNetwork(name string) (*dockertest.Network, error) { - network, err := dockertestutil.GetFirstOrCreateNetwork(s.pool, name) + return s.AddNetworkWithSubnet(name, "") +} + +func (s *Scenario) AddNetworkWithSubnet(name, subnet string) (*dockertest.Network, error) { + network, err := dockertestutil.GetFirstOrCreateNetworkWithSubnet(s.pool, name, subnet) if err != nil { return nil, fmt.Errorf("creating or getting network: %w", err) }