From 93d79d8da90f05399e4e5e69a85d7e336617ed09 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 18 Mar 2026 10:24:30 +0000 Subject: [PATCH] policy: include IPv6 in identity-based alias resolution AppendToIPSet now adds both IPv4 and IPv6 addresses for nodes, matching Tailscale's FilterRule wire format where identity-based aliases (tags, users, groups, autogroups) resolve to both address families. Update ReduceFilterRules test expectations to include IPv6 entries. Updates #2180 --- hscontrol/policy/policyutil/reduce_test.go | 34 ++- .../policy/v2/tailscale_grants_compat_test.go | 205 ++---------------- hscontrol/policy/v2/types.go | 44 +--- hscontrol/types/node.go | 15 +- 4 files changed, 60 insertions(+), 238 deletions(-) diff --git a/hscontrol/policy/policyutil/reduce_test.go b/hscontrol/policy/policyutil/reduce_test.go index d21e3e9d..db0262fc 100644 --- a/hscontrol/policy/policyutil/reduce_test.go +++ b/hscontrol/policy/policyutil/reduce_test.go @@ -209,12 +209,17 @@ func TestReduceFilterRules(t *testing.T) { { SrcIPs: []string{ "100.64.0.1-100.64.0.2", + "fd7a:115c:a1e0::1-fd7a:115c:a1e0::2", }, DstPorts: []tailcfg.NetPortRange{ { IP: "100.64.0.1", Ports: tailcfg.PortRangeAny, }, + { + IP: "fd7a:115c:a1e0::1", + Ports: tailcfg.PortRangeAny, + }, { IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny, @@ -347,7 +352,10 @@ func TestReduceFilterRules(t *testing.T) { // autogroup:internet does NOT generate packet filters - it's handled // by exit node routing via AllowedIPs, not by packet filtering. { - SrcIPs: []string{"100.64.0.1-100.64.0.2"}, + SrcIPs: []string{ + "100.64.0.1-100.64.0.2", + "fd7a:115c:a1e0::1-fd7a:115c:a1e0::2", + }, DstPorts: []tailcfg.NetPortRange{ { IP: "100.64.0.100", @@ -447,7 +455,10 @@ func TestReduceFilterRules(t *testing.T) { want: []tailcfg.FilterRule{ // Merged: Both ACL rules combined (same SrcIPs) { - SrcIPs: []string{"100.64.0.1-100.64.0.2"}, + SrcIPs: []string{ + "100.64.0.1-100.64.0.2", + "fd7a:115c:a1e0::1-fd7a:115c:a1e0::2", + }, DstPorts: []tailcfg.NetPortRange{ { IP: "100.64.0.100", @@ -549,7 +560,10 @@ func TestReduceFilterRules(t *testing.T) { want: []tailcfg.FilterRule{ // Merged: Both ACL rules combined (same SrcIPs) { - SrcIPs: []string{"100.64.0.1-100.64.0.2"}, + SrcIPs: []string{ + "100.64.0.1-100.64.0.2", + "fd7a:115c:a1e0::1-fd7a:115c:a1e0::2", + }, DstPorts: []tailcfg.NetPortRange{ { IP: "100.64.0.100", @@ -629,7 +643,10 @@ func TestReduceFilterRules(t *testing.T) { want: []tailcfg.FilterRule{ // Merged: Both ACL rules combined (same SrcIPs) { - SrcIPs: []string{"100.64.0.1-100.64.0.2"}, + SrcIPs: []string{ + "100.64.0.1-100.64.0.2", + "fd7a:115c:a1e0::1-fd7a:115c:a1e0::2", + }, DstPorts: []tailcfg.NetPortRange{ { IP: "100.64.0.100", @@ -696,12 +713,19 @@ func TestReduceFilterRules(t *testing.T) { }, want: []tailcfg.FilterRule{ { - SrcIPs: []string{"100.64.0.1"}, + SrcIPs: []string{ + "100.64.0.1", + "fd7a:115c:a1e0::1", + }, DstPorts: []tailcfg.NetPortRange{ { IP: "100.64.0.100", Ports: tailcfg.PortRangeAny, }, + { + IP: "fd7a:115c:a1e0::100", + Ports: tailcfg.PortRangeAny, + }, { IP: "172.16.0.21", Ports: tailcfg.PortRangeAny, diff --git a/hscontrol/policy/v2/tailscale_grants_compat_test.go b/hscontrol/policy/v2/tailscale_grants_compat_test.go index f4aae1e2..e05f792c 100644 --- a/hscontrol/policy/v2/tailscale_grants_compat_test.go +++ b/hscontrol/policy/v2/tailscale_grants_compat_test.go @@ -227,149 +227,7 @@ func loadGrantTestFile(t *testing.T, path string) grantTestFile { // Total: 207 tests skipped, 30 tests expected to pass. var grantSkipReasons = map[string]string{ // ======================================================================== - // MISSING_IPV6_ADDRS (90 tests) - // - // TODO: Include IPv6 addresses when resolving identity-based aliases in - // filter rules. - // - // When compiling filter rules, headscale resolves identity-based aliases - // (tags, groups, users, autogroups) to only IPv4 addresses. Tailscale - // includes both IPv4 AND the corresponding fd7a:115c:a1e0:: IPv6 address - // in SrcIPs and DstPorts. - // - // IMPORTANT: This only applies to IDENTITY-based aliases. Address-based - // aliases (raw IPs like "100.108.74.26", host aliases like "webserver") - // correctly resolve to IPv4-only in both Tailscale and headscale. - // - // The rule (verified 100% across 790 node-IP references in test data): - // Identity aliases (tag:X, group:X, user@Y, autogroup:X, *) - // → include BOTH node.IPv4 and node.IPv6 - // Address aliases (raw IPv4/IPv6, host alias names) - // → include ONLY the literal/resolved IP - // - // Example diff (tag:client src → tagged-server node): - // SrcIPs: headscale=["100.83.200.69"] - // SrcIPs: tailscale=["100.83.200.69", "fd7a:115c:a1e0::c537:c845"] - // - // Fix: When resolving an identity alias (tag, group, user, autogroup, *) - // to IPs, include both node.IPv4 and node.IPv6 addresses. When resolving - // an address alias (raw IP, host alias), keep only the literal IP. - // ======================================================================== - - // J-series: Protocol-specific IP grants with identity src/dst - "GRANT-J1": "MISSING_IPV6_ADDRS", - "GRANT-J2": "MISSING_IPV6_ADDRS", - "GRANT-J3": "MISSING_IPV6_ADDRS", - "GRANT-J4": "MISSING_IPV6_ADDRS", - "GRANT-J5": "MISSING_IPV6_ADDRS", - "GRANT-J6": "MISSING_IPV6_ADDRS", - - // K-series: Various IP grant patterns with identity aliases - "GRANT-K4": "MISSING_IPV6_ADDRS", - "GRANT-K16": "MISSING_IPV6_ADDRS", - "GRANT-K17": "MISSING_IPV6_ADDRS", - "GRANT-K22": "MISSING_IPV6_ADDRS", - "GRANT-K26": "MISSING_IPV6_ADDRS", - - // P02-series: Source targeting (user, group, tag) - "GRANT-P02_1": "MISSING_IPV6_ADDRS", - "GRANT-P02_2": "MISSING_IPV6_ADDRS", - "GRANT-P02_3": "MISSING_IPV6_ADDRS", - "GRANT-P02_4": "MISSING_IPV6_ADDRS", - "GRANT-P02_5_CORRECT": "MISSING_IPV6_ADDRS", - "GRANT-P02_5_NAIVE": "MISSING_IPV6_ADDRS", - - // P03-series: Destination targeting - "GRANT-P03_1": "MISSING_IPV6_ADDRS", - "GRANT-P03_2": "MISSING_IPV6_ADDRS", - "GRANT-P03_3": "MISSING_IPV6_ADDRS", - "GRANT-P03_4": "MISSING_IPV6_ADDRS", - - // P04-series: autogroup:member grants - "GRANT-P04_1": "MISSING_IPV6_ADDRS", - "GRANT-P04_2": "MISSING_IPV6_ADDRS", - - // P06-series: IP protocol grants - "GRANT-P06_1": "MISSING_IPV6_ADDRS", - "GRANT-P06_2": "MISSING_IPV6_ADDRS", - "GRANT-P06_3": "MISSING_IPV6_ADDRS", - "GRANT-P06_4": "MISSING_IPV6_ADDRS", - "GRANT-P06_5": "MISSING_IPV6_ADDRS", - "GRANT-P06_6": "MISSING_IPV6_ADDRS", - "GRANT-P06_7": "MISSING_IPV6_ADDRS", - - // P08-series: Multiple grants / rule merging - "GRANT-P08_1": "MISSING_IPV6_ADDRS", - "GRANT-P08_2": "MISSING_IPV6_ADDRS", - "GRANT-P08_4": "MISSING_IPV6_ADDRS", - "GRANT-P08_5": "MISSING_IPV6_ADDRS", - "GRANT-P08_6": "MISSING_IPV6_ADDRS", - "GRANT-P08_7": "MISSING_IPV6_ADDRS", - - // P09-series: ACL-to-grant conversion equivalence tests - "GRANT-P09_1A": "MISSING_IPV6_ADDRS", - "GRANT-P09_1B": "MISSING_IPV6_ADDRS", - "GRANT-P09_1C": "MISSING_IPV6_ADDRS", - "GRANT-P09_1D": "MISSING_IPV6_ADDRS", - "GRANT-P09_1E": "MISSING_IPV6_ADDRS", - "GRANT-P09_2A_CORRECT": "MISSING_IPV6_ADDRS", - "GRANT-P09_2A_NAIVE": "MISSING_IPV6_ADDRS", - "GRANT-P09_2B_CORRECT": "MISSING_IPV6_ADDRS", - "GRANT-P09_2B_NAIVE": "MISSING_IPV6_ADDRS", - "GRANT-P09_2C": "MISSING_IPV6_ADDRS", - "GRANT-P09_3A": "MISSING_IPV6_ADDRS", - "GRANT-P09_3B": "MISSING_IPV6_ADDRS", - "GRANT-P09_3C": "MISSING_IPV6_ADDRS", - "GRANT-P09_4A": "MISSING_IPV6_ADDRS", - "GRANT-P09_4B": "MISSING_IPV6_ADDRS", - "GRANT-P09_4C": "MISSING_IPV6_ADDRS", - "GRANT-P09_4D": "MISSING_IPV6_ADDRS", - "GRANT-P09_4F": "MISSING_IPV6_ADDRS", - "GRANT-P09_4G": "MISSING_IPV6_ADDRS", - "GRANT-P09_5A": "MISSING_IPV6_ADDRS", - "GRANT-P09_5B": "MISSING_IPV6_ADDRS", - "GRANT-P09_5C_NAIVE": "MISSING_IPV6_ADDRS", - "GRANT-P09_6C": "MISSING_IPV6_ADDRS", - "GRANT-P09_7B_NAIVE": "MISSING_IPV6_ADDRS", - "GRANT-P09_7C": "MISSING_IPV6_ADDRS", - "GRANT-P09_7D_NAIVE": "MISSING_IPV6_ADDRS", - "GRANT-P09_8A": "MISSING_IPV6_ADDRS", - "GRANT-P09_8B": "MISSING_IPV6_ADDRS", - "GRANT-P09_8C": "MISSING_IPV6_ADDRS", - "GRANT-P09_9A": "MISSING_IPV6_ADDRS", - "GRANT-P09_9B": "MISSING_IPV6_ADDRS", - "GRANT-P09_9C": "MISSING_IPV6_ADDRS", - "GRANT-P09_10A": "MISSING_IPV6_ADDRS", - "GRANT-P09_10B": "MISSING_IPV6_ADDRS", - "GRANT-P09_10C": "MISSING_IPV6_ADDRS", - "GRANT-P09_10D": "MISSING_IPV6_ADDRS", - "GRANT-P09_11A": "MISSING_IPV6_ADDRS", - "GRANT-P09_11B": "MISSING_IPV6_ADDRS", - "GRANT-P09_11C_NAIVE": "MISSING_IPV6_ADDRS", - "GRANT-P09_11D": "MISSING_IPV6_ADDRS", - "GRANT-P09_12A": "MISSING_IPV6_ADDRS", - "GRANT-P09_12B": "MISSING_IPV6_ADDRS + SUBNET_ROUTE_FILTER_RULES: tagged-server subtest missing IPv6; subnet-router subtest missing entire rule for 10.0.0.0/8", - "GRANT-P09_14A": "MISSING_IPV6_ADDRS", - "GRANT-P09_14B": "MISSING_IPV6_ADDRS", - "GRANT-P09_14C": "MISSING_IPV6_ADDRS", - "GRANT-P09_14D": "MISSING_IPV6_ADDRS", - "GRANT-P09_14E": "MISSING_IPV6_ADDRS", - "GRANT-P09_14F": "MISSING_IPV6_ADDRS", - "GRANT-P09_14G": "MISSING_IPV6_ADDRS", - "GRANT-P09_14H": "MISSING_IPV6_ADDRS", - "GRANT-P09_14I": "MISSING_IPV6_ADDRS", - - // P10-series: Host alias grants (only identity-src subtests fail) - "GRANT-P10_2": "MISSING_IPV6_ADDRS", - - // P11-series: autogroup:tagged grants - "GRANT-P11_2": "MISSING_IPV6_ADDRS", - - // P13-series: CIDR destination grants (identity-src subtests) - "GRANT-P13_4": "MISSING_IPV6_ADDRS", - - // ======================================================================== - // SUBNET_ROUTE_FILTER_RULES (10 tests) + // SUBNET_ROUTE_FILTER_RULES (11 tests) // // TODO: Generate filter rules for non-Tailscale CIDR destinations on // subnet-router nodes. @@ -390,16 +248,16 @@ var grantSkipReasons = map[string]string{ // 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-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", - // Note: GRANT-P09_12B also has a subnet-router subtest failure — listed under MISSING_IPV6_ADDRS above + "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", // ======================================================================== // AUTOGROUP_SELF_CIDR_FORMAT (4 tests) @@ -408,18 +266,16 @@ var grantSkipReasons = map[string]string{ // // When compiling autogroup:self grants, headscale appends /32 to IPv4 // and /128 to IPv6 DstPort IPs. Tailscale uses bare IPs without a CIDR - // suffix. These tests also have missing IPv6 in SrcIPs. + // suffix. // // Example diff (user1 node, autogroup:member -> autogroup:self): // DstPorts: tailscale=[{IP:"100.90.199.68"}, {IP:"fd7a:...::2d01:c747"}] // DstPorts: headscale=[{IP:"100.90.199.68/32"}, {IP:"fd7a:...::2d01:c747/128"}] - // SrcIPs: tailscale=["100.90.199.68", "fd7a:...::2d01:c747"] - // SrcIPs: headscale=["100.90.199.68"] // ======================================================================== - "GRANT-P09_4E": "AUTOGROUP_SELF_CIDR_FORMAT: autogroup:member -> autogroup:self — DstPorts IPs have /32 and /128 suffix + missing IPv6 in SrcIPs", - "GRANT-P09_13E": "AUTOGROUP_SELF_CIDR_FORMAT: autogroup:member -> autogroup:self with ip:[*] — DstPorts IPs have CIDR suffix + missing IPv6 in SrcIPs", - "GRANT-P09_13F": "AUTOGROUP_SELF_CIDR_FORMAT: single user -> autogroup:self with ip:[22] — DstPorts IPs have CIDR suffix + missing IPv6 in SrcIPs", - "GRANT-P09_13G": "AUTOGROUP_SELF_CIDR_FORMAT: single user -> autogroup:self with ip:[22,80,443] — DstPorts IPs have CIDR suffix + missing IPv6 in SrcIPs", + "GRANT-P09_4E": "AUTOGROUP_SELF_CIDR_FORMAT: autogroup:member -> autogroup:self — DstPorts IPs have /32 and /128 suffix", + "GRANT-P09_13E": "AUTOGROUP_SELF_CIDR_FORMAT: autogroup:member -> autogroup:self with ip:[*] — DstPorts IPs have CIDR suffix", + "GRANT-P09_13F": "AUTOGROUP_SELF_CIDR_FORMAT: single user -> autogroup:self with ip:[22] — DstPorts IPs have CIDR suffix", + "GRANT-P09_13G": "AUTOGROUP_SELF_CIDR_FORMAT: single user -> autogroup:self with ip:[22,80,443] — DstPorts IPs have CIDR suffix", // ======================================================================== // USER_PASSKEY_WILDCARD (2 tests) @@ -438,25 +294,6 @@ var grantSkipReasons = map[string]string{ "GRANT-K20": "USER_PASSKEY_WILDCARD: src=user:*@passkey, dst=tag:server — source can't be resolved, no rules produced", "GRANT-K21": "USER_PASSKEY_WILDCARD: src=*, dst=user:*@passkey — destination can't be resolved, no rules produced", - // ======================================================================== - // RAW_IPV6_ADDR_EXPANSION (2 tests) - // - // TODO: Don't expand raw IPv6 addresses to include the matching node's IPv4. - // - // When a grant uses a raw fd7a: IPv6 address as src or dst, headscale - // resolves it to BOTH the IPv4 and IPv6 of the matching node. Tailscale - // keeps only the specific address that was referenced in the grant. - // - // Example (GRANT-K14, src=fd7a:115c:a1e0::c537:c845): - // SrcIPs: tailscale=["fd7a:115c:a1e0::c537:c845"] - // SrcIPs: headscale=["100.83.200.69", "fd7a:115c:a1e0::c537:c845"] - // Example (GRANT-K15, dst=fd7a:115c:a1e0::b901:4a87): - // DstPorts: tailscale=[{IP:"fd7a:...::b901:4a87"}] - // DstPorts: headscale=[{IP:"100.108.74.26"}, {IP:"fd7a:...::b901:4a87"}] - // ======================================================================== - "GRANT-K14": "RAW_IPV6_ADDR_EXPANSION: src=fd7a:...::c537:c845 — headscale adds extra IPv4 SrcIP + missing IPv6 in DstPorts", - "GRANT-K15": "RAW_IPV6_ADDR_EXPANSION: dst=fd7a:...::b901:4a87 — headscale adds extra IPv4 DstPort entry", - // ======================================================================== // SRCIPS_WILDCARD_NODE_DEDUP (1 test) // @@ -465,13 +302,11 @@ var grantSkipReasons = map[string]string{ // headscale only produces the wildcard ranges, omitting the individual // node IPs that are technically covered by those ranges. // - // Also has missing IPv6 in DstPorts. - // // Example (GRANT-P09_7A, src=[*, autogroup:member, tag:client, ...]): // SrcIPs: tailscale=[individual IPs + CGNAT ranges + IPv6s] (20 entries) // SrcIPs: headscale=[10.33.0.0/16, CGNAT ranges, fd7a::/48] (4 entries) // ======================================================================== - "GRANT-P09_7A": "SRCIPS_WILDCARD_NODE_DEDUP: src=[*,...] — individual node IPs missing from SrcIPs + missing IPv6 in DstPorts", + "GRANT-P09_7A": "SRCIPS_WILDCARD_NODE_DEDUP: src=[*,...] — individual node IPs missing from SrcIPs", // ======================================================================== // CAPGRANT_COMPILATION (49 tests) @@ -729,19 +564,17 @@ var grantSkipReasons = map[string]string{ // // CAPGRANT_COMPILATION - 49 tests: Implement app->CapGrant FilterRule compilation // 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 +// 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 // AUTOGROUP_SELF_CIDR_FORMAT - 4 tests: DstPorts IPs get /32 or /128 suffix for autogroup:self // VIA_COMPILATION - 3 tests: Via route compilation // AUTOGROUP_DANGER_ALL - 3 tests: Implement autogroup:danger-all support // USER_PASSKEY_WILDCARD - 2 tests: user:*@passkey wildcard pattern unresolvable // VALIDATION_STRICTNESS - 2 tests: headscale too strict (rejects what Tailscale accepts) -// 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: 113 tests skipped, ~124 tests expected to pass. func TestGrantsCompat(t *testing.T) { t.Parallel() diff --git a/hscontrol/policy/v2/types.go b/hscontrol/policy/v2/types.go index cc194e29..00870741 100644 --- a/hscontrol/policy/v2/types.go +++ b/hscontrol/policy/v2/types.go @@ -578,7 +578,7 @@ func (h *Host) Resolve(p *Policy, _ types.Users, nodes views.Slice[types.NodeVie return newResolvedAddresses(h.resolve(p, nil, nodes)) } -func (h *Host) resolve(p *Policy, _ types.Users, nodes views.Slice[types.NodeView]) (*netipx.IPSet, error) { +func (h *Host) resolve(p *Policy, _ types.Users, _ views.Slice[types.NodeView]) (*netipx.IPSet, error) { var ( ips netipx.IPSetBuilder errs []error @@ -594,25 +594,11 @@ func (h *Host) resolve(p *Policy, _ types.Users, nodes views.Slice[types.NodeVie errs = append(errs, err) } + // Address-based aliases (host names) resolve to exactly the + // literal prefix from the hosts map. They do NOT expand to + // include the matching node's other IP addresses. ips.AddPrefix(netip.Prefix(pref)) - // If the IP is a single host, look for a node to ensure we add all the IPs of - // the node to the IPSet. - appendIfNodeHasIP(nodes, &ips, netip.Prefix(pref)) - - // TODO(kradalby): I am a bit unsure what is the correct way to do this, - // should a host with a non single IP be able to resolve the full host (inc all IPs). - ipsTemp, err := ips.IPSet() - if err != nil { - errs = append(errs, err) - } - - for _, node := range nodes.All() { - if node.InIPSet(ipsTemp) { - node.AppendToIPSet(&ips) - } - } - return buildIPSetMultiErr(&ips, errs) } @@ -679,34 +665,20 @@ func (p *Prefix) Resolve(_ *Policy, _ types.Users, nodes views.Slice[types.NodeV return newResolvedAddresses(p.resolve(nil, nil, nodes)) } -func (p *Prefix) resolve(_ *Policy, _ types.Users, nodes views.Slice[types.NodeView]) (*netipx.IPSet, error) { +func (p *Prefix) resolve(_ *Policy, _ types.Users, _ views.Slice[types.NodeView]) (*netipx.IPSet, error) { var ( ips netipx.IPSetBuilder errs []error ) + // Address-based aliases resolve to exactly the literal prefix. + // Unlike identity-based aliases (tags, users, groups), they do + // NOT expand to include the matching node's other IP addresses. ips.AddPrefix(netip.Prefix(*p)) - // If the IP is a single host, look for a node to ensure we add all the IPs of - // the node to the IPSet. - appendIfNodeHasIP(nodes, &ips, netip.Prefix(*p)) return buildIPSetMultiErr(&ips, errs) } -// appendIfNodeHasIP appends the IPs of the nodes to the IPSet if the node has the -// IP address in the prefix. -func appendIfNodeHasIP(nodes views.Slice[types.NodeView], ips *netipx.IPSetBuilder, pref netip.Prefix) { - if !pref.IsSingleIP() && !tsaddr.IsTailscaleIP(pref.Addr()) { - return - } - - for _, node := range nodes.All() { - if node.HasIP(pref.Addr()) { - node.AppendToIPSet(ips) - } - } -} - // AutoGroup is a special string which is always prefixed with `autogroup:`. type AutoGroup string diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index d260656b..9e88fce7 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -297,25 +297,18 @@ func (node *Node) InIPSet(set *netipx.IPSet) bool { return slices.ContainsFunc(node.IPs(), set.Contains) } -// AppendToIPSet adds the individual ips in NodeAddresses to a -// given netipx.IPSetBuilder. +// AppendToIPSet adds all IP addresses of the node to the given +// netipx.IPSetBuilder. For identity-based aliases (tags, users, +// groups, autogroups), both IPv4 and IPv6 must be included to +// match Tailscale's behavior in the FilterRule wire format. func (node *Node) AppendToIPSet(build *netipx.IPSetBuilder) { if node.IPv4 != nil { build.Add(*node.IPv4) - return } if node.IPv6 != nil { build.Add(*node.IPv6) } - - // TODO(kradalby): Evaluate what we want to do here: - // Tailscale only adds the IPv4 addresses to any packet filter rule that is resolved to a given node. - // Presumably, it will add the IPv4 if a node does not have an IPv4. - // Until this change, we always added both, and that might be something people are dependent on, and we might want to keep it. - // for _, ip := range node.IPs() { - // build.Add(ip) - // } } func (node *Node) CanAccess(matchers []matcher.Match, node2 *Node) bool {