mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-17 14:02:07 +09:00
@@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/juanfont/headscale/hscontrol/util"
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"tailscale.com/net/tsaddr"
|
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -48,33 +47,18 @@ func ReduceFilterRules(node types.NodeView, rules []tailcfg.FilterRule) []tailcf
|
|||||||
continue DEST_LOOP
|
continue DEST_LOOP
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the node advertises routes, ensure filter rules
|
// If the node has approved subnet routes, preserve
|
||||||
// targeting those routes are preserved. Exit routes
|
// filter rules targeting those routes. SubnetRoutes()
|
||||||
// (0.0.0.0/0, ::/0) are skipped because exit nodes
|
// returns only approved, non-exit routes — matching
|
||||||
// handle traffic via AllowedIPs/routing, not packet
|
// Tailscale SaaS behavior, which does not generate
|
||||||
// filter rules. This matches Tailscale SaaS behavior.
|
// filter rules for advertised-but-unapproved routes.
|
||||||
//
|
// Exit routes (0.0.0.0/0, ::/0) are excluded by
|
||||||
// NOTE: This uses RoutableIPs (advertised routes)
|
// SubnetRoutes() and handled separately via
|
||||||
// rather than SubnetRoutes (approved routes). The two
|
// AllowedIPs/routing.
|
||||||
// sets are identical in all 98 golden file captures
|
for _, subnetRoute := range node.SubnetRoutes() {
|
||||||
// from Tailscale SaaS, so we cannot determine from
|
if expanded.OverlapsPrefix(subnetRoute) {
|
||||||
// captured data which set Tailscale actually checks.
|
dests = append(dests, dest)
|
||||||
// RoutableIPs is a superset of SubnetRoutes (which
|
continue DEST_LOOP
|
||||||
// further filters by approval), so this is the more
|
|
||||||
// permissive choice. See MISSING_SAAS_DATA.md for
|
|
||||||
// the capture needed to resolve this ambiguity.
|
|
||||||
if node.Hostinfo().Valid() {
|
|
||||||
routableIPs := node.Hostinfo().RoutableIPs()
|
|
||||||
if routableIPs.Len() > 0 {
|
|
||||||
for _, routableIP := range routableIPs.All() {
|
|
||||||
if tsaddr.IsExitRoute(routableIP) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if expanded.OverlapsPrefix(routableIP) {
|
|
||||||
dests = append(dests, dest)
|
|
||||||
continue DEST_LOOP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,23 +111,16 @@ func reduceCapGrantRule(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also check routable IPs (subnet routes) — nodes that
|
// Also check approved subnet routes — nodes serving
|
||||||
// advertise routes should receive CapGrant rules for
|
// approved routes should receive CapGrant rules for
|
||||||
// destinations that overlap their routes.
|
// destinations that overlap those routes. SubnetRoutes()
|
||||||
if node.Hostinfo().Valid() {
|
// excludes both unapproved and exit routes, matching
|
||||||
routableIPs := node.Hostinfo().RoutableIPs()
|
// Tailscale SaaS behavior.
|
||||||
if routableIPs.Len() > 0 {
|
for _, dst := range cg.Dsts {
|
||||||
for _, dst := range cg.Dsts {
|
for _, subnetRoute := range node.SubnetRoutes() {
|
||||||
for _, routableIP := range routableIPs.All() {
|
if dst.Overlaps(subnetRoute) {
|
||||||
if tsaddr.IsExitRoute(routableIP) {
|
// For route overlaps, keep the original prefix.
|
||||||
continue
|
matchingDsts = append(matchingDsts, dst)
|
||||||
}
|
|
||||||
|
|
||||||
if dst.Overlaps(routableIP) {
|
|
||||||
// For route overlaps, keep the original prefix.
|
|
||||||
matchingDsts = append(matchingDsts, dst)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,6 +196,9 @@ func TestReduceFilterRules(t *testing.T) {
|
|||||||
netip.MustParsePrefix("10.33.0.0/16"),
|
netip.MustParsePrefix("10.33.0.0/16"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ApprovedRoutes: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("10.33.0.0/16"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
peers: types.Nodes{
|
peers: types.Nodes{
|
||||||
&types.Node{
|
&types.Node{
|
||||||
@@ -518,6 +521,7 @@ func TestReduceFilterRules(t *testing.T) {
|
|||||||
Hostinfo: &tailcfg.Hostinfo{
|
Hostinfo: &tailcfg.Hostinfo{
|
||||||
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("8.0.0.0/16"), netip.MustParsePrefix("16.0.0.0/16")},
|
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("8.0.0.0/16"), netip.MustParsePrefix("16.0.0.0/16")},
|
||||||
},
|
},
|
||||||
|
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("8.0.0.0/16"), netip.MustParsePrefix("16.0.0.0/16")},
|
||||||
},
|
},
|
||||||
peers: types.Nodes{
|
peers: types.Nodes{
|
||||||
&types.Node{
|
&types.Node{
|
||||||
@@ -601,6 +605,7 @@ func TestReduceFilterRules(t *testing.T) {
|
|||||||
Hostinfo: &tailcfg.Hostinfo{
|
Hostinfo: &tailcfg.Hostinfo{
|
||||||
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("8.0.0.0/8"), netip.MustParsePrefix("16.0.0.0/8")},
|
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("8.0.0.0/8"), netip.MustParsePrefix("16.0.0.0/8")},
|
||||||
},
|
},
|
||||||
|
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("8.0.0.0/8"), netip.MustParsePrefix("16.0.0.0/8")},
|
||||||
},
|
},
|
||||||
peers: types.Nodes{
|
peers: types.Nodes{
|
||||||
&types.Node{
|
&types.Node{
|
||||||
@@ -676,7 +681,8 @@ func TestReduceFilterRules(t *testing.T) {
|
|||||||
Hostinfo: &tailcfg.Hostinfo{
|
Hostinfo: &tailcfg.Hostinfo{
|
||||||
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("172.16.0.0/24")},
|
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("172.16.0.0/24")},
|
||||||
},
|
},
|
||||||
Tags: []string{"tag:access-servers"},
|
ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("172.16.0.0/24")},
|
||||||
|
Tags: []string{"tag:access-servers"},
|
||||||
},
|
},
|
||||||
peers: types.Nodes{
|
peers: types.Nodes{
|
||||||
&types.Node{
|
&types.Node{
|
||||||
@@ -784,16 +790,13 @@ func TestReduceFilterRules(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestReduceFilterRulesPartialApproval documents the behavior of
|
// TestReduceFilterRulesPartialApproval verifies that ReduceFilterRules
|
||||||
// ReduceFilterRules when RoutableIPs (advertised) and SubnetRoutes
|
// only preserves filter rules for routes that are both advertised
|
||||||
// (approved) differ. In production, SubnetRoutes is always a subset
|
// (RoutableIPs) AND approved (ApprovedRoutes), matching Tailscale
|
||||||
// of RoutableIPs, but the two code paths in ReduceFilterRules handle
|
// SaaS behavior. Advertised-but-unapproved routes do not cause rule
|
||||||
// them independently:
|
// preservation: SaaS never generates filter rules for unapproved
|
||||||
// - Lines 56-68: Check RoutableIPs (advertised, may be unapproved)
|
// routes, and headscale consults node.SubnetRoutes() (which filters
|
||||||
// - Lines 73-79: Check SubnetRoutes (approved only)
|
// by approval) rather than Hostinfo.RoutableIPs() (which does not).
|
||||||
//
|
|
||||||
// This test documents that ReduceFilterRules includes filter rules
|
|
||||||
// for advertised-but-unapproved routes via the RoutableIPs check.
|
|
||||||
func TestReduceFilterRulesPartialApproval(t *testing.T) {
|
func TestReduceFilterRulesPartialApproval(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -828,7 +831,7 @@ func TestReduceFilterRulesPartialApproval(t *testing.T) {
|
|||||||
wantRoutes: []string{"10.33.0.0/16"},
|
wantRoutes: []string{"10.33.0.0/16"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unapproved-route-still-included-via-routableips",
|
name: "unapproved-route-excluded",
|
||||||
node: &types.Node{
|
node: &types.Node{
|
||||||
IPv4: ap("100.64.0.1"),
|
IPv4: ap("100.64.0.1"),
|
||||||
IPv6: ap("fd7a:115c:a1e0::1"),
|
IPv6: ap("fd7a:115c:a1e0::1"),
|
||||||
@@ -853,11 +856,11 @@ func TestReduceFilterRulesPartialApproval(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// The RoutableIPs check (line 63) matches because
|
// SubnetRoutes() does NOT contain 172.16.0.0/24
|
||||||
// 172.16.0.0/24 IS in RoutableIPs. The rule is
|
// (only approved routes), and the ACL dst does not
|
||||||
// kept even though the route is not approved.
|
// overlap the node's own IPs, so the rule is
|
||||||
wantCount: 1,
|
// dropped. This matches Tailscale SaaS behavior.
|
||||||
wantRoutes: []string{"172.16.0.0/24"},
|
wantCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "neither-advertised-nor-approved-excluded",
|
name: "neither-advertised-nor-approved-excluded",
|
||||||
|
|||||||
Reference in New Issue
Block a user