{"record":{"id":"8c2f6820be4e27e4","repo":"netbirdio/netbird","slug":"tcp-udp-services-must-have-exactly-one-target","errorCode":null,"errorMessage":"TCP/UDP services must have exactly one target","messagePattern":"TCP/UDP services must have exactly one target","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"error","filePath":"management/internals/modules/reverseproxy/service/service.go","lineNumber":928,"sourceCode":"func (s *Service) validateHTTPMode() error {\n\tif s.Domain == \"\" {\n\t\treturn errors.New(\"service domain is required\")\n\t}\n\tif s.ListenPort != 0 {\n\t\treturn errors.New(\"listen_port is not supported for HTTP services\")\n\t}\n\treturn s.validateHTTPTargets()\n}\n\nfunc (s *Service) validateTCPUDPMode() error {\n\tif s.Domain == \"\" {\n\t\treturn errors.New(\"domain is required for TCP/UDP services (used for cluster derivation)\")\n\t}\n\tif s.isAuthEnabled() {\n\t\treturn errors.New(\"auth is not supported for TCP/UDP services\")\n\t}\n\tif len(s.Targets) != 1 {\n\t\treturn errors.New(\"TCP/UDP services must have exactly one target\")\n\t}\n\tif s.Mode == ModeUDP && s.Targets[0].ProxyProtocol {\n\t\treturn errors.New(\"proxy_protocol is not supported for UDP services\")\n\t}\n\treturn s.validateL4Target(s.Targets[0])\n}\n\nfunc (s *Service) validateTLSMode() error {\n\tif s.Domain == \"\" {\n\t\treturn errors.New(\"domain is required for TLS services (used for SNI matching)\")\n\t}\n\tif s.isAuthEnabled() {\n\t\treturn errors.New(\"auth is not supported for TLS services\")\n\t}\n\tif s.ListenPort == 0 {\n\t\treturn errors.New(\"listen_port is required for TLS services\")\n\t}\n\tif len(s.Targets) != 1 {","sourceCodeStart":910,"sourceCodeEnd":946,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/management/internals/modules/reverseproxy/service/service.go#L910-L946","documentation":"Returned by validateTCPUDPMode when len(s.Targets) != 1. An L4 service forwards exactly one upstream; there is no load balancing or fallback across multiple targets at the TCP/UDP layer in this model. HTTP services, by contrast, may declare multiple path-routed targets, which is the usual source of the confusion.","triggerScenarios":"Submitting a tcp/udp service with 0 targets (also caught earlier by Validate's 'at least one target' check only when 0) or with 2+ targets, e.g. a primary and a backup upstream.","commonSituations":"Reusing an HTTP multi-target (path-based routing) template for an L4 service. Trying to approximate high availability by listing two database replicas as targets. Splitting traffic with the expectation the proxy round-robins at L4.","solutions":["Reduce the targets array to exactly one entry for the tcp/udp service.","Expose each upstream as its own service (own listen_port) and load-balance in front of them if needed.","If you need multiple path-routed backends, switch the service to mode http."],"exampleFix":"// before\n{ \"mode\": \"tcp\", \"listen_port\": 5432,\n  \"targets\": [ {\"target_id\": \"peer-a\", \"port\": 5432}, {\"target_id\": \"peer-b\", \"port\": 5432} ] }\n\n// after\n{ \"mode\": \"tcp\", \"listen_port\": 5432,\n  \"targets\": [ {\"target_id\": \"peer-a\", \"port\": 5432} ] }","handlingStrategy":"validation","validationCode":"func checkL4SingleTarget(mode string, targets []Target) error {\n\tif mode == \"tcp\" || mode == \"udp\" {\n\t\tif len(targets) != 1 {\n\t\t\treturn fmt.Errorf(\"tcp/udp needs exactly 1 target, got %d\", len(targets))\n\t\t}\n\t}\n\treturn nil\n}","typeGuard":"func isL4TargetCountValid(mode string, targets []Target) bool {\n\tif mode != \"tcp\" && mode != \"udp\" {\n\t\treturn true\n\t}\n\treturn len(targets) == 1\n}","tryCatchPattern":"if err := svc.Validate(); err != nil {\n\tif strings.Contains(err.Error(), \"exactly one target\") {\n\t\treturn respondBadRequest(errors.New(\"split into one service per upstream\"))\n\t}\n\treturn respondBadRequest(err)\n}","preventionTips":["Never carry HTTP multi-target templates into L4 service definitions.","Model L4 services as one-listener-one-upstream in your config DSL.","Unit-test the payload builder for each mode to lock target cardinality."],"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"}