{"record":{"id":"51771d8b07884a7c","repo":"juanfont/headscale","slug":"port-range-q-w","errorCode":null,"errorMessage":"port range %q: %w","messagePattern":"port range %q: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":946,"sourceCode":"\terr := json.Unmarshal(b, &v)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch vs := v.(type) {\n\tcase string:\n\t\tif vs == \"*\" {\n\t\t\tve.Protocol = ProtocolNameWildcard\n\t\t\tve.Ports = []tailcfg.PortRange{tailcfg.PortRangeAny}\n\n\t\t\treturn nil\n\t\t}\n\n\t\t// Only contains a port, no protocol\n\t\tif !strings.Contains(vs, \":\") {\n\t\t\tports, err := parsePortRange(vs)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"port range %q: %w\", vs, err)\n\t\t\t}\n\n\t\t\tve.Protocol = ProtocolNameWildcard\n\t\t\tve.Ports = ports\n\n\t\t\treturn nil\n\t\t}\n\n\t\tparts := strings.Split(vs, \":\")\n\t\tif len(parts) != 2 {\n\t\t\treturn fmt.Errorf(\"%w, got: %v(%d)\", ErrProtocolPortInvalidFormat, parts, len(parts))\n\t\t}\n\n\t\tprotocol := Protocol(parts[0])\n\n\t\terr := protocol.validate()\n\t\tif err != nil {\n\t\t\treturn err","sourceCodeStart":928,"sourceCodeEnd":964,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L928-L964","documentation":"ProtocolPort.UnmarshalJSON hit the bare-port branch (string with no ':') and parsePortRange rejected it. The value was meant to be just a port or port range ('80', '8080-8090', or '*') but the fragment is malformed.","triggerScenarios":"Grant protocols entries like \"80-\" , \"http\" (a word, not a port, and no colon so it is treated as a port), or \"99999\". strings.Contains(vs, \":\") is false and parsePortRange(vs) errors.","commonSituations":"Forgetting the protocol prefix: writing \"tcp\" or \"80\" where \"tcp:80\" was intended; service names instead of numbers ('ssh' instead of '22').","solutions":["Write protocol-qualified forms: 'tcp:22', 'udp:53', 'tcp:8080-8090'.","If you meant a bare wildcard, use '*' exactly.","Replace service names with their numeric ports."],"exampleFix":"// before\n\"protocols\": [\"ssh\"]\n\n// after\n\"protocols\": [\"tcp:22\"]","handlingStrategy":"validation","validationCode":"var barePortRe = regexp.MustCompile(`^(\\*|[0-9]+(-[0-9]+)?)$`)\nif !strings.Contains(s, \":\") && !barePortRe.MatchString(s) {\n    return fmt.Errorf(\"%q is neither '*'/port-range nor proto:port; did you mean 'tcp:%s'?\", s, s)\n}","typeGuard":null,"tryCatchPattern":"if err := pp.UnmarshalJSON([]byte(s)); err != nil {\n    if strings.Contains(err.Error(), \"port range\") {\n        // likely a protocol name without ':port'; rewrite as proto:port\n    }\n    return err\n}","preventionTips":["Default to the qualified form 'tcp:22'/'udp:53'.","Use numbers, not service names.","'*' is the only valid bare word."],"tags":["policy","acl","ports","protocols","validation","go"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}