ensure online status and route changes are propagated (#1564)
This commit is contained in:
parent
0153e26392
commit
f65f4eca35
40 changed files with 3170 additions and 857 deletions
|
@ -4,7 +4,6 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -22,7 +21,7 @@ func executeAndUnmarshal[T any](headscale ControlServer, command []string, resul
|
|||
|
||||
err = json.Unmarshal([]byte(str), result)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to unmarshal: %s\n command err: %s", err, str)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -178,7 +177,11 @@ func TestPreAuthKeyCommand(t *testing.T) {
|
|||
assert.Equal(
|
||||
t,
|
||||
[]string{keys[0].GetId(), keys[1].GetId(), keys[2].GetId()},
|
||||
[]string{listedPreAuthKeys[1].GetId(), listedPreAuthKeys[2].GetId(), listedPreAuthKeys[3].GetId()},
|
||||
[]string{
|
||||
listedPreAuthKeys[1].GetId(),
|
||||
listedPreAuthKeys[2].GetId(),
|
||||
listedPreAuthKeys[3].GetId(),
|
||||
},
|
||||
)
|
||||
|
||||
assert.NotEmpty(t, listedPreAuthKeys[1].GetKey())
|
||||
|
@ -384,141 +387,6 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
|||
assert.Len(t, listedPreAuthKeys, 3)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// advertise routes using the up command
|
||||
for i, client := range allClients {
|
||||
routeStr := fmt.Sprintf("10.0.%d.0/24", i)
|
||||
command := []string{
|
||||
"tailscale",
|
||||
"set",
|
||||
"--advertise-routes=" + routeStr,
|
||||
}
|
||||
_, _, 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, route.GetAdvertised(), true)
|
||||
assert.Equal(t, route.GetEnabled(), false)
|
||||
assert.Equal(t, route.GetIsPrimary(), false)
|
||||
}
|
||||
|
||||
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, route.GetAdvertised(), true)
|
||||
assert.Equal(t, route.GetEnabled(), true)
|
||||
assert.Equal(t, route.GetIsPrimary(), true)
|
||||
}
|
||||
|
||||
routeIDToBeDisabled := enablingRoutes[0].GetId()
|
||||
|
||||
_, err = headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"routes",
|
||||
"disable",
|
||||
"--route",
|
||||
strconv.Itoa(int(routeIDToBeDisabled)),
|
||||
})
|
||||
assertNoErr(t, err)
|
||||
|
||||
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() == routeIDToBeDisabled {
|
||||
assert.Equal(t, route.GetEnabled(), false)
|
||||
assert.Equal(t, route.GetIsPrimary(), false)
|
||||
} else {
|
||||
assert.Equal(t, route.GetEnabled(), true)
|
||||
assert.Equal(t, route.GetIsPrimary(), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApiKeyCommand(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
t.Parallel()
|
||||
|
|
|
@ -44,6 +44,9 @@ func TestDERPServerScenario(t *testing.T) {
|
|||
headscaleConfig["HEADSCALE_DERP_SERVER_REGION_NAME"] = "Headscale Embedded DERP"
|
||||
headscaleConfig["HEADSCALE_DERP_SERVER_STUN_LISTEN_ADDR"] = "0.0.0.0:3478"
|
||||
headscaleConfig["HEADSCALE_DERP_SERVER_PRIVATE_KEY_PATH"] = "/tmp/derp.key"
|
||||
// Envknob for enabling DERP debug logs
|
||||
headscaleConfig["DERP_DEBUG_LOGS"] = "true"
|
||||
headscaleConfig["DERP_PROBER_DEBUG_LOGS"] = "true"
|
||||
|
||||
err = scenario.CreateHeadscaleEnv(
|
||||
spec,
|
||||
|
|
|
@ -14,6 +14,8 @@ import (
|
|||
"github.com/rs/zerolog/log"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
func TestPingAllByIP(t *testing.T) {
|
||||
|
@ -248,9 +250,8 @@ func TestPingAllByHostname(t *testing.T) {
|
|||
defer scenario.Shutdown()
|
||||
|
||||
spec := map[string]int{
|
||||
// Omit 1.16.2 (-1) because it does not have the FQDN field
|
||||
"user3": len(MustTestVersions) - 1,
|
||||
"user4": len(MustTestVersions) - 1,
|
||||
"user3": len(MustTestVersions),
|
||||
"user4": len(MustTestVersions),
|
||||
}
|
||||
|
||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("pingallbyname"))
|
||||
|
@ -296,8 +297,7 @@ func TestTaildrop(t *testing.T) {
|
|||
defer scenario.Shutdown()
|
||||
|
||||
spec := map[string]int{
|
||||
// Omit 1.16.2 (-1) because it does not have the FQDN field
|
||||
"taildrop": len(MustTestVersions) - 1,
|
||||
"taildrop": len(MustTestVersions),
|
||||
}
|
||||
|
||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("taildrop"))
|
||||
|
@ -313,6 +313,42 @@ func TestTaildrop(t *testing.T) {
|
|||
_, err = scenario.ListTailscaleClientsFQDNs()
|
||||
assertNoErrListFQDN(t, err)
|
||||
|
||||
for _, client := range allClients {
|
||||
if !strings.Contains(client.Hostname(), "head") {
|
||||
command := []string{"apk", "add", "curl"}
|
||||
_, _, err := client.Execute(command)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to install curl on %s, err: %s", client.Hostname(), err)
|
||||
}
|
||||
|
||||
}
|
||||
curlCommand := []string{"curl", "--unix-socket", "/var/run/tailscale/tailscaled.sock", "http://local-tailscaled.sock/localapi/v0/file-targets"}
|
||||
err = retry(10, 1*time.Second, func() error {
|
||||
result, _, err := client.Execute(curlCommand)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var fts []apitype.FileTarget
|
||||
err = json.Unmarshal([]byte(result), &fts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(fts) != len(allClients)-1 {
|
||||
ftStr := fmt.Sprintf("FileTargets for %s:\n", client.Hostname())
|
||||
for _, ft := range fts {
|
||||
ftStr += fmt.Sprintf("\t%s\n", ft.Node.Name)
|
||||
}
|
||||
return fmt.Errorf("client %s does not have all its peers as FileTargets, got %d, want: %d\n%s", client.Hostname(), len(fts), len(allClients)-1, ftStr)
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("failed to query localapi for filetarget on %s, err: %s", client.Hostname(), err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, client := range allClients {
|
||||
command := []string{"touch", fmt.Sprintf("/tmp/file_from_%s", client.Hostname())}
|
||||
|
||||
|
@ -347,8 +383,9 @@ func TestTaildrop(t *testing.T) {
|
|||
})
|
||||
if err != nil {
|
||||
t.Fatalf(
|
||||
"failed to send taildrop file on %s, err: %s",
|
||||
"failed to send taildrop file on %s with command %q, err: %s",
|
||||
client.Hostname(),
|
||||
strings.Join(command, " "),
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
@ -517,25 +554,176 @@ func TestExpireNode(t *testing.T) {
|
|||
err = json.Unmarshal([]byte(result), &node)
|
||||
assertNoErr(t, err)
|
||||
|
||||
var expiredNodeKey key.NodePublic
|
||||
err = expiredNodeKey.UnmarshalText([]byte(node.GetNodeKey()))
|
||||
assertNoErr(t, err)
|
||||
|
||||
t.Logf("Node %s with node_key %s has been expired", node.GetName(), expiredNodeKey.String())
|
||||
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
// Verify that the expired not is no longer present in the Peer list
|
||||
// of connected nodes.
|
||||
now := time.Now()
|
||||
|
||||
// Verify that the expired node has been marked in all peers list.
|
||||
for _, client := range allClients {
|
||||
status, err := client.Status()
|
||||
assertNoErr(t, err)
|
||||
|
||||
for _, peerKey := range status.Peers() {
|
||||
peerStatus := status.Peer[peerKey]
|
||||
|
||||
peerPublicKey := strings.TrimPrefix(peerStatus.PublicKey.String(), "nodekey:")
|
||||
|
||||
assert.NotEqual(t, node.GetNodeKey(), peerPublicKey)
|
||||
}
|
||||
|
||||
if client.Hostname() != node.GetName() {
|
||||
// Assert that we have the original count - self - expired node
|
||||
assert.Len(t, status.Peers(), len(MustTestVersions)-2)
|
||||
t.Logf("available peers of %s: %v", client.Hostname(), status.Peers())
|
||||
|
||||
// In addition to marking nodes expired, we filter them out during the map response
|
||||
// this check ensures that the node is either not present, or that it is expired
|
||||
// if it is in the map response.
|
||||
if peerStatus, ok := status.Peer[expiredNodeKey]; ok {
|
||||
assertNotNil(t, peerStatus.Expired)
|
||||
assert.Truef(t, peerStatus.KeyExpiry.Before(now), "node %s should have a key expire before %s, was %s", peerStatus.HostName, now.String(), peerStatus.KeyExpiry)
|
||||
assert.Truef(t, peerStatus.Expired, "node %s should be expired, expired is %v", peerStatus.HostName, peerStatus.Expired)
|
||||
}
|
||||
|
||||
// TODO(kradalby): We do not propogate expiry correctly, nodes should be aware
|
||||
// of their status, and this should be sent directly to the node when its
|
||||
// expired. This needs a notifier that goes directly to the node (currently we only do peers)
|
||||
// so fix this in a follow up PR.
|
||||
// } else {
|
||||
// assert.True(t, status.Self.Expired)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeOnlineLastSeenStatus(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
t.Parallel()
|
||||
|
||||
scenario, err := NewScenario()
|
||||
assertNoErr(t, err)
|
||||
defer scenario.Shutdown()
|
||||
|
||||
spec := map[string]int{
|
||||
"user1": len(MustTestVersions),
|
||||
}
|
||||
|
||||
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("onlinelastseen"))
|
||||
assertNoErrHeadscaleEnv(t, err)
|
||||
|
||||
allClients, err := scenario.ListTailscaleClients()
|
||||
assertNoErrListClients(t, err)
|
||||
|
||||
allIps, err := scenario.ListTailscaleClientsIPs()
|
||||
assertNoErrListClientIPs(t, err)
|
||||
|
||||
err = scenario.WaitForTailscaleSync()
|
||||
assertNoErrSync(t, err)
|
||||
|
||||
allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string {
|
||||
return x.String()
|
||||
})
|
||||
|
||||
success := pingAllHelper(t, allClients, allAddrs)
|
||||
t.Logf("before expire: %d successful pings out of %d", success, len(allClients)*len(allIps))
|
||||
|
||||
for _, client := range allClients {
|
||||
status, err := client.Status()
|
||||
assertNoErr(t, err)
|
||||
|
||||
// Assert that we have the original count - self
|
||||
assert.Len(t, status.Peers(), len(MustTestVersions)-1)
|
||||
}
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
assertNoErr(t, err)
|
||||
|
||||
keepAliveInterval := 60 * time.Second
|
||||
|
||||
// Duration is chosen arbitrarily, 10m is reported in #1561
|
||||
testDuration := 12 * time.Minute
|
||||
start := time.Now()
|
||||
end := start.Add(testDuration)
|
||||
|
||||
log.Printf("Starting online test from %v to %v", start, end)
|
||||
|
||||
for {
|
||||
// Let the test run continuously for X minutes to verify
|
||||
// all nodes stay connected and has the expected status over time.
|
||||
if end.Before(time.Now()) {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := headscale.Execute([]string{
|
||||
"headscale", "nodes", "list", "--output", "json",
|
||||
})
|
||||
assertNoErr(t, err)
|
||||
|
||||
var nodes []*v1.Node
|
||||
err = json.Unmarshal([]byte(result), &nodes)
|
||||
assertNoErr(t, err)
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// Threshold with some leeway
|
||||
lastSeenThreshold := now.Add(-keepAliveInterval - (10 * time.Second))
|
||||
|
||||
// Verify that headscale reports the nodes as online
|
||||
for _, node := range nodes {
|
||||
// All nodes should be online
|
||||
assert.Truef(
|
||||
t,
|
||||
node.GetOnline(),
|
||||
"expected %s to have online status in Headscale, marked as offline %s after start",
|
||||
node.GetName(),
|
||||
time.Since(start),
|
||||
)
|
||||
|
||||
lastSeen := node.GetLastSeen().AsTime()
|
||||
// All nodes should have been last seen between now and the keepAliveInterval
|
||||
assert.Truef(
|
||||
t,
|
||||
lastSeen.After(lastSeenThreshold),
|
||||
"lastSeen (%v) was not %s after the threshold (%v)",
|
||||
lastSeen,
|
||||
keepAliveInterval,
|
||||
lastSeenThreshold,
|
||||
)
|
||||
}
|
||||
|
||||
// Verify that all nodes report all nodes to be online
|
||||
for _, client := range allClients {
|
||||
status, err := client.Status()
|
||||
assertNoErr(t, err)
|
||||
|
||||
for _, peerKey := range status.Peers() {
|
||||
peerStatus := status.Peer[peerKey]
|
||||
|
||||
// .Online is only available from CapVer 16, which
|
||||
// is not present in 1.18 which is the lowest we
|
||||
// test.
|
||||
if strings.Contains(client.Hostname(), "1-18") {
|
||||
continue
|
||||
}
|
||||
|
||||
// All peers of this nodess are reporting to be
|
||||
// connected to the control server
|
||||
assert.Truef(
|
||||
t,
|
||||
peerStatus.Online,
|
||||
"expected node %s to be marked as online in %s peer list, marked as offline %s after start",
|
||||
peerStatus.HostName,
|
||||
client.Hostname(),
|
||||
time.Since(start),
|
||||
)
|
||||
|
||||
// from docs: last seen to tailcontrol; only present if offline
|
||||
// assert.Nilf(
|
||||
// t,
|
||||
// peerStatus.LastSeen,
|
||||
// "expected node %s to not have LastSeen set, got %s",
|
||||
// peerStatus.HostName,
|
||||
// peerStatus.LastSeen,
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
||||
// Check maximum once per second
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
|
|
780
integration/route_test.go
Normal file
780
integration/route_test.go
Normal file
|
@ -0,0 +1,780 @@
|
|||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/netip"
|
||||
"sort"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"github.com/juanfont/headscale/integration/hsic"
|
||||
"github.com/juanfont/headscale/integration/tsic"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// 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, route.GetAdvertised(), true)
|
||||
assert.Equal(t, route.GetEnabled(), false)
|
||||
assert.Equal(t, route.GetIsPrimary(), false)
|
||||
}
|
||||
|
||||
// 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, route.GetAdvertised(), true)
|
||||
assert.Equal(t, route.GetEnabled(), true)
|
||||
assert.Equal(t, route.GetIsPrimary(), true)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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, route.GetEnabled(), false)
|
||||
assert.Equal(t, route.GetIsPrimary(), false)
|
||||
} else {
|
||||
assert.Equal(t, route.GetEnabled(), true)
|
||||
assert.Equal(t, route.GetIsPrimary(), true)
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
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 {
|
||||
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, 2)
|
||||
|
||||
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)
|
||||
|
||||
// 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)]),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -29,7 +29,7 @@ run_tests() {
|
|||
-failfast \
|
||||
-timeout 120m \
|
||||
-parallel 1 \
|
||||
-run "^$test_name\$" >/dev/null 2>&1
|
||||
-run "^$test_name\$" >./control_logs/"$test_name"_"$i".log 2>&1
|
||||
status=$?
|
||||
end=$(date +%s)
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/juanfont/headscale/integration/tsic"
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
|
@ -93,7 +94,7 @@ var (
|
|||
//
|
||||
// - Two unstable (HEAD and unstable)
|
||||
// - Two latest versions
|
||||
// - Two oldest versions.
|
||||
// - Two oldest supported version.
|
||||
MustTestVersions = append(
|
||||
AllVersions[0:4],
|
||||
AllVersions[len(AllVersions)-2:]...,
|
||||
|
@ -296,11 +297,13 @@ func (s *Scenario) CreateTailscaleNodesInUser(
|
|||
opts ...tsic.Option,
|
||||
) error {
|
||||
if user, ok := s.users[userStr]; ok {
|
||||
var versions []string
|
||||
for i := 0; i < count; i++ {
|
||||
version := requestedVersion
|
||||
if requestedVersion == "all" {
|
||||
version = MustTestVersions[i%len(MustTestVersions)]
|
||||
}
|
||||
versions = append(versions, version)
|
||||
|
||||
headscale, err := s.Headscale()
|
||||
if err != nil {
|
||||
|
@ -350,6 +353,8 @@ func (s *Scenario) CreateTailscaleNodesInUser(
|
|||
return err
|
||||
}
|
||||
|
||||
log.Printf("testing versions %v", lo.Uniq(versions))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -403,7 +408,17 @@ func (s *Scenario) CountTailscale() int {
|
|||
func (s *Scenario) WaitForTailscaleSync() error {
|
||||
tsCount := s.CountTailscale()
|
||||
|
||||
return s.WaitForTailscaleSyncWithPeerCount(tsCount - 1)
|
||||
err := s.WaitForTailscaleSyncWithPeerCount(tsCount - 1)
|
||||
if err != nil {
|
||||
for _, user := range s.users {
|
||||
for _, client := range user.Clients {
|
||||
peers, _ := client.PrettyPeers()
|
||||
log.Println(peers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// WaitForTailscaleSyncWithPeerCount blocks execution until all the TailscaleClient reports
|
||||
|
|
|
@ -109,7 +109,7 @@ func TestSSHOneUserToAll(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
len(MustTestVersions)-2,
|
||||
len(MustTestVersions),
|
||||
)
|
||||
defer scenario.Shutdown()
|
||||
|
||||
|
@ -174,7 +174,7 @@ func TestSSHMultipleUsersAllToAll(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
len(MustTestVersions)-2,
|
||||
len(MustTestVersions),
|
||||
)
|
||||
defer scenario.Shutdown()
|
||||
|
||||
|
@ -220,7 +220,7 @@ func TestSSHNoSSHConfigured(t *testing.T) {
|
|||
},
|
||||
SSHs: []policy.SSH{},
|
||||
},
|
||||
len(MustTestVersions)-2,
|
||||
len(MustTestVersions),
|
||||
)
|
||||
defer scenario.Shutdown()
|
||||
|
||||
|
@ -269,7 +269,7 @@ func TestSSHIsBlockedInACL(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
len(MustTestVersions)-2,
|
||||
len(MustTestVersions),
|
||||
)
|
||||
defer scenario.Shutdown()
|
||||
|
||||
|
@ -325,7 +325,7 @@ func TestSSHUserOnlyIsolation(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
len(MustTestVersions)-2,
|
||||
len(MustTestVersions),
|
||||
)
|
||||
defer scenario.Shutdown()
|
||||
|
||||
|
|
|
@ -21,6 +21,8 @@ type TailscaleClient interface {
|
|||
Login(loginServer, authKey string) error
|
||||
LoginWithURL(loginServer string) (*url.URL, error)
|
||||
Logout() error
|
||||
Up() error
|
||||
Down() error
|
||||
IPs() ([]netip.Addr, error)
|
||||
FQDN() (string, error)
|
||||
Status() (*ipnstate.Status, error)
|
||||
|
@ -30,4 +32,5 @@ type TailscaleClient interface {
|
|||
Ping(hostnameOrIP string, opts ...tsic.PingOption) error
|
||||
Curl(url string, opts ...tsic.CurlOption) (string, error)
|
||||
ID() string
|
||||
PrettyPeers() (string, error)
|
||||
}
|
||||
|
|
|
@ -285,6 +285,15 @@ func (t *TailscaleInContainer) hasTLS() bool {
|
|||
|
||||
// Shutdown stops and cleans up the Tailscale container.
|
||||
func (t *TailscaleInContainer) Shutdown() error {
|
||||
err := t.SaveLog("/tmp/control")
|
||||
if err != nil {
|
||||
log.Printf(
|
||||
"Failed to save log from %s: %s",
|
||||
t.hostname,
|
||||
fmt.Errorf("failed to save log: %w", err),
|
||||
)
|
||||
}
|
||||
|
||||
return t.pool.Purge(t.container)
|
||||
}
|
||||
|
||||
|
@ -417,6 +426,44 @@ func (t *TailscaleInContainer) Logout() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Helper that runs `tailscale up` with no arguments.
|
||||
func (t *TailscaleInContainer) Up() error {
|
||||
command := []string{
|
||||
"tailscale",
|
||||
"up",
|
||||
}
|
||||
|
||||
if _, _, err := t.Execute(command, dockertestutil.ExecuteCommandTimeout(dockerExecuteTimeout)); err != nil {
|
||||
return fmt.Errorf(
|
||||
"%s failed to bring tailscale client up (%s): %w",
|
||||
t.hostname,
|
||||
strings.Join(command, " "),
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper that runs `tailscale down` with no arguments.
|
||||
func (t *TailscaleInContainer) Down() error {
|
||||
command := []string{
|
||||
"tailscale",
|
||||
"down",
|
||||
}
|
||||
|
||||
if _, _, err := t.Execute(command, dockertestutil.ExecuteCommandTimeout(dockerExecuteTimeout)); err != nil {
|
||||
return fmt.Errorf(
|
||||
"%s failed to bring tailscale client down (%s): %w",
|
||||
t.hostname,
|
||||
strings.Join(command, " "),
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IPs returns the netip.Addr of the Tailscale instance.
|
||||
func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
|
||||
if t.ips != nil && len(t.ips) != 0 {
|
||||
|
@ -486,6 +533,34 @@ func (t *TailscaleInContainer) FQDN() (string, error) {
|
|||
return status.Self.DNSName, nil
|
||||
}
|
||||
|
||||
// PrettyPeers returns a formatted-ish table of peers in the client.
|
||||
func (t *TailscaleInContainer) PrettyPeers() (string, error) {
|
||||
status, err := t.Status()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get FQDN: %w", err)
|
||||
}
|
||||
|
||||
str := fmt.Sprintf("Peers of %s\n", t.hostname)
|
||||
str += "Hostname\tOnline\tLastSeen\n"
|
||||
|
||||
peerCount := len(status.Peers())
|
||||
onlineCount := 0
|
||||
|
||||
for _, peerKey := range status.Peers() {
|
||||
peer := status.Peer[peerKey]
|
||||
|
||||
if peer.Online {
|
||||
onlineCount++
|
||||
}
|
||||
|
||||
str += fmt.Sprintf("%s\t%t\t%s\n", peer.HostName, peer.Online, peer.LastSeen)
|
||||
}
|
||||
|
||||
str += fmt.Sprintf("Peer Count: %d, Online Count: %d\n\n", peerCount, onlineCount)
|
||||
|
||||
return str, nil
|
||||
}
|
||||
|
||||
// WaitForNeedsLogin blocks until the Tailscale (tailscaled) instance has
|
||||
// started and needs to be logged into.
|
||||
func (t *TailscaleInContainer) WaitForNeedsLogin() error {
|
||||
|
@ -531,7 +606,7 @@ func (t *TailscaleInContainer) WaitForRunning() error {
|
|||
}
|
||||
|
||||
// WaitForPeers blocks until N number of peers is present in the
|
||||
// Peer list of the Tailscale instance.
|
||||
// Peer list of the Tailscale instance and is reporting Online.
|
||||
func (t *TailscaleInContainer) WaitForPeers(expected int) error {
|
||||
return t.pool.Retry(func() error {
|
||||
status, err := t.Status()
|
||||
|
@ -547,6 +622,14 @@ func (t *TailscaleInContainer) WaitForPeers(expected int) error {
|
|||
expected,
|
||||
len(peers),
|
||||
)
|
||||
} else {
|
||||
for _, peerKey := range peers {
|
||||
peer := status.Peer[peerKey]
|
||||
|
||||
if !peer.Online {
|
||||
return fmt.Errorf("[%s] peer count correct, but %s is not online", t.hostname, peer.HostName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -738,3 +821,9 @@ func (t *TailscaleInContainer) Curl(url string, opts ...CurlOption) (string, err
|
|||
func (t *TailscaleInContainer) WriteFile(path string, data []byte) error {
|
||||
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
|
||||
}
|
||||
|
||||
// SaveLog saves the current stdout log of the container to a path
|
||||
// on the host system.
|
||||
func (t *TailscaleInContainer) SaveLog(path string) error {
|
||||
return dockertestutil.SaveLog(t.pool, t.container, path)
|
||||
}
|
||||
|
|
|
@ -26,6 +26,13 @@ func assertNoErrf(t *testing.T, msg string, err error) {
|
|||
}
|
||||
}
|
||||
|
||||
func assertNotNil(t *testing.T, thing interface{}) {
|
||||
t.Helper()
|
||||
if thing == nil {
|
||||
t.Fatal("got unexpected nil")
|
||||
}
|
||||
}
|
||||
|
||||
func assertNoErrHeadscaleEnv(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assertNoErrf(t, "failed to create headscale environment: %s", err)
|
||||
|
@ -68,13 +75,13 @@ func assertContains(t *testing.T, str, subStr string) {
|
|||
}
|
||||
}
|
||||
|
||||
func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) int {
|
||||
func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string, opts ...tsic.PingOption) int {
|
||||
t.Helper()
|
||||
success := 0
|
||||
|
||||
for _, client := range clients {
|
||||
for _, addr := range addrs {
|
||||
err := client.Ping(addr)
|
||||
err := client.Ping(addr, opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to ping %s from %s: %s", addr, client.Hostname(), err)
|
||||
} else {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue