nats-io/nats-server · error

%w: TCP4 with IPv6 address

Error message

%w: TCP4 with IPv6 address

What it means

The header declared protocol TCP4 but at least one of the address fields contains a colon, indicating an IPv6 literal. The PROXY protocol spec requires TCP4 to carry IPv4 dotted-quad addresses for both source and destination, so the server rejects the header with errProxyProtoInvalid.

Source

Thrown at server/client_proxyproto.go:216

	srcPort, err := strconv.ParseUint(parts[3], 10, 16)
	if err != nil {
		return nil, nil, fmt.Errorf("invalid source port: %w", err)
	}

	dstPort, err := strconv.ParseUint(parts[4], 10, 16)
	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.

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Make the proxy emit TCP6 when addresses are IPv6 literals
  2. If the address is IPv4-mapped IPv6, use TCP6 (the server accepts '::ffff:x.x.x.x' for TCP6) or rewrite to a plain IPv4 literal with TCP4
  3. Fix the proxy's family-detection logic to derive the protocol keyword from the actual socket family
  4. Update hardcoded 'TCP4' templates used in dual-stack environments

Example fix

// before
"PROXY TCP4 2001:db8::1 2001:db8::2 35646 4222\r\n"
// after
"PROXY TCP6 2001:db8::1 2001:db8::2 35646 4222\r\n"
Defensive patterns

Strategy: validation

Validate before calling

// Before sending, derive the keyword from the address family:
proto := "TCP4"
if strings.Contains(srcIPStr, ":") || strings.Contains(dstIPStr, ":") {
    proto = "TCP6"
}
header := fmt.Sprintf("PROXY %s %s %s %d %d\r\n", proto, srcIPStr, dstIPStr, sport, dport)

Type guard

func protoMatchesFamily(proto, src, dst string) bool {
    srcV6, dstV6 := strings.Contains(src, ":"), strings.Contains(dst, ":")
    if proto == "TCP4" { return !srcV6 && !dstV6 }
    if proto == "TCP6" { return srcV6 && dstV6 }
    return false
}

Try / catch

_, _, err := readProxyProtoHeader(conn)
if err != nil {
    if errors.Is(err, errProxyProtoInvalid) && strings.Contains(err.Error(), "TCP4 with IPv6") {
        log.Printf("peer sent TCP4 with IPv6 address: %v", err)
        return
    }
    return err
}

Prevention

When it happens

Trigger: Header like 'PROXY TCP4 2001:db8::1 198.51.100.7 35646 4222\r\n' (or with an IPv6 destination); any colon in either IP field while parts[0] == 'TCP4'.

Common situations: Dual-stack proxy hardcoding TCP4 while its sockets are IPv6; proxy behind an IPv6-only upstream; template not switching protocol keyword based on address family; NAT64/mapped-address setups emitting v6 literals under TCP4.

Related errors


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