mirror of
https://github.com/juanfont/headscale.git
synced 2026-04-08 05:57:46 +09:00
policy: fix wildcard DstPorts format and proto:icmp handling
Fix two compatibility issues discovered in Tailscale SaaS testing:
1. Wildcard DstPorts format: Headscale was expanding wildcard
destinations to CGNAT ranges (100.64.0.0/10, fd7a:115c:a1e0::/48)
while Tailscale uses {IP: "*"} directly. Add detection for
wildcard (Asterix) alias type in filter compilation to use the
correct format.
2. proto:icmp handling: The "icmp" protocol name was returning both
ICMPv4 (1) and ICMPv6 (58), but Tailscale only returns ICMPv4.
Users should use "ipv6-icmp" or protocol number 58 explicitly
for IPv6 ICMP.
Update all test expectations accordingly. This significantly reduces
test file line count by replacing duplicated CGNAT range patterns
with single wildcard entries.
This commit is contained in:
@@ -49,6 +49,18 @@ func (pol *Policy) compileFilterRules(
|
||||
|
||||
var destPorts []tailcfg.NetPortRange
|
||||
for _, dest := range acl.Destinations {
|
||||
// Check if destination is a wildcard - use "*" directly instead of expanding
|
||||
if _, isWildcard := dest.Alias.(Asterix); isWildcard {
|
||||
for _, port := range dest.Ports {
|
||||
destPorts = append(destPorts, tailcfg.NetPortRange{
|
||||
IP: "*",
|
||||
Ports: port,
|
||||
})
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
ips, err := dest.Resolve(pol, users, nodes)
|
||||
if err != nil {
|
||||
log.Trace().Caller().Err(err).Msgf("resolving destination ips")
|
||||
@@ -235,6 +247,18 @@ func (pol *Policy) compileACLWithAutogroupSelf(
|
||||
var destPorts []tailcfg.NetPortRange
|
||||
|
||||
for _, dest := range otherDests {
|
||||
// Check if destination is a wildcard - use "*" directly instead of expanding
|
||||
if _, isWildcard := dest.Alias.(Asterix); isWildcard {
|
||||
for _, port := range dest.Ports {
|
||||
destPorts = append(destPorts, tailcfg.NetPortRange{
|
||||
IP: "*",
|
||||
Ports: port,
|
||||
})
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
ips, err := dest.Resolve(pol, users, nodes)
|
||||
if err != nil {
|
||||
log.Trace().Caller().Err(err).Msgf("resolving destination ips")
|
||||
|
||||
@@ -97,10 +97,8 @@ func TestParsing(t *testing.T) {
|
||||
{
|
||||
SrcIPs: []string{"100.100.101.0/24", "192.168.1.0/24"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "100.64.0.0/10", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
||||
{IP: "100.64.0.0/10", Ports: tailcfg.PortRange{First: 3389, Last: 3389}},
|
||||
{IP: "fd7a:115c:a1e0::/48", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
||||
{IP: "fd7a:115c:a1e0::/48", Ports: tailcfg.PortRange{First: 3389, Last: 3389}},
|
||||
{IP: "*", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
||||
{IP: "*", Ports: tailcfg.PortRange{First: 3389, Last: 3389}},
|
||||
{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
|
||||
},
|
||||
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
||||
@@ -171,7 +169,8 @@ func TestParsing(t *testing.T) {
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
|
||||
},
|
||||
IPProto: []int{ProtocolICMP, ProtocolIPv6ICMP},
|
||||
// proto:icmp only includes ICMP (1), not ICMPv6 (58)
|
||||
IPProto: []int{ProtocolICMP},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1181,48 +1181,15 @@ func TestTailscaleRoutesCompatExitNodes(t *testing.T) {
|
||||
// with expanded autogroup:internet CIDRs in Headscale (Tailscale: nil)
|
||||
},
|
||||
},
|
||||
// TODO: Verify Tailscale DstPorts format for wildcard destinations
|
||||
//
|
||||
// B3: Exit node advertises exit routes (verify RoutableIPs)
|
||||
//
|
||||
// This test verifies that exit-node has 0.0.0.0/0 and ::/0 in RoutableIPs.
|
||||
// The filter test is a proxy for this - all nodes get wildcard filters.
|
||||
//
|
||||
// TAILSCALE BEHAVIOR:
|
||||
// - Uses "*" in DstPorts.IP for wildcard destinations
|
||||
//
|
||||
// HEADSCALE BEHAVIOR:
|
||||
// - Uses explicit CIDR ranges (100.64.0.0/10, fd7a:115c:a1e0::/48)
|
||||
//
|
||||
// ROOT CAUSE:
|
||||
// Different representation of wildcard destinations in filter rules
|
||||
//
|
||||
// FIX REQUIRED:
|
||||
// Verify if Tailscale actually uses "*" or if our expected values are wrong
|
||||
// All nodes get wildcard filters with {IP: "*"} format matching Tailscale.
|
||||
{
|
||||
name: "B3_exit_node_advertises_routes",
|
||||
policy: makeRoutesPolicy(`
|
||||
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
|
||||
`),
|
||||
/* EXPECTED (Tailscale) - if it uses "*" format:
|
||||
wantFilters: map[string][]tailcfg.FilterRule{
|
||||
"client1": {
|
||||
{
|
||||
SrcIPs: []string{
|
||||
"100.64.0.0/10",
|
||||
"fd7a:115c:a1e0::/48",
|
||||
},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "*", Ports: tailcfg.PortRangeAny},
|
||||
},
|
||||
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
||||
},
|
||||
},
|
||||
// Other nodes would also get filters - need Tailscale verification
|
||||
},
|
||||
*/
|
||||
// ACTUAL (Headscale):
|
||||
// All nodes receive the same wildcard filter with explicit CIDRs
|
||||
wantFilters: map[string][]tailcfg.FilterRule{
|
||||
"client1": wildcardFilter,
|
||||
"client2": wildcardFilter,
|
||||
@@ -1235,46 +1202,15 @@ func TestTailscaleRoutesCompatExitNodes(t *testing.T) {
|
||||
"user1": wildcardFilter,
|
||||
},
|
||||
},
|
||||
// TODO: Verify Tailscale DstPorts format for wildcard destinations
|
||||
//
|
||||
// B5: Exit node with wildcard destination has ExitNodeOption
|
||||
//
|
||||
// Exit nodes should have ExitNodeOption=true in MapResponse.
|
||||
// The filter test is a proxy - all nodes should get wildcard filters.
|
||||
//
|
||||
// TAILSCALE BEHAVIOR:
|
||||
// - Exit nodes with approved exit routes have ExitNodeOption=true
|
||||
// - DstPorts may use "*" format
|
||||
//
|
||||
// HEADSCALE BEHAVIOR:
|
||||
// - All nodes get filters with explicit CIDR ranges
|
||||
//
|
||||
// ROOT CAUSE:
|
||||
// Different DstPorts format; need to verify Tailscale's actual format
|
||||
// All nodes get wildcard filters with {IP: "*"} format matching Tailscale.
|
||||
{
|
||||
name: "B5_exit_with_wildcard_dst",
|
||||
policy: makeRoutesPolicy(`
|
||||
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
|
||||
`),
|
||||
/* EXPECTED (Tailscale) - if it uses "*" format:
|
||||
wantFilters: map[string][]tailcfg.FilterRule{
|
||||
"client1": {
|
||||
{
|
||||
SrcIPs: []string{
|
||||
"100.64.0.0/10",
|
||||
"fd7a:115c:a1e0::/48",
|
||||
},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "*", Ports: tailcfg.PortRangeAny},
|
||||
},
|
||||
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
||||
},
|
||||
},
|
||||
// Other nodes - need Tailscale verification
|
||||
},
|
||||
*/
|
||||
// ACTUAL (Headscale):
|
||||
// All nodes receive the same wildcard filter with explicit CIDRs
|
||||
wantFilters: map[string][]tailcfg.FilterRule{
|
||||
"client1": wildcardFilter,
|
||||
"client2": wildcardFilter,
|
||||
@@ -1602,42 +1538,15 @@ func TestTailscaleRoutesCompatExitNodes(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
// TODO: Verify Tailscale DstPorts format for wildcard destinations
|
||||
//
|
||||
// B9: Exit routes appear in peer AllowedIPs
|
||||
//
|
||||
// When viewing exit-node as a peer, AllowedIPs should include exit routes.
|
||||
// This is a MapResponse property test, filter test is a proxy.
|
||||
//
|
||||
// TAILSCALE BEHAVIOR:
|
||||
// - Need to verify actual format (may use "*" in DstPorts.IP)
|
||||
//
|
||||
// HEADSCALE BEHAVIOR:
|
||||
// - All nodes get wildcard filter with explicit CIDR ranges
|
||||
// All nodes get wildcard filters with {IP: "*"} format matching Tailscale.
|
||||
{
|
||||
name: "B9_exit_routes_in_allowedips",
|
||||
policy: makeRoutesPolicy(`
|
||||
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
|
||||
`),
|
||||
/* EXPECTED (Tailscale) - if it uses "*" format:
|
||||
wantFilters: map[string][]tailcfg.FilterRule{
|
||||
"client1": {
|
||||
{
|
||||
SrcIPs: []string{
|
||||
"100.64.0.0/10",
|
||||
"fd7a:115c:a1e0::/48",
|
||||
},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "*", Ports: tailcfg.PortRangeAny},
|
||||
},
|
||||
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
||||
},
|
||||
},
|
||||
// Other nodes - need Tailscale verification
|
||||
},
|
||||
*/
|
||||
// ACTUAL (Headscale):
|
||||
// All nodes receive the same wildcard filter with explicit CIDRs
|
||||
wantFilters: map[string][]tailcfg.FilterRule{
|
||||
"client1": wildcardFilter,
|
||||
"client2": wildcardFilter,
|
||||
@@ -1650,42 +1559,16 @@ func TestTailscaleRoutesCompatExitNodes(t *testing.T) {
|
||||
"user1": wildcardFilter,
|
||||
},
|
||||
},
|
||||
// TODO: Verify Tailscale DstPorts format for wildcard destinations
|
||||
//
|
||||
// B10: Exit routes NOT in PrimaryRoutes field
|
||||
//
|
||||
// PrimaryRoutes is for subnet routes only, not exit routes.
|
||||
// Exit routes (0.0.0.0/0, ::/0) should NOT appear in PrimaryRoutes.
|
||||
//
|
||||
// TAILSCALE BEHAVIOR:
|
||||
// - Need to verify actual format
|
||||
//
|
||||
// HEADSCALE BEHAVIOR:
|
||||
// - All nodes get wildcard filter with explicit CIDR ranges
|
||||
// All nodes get wildcard filters with {IP: "*"} format matching Tailscale.
|
||||
{
|
||||
name: "B10_exit_routes_not_in_primaryroutes",
|
||||
policy: makeRoutesPolicy(`
|
||||
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
|
||||
`),
|
||||
/* EXPECTED (Tailscale) - if it uses "*" format:
|
||||
wantFilters: map[string][]tailcfg.FilterRule{
|
||||
"client1": {
|
||||
{
|
||||
SrcIPs: []string{
|
||||
"100.64.0.0/10",
|
||||
"fd7a:115c:a1e0::/48",
|
||||
},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "*", Ports: tailcfg.PortRangeAny},
|
||||
},
|
||||
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
|
||||
},
|
||||
},
|
||||
// Other nodes - need Tailscale verification
|
||||
},
|
||||
*/
|
||||
// ACTUAL (Headscale):
|
||||
// All nodes receive the same wildcard filter with explicit CIDRs
|
||||
wantFilters: map[string][]tailcfg.FilterRule{
|
||||
"client1": wildcardFilter,
|
||||
"client2": wildcardFilter,
|
||||
|
||||
@@ -1375,8 +1375,9 @@ func (p Protocol) parseProtocol() ([]int, bool) {
|
||||
return []int{ProtocolAH}, true
|
||||
case ProtocolNameSCTP:
|
||||
return []int{ProtocolSCTP}, false
|
||||
case ProtoNameICMP:
|
||||
return []int{ProtocolICMP, ProtocolIPv6ICMP}, true
|
||||
case ProtocolNameICMP:
|
||||
// ICMP only - use "ipv6-icmp" or protocol number 58 for ICMPv6
|
||||
return []int{ProtocolICMP}, true
|
||||
default:
|
||||
// Try to parse as a numeric protocol number
|
||||
// This should not fail since validation happened during unmarshaling
|
||||
|
||||
Reference in New Issue
Block a user