netbirdio/netbird · error

target_id is required for L4 services

Error message

target_id is required for L4 services

What it means

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.

Source

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

		return nil
	}
	if strings.ContainsAny(host, " \t/") {
		return fmt.Errorf("target %d: host %q contains invalid characters", idx, host)
	}
	if _, _, err := net.SplitHostPort(host); err == nil {
		return fmt.Errorf("target %d: host %q must not include a port (set target.port instead)", idx, host)
	}
	return nil
}

func (s *Service) validateL4Target(target *Target) error {
	// L4 services have a single target; per-target disable is meaningless
	// (use the service-level Enabled flag instead). Force it on so that
	// buildPathMappings always includes the target in the proto.
	target.Enabled = true

	if target.TargetId == "" {
		return errors.New("target_id is required for L4 services")
	}
	// Cluster targets resolve their upstream host:port from the target's
	// own Host/Port fields just like the other L4 types — buildPathMappings
	// emits net.JoinHostPort(target.Host, target.Port) for every L4
	// target, so allowing port=0 here would let ":0" reach the proxy.
	if target.Port == 0 {
		return errors.New("target port is required for L4 services")
	}
	switch target.TargetType {
	case TargetTypePeer, TargetTypeHost, TargetTypeDomain:
		if err := validateDirectUpstreamHost(0, target); err != nil {
			return err
		}
	case TargetTypeSubnet:
		if target.Host == "" {
			return errors.New("target host is required for subnet targets")
		}
	case TargetTypeCluster:

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Set target_id to the peer ID (peer targets), resource identifier, or - for cluster targets - the cluster address string.
  2. Do not rely on target.host to identify the upstream for L4; it is only a dial address for subnet/direct-upstream targets.
  3. If the upstream has no NetBird identity at all, use target_type "host" or "domain" with direct_upstream and still provide a target_id.

Example fix

// before
{ "mode": "tcp", "listen_port": 5432,
  "targets": [ { "target_type": "peer", "host": "10.10.0.5", "port": 5432 } ] }

// after
{ "mode": "tcp", "listen_port": 5432,
  "targets": [ { "target_type": "peer", "target_id": "peer-a-id", "port": 5432 } ] }
Defensive patterns

Strategy: validation

Validate before calling

func checkL4TargetID(t Target) error {
	if t.TargetId == "" {
		return errors.New("target_id is required for L4 targets")
	}
	return nil
}

Type guard

func hasL4TargetID(t Target) bool {
	return t.TargetId != ""
}

Try / catch

if err := svc.Validate(); err != nil {
	if strings.Contains(err.Error(), "target_id is required for L4") {
		return respondBadRequest(errors.New("set target_id: peer ID, resource ID, or cluster address"))
	}
	return respondBadRequest(err)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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