netbirdio/netbird · error

invalid port forward specification: %s (expected format: [lo

Error message

invalid port forward specification: %s (expected format: [local_host:]local_port:remote_target)

What it means

Returned by parsePortForwardSpec when splitting the spec on ':' yields fewer than two parts — i.e., the spec contains no colon at all. The message embeds the expected grammar `[local_host:]local_port:remote_target`. This fires before the 2/3/4-part handlers run, so a single-token spec like `8080` never reaches socket/port validation.

Source

Thrown at client/cmd/ssh.go:719

}

// parsePortForwardSpec parses port forward specifications like "8080:localhost:80" or "[::1]:8080:localhost:80".
// Also supports Unix sockets like "8080:/tmp/socket" or "127.0.0.1:8080:/tmp/socket".
func parsePortForwardSpec(spec string) (string, string, error) {
	// Support formats:
	// port:host:hostport  -> localhost:port -> host:hostport
	// host:port:host:hostport  -> host:port -> host:hostport
	// [host]:port:host:hostport -> [host]:port -> host:hostport
	// port:unix_socket_path -> localhost:port -> unix_socket_path
	// host:port:unix_socket_path -> host:port -> unix_socket_path

	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]

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Write the spec with at least two colon-separated parts: for -L the minimum full form is local_port:remote_host:remote_port (or port:/unix/path for sockets).
  2. Quote the argument: -L "8080:localhost:80" so no shell or config layer can split it.
  3. If the value comes from config, print it once at render time to confirm colons survived.

Example fix

# before
netbird ssh -L 8080 peer1
# -> invalid port forward specification: 8080 (expected format: [local_host:]local_port:remote_target)

# after
netbird ssh -L 8080:localhost:80 peer1
Defensive patterns

Strategy: validation

Validate before calling

if !strings.Contains(spec, ":") {
	return fmt.Errorf("forward spec %q must contain at least one ':' (port:target...)", spec)
}

Type guard

func hasForwardColon(s string) bool { return strings.Count(s, ":") >= 1 }

Try / catch

if len(parts) < 2 {
	// spec had no colon at all: treat as a construction bug upstream,
	// log the raw spec + its source (env var/config key) for tracing
}

Prevention

When it happens

Trigger: `netbird ssh -L 8080 peer` (a bare port with no colon), `-R 9000` , or a spec whose colon was eaten by shell parsing/quoting so only one field remains. Applies identically to -L and -R since both call parsePortForwardSpec first.

Common situations: Assuming the flag takes just a port and the target is implied; YAML/JSON or env-file values losing the colon; HCL/terraform heredocs interpreting `8080:host:80` and emitting only the first token; muscle memory from tools with different flag shapes.

Related errors


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