{"record":{"id":"81a858ced3f5f767","repo":"netbirdio/netbird","slug":"target-id-is-required-for-l4-services","errorCode":null,"errorMessage":"target_id is required for L4 services","messagePattern":"target_id is required for L4 services","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"error","filePath":"management/internals/modules/reverseproxy/service/service.go","lineNumber":1032,"sourceCode":"\t\treturn nil\n\t}\n\tif strings.ContainsAny(host, \" \\t/\") {\n\t\treturn fmt.Errorf(\"target %d: host %q contains invalid characters\", idx, host)\n\t}\n\tif _, _, err := net.SplitHostPort(host); err == nil {\n\t\treturn fmt.Errorf(\"target %d: host %q must not include a port (set target.port instead)\", idx, host)\n\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:","sourceCodeStart":1014,"sourceCodeEnd":1050,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/management/internals/modules/reverseproxy/service/service.go#L1014-L1050","documentation":"Returned by validateL4Target when the single target of a tcp/udp/tls service has an empty TargetId. For peer/host/domain targets the ID identifies the peer or network resource to dial; for cluster targets target_id itself carries the cluster address that the proxy resolves at request time (the Host field is not used for that). Note this function also force-sets target.Enabled=true because per-target disable is meaningless when there is exactly one target - use the service-level Enabled flag.","triggerScenarios":"Submitting an L4 target with only host and port filled (host:port style config) and no target_id; a cluster target where the address was placed in the host field instead of target_id; constructing targets programmatically and never populating the ID.","commonSituations":"Porting a plain host:port forward definition where there was never an ID concept. Automation that copies HTTP targets (where host is later overwritten by peer lookup) into an L4 service. UI forms that make target_id optional.","solutions":["Set target_id to the peer ID (peer targets), resource identifier, or - for cluster targets - the cluster address string.","Do not rely on target.host to identify the upstream for L4; it is only a dial address for subnet/direct-upstream targets.","If the upstream has no NetBird identity at all, use target_type \"host\" or \"domain\" with direct_upstream and still provide a target_id."],"exampleFix":"// before\n{ \"mode\": \"tcp\", \"listen_port\": 5432,\n  \"targets\": [ { \"target_type\": \"peer\", \"host\": \"10.10.0.5\", \"port\": 5432 } ] }\n\n// after\n{ \"mode\": \"tcp\", \"listen_port\": 5432,\n  \"targets\": [ { \"target_type\": \"peer\", \"target_id\": \"peer-a-id\", \"port\": 5432 } ] }","handlingStrategy":"validation","validationCode":"func checkL4TargetID(t Target) error {\n\tif t.TargetId == \"\" {\n\t\treturn errors.New(\"target_id is required for L4 targets\")\n\t}\n\treturn nil\n}","typeGuard":"func hasL4TargetID(t Target) bool {\n\treturn t.TargetId != \"\"\n}","tryCatchPattern":"if err := svc.Validate(); err != nil {\n\tif strings.Contains(err.Error(), \"target_id is required for L4\") {\n\t\treturn respondBadRequest(errors.New(\"set target_id: peer ID, resource ID, or cluster address\"))\n\t}\n\treturn respondBadRequest(err)\n}","preventionTips":["Resolve peer/resource IDs from the management API before building the target - never hand-type them.","For cluster targets remember the address lives in target_id, not host.","Make target_id a required field in your client models for L4 services."],"tags":["netbird","reverse-proxy","validation","l4","targets","configuration","go"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}