{"record":{"id":"a9293e541fed781c","repo":"juanfont/headscale","slug":"dst-q-port-range-q-w","errorCode":null,"errorMessage":"dst=%q: port range %q: %w","messagePattern":"dst=%q: port range %q: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":891,"sourceCode":"\n\tswitch vs := v.(type) {\n\tcase string:\n\t\tvar (\n\t\t\tportsPart string\n\t\t\terr       error\n\t\t)\n\n\t\toriginalDst := vs\n\n\t\tif strings.Contains(vs, \":\") {\n\t\t\tvs, portsPart, err = splitDestinationAndPort(vs)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tports, err := parsePortRange(portsPart)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\"dst=%q: port range %q: %w\",\n\t\t\t\t\toriginalDst, portsPart, err,\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tve.Ports = ports\n\t\t} else {\n\t\t\treturn ErrHostportMissingColon\n\t\t}\n\n\t\tve.Alias, err = parseAlias(vs)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := ve.Validate(); err != nil { //nolint:noinlineerr\n\t\t\treturn err\n\t\t}","sourceCodeStart":873,"sourceCodeEnd":909,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L873-L909","documentation":"While unmarshalling a test destination (host:port form), the part after the colon failed parsePortRange. The error is prefixed with the full original destination and the offending port substring so the exact bad fragment is visible.","triggerScenarios":"Destinations like 'web:80-90-100' (double range), 'web:abc', 'web:70000', or 'web:0x50'. splitDestinationAndPort succeeds but parsePortRange(portsPart) errors in the test-destination unmarshaller.","commonSituations":"Comma-spliced ports ('web:80,443' parsed as one fragment), ranges written with '..' instead of '-', port numbers from config copied in decimal-with-suffix form.","solutions":["Use a single port or a simple 'low-high' range: 'web:80', 'web:8080-8090'.","Split comma-separated ports into separate destination entries.","Keep ports within 0-65535 and range bounds ordered."],"exampleFix":"// before\n\"accept\": [\"web:80,443\"]\n\n// after\n\"accept\": [\"web:80\", \"web:443\"]","handlingStrategy":"validation","validationCode":"var portRangeRe = regexp.MustCompile(`^[0-9]+(-[0-9]+)?$`)\n_, ports := splitDestinationAndPortSafe(dst)\nif !portRangeRe.MatchString(ports) {\n    return fmt.Errorf(\"bad ports %q in %q; want N or N-M\", ports, dst)\n}","typeGuard":null,"tryCatchPattern":"if err := v2.UnmarshalTestDestination(dst, &ve); err != nil {\n    if strings.Contains(err.Error(), \"port range\") {\n        // error shows dst= and the exact bad fragment; fix that entry\n    }\n    return err\n}","preventionTips":["One port or one range per destination; separate entries for more.","Numeric ports only (0-65535), '-' as the range separator.","Schema-lint the tests section in CI."],"tags":["policy","acl","ports","validation","testing","go"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}