juanfont/headscale · error · ErrInputEndsWithColon

input cannot end with a colon character

Error message

input cannot end with a colon character

What it means

ErrInputEndsWithColon is returned by splitDestinationAndPort (hscontrol/policy/v2/utils.go:75) when the input's last character is ':', e.g. "tag:server:". The port section after the final colon is empty, which is invalid — a wildcard must be written explicitly as "*" rather than left blank.

Source

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

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
// port strings never contain colons.
func splitDestinationAndPort(input string) (string, string, error) {

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Replace the empty port with a real port or "*": "tag:web:*"
  2. If a templated port is optional, default the variable to "*" rather than ""
  3. Lint the policy for dst entries ending in ':'

Example fix

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

Strategy: validation

Validate before calling

func dstHasExplicitPort(dst string) bool { return !strings.HasSuffix(dst, ":") }

Try / catch

if errors.Is(err, policyv2.ErrInputEndsWithColon) {
    // replace the trailing colon with "*" or a numeric port
}

Prevention

When it happens

Trigger: Dst entries like "10.0.0.1:", "tag:web:", or the bracketed forms "[::1]:" and "[fd7a::1]/128:" (see utils_test.go:108-110). Raised when lastColonIndex == len(input)-1.

Common situations: Trailing colon left after deleting a port while editing; templating a port variable that renders empty; assuming an empty port means 'all ports' (it does not — use "*").

Related errors


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