ipn/ipnlocal: allowed suggested exit nodes policy (#12240)

Updates tailscale/corp#19681

Signed-off-by: Claire Wang <claire@tailscale.com>
This commit is contained in:
Claire Wang 2024-05-27 16:22:36 -04:00 committed by GitHub
parent 5ad0dad15e
commit f1d10c12ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 147 additions and 0 deletions

View File

@ -6453,8 +6453,17 @@ func suggestExitNode(report *netcheck.Report, netMap *netmap.NetworkMap, r *rand
if report.PreferredDERP == 0 {
return res, ErrNoPreferredDERP
}
var allowedCandidates set.Set[string]
if allowed, err := syspolicy.GetStringArray(syspolicy.AllowedSuggestedExitNodes, nil); err != nil {
return res, fmt.Errorf("unable to read %s policy: %w", syspolicy.AllowedSuggestedExitNodes, err)
} else if allowed != nil {
allowedCandidates = set.SetOf(allowed)
}
candidates := make([]tailcfg.NodeView, 0, len(netMap.Peers))
for _, peer := range netMap.Peers {
if allowedCandidates != nil && !allowedCandidates.Contains(string(peer.StableID())) {
continue
}
if peer.CapMap().Has(tailcfg.NodeAttrSuggestExitNode) && tsaddr.ContainsExitRoutes(peer.AllowedIPs()) {
candidates = append(candidates, peer)
}

View File

@ -1595,6 +1595,9 @@ type mockSyspolicyHandler struct {
// queried by the current test. If the policy is expected but unset, then
// use nil, otherwise use a string equal to the policy's desired value.
stringPolicies map[syspolicy.Key]*string
// stringArrayPolicies is the collection of policies that we expected to see
// queries by the current test, that return policy string arrays.
stringArrayPolicies map[syspolicy.Key][]string
// failUnknownPolicies is set if policies other than those in stringPolicies
// (uint64 or bool policies are not supported by mockSyspolicyHandler yet)
// should be considered a test failure if they are queried.
@ -1632,6 +1635,12 @@ func (h *mockSyspolicyHandler) ReadStringArray(key string) ([]string, error) {
if h.failUnknownPolicies {
h.t.Errorf("ReadStringArray(%q) unexpectedly called", key)
}
if s, ok := h.stringArrayPolicies[syspolicy.Key(key)]; ok {
if s == nil {
return []string{}, syspolicy.ErrNoSuchKey
}
return s, nil
}
return nil, syspolicy.ErrNoSuchKey
}
@ -3474,6 +3483,7 @@ func TestLocalBackendSuggestExitNode(t *testing.T) {
lastSuggestedExitNode lastSuggestedExitNode
report *netcheck.Report
netMap netmap.NetworkMap
allowedSuggestedExitNodes []string
wantID tailcfg.StableNodeID
wantName string
wantErr error
@ -3766,10 +3776,138 @@ func TestLocalBackendSuggestExitNode(t *testing.T) {
},
wantErr: ErrCannotSuggestExitNode,
},
{
name: "only pick from allowed suggested exit nodes",
lastSuggestedExitNode: lastSuggestedExitNode{name: "test", id: "test"},
report: &netcheck.Report{
RegionLatency: map[int]time.Duration{
1: 10,
2: 10,
3: 5,
},
PreferredDERP: 1,
},
netMap: netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.1.1/32"),
netip.MustParsePrefix("fe70::1/128"),
},
}).View(),
DERPMap: &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
1: {},
2: {},
3: {},
},
},
Peers: []tailcfg.NodeView{
(&tailcfg.Node{
ID: 2,
StableID: "test",
Name: "test",
DERP: "127.3.3.40:1",
AllowedIPs: []netip.Prefix{
netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"),
},
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
tailcfg.NodeAttrSuggestExitNode: {},
tailcfg.NodeAttrAutoExitNode: {},
}),
}).View(),
(&tailcfg.Node{
ID: 3,
StableID: "foo",
Name: "foo",
DERP: "127.3.3.40:3",
AllowedIPs: []netip.Prefix{
netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"),
},
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
tailcfg.NodeAttrSuggestExitNode: {},
tailcfg.NodeAttrAutoExitNode: {},
}),
}).View(),
},
},
allowedSuggestedExitNodes: []string{"test"},
wantID: "test",
wantName: "test",
wantLastSuggestedExitNode: lastSuggestedExitNode{name: "test", id: "test"},
},
{
name: "allowed suggested exit nodes not nil but length 0",
lastSuggestedExitNode: lastSuggestedExitNode{name: "test", id: "test"},
report: &netcheck.Report{
RegionLatency: map[int]time.Duration{
1: 10,
2: 10,
3: 5,
},
PreferredDERP: 1,
},
netMap: netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.1.1/32"),
netip.MustParsePrefix("fe70::1/128"),
},
}).View(),
DERPMap: &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
1: {},
2: {},
3: {},
},
},
Peers: []tailcfg.NodeView{
(&tailcfg.Node{
ID: 2,
StableID: "test",
Name: "test",
DERP: "127.3.3.40:1",
AllowedIPs: []netip.Prefix{
netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"),
},
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
tailcfg.NodeAttrSuggestExitNode: {},
tailcfg.NodeAttrAutoExitNode: {},
}),
}).View(),
(&tailcfg.Node{
ID: 3,
StableID: "foo",
Name: "foo",
DERP: "127.3.3.40:3",
AllowedIPs: []netip.Prefix{
netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"),
},
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
tailcfg.NodeAttrSuggestExitNode: {},
tailcfg.NodeAttrAutoExitNode: {},
}),
}).View(),
},
},
allowedSuggestedExitNodes: []string{},
wantID: "foo",
wantName: "foo",
wantLastSuggestedExitNode: lastSuggestedExitNode{name: "foo", id: "foo"},
},
}
for _, tt := range tests {
lb := newTestLocalBackend(t)
msh := &mockSyspolicyHandler{
t: t,
stringArrayPolicies: map[syspolicy.Key][]string{
syspolicy.AllowedSuggestedExitNodes: nil,
},
}
if len(tt.allowedSuggestedExitNodes) != 0 {
msh.stringArrayPolicies[syspolicy.AllowedSuggestedExitNodes] = tt.allowedSuggestedExitNodes
}
syspolicy.SetHandlerForTest(t, msh)
lb.lastSuggestedExitNode = tt.lastSuggestedExitNode
lb.netMap = &tt.netMap
lb.sys.MagicSock.Get().SetLastNetcheckReportForTest(context.Background(), tt.report)