{"record":{"id":"926bc5175dba6703","repo":"juanfont/headscale","slug":"input-cannot-start-with-a-colon-character","errorCode":null,"errorMessage":"input cannot start with a colon character","messagePattern":"input cannot start with a colon character","errorType":"validation","errorClass":"ErrInputStartsWithColon","httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/utils.go","lineNumber":17,"sourceCode":"package v2\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/netip\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"tailscale.com/tailcfg\"\n)\n\n// Port parsing errors.\nvar (\n\tErrInputMissingColon      = errors.New(\"input must contain a colon character separating destination and port\")\n\tErrInputStartsWithColon   = errors.New(\"input cannot start with a colon character\")\n\tErrInputEndsWithColon     = errors.New(\"input cannot end with a colon character\")\n\tErrInvalidPortRangeFormat = errors.New(\"invalid port range format\")\n\tErrPortRangeInverted      = errors.New(\"invalid port range: first port is greater than last port\")\n\tErrPortMustBePositive     = errors.New(\"first port must be >0, or use '*' for wildcard\")\n\tErrInvalidPortNumber      = errors.New(\"invalid first integer\")\n\tErrPortNumberOutOfRange   = errors.New(\"port number out of range\")\n\tErrBracketsNotIPv6        = errors.New(\"square brackets are only valid around IPv6 addresses\")\n)\n\n// splitDestinationAndPort takes an input string and returns the destination and port as a tuple, or an error if the input is invalid.\n// It supports two bracketed IPv6 forms:\n//   - \"[addr]:port\" (RFC 3986, e.g. \"[::1]:80\")\n//   - \"[addr]/prefix:port\" (e.g. \"[fd7a::1]/128:80,443\")\n//\n// Brackets are only accepted around IPv6 addresses, not IPv4, hostnames, or other alias types.\n// Bracket stripping reduces both forms to bare \"addr:port\" or \"addr/prefix:port\",\n// which the normal [strings.LastIndex] of \":\" split handles correctly because\n// port strings never contain colons.","sourceCodeStart":1,"sourceCodeEnd":35,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/utils.go#L1-L35","documentation":"ErrInputStartsWithColon is returned by splitDestinationAndPort (hscontrol/policy/v2/utils.go:71) when the input's only/last colon is the first character, e.g. \":22\". There is no destination before the colon, so the split cannot produce a host. It distinguishes this specific typo from the generic missing-colon case.","triggerScenarios":"A dst entry like \":80\" or \":*\" — a port with no host. Raised when strings.LastIndex(\":\") == 0.","commonSituations":"Variable interpolation producing an empty host (\"${host}:22\" with empty ${host}); deleting the host part while editing a rule; copy-paste losing the hostname.","solutions":["Fix the entry to include a host before the colon, e.g. \"tag:web:22\"","If templating produced it, ensure the host variable is non-empty before rendering the policy","Scan the policy for dst entries matching ^:\\d or ^:\\*"],"exampleFix":"// before\n\"dst\": [\":22\"]\n// after\n\"dst\": [\"tag:web:22\"]","handlingStrategy":"validation","validationCode":"func dstStartsWithHost(dst string) bool { return !strings.HasPrefix(dst, \":\") }","typeGuard":null,"tryCatchPattern":"if errors.Is(err, policyv2.ErrInputStartsWithColon) {\n    // a templated host rendered empty; fix the variable or hardcode the host\n}","preventionTips":["Never interpolate an empty host variable into a dst","Check rendered policy output when templates drive dst generation"],"tags":["policy","acl","parsing","ports"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}