netbirdio/netbird · error

invalid local address: %w

Error message

invalid local address: %w

What it means

Wraps validateDestinationPort(localAddr) in the -R (remote forward) path. For remote forwards the spec is parsed as remoteAddr:localAddr and the validator is applied to the local address — the endpoint your machine will serve to the remote peer. It must be host:numeric-port (1-65535) or a unix socket path (leading / or ./).

Source

Thrown at client/cmd/ssh.go:660

	go func() {
		if err := c.LocalPortForward(ctx, localAddr, remoteAddr); err != nil && !errors.Is(err, context.Canceled) {
			cmd.Printf("Local port forward error: %v\n", err)
		}
	}()

	return nil
}

// parseAndStartRemoteForward parses and starts a remote port forward (-R)
func parseAndStartRemoteForward(ctx context.Context, c *sshclient.Client, forward string, cmd *cobra.Command) error {
	remoteAddr, localAddr, err := parsePortForwardSpec(forward)
	if err != nil {
		return err
	}

	if err := validateDestinationPort(localAddr); err != nil {
		return fmt.Errorf("invalid local address: %w", err)
	}

	log.Debugf("Remote port forwarding: %s -> %s", remoteAddr, localAddr)

	go func() {
		if err := c.RemotePortForward(ctx, remoteAddr, localAddr); err != nil && !errors.Is(err, context.Canceled) {
			cmd.Printf("Remote port forward error: %v\n", err)
		}
	}()

	return nil
}

// validateDestinationPort checks that the destination address has a valid port.
// Port 0 is only valid for bind addresses (where the OS picks an available port),
// not for destination addresses where we need to connect.
func validateDestinationPort(addr string) error {
	if strings.HasPrefix(addr, "/") || strings.HasPrefix(addr, "./") {

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Complete the spec with the local service's host and numeric port: -R 9000:127.0.0.1:8080.
  2. Never use 0 for the local port — the remote peer dials a concrete endpoint.
  3. Use only digits for the local port (no service names), 1-65535.
  4. For unix socket targets, keep the absolute or ./-prefixed path so validation skips the port checks.

Example fix

# before
netbird ssh -R 9000:localhost peer1
# -> start port forwarding: remote port forward 9000:localhost: invalid local address: parse address localhost: missing port in address

# after
netbird ssh -R 9000:127.0.0.1:8080 peer1
Defensive patterns

Strategy: validation

Validate before calling

// pre-check the -R local endpoint (what the remote peer dials back to)
func validateLocalTarget(addr string) error {
	if strings.HasPrefix(addr, "/") || strings.HasPrefix(addr, "./") {
		return nil
	}
	host, portStr, err := net.SplitHostPort(addr)
	if err != nil {
		return fmt.Errorf("local endpoint %q must be host:port", addr)
	}
	_ = host
	p, err := strconv.Atoi(portStr)
	if err != nil || p < 1 || p > 65535 {
		return fmt.Errorf("local port %q invalid (need 1-65535)", portStr)
	}
	return nil
}

Try / catch

if err := validateDestinationPort(localAddr); err != nil {
	return fmt.Errorf("invalid local address: %w", err)
	// the validated side is the LOCAL endpoint even though the flag is -R;
	// read the wrapped cause to see if it is shape (SplitHostPort) or value (port)
}

Prevention

When it happens

Trigger: `netbird ssh -R 9000:localhost peer` (local side missing its port), `-R 9000:localhost:0`, `-R 9000:localhost:99999`, `-R 9000:svc:https` (non-numeric), or a local endpoint that fails net.SplitHostPort such as a bare unbracketed IPv6 address.

Common situations: Thinking -R ends at the remote bind port and omitting the local service port; environment-variable-built specs with an empty local port; pointing the local side at a socket but forgetting the leading / (so it is treated as a host with a bad port).

Related errors


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