netbirdio/netbird · error

invalid port forward specification: %s

Error message

invalid port forward specification: %s

What it means

Returned by parsePortForwardSpec's default branch when the spec splits into more than four colon-separated parts. Valid specs have 2, 3, or 4 parts (port:socket, port:host:port / host:port:socket, host:port:host:port); five or more tokens have no handler. IPv6-looking specs with many colons land here unless they start with '[' and contain ']:' to be routed to the IPv6 parser.

Source

Thrown at client/cmd/ssh.go:730

	if strings.HasPrefix(spec, "[") && strings.Contains(spec, "]:") {
		return parseIPv6ForwardSpec(spec)
	}

	parts := strings.Split(spec, ":")
	if len(parts) < 2 {
		return "", "", fmt.Errorf("invalid port forward specification: %s (expected format: [local_host:]local_port:remote_target)", spec)
	}

	switch len(parts) {
	case 2:
		return parseTwoPartForwardSpec(parts, spec)
	case 3:
		return parseThreePartForwardSpec(parts)
	case 4:
		return parseFourPartForwardSpec(parts)
	default:
		return "", "", fmt.Errorf("invalid port forward specification: %s", spec)
	}
}

// parseTwoPartForwardSpec handles "port:unix_socket" format.
func parseTwoPartForwardSpec(parts []string, spec string) (string, string, error) {
	if isUnixSocket(parts[1]) {
		localAddr := "localhost:" + parts[0]
		remoteAddr := parts[1]
		return localAddr, remoteAddr, nil
	}
	return "", "", fmt.Errorf("invalid port forward specification: %s (expected format: [local_host:]local_port:remote_host:remote_port or [local_host:]local_port:unix_socket)", spec)
}

// parseThreePartForwardSpec handles "port:host:hostport" or "host:port:unix_socket" formats.
func parseThreePartForwardSpec(parts []string) (string, string, error) {
	if isUnixSocket(parts[2]) {
		localHost := normalizeLocalHost(parts[0])
		localAddr := localHost + ":" + parts[1]

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Reduce to at most 4 parts: [local_host:]local_port:remote_host:remote_port.
  2. Bracket IPv6 local hosts so the spec is routed to parseIPv6ForwardSpec: -L [::1]:8080:host:80.
  3. Drop trailing colons and extra segments introduced by copy-paste or templating.

Example fix

# before
netbird ssh -L ::1:8080:host:80 peer1
# -> invalid port forward specification: ::1:8080:host:80

# after
netbird ssh -L '[::1]:8080:host:80' peer1
Defensive patterns

Strategy: validation

Validate before calling

n := strings.Count(spec, ":") // +1 = parts count; require 2..4 parts
if n < 1 || n > 3 {
	if strings.Count(spec, ":") >= 3 && strings.HasPrefix(spec, "[") && !strings.Contains(spec, "]:") {
		return fmt.Errorf("IPv6 spec %q must be written as [v6]:port:host:hostport", spec)
	}
	return fmt.Errorf("spec %q must have 2-4 colon parts", spec)
}

Type guard

func isWellFormedForwardSpec(s string) bool {
	if strings.HasPrefix(s, "[") {
		return strings.Contains(s, "]:")
	}
	n := strings.Count(s, ":")
	return n >= 1 && n <= 3 // 2-4 parts
}

Try / catch

if len(parts) > 4 {
	// over-segmented: usually an unbracketed IPv6 or a trailing ':' —
	// re-normalize (bracket v6, trim separators) and retry once, else reject
}

Prevention

When it happens

Trigger: `-L 8080:host:80:extra` (5 parts), unbracketed IPv6 with ports like `-L ::1:8080:host:80` (6 parts and no leading '['), or specs pasted with a trailing `:`. The '[...]' guard earlier only diverts specs that both start with '[' and contain ']:' — anything else with >4 parts hits this branch.

Common situations: Adding an extra hop or protocol suffix by mistake; unbracketed IPv6 addresses (the classic — every v6 literal adds 7+ colons); appending a trailing separator in generated config.

Related errors


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