{"record":{"id":"806d02dc7fe4c1de","repo":"netbirdio/netbird","slug":"target-port-is-required-for-l4-services","errorCode":null,"errorMessage":"target port is required for L4 services","messagePattern":"target port is required for L4 services","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"error","filePath":"management/internals/modules/reverseproxy/service/service.go","lineNumber":1039,"sourceCode":"\t}\n\treturn nil\n}\n\nfunc (s *Service) validateL4Target(target *Target) error {\n\t// L4 services have a single target; per-target disable is meaningless\n\t// (use the service-level Enabled flag instead). Force it on so that\n\t// buildPathMappings always includes the target in the proto.\n\ttarget.Enabled = true\n\n\tif target.TargetId == \"\" {\n\t\treturn errors.New(\"target_id is required for L4 services\")\n\t}\n\t// Cluster targets resolve their upstream host:port from the target's\n\t// own Host/Port fields just like the other L4 types — buildPathMappings\n\t// emits net.JoinHostPort(target.Host, target.Port) for every L4\n\t// target, so allowing port=0 here would let \":0\" reach the proxy.\n\tif target.Port == 0 {\n\t\treturn errors.New(\"target port is required for L4 services\")\n\t}\n\tswitch target.TargetType {\n\tcase TargetTypePeer, TargetTypeHost, TargetTypeDomain:\n\t\tif err := validateDirectUpstreamHost(0, target); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase TargetTypeSubnet:\n\t\tif target.Host == \"\" {\n\t\t\treturn errors.New(\"target host is required for subnet targets\")\n\t\t}\n\tcase TargetTypeCluster:\n\t\t// target_id carries the cluster address; the proxy resolves\n\t\t// the upstream at request time.\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid target_type %q for L4 service\", target.TargetType)\n\t}\n\tif target.Path != nil && *target.Path != \"\" && *target.Path != \"/\" {\n\t\treturn errors.New(\"path is not supported for L4 services\")","sourceCodeStart":1021,"sourceCodeEnd":1057,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/management/internals/modules/reverseproxy/service/service.go#L1021-L1057","documentation":"Returned by validateL4Target when the L4 target's Port is 0 (the uint16 zero value, i.e. omitted). The proxy's path mapping builds the upstream dial address with net.JoinHostPort(target.Host, target.Port), so a zero port would literally send \":0\" to the dialer; the validation closes that gap. Cluster targets are included: they resolve host:port from these same fields.","triggerScenarios":"An L4 target JSON with no port key (defaults to 0); relying on a 'default port' convention that this API does not have; port field typed as string in the client and silently dropped during serialization to uint16.","commonSituations":"Assuming listen_port on the service implies the upstream port. Copying HTTP target configs where the port lived elsewhere. Client structs where port is an int and 0 means 'unset' - the server cannot distinguish that from an invalid value, so it must be explicit.","solutions":["Set target.port to the upstream's real port (1-65535).","Remember listen_port is the proxy-side listener; target.port is what the upstream listens on - both are needed for tcp/udp.","If your client marshals port from a string, validate and convert before sending so the field is actually present."],"exampleFix":"// before\n{ \"mode\": \"tcp\", \"listen_port\": 5432,\n  \"targets\": [ { \"target_type\": \"peer\", \"target_id\": \"peer-a\" } ] }\n\n// after\n{ \"mode\": \"tcp\", \"listen_port\": 5432,\n  \"targets\": [ { \"target_type\": \"peer\", \"target_id\": \"peer-a\", \"port\": 5432 } ] }","handlingStrategy":"validation","validationCode":"func checkL4TargetPort(t Target) error {\n\tif t.Port == 0 {\n\t\treturn errors.New(\"target port must be 1-65535 for L4 targets\")\n\t}\n\treturn nil\n}","typeGuard":"func hasL4TargetPort(t Target) bool {\n\treturn t.Port != 0\n}","tryCatchPattern":"if err := svc.Validate(); err != nil {\n\tif strings.Contains(err.Error(), \"target port is required for L4\") {\n\t\treturn respondBadRequest(errors.New(\"set the upstream port explicitly - there is no default\"))\n\t}\n\treturn respondBadRequest(err)\n}","preventionTips":["Never rely on an implied default upstream port; always resolve and set it.","uint16 zero means omitted server-side - keep port fields non-zero in client structs.","Verify serialization: a string-typed port in the client must actually reach the JSON as a number."],"tags":["netbird","reverse-proxy","validation","l4","port","targets","go"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}