mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-11-04 01:51:04 +01:00 
			
		
		
		
	* linter fixes Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * conf Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * update nix hash Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --------- Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
		
			
				
	
	
		
			1319 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1319 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package integration
 | 
						|
 | 
						|
import (
 | 
						|
	"fmt"
 | 
						|
	"log"
 | 
						|
	"net/netip"
 | 
						|
	"sort"
 | 
						|
	"strconv"
 | 
						|
	"testing"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/google/go-cmp/cmp"
 | 
						|
	"github.com/google/go-cmp/cmp/cmpopts"
 | 
						|
	v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
 | 
						|
	"github.com/juanfont/headscale/hscontrol/policy"
 | 
						|
	"github.com/juanfont/headscale/hscontrol/util"
 | 
						|
	"github.com/juanfont/headscale/integration/hsic"
 | 
						|
	"github.com/juanfont/headscale/integration/tsic"
 | 
						|
	"github.com/stretchr/testify/assert"
 | 
						|
	"tailscale.com/types/ipproto"
 | 
						|
	"tailscale.com/types/views"
 | 
						|
	"tailscale.com/wgengine/filter"
 | 
						|
)
 | 
						|
 | 
						|
var allPorts = filter.PortRange{First: 0, Last: 0xffff}
 | 
						|
 | 
						|
// This test is both testing the routes command and the propagation of
 | 
						|
// routes.
 | 
						|
func TestEnablingRoutes(t *testing.T) {
 | 
						|
	IntegrationSkip(t)
 | 
						|
	t.Parallel()
 | 
						|
 | 
						|
	user := "enable-routing"
 | 
						|
 | 
						|
	scenario, err := NewScenario(dockertestMaxWait())
 | 
						|
	assertNoErrf(t, "failed to create scenario: %s", err)
 | 
						|
	defer scenario.ShutdownAssertNoPanics(t)
 | 
						|
 | 
						|
	spec := map[string]int{
 | 
						|
		user: 3,
 | 
						|
	}
 | 
						|
 | 
						|
	err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clienableroute"))
 | 
						|
	assertNoErrHeadscaleEnv(t, err)
 | 
						|
 | 
						|
	allClients, err := scenario.ListTailscaleClients()
 | 
						|
	assertNoErrListClients(t, err)
 | 
						|
 | 
						|
	err = scenario.WaitForTailscaleSync()
 | 
						|
	assertNoErrSync(t, err)
 | 
						|
 | 
						|
	headscale, err := scenario.Headscale()
 | 
						|
	assertNoErrGetHeadscale(t, err)
 | 
						|
 | 
						|
	expectedRoutes := map[string]string{
 | 
						|
		"1": "10.0.0.0/24",
 | 
						|
		"2": "10.0.1.0/24",
 | 
						|
		"3": "10.0.2.0/24",
 | 
						|
	}
 | 
						|
 | 
						|
	// advertise routes using the up command
 | 
						|
	for _, client := range allClients {
 | 
						|
		status, err := client.Status()
 | 
						|
		assertNoErr(t, err)
 | 
						|
 | 
						|
		command := []string{
 | 
						|
			"tailscale",
 | 
						|
			"set",
 | 
						|
			"--advertise-routes=" + expectedRoutes[string(status.Self.ID)],
 | 
						|
		}
 | 
						|
		_, _, err = client.Execute(command)
 | 
						|
		assertNoErrf(t, "failed to advertise route: %s", err)
 | 
						|
	}
 | 
						|
 | 
						|
	err = scenario.WaitForTailscaleSync()
 | 
						|
	assertNoErrSync(t, err)
 | 
						|
 | 
						|
	var routes []*v1.Route
 | 
						|
	err = executeAndUnmarshal(
 | 
						|
		headscale,
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"list",
 | 
						|
			"--output",
 | 
						|
			"json",
 | 
						|
		},
 | 
						|
		&routes,
 | 
						|
	)
 | 
						|
 | 
						|
	assertNoErr(t, err)
 | 
						|
	assert.Len(t, routes, 3)
 | 
						|
 | 
						|
	for _, route := range routes {
 | 
						|
		assert.True(t, route.GetAdvertised())
 | 
						|
		assert.False(t, route.GetEnabled())
 | 
						|
		assert.False(t, route.GetIsPrimary())
 | 
						|
	}
 | 
						|
 | 
						|
	// Verify that no routes has been sent to the client,
 | 
						|
	// they are not yet enabled.
 | 
						|
	for _, client := range allClients {
 | 
						|
		status, err := client.Status()
 | 
						|
		assertNoErr(t, err)
 | 
						|
 | 
						|
		for _, peerKey := range status.Peers() {
 | 
						|
			peerStatus := status.Peer[peerKey]
 | 
						|
 | 
						|
			assert.Nil(t, peerStatus.PrimaryRoutes)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Enable all routes
 | 
						|
	for _, route := range routes {
 | 
						|
		_, err = headscale.Execute(
 | 
						|
			[]string{
 | 
						|
				"headscale",
 | 
						|
				"routes",
 | 
						|
				"enable",
 | 
						|
				"--route",
 | 
						|
				strconv.Itoa(int(route.GetId())),
 | 
						|
			})
 | 
						|
		assertNoErr(t, err)
 | 
						|
	}
 | 
						|
 | 
						|
	var enablingRoutes []*v1.Route
 | 
						|
	err = executeAndUnmarshal(
 | 
						|
		headscale,
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"list",
 | 
						|
			"--output",
 | 
						|
			"json",
 | 
						|
		},
 | 
						|
		&enablingRoutes,
 | 
						|
	)
 | 
						|
	assertNoErr(t, err)
 | 
						|
	assert.Len(t, enablingRoutes, 3)
 | 
						|
 | 
						|
	for _, route := range enablingRoutes {
 | 
						|
		assert.True(t, route.GetAdvertised())
 | 
						|
		assert.True(t, route.GetEnabled())
 | 
						|
		assert.True(t, route.GetIsPrimary())
 | 
						|
	}
 | 
						|
 | 
						|
	time.Sleep(5 * time.Second)
 | 
						|
 | 
						|
	// Verify that the clients can see the new routes
 | 
						|
	for _, client := range allClients {
 | 
						|
		status, err := client.Status()
 | 
						|
		assertNoErr(t, err)
 | 
						|
 | 
						|
		for _, peerKey := range status.Peers() {
 | 
						|
			peerStatus := status.Peer[peerKey]
 | 
						|
 | 
						|
			assert.NotNil(t, peerStatus.PrimaryRoutes)
 | 
						|
			if peerStatus.PrimaryRoutes == nil {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
 | 
						|
			pRoutes := peerStatus.PrimaryRoutes.AsSlice()
 | 
						|
 | 
						|
			assert.Len(t, pRoutes, 1)
 | 
						|
 | 
						|
			if len(pRoutes) > 0 {
 | 
						|
				peerRoute := peerStatus.PrimaryRoutes.AsSlice()[0]
 | 
						|
 | 
						|
				// id starts at 1, we created routes with 0 index
 | 
						|
				assert.Equalf(
 | 
						|
					t,
 | 
						|
					expectedRoutes[string(peerStatus.ID)],
 | 
						|
					peerRoute.String(),
 | 
						|
					"expected route %s to be present on peer %s (%s) in %s (%s) status",
 | 
						|
					expectedRoutes[string(peerStatus.ID)],
 | 
						|
					peerStatus.HostName,
 | 
						|
					peerStatus.ID,
 | 
						|
					client.Hostname(),
 | 
						|
					client.ID(),
 | 
						|
				)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	routeToBeDisabled := enablingRoutes[0]
 | 
						|
	log.Printf("preparing to disable %v", routeToBeDisabled)
 | 
						|
 | 
						|
	_, err = headscale.Execute(
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"disable",
 | 
						|
			"--route",
 | 
						|
			strconv.Itoa(int(routeToBeDisabled.GetId())),
 | 
						|
		})
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	time.Sleep(5 * time.Second)
 | 
						|
 | 
						|
	var disablingRoutes []*v1.Route
 | 
						|
	err = executeAndUnmarshal(
 | 
						|
		headscale,
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"list",
 | 
						|
			"--output",
 | 
						|
			"json",
 | 
						|
		},
 | 
						|
		&disablingRoutes,
 | 
						|
	)
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	for _, route := range disablingRoutes {
 | 
						|
		assert.True(t, route.GetAdvertised())
 | 
						|
 | 
						|
		if route.GetId() == routeToBeDisabled.GetId() {
 | 
						|
			assert.False(t, route.GetEnabled())
 | 
						|
 | 
						|
			// since this is the only route of this cidr,
 | 
						|
			// it will not failover, and remain Primary
 | 
						|
			// until something can replace it.
 | 
						|
			assert.True(t, route.GetIsPrimary())
 | 
						|
		} else {
 | 
						|
			assert.True(t, route.GetEnabled())
 | 
						|
			assert.True(t, route.GetIsPrimary())
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Verify that the clients can see the new routes
 | 
						|
	for _, client := range allClients {
 | 
						|
		status, err := client.Status()
 | 
						|
		assertNoErr(t, err)
 | 
						|
 | 
						|
		for _, peerKey := range status.Peers() {
 | 
						|
			peerStatus := status.Peer[peerKey]
 | 
						|
 | 
						|
			if string(peerStatus.ID) == fmt.Sprintf("%d", routeToBeDisabled.GetNode().GetId()) {
 | 
						|
				assert.Nilf(
 | 
						|
					t,
 | 
						|
					peerStatus.PrimaryRoutes,
 | 
						|
					"expected node %s to have no routes, got primary route (%v)",
 | 
						|
					peerStatus.HostName,
 | 
						|
					peerStatus.PrimaryRoutes,
 | 
						|
				)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestHASubnetRouterFailover(t *testing.T) {
 | 
						|
	IntegrationSkip(t)
 | 
						|
	t.Parallel()
 | 
						|
 | 
						|
	user := "enable-routing"
 | 
						|
 | 
						|
	scenario, err := NewScenario(dockertestMaxWait())
 | 
						|
	assertNoErrf(t, "failed to create scenario: %s", err)
 | 
						|
	defer scenario.ShutdownAssertNoPanics(t)
 | 
						|
 | 
						|
	spec := map[string]int{
 | 
						|
		user: 3,
 | 
						|
	}
 | 
						|
 | 
						|
	err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clienableroute"))
 | 
						|
	assertNoErrHeadscaleEnv(t, err)
 | 
						|
 | 
						|
	allClients, err := scenario.ListTailscaleClients()
 | 
						|
	assertNoErrListClients(t, err)
 | 
						|
 | 
						|
	err = scenario.WaitForTailscaleSync()
 | 
						|
	assertNoErrSync(t, err)
 | 
						|
 | 
						|
	headscale, err := scenario.Headscale()
 | 
						|
	assertNoErrGetHeadscale(t, err)
 | 
						|
 | 
						|
	expectedRoutes := map[string]string{
 | 
						|
		"1": "10.0.0.0/24",
 | 
						|
		"2": "10.0.0.0/24",
 | 
						|
	}
 | 
						|
 | 
						|
	// Sort nodes by ID
 | 
						|
	sort.SliceStable(allClients, func(i, j int) bool {
 | 
						|
		statusI, err := allClients[i].Status()
 | 
						|
		if err != nil {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
 | 
						|
		statusJ, err := allClients[j].Status()
 | 
						|
		if err != nil {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
 | 
						|
		return statusI.Self.ID < statusJ.Self.ID
 | 
						|
	})
 | 
						|
 | 
						|
	subRouter1 := allClients[0]
 | 
						|
	subRouter2 := allClients[1]
 | 
						|
 | 
						|
	client := allClients[2]
 | 
						|
 | 
						|
	t.Logf("Advertise route from r1 (%s) and r2 (%s), making it HA, n1 is primary", subRouter1.Hostname(), subRouter2.Hostname())
 | 
						|
	// advertise HA route on node 1 and 2
 | 
						|
	// ID 1 will be primary
 | 
						|
	// ID 2 will be secondary
 | 
						|
	for _, client := range allClients[:2] {
 | 
						|
		status, err := client.Status()
 | 
						|
		assertNoErr(t, err)
 | 
						|
 | 
						|
		if route, ok := expectedRoutes[string(status.Self.ID)]; ok {
 | 
						|
			command := []string{
 | 
						|
				"tailscale",
 | 
						|
				"set",
 | 
						|
				"--advertise-routes=" + route,
 | 
						|
			}
 | 
						|
			_, _, err = client.Execute(command)
 | 
						|
			assertNoErrf(t, "failed to advertise route: %s", err)
 | 
						|
		} else {
 | 
						|
			t.Fatalf("failed to find route for Node %s (id: %s)", status.Self.HostName, status.Self.ID)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	err = scenario.WaitForTailscaleSync()
 | 
						|
	assertNoErrSync(t, err)
 | 
						|
 | 
						|
	var routes []*v1.Route
 | 
						|
	err = executeAndUnmarshal(
 | 
						|
		headscale,
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"list",
 | 
						|
			"--output",
 | 
						|
			"json",
 | 
						|
		},
 | 
						|
		&routes,
 | 
						|
	)
 | 
						|
 | 
						|
	assertNoErr(t, err)
 | 
						|
	assert.Len(t, routes, 2)
 | 
						|
 | 
						|
	t.Logf("initial routes %#v", routes)
 | 
						|
 | 
						|
	for _, route := range routes {
 | 
						|
		assert.True(t, route.GetAdvertised())
 | 
						|
		assert.False(t, route.GetEnabled())
 | 
						|
		assert.False(t, route.GetIsPrimary())
 | 
						|
	}
 | 
						|
 | 
						|
	// Verify that no routes has been sent to the client,
 | 
						|
	// they are not yet enabled.
 | 
						|
	for _, client := range allClients {
 | 
						|
		status, err := client.Status()
 | 
						|
		assertNoErr(t, err)
 | 
						|
 | 
						|
		for _, peerKey := range status.Peers() {
 | 
						|
			peerStatus := status.Peer[peerKey]
 | 
						|
 | 
						|
			assert.Nil(t, peerStatus.PrimaryRoutes)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Enable all routes
 | 
						|
	for _, route := range routes {
 | 
						|
		_, err = headscale.Execute(
 | 
						|
			[]string{
 | 
						|
				"headscale",
 | 
						|
				"routes",
 | 
						|
				"enable",
 | 
						|
				"--route",
 | 
						|
				strconv.Itoa(int(route.GetId())),
 | 
						|
			})
 | 
						|
		assertNoErr(t, err)
 | 
						|
 | 
						|
		time.Sleep(time.Second)
 | 
						|
	}
 | 
						|
 | 
						|
	var enablingRoutes []*v1.Route
 | 
						|
	err = executeAndUnmarshal(
 | 
						|
		headscale,
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"list",
 | 
						|
			"--output",
 | 
						|
			"json",
 | 
						|
		},
 | 
						|
		&enablingRoutes,
 | 
						|
	)
 | 
						|
	assertNoErr(t, err)
 | 
						|
	assert.Len(t, enablingRoutes, 2)
 | 
						|
 | 
						|
	// Node 1 is primary
 | 
						|
	assert.True(t, enablingRoutes[0].GetAdvertised())
 | 
						|
	assert.True(t, enablingRoutes[0].GetEnabled())
 | 
						|
	assert.True(t, enablingRoutes[0].GetIsPrimary(), "both subnet routers are up, expected r1 to be primary")
 | 
						|
 | 
						|
	// Node 2 is not primary
 | 
						|
	assert.True(t, enablingRoutes[1].GetAdvertised())
 | 
						|
	assert.True(t, enablingRoutes[1].GetEnabled())
 | 
						|
	assert.False(t, enablingRoutes[1].GetIsPrimary(), "both subnet routers are up, expected r2 to be non-primary")
 | 
						|
 | 
						|
	// Verify that the client has routes from the primary machine
 | 
						|
	srs1, err := subRouter1.Status()
 | 
						|
	srs2, err := subRouter2.Status()
 | 
						|
 | 
						|
	clientStatus, err := client.Status()
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	srs1PeerStatus := clientStatus.Peer[srs1.Self.PublicKey]
 | 
						|
	srs2PeerStatus := clientStatus.Peer[srs2.Self.PublicKey]
 | 
						|
 | 
						|
	assert.True(t, srs1PeerStatus.Online, "r1 up, r2 up")
 | 
						|
	assert.True(t, srs2PeerStatus.Online, "r1 up, r2 up")
 | 
						|
 | 
						|
	assertNotNil(t, srs1PeerStatus.PrimaryRoutes)
 | 
						|
	assert.Nil(t, srs2PeerStatus.PrimaryRoutes)
 | 
						|
 | 
						|
	assert.Contains(
 | 
						|
		t,
 | 
						|
		srs1PeerStatus.PrimaryRoutes.AsSlice(),
 | 
						|
		netip.MustParsePrefix(expectedRoutes[string(srs1.Self.ID)]),
 | 
						|
	)
 | 
						|
 | 
						|
	// Take down the current primary
 | 
						|
	t.Logf("taking down subnet router r1 (%s)", subRouter1.Hostname())
 | 
						|
	t.Logf("expecting r2 (%s) to take over as primary", subRouter2.Hostname())
 | 
						|
	err = subRouter1.Down()
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	time.Sleep(5 * time.Second)
 | 
						|
 | 
						|
	var routesAfterMove []*v1.Route
 | 
						|
	err = executeAndUnmarshal(
 | 
						|
		headscale,
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"list",
 | 
						|
			"--output",
 | 
						|
			"json",
 | 
						|
		},
 | 
						|
		&routesAfterMove,
 | 
						|
	)
 | 
						|
	assertNoErr(t, err)
 | 
						|
	assert.Len(t, routesAfterMove, 2)
 | 
						|
 | 
						|
	// Node 1 is not primary
 | 
						|
	assert.True(t, routesAfterMove[0].GetAdvertised())
 | 
						|
	assert.True(t, routesAfterMove[0].GetEnabled())
 | 
						|
	assert.False(t, routesAfterMove[0].GetIsPrimary(), "r1 is down, expected r2 to be primary")
 | 
						|
 | 
						|
	// Node 2 is primary
 | 
						|
	assert.True(t, routesAfterMove[1].GetAdvertised())
 | 
						|
	assert.True(t, routesAfterMove[1].GetEnabled())
 | 
						|
	assert.True(t, routesAfterMove[1].GetIsPrimary(), "r1 is down, expected r2 to be primary")
 | 
						|
 | 
						|
	srs2, err = subRouter2.Status()
 | 
						|
 | 
						|
	clientStatus, err = client.Status()
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
 | 
						|
	srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]
 | 
						|
 | 
						|
	assert.False(t, srs1PeerStatus.Online, "r1 down, r2 down")
 | 
						|
	assert.True(t, srs2PeerStatus.Online, "r1 down, r2 up")
 | 
						|
 | 
						|
	assert.Nil(t, srs1PeerStatus.PrimaryRoutes)
 | 
						|
	assertNotNil(t, srs2PeerStatus.PrimaryRoutes)
 | 
						|
 | 
						|
	if srs2PeerStatus.PrimaryRoutes != nil {
 | 
						|
		assert.Contains(
 | 
						|
			t,
 | 
						|
			srs2PeerStatus.PrimaryRoutes.AsSlice(),
 | 
						|
			netip.MustParsePrefix(expectedRoutes[string(srs2.Self.ID)]),
 | 
						|
		)
 | 
						|
	}
 | 
						|
 | 
						|
	// Take down subnet router 2, leaving none available
 | 
						|
	t.Logf("taking down subnet router r2 (%s)", subRouter2.Hostname())
 | 
						|
	t.Logf("expecting r2 (%s) to remain primary, no other available", subRouter2.Hostname())
 | 
						|
	err = subRouter2.Down()
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	time.Sleep(5 * time.Second)
 | 
						|
 | 
						|
	var routesAfterBothDown []*v1.Route
 | 
						|
	err = executeAndUnmarshal(
 | 
						|
		headscale,
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"list",
 | 
						|
			"--output",
 | 
						|
			"json",
 | 
						|
		},
 | 
						|
		&routesAfterBothDown,
 | 
						|
	)
 | 
						|
	assertNoErr(t, err)
 | 
						|
	assert.Len(t, routesAfterBothDown, 2)
 | 
						|
 | 
						|
	// Node 1 is not primary
 | 
						|
	assert.True(t, routesAfterBothDown[0].GetAdvertised())
 | 
						|
	assert.True(t, routesAfterBothDown[0].GetEnabled())
 | 
						|
	assert.False(t, routesAfterBothDown[0].GetIsPrimary(), "r1 and r2 is down, expected r2 to _still_ be primary")
 | 
						|
 | 
						|
	// Node 2 is primary
 | 
						|
	// if the node goes down, but no other suitable route is
 | 
						|
	// available, keep the last known good route.
 | 
						|
	assert.True(t, routesAfterBothDown[1].GetAdvertised())
 | 
						|
	assert.True(t, routesAfterBothDown[1].GetEnabled())
 | 
						|
	assert.True(t, routesAfterBothDown[1].GetIsPrimary(), "r1 and r2 is down, expected r2 to _still_ be primary")
 | 
						|
 | 
						|
	// TODO(kradalby): Check client status
 | 
						|
	// Both are expected to be down
 | 
						|
 | 
						|
	// Verify that the route is not presented from either router
 | 
						|
	clientStatus, err = client.Status()
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
 | 
						|
	srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]
 | 
						|
 | 
						|
	assert.False(t, srs1PeerStatus.Online, "r1 down, r2 down")
 | 
						|
	assert.False(t, srs2PeerStatus.Online, "r1 down, r2 down")
 | 
						|
 | 
						|
	assert.Nil(t, srs1PeerStatus.PrimaryRoutes)
 | 
						|
	assertNotNil(t, srs2PeerStatus.PrimaryRoutes)
 | 
						|
 | 
						|
	if srs2PeerStatus.PrimaryRoutes != nil {
 | 
						|
		assert.Contains(
 | 
						|
			t,
 | 
						|
			srs2PeerStatus.PrimaryRoutes.AsSlice(),
 | 
						|
			netip.MustParsePrefix(expectedRoutes[string(srs2.Self.ID)]),
 | 
						|
		)
 | 
						|
	}
 | 
						|
 | 
						|
	// Bring up subnet router 1, making the route available from there.
 | 
						|
	t.Logf("bringing up subnet router r1 (%s)", subRouter1.Hostname())
 | 
						|
	t.Logf("expecting r1 (%s) to take over as primary (only one online)", subRouter1.Hostname())
 | 
						|
	err = subRouter1.Up()
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	time.Sleep(5 * time.Second)
 | 
						|
 | 
						|
	var routesAfter1Up []*v1.Route
 | 
						|
	err = executeAndUnmarshal(
 | 
						|
		headscale,
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"list",
 | 
						|
			"--output",
 | 
						|
			"json",
 | 
						|
		},
 | 
						|
		&routesAfter1Up,
 | 
						|
	)
 | 
						|
	assertNoErr(t, err)
 | 
						|
	assert.Len(t, routesAfter1Up, 2)
 | 
						|
 | 
						|
	// Node 1 is primary
 | 
						|
	assert.True(t, routesAfter1Up[0].GetAdvertised())
 | 
						|
	assert.True(t, routesAfter1Up[0].GetEnabled())
 | 
						|
	assert.True(t, routesAfter1Up[0].GetIsPrimary(), "r1 is back up, expected r1 to become be primary")
 | 
						|
 | 
						|
	// Node 2 is not primary
 | 
						|
	assert.True(t, routesAfter1Up[1].GetAdvertised())
 | 
						|
	assert.True(t, routesAfter1Up[1].GetEnabled())
 | 
						|
	assert.False(t, routesAfter1Up[1].GetIsPrimary(), "r1 is back up, expected r1 to become be primary")
 | 
						|
 | 
						|
	// Verify that the route is announced from subnet router 1
 | 
						|
	clientStatus, err = client.Status()
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
 | 
						|
	srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]
 | 
						|
 | 
						|
	assert.True(t, srs1PeerStatus.Online, "r1 is back up, r2 down")
 | 
						|
	assert.False(t, srs2PeerStatus.Online, "r1 is back up, r2 down")
 | 
						|
 | 
						|
	assert.NotNil(t, srs1PeerStatus.PrimaryRoutes)
 | 
						|
	assert.Nil(t, srs2PeerStatus.PrimaryRoutes)
 | 
						|
 | 
						|
	if srs1PeerStatus.PrimaryRoutes != nil {
 | 
						|
		assert.Contains(
 | 
						|
			t,
 | 
						|
			srs1PeerStatus.PrimaryRoutes.AsSlice(),
 | 
						|
			netip.MustParsePrefix(expectedRoutes[string(srs1.Self.ID)]),
 | 
						|
		)
 | 
						|
	}
 | 
						|
 | 
						|
	// Bring up subnet router 2, should result in no change.
 | 
						|
	t.Logf("bringing up subnet router r2 (%s)", subRouter2.Hostname())
 | 
						|
	t.Logf("both online, expecting r1 (%s) to still be primary (no flapping)", subRouter1.Hostname())
 | 
						|
	err = subRouter2.Up()
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	time.Sleep(5 * time.Second)
 | 
						|
 | 
						|
	var routesAfter2Up []*v1.Route
 | 
						|
	err = executeAndUnmarshal(
 | 
						|
		headscale,
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"list",
 | 
						|
			"--output",
 | 
						|
			"json",
 | 
						|
		},
 | 
						|
		&routesAfter2Up,
 | 
						|
	)
 | 
						|
	assertNoErr(t, err)
 | 
						|
	assert.Len(t, routesAfter2Up, 2)
 | 
						|
 | 
						|
	// Node 1 is not primary
 | 
						|
	assert.True(t, routesAfter2Up[0].GetAdvertised())
 | 
						|
	assert.True(t, routesAfter2Up[0].GetEnabled())
 | 
						|
	assert.True(t, routesAfter2Up[0].GetIsPrimary(), "r1 and r2 is back up, expected r1 to _still_ be primary")
 | 
						|
 | 
						|
	// Node 2 is primary
 | 
						|
	assert.True(t, routesAfter2Up[1].GetAdvertised())
 | 
						|
	assert.True(t, routesAfter2Up[1].GetEnabled())
 | 
						|
	assert.False(t, routesAfter2Up[1].GetIsPrimary(), "r1 and r2 is back up, expected r1 to _still_ be primary")
 | 
						|
 | 
						|
	// Verify that the route is announced from subnet router 1
 | 
						|
	clientStatus, err = client.Status()
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
 | 
						|
	srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]
 | 
						|
 | 
						|
	assert.True(t, srs1PeerStatus.Online, "r1 up, r2 up")
 | 
						|
	assert.True(t, srs2PeerStatus.Online, "r1 up, r2 up")
 | 
						|
 | 
						|
	assert.NotNil(t, srs1PeerStatus.PrimaryRoutes)
 | 
						|
	assert.Nil(t, srs2PeerStatus.PrimaryRoutes)
 | 
						|
 | 
						|
	if srs1PeerStatus.PrimaryRoutes != nil {
 | 
						|
		assert.Contains(
 | 
						|
			t,
 | 
						|
			srs1PeerStatus.PrimaryRoutes.AsSlice(),
 | 
						|
			netip.MustParsePrefix(expectedRoutes[string(srs1.Self.ID)]),
 | 
						|
		)
 | 
						|
	}
 | 
						|
 | 
						|
	// Disable the route of subnet router 1, making it failover to 2
 | 
						|
	t.Logf("disabling route in subnet router r1 (%s)", subRouter1.Hostname())
 | 
						|
	t.Logf("expecting route to failover to r2 (%s), which is still available", subRouter2.Hostname())
 | 
						|
	_, err = headscale.Execute(
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"disable",
 | 
						|
			"--route",
 | 
						|
			fmt.Sprintf("%d", routesAfter2Up[0].GetId()),
 | 
						|
		})
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	time.Sleep(5 * time.Second)
 | 
						|
 | 
						|
	var routesAfterDisabling1 []*v1.Route
 | 
						|
	err = executeAndUnmarshal(
 | 
						|
		headscale,
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"list",
 | 
						|
			"--output",
 | 
						|
			"json",
 | 
						|
		},
 | 
						|
		&routesAfterDisabling1,
 | 
						|
	)
 | 
						|
	assertNoErr(t, err)
 | 
						|
	assert.Len(t, routesAfterDisabling1, 2)
 | 
						|
 | 
						|
	t.Logf("routes after disabling r1 %#v", routesAfterDisabling1)
 | 
						|
 | 
						|
	// Node 1 is not primary
 | 
						|
	assert.True(t, routesAfterDisabling1[0].GetAdvertised())
 | 
						|
	assert.False(t, routesAfterDisabling1[0].GetEnabled())
 | 
						|
	assert.False(t, routesAfterDisabling1[0].GetIsPrimary())
 | 
						|
 | 
						|
	// Node 2 is primary
 | 
						|
	assert.True(t, routesAfterDisabling1[1].GetAdvertised())
 | 
						|
	assert.True(t, routesAfterDisabling1[1].GetEnabled())
 | 
						|
	assert.True(t, routesAfterDisabling1[1].GetIsPrimary())
 | 
						|
 | 
						|
	// Verify that the route is announced from subnet router 1
 | 
						|
	clientStatus, err = client.Status()
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
 | 
						|
	srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]
 | 
						|
 | 
						|
	assert.Nil(t, srs1PeerStatus.PrimaryRoutes)
 | 
						|
	assert.NotNil(t, srs2PeerStatus.PrimaryRoutes)
 | 
						|
 | 
						|
	if srs2PeerStatus.PrimaryRoutes != nil {
 | 
						|
		assert.Contains(
 | 
						|
			t,
 | 
						|
			srs2PeerStatus.PrimaryRoutes.AsSlice(),
 | 
						|
			netip.MustParsePrefix(expectedRoutes[string(srs2.Self.ID)]),
 | 
						|
		)
 | 
						|
	}
 | 
						|
 | 
						|
	// enable the route of subnet router 1, no change expected
 | 
						|
	t.Logf("enabling route in subnet router 1 (%s)", subRouter1.Hostname())
 | 
						|
	t.Logf("both online, expecting r2 (%s) to still be primary (no flapping)", subRouter2.Hostname())
 | 
						|
	_, err = headscale.Execute(
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"enable",
 | 
						|
			"--route",
 | 
						|
			fmt.Sprintf("%d", routesAfter2Up[0].GetId()),
 | 
						|
		})
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	time.Sleep(5 * time.Second)
 | 
						|
 | 
						|
	var routesAfterEnabling1 []*v1.Route
 | 
						|
	err = executeAndUnmarshal(
 | 
						|
		headscale,
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"list",
 | 
						|
			"--output",
 | 
						|
			"json",
 | 
						|
		},
 | 
						|
		&routesAfterEnabling1,
 | 
						|
	)
 | 
						|
	assertNoErr(t, err)
 | 
						|
	assert.Len(t, routesAfterEnabling1, 2)
 | 
						|
 | 
						|
	// Node 1 is not primary
 | 
						|
	assert.True(t, routesAfterEnabling1[0].GetAdvertised())
 | 
						|
	assert.True(t, routesAfterEnabling1[0].GetEnabled())
 | 
						|
	assert.False(t, routesAfterEnabling1[0].GetIsPrimary())
 | 
						|
 | 
						|
	// Node 2 is primary
 | 
						|
	assert.True(t, routesAfterEnabling1[1].GetAdvertised())
 | 
						|
	assert.True(t, routesAfterEnabling1[1].GetEnabled())
 | 
						|
	assert.True(t, routesAfterEnabling1[1].GetIsPrimary())
 | 
						|
 | 
						|
	// Verify that the route is announced from subnet router 1
 | 
						|
	clientStatus, err = client.Status()
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
 | 
						|
	srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]
 | 
						|
 | 
						|
	assert.Nil(t, srs1PeerStatus.PrimaryRoutes)
 | 
						|
	assert.NotNil(t, srs2PeerStatus.PrimaryRoutes)
 | 
						|
 | 
						|
	if srs2PeerStatus.PrimaryRoutes != nil {
 | 
						|
		assert.Contains(
 | 
						|
			t,
 | 
						|
			srs2PeerStatus.PrimaryRoutes.AsSlice(),
 | 
						|
			netip.MustParsePrefix(expectedRoutes[string(srs2.Self.ID)]),
 | 
						|
		)
 | 
						|
	}
 | 
						|
 | 
						|
	// delete the route of subnet router 2, failover to one expected
 | 
						|
	t.Logf("deleting route in subnet router r2 (%s)", subRouter2.Hostname())
 | 
						|
	t.Logf("expecting route to failover to r1 (%s)", subRouter1.Hostname())
 | 
						|
	_, err = headscale.Execute(
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"delete",
 | 
						|
			"--route",
 | 
						|
			fmt.Sprintf("%d", routesAfterEnabling1[1].GetId()),
 | 
						|
		})
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	time.Sleep(5 * time.Second)
 | 
						|
 | 
						|
	var routesAfterDeleting2 []*v1.Route
 | 
						|
	err = executeAndUnmarshal(
 | 
						|
		headscale,
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"list",
 | 
						|
			"--output",
 | 
						|
			"json",
 | 
						|
		},
 | 
						|
		&routesAfterDeleting2,
 | 
						|
	)
 | 
						|
	assertNoErr(t, err)
 | 
						|
	assert.Len(t, routesAfterDeleting2, 1)
 | 
						|
 | 
						|
	t.Logf("routes after deleting r2 %#v", routesAfterDeleting2)
 | 
						|
 | 
						|
	// Node 1 is primary
 | 
						|
	assert.True(t, routesAfterDeleting2[0].GetAdvertised())
 | 
						|
	assert.True(t, routesAfterDeleting2[0].GetEnabled())
 | 
						|
	assert.True(t, routesAfterDeleting2[0].GetIsPrimary())
 | 
						|
 | 
						|
	// Verify that the route is announced from subnet router 1
 | 
						|
	clientStatus, err = client.Status()
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
 | 
						|
	srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]
 | 
						|
 | 
						|
	assertNotNil(t, srs1PeerStatus.PrimaryRoutes)
 | 
						|
	assert.Nil(t, srs2PeerStatus.PrimaryRoutes)
 | 
						|
 | 
						|
	if srs1PeerStatus.PrimaryRoutes != nil {
 | 
						|
		assert.Contains(
 | 
						|
			t,
 | 
						|
			srs1PeerStatus.PrimaryRoutes.AsSlice(),
 | 
						|
			netip.MustParsePrefix(expectedRoutes[string(srs1.Self.ID)]),
 | 
						|
		)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestEnableDisableAutoApprovedRoute(t *testing.T) {
 | 
						|
	IntegrationSkip(t)
 | 
						|
	t.Parallel()
 | 
						|
 | 
						|
	expectedRoutes := "172.0.0.0/24"
 | 
						|
 | 
						|
	user := "enable-disable-routing"
 | 
						|
 | 
						|
	scenario, err := NewScenario(dockertestMaxWait())
 | 
						|
	assertNoErrf(t, "failed to create scenario: %s", err)
 | 
						|
	defer scenario.ShutdownAssertNoPanics(t)
 | 
						|
 | 
						|
	spec := map[string]int{
 | 
						|
		user: 1,
 | 
						|
	}
 | 
						|
 | 
						|
	err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithTags([]string{"tag:approve"})}, hsic.WithTestName("clienableroute"), hsic.WithACLPolicy(
 | 
						|
		&policy.ACLPolicy{
 | 
						|
			ACLs: []policy.ACL{
 | 
						|
				{
 | 
						|
					Action:       "accept",
 | 
						|
					Sources:      []string{"*"},
 | 
						|
					Destinations: []string{"*:*"},
 | 
						|
				},
 | 
						|
			},
 | 
						|
			TagOwners: map[string][]string{
 | 
						|
				"tag:approve": {user},
 | 
						|
			},
 | 
						|
			AutoApprovers: policy.AutoApprovers{
 | 
						|
				Routes: map[string][]string{
 | 
						|
					expectedRoutes: {"tag:approve"},
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	))
 | 
						|
	assertNoErrHeadscaleEnv(t, err)
 | 
						|
 | 
						|
	allClients, err := scenario.ListTailscaleClients()
 | 
						|
	assertNoErrListClients(t, err)
 | 
						|
 | 
						|
	err = scenario.WaitForTailscaleSync()
 | 
						|
	assertNoErrSync(t, err)
 | 
						|
 | 
						|
	headscale, err := scenario.Headscale()
 | 
						|
	assertNoErrGetHeadscale(t, err)
 | 
						|
 | 
						|
	subRouter1 := allClients[0]
 | 
						|
 | 
						|
	// Initially advertise route
 | 
						|
	command := []string{
 | 
						|
		"tailscale",
 | 
						|
		"set",
 | 
						|
		"--advertise-routes=" + expectedRoutes,
 | 
						|
	}
 | 
						|
	_, _, err = subRouter1.Execute(command)
 | 
						|
	assertNoErrf(t, "failed to advertise route: %s", err)
 | 
						|
 | 
						|
	time.Sleep(10 * time.Second)
 | 
						|
 | 
						|
	var routes []*v1.Route
 | 
						|
	err = executeAndUnmarshal(
 | 
						|
		headscale,
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"list",
 | 
						|
			"--output",
 | 
						|
			"json",
 | 
						|
		},
 | 
						|
		&routes,
 | 
						|
	)
 | 
						|
	assertNoErr(t, err)
 | 
						|
	assert.Len(t, routes, 1)
 | 
						|
 | 
						|
	// All routes should be auto approved and enabled
 | 
						|
	assert.True(t, routes[0].GetAdvertised())
 | 
						|
	assert.True(t, routes[0].GetEnabled())
 | 
						|
	assert.True(t, routes[0].GetIsPrimary())
 | 
						|
 | 
						|
	// Stop advertising route
 | 
						|
	command = []string{
 | 
						|
		"tailscale",
 | 
						|
		"set",
 | 
						|
		"--advertise-routes=",
 | 
						|
	}
 | 
						|
	_, _, err = subRouter1.Execute(command)
 | 
						|
	assertNoErrf(t, "failed to remove advertised route: %s", err)
 | 
						|
 | 
						|
	time.Sleep(10 * time.Second)
 | 
						|
 | 
						|
	var notAdvertisedRoutes []*v1.Route
 | 
						|
	err = executeAndUnmarshal(
 | 
						|
		headscale,
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"list",
 | 
						|
			"--output",
 | 
						|
			"json",
 | 
						|
		},
 | 
						|
		¬AdvertisedRoutes,
 | 
						|
	)
 | 
						|
	assertNoErr(t, err)
 | 
						|
	assert.Len(t, notAdvertisedRoutes, 1)
 | 
						|
 | 
						|
	// Route is no longer advertised
 | 
						|
	assert.False(t, notAdvertisedRoutes[0].GetAdvertised())
 | 
						|
	assert.False(t, notAdvertisedRoutes[0].GetEnabled())
 | 
						|
	assert.True(t, notAdvertisedRoutes[0].GetIsPrimary())
 | 
						|
 | 
						|
	// Advertise route again
 | 
						|
	command = []string{
 | 
						|
		"tailscale",
 | 
						|
		"set",
 | 
						|
		"--advertise-routes=" + expectedRoutes,
 | 
						|
	}
 | 
						|
	_, _, err = subRouter1.Execute(command)
 | 
						|
	assertNoErrf(t, "failed to advertise route: %s", err)
 | 
						|
 | 
						|
	time.Sleep(10 * time.Second)
 | 
						|
 | 
						|
	var reAdvertisedRoutes []*v1.Route
 | 
						|
	err = executeAndUnmarshal(
 | 
						|
		headscale,
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"list",
 | 
						|
			"--output",
 | 
						|
			"json",
 | 
						|
		},
 | 
						|
		&reAdvertisedRoutes,
 | 
						|
	)
 | 
						|
	assertNoErr(t, err)
 | 
						|
	assert.Len(t, reAdvertisedRoutes, 1)
 | 
						|
 | 
						|
	// All routes should be auto approved and enabled
 | 
						|
	assert.True(t, reAdvertisedRoutes[0].GetAdvertised())
 | 
						|
	assert.True(t, reAdvertisedRoutes[0].GetEnabled())
 | 
						|
	assert.True(t, reAdvertisedRoutes[0].GetIsPrimary())
 | 
						|
}
 | 
						|
 | 
						|
func TestAutoApprovedSubRoute2068(t *testing.T) {
 | 
						|
	IntegrationSkip(t)
 | 
						|
	t.Parallel()
 | 
						|
 | 
						|
	expectedRoutes := "10.42.7.0/24"
 | 
						|
 | 
						|
	user := "subroute"
 | 
						|
 | 
						|
	scenario, err := NewScenario(dockertestMaxWait())
 | 
						|
	assertNoErrf(t, "failed to create scenario: %s", err)
 | 
						|
	defer scenario.ShutdownAssertNoPanics(t)
 | 
						|
 | 
						|
	spec := map[string]int{
 | 
						|
		user: 1,
 | 
						|
	}
 | 
						|
 | 
						|
	err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithTags([]string{"tag:approve"})}, hsic.WithTestName("clienableroute"), hsic.WithACLPolicy(
 | 
						|
		&policy.ACLPolicy{
 | 
						|
			ACLs: []policy.ACL{
 | 
						|
				{
 | 
						|
					Action:       "accept",
 | 
						|
					Sources:      []string{"*"},
 | 
						|
					Destinations: []string{"*:*"},
 | 
						|
				},
 | 
						|
			},
 | 
						|
			TagOwners: map[string][]string{
 | 
						|
				"tag:approve": {user},
 | 
						|
			},
 | 
						|
			AutoApprovers: policy.AutoApprovers{
 | 
						|
				Routes: map[string][]string{
 | 
						|
					"10.42.0.0/16": {"tag:approve"},
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	))
 | 
						|
	assertNoErrHeadscaleEnv(t, err)
 | 
						|
 | 
						|
	allClients, err := scenario.ListTailscaleClients()
 | 
						|
	assertNoErrListClients(t, err)
 | 
						|
 | 
						|
	err = scenario.WaitForTailscaleSync()
 | 
						|
	assertNoErrSync(t, err)
 | 
						|
 | 
						|
	headscale, err := scenario.Headscale()
 | 
						|
	assertNoErrGetHeadscale(t, err)
 | 
						|
 | 
						|
	subRouter1 := allClients[0]
 | 
						|
 | 
						|
	// Initially advertise route
 | 
						|
	command := []string{
 | 
						|
		"tailscale",
 | 
						|
		"set",
 | 
						|
		"--advertise-routes=" + expectedRoutes,
 | 
						|
	}
 | 
						|
	_, _, err = subRouter1.Execute(command)
 | 
						|
	assertNoErrf(t, "failed to advertise route: %s", err)
 | 
						|
 | 
						|
	time.Sleep(10 * time.Second)
 | 
						|
 | 
						|
	var routes []*v1.Route
 | 
						|
	err = executeAndUnmarshal(
 | 
						|
		headscale,
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"list",
 | 
						|
			"--output",
 | 
						|
			"json",
 | 
						|
		},
 | 
						|
		&routes,
 | 
						|
	)
 | 
						|
	assertNoErr(t, err)
 | 
						|
	assert.Len(t, routes, 1)
 | 
						|
 | 
						|
	want := []*v1.Route{
 | 
						|
		{
 | 
						|
			Id:         1,
 | 
						|
			Prefix:     expectedRoutes,
 | 
						|
			Advertised: true,
 | 
						|
			Enabled:    true,
 | 
						|
			IsPrimary:  true,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	if diff := cmp.Diff(want, routes, cmpopts.IgnoreUnexported(v1.Route{}), cmpopts.IgnoreFields(v1.Route{}, "Node", "CreatedAt", "UpdatedAt", "DeletedAt")); diff != "" {
 | 
						|
		t.Errorf("unexpected routes (-want +got):\n%s", diff)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// TestSubnetRouteACL verifies that Subnet routes are distributed
 | 
						|
// as expected when ACLs are activated.
 | 
						|
// It implements the issue from
 | 
						|
// https://github.com/juanfont/headscale/issues/1604
 | 
						|
func TestSubnetRouteACL(t *testing.T) {
 | 
						|
	IntegrationSkip(t)
 | 
						|
	t.Parallel()
 | 
						|
 | 
						|
	user := "subnet-route-acl"
 | 
						|
 | 
						|
	scenario, err := NewScenario(dockertestMaxWait())
 | 
						|
	assertNoErrf(t, "failed to create scenario: %s", err)
 | 
						|
	defer scenario.ShutdownAssertNoPanics(t)
 | 
						|
 | 
						|
	spec := map[string]int{
 | 
						|
		user: 2,
 | 
						|
	}
 | 
						|
 | 
						|
	err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clienableroute"), hsic.WithACLPolicy(
 | 
						|
		&policy.ACLPolicy{
 | 
						|
			Groups: policy.Groups{
 | 
						|
				"group:admins": {user},
 | 
						|
			},
 | 
						|
			ACLs: []policy.ACL{
 | 
						|
				{
 | 
						|
					Action:       "accept",
 | 
						|
					Sources:      []string{"group:admins"},
 | 
						|
					Destinations: []string{"group:admins:*"},
 | 
						|
				},
 | 
						|
				{
 | 
						|
					Action:       "accept",
 | 
						|
					Sources:      []string{"group:admins"},
 | 
						|
					Destinations: []string{"10.33.0.0/16:*"},
 | 
						|
				},
 | 
						|
				// {
 | 
						|
				// 	Action:       "accept",
 | 
						|
				// 	Sources:      []string{"group:admins"},
 | 
						|
				// 	Destinations: []string{"0.0.0.0/0:*"},
 | 
						|
				// },
 | 
						|
			},
 | 
						|
		},
 | 
						|
	))
 | 
						|
	assertNoErrHeadscaleEnv(t, err)
 | 
						|
 | 
						|
	allClients, err := scenario.ListTailscaleClients()
 | 
						|
	assertNoErrListClients(t, err)
 | 
						|
 | 
						|
	err = scenario.WaitForTailscaleSync()
 | 
						|
	assertNoErrSync(t, err)
 | 
						|
 | 
						|
	headscale, err := scenario.Headscale()
 | 
						|
	assertNoErrGetHeadscale(t, err)
 | 
						|
 | 
						|
	expectedRoutes := map[string]string{
 | 
						|
		"1": "10.33.0.0/16",
 | 
						|
	}
 | 
						|
 | 
						|
	// Sort nodes by ID
 | 
						|
	sort.SliceStable(allClients, func(i, j int) bool {
 | 
						|
		statusI, err := allClients[i].Status()
 | 
						|
		if err != nil {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
 | 
						|
		statusJ, err := allClients[j].Status()
 | 
						|
		if err != nil {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
 | 
						|
		return statusI.Self.ID < statusJ.Self.ID
 | 
						|
	})
 | 
						|
 | 
						|
	subRouter1 := allClients[0]
 | 
						|
 | 
						|
	client := allClients[1]
 | 
						|
 | 
						|
	// advertise HA route on node 1 and 2
 | 
						|
	// ID 1 will be primary
 | 
						|
	// ID 2 will be secondary
 | 
						|
	for _, client := range allClients {
 | 
						|
		status, err := client.Status()
 | 
						|
		assertNoErr(t, err)
 | 
						|
 | 
						|
		if route, ok := expectedRoutes[string(status.Self.ID)]; ok {
 | 
						|
			command := []string{
 | 
						|
				"tailscale",
 | 
						|
				"set",
 | 
						|
				"--advertise-routes=" + route,
 | 
						|
			}
 | 
						|
			_, _, err = client.Execute(command)
 | 
						|
			assertNoErrf(t, "failed to advertise route: %s", err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	err = scenario.WaitForTailscaleSync()
 | 
						|
	assertNoErrSync(t, err)
 | 
						|
 | 
						|
	var routes []*v1.Route
 | 
						|
	err = executeAndUnmarshal(
 | 
						|
		headscale,
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"list",
 | 
						|
			"--output",
 | 
						|
			"json",
 | 
						|
		},
 | 
						|
		&routes,
 | 
						|
	)
 | 
						|
 | 
						|
	assertNoErr(t, err)
 | 
						|
	assert.Len(t, routes, 1)
 | 
						|
 | 
						|
	for _, route := range routes {
 | 
						|
		assert.True(t, route.GetAdvertised())
 | 
						|
		assert.False(t, route.GetEnabled())
 | 
						|
		assert.False(t, route.GetIsPrimary())
 | 
						|
	}
 | 
						|
 | 
						|
	// Verify that no routes has been sent to the client,
 | 
						|
	// they are not yet enabled.
 | 
						|
	for _, client := range allClients {
 | 
						|
		status, err := client.Status()
 | 
						|
		assertNoErr(t, err)
 | 
						|
 | 
						|
		for _, peerKey := range status.Peers() {
 | 
						|
			peerStatus := status.Peer[peerKey]
 | 
						|
 | 
						|
			assert.Nil(t, peerStatus.PrimaryRoutes)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Enable all routes
 | 
						|
	for _, route := range routes {
 | 
						|
		_, err = headscale.Execute(
 | 
						|
			[]string{
 | 
						|
				"headscale",
 | 
						|
				"routes",
 | 
						|
				"enable",
 | 
						|
				"--route",
 | 
						|
				strconv.Itoa(int(route.GetId())),
 | 
						|
			})
 | 
						|
		assertNoErr(t, err)
 | 
						|
	}
 | 
						|
 | 
						|
	time.Sleep(5 * time.Second)
 | 
						|
 | 
						|
	var enablingRoutes []*v1.Route
 | 
						|
	err = executeAndUnmarshal(
 | 
						|
		headscale,
 | 
						|
		[]string{
 | 
						|
			"headscale",
 | 
						|
			"routes",
 | 
						|
			"list",
 | 
						|
			"--output",
 | 
						|
			"json",
 | 
						|
		},
 | 
						|
		&enablingRoutes,
 | 
						|
	)
 | 
						|
	assertNoErr(t, err)
 | 
						|
	assert.Len(t, enablingRoutes, 1)
 | 
						|
 | 
						|
	// Node 1 has active route
 | 
						|
	assert.True(t, enablingRoutes[0].GetAdvertised())
 | 
						|
	assert.True(t, enablingRoutes[0].GetEnabled())
 | 
						|
	assert.True(t, enablingRoutes[0].GetIsPrimary())
 | 
						|
 | 
						|
	// Verify that the client has routes from the primary machine
 | 
						|
	srs1, _ := subRouter1.Status()
 | 
						|
 | 
						|
	clientStatus, err := client.Status()
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	srs1PeerStatus := clientStatus.Peer[srs1.Self.PublicKey]
 | 
						|
 | 
						|
	assertNotNil(t, srs1PeerStatus.PrimaryRoutes)
 | 
						|
 | 
						|
	t.Logf("subnet1 has following routes: %v", srs1PeerStatus.PrimaryRoutes.AsSlice())
 | 
						|
	assert.Len(t, srs1PeerStatus.PrimaryRoutes.AsSlice(), 1)
 | 
						|
	assert.Contains(
 | 
						|
		t,
 | 
						|
		srs1PeerStatus.PrimaryRoutes.AsSlice(),
 | 
						|
		netip.MustParsePrefix(expectedRoutes[string(srs1.Self.ID)]),
 | 
						|
	)
 | 
						|
 | 
						|
	clientNm, err := client.Netmap()
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	wantClientFilter := []filter.Match{
 | 
						|
		{
 | 
						|
			IPProto: views.SliceOf([]ipproto.Proto{
 | 
						|
				ipproto.TCP, ipproto.UDP, ipproto.ICMPv4, ipproto.ICMPv6,
 | 
						|
			}),
 | 
						|
			Srcs: []netip.Prefix{
 | 
						|
				netip.MustParsePrefix("100.64.0.1/32"),
 | 
						|
				netip.MustParsePrefix("100.64.0.2/32"),
 | 
						|
				netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
 | 
						|
				netip.MustParsePrefix("fd7a:115c:a1e0::2/128"),
 | 
						|
			},
 | 
						|
			Dsts: []filter.NetPortRange{
 | 
						|
				{
 | 
						|
					Net:   netip.MustParsePrefix("100.64.0.2/32"),
 | 
						|
					Ports: allPorts,
 | 
						|
				},
 | 
						|
				{
 | 
						|
					Net:   netip.MustParsePrefix("fd7a:115c:a1e0::2/128"),
 | 
						|
					Ports: allPorts,
 | 
						|
				},
 | 
						|
			},
 | 
						|
			Caps: []filter.CapMatch{},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	if diff := cmp.Diff(wantClientFilter, clientNm.PacketFilter, util.ViewSliceIPProtoComparer, util.PrefixComparer); diff != "" {
 | 
						|
		t.Errorf("Client (%s) filter, unexpected result (-want +got):\n%s", client.Hostname(), diff)
 | 
						|
	}
 | 
						|
 | 
						|
	subnetNm, err := subRouter1.Netmap()
 | 
						|
	assertNoErr(t, err)
 | 
						|
 | 
						|
	wantSubnetFilter := []filter.Match{
 | 
						|
		{
 | 
						|
			IPProto: views.SliceOf([]ipproto.Proto{
 | 
						|
				ipproto.TCP, ipproto.UDP, ipproto.ICMPv4, ipproto.ICMPv6,
 | 
						|
			}),
 | 
						|
			Srcs: []netip.Prefix{
 | 
						|
				netip.MustParsePrefix("100.64.0.1/32"),
 | 
						|
				netip.MustParsePrefix("100.64.0.2/32"),
 | 
						|
				netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
 | 
						|
				netip.MustParsePrefix("fd7a:115c:a1e0::2/128"),
 | 
						|
			},
 | 
						|
			Dsts: []filter.NetPortRange{
 | 
						|
				{
 | 
						|
					Net:   netip.MustParsePrefix("100.64.0.1/32"),
 | 
						|
					Ports: allPorts,
 | 
						|
				},
 | 
						|
				{
 | 
						|
					Net:   netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
 | 
						|
					Ports: allPorts,
 | 
						|
				},
 | 
						|
			},
 | 
						|
			Caps: []filter.CapMatch{},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			IPProto: views.SliceOf([]ipproto.Proto{
 | 
						|
				ipproto.TCP, ipproto.UDP, ipproto.ICMPv4, ipproto.ICMPv6,
 | 
						|
			}),
 | 
						|
			Srcs: []netip.Prefix{
 | 
						|
				netip.MustParsePrefix("100.64.0.1/32"),
 | 
						|
				netip.MustParsePrefix("100.64.0.2/32"),
 | 
						|
				netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
 | 
						|
				netip.MustParsePrefix("fd7a:115c:a1e0::2/128"),
 | 
						|
			},
 | 
						|
			Dsts: []filter.NetPortRange{
 | 
						|
				{
 | 
						|
					Net:   netip.MustParsePrefix("10.33.0.0/16"),
 | 
						|
					Ports: allPorts,
 | 
						|
				},
 | 
						|
			},
 | 
						|
			Caps: []filter.CapMatch{},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	if diff := cmp.Diff(wantSubnetFilter, subnetNm.PacketFilter, util.ViewSliceIPProtoComparer, util.PrefixComparer); diff != "" {
 | 
						|
		t.Errorf("Subnet (%s) filter, unexpected result (-want +got):\n%s", subRouter1.Hostname(), diff)
 | 
						|
	}
 | 
						|
}
 |