netbirdio/netbird · error

domain is required for TCP/UDP services (used for cluster de

Error message

domain is required for TCP/UDP services (used for cluster derivation)

What it means

Returned by validateTCPUDPMode when a tcp or udp service has an empty Domain. Even though L4 traffic is routed by port, the management plane uses the domain to derive the cluster identity that fronts the port, so it is mandatory for every mode. This mirrors validateHTTPMode and validateTLSMode, which also require a domain.

Source

Thrown at management/internals/modules/reverseproxy/service/service.go:922

	if s.Auth.BearerAuth != nil && s.Auth.BearerAuth.Enabled {
		return errors.New("private services cannot enable bearer auth (SSO): NetBird-only access and SSO are mutually exclusive")
	}
	return nil
}

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() {

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Set the service domain to your account's zone, e.g. "domain": "netbird.example.com".
  2. Reuse the same domain value you use for HTTP services in that account; it is per-account configuration, not per-protocol.
  3. Check the API payload actually serializes the field (empty string and omitted field are indistinguishable server-side).

Example fix

// before
{ "name": "db", "mode": "tcp", "listen_port": 5432, "targets": [...] }

// after
{ "name": "db", "mode": "tcp", "domain": "netbird.example.com", "listen_port": 5432, "targets": [...] }
Defensive patterns

Strategy: validation

Validate before calling

func checkL4Domain(mode, domain string) error {
	if (mode == "tcp" || mode == "udp") && domain == "" {
		return errors.New("domain is required for tcp/udp services")
	}
	return nil
}

Type guard

func hasL4Domain(mode, domain string) bool {
	return mode != "tcp" && mode != "udp" || domain != ""
}

Try / catch

if err := svc.Validate(); err != nil {
	if strings.Contains(err.Error(), "domain is required for TCP/UDP") {
		return respondBadRequest(fmt.Errorf("set the account domain on the service payload"))
	}
	return respondBadRequest(err)
}

Prevention

When it happens

Trigger: Creating or updating a service with mode "tcp"/"udp" and no domain field in the request payload; a client that only sends listen_port and targets for L4 services assuming the domain is HTTP-only.

Common situations: Scripts or IaC modules that create port-forwards and skip the domain because 'ports don't need names'. Building an L4 service from scratch via the REST API with a minimal payload. Assuming the management service will auto-generate a domain (it does that for peer-expose requests, not for operator-created services).

Related errors


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/c2b52ac1db037771. Report an issue: GitHub.