mirror of
https://github.com/juanfont/headscale.git
synced 2026-03-27 03:41:23 +01:00
Three corrections to issue tests that had wrong assumptions about when data becomes available: 1. initial_map_should_include_peer_online_status: use WaitForCondition instead of checking the initial netmap. Online status is set by Connect() which sends a PeerChange patch after the initial RegisterResponse, so it may not be present immediately. 2. disco_key_should_propagate_to_peers: use WaitForCondition. The DiscoKey is sent in the first MapRequest (not RegisterRequest), so peers may not see it until a subsequent map update. 3. approved_route_without_announcement: invert the test expectation. Tailscale uses a strict advertise-then-approve model -- routes are only distributed when the node advertises them (Hostinfo.RoutableIPs) AND they are approved. An approval without advertisement is a dormant pre-approval. The test now asserts the route does NOT appear in AllowedIPs, matching upstream Tailscale semantics. Also fix TestClient.Reconnect to clear the cached netmap and drain pending updates before re-registering. Without this, WaitForPeers returned immediately based on the old session's stale data.
884 lines
24 KiB
Go
884 lines
24 KiB
Go
package servertest_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/netip"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/juanfont/headscale/hscontrol/servertest"
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/netmap"
|
|
)
|
|
|
|
// These tests are intentionally strict about expected behavior.
|
|
// Failures surface real issues in the control plane.
|
|
|
|
// TestIssuesMapContent tests issues with MapResponse content correctness.
|
|
func TestIssuesMapContent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// After mesh formation, all peers should have a known Online status.
|
|
// The Online field is set when Connect() sends a NodeOnline PeerChange
|
|
// patch. The initial MapResponse (from auth handler) may have Online=nil
|
|
// because Connect() hasn't run yet, so we wait for the status to propagate.
|
|
t.Run("initial_map_should_include_peer_online_status", func(t *testing.T) {
|
|
t.Parallel()
|
|
h := servertest.NewHarness(t, 3)
|
|
|
|
for _, c := range h.Clients() {
|
|
c.WaitForCondition(t, "all peers have known Online status",
|
|
10*time.Second,
|
|
func(nm *netmap.NetworkMap) bool {
|
|
if len(nm.Peers) < 2 {
|
|
return false
|
|
}
|
|
|
|
for _, peer := range nm.Peers {
|
|
if _, known := peer.Online().GetOk(); !known {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
})
|
|
}
|
|
})
|
|
|
|
// DiscoPublicKey set by the client should be visible to peers.
|
|
t.Run("disco_key_should_propagate_to_peers", func(t *testing.T) {
|
|
t.Parallel()
|
|
h := servertest.NewHarness(t, 2)
|
|
|
|
// The DiscoKey is sent in the first MapRequest (not the RegisterRequest),
|
|
// so it may take an extra map update to propagate to peers. Wait for
|
|
// the condition rather than checking the initial netmap.
|
|
h.Client(0).WaitForCondition(t, "peer has non-zero DiscoKey",
|
|
10*time.Second,
|
|
func(nm *netmap.NetworkMap) bool {
|
|
if len(nm.Peers) < 1 {
|
|
return false
|
|
}
|
|
|
|
return !nm.Peers[0].DiscoKey().IsZero()
|
|
})
|
|
})
|
|
|
|
// All peers should reference a valid DERP region.
|
|
t.Run("peers_have_valid_derp_region", func(t *testing.T) {
|
|
t.Parallel()
|
|
h := servertest.NewHarness(t, 3)
|
|
|
|
for _, c := range h.Clients() {
|
|
nm := c.Netmap()
|
|
require.NotNil(t, nm)
|
|
require.NotNil(t, nm.DERPMap)
|
|
|
|
for _, peer := range nm.Peers {
|
|
derpRegion := peer.HomeDERP()
|
|
|
|
if derpRegion != 0 {
|
|
_, regionExists := nm.DERPMap.Regions[derpRegion]
|
|
assert.True(t, regionExists,
|
|
"client %s: peer %d has HomeDERP=%d which is not in DERPMap",
|
|
c.Name, peer.ID(), derpRegion)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
// Each peer should have a valid user profile in the netmap.
|
|
t.Run("all_peers_have_user_profiles", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := servertest.NewServer(t)
|
|
user1 := srv.CreateUser(t, "profile-user1")
|
|
user2 := srv.CreateUser(t, "profile-user2")
|
|
|
|
c1 := servertest.NewClient(t, srv, "profile-node1",
|
|
servertest.WithUser(user1))
|
|
c2 := servertest.NewClient(t, srv, "profile-node2",
|
|
servertest.WithUser(user2))
|
|
|
|
c1.WaitForPeers(t, 1, 10*time.Second)
|
|
c2.WaitForPeers(t, 1, 10*time.Second)
|
|
|
|
nm := c1.Netmap()
|
|
require.NotNil(t, nm)
|
|
|
|
selfUserID := nm.SelfNode.User()
|
|
selfProfile, hasSelf := nm.UserProfiles[selfUserID]
|
|
assert.True(t, hasSelf, "should have self user profile")
|
|
|
|
if hasSelf {
|
|
assert.NotEmpty(t, selfProfile.DisplayName(),
|
|
"self user profile should have a display name")
|
|
}
|
|
|
|
require.Len(t, nm.Peers, 1)
|
|
peerUserID := nm.Peers[0].User()
|
|
|
|
peerProfile, hasPeer := nm.UserProfiles[peerUserID]
|
|
assert.True(t, hasPeer,
|
|
"should have peer's user profile (user %d)", peerUserID)
|
|
|
|
if hasPeer {
|
|
assert.NotEmpty(t, peerProfile.DisplayName(),
|
|
"peer user profile should have a display name")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestIssuesRoutes tests issues with route propagation.
|
|
func TestIssuesRoutes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Approving a route via API without the node announcing it must NOT
|
|
// make the route visible in AllowedIPs. Tailscale uses a strict
|
|
// advertise-then-approve model: routes are only distributed when the
|
|
// node advertises them (Hostinfo.RoutableIPs) AND they are approved.
|
|
// An approval without advertisement is a dormant pre-approval that
|
|
// activates once the node starts advertising.
|
|
t.Run("approved_route_without_announcement_not_distributed", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := servertest.NewServer(t)
|
|
user := srv.CreateUser(t, "noannounce-user")
|
|
|
|
c1 := servertest.NewClient(t, srv, "noannounce-node1",
|
|
servertest.WithUser(user))
|
|
c2 := servertest.NewClient(t, srv, "noannounce-node2",
|
|
servertest.WithUser(user))
|
|
|
|
c1.WaitForPeers(t, 1, 10*time.Second)
|
|
|
|
nodeID := findNodeID(t, srv, "noannounce-node1")
|
|
route := netip.MustParsePrefix("10.0.0.0/24")
|
|
|
|
// The API should accept the approval without error — the route
|
|
// is stored but dormant because the node is not advertising it.
|
|
_, routeChange, err := srv.State().SetApprovedRoutes(
|
|
nodeID, []netip.Prefix{route})
|
|
require.NoError(t, err)
|
|
srv.App.Change(routeChange)
|
|
|
|
// Wait for any updates triggered by the route change to propagate,
|
|
// then verify the route does NOT appear in AllowedIPs.
|
|
timer := time.NewTimer(3 * time.Second)
|
|
defer timer.Stop()
|
|
|
|
<-timer.C
|
|
|
|
nm := c2.Netmap()
|
|
require.NotNil(t, nm)
|
|
|
|
for _, p := range nm.Peers {
|
|
hi := p.Hostinfo()
|
|
if hi.Valid() && hi.Hostname() == "noannounce-node1" {
|
|
for i := range p.AllowedIPs().Len() {
|
|
assert.NotEqual(t, route, p.AllowedIPs().At(i),
|
|
"approved-but-not-announced route should not appear in AllowedIPs")
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
// When the server approves routes for a node, that node
|
|
// should receive a self-update reflecting the change.
|
|
t.Run("self_update_after_route_approval", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := servertest.NewServer(t)
|
|
user := srv.CreateUser(t, "selfup-user")
|
|
|
|
c1 := servertest.NewClient(t, srv, "selfup-node1",
|
|
servertest.WithUser(user))
|
|
servertest.NewClient(t, srv, "selfup-node2",
|
|
servertest.WithUser(user))
|
|
|
|
c1.WaitForPeers(t, 1, 10*time.Second)
|
|
|
|
nodeID := findNodeID(t, srv, "selfup-node1")
|
|
route := netip.MustParsePrefix("10.77.0.0/24")
|
|
|
|
countBefore := c1.UpdateCount()
|
|
|
|
_, routeChange, err := srv.State().SetApprovedRoutes(
|
|
nodeID, []netip.Prefix{route})
|
|
require.NoError(t, err)
|
|
srv.App.Change(routeChange)
|
|
|
|
c1.WaitForCondition(t, "self-update after route approval",
|
|
10*time.Second,
|
|
func(nm *netmap.NetworkMap) bool {
|
|
return c1.UpdateCount() > countBefore
|
|
})
|
|
})
|
|
|
|
// Hostinfo route advertisement should be stored on server.
|
|
t.Run("hostinfo_route_advertisement_stored_on_server", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := servertest.NewServer(t)
|
|
user := srv.CreateUser(t, "histore-user")
|
|
|
|
c1 := servertest.NewClient(t, srv, "histore-node1",
|
|
servertest.WithUser(user))
|
|
c2 := servertest.NewClient(t, srv, "histore-node2",
|
|
servertest.WithUser(user))
|
|
|
|
c1.WaitForPeers(t, 1, 10*time.Second)
|
|
|
|
route := netip.MustParsePrefix("10.99.0.0/24")
|
|
|
|
c1.Direct().SetHostinfo(&tailcfg.Hostinfo{
|
|
BackendLogID: "servertest-histore-node1",
|
|
Hostname: "histore-node1",
|
|
RoutableIPs: []netip.Prefix{route},
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
_ = c1.Direct().SendUpdate(ctx)
|
|
|
|
c2.WaitForCondition(t, "route in peer hostinfo", 10*time.Second,
|
|
func(nm *netmap.NetworkMap) bool {
|
|
for _, p := range nm.Peers {
|
|
hi := p.Hostinfo()
|
|
if hi.Valid() && hi.Hostname() == "histore-node1" {
|
|
return hi.RoutableIPs().Len() > 0
|
|
}
|
|
}
|
|
|
|
return false
|
|
})
|
|
|
|
nodeID := findNodeID(t, srv, "histore-node1")
|
|
nv, ok := srv.State().GetNodeByID(nodeID)
|
|
require.True(t, ok, "node should exist in server state")
|
|
|
|
announced := nv.AnnouncedRoutes()
|
|
assert.Contains(t, announced, route,
|
|
"server should store the advertised route as announced")
|
|
})
|
|
}
|
|
|
|
// TestIssuesIPAllocation tests IP address allocation correctness.
|
|
func TestIssuesIPAllocation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Every node should get unique IPs.
|
|
t.Run("ip_addresses_are_unique_across_nodes", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := servertest.NewServer(t)
|
|
user := srv.CreateUser(t, "ipuniq-user")
|
|
|
|
const n = 10
|
|
|
|
clients := make([]*servertest.TestClient, n)
|
|
for i := range n {
|
|
clients[i] = servertest.NewClient(t, srv,
|
|
fmt.Sprintf("ipuniq-%d", i),
|
|
servertest.WithUser(user))
|
|
}
|
|
|
|
for _, c := range clients {
|
|
c.WaitForUpdate(t, 15*time.Second)
|
|
}
|
|
|
|
seen := make(map[netip.Prefix]string)
|
|
|
|
for _, c := range clients {
|
|
nm := c.Netmap()
|
|
require.NotNil(t, nm)
|
|
require.True(t, nm.SelfNode.Valid())
|
|
|
|
for i := range nm.SelfNode.Addresses().Len() {
|
|
addr := nm.SelfNode.Addresses().At(i)
|
|
if other, exists := seen[addr]; exists {
|
|
t.Errorf("IP collision: %v assigned to both %s and %s",
|
|
addr, other, c.Name)
|
|
}
|
|
|
|
seen[addr] = c.Name
|
|
}
|
|
}
|
|
})
|
|
|
|
// After reconnect, IP addresses should be stable.
|
|
t.Run("reconnect_preserves_ip_addresses", func(t *testing.T) {
|
|
t.Parallel()
|
|
h := servertest.NewHarness(t, 2)
|
|
|
|
nm := h.Client(0).Netmap()
|
|
require.NotNil(t, nm)
|
|
require.True(t, nm.SelfNode.Valid())
|
|
|
|
addrsBefore := make([]netip.Prefix, 0, nm.SelfNode.Addresses().Len())
|
|
for i := range nm.SelfNode.Addresses().Len() {
|
|
addrsBefore = append(addrsBefore, nm.SelfNode.Addresses().At(i))
|
|
}
|
|
|
|
require.NotEmpty(t, addrsBefore)
|
|
|
|
h.Client(0).Disconnect(t)
|
|
h.Client(0).Reconnect(t)
|
|
h.Client(0).WaitForPeers(t, 1, 15*time.Second)
|
|
|
|
nmAfter := h.Client(0).Netmap()
|
|
require.NotNil(t, nmAfter)
|
|
require.True(t, nmAfter.SelfNode.Valid())
|
|
|
|
addrsAfter := make([]netip.Prefix, 0, nmAfter.SelfNode.Addresses().Len())
|
|
for i := range nmAfter.SelfNode.Addresses().Len() {
|
|
addrsAfter = append(addrsAfter, nmAfter.SelfNode.Addresses().At(i))
|
|
}
|
|
|
|
assert.Equal(t, addrsBefore, addrsAfter,
|
|
"IP addresses should be stable across reconnect")
|
|
})
|
|
|
|
// New peers should have addresses immediately.
|
|
t.Run("new_peer_has_addresses_immediately", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := servertest.NewServer(t)
|
|
user := srv.CreateUser(t, "newaddr-user")
|
|
|
|
c1 := servertest.NewClient(t, srv, "newaddr-node1",
|
|
servertest.WithUser(user))
|
|
c1.WaitForUpdate(t, 10*time.Second)
|
|
|
|
servertest.NewClient(t, srv, "newaddr-node2",
|
|
servertest.WithUser(user))
|
|
|
|
c1.WaitForPeers(t, 1, 10*time.Second)
|
|
|
|
nm := c1.Netmap()
|
|
require.NotNil(t, nm)
|
|
require.Len(t, nm.Peers, 1)
|
|
|
|
assert.Positive(t, nm.Peers[0].Addresses().Len(),
|
|
"new peer should have addresses in the first update that includes it")
|
|
})
|
|
}
|
|
|
|
// TestIssuesServerMutations tests that server-side mutations propagate correctly.
|
|
func TestIssuesServerMutations(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Renaming a node via API should propagate to peers.
|
|
t.Run("node_rename_propagates_to_peers", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := servertest.NewServer(t)
|
|
user := srv.CreateUser(t, "rename-user")
|
|
|
|
c1 := servertest.NewClient(t, srv, "rename-node1",
|
|
servertest.WithUser(user))
|
|
c2 := servertest.NewClient(t, srv, "rename-node2",
|
|
servertest.WithUser(user))
|
|
|
|
c1.WaitForPeers(t, 1, 10*time.Second)
|
|
|
|
nodeID := findNodeID(t, srv, "rename-node1")
|
|
|
|
_, renameChange, err := srv.State().RenameNode(nodeID, "renamed-node1")
|
|
require.NoError(t, err)
|
|
srv.App.Change(renameChange)
|
|
|
|
c2.WaitForCondition(t, "renamed peer visible", 10*time.Second,
|
|
func(nm *netmap.NetworkMap) bool {
|
|
for _, p := range nm.Peers {
|
|
if p.Name() == "renamed-node1" {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
})
|
|
})
|
|
|
|
// Deleting a node via API should remove it from all peers.
|
|
t.Run("node_delete_removes_from_all_peers", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := servertest.NewServer(t)
|
|
user := srv.CreateUser(t, "del-user")
|
|
|
|
c1 := servertest.NewClient(t, srv, "del-node1",
|
|
servertest.WithUser(user))
|
|
servertest.NewClient(t, srv, "del-node2",
|
|
servertest.WithUser(user))
|
|
c3 := servertest.NewClient(t, srv, "del-node3",
|
|
servertest.WithUser(user))
|
|
|
|
c1.WaitForPeers(t, 2, 15*time.Second)
|
|
|
|
nodeID2 := findNodeID(t, srv, "del-node2")
|
|
node2View, ok := srv.State().GetNodeByID(nodeID2)
|
|
require.True(t, ok)
|
|
|
|
deleteChange, err := srv.State().DeleteNode(node2View)
|
|
require.NoError(t, err)
|
|
srv.App.Change(deleteChange)
|
|
|
|
c1.WaitForCondition(t, "deleted peer gone", 10*time.Second,
|
|
func(nm *netmap.NetworkMap) bool {
|
|
for _, p := range nm.Peers {
|
|
hi := p.Hostinfo()
|
|
if hi.Valid() && hi.Hostname() == "del-node2" {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
c3.WaitForCondition(t, "deleted peer gone from c3", 10*time.Second,
|
|
func(nm *netmap.NetworkMap) bool {
|
|
for _, p := range nm.Peers {
|
|
hi := p.Hostinfo()
|
|
if hi.Valid() && hi.Hostname() == "del-node2" {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
assert.Len(t, c1.Peers(), 1)
|
|
assert.Len(t, c3.Peers(), 1)
|
|
})
|
|
|
|
// Hostinfo changes should propagate to peers.
|
|
t.Run("hostinfo_changes_propagate_to_peers", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := servertest.NewServer(t)
|
|
user := srv.CreateUser(t, "hichange-user")
|
|
|
|
c1 := servertest.NewClient(t, srv, "hichange-node1",
|
|
servertest.WithUser(user))
|
|
c2 := servertest.NewClient(t, srv, "hichange-node2",
|
|
servertest.WithUser(user))
|
|
|
|
c1.WaitForPeers(t, 1, 10*time.Second)
|
|
|
|
c1.Direct().SetHostinfo(&tailcfg.Hostinfo{
|
|
BackendLogID: "servertest-hichange-node1",
|
|
Hostname: "hichange-node1",
|
|
OS: "TestOS",
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
_ = c1.Direct().SendUpdate(ctx)
|
|
|
|
c2.WaitForCondition(t, "OS change visible", 10*time.Second,
|
|
func(nm *netmap.NetworkMap) bool {
|
|
for _, p := range nm.Peers {
|
|
hi := p.Hostinfo()
|
|
if hi.Valid() && hi.Hostname() == "hichange-node1" {
|
|
return hi.OS() == "TestOS"
|
|
}
|
|
}
|
|
|
|
return false
|
|
})
|
|
})
|
|
}
|
|
|
|
// TestIssuesNodeStoreConsistency tests NodeStore + DB consistency.
|
|
func TestIssuesNodeStoreConsistency(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// NodeStore and DB should agree after mutations.
|
|
t.Run("nodestore_db_consistency_after_operations", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := servertest.NewServer(t)
|
|
user := srv.CreateUser(t, "consist-user")
|
|
|
|
c1 := servertest.NewClient(t, srv, "consist-node1",
|
|
servertest.WithUser(user))
|
|
servertest.NewClient(t, srv, "consist-node2",
|
|
servertest.WithUser(user))
|
|
|
|
c1.WaitForPeers(t, 1, 10*time.Second)
|
|
|
|
nodeID1 := findNodeID(t, srv, "consist-node1")
|
|
|
|
route := netip.MustParsePrefix("10.50.0.0/24")
|
|
_, routeChange, err := srv.State().SetApprovedRoutes(
|
|
nodeID1, []netip.Prefix{route})
|
|
require.NoError(t, err)
|
|
srv.App.Change(routeChange)
|
|
|
|
nsView, ok := srv.State().GetNodeByID(nodeID1)
|
|
require.True(t, ok, "node should be in NodeStore")
|
|
|
|
dbNode, err := srv.State().DB().GetNodeByID(nodeID1)
|
|
require.NoError(t, err, "node should be in database")
|
|
|
|
nsRoutes := nsView.ApprovedRoutes().AsSlice()
|
|
dbRoutes := dbNode.ApprovedRoutes
|
|
|
|
assert.Equal(t, nsRoutes, dbRoutes,
|
|
"NodeStore and DB should agree on approved routes")
|
|
})
|
|
|
|
// After rapid reconnect, NodeStore should reflect correct state.
|
|
t.Run("nodestore_correct_after_rapid_reconnect", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := servertest.NewServer(t)
|
|
user := srv.CreateUser(t, "nsrecon-user")
|
|
|
|
c1 := servertest.NewClient(t, srv, "nsrecon-node1",
|
|
servertest.WithUser(user))
|
|
servertest.NewClient(t, srv, "nsrecon-node2",
|
|
servertest.WithUser(user))
|
|
|
|
c1.WaitForPeers(t, 1, 10*time.Second)
|
|
|
|
nodeID1 := findNodeID(t, srv, "nsrecon-node1")
|
|
|
|
for range 5 {
|
|
c1.Disconnect(t)
|
|
c1.Reconnect(t)
|
|
}
|
|
|
|
c1.WaitForPeers(t, 1, 15*time.Second)
|
|
|
|
nv, ok := srv.State().GetNodeByID(nodeID1)
|
|
require.True(t, ok)
|
|
|
|
isOnline, known := nv.IsOnline().GetOk()
|
|
assert.True(t, known, "NodeStore should know online status after reconnect")
|
|
assert.True(t, isOnline, "NodeStore should show node as online after reconnect")
|
|
})
|
|
}
|
|
|
|
// TestIssuesGracePeriod tests the disconnect grace period behavior.
|
|
func TestIssuesGracePeriod(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Offline status should arrive promptly after grace period.
|
|
t.Run("offline_status_arrives_within_grace_period_plus_margin", func(t *testing.T) {
|
|
t.Parallel()
|
|
h := servertest.NewHarness(t, 2)
|
|
|
|
peerName := h.Client(1).Name
|
|
|
|
h.Client(0).WaitForCondition(t, "peer online", 15*time.Second,
|
|
func(nm *netmap.NetworkMap) bool {
|
|
for _, p := range nm.Peers {
|
|
hi := p.Hostinfo()
|
|
if hi.Valid() && hi.Hostname() == peerName {
|
|
isOnline, known := p.Online().GetOk()
|
|
|
|
return known && isOnline
|
|
}
|
|
}
|
|
|
|
return false
|
|
})
|
|
|
|
disconnectTime := time.Now()
|
|
|
|
h.Client(1).Disconnect(t)
|
|
|
|
h.Client(0).WaitForCondition(t, "peer offline", 20*time.Second,
|
|
func(nm *netmap.NetworkMap) bool {
|
|
for _, p := range nm.Peers {
|
|
hi := p.Hostinfo()
|
|
if hi.Valid() && hi.Hostname() == peerName {
|
|
isOnline, known := p.Online().GetOk()
|
|
|
|
return known && !isOnline
|
|
}
|
|
}
|
|
|
|
return false
|
|
})
|
|
|
|
elapsed := time.Since(disconnectTime)
|
|
t.Logf("offline status arrived after %v", elapsed)
|
|
|
|
assert.Greater(t, elapsed, 8*time.Second,
|
|
"offline status arrived too quickly -- grace period may not be working")
|
|
assert.Less(t, elapsed, 20*time.Second,
|
|
"offline status took too long -- propagation delay issue")
|
|
})
|
|
|
|
// Ephemeral nodes should be fully deleted.
|
|
t.Run("ephemeral_node_deleted_not_just_offline", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := servertest.NewServer(t,
|
|
servertest.WithEphemeralTimeout(3*time.Second))
|
|
user := srv.CreateUser(t, "eph-del-user")
|
|
|
|
regular := servertest.NewClient(t, srv, "eph-del-regular",
|
|
servertest.WithUser(user))
|
|
ephemeral := servertest.NewClient(t, srv, "eph-del-ephemeral",
|
|
servertest.WithUser(user), servertest.WithEphemeral())
|
|
|
|
regular.WaitForPeers(t, 1, 10*time.Second)
|
|
|
|
_, found := regular.PeerByName("eph-del-ephemeral")
|
|
require.True(t, found)
|
|
|
|
// Ensure the ephemeral node's long-poll session is fully
|
|
// established on the server before disconnecting. Without
|
|
// this, the Disconnect may cancel a PollNetMap that hasn't
|
|
// yet reached serveLongPoll, so no grace period or ephemeral
|
|
// GC would ever be scheduled.
|
|
ephemeral.WaitForPeers(t, 1, 10*time.Second)
|
|
|
|
ephemeral.Disconnect(t)
|
|
|
|
// Grace period (10s) + ephemeral GC timeout (3s) + propagation.
|
|
// Use a generous timeout for CI environments under load.
|
|
regular.WaitForCondition(t, "ephemeral peer removed", 60*time.Second,
|
|
func(nm *netmap.NetworkMap) bool {
|
|
for _, p := range nm.Peers {
|
|
hi := p.Hostinfo()
|
|
if hi.Valid() && hi.Hostname() == "eph-del-ephemeral" {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
nodes := srv.State().ListNodes()
|
|
for i := range nodes.Len() {
|
|
n := nodes.At(i)
|
|
assert.NotEqual(t, "eph-del-ephemeral", n.Hostname(),
|
|
"ephemeral node should be deleted from server state")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestIssuesScale tests behavior under scale and rapid changes.
|
|
func TestIssuesScale(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("simultaneous_connect_all_see_all", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := servertest.NewServer(t)
|
|
user := srv.CreateUser(t, "simul-user")
|
|
|
|
const n = 10
|
|
|
|
clients := make([]*servertest.TestClient, n)
|
|
for i := range n {
|
|
clients[i] = servertest.NewClient(t, srv,
|
|
fmt.Sprintf("simul-node-%d", i),
|
|
servertest.WithUser(user))
|
|
}
|
|
|
|
for _, c := range clients {
|
|
c.WaitForPeers(t, n-1, 30*time.Second)
|
|
}
|
|
|
|
servertest.AssertMeshComplete(t, clients)
|
|
servertest.AssertSymmetricVisibility(t, clients)
|
|
})
|
|
|
|
// Many rapid additions should all be delivered.
|
|
t.Run("rapid_sequential_additions", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := servertest.NewServer(t)
|
|
user := srv.CreateUser(t, "rapid-user")
|
|
|
|
c1 := servertest.NewClient(t, srv, "rapid-node1",
|
|
servertest.WithUser(user))
|
|
c1.WaitForUpdate(t, 10*time.Second)
|
|
|
|
for i := range 5 {
|
|
servertest.NewClient(t, srv,
|
|
fmt.Sprintf("rapid-node-%d", i+2),
|
|
servertest.WithUser(user))
|
|
}
|
|
|
|
c1.WaitForPeers(t, 5, 30*time.Second)
|
|
assert.Len(t, c1.Peers(), 5)
|
|
})
|
|
|
|
// Reconnect should give a complete map.
|
|
t.Run("reconnect_gets_complete_map", func(t *testing.T) {
|
|
t.Parallel()
|
|
h := servertest.NewHarness(t, 3)
|
|
|
|
h.Client(0).Disconnect(t)
|
|
h.Client(0).Reconnect(t)
|
|
h.Client(0).WaitForPeers(t, 2, 15*time.Second)
|
|
|
|
nm := h.Client(0).Netmap()
|
|
require.NotNil(t, nm)
|
|
assert.Len(t, nm.Peers, 2)
|
|
assert.True(t, nm.SelfNode.Valid())
|
|
assert.Positive(t, nm.SelfNode.Addresses().Len())
|
|
})
|
|
}
|
|
|
|
// TestIssuesIdentity tests node identity and naming behavior.
|
|
func TestIssuesIdentity(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Cross-user visibility with default policy.
|
|
t.Run("cross_user_visibility_default_policy", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := servertest.NewServer(t)
|
|
user1 := srv.CreateUser(t, "xuser1")
|
|
user2 := srv.CreateUser(t, "xuser2")
|
|
|
|
c1 := servertest.NewClient(t, srv, "xuser-node1",
|
|
servertest.WithUser(user1))
|
|
c2 := servertest.NewClient(t, srv, "xuser-node2",
|
|
servertest.WithUser(user2))
|
|
|
|
c1.WaitForPeers(t, 1, 10*time.Second)
|
|
c2.WaitForPeers(t, 1, 10*time.Second)
|
|
|
|
_, found := c1.PeerByName("xuser-node2")
|
|
assert.True(t, found, "user1's node should see user2's node")
|
|
|
|
_, found = c2.PeerByName("xuser-node1")
|
|
assert.True(t, found, "user2's node should see user1's node")
|
|
})
|
|
|
|
// Multiple nodes same user should be distinct.
|
|
t.Run("multiple_nodes_same_user_distinct", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := servertest.NewServer(t)
|
|
user := srv.CreateUser(t, "sameuser")
|
|
|
|
c1 := servertest.NewClient(t, srv, "sameuser-node1",
|
|
servertest.WithUser(user))
|
|
c2 := servertest.NewClient(t, srv, "sameuser-node2",
|
|
servertest.WithUser(user))
|
|
c3 := servertest.NewClient(t, srv, "sameuser-node3",
|
|
servertest.WithUser(user))
|
|
|
|
c1.WaitForPeers(t, 2, 15*time.Second)
|
|
c2.WaitForPeers(t, 2, 15*time.Second)
|
|
c3.WaitForPeers(t, 2, 15*time.Second)
|
|
|
|
nm1 := c1.Netmap()
|
|
nm2 := c2.Netmap()
|
|
nm3 := c3.Netmap()
|
|
|
|
require.NotNil(t, nm1)
|
|
require.NotNil(t, nm2)
|
|
require.NotNil(t, nm3)
|
|
|
|
ids := map[tailcfg.NodeID]string{
|
|
nm1.SelfNode.ID(): c1.Name,
|
|
nm2.SelfNode.ID(): c2.Name,
|
|
nm3.SelfNode.ID(): c3.Name,
|
|
}
|
|
assert.Len(t, ids, 3,
|
|
"three nodes with same user should have distinct node IDs")
|
|
})
|
|
|
|
// Same hostname should get unique GivenNames.
|
|
t.Run("same_hostname_gets_unique_given_names", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := servertest.NewServer(t)
|
|
user := srv.CreateUser(t, "samename-user")
|
|
|
|
c1 := servertest.NewClient(t, srv, "samename",
|
|
servertest.WithUser(user))
|
|
c2 := servertest.NewClient(t, srv, "samename",
|
|
servertest.WithUser(user))
|
|
|
|
c1.WaitForPeers(t, 1, 10*time.Second)
|
|
c2.WaitForPeers(t, 1, 10*time.Second)
|
|
|
|
nm1 := c1.Netmap()
|
|
nm2 := c2.Netmap()
|
|
|
|
require.NotNil(t, nm1)
|
|
require.NotNil(t, nm2)
|
|
require.True(t, nm1.SelfNode.Valid())
|
|
require.True(t, nm2.SelfNode.Valid())
|
|
|
|
name1 := nm1.SelfNode.Name()
|
|
name2 := nm2.SelfNode.Name()
|
|
|
|
assert.NotEqual(t, name1, name2,
|
|
"nodes with same hostname should get distinct Name (GivenName): %q vs %q",
|
|
name1, name2)
|
|
})
|
|
|
|
// Policy change during connect should still converge.
|
|
t.Run("policy_change_during_connect", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := servertest.NewServer(t)
|
|
user := srv.CreateUser(t, "polcon-user")
|
|
|
|
c1 := servertest.NewClient(t, srv, "polcon-node1",
|
|
servertest.WithUser(user))
|
|
c1.WaitForUpdate(t, 10*time.Second)
|
|
|
|
changed, err := srv.State().SetPolicy([]byte(`{
|
|
"acls": [
|
|
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
|
|
]
|
|
}`))
|
|
require.NoError(t, err)
|
|
|
|
if changed {
|
|
changes, err := srv.State().ReloadPolicy()
|
|
require.NoError(t, err)
|
|
srv.App.Change(changes...)
|
|
}
|
|
|
|
c2 := servertest.NewClient(t, srv, "polcon-node2",
|
|
servertest.WithUser(user))
|
|
|
|
c1.WaitForPeers(t, 1, 15*time.Second)
|
|
c2.WaitForPeers(t, 1, 15*time.Second)
|
|
|
|
for _, c := range []*servertest.TestClient{c1, c2} {
|
|
nm := c.Netmap()
|
|
require.NotNil(t, nm)
|
|
assert.NotNil(t, nm.PacketFilter,
|
|
"client %s should have packet filter after policy change", c.Name)
|
|
}
|
|
})
|
|
}
|
|
|
|
func findNodeID(tb testing.TB, srv *servertest.TestServer, hostname string) types.NodeID {
|
|
tb.Helper()
|
|
|
|
nodes := srv.State().ListNodes()
|
|
for i := range nodes.Len() {
|
|
n := nodes.At(i)
|
|
if n.Hostname() == hostname {
|
|
return n.ID()
|
|
}
|
|
}
|
|
|
|
tb.Fatalf("node %q not found in server state", hostname)
|
|
|
|
return 0
|
|
}
|