{"record":{"id":"f2cd9262aef58c54","repo":"juanfont/headscale","slug":"protocol-does-not-support-specific-ports","errorCode":null,"errorMessage":"protocol does not support specific ports","messagePattern":"protocol does not support specific ports","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":146,"sourceCode":"\tErrAutoApproverNotAlias        = errors.New(\"auto approver is not an alias\")\n\tErrInvalidACLAction            = errors.New(\"invalid ACL action\")\n\tErrInvalidSSHAction            = errors.New(\"invalid SSH action\")\n\tErrInvalidProtocolNumber       = errors.New(\"invalid protocol number\")\n\tErrProtocolLeadingZero         = errors.New(\"leading 0 not permitted in protocol number\")\n\tErrProtocolOutOfRange          = errors.New(\"protocol number out of range (0-255)\")\n\tErrAutogroupNotSupported       = errors.New(\"autogroup not supported in headscale\")\n\tErrAutogroupInternetSrc        = errors.New(\"autogroup:internet can only be used in ACL destinations\")\n\tErrAutogroupSelfSrc            = errors.New(\"\\\"autogroup:self\\\" not valid on the src side of a rule\")\n\tErrAutogroupNotSupportedACLSrc = errors.New(\"autogroup not supported for ACL sources\")\n\tErrAutogroupNotSupportedACLDst = errors.New(\"autogroup not supported for ACL destinations\")\n\tErrAutogroupDangerAllDst       = errors.New(\"cannot use autogroup:danger-all as a dst\")\n\tErrAutogroupNotSupportedSSHSrc = errors.New(\"autogroup not supported for SSH sources\")\n\tErrAutogroupNotSupportedSSHDst = errors.New(\"autogroup not supported for SSH destinations\")\n\tErrHostNotDefined              = errors.New(\"host not defined in policy\")\n\tErrSSHSourceAliasNotSupported  = errors.New(\"alias not supported for SSH source\")\n\tErrSSHDestAliasNotSupported    = errors.New(\"alias not supported for SSH destination\")\n\tErrUnknownField                = errors.New(\"unknown field\")\n\tErrProtocolNoSpecificPorts     = errors.New(\"protocol does not support specific ports\")\n\tErrTestEmptyAssertions         = errors.New(\"test entry must have at least one of \\\"accept\\\" or \\\"deny\\\"\")\n\tErrTestProtocolNotAllowed      = errors.New(\"test protocol must be tcp, udp, sctp, or empty\")\n\tErrTestDestinationMultiPort    = errors.New(\"test destination port must be a single port\")\n\tErrTestDestinationCIDR         = errors.New(\"test destination must be a single host, not a CIDR range\")\n\tErrAutogroupInternetTestDst    = errors.New(\"autogroup:internet not valid as a test destination\")\n\tErrSSHTestEmptySrc             = errors.New(\"SSH tests entry must have a non-empty src\")\n\tErrSSHTestEmptyDst             = errors.New(\"SSH tests entry must have at least one dst\")\n\tErrSSHTestDstUnknownTag        = errors.New(\"SSH tests dst contains unknown tag\")\n\tErrSSHTestDstDisallowedElement = errors.New(\"SSH tests dst contains disallowed element\")\n)\n\ntype resolved struct {\n\tips netipx.IPSet\n}\n\nfunc newResolved(ipb *netipx.IPSetBuilder) (resolved, error) {\n\tips, err := ipb.IPSet()\n\tif err != nil {","sourceCodeStart":128,"sourceCodeEnd":164,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L128-L164","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Drop the port suffix and use the wildcard: dst \"tag:server:*\" for non-tcp/udp/sctp protocols","Or change the rule's proto to tcp, udp, or sctp if the traffic actually has ports","If you intended 'all protocols to these ports', remove the proto field entirely (empty proto supports ports)","If you intended raw protocol traffic (e.g. GRE tunnel), keep proto and use \"*\" ports"],"exampleFix":"// before\n{\"proto\": \"icmp\", \"src\": [\"group:admin\"], \"dst\": [\"tag:server:80\"]}\n// after\n{\"proto\": \"icmp\", \"src\": [\"group:admin\"], \"dst\": [\"tag:server:*\"]}","handlingStrategy":"validation","validationCode":"// Pre-flight a rule before writing it into a policy\nfunc ruleProtoAllowsPorts(proto string) bool {\n    switch proto {\n    case \"\", \"tcp\", \"udp\", \"sctp\":\n        return true\n    }\n    return false\n}\n// if !ruleProtoAllowsPorts(rule.Proto) { ensure every dst port is \"*\" }","typeGuard":null,"tryCatchPattern":"if err := pol.Validate(); err != nil {\n    if errors.Is(err, policyv2.ErrProtocolNoSpecificPorts) {\n        // tell user to use \"*\" ports for non-tcp/udp/sctp protocols\n    }\n}","preventionTips":["Only tcp/udp/sctp (or omitted proto) may carry specific ports","When changing a rule's proto away from tcp/udp/sctp, strip its port suffixes in the same edit","Add a policy-lint step to CI that flags non-port protocols with non-wildcard ports"],"tags":["policy","acl","protocol","ports","validation"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}