netbirdio/netbird · error

target port is required for L4 services

Error message

target port is required for L4 services

What it means

Returned by validateL4Target when the L4 target's Port is 0 (the uint16 zero value, i.e. omitted). The proxy's path mapping builds the upstream dial address with net.JoinHostPort(target.Host, target.Port), so a zero port would literally send ":0" to the dialer; the validation closes that gap. Cluster targets are included: they resolve host:port from these same fields.

Source

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

	}
	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:
		// target_id carries the cluster address; the proxy resolves
		// the upstream at request time.
	default:
		return fmt.Errorf("invalid target_type %q for L4 service", target.TargetType)
	}
	if target.Path != nil && *target.Path != "" && *target.Path != "/" {
		return errors.New("path is not supported for L4 services")

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Set target.port to the upstream's real port (1-65535).
  2. Remember listen_port is the proxy-side listener; target.port is what the upstream listens on - both are needed for tcp/udp.
  3. If your client marshals port from a string, validate and convert before sending so the field is actually present.

Example fix

// before
{ "mode": "tcp", "listen_port": 5432,
  "targets": [ { "target_type": "peer", "target_id": "peer-a" } ] }

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

Strategy: validation

Validate before calling

func checkL4TargetPort(t Target) error {
	if t.Port == 0 {
		return errors.New("target port must be 1-65535 for L4 targets")
	}
	return nil
}

Type guard

func hasL4TargetPort(t Target) bool {
	return t.Port != 0
}

Try / catch

if err := svc.Validate(); err != nil {
	if strings.Contains(err.Error(), "target port is required for L4") {
		return respondBadRequest(errors.New("set the upstream port explicitly - there is no default"))
	}
	return respondBadRequest(err)
}

Prevention

When it happens

Trigger: An L4 target JSON with no port key (defaults to 0); relying on a 'default port' convention that this API does not have; port field typed as string in the client and silently dropped during serialization to uint16.

Common situations: Assuming listen_port on the service implies the upstream port. Copying HTTP target configs where the port lived elsewhere. Client structs where port is an int and 0 means 'unset' - the server cannot distinguish that from an invalid value, so it must be explicit.

Related errors


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