juanfont/headscale · error

protocol does not support specific ports

Error message

protocol does not support specific ports

What it means

ErrProtocolNoSpecificPorts is returned by validateProtocolPortCompatibility (hscontrol/policy/v2/types.go:3184) when an ACL/grant rule names a protocol other than TCP, UDP, or SCTP (or empty) but its destinations carry specific port ranges. Only connection-oriented protocols with port fields may pin ports; for everything else the port list must be the wildcard. The error names the offending protocol and reminds that only "*" is allowed.

Source

Thrown at hscontrol/policy/v2/types.go:146

	ErrAutoApproverNotAlias        = errors.New("auto approver is not an alias")
	ErrInvalidACLAction            = errors.New("invalid ACL action")
	ErrInvalidSSHAction            = errors.New("invalid SSH action")
	ErrInvalidProtocolNumber       = errors.New("invalid protocol number")
	ErrProtocolLeadingZero         = errors.New("leading 0 not permitted in protocol number")
	ErrProtocolOutOfRange          = errors.New("protocol number out of range (0-255)")
	ErrAutogroupNotSupported       = errors.New("autogroup not supported in headscale")
	ErrAutogroupInternetSrc        = errors.New("autogroup:internet can only be used in ACL destinations")
	ErrAutogroupSelfSrc            = errors.New("\"autogroup:self\" not valid on the src side of a rule")
	ErrAutogroupNotSupportedACLSrc = errors.New("autogroup not supported for ACL sources")
	ErrAutogroupNotSupportedACLDst = errors.New("autogroup not supported for ACL destinations")
	ErrAutogroupDangerAllDst       = errors.New("cannot use autogroup:danger-all as a dst")
	ErrAutogroupNotSupportedSSHSrc = errors.New("autogroup not supported for SSH sources")
	ErrAutogroupNotSupportedSSHDst = errors.New("autogroup not supported for SSH destinations")
	ErrHostNotDefined              = errors.New("host not defined in policy")
	ErrSSHSourceAliasNotSupported  = errors.New("alias not supported for SSH source")
	ErrSSHDestAliasNotSupported    = errors.New("alias not supported for SSH destination")
	ErrUnknownField                = errors.New("unknown field")
	ErrProtocolNoSpecificPorts     = errors.New("protocol does not support specific ports")
	ErrTestEmptyAssertions         = errors.New("test entry must have at least one of \"accept\" or \"deny\"")
	ErrTestProtocolNotAllowed      = errors.New("test protocol must be tcp, udp, sctp, or empty")
	ErrTestDestinationMultiPort    = errors.New("test destination port must be a single port")
	ErrTestDestinationCIDR         = errors.New("test destination must be a single host, not a CIDR range")
	ErrAutogroupInternetTestDst    = errors.New("autogroup:internet not valid as a test destination")
	ErrSSHTestEmptySrc             = errors.New("SSH tests entry must have a non-empty src")
	ErrSSHTestEmptyDst             = errors.New("SSH tests entry must have at least one dst")
	ErrSSHTestDstUnknownTag        = errors.New("SSH tests dst contains unknown tag")
	ErrSSHTestDstDisallowedElement = errors.New("SSH tests dst contains disallowed element")
)

type resolved struct {
	ips netipx.IPSet
}

func newResolved(ipb *netipx.IPSetBuilder) (resolved, error) {
	ips, err := ipb.IPSet()
	if err != nil {

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Drop the port suffix and use the wildcard: dst "tag:server:*" for non-tcp/udp/sctp protocols
  2. Or change the rule's proto to tcp, udp, or sctp if the traffic actually has ports
  3. If you intended 'all protocols to these ports', remove the proto field entirely (empty proto supports ports)
  4. If you intended raw protocol traffic (e.g. GRE tunnel), keep proto and use "*" ports

Example fix

// before
{"proto": "icmp", "src": ["group:admin"], "dst": ["tag:server:80"]}
// after
{"proto": "icmp", "src": ["group:admin"], "dst": ["tag:server:*"]}
Defensive patterns

Strategy: validation

Validate before calling

// Pre-flight a rule before writing it into a policy
func ruleProtoAllowsPorts(proto string) bool {
    switch proto {
    case "", "tcp", "udp", "sctp":
        return true
    }
    return false
}
// if !ruleProtoAllowsPorts(rule.Proto) { ensure every dst port is "*" }

Try / catch

if err := pol.Validate(); err != nil {
    if errors.Is(err, policyv2.ErrProtocolNoSpecificPorts) {
        // tell user to use "*" ports for non-tcp/udp/sctp protocols
    }
}

Prevention

When it happens

Trigger: A policy rule like {"proto": "icmp", "dst": ["tag:server:80"]}, or proto "gre"/"esp"/numeric 47 with any port other than the wildcard 0-65535. Raised during policy.validate() -> validateProtocolPortCompatibility when portRange.First != 0 || portRange.Last != 65535 for a non-tcp/udp/sctp protocol.

Common situations: Copying a TCP rule and changing only the proto to icmp/esp/gre while keeping ":22" or ":80-443"; using numeric protocols (e.g. 50 for ESP) with port suffixes; forgetting that an omitted proto defaults to all protocols but still permits ports, while an explicit non-port protocol does not.

Related errors


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