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

  1. Delete the proto field entirely — an omitted proto means all protocols
  2. Keep "*" only on the port side (dst "host:*") where it is valid
  3. 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

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


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/84c21075eb5790f6. Report an issue: GitHub.