|
|
@@ -35,6 +35,7 @@ import (
|
|
|
"tailscale.com/net/netcheck"
|
|
|
"tailscale.com/net/netmon"
|
|
|
"tailscale.com/net/tsaddr"
|
|
|
+ "tailscale.com/net/tsdial"
|
|
|
"tailscale.com/tailcfg"
|
|
|
"tailscale.com/tsd"
|
|
|
"tailscale.com/tstest"
|
|
|
@@ -1647,16 +1648,17 @@ func (h *mockSyspolicyHandler) ReadStringArray(key string) ([]string, error) {
|
|
|
func TestSetExitNodeIDPolicy(t *testing.T) {
|
|
|
pfx := netip.MustParsePrefix
|
|
|
tests := []struct {
|
|
|
- name string
|
|
|
- exitNodeIPKey bool
|
|
|
- exitNodeIDKey bool
|
|
|
- exitNodeID string
|
|
|
- exitNodeIP string
|
|
|
- prefs *ipn.Prefs
|
|
|
- exitNodeIPWant string
|
|
|
- exitNodeIDWant string
|
|
|
- prefsChanged bool
|
|
|
- nm *netmap.NetworkMap
|
|
|
+ name string
|
|
|
+ exitNodeIPKey bool
|
|
|
+ exitNodeIDKey bool
|
|
|
+ exitNodeID string
|
|
|
+ exitNodeIP string
|
|
|
+ prefs *ipn.Prefs
|
|
|
+ exitNodeIPWant string
|
|
|
+ exitNodeIDWant string
|
|
|
+ prefsChanged bool
|
|
|
+ nm *netmap.NetworkMap
|
|
|
+ lastSuggestedExitNode tailcfg.StableNodeID
|
|
|
}{
|
|
|
{
|
|
|
name: "ExitNodeID key is set",
|
|
|
@@ -1835,6 +1837,21 @@ func TestSetExitNodeIDPolicy(t *testing.T) {
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
+ {
|
|
|
+ name: "ExitNodeID key is set to auto and last suggested exit node is populated",
|
|
|
+ exitNodeIDKey: true,
|
|
|
+ exitNodeID: "auto:any",
|
|
|
+ lastSuggestedExitNode: "123",
|
|
|
+ exitNodeIDWant: "123",
|
|
|
+ prefsChanged: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "ExitNodeID key is set to auto and last suggested exit node is not populated",
|
|
|
+ exitNodeIDKey: true,
|
|
|
+ exitNodeID: "auto:any",
|
|
|
+ prefsChanged: true,
|
|
|
+ exitNodeIDWant: "auto:any",
|
|
|
+ },
|
|
|
}
|
|
|
|
|
|
for _, test := range tests {
|
|
|
@@ -1864,7 +1881,8 @@ func TestSetExitNodeIDPolicy(t *testing.T) {
|
|
|
pm.prefs = test.prefs.View()
|
|
|
b.netMap = test.nm
|
|
|
b.pm = pm
|
|
|
- changed := setExitNodeID(b.pm.prefs.AsStruct(), test.nm)
|
|
|
+ b.lastSuggestedExitNode = test.lastSuggestedExitNode
|
|
|
+ changed := setExitNodeID(b.pm.prefs.AsStruct(), test.nm, tailcfg.StableNodeID(test.lastSuggestedExitNode))
|
|
|
b.SetPrefsForTest(pm.CurrentPrefs().AsStruct())
|
|
|
|
|
|
if got := b.pm.prefs.ExitNodeID(); got != tailcfg.StableNodeID(test.exitNodeIDWant) {
|
|
|
@@ -1885,6 +1903,222 @@ func TestSetExitNodeIDPolicy(t *testing.T) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+func TestUpdateNetmapDeltaAutoExitNode(t *testing.T) {
|
|
|
+ peer1 := makePeer(1, withCap(26), withSuggest(), withExitRoutes())
|
|
|
+ peer2 := makePeer(2, withCap(26), withSuggest(), withExitRoutes())
|
|
|
+ derpMap := &tailcfg.DERPMap{
|
|
|
+ Regions: map[int]*tailcfg.DERPRegion{
|
|
|
+ 1: {
|
|
|
+ Nodes: []*tailcfg.DERPNode{
|
|
|
+ {
|
|
|
+ Name: "t1",
|
|
|
+ RegionID: 1,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ 2: {
|
|
|
+ Nodes: []*tailcfg.DERPNode{
|
|
|
+ {
|
|
|
+ Name: "t2",
|
|
|
+ RegionID: 2,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }
|
|
|
+ report := &netcheck.Report{
|
|
|
+ RegionLatency: map[int]time.Duration{
|
|
|
+ 1: 10 * time.Millisecond,
|
|
|
+ 2: 5 * time.Millisecond,
|
|
|
+ 3: 30 * time.Millisecond,
|
|
|
+ },
|
|
|
+ PreferredDERP: 2,
|
|
|
+ }
|
|
|
+ tests := []struct {
|
|
|
+ name string
|
|
|
+ lastSuggestedExitNode tailcfg.StableNodeID
|
|
|
+ netmap *netmap.NetworkMap
|
|
|
+ muts []*tailcfg.PeerChange
|
|
|
+ exitNodeIDWant tailcfg.StableNodeID
|
|
|
+ updateNetmapDeltaResponse bool
|
|
|
+ report *netcheck.Report
|
|
|
+ }{
|
|
|
+ {
|
|
|
+ name: "selected auto exit node goes offline",
|
|
|
+ lastSuggestedExitNode: peer1.StableID(),
|
|
|
+ netmap: &netmap.NetworkMap{
|
|
|
+ Peers: []tailcfg.NodeView{
|
|
|
+ peer1,
|
|
|
+ peer2,
|
|
|
+ },
|
|
|
+ DERPMap: derpMap,
|
|
|
+ },
|
|
|
+ muts: []*tailcfg.PeerChange{
|
|
|
+ {
|
|
|
+ NodeID: 1,
|
|
|
+ Online: ptr.To(false),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ NodeID: 2,
|
|
|
+ Online: ptr.To(true),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ exitNodeIDWant: peer2.StableID(),
|
|
|
+ updateNetmapDeltaResponse: false,
|
|
|
+ report: report,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "other exit node goes offline doesn't change selected auto exit node that's still online",
|
|
|
+ lastSuggestedExitNode: peer2.StableID(),
|
|
|
+ netmap: &netmap.NetworkMap{
|
|
|
+ Peers: []tailcfg.NodeView{
|
|
|
+ peer1,
|
|
|
+ peer2,
|
|
|
+ },
|
|
|
+ DERPMap: derpMap,
|
|
|
+ },
|
|
|
+ muts: []*tailcfg.PeerChange{
|
|
|
+ {
|
|
|
+ NodeID: 1,
|
|
|
+ Online: ptr.To(false),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ NodeID: 2,
|
|
|
+ Online: ptr.To(true),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ exitNodeIDWant: peer2.StableID(),
|
|
|
+ updateNetmapDeltaResponse: true,
|
|
|
+ report: report,
|
|
|
+ },
|
|
|
+ }
|
|
|
+ msh := &mockSyspolicyHandler{
|
|
|
+ t: t,
|
|
|
+ stringPolicies: map[syspolicy.Key]*string{
|
|
|
+ syspolicy.ExitNodeID: ptr.To("auto:any"),
|
|
|
+ },
|
|
|
+ }
|
|
|
+ syspolicy.SetHandlerForTest(t, msh)
|
|
|
+ for _, tt := range tests {
|
|
|
+ t.Run(tt.name, func(t *testing.T) {
|
|
|
+ b := newTestLocalBackend(t)
|
|
|
+ b.netMap = tt.netmap
|
|
|
+ b.updatePeersFromNetmapLocked(b.netMap)
|
|
|
+ b.lastSuggestedExitNode = tt.lastSuggestedExitNode
|
|
|
+ b.sys.MagicSock.Get().SetLastNetcheckReportForTest(b.ctx, tt.report)
|
|
|
+ b.SetPrefsForTest(b.pm.CurrentPrefs().AsStruct())
|
|
|
+ someTime := time.Unix(123, 0)
|
|
|
+ muts, ok := netmap.MutationsFromMapResponse(&tailcfg.MapResponse{
|
|
|
+ PeersChangedPatch: tt.muts,
|
|
|
+ }, someTime)
|
|
|
+ if !ok {
|
|
|
+ t.Fatal("netmap.MutationsFromMapResponse failed")
|
|
|
+ }
|
|
|
+ if b.pm.prefs.ExitNodeID() != tt.lastSuggestedExitNode {
|
|
|
+ t.Fatalf("did not set exit node ID to last suggested exit node despite auto policy")
|
|
|
+ }
|
|
|
+
|
|
|
+ got := b.UpdateNetmapDelta(muts)
|
|
|
+ if got != tt.updateNetmapDeltaResponse {
|
|
|
+ t.Fatalf("got %v expected %v from UpdateNetmapDelta", got, tt.updateNetmapDeltaResponse)
|
|
|
+ }
|
|
|
+ if b.pm.prefs.ExitNodeID() != tt.exitNodeIDWant {
|
|
|
+ t.Fatalf("did not get expected exit node id after UpdateNetmapDelta")
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestAutoExitNodeSetNetInfoCallback(t *testing.T) {
|
|
|
+ b := newTestLocalBackend(t)
|
|
|
+ hi := hostinfo.New()
|
|
|
+ ni := tailcfg.NetInfo{LinkType: "wired"}
|
|
|
+ hi.NetInfo = &ni
|
|
|
+ b.hostinfo = hi
|
|
|
+ k := key.NewMachine()
|
|
|
+ var cc *mockControl
|
|
|
+ opts := controlclient.Options{
|
|
|
+ ServerURL: "https://example.com",
|
|
|
+ GetMachinePrivateKey: func() (key.MachinePrivate, error) {
|
|
|
+ return k, nil
|
|
|
+ },
|
|
|
+ Dialer: tsdial.NewDialer(netmon.NewStatic()),
|
|
|
+ Logf: b.logf,
|
|
|
+ }
|
|
|
+ cc = newClient(t, opts)
|
|
|
+ b.cc = cc
|
|
|
+ msh := &mockSyspolicyHandler{
|
|
|
+ t: t,
|
|
|
+ stringPolicies: map[syspolicy.Key]*string{
|
|
|
+ syspolicy.ExitNodeID: ptr.To("auto:any"),
|
|
|
+ },
|
|
|
+ }
|
|
|
+ syspolicy.SetHandlerForTest(t, msh)
|
|
|
+ peer1 := makePeer(1, withCap(26), withDERP(3), withSuggest(), withExitRoutes())
|
|
|
+ peer2 := makePeer(2, withCap(26), withDERP(2), withSuggest(), withExitRoutes())
|
|
|
+ selfNode := tailcfg.Node{
|
|
|
+ Addresses: []netip.Prefix{
|
|
|
+ netip.MustParsePrefix("100.64.1.1/32"),
|
|
|
+ netip.MustParsePrefix("fe70::1/128"),
|
|
|
+ },
|
|
|
+ DERP: "127.3.3.40:2",
|
|
|
+ }
|
|
|
+ defaultDERPMap := &tailcfg.DERPMap{
|
|
|
+ Regions: map[int]*tailcfg.DERPRegion{
|
|
|
+ 1: {
|
|
|
+ Nodes: []*tailcfg.DERPNode{
|
|
|
+ {
|
|
|
+ Name: "t1",
|
|
|
+ RegionID: 1,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ 2: {
|
|
|
+ Nodes: []*tailcfg.DERPNode{
|
|
|
+ {
|
|
|
+ Name: "t2",
|
|
|
+ RegionID: 2,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ 3: {
|
|
|
+ Nodes: []*tailcfg.DERPNode{
|
|
|
+ {
|
|
|
+ Name: "t3",
|
|
|
+ RegionID: 3,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }
|
|
|
+ b.netMap = &netmap.NetworkMap{
|
|
|
+ SelfNode: selfNode.View(),
|
|
|
+ Peers: []tailcfg.NodeView{
|
|
|
+ peer1,
|
|
|
+ peer2,
|
|
|
+ },
|
|
|
+ DERPMap: defaultDERPMap,
|
|
|
+ }
|
|
|
+ b.lastSuggestedExitNode = peer1.StableID()
|
|
|
+ b.SetPrefsForTest(b.pm.CurrentPrefs().AsStruct())
|
|
|
+ if eid := b.Prefs().ExitNodeID(); eid != peer1.StableID() {
|
|
|
+ t.Errorf("got initial exit node %v, want %v", eid, peer1.StableID())
|
|
|
+ }
|
|
|
+ b.refreshAutoExitNode = true
|
|
|
+ b.sys.MagicSock.Get().SetLastNetcheckReportForTest(b.ctx, &netcheck.Report{
|
|
|
+ RegionLatency: map[int]time.Duration{
|
|
|
+ 1: 10 * time.Millisecond,
|
|
|
+ 2: 5 * time.Millisecond,
|
|
|
+ 3: 30 * time.Millisecond,
|
|
|
+ },
|
|
|
+ PreferredDERP: 2,
|
|
|
+ })
|
|
|
+ b.setNetInfo(&ni)
|
|
|
+ if eid := b.Prefs().ExitNodeID(); eid != peer2.StableID() {
|
|
|
+ t.Errorf("got final exit node %v, want %v", eid, peer2.StableID())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
func TestApplySysPolicy(t *testing.T) {
|
|
|
tests := []struct {
|
|
|
name string
|
|
|
@@ -2796,6 +3030,12 @@ func withSuggest() peerOptFunc {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+func withCap(version tailcfg.CapabilityVersion) peerOptFunc {
|
|
|
+ return func(n *tailcfg.Node) {
|
|
|
+ n.Cap = version
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
func deterministicRegionForTest(t testing.TB, want views.Slice[int], use int) selectRegionFunc {
|
|
|
t.Helper()
|
|
|
|
|
|
@@ -3473,6 +3713,55 @@ func TestMinLatencyDERPregion(t *testing.T) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+func TestShouldAutoExitNode(t *testing.T) {
|
|
|
+ tests := []struct {
|
|
|
+ name string
|
|
|
+ exitNodeIDPolicyValue string
|
|
|
+ expectedBool bool
|
|
|
+ }{
|
|
|
+ {
|
|
|
+ name: "auto:any",
|
|
|
+ exitNodeIDPolicyValue: "auto:any",
|
|
|
+ expectedBool: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "no auto prefix",
|
|
|
+ exitNodeIDPolicyValue: "foo",
|
|
|
+ expectedBool: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "auto prefix but empty suffix",
|
|
|
+ exitNodeIDPolicyValue: "auto:",
|
|
|
+ expectedBool: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "auto prefix no colon",
|
|
|
+ exitNodeIDPolicyValue: "auto",
|
|
|
+ expectedBool: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "auto prefix invalid suffix",
|
|
|
+ exitNodeIDPolicyValue: "auto:foo",
|
|
|
+ expectedBool: false,
|
|
|
+ },
|
|
|
+ }
|
|
|
+ for _, tt := range tests {
|
|
|
+ t.Run(tt.name, func(t *testing.T) {
|
|
|
+ msh := &mockSyspolicyHandler{
|
|
|
+ t: t,
|
|
|
+ stringPolicies: map[syspolicy.Key]*string{
|
|
|
+ syspolicy.ExitNodeID: ptr.To(tt.exitNodeIDPolicyValue),
|
|
|
+ },
|
|
|
+ }
|
|
|
+ syspolicy.SetHandlerForTest(t, msh)
|
|
|
+ got := shouldAutoExitNode()
|
|
|
+ if got != tt.expectedBool {
|
|
|
+ t.Fatalf("expected %v got %v for %v policy value", tt.expectedBool, got, tt.exitNodeIDPolicyValue)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
func TestEnableAutoUpdates(t *testing.T) {
|
|
|
lb := newTestLocalBackend(t)
|
|
|
|