juanfont/headscale · error
test destination port must be a single port
Error message
test destination port must be a single port
What it means
ErrTestDestinationMultiPort is returned by validateTestDestination (hscontrol/policy/v2/types.go:3255) when a tests-block accept/deny destination does not name exactly one single port. The parsed port list must have exactly one range whose First equals its Last; wildcards (*), comma lists, and ranges like 80-443 are rejected because a multi-port destination has no single allow/deny answer.
Source
Thrown at hscontrol/policy/v2/types.go:149
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 {
return resolved{}, err
}
View on GitHub (pinned to 565fd254d0)
Solutions
- Narrow the test dst to one exact port, e.g. "tag:server:443"
- Split multi-port assertions into one entry per port (or one per range endpoint)
- Keep ranges and wildcards in grants/ACLs only, not tests
Example fix
// before
"tests": [{"src": "user1", "accept": ["tag:web:80,443"]}]
// after
"tests": [
{"src": "user1", "accept": ["tag:web:80"]},
{"src": "user1", "accept": ["tag:web:443"]}
] Defensive patterns
Strategy: validation
Validate before calling
// Check a tests dst string pins one port before adding it
func testDstSinglePort(dst string) bool {
i := strings.LastIndex(dst, ":")
if i == -1 { return false }
port := dst[i+1:]
if strings.ContainsAny(port, "-,") || port == "*" { return false }
n, err := strconv.Atoi(port)
return err == nil && n >= 1 && n <= 65535
} Try / catch
if errors.Is(err, policyv2.ErrTestDestinationMultiPort) {
// split the dst into one entry per port
} Prevention
- Write tests destinations as host:port with exactly one numeric port
- Split range/list assertions into multiple test entries
- Keep multi-port forms in grants only
When it happens
Trigger: Test destinations such as "tag:server:*", "10.0.0.1:80,443", or "tag:server:1000-2000" — i.e. len(awp.Ports) != 1 or Ports[0].First != Ports[0].Last. Raised during validateTests for each accept/deny entry.
Common situations: Copy-pasting an ACL dst (where ranges and lists are fine) into a tests block; testing a whole service range in one entry instead of splitting; forgetting the tests block is stricter than grant destinations by design (comment at types.go:3192-3196).
Related errors
- protocol does not support specific ports
- test entry must have at least one of "accept" or "deny"
- test protocol must be tcp, udp, sctp, or empty
- test destination must be a single host, not a CIDR range
- SSH tests entry must have a non-empty src
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/c9003cc8bf004c20.
Report an issue: GitHub.