1
0
mirror of https://github.com/juanfont/headscale.git synced 2026-02-07 20:04:00 +01:00

policy: add ICMP protocols to default and export constants

When ACL rules don't specify a protocol, Headscale now defaults to
[TCP, UDP, ICMP, ICMPv6] instead of just [TCP, UDP], matching
Tailscale's behavior.
Also export protocol number constants (ProtocolTCP, ProtocolUDP, etc.)
for use in external test packages, renaming the string protocol
constants to ProtoNameTCP, ProtoNameUDP, etc. to avoid conflicts.
This resolves 78 ICMP-related TODOs in the Tailscale compatibility
tests, reducing the total from 165 to 87.

Updates #3036
This commit is contained in:
Kristoffer Dalby 2026-01-23 20:16:02 +00:00
parent 53d17aa321
commit f735502eae
5 changed files with 529 additions and 604 deletions

View File

@ -4,6 +4,7 @@
### Changes
- **ACL Policy**: Add ICMP and IPv6-ICMP protocols to default filter rules and export protocol constants [#3036](https://github.com/juanfont/headscale/pull/3036)
- **ACL Policy**: Fix autogroup:self handling for tagged nodes - tagged nodes no longer incorrectly receive autogroup:self filter rules [#3036](https://github.com/juanfont/headscale/pull/3036)
## 0.28.0 (2026-02-04)

View File

@ -9,6 +9,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/policy/policyutil"
v2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log"
@ -223,7 +224,7 @@ func TestReduceFilterRules(t *testing.T) {
Ports: tailcfg.PortRangeAny,
},
},
IPProto: []int{6, 17},
IPProto: []int{v2.ProtocolTCP, v2.ProtocolUDP, v2.ProtocolICMP, v2.ProtocolIPv6ICMP},
},
{
SrcIPs: []string{
@ -238,7 +239,7 @@ func TestReduceFilterRules(t *testing.T) {
Ports: tailcfg.PortRangeAny,
},
},
IPProto: []int{6, 17},
IPProto: []int{v2.ProtocolTCP, v2.ProtocolUDP, v2.ProtocolICMP, v2.ProtocolIPv6ICMP},
},
},
},
@ -374,12 +375,12 @@ func TestReduceFilterRules(t *testing.T) {
Ports: tailcfg.PortRangeAny,
},
},
IPProto: []int{6, 17},
IPProto: []int{v2.ProtocolTCP, v2.ProtocolUDP, v2.ProtocolICMP, v2.ProtocolIPv6ICMP},
},
{
SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"},
DstPorts: hsExitNodeDestForTest,
IPProto: []int{6, 17},
IPProto: []int{v2.ProtocolTCP, v2.ProtocolUDP, v2.ProtocolICMP, v2.ProtocolIPv6ICMP},
},
},
},
@ -483,7 +484,7 @@ func TestReduceFilterRules(t *testing.T) {
Ports: tailcfg.PortRangeAny,
},
},
IPProto: []int{6, 17},
IPProto: []int{v2.ProtocolTCP, v2.ProtocolUDP, v2.ProtocolICMP, v2.ProtocolIPv6ICMP},
},
{
SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"},
@ -519,7 +520,7 @@ func TestReduceFilterRules(t *testing.T) {
{IP: "200.0.0.0/5", Ports: tailcfg.PortRangeAny},
{IP: "208.0.0.0/4", Ports: tailcfg.PortRangeAny},
},
IPProto: []int{6, 17},
IPProto: []int{v2.ProtocolTCP, v2.ProtocolUDP, v2.ProtocolICMP, v2.ProtocolIPv6ICMP},
},
},
},
@ -595,7 +596,7 @@ func TestReduceFilterRules(t *testing.T) {
Ports: tailcfg.PortRangeAny,
},
},
IPProto: []int{6, 17},
IPProto: []int{v2.ProtocolTCP, v2.ProtocolUDP, v2.ProtocolICMP, v2.ProtocolIPv6ICMP},
},
{
SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"},
@ -609,7 +610,7 @@ func TestReduceFilterRules(t *testing.T) {
Ports: tailcfg.PortRangeAny,
},
},
IPProto: []int{6, 17},
IPProto: []int{v2.ProtocolTCP, v2.ProtocolUDP, v2.ProtocolICMP, v2.ProtocolIPv6ICMP},
},
},
},
@ -685,7 +686,7 @@ func TestReduceFilterRules(t *testing.T) {
Ports: tailcfg.PortRangeAny,
},
},
IPProto: []int{6, 17},
IPProto: []int{v2.ProtocolTCP, v2.ProtocolUDP, v2.ProtocolICMP, v2.ProtocolIPv6ICMP},
},
{
SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"},
@ -699,7 +700,7 @@ func TestReduceFilterRules(t *testing.T) {
Ports: tailcfg.PortRangeAny,
},
},
IPProto: []int{6, 17},
IPProto: []int{v2.ProtocolTCP, v2.ProtocolUDP, v2.ProtocolICMP, v2.ProtocolIPv6ICMP},
},
},
},
@ -767,7 +768,7 @@ func TestReduceFilterRules(t *testing.T) {
Ports: tailcfg.PortRangeAny,
},
},
IPProto: []int{6, 17},
IPProto: []int{v2.ProtocolTCP, v2.ProtocolUDP, v2.ProtocolICMP, v2.ProtocolIPv6ICMP},
},
},
},

View File

@ -103,7 +103,7 @@ func TestParsing(t *testing.T) {
{IP: "::/0", Ports: tailcfg.PortRange{First: 3389, Last: 3389}},
{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
},
IPProto: []int{protocolTCP, protocolUDP},
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
},
},
wantErr: false,
@ -157,21 +157,21 @@ func TestParsing(t *testing.T) {
DstPorts: []tailcfg.NetPortRange{
{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
},
IPProto: []int{protocolTCP},
IPProto: []int{ProtocolTCP},
},
{
SrcIPs: []string{"0.0.0.0/0", "::/0"},
DstPorts: []tailcfg.NetPortRange{
{IP: "100.100.100.100/32", Ports: tailcfg.PortRange{First: 53, Last: 53}},
},
IPProto: []int{protocolUDP},
IPProto: []int{ProtocolUDP},
},
{
SrcIPs: []string{"0.0.0.0/0", "::/0"},
DstPorts: []tailcfg.NetPortRange{
{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
},
IPProto: []int{protocolICMP, protocolIPv6ICMP},
IPProto: []int{ProtocolICMP, ProtocolIPv6ICMP},
},
},
wantErr: false,
@ -205,7 +205,7 @@ func TestParsing(t *testing.T) {
DstPorts: []tailcfg.NetPortRange{
{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
},
IPProto: []int{protocolTCP, protocolUDP},
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
},
},
wantErr: false,
@ -242,7 +242,7 @@ func TestParsing(t *testing.T) {
Ports: tailcfg.PortRange{First: 5400, Last: 5500},
},
},
IPProto: []int{protocolTCP, protocolUDP},
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
},
},
wantErr: false,
@ -282,7 +282,7 @@ func TestParsing(t *testing.T) {
DstPorts: []tailcfg.NetPortRange{
{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
},
IPProto: []int{protocolTCP, protocolUDP},
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
},
},
wantErr: false,
@ -316,7 +316,7 @@ func TestParsing(t *testing.T) {
DstPorts: []tailcfg.NetPortRange{
{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
},
IPProto: []int{protocolTCP, protocolUDP},
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
},
},
wantErr: false,
@ -350,7 +350,7 @@ func TestParsing(t *testing.T) {
DstPorts: []tailcfg.NetPortRange{
{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
},
IPProto: []int{protocolTCP, protocolUDP},
IPProto: []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP},
},
},
wantErr: false,

File diff suppressed because it is too large Load Diff

View File

@ -1281,21 +1281,21 @@ func (a SSHAction) MarshalJSON() ([]byte, error) {
type Protocol string
const (
ProtocolICMP Protocol = "icmp"
ProtocolIGMP Protocol = "igmp"
ProtocolIPv4 Protocol = "ipv4"
ProtocolIPInIP Protocol = "ip-in-ip"
ProtocolTCP Protocol = "tcp"
ProtocolEGP Protocol = "egp"
ProtocolIGP Protocol = "igp"
ProtocolUDP Protocol = "udp"
ProtocolGRE Protocol = "gre"
ProtocolESP Protocol = "esp"
ProtocolAH Protocol = "ah"
ProtocolIPv6ICMP Protocol = "ipv6-icmp"
ProtocolSCTP Protocol = "sctp"
ProtocolFC Protocol = "fc"
ProtocolWildcard Protocol = "*"
ProtocolNameICMP Protocol = "icmp"
ProtocolNameIGMP Protocol = "igmp"
ProtocolNameIPv4 Protocol = "ipv4"
ProtocolNameIPInIP Protocol = "ip-in-ip"
ProtocolNameTCP Protocol = "tcp"
ProtocolNameEGP Protocol = "egp"
ProtocolNameIGP Protocol = "igp"
ProtocolNameUDP Protocol = "udp"
ProtocolNameGRE Protocol = "gre"
ProtocolNameESP Protocol = "esp"
ProtocolNameAH Protocol = "ah"
ProtocolNameIPv6ICMP Protocol = "ipv6-icmp"
ProtocolNameSCTP Protocol = "sctp"
ProtocolNameFC Protocol = "fc"
ProtocolNameWildcard Protocol = "*"
)
// String returns the string representation of the Protocol.
@ -1306,33 +1306,33 @@ func (p Protocol) String() string {
// Description returns the human-readable description of the Protocol.
func (p Protocol) Description() string {
switch p {
case ProtocolICMP:
case ProtocolNameICMP:
return "Internet Control Message Protocol"
case ProtocolIGMP:
case ProtocolNameIGMP:
return "Internet Group Management Protocol"
case ProtocolIPv4:
case ProtocolNameIPv4:
return "IPv4 encapsulation"
case ProtocolTCP:
case ProtocolNameTCP:
return "Transmission Control Protocol"
case ProtocolEGP:
case ProtocolNameEGP:
return "Exterior Gateway Protocol"
case ProtocolIGP:
case ProtocolNameIGP:
return "Interior Gateway Protocol"
case ProtocolUDP:
case ProtocolNameUDP:
return "User Datagram Protocol"
case ProtocolGRE:
case ProtocolNameGRE:
return "Generic Routing Encapsulation"
case ProtocolESP:
case ProtocolNameESP:
return "Encapsulating Security Payload"
case ProtocolAH:
case ProtocolNameAH:
return "Authentication Header"
case ProtocolIPv6ICMP:
case ProtocolNameIPv6ICMP:
return "Internet Control Message Protocol for IPv6"
case ProtocolSCTP:
case ProtocolNameSCTP:
return "Stream Control Transmission Protocol"
case ProtocolFC:
case ProtocolNameFC:
return "Fibre Channel"
case ProtocolWildcard:
case ProtocolNameWildcard:
return "Wildcard (not supported - use specific protocol)"
default:
return "Unknown Protocol"
@ -1344,42 +1344,43 @@ func (p Protocol) Description() string {
func (p Protocol) parseProtocol() ([]int, bool) {
switch p {
case "":
// Empty protocol applies to TCP and UDP traffic only
return []int{protocolTCP, protocolUDP}, false
case ProtocolWildcard:
// Empty protocol applies to TCP, UDP, ICMP, and ICMPv6 traffic
// This matches Tailscale's behavior for protocol defaults
return []int{ProtocolTCP, ProtocolUDP, ProtocolICMP, ProtocolIPv6ICMP}, false
case ProtocolNameWildcard:
// Wildcard protocol - defensive handling (should not reach here due to validation)
return nil, false
case ProtocolIGMP:
return []int{protocolIGMP}, true
case ProtocolIPv4, ProtocolIPInIP:
return []int{protocolIPv4}, true
case ProtocolTCP:
return []int{protocolTCP}, false
case ProtocolEGP:
return []int{protocolEGP}, true
case ProtocolIGP:
return []int{protocolIGP}, true
case ProtocolUDP:
return []int{protocolUDP}, false
case ProtocolGRE:
return []int{protocolGRE}, true
case ProtocolESP:
return []int{protocolESP}, true
case ProtocolAH:
return []int{protocolAH}, true
case ProtocolSCTP:
return []int{protocolSCTP}, false
case ProtocolICMP:
return []int{protocolICMP, protocolIPv6ICMP}, true
case ProtocolNameIGMP:
return []int{ProtocolIGMP}, true
case ProtocolNameIPv4, ProtocolNameIPInIP:
return []int{ProtocolIPv4}, true
case ProtocolNameTCP:
return []int{ProtocolTCP}, false
case ProtocolNameEGP:
return []int{ProtocolEGP}, true
case ProtocolNameIGP:
return []int{ProtocolIGP}, true
case ProtocolNameUDP:
return []int{ProtocolUDP}, false
case ProtocolNameGRE:
return []int{ProtocolGRE}, true
case ProtocolNameESP:
return []int{ProtocolESP}, true
case ProtocolNameAH:
return []int{ProtocolAH}, true
case ProtocolNameSCTP:
return []int{ProtocolSCTP}, false
case ProtoNameICMP:
return []int{ProtocolICMP, ProtocolIPv6ICMP}, true
default:
// Try to parse as a numeric protocol number
// This should not fail since validation happened during unmarshaling
protocolNumber, _ := strconv.Atoi(string(p))
// Determine if wildcard is needed based on protocol number
needsWildcard := protocolNumber != protocolTCP &&
protocolNumber != protocolUDP &&
protocolNumber != protocolSCTP
needsWildcard := protocolNumber != ProtocolTCP &&
protocolNumber != ProtocolUDP &&
protocolNumber != ProtocolSCTP
return []int{protocolNumber}, needsWildcard
}
@ -1403,11 +1404,11 @@ func (p *Protocol) UnmarshalJSON(b []byte) error {
// validate checks if the Protocol is valid.
func (p Protocol) validate() error {
switch p {
case "", ProtocolICMP, ProtocolIGMP, ProtocolIPv4, ProtocolIPInIP,
ProtocolTCP, ProtocolEGP, ProtocolIGP, ProtocolUDP, ProtocolGRE,
ProtocolESP, ProtocolAH, ProtocolSCTP:
case "", ProtocolNameICMP, ProtocolNameIGMP, ProtocolNameIPv4, ProtocolNameIPInIP,
ProtocolNameTCP, ProtocolNameEGP, ProtocolNameIGP, ProtocolNameUDP, ProtocolNameGRE,
ProtocolNameESP, ProtocolNameAH, ProtocolNameSCTP:
return nil
case ProtocolWildcard:
case ProtocolNameWildcard:
// Wildcard "*" is not allowed - Tailscale rejects it
return fmt.Errorf("proto name \"*\" not known; use protocol number 0-255 or protocol name (icmp, tcp, udp, etc.)")
default:
@ -1439,19 +1440,19 @@ func (p Protocol) MarshalJSON() ([]byte, error) {
// Protocol constants matching the IANA numbers
const (
protocolICMP = 1 // Internet Control Message
protocolIGMP = 2 // Internet Group Management
protocolIPv4 = 4 // IPv4 encapsulation
protocolTCP = 6 // Transmission Control
protocolEGP = 8 // Exterior Gateway Protocol
protocolIGP = 9 // any private interior gateway (used by Cisco for their IGRP)
protocolUDP = 17 // User Datagram
protocolGRE = 47 // Generic Routing Encapsulation
protocolESP = 50 // Encap Security Payload
protocolAH = 51 // Authentication Header
protocolIPv6ICMP = 58 // ICMP for IPv6
protocolSCTP = 132 // Stream Control Transmission Protocol
protocolFC = 133 // Fibre Channel
ProtocolICMP = 1 // Internet Control Message
ProtocolIGMP = 2 // Internet Group Management
ProtocolIPv4 = 4 // IPv4 encapsulation
ProtocolTCP = 6 // Transmission Control
ProtocolEGP = 8 // Exterior Gateway Protocol
ProtocolIGP = 9 // any private interior gateway (used by Cisco for their IGRP)
ProtocolUDP = 17 // User Datagram
ProtocolGRE = 47 // Generic Routing Encapsulation
ProtocolESP = 50 // Encap Security Payload
ProtocolAH = 51 // Authentication Header
ProtocolIPv6ICMP = 58 // ICMP for IPv6
ProtocolSCTP = 132 // Stream Control Transmission Protocol
ProtocolFC = 133 // Fibre Channel
)
type ACL struct {
@ -2114,7 +2115,7 @@ func unmarshalPolicy(b []byte) (*Policy, error) {
// can have specific ports. All other protocols should only use wildcard ports.
func validateProtocolPortCompatibility(protocol Protocol, destinations []AliasWithPorts) error {
// Only TCP, UDP, and SCTP support specific ports
supportsSpecificPorts := protocol == ProtocolTCP || protocol == ProtocolUDP || protocol == ProtocolSCTP || protocol == ""
supportsSpecificPorts := protocol == ProtocolNameTCP || protocol == ProtocolNameUDP || protocol == ProtocolNameSCTP || protocol == ""
if supportsSpecificPorts {
return nil // No validation needed for these protocols