{"record":{"id":"71b5decdc73ab923","repo":"juanfont/headscale","slug":"input-cannot-end-with-a-colon-character","errorCode":null,"errorMessage":"input cannot end with a colon character","messagePattern":"input cannot end with a colon character","errorType":"validation","errorClass":"ErrInputEndsWithColon","httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/utils.go","lineNumber":18,"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.\nfunc splitDestinationAndPort(input string) (string, string, error) {","sourceCodeStart":1,"sourceCodeEnd":36,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/utils.go#L1-L36","documentation":"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.","triggerScenarios":"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.","commonSituations":"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 \"*\").","solutions":["Replace the empty port with a real port or \"*\": \"tag:web:*\"","If a templated port is optional, default the variable to \"*\" rather than \"\"","Lint the policy for dst entries ending in ':'"],"exampleFix":"// before\n\"dst\": [\"tag:web:\"]\n// after\n\"dst\": [\"tag:web:*\"]","handlingStrategy":"validation","validationCode":"func dstHasExplicitPort(dst string) bool { return !strings.HasSuffix(dst, \":\") }","typeGuard":null,"tryCatchPattern":"if errors.Is(err, policyv2.ErrInputEndsWithColon) {\n    // replace the trailing colon with \"*\" or a numeric port\n}","preventionTips":["Empty port is not 'any port' — write \"*\"","Default optional port template variables to \"*\" not \"\""],"tags":["policy","acl","parsing","ports"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}