policy/v2: remove resolved SUBNET_ROUTE_FILTER_RULES grant skips

Remove 10 grant skip entries for subnet route filter rule generation.
These tests now pass after the exit route exclusion fix in
ReduceFilterRules, which correctly handles routable IPs overlap
for subnet-router nodes.

Updates skip count from 207 to 197 (v1) and 109 to 99 (v2),
with 10 additional tests now expected to pass.

Updates #2180
This commit is contained in:
Kristoffer Dalby
2026-03-18 14:40:35 +00:00
parent 044f3fc0ec
commit 19b5a39aec
5 changed files with 31 additions and 61 deletions

View File

@@ -218,7 +218,6 @@ func loadGrantTestFile(t *testing.T, path string) grantTestFile {
// ERROR_VALIDATION_GAP - 23 tests: Implement missing grant validation rules
// MISSING_IPV6_ADDRS - 90 tests: Include IPv6 for identity-based alias resolution
// CAPGRANT_COMPILATION_AND_SRCIPS - 11 tests: Both CapGrant compilation + SrcIPs format
// SUBNET_ROUTE_FILTER_RULES - 10 tests: Generate filter rules for subnet-routed CIDRs
// VIA_COMPILATION_AND_SRCIPS_FORMAT - 7 tests: Via route compilation + SrcIPs format
// AUTOGROUP_SELF_CIDR_FORMAT - 4 tests: DstPorts IPs get /32 or /128 suffix for autogroup:self
// VIA_COMPILATION - 3 tests: Via route compilation
@@ -228,41 +227,8 @@ func loadGrantTestFile(t *testing.T, path string) grantTestFile {
// RAW_IPV6_ADDR_EXPANSION - 2 tests: Raw fd7a: IPv6 src/dst expanded to include IPv4
// SRCIPS_WILDCARD_NODE_DEDUP - 1 test: Wildcard+specific source node IP deduplication
//
// Total: 207 tests skipped, 30 tests expected to pass.
// Total: 197 tests skipped, 40 tests expected to pass.
var grantSkipReasons = map[string]string{
// ========================================================================
// SUBNET_ROUTE_FILTER_RULES (11 tests)
//
// TODO: Generate filter rules for non-Tailscale CIDR destinations on
// subnet-router nodes.
//
// When a grant targets a non-Tailscale CIDR (e.g., 10.0.0.0/8,
// 10.33.0.0/16, 10.33.1.0/24), Tailscale generates FilterRules on the
// subnet-router node that advertises overlapping routes. headscale
// produces no rules for these destinations, resulting in empty output
// on the subnet-router node.
//
// Example (GRANT-P13_1, dst=10.33.0.0/16):
// tailscale produces on subnet-router:
// SrcIPs=["100.103.90.82","100.110.121.96","100.90.199.68", + IPv6s]
// DstPorts=[{IP:"10.33.0.0/16", Ports:"22"}]
// headscale produces: [] (empty)
//
// Fix: During filter rule compilation, check if a destination CIDR
// overlaps with any subnet route advertised by the current node, and
// if so, generate the appropriate FilterRule.
// ========================================================================
"GRANT-P08_8": "SUBNET_ROUTE_FILTER_RULES: dst=10.0.0.0/8 — subnet-router gets no rules",
"GRANT-P09_6D": "SUBNET_ROUTE_FILTER_RULES: dst=internal (host alias for 10.0.0.0/8) — subnet-router gets no rules",
"GRANT-P10_3": "SUBNET_ROUTE_FILTER_RULES: dst=host alias for 10.33.0.0/16 — subnet-router gets no rules",
"GRANT-P10_4": "SUBNET_ROUTE_FILTER_RULES: dst=host alias for 10.33.0.0/16 — subnet-router gets no rules",
"GRANT-P13_1": "SUBNET_ROUTE_FILTER_RULES: dst=10.33.0.0/16 port 22 — subnet-router gets no rules",
"GRANT-P13_2": "SUBNET_ROUTE_FILTER_RULES: dst=10.33.0.0/16 port 80-443 — subnet-router gets no rules",
"GRANT-P13_3": "SUBNET_ROUTE_FILTER_RULES: dst=10.33.0.0/16 ports 22,80,443 — subnet-router gets no rules",
"GRANT-P09_12B": "SUBNET_ROUTE_FILTER_RULES: subnet-router subtest missing entire rule for 10.0.0.0/8",
"GRANT-P15_1": "SUBNET_ROUTE_FILTER_RULES: dst=10.33.1.0/24 port 22 — subnet-router gets no rules",
"GRANT-P15_3": "SUBNET_ROUTE_FILTER_RULES: dst=10.32.0.0/14 port 22 — subnet-router gets no rules",
// ========================================================================
// USER_PASSKEY_WILDCARD (2 tests)
//
@@ -551,7 +517,6 @@ var grantSkipReasons = map[string]string{
// CAPGRANT_COMPILATION - 49 tests: Implement app->CapGrant FilterRule compilation
// ERROR_VALIDATION_GAP - 23 tests: Implement missing grant validation rules
// CAPGRANT_COMPILATION_AND_SRCIPS - 11 tests: Both CapGrant compilation + SrcIPs format
// SUBNET_ROUTE_FILTER_RULES - 11 tests: Generate filter rules for subnet-routed CIDRs
// VIA_COMPILATION_AND_SRCIPS_FORMAT - 7 tests: Via route compilation + SrcIPs format
// VIA_COMPILATION - 3 tests: Via route compilation
// AUTOGROUP_DANGER_ALL - 3 tests: Implement autogroup:danger-all support
@@ -559,7 +524,7 @@ var grantSkipReasons = map[string]string{
// VALIDATION_STRICTNESS - 2 tests: headscale too strict (rejects what Tailscale accepts)
// SRCIPS_WILDCARD_NODE_DEDUP - 1 test: Wildcard+specific source node IP deduplication
//
// Total: 109 tests skipped, ~128 tests expected to pass.
// Total: 99 tests skipped, ~138 tests expected to pass.
func TestGrantsCompat(t *testing.T) {
t.Parallel()

View File

@@ -62,7 +62,7 @@ const (
// ACL validation errors.
var (
ErrACLAutogroupSelfInvalidSource = errors.New("autogroup:self destination requires sources to be users, groups, or autogroup:member only")
ErrACLAutogroupSelfInvalidSource = errors.New("autogroup:self can only be used with users, groups, or supported autogroups")
)
// Grant validation errors.
@@ -102,7 +102,7 @@ var (
ErrGroupValueNotArray = errors.New("group value must be an array of users")
ErrNestedGroups = errors.New("nested groups are not allowed")
ErrInvalidHostIP = errors.New("hostname contains invalid IP address")
ErrTagNotDefined = errors.New("tag not defined in policy")
ErrTagNotDefined = errors.New("tag not found")
ErrAutoApproverNotAlias = errors.New("auto approver is not an alias")
ErrInvalidACLAction = errors.New("invalid ACL action")
ErrInvalidSSHAction = errors.New("invalid SSH action")
@@ -111,7 +111,7 @@ var (
ErrProtocolOutOfRange = errors.New("protocol number out of range (0-255)")
ErrAutogroupNotSupported = errors.New("autogroup not supported in headscale")
ErrAutogroupInternetSrc = errors.New("autogroup:internet can only be used in ACL destinations")
ErrAutogroupSelfSrc = errors.New("autogroup:self can only be used in ACL destinations")
ErrAutogroupSelfSrc = errors.New("\"autogroup:self\" not valid on the src side of a rule")
ErrAutogroupNotSupportedACLSrc = errors.New("autogroup not supported for ACL sources")
ErrAutogroupNotSupportedACLDst = errors.New("autogroup not supported for ACL destinations")
ErrAutogroupNotSupportedSSHSrc = errors.New("autogroup not supported for SSH sources")
@@ -835,6 +835,8 @@ func (ve *AliasWithPorts) UnmarshalJSON(b []byte) error {
err error
)
originalDst := vs
if strings.Contains(vs, ":") {
vs, portsPart, err = splitDestinationAndPort(vs)
if err != nil {
@@ -843,7 +845,10 @@ func (ve *AliasWithPorts) UnmarshalJSON(b []byte) error {
ports, err := parsePortRange(portsPart)
if err != nil {
return err
return fmt.Errorf(
"dst=%q: port range %q: %w",
originalDst, portsPart, err,
)
}
ve.Ports = ports
@@ -895,7 +900,7 @@ func (ve *ProtocolPort) UnmarshalJSON(b []byte) error {
if !strings.Contains(vs, ":") {
ports, err := parsePortRange(vs)
if err != nil {
return err
return fmt.Errorf("port range %q: %w", vs, err)
}
ve.Protocol = ProtocolNameWildcard
@@ -920,7 +925,7 @@ func (ve *ProtocolPort) UnmarshalJSON(b []byte) error {
ports, err := parsePortRange(portsPart)
if err != nil {
return err
return fmt.Errorf("port range %q: %w", portsPart, err)
}
ve.Protocol = protocol
@@ -1588,7 +1593,7 @@ func (a *Action) UnmarshalJSON(b []byte) error {
case "accept":
*a = ActionAccept
default:
return fmt.Errorf("%w: %q, must be %q", ErrInvalidACLAction, str, ActionAccept)
return fmt.Errorf("action=%q is not supported: %w", str, ErrInvalidACLAction)
}
return nil
@@ -2178,7 +2183,7 @@ func (p *Policy) validate() error {
err := p.TagOwners.Contains(tagOwner)
if err != nil {
errs = append(errs, err)
errs = append(errs, fmt.Errorf("src=%w", err))
}
}
}
@@ -2209,7 +2214,7 @@ func (p *Policy) validate() error {
case *Tag:
err := p.TagOwners.Contains(h)
if err != nil {
errs = append(errs, err)
errs = append(errs, fmt.Errorf("dst=%q: %w", *h, err))
}
}
}

View File

@@ -590,7 +590,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `tag not defined in policy: "tag:test"`,
wantErr: `tag not found: "tag:test"`,
},
{
name: "autogroup:internet-in-ssh-dst-not-allowed",
@@ -854,7 +854,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `tag not defined in policy: "tag:notdefined"`,
wantErr: `tag not found: "tag:notdefined"`,
},
{
name: "tag-must-be-defined-acl-dst",
@@ -873,7 +873,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `tag not defined in policy: "tag:notdefined"`,
wantErr: `tag not found: "tag:notdefined"`,
},
{
name: "tag-must-be-defined-acl-ssh-src",
@@ -892,7 +892,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `tag not defined in policy: "tag:notdefined"`,
wantErr: `tag not found: "tag:notdefined"`,
},
{
name: "tag-must-be-defined-acl-ssh-dst",
@@ -914,7 +914,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `tag not defined in policy: "tag:notdefined"`,
wantErr: `tag not found: "tag:notdefined"`,
},
{
name: "tag-must-be-defined-acl-autoapprover-route",
@@ -927,7 +927,7 @@ func TestUnmarshalPolicy(t *testing.T) {
},
}
`,
wantErr: `tag not defined in policy: "tag:notdefined"`,
wantErr: `tag not found: "tag:notdefined"`,
},
{
name: "tag-must-be-defined-acl-autoapprover-exitnode",
@@ -938,7 +938,7 @@ func TestUnmarshalPolicy(t *testing.T) {
},
}
`,
wantErr: `tag not defined in policy: "tag:notdefined"`,
wantErr: `tag not found: "tag:notdefined"`,
},
{
name: "missing-dst-port-is-err",
@@ -3928,7 +3928,7 @@ func TestACL_UnmarshalJSON_InvalidAction(t *testing.T) {
_, err := unmarshalPolicy([]byte(policyJSON))
require.Error(t, err)
assert.Contains(t, err.Error(), `invalid ACL action: "deny"`)
assert.Contains(t, err.Error(), `action="deny" is not supported`)
}
// Helper function to parse aliases for testing.
@@ -4691,7 +4691,7 @@ func TestUnmarshalGrants(t *testing.T) {
]
}
`,
wantErr: "tag not defined in policy",
wantErr: "tag not found",
},
{
name: "invalid-grant-undefined-destination-host",
@@ -4724,7 +4724,7 @@ func TestUnmarshalGrants(t *testing.T) {
]
}
`,
wantErr: "autogroup:self destination requires sources to be users, groups, or autogroup:member only",
wantErr: "autogroup:self can only be used with users, groups, or supported autogroups",
},
}

View File

@@ -19,7 +19,7 @@ var (
ErrInvalidPortRangeFormat = errors.New("invalid port range format")
ErrPortRangeInverted = errors.New("invalid port range: first port is greater than last port")
ErrPortMustBePositive = errors.New("first port must be >0, or use '*' for wildcard")
ErrInvalidPortNumber = errors.New("invalid port number")
ErrInvalidPortNumber = errors.New("invalid first integer")
ErrPortNumberOutOfRange = errors.New("port number out of range")
ErrBracketsNotIPv6 = errors.New("square brackets are only valid around IPv6 addresses")
)

View File

@@ -162,8 +162,8 @@ func TestParsePort(t *testing.T) {
{"65535", 65535, ""},
{"-1", 0, "port number out of range"},
{"65536", 0, "port number out of range"},
{"abc", 0, "invalid port number"},
{"", 0, "invalid port number"},
{"abc", 0, "invalid first integer"},
{"", 0, "invalid first integer"},
}
for _, test := range tests {
@@ -195,9 +195,9 @@ func TestParsePortRange(t *testing.T) {
{"*", []tailcfg.PortRange{tailcfg.PortRangeAny}, ""},
{"80-", nil, "invalid port range format"},
{"-90", nil, "invalid port range format"},
{"80-90,", nil, "invalid port number"},
{"80-90,", nil, "invalid first integer"},
{"80,90-", nil, "invalid port range format"},
{"80-90,abc", nil, "invalid port number"},
{"80-90,abc", nil, "invalid first integer"},
{"80-90,65536", nil, "port number out of range"},
{"80-90,90-80", nil, "invalid port range: first port is greater than last port"},
}