nats-io/nats-server · error

%w: invalid protocol %s

Error message

%w: invalid protocol %s

What it means

The first field of the v1 header was neither TCP4, TCP6, nor the earlier-handled UNKNOWN. The PROXY protocol v1 spec only defines those keywords, so any other protocol token (e.g. UDP4, TCP, or garbage) makes the header invalid and the server rejects it with errProxyProtoInvalid, including the offending value in the message.

Source

Thrown at server/client_proxyproto.go:222

	if err != nil {
		return nil, nil, fmt.Errorf("invalid dest port: %w", err)
	}

	// Validate protocol matches IP version. The textual form determines the
	// family: TCP4 requires dotted-quad addresses, TCP6 requires IPv6
	// addresses. IPv4-mapped IPv6 addresses (e.g. "::ffff:192.0.2.1") are
	// valid for TCP6 since dual-stack proxies can emit those for IPv4
	// clients on IPv6 sockets, matching the v2 parser behavior.
	srcIsV6 := strings.Contains(parts[1], ":")
	dstIsV6 := strings.Contains(parts[2], ":")
	if protocol == proxyProtoV1TCP4 && (srcIsV6 || dstIsV6) {
		return nil, nil, fmt.Errorf("%w: TCP4 with IPv6 address", errProxyProtoInvalid)
	}
	if protocol == proxyProtoV1TCP6 && (!srcIsV6 || !dstIsV6) {
		return nil, nil, fmt.Errorf("%w: TCP6 with IPv4 address", errProxyProtoInvalid)
	}
	if protocol != proxyProtoV1TCP4 && protocol != proxyProtoV1TCP6 {
		return nil, nil, fmt.Errorf("%w: invalid protocol %s", errProxyProtoInvalid, protocol)
	}

	return &proxyProtoAddr{
		srcIP:   srcIP,
		srcPort: uint16(srcPort),
		dstIP:   dstIP,
		dstPort: uint16(dstPort),
	}, remaining, nil
}

// readProxyProtoHeader reads and parses PROXY protocol (v1 or v2) from the connection.
// Automatically detects version and routes to appropriate parser.
// If the command is LOCAL/UNKNOWN (health check), it returns nil for addr and no error.
// If the command is PROXY, it returns the parsed address information.
// It also returns any bytes that were read past the v1 header terminator so the
// caller can replay them into the normal client parser.
// The connection must be fresh (no data read yet).
func readProxyProtoHeader(conn net.Conn) (*proxyProtoAddr, []byte, error) {

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Change the sender to emit only TCP4 or TCP6 (or UNKNOWN for health checks)
  2. Fix case-sensitivity: the keyword must be uppercase TCP4/TCP6
  3. If you need UDP proxying, this server does not support it via PROXY protocol v1 - use a supported transport
  4. Correct typo'd protocol keywords in test clients and templates

Example fix

// before
"PROXY UDP4 192.0.2.1 198.51.100.7 35646 4222\r\n"
// after
"PROXY TCP4 192.0.2.1 198.51.100.7 35646 4222\r\n"
Defensive patterns

Strategy: validation

Validate before calling

if proto != "TCP4" && proto != "TCP6" && proto != "UNKNOWN" {
    return fmt.Errorf("unsupported v1 protocol keyword %q; use TCP4/TCP6/UNKNOWN", proto)
}

Type guard

func isKnownV1Protocol(s string) bool {
    switch s {
    case "TCP4", "TCP6", "UNKNOWN":
        return true
    }
    return false
}

Try / catch

_, _, err := readProxyProtoHeader(conn)
if err != nil {
    if errors.Is(err, errProxyProtoInvalid) && strings.Contains(err.Error(), "invalid protocol") {
        log.Printf("peer sent unsupported v1 protocol keyword: %v", err)
        return
    }
    return err
}

Prevention

When it happens

Trigger: Header like 'PROXY UDP4 192.0.2.1 198.51.100.7 35646 4222\r\n' or 'PROXY tcp4 ...' (lowercase); reached only when there are exactly 5 fields and the keyword is not TCP4/TCP6/UNKNOWN.

Common situations: Proxy supporting UDP proxy-protocol extensions not supported by this server; case-sensitivity bugs in custom senders; test fixtures with typos like 'PROXY TCP 192.0.2.1 ...'.

Related errors


AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02). Data as JSON: /api/errors/512d605b6fde1bcf. Report an issue: GitHub.