{"record":{"id":"17053c5d676c1c81","repo":"netbirdio/netbird","slug":"invalid-rule-format-expected-protocol-port-or-pro","errorCode":null,"errorMessage":"invalid rule format: expected protocol/port or protocol/port-range","messagePattern":"invalid rule format: expected protocol/port or protocol/port-range","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/management/types/policy.go","lineNumber":199,"sourceCode":"\tfor groupID := range groups {\n\t\tgroupIDs = append(groupIDs, groupID)\n\t}\n\n\treturn groupIDs\n}\n\nfunc ParseRuleString(rule string) (PolicyRuleProtocolType, RulePortRange, error) {\n\trule = strings.TrimSpace(strings.ToLower(rule))\n\tif rule == \"all\" {\n\t\treturn PolicyRuleProtocolALL, RulePortRange{}, nil\n\t}\n\tif rule == \"icmp\" {\n\t\treturn PolicyRuleProtocolICMP, RulePortRange{}, nil\n\t}\n\n\tsplit := strings.Split(rule, \"/\")\n\tif len(split) != 2 {\n\t\treturn \"\", RulePortRange{}, errors.New(\"invalid rule format: expected protocol/port or protocol/port-range\")\n\t}\n\n\tprotoStr := strings.TrimSpace(split[0])\n\tportStr := strings.TrimSpace(split[1])\n\n\tvar protocol PolicyRuleProtocolType\n\tswitch protoStr {\n\tcase \"tcp\":\n\t\tprotocol = PolicyRuleProtocolTCP\n\tcase \"udp\":\n\t\tprotocol = PolicyRuleProtocolUDP\n\tcase \"icmp\":\n\t\treturn \"\", RulePortRange{}, errors.New(\"icmp does not accept ports; use 'icmp' without '/…'\")\n\tcase \"netbird-ssh\":\n\t\treturn PolicyRuleProtocolNetbirdSSH, RulePortRange{Start: nativeSSHPortNumber, End: nativeSSHPortNumber}, nil\n\tdefault:\n\t\treturn \"\", RulePortRange{}, fmt.Errorf(\"invalid protocol: %q\", protoStr)\n\t}","sourceCodeStart":181,"sourceCodeEnd":217,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/shared/management/types/policy.go#L181-L217","documentation":"ParseRuleString only accepts the bare keywords 'all' and 'icmp' or strings of the form protocol/port with exactly one '/' separator. It splits on '/' and requires exactly two segments; anything else fails here before protocol or port parsing begins.","triggerScenarios":"Inputs like \"tcp\" (no port), \"tcp/80/443\" (two slashes), \"80\" (port only), \"tcp:80\" (wrong separator), or an empty/whitespace-only string.","commonSituations":"Hand-written access-control rule strings in policy payloads; CSV/YAML imports with malformed cells; free-text UI fields without pre-validation.","solutions":["Format rules as protocol/port, e.g. tcp/22, or a range like udp/5000-5010","Use the bare keywords 'all' or 'icmp' when no port applies","Normalize separators and trim whitespace before submitting rules"],"exampleFix":"// before\nproto, ports, err := types.ParseRuleString(\"tcp 80\")\n\n// after\nproto, ports, err := types.ParseRuleString(\"tcp/80\")","handlingStrategy":"validation","validationCode":"var ruleRe = regexp.MustCompile(`^(all|icmp|[a-z-]+/[0-9]+(-[0-9]+)?)$`)\nif !ruleRe.MatchString(strings.ToLower(strings.TrimSpace(rule))) {\n\treturn fmt.Errorf(\"rule %q must be 'all', 'icmp', or protocol/port[-range]\", rule)\n}","typeGuard":"func isParsableRule(rule string) bool {\n\tr := strings.ToLower(strings.TrimSpace(rule))\n\tif r == \"all\" || r == \"icmp\" {\n\t\treturn true\n\t}\n\tparts := strings.Split(r, \"/\")\n\treturn len(parts) == 2 && parts[0] != \"\" && parts[1] != \"\"\n}","tryCatchPattern":"if _, _, err := types.ParseRuleString(rule); err != nil {\n\treturn fmt.Errorf(\"rejecting rule %q from import: %w\", rule, err)\n}","preventionTips":["Validate rule strings at the input boundary (UI, import parser)","Use one canonical producer for rule strings instead of formatting at several call sites","Reject wrong separators like ':' early with a clear message"],"tags":["policy","validation","parsing","firewall-rules"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}