netbirdio/netbird · error
TCP/UDP services must have exactly one target
Error message
TCP/UDP services must have exactly one target
What it means
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.
Source
Thrown at management/internals/modules/reverseproxy/service/service.go:928
func (s *Service) validateHTTPMode() error {
if s.Domain == "" {
return errors.New("service domain is required")
}
if s.ListenPort != 0 {
return errors.New("listen_port is not supported for HTTP services")
}
return s.validateHTTPTargets()
}
func (s *Service) validateTCPUDPMode() error {
if s.Domain == "" {
return errors.New("domain is required for TCP/UDP services (used for cluster derivation)")
}
if s.isAuthEnabled() {
return errors.New("auth is not supported for TCP/UDP services")
}
if len(s.Targets) != 1 {
return errors.New("TCP/UDP services must have exactly one target")
}
if s.Mode == ModeUDP && s.Targets[0].ProxyProtocol {
return errors.New("proxy_protocol is not supported for UDP services")
}
return s.validateL4Target(s.Targets[0])
}
func (s *Service) validateTLSMode() error {
if s.Domain == "" {
return errors.New("domain is required for TLS services (used for SNI matching)")
}
if s.isAuthEnabled() {
return errors.New("auth is not supported for TLS services")
}
if s.ListenPort == 0 {
return errors.New("listen_port is required for TLS services")
}
if len(s.Targets) != 1 {View on GitHub (pinned to 93e97f4bf1)
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.
Example fix
// before
{ "mode": "tcp", "listen_port": 5432,
"targets": [ {"target_id": "peer-a", "port": 5432}, {"target_id": "peer-b", "port": 5432} ] }
// after
{ "mode": "tcp", "listen_port": 5432,
"targets": [ {"target_id": "peer-a", "port": 5432} ] } Defensive patterns
Strategy: validation
Validate before calling
func checkL4SingleTarget(mode string, targets []Target) error {
if mode == "tcp" || mode == "udp" {
if len(targets) != 1 {
return fmt.Errorf("tcp/udp needs exactly 1 target, got %d", len(targets))
}
}
return nil
} Type guard
func isL4TargetCountValid(mode string, targets []Target) bool {
if mode != "tcp" && mode != "udp" {
return true
}
return len(targets) == 1
} Try / catch
if err := svc.Validate(); err != nil {
if strings.Contains(err.Error(), "exactly one target") {
return respondBadRequest(errors.New("split into one service per upstream"))
}
return respondBadRequest(err)
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- target_id is required for L4 services
- domain is required for TCP/UDP services (used for cluster de
- auth is not supported for TCP/UDP services
- TLS services must have exactly one target
- target port is required for L4 services
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/8c2f6820be4e27e4.
Report an issue: GitHub.