netbirdio/netbird · error

invalid IPv6 port forward specification: %s (expected [ipv6]

Error message

invalid IPv6 port forward specification: %s (expected [ipv6]:port:host:hostport)

What it means

Returned by parseIPv6ForwardSpec when the bracketed IPv6 host was split off successfully (a ']:' was found) but the remainder does not split into exactly three colon-separated parts — local port, remote host, remote port. The expected full grammar is [ipv6]:port:host:hostport, so remainders with 2 or 4+ parts are rejected with this format-hint message.

Source

Thrown at client/cmd/ssh.go:777

	localHost := normalizeLocalHost(parts[0])
	localAddr := localHost + ":" + parts[1]
	remoteAddr := parts[2] + ":" + parts[3]
	return localAddr, remoteAddr, nil
}

// parseIPv6ForwardSpec handles "[host]:port:host:hostport" format.
func parseIPv6ForwardSpec(spec string) (string, string, error) {
	idx := strings.Index(spec, "]:")
	if idx == -1 {
		return "", "", fmt.Errorf("invalid IPv6 port forward specification: %s", spec)
	}

	ipv6Host := spec[:idx+1]
	remaining := spec[idx+2:]

	parts := strings.Split(remaining, ":")
	if len(parts) != 3 {
		return "", "", fmt.Errorf("invalid IPv6 port forward specification: %s (expected [ipv6]:port:host:hostport)", spec)
	}

	localAddr := ipv6Host + ":" + parts[0]
	remoteAddr := parts[1] + ":" + parts[2]
	return localAddr, remoteAddr, nil
}

// isUnixSocket checks if a path is a Unix socket path.
func isUnixSocket(path string) bool {
	return strings.HasPrefix(path, "/") || strings.HasPrefix(path, "./")
}

// normalizeLocalHost converts "*" to "" for binding to all interfaces (dual-stack).
func normalizeLocalHost(host string) string {
	if host == "*" {
		return ""
	}
	return host

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Use exactly three parts after the bracketed host: [v6]:local_port:remote_host:remote_port, with the remote host as a name or IPv4 (e.g., [::1]:8080:10.10.0.5:80).
  2. Give the remote side its explicit numeric port — the IPv6 form has no shorter variant.
  3. If the destination must be IPv6, note the parser's limit and route via a name or an IPv4-mapped intermediate instead of a bracketed remote literal.

Example fix

# before
netbird ssh -L '[::1]:8080:host' peer1
# -> invalid IPv6 port forward specification: 8080:host (expected [ipv6]:port:host:hostport)

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

Strategy: validation

Validate before calling

// after splitting off [v6]:, the remainder must be exactly port:host:port
rem := spec[idx+2:]
parts := strings.Split(rem, ":")
if len(parts) != 3 {
	return fmt.Errorf("after [v6]: expected port:host:hostport, got %d parts in %q", len(parts), rem)
}
if p, _ := strconv.Atoi(parts[0]); p < 1 || p > 65535 { return fmt.Errorf("bad local port") }
if p, _ := strconv.Atoi(parts[2]); p < 1 || p > 65535 { return fmt.Errorf("bad remote port") }

Type guard

func isCompleteV6Forward(s string) bool {
	i := strings.Index(s, "]:")
	if i == -1 {
		return false
	}
	return len(strings.Split(s[i+2:], ":")) == 3
}

Try / catch

if len(parts) != 3 {
	// remainder over/under-segmented: missing remote port is the common case;
	// surface the expected [ipv6]:port:host:hostport shape verbatim
}

Prevention

When it happens

Trigger: `-L [::1]:8080:host peer` (remainder `8080:host` — 2 parts, remote port missing), `-L [::1]:8080:host:80:extra peer` (4 parts), or a remainder containing another unbracketed IPv6 literal such as `[::1]:8080:2001:db8::1:80`, which splits into more than three parts and fails.

Common situations: Remembering to bracket the local v6 host but not the remote one; omitting the remote port in the IPv6 form specifically; extra segments from templating. A remote IPv6 destination cannot actually be expressed in this grammar — only the local bind side supports brackets.

Related errors


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