juanfont/headscale · error · errUnknownProtocolWildcard
proto name "*" not known; use protocol number 0-255 or proto
Error message
proto name "*" not known; use protocol number 0-255 or protocol name (icmp, tcp, udp, etc.)
What it means
errUnknownProtocolWildcard (lowercase, unexported; hscontrol/policy/v2/types.go:2005) is returned by Protocol.validate (types.go:1763) when a rule's proto is the string "*". Although "*" reads as 'any protocol', headscale (matching Tailscale SaaS) rejects it explicitly: to match all protocols you omit the proto field instead, and to match all ports you use "*" on the destination port side.
Source
Thrown at hscontrol/policy/v2/types.go:2005
SSHs []SSH `json:"ssh,omitempty"`
Tests []PolicyTest `json:"tests,omitempty"`
SSHTests []SSHPolicyTest `json:"sshTests,omitempty"`
RandomizeClientPort bool `json:"randomizeClientPort,omitempty"`
}
// MarshalJSON is deliberately not implemented for [Policy].
// We use the default JSON marshalling behavior provided by the Go runtime.
var (
// TODO(kradalby): Add these checks for tagOwners and autoApprovers.
autogroupForSrc = []AutoGroup{AutoGroupMember, AutoGroupTagged, AutoGroupDangerAll}
autogroupForDst = []AutoGroup{AutoGroupInternet, AutoGroupMember, AutoGroupTagged, AutoGroupSelf}
autogroupForSSHSrc = []AutoGroup{AutoGroupMember, AutoGroupTagged}
autogroupForSSHDst = []AutoGroup{AutoGroupMember, AutoGroupTagged, AutoGroupSelf}
autogroupForNodeAttrs = []AutoGroup{AutoGroupMember, AutoGroupTagged}
autogroupNotSupported = []AutoGroup{}
errUnknownProtocolWildcard = errors.New("proto name \"*\" not known; use protocol number 0-255 or protocol name (icmp, tcp, udp, etc.)")
)
// reservedTSRanges are CGNAT subranges that Tailscale uses internally and that
// nodeAttrs ipPool entries must not overlap.
//
// - 100.100.100.0/24 is MagicDNS / TSMP
// - 100.115.92.0/23 is the Quad100 / IPN service range
//
// (See https://tailscale.com/kb/1304/ip-pool for the operator-facing list.)
var reservedTSRanges = []netip.Prefix{
netip.MustParsePrefix("100.100.100.0/24"),
netip.MustParsePrefix("100.115.92.0/23"),
}
func validateAutogroupSupported(ag *AutoGroup) error {
if ag == nil {
return nil
}View on GitHub (pinned to 565fd254d0)
Solutions
- Delete the proto field entirely — an omitted proto means all protocols
- Keep "*" only on the port side (dst "host:*") where it is valid
- If you meant 'all protocols to all ports', use {"src": [...], "dst": ["x:*"]} with no proto
Example fix
// before
{"proto": "*", "src": ["group:admin"], "dst": ["tag:server:*"]}
// after
{"src": ["group:admin"], "dst": ["tag:server:*"]} Defensive patterns
Strategy: validation
Validate before calling
// Reject wildcard protocol before serialising a policy
func protoValid(p string) bool { return p != "*" } Try / catch
if err := policyv2.LoadPolicy(buf); err != nil {
if strings.Contains(err.Error(), "proto name \"*\" not known") {
// sentinel is unexported; match by message or pre-validate proto != "*"
}
} Prevention
- Never write proto:"*"; omit proto for all protocols
- Reserve "*" for the port position only
- Note the sentinel is unexported, so pre-validate rather than errors.Is
When it happens
Trigger: An ACL/grant rule with "proto": "*". Protocol.UnmarshalJSON lowercases the input, canonicalises numbers to names, then validate() hits case ProtocolNameWildcard and returns this error at parse time.
Common situations: Writing a deny-all-protocols rule using proto:"*" out of instinct; converting firewall rules from other systems (iptables ANY) that use a wildcard protocol token; docs/examples from other tools suggesting "*" as any-proto.
Related errors
- invalid protocol number
- leading 0 not permitted in protocol number
- protocol number out of range (0-255)
- protocol does not support specific ports
- test(s) failed
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/84c21075eb5790f6.
Report an issue: GitHub.