mirror of
https://github.com/juanfont/headscale.git
synced 2024-11-23 18:15:26 +00:00
1226 lines
30 KiB
Go
1226 lines
30 KiB
Go
package integration
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net/netip"
|
|
"sort"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
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/wgengine/filter"
|
|
)
|
|
|
|
// 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.Shutdown()
|
|
|
|
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.Equal(t, true, route.GetAdvertised())
|
|
assert.Equal(t, false, route.GetEnabled())
|
|
assert.Equal(t, false, 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.Equal(t, true, route.GetAdvertised())
|
|
assert.Equal(t, true, route.GetEnabled())
|
|
assert.Equal(t, true, 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.Equal(t, true, route.GetAdvertised())
|
|
|
|
if route.GetId() == routeToBeDisabled.GetId() {
|
|
assert.Equal(t, false, route.GetEnabled())
|
|
|
|
// since this is the only route of this cidr,
|
|
// it will not failover, and remain Primary
|
|
// until something can replace it.
|
|
assert.Equal(t, true, route.GetIsPrimary())
|
|
} else {
|
|
assert.Equal(t, true, route.GetEnabled())
|
|
assert.Equal(t, true, 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.Shutdown()
|
|
|
|
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.Equal(t, true, route.GetAdvertised())
|
|
assert.Equal(t, false, route.GetEnabled())
|
|
assert.Equal(t, false, 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.Equal(t, true, enablingRoutes[0].GetAdvertised())
|
|
assert.Equal(t, true, enablingRoutes[0].GetEnabled())
|
|
assert.Equal(t, true, enablingRoutes[0].GetIsPrimary(), "both subnet routers are up, expected r1 to be primary")
|
|
|
|
// Node 2 is not primary
|
|
assert.Equal(t, true, enablingRoutes[1].GetAdvertised())
|
|
assert.Equal(t, true, enablingRoutes[1].GetEnabled())
|
|
assert.Equal(t, false, 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.Equal(t, true, routesAfterMove[0].GetAdvertised())
|
|
assert.Equal(t, true, routesAfterMove[0].GetEnabled())
|
|
assert.Equal(t, false, routesAfterMove[0].GetIsPrimary(), "r1 is down, expected r2 to be primary")
|
|
|
|
// Node 2 is primary
|
|
assert.Equal(t, true, routesAfterMove[1].GetAdvertised())
|
|
assert.Equal(t, true, routesAfterMove[1].GetEnabled())
|
|
assert.Equal(t, true, 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.Equal(t, true, routesAfterBothDown[0].GetAdvertised())
|
|
assert.Equal(t, true, routesAfterBothDown[0].GetEnabled())
|
|
assert.Equal(t, false, 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.Equal(t, true, routesAfterBothDown[1].GetAdvertised())
|
|
assert.Equal(t, true, routesAfterBothDown[1].GetEnabled())
|
|
assert.Equal(t, true, 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.Equal(t, true, routesAfter1Up[0].GetAdvertised())
|
|
assert.Equal(t, true, routesAfter1Up[0].GetEnabled())
|
|
assert.Equal(t, true, routesAfter1Up[0].GetIsPrimary(), "r1 is back up, expected r1 to become be primary")
|
|
|
|
// Node 2 is not primary
|
|
assert.Equal(t, true, routesAfter1Up[1].GetAdvertised())
|
|
assert.Equal(t, true, routesAfter1Up[1].GetEnabled())
|
|
assert.Equal(t, false, 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.Equal(t, true, routesAfter2Up[0].GetAdvertised())
|
|
assert.Equal(t, true, routesAfter2Up[0].GetEnabled())
|
|
assert.Equal(t, true, routesAfter2Up[0].GetIsPrimary(), "r1 and r2 is back up, expected r1 to _still_ be primary")
|
|
|
|
// Node 2 is primary
|
|
assert.Equal(t, true, routesAfter2Up[1].GetAdvertised())
|
|
assert.Equal(t, true, routesAfter2Up[1].GetEnabled())
|
|
assert.Equal(t, false, 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.Equal(t, true, routesAfterDisabling1[0].GetAdvertised())
|
|
assert.Equal(t, false, routesAfterDisabling1[0].GetEnabled())
|
|
assert.Equal(t, false, routesAfterDisabling1[0].GetIsPrimary())
|
|
|
|
// Node 2 is primary
|
|
assert.Equal(t, true, routesAfterDisabling1[1].GetAdvertised())
|
|
assert.Equal(t, true, routesAfterDisabling1[1].GetEnabled())
|
|
assert.Equal(t, true, 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.Equal(t, true, routesAfterEnabling1[0].GetAdvertised())
|
|
assert.Equal(t, true, routesAfterEnabling1[0].GetEnabled())
|
|
assert.Equal(t, false, routesAfterEnabling1[0].GetIsPrimary())
|
|
|
|
// Node 2 is primary
|
|
assert.Equal(t, true, routesAfterEnabling1[1].GetAdvertised())
|
|
assert.Equal(t, true, routesAfterEnabling1[1].GetEnabled())
|
|
assert.Equal(t, true, 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.Equal(t, true, routesAfterDeleting2[0].GetAdvertised())
|
|
assert.Equal(t, true, routesAfterDeleting2[0].GetEnabled())
|
|
assert.Equal(t, true, 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.Shutdown()
|
|
|
|
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.Equal(t, true, routes[0].GetAdvertised())
|
|
assert.Equal(t, true, routes[0].GetEnabled())
|
|
assert.Equal(t, true, 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.Equal(t, false, notAdvertisedRoutes[0].GetAdvertised())
|
|
assert.Equal(t, false, notAdvertisedRoutes[0].GetEnabled())
|
|
assert.Equal(t, true, 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.Equal(t, true, reAdvertisedRoutes[0].GetAdvertised())
|
|
assert.Equal(t, true, reAdvertisedRoutes[0].GetEnabled())
|
|
assert.Equal(t, true, reAdvertisedRoutes[0].GetIsPrimary())
|
|
}
|
|
|
|
// 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.Shutdown()
|
|
|
|
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.Equal(t, true, route.GetAdvertised())
|
|
assert.Equal(t, false, route.GetEnabled())
|
|
assert.Equal(t, false, 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.Equal(t, true, enablingRoutes[0].GetAdvertised())
|
|
assert.Equal(t, true, enablingRoutes[0].GetEnabled())
|
|
assert.Equal(t, true, 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: []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: filter.PortRange{0, 0xffff},
|
|
},
|
|
{
|
|
Net: netip.MustParsePrefix("fd7a:115c:a1e0::2/128"),
|
|
Ports: filter.PortRange{0, 0xffff},
|
|
},
|
|
},
|
|
Caps: []filter.CapMatch{},
|
|
},
|
|
}
|
|
|
|
if diff := cmp.Diff(wantClientFilter, clientNm.PacketFilter, 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: []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: filter.PortRange{0, 0xffff},
|
|
},
|
|
{
|
|
Net: netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
|
Ports: filter.PortRange{0, 0xffff},
|
|
},
|
|
},
|
|
Caps: []filter.CapMatch{},
|
|
},
|
|
{
|
|
IPProto: []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: filter.PortRange{0, 0xffff},
|
|
},
|
|
},
|
|
Caps: []filter.CapMatch{},
|
|
},
|
|
}
|
|
|
|
if diff := cmp.Diff(wantSubnetFilter, subnetNm.PacketFilter, util.PrefixComparer); diff != "" {
|
|
t.Errorf("Subnet (%s) filter, unexpected result (-want +got):\n%s", subRouter1.Hostname(), diff)
|
|
}
|
|
}
|