mirror of
https://github.com/juanfont/headscale.git
synced 2026-11-05 21:19:25 +09:00
policy/v2: bound via route steering by the peer's routes
Include and Exclude held one entry per overlapping dst, and the election walked them per dst. Keep one entry per route; a peer without routes returns at once.
This commit is contained in:
1 parent
d8f1651ab8
commit
44e1e3fdef
2 files changed
+113
-30
No files matched your search
@@ -1440,6 +1440,11 @@ func (pm *PolicyManager) ViaRoutesForPeer(viewer, peer types.NodeView) types.Via
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A peer without routes has nothing to steer.
|
||||||
|
if len(peer.SubnetRoutes()) == 0 && !peer.IsExitNode() {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// Clip so the appends below allocate: callers share pm.pol under RLock,
|
// Clip so the appends below allocate: callers share pm.pol under RLock,
|
||||||
// and writing into its spare capacity races between them.
|
// and writing into its spare capacity races between them.
|
||||||
grants := slices.Clip(pm.pol.Grants)
|
grants := slices.Clip(pm.pol.Grants)
|
||||||
@@ -1504,11 +1509,13 @@ func (pm *PolicyManager) ViaRoutesForPeer(viewer, peer types.NodeView) types.Via
|
|||||||
|
|
||||||
var matchedPrefixes []netip.Prefix
|
var matchedPrefixes []netip.Prefix
|
||||||
|
|
||||||
for _, dstPrefix := range resolvedDstPrefixes[i] {
|
// Emit each route once, however many dst prefixes overlap it:
|
||||||
for _, route := range peerSubnetRoutes {
|
// the passes below walk Include and Exclude per grant, so their
|
||||||
if dstPrefix.Overlaps(route) {
|
// size must be bounded by the peer's routes, not by the grant's
|
||||||
matchedPrefixes = append(matchedPrefixes, route)
|
// destinations.
|
||||||
}
|
for _, route := range peerSubnetRoutes {
|
||||||
|
if slices.ContainsFunc(resolvedDstPrefixes[i], route.Overlaps) {
|
||||||
|
matchedPrefixes = append(matchedPrefixes, route)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1536,10 +1543,16 @@ func (pm *PolicyManager) ViaRoutesForPeer(viewer, peer types.NodeView) types.Via
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if peerHasVia {
|
// Several grants, or repeated announcements, can match the same
|
||||||
result.Include = append(result.Include, matchedPrefixes...)
|
// route; keep one entry per route.
|
||||||
} else {
|
for _, p := range matchedPrefixes {
|
||||||
result.Exclude = append(result.Exclude, matchedPrefixes...)
|
if peerHasVia {
|
||||||
|
if !slices.Contains(result.Include, p) {
|
||||||
|
result.Include = append(result.Include, p)
|
||||||
|
}
|
||||||
|
} else if !slices.Contains(result.Exclude, p) {
|
||||||
|
result.Exclude = append(result.Exclude, p)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1579,33 +1592,34 @@ func (pm *PolicyManager) ViaRoutesForPeer(viewer, peer types.NodeView) types.Via
|
|||||||
|
|
||||||
// Elect per matched route, not per dst — a peer can only
|
// Elect per matched route, not per dst — a peer can only
|
||||||
// be primary for a prefix it actually advertises, and one
|
// be primary for a prefix it actually advertises, and one
|
||||||
// dst may cover multiple distinct routes.
|
// dst may cover multiple distinct routes. The election
|
||||||
for _, dstPrefix := range resolvedDstPrefixes[i] {
|
// does not depend on which dst matched, so each included
|
||||||
for _, included := range slices.Clone(result.Include) {
|
// route is looked at once per grant.
|
||||||
if !dstPrefix.Overlaps(included) {
|
for _, included := range slices.Clone(result.Include) {
|
||||||
continue
|
if !slices.ContainsFunc(resolvedDstPrefixes[i], included.Overlaps) {
|
||||||
}
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var viaPrimaryID types.NodeID
|
var viaPrimaryID types.NodeID
|
||||||
|
|
||||||
for _, viaTag := range grant.Via {
|
for _, viaTag := range grant.Via {
|
||||||
for _, node := range pm.nodes.All() {
|
for _, node := range pm.nodes.All() {
|
||||||
if node.HasTag(string(viaTag)) &&
|
if node.HasTag(string(viaTag)) &&
|
||||||
slices.Contains(node.SubnetRoutes(), included) {
|
slices.Contains(node.SubnetRoutes(), included) {
|
||||||
if viaPrimaryID == 0 || node.ID() < viaPrimaryID {
|
if viaPrimaryID == 0 || node.ID() < viaPrimaryID {
|
||||||
viaPrimaryID = node.ID()
|
viaPrimaryID = node.ID()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if viaPrimaryID != 0 && peer.ID() != viaPrimaryID {
|
if viaPrimaryID != 0 && peer.ID() != viaPrimaryID {
|
||||||
result.Include = slices.DeleteFunc(result.Include, func(p netip.Prefix) bool {
|
result.Include = slices.DeleteFunc(
|
||||||
return p == included
|
result.Include,
|
||||||
})
|
func(p netip.Prefix) bool { return p == included },
|
||||||
if !slices.Contains(result.Exclude, included) {
|
)
|
||||||
result.Exclude = append(result.Exclude, included)
|
if !slices.Contains(result.Exclude, included) {
|
||||||
}
|
result.Exclude = append(result.Exclude, included)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2571,6 +2571,75 @@ func TestViaRoutesForPeer(t *testing.T) {
|
|||||||
require.Empty(t, result.Exclude)
|
require.Empty(t, result.Exclude)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// A route is steered once, however many dst prefixes or grants overlap
|
||||||
|
// it and however often the peer announces it. Otherwise Include grows
|
||||||
|
// with the policy instead of the peer's routes, and the election pass,
|
||||||
|
// which walks Include per dst, goes quadratic under the policy lock.
|
||||||
|
t.Run("route_steered_once_per_peer", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
nodes := types.Nodes{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Hostname: "viewer",
|
||||||
|
IPv4: ap("100.64.0.1"),
|
||||||
|
User: new(users[0]),
|
||||||
|
UserID: new(users[0].ID),
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
Hostname: "router",
|
||||||
|
IPv4: ap("100.64.0.2"),
|
||||||
|
User: new(users[0]),
|
||||||
|
UserID: new(users[0].ID),
|
||||||
|
Tags: []string{"tag:router"},
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{
|
||||||
|
RoutableIPs: []netip.Prefix{mp("10.0.0.0/24"), mp("10.0.0.0/24")},
|
||||||
|
},
|
||||||
|
ApprovedRoutes: []netip.Prefix{mp("10.0.0.0/24")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 3,
|
||||||
|
Hostname: "other",
|
||||||
|
IPv4: ap("100.64.0.3"),
|
||||||
|
User: new(users[0]),
|
||||||
|
UserID: new(users[0].ID),
|
||||||
|
Tags: []string{"tag:other"},
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{
|
||||||
|
RoutableIPs: []netip.Prefix{mp("10.0.0.0/24"), mp("10.0.0.0/24")},
|
||||||
|
},
|
||||||
|
ApprovedRoutes: []netip.Prefix{mp("10.0.0.0/24")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
grant := `{
|
||||||
|
"src": ["user1@"],
|
||||||
|
"dst": ["10.0.0.0/24", "10.0.0.0/24", "10.0.0.0/25", "10.0.0.7/32"],
|
||||||
|
"ip": ["*"],
|
||||||
|
"via": ["tag:router"]
|
||||||
|
}`
|
||||||
|
|
||||||
|
pol := `{
|
||||||
|
"tagOwners": {
|
||||||
|
"tag:router": ["user1@"],
|
||||||
|
"tag:other": ["user1@"]
|
||||||
|
},
|
||||||
|
"grants": [` + grant + `, ` + grant + `]
|
||||||
|
}`
|
||||||
|
|
||||||
|
pm, err := NewPolicyManager([]byte(pol), users, nodes.ViewSlice())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
result := pm.ViaRoutesForPeer(nodes[0].View(), nodes[1].View())
|
||||||
|
require.Equal(t, []netip.Prefix{mp("10.0.0.0/24")}, result.Include)
|
||||||
|
require.Empty(t, result.Exclude)
|
||||||
|
|
||||||
|
result = pm.ViaRoutesForPeer(nodes[0].View(), nodes[2].View())
|
||||||
|
require.Empty(t, result.Include)
|
||||||
|
require.Equal(t, []netip.Prefix{mp("10.0.0.0/24")}, result.Exclude)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestBuildPeerMap_AutogroupInternetMakesExitNodeVisible reproduces
|
// TestBuildPeerMap_AutogroupInternetMakesExitNodeVisible reproduces
|
||||||
|
|||||||
Reference in new issue
Block a user