Skip to content

v1: accept plain IPv4 addresses in TCP6 headers#167

Open
cmarker-gl wants to merge 1 commit intopires:mainfrom
cmarker-gl:cm-fix-v1-tcp6-ipv4-addr
Open

v1: accept plain IPv4 addresses in TCP6 headers#167
cmarker-gl wants to merge 1 commit intopires:mainfrom
cmarker-gl:cm-fix-v1-tcp6-ipv4-addr

Conversation

@cmarker-gl
Copy link

Problem

When proxying SSH connections through a chain involving nginx's OSS stream module, PROXY protocol v1 headers with mixed address families are emitted and currently rejected by this library.

The chain looks like:

IPv6 client → Cloudflare Spectrum (PROXY v2) → nginx stream (decodes v2, re-encodes as v1) → application (go-proxyproto)

nginx's OSS stream module (proxy_protocol on) always re-encodes as PROXY protocol v1. When the original client is IPv6 but the backend is IPv4, nginx emits:

PROXY TCP6 2001:db8::1 192.0.2.1 51512 22\r\n

This is technically outside the strict spec (which says both addresses in a TCP6 header should be IPv6 format), but it is the real-world output of a widely deployed proxy and there is no workaround available in nginx OSS.

go-proxyproto was returning proxyproto: invalid address for these headers, causing the connection to fail. IPv4-only connections worked fine.

Root Cause

parseV1IPAddress in v1.go handled TCPv6 by accepting addresses where addr.Is6() or addr.Is4In6() (i.e. ::ffff:x.x.x.x notation). A plain IPv4 address like 192.0.2.1 parses as addr.Is4() — neither condition matched, so it fell through to ErrInvalidAddress.

Fix

When parsing a TCPv6 header and the address is a plain IPv4, promote it to a 16-byte IPv4-mapped-IPv6 representation via addr.As16(). This is consistent with how net.IP.To16() works elsewhere in the library and produces a valid net.IP that callers can use normally.

if addr.Is4() {
    a16 := addr.As16()
    return net.IP(a16[:]), nil
}

Tests

  • Removed the previous "TCP6 with IPv4 addresses" invalid test, which was asserting the now-intentionally-relaxed behaviour
  • Added three new valid parse cases:
    • PROXY TCP4 1.2.3.4 5.6.7.8 12345 22\r\n — baseline IPv4
    • PROXY TCP6 2001:db8::1 192.0.2.1 51512 22\r\n — IPv6 src, IPv4 dst (the nginx mixed case); skipWrite: true since round-trip serialisation changes the destination representation
    • PROXY TCP6 ::1 ::1 1234 5678\r\n — loopback IPv6

Notes

  • PROXY protocol v2 is not affected — v2 is binary and reads fixed-width 16-byte address fields directly, so mixed address families cannot arise in the same way
  • No API changes; downstream consumers need only bump their dependency to pick up the fix
  • This code was developed in part with GitLab Duo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant