juanfont/headscale · error

port range %q: %w

Error message

port range %q: %w

What it means

ProtocolPort.UnmarshalJSON hit the bare-port branch (string with no ':') and parsePortRange rejected it. The value was meant to be just a port or port range ('80', '8080-8090', or '*') but the fragment is malformed.

Source

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

	err := json.Unmarshal(b, &v)
	if err != nil {
		return err
	}

	switch vs := v.(type) {
	case string:
		if vs == "*" {
			ve.Protocol = ProtocolNameWildcard
			ve.Ports = []tailcfg.PortRange{tailcfg.PortRangeAny}

			return nil
		}

		// Only contains a port, no protocol
		if !strings.Contains(vs, ":") {
			ports, err := parsePortRange(vs)
			if err != nil {
				return fmt.Errorf("port range %q: %w", vs, err)
			}

			ve.Protocol = ProtocolNameWildcard
			ve.Ports = ports

			return nil
		}

		parts := strings.Split(vs, ":")
		if len(parts) != 2 {
			return fmt.Errorf("%w, got: %v(%d)", ErrProtocolPortInvalidFormat, parts, len(parts))
		}

		protocol := Protocol(parts[0])

		err := protocol.validate()
		if err != nil {
			return err

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Write protocol-qualified forms: 'tcp:22', 'udp:53', 'tcp:8080-8090'.
  2. If you meant a bare wildcard, use '*' exactly.
  3. Replace service names with their numeric ports.

Example fix

// before
"protocols": ["ssh"]

// after
"protocols": ["tcp:22"]
Defensive patterns

Strategy: validation

Validate before calling

var barePortRe = regexp.MustCompile(`^(\*|[0-9]+(-[0-9]+)?)$`)
if !strings.Contains(s, ":") && !barePortRe.MatchString(s) {
    return fmt.Errorf("%q is neither '*'/port-range nor proto:port; did you mean 'tcp:%s'?", s, s)
}

Try / catch

if err := pp.UnmarshalJSON([]byte(s)); err != nil {
    if strings.Contains(err.Error(), "port range") {
        // likely a protocol name without ':port'; rewrite as proto:port
    }
    return err
}

Prevention

When it happens

Trigger: Grant protocols entries like "80-" , "http" (a word, not a port, and no colon so it is treated as a port), or "99999". strings.Contains(vs, ":") is false and parsePortRange(vs) errors.

Common situations: Forgetting the protocol prefix: writing "tcp" or "80" where "tcp:80" was intended; service names instead of numbers ('ssh' instead of '22').

Related errors


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