mirror of
https://github.com/juanfont/headscale.git
synced 2024-11-27 12:05:26 +00:00
83769ba715
This commits removes the locks used to guard data integrity for the database and replaces them with Transactions, turns out that SQL had a way to deal with this all along. This reduces the complexity we had with multiple locks that might stack or recurse (database, nofitifer, mapper). All notifications and state updates are now triggered _after_ a database change. Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
1202 lines
28 KiB
Go
1202 lines
28 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()
|
|
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())
|
|
assert.Equal(t, false, 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()
|
|
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]
|
|
|
|
// 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())
|
|
|
|
// 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())
|
|
|
|
// 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]
|
|
|
|
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 1 (%s)", subRouter1.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())
|
|
|
|
// 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())
|
|
|
|
// TODO(kradalby): Check client status
|
|
// Route is expected to be on SR2
|
|
|
|
srs2, err = subRouter2.Status()
|
|
|
|
clientStatus, err = client.Status()
|
|
assertNoErr(t, err)
|
|
|
|
srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
|
|
srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]
|
|
|
|
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 2 (%s)", 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())
|
|
|
|
// 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())
|
|
|
|
// 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.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 1 (%s)", 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())
|
|
|
|
// 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())
|
|
|
|
// 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.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 2 (%s)", subRouter2.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())
|
|
|
|
// 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())
|
|
|
|
// 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.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 1 (%s)", subRouter1.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 disabling1 %#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())
|
|
_, 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 2 (%s)", subRouter2.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 deleting2 %#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()
|
|
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()
|
|
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)
|
|
}
|
|
}
|