juanfont/headscale · error · ErrInputMissingColon

input must contain a colon character separating destination

Error message

input must contain a colon character separating destination and port

What it means

ErrInputMissingColon is returned by splitDestinationAndPort (hscontrol/policy/v2/utils.go:67) when a destination-and-port string contains no ':' at all, so destination and port cannot be separated. Every ACL/grant destination must use the "destination:port" form (or the bracketed IPv6 forms "[addr]:port" / "[addr]/prefix:port"). It propagates out of destination parsing during policy load.

Source

Thrown at hscontrol/policy/v2/utils.go:16

package v2

import (
	"errors"
	"fmt"
	"net/netip"
	"slices"
	"strconv"
	"strings"

	"tailscale.com/tailcfg"
)

// Port parsing errors.
var (
	ErrInputMissingColon      = errors.New("input must contain a colon character separating destination and port")
	ErrInputStartsWithColon   = errors.New("input cannot start with a colon character")
	ErrInputEndsWithColon     = errors.New("input cannot end with a colon character")
	ErrInvalidPortRangeFormat = errors.New("invalid port range format")
	ErrPortRangeInverted      = errors.New("invalid port range: first port is greater than last port")
	ErrPortMustBePositive     = errors.New("first port must be >0, or use '*' for wildcard")
	ErrInvalidPortNumber      = errors.New("invalid first integer")
	ErrPortNumberOutOfRange   = errors.New("port number out of range")
	ErrBracketsNotIPv6        = errors.New("square brackets are only valid around IPv6 addresses")
)

// splitDestinationAndPort takes an input string and returns the destination and port as a tuple, or an error if the input is invalid.
// It supports two bracketed IPv6 forms:
//   - "[addr]:port" (RFC 3986, e.g. "[::1]:80")
//   - "[addr]/prefix:port" (e.g. "[fd7a::1]/128:80,443")
//
// Brackets are only accepted around IPv6 addresses, not IPv4, hostnames, or other alias types.
// Bracket stripping reduces both forms to bare "addr:port" or "addr/prefix:port",
// which the normal [strings.LastIndex] of ":" split handles correctly because

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Append a port to the destination: ":22", ":80,443", or ":*"
  2. For IPv6, use bracketed form "[fd7a::1]:22" (brackets are required, see ErrBracketsNotIPv6)
  3. Check the erroring rule's dst array for any entry lacking a colon

Example fix

// before
"dst": ["tag:server"]
// after
"dst": ["tag:server:*"]
Defensive patterns

Strategy: validation

Validate before calling

// Check a dst string is splittable before adding to a policy
func dstHasPort(dst string) bool {
    i := strings.LastIndex(dst, ":")
    return i > 0 && i < len(dst)-1
}

Try / catch

if errors.Is(err, policyv2.ErrInputMissingColon) {
    // append a port (":*" for any) to the destination
}

Prevention

When it happens

Trigger: A dst like "tag:server" — note the colon in "tag:" does not count only if... actually "tag:server:22" is fine; the error fires for inputs with zero colons such as "10.0.0.1", "example-host", "autogroup:member" (no port). Raised when strings.LastIndex(input, ":") == -1.

Common situations: Forgetting the port suffix when writing dst entries; using a bare hostname where "host:*" was intended; YAML anchors or templating that strip the trailing ":*" portion.

Related errors


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