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_host:remote_port or [local_host:]local_port:unix_socket)

What it means

Returned by parseTwoPartForwardSpec when the spec has exactly two colon parts but the second is not a unix socket path (does not start with / or ./). Two-part specs are only legal as `port:/abs/path` or `port:./rel/path`; a two-part `port:host` form has no meaning because the remote port would be missing, so it is rejected with the full expected-format message.

Source

Thrown at client/cmd/ssh.go:741

	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]
		remoteAddr := parts[2]
		return localAddr, remoteAddr, nil
	}
	localAddr := "localhost:" + parts[0]
	remoteAddr := parts[1] + ":" + parts[2]
	return localAddr, remoteAddr, nil
}

// parseFourPartForwardSpec handles "host:port:host:hostport" format.
func parseFourPartForwardSpec(parts []string) (string, string, error) {
	localHost := normalizeLocalHost(parts[0])

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. If the target is TCP, write the full 3-part form: port:host:remote_port (e.g., 8080:localhost:80).
  2. If the target is a unix socket, keep the path absolute (8080:/run/svc.sock) or ./-prefixed so isUnixSocket recognizes it.
  3. Avoid guessing defaults — every destination needs its explicit port or path.

Example fix

# before
netbird ssh -L 8080:localhost peer1
# -> invalid port forward specification: 8080:localhost (expected format: [local_host:]local_port:remote_host:remote_port or [local_host:]local_port:unix_socket)

# after
netbird ssh -L 8080:localhost:80 peer1
# or, for a socket:
netbird ssh -L 8080:/var/run/service.sock peer1
Defensive patterns

Strategy: validation

Validate before calling

// two-part specs are ONLY port + unix socket
parts := strings.Split(spec, ":")
if len(parts) == 2 {
	if !strings.HasPrefix(parts[1], "/") && !strings.HasPrefix(parts[1], "./") {
		return fmt.Errorf("2-part spec %q needs a socket path as target or a 3rd part (host:port)", spec)
	}
}

Type guard

func isSocketTarget(s string) bool { return strings.HasPrefix(s, "/") || strings.HasPrefix(s, "./") }

Try / catch

if len(parts) == 2 && !isUnixSocket(parts[1]) {
	// host-only second part: auto-expand to 3 parts with the known service port
	// or reject — never guess silently in production paths
}

Prevention

When it happens

Trigger: `-L 8080:localhost peer` (host without port, hoping the default applies), `-R 9000:service peer`, or `-L 8080:socket peer` where the intended socket path lost its leading slash. Any 2-part spec whose second element fails the isUnixSocket prefix check.

Common situations: Abbreviating to host-only when the remote port is 'obvious' (db -> 5432); socket paths passed relative without ./ ; typos dropping the port after editing a longer spec down.

Related errors


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