Skip to content

v1: accept plain IPv4 addresses in TCP6 headers#167

Merged
pires merged 3 commits intopires:mainfrom
cmarker-gl:cm-fix-v1-tcp6-ipv4-addr
Apr 15, 2026
Merged

v1: accept plain IPv4 addresses in TCP6 headers#167
pires merged 3 commits intopires:mainfrom
cmarker-gl:cm-fix-v1-tcp6-ipv4-addr

Conversation

@cmarker-gl
Copy link
Copy Markdown
Contributor

@cmarker-gl cmarker-gl commented Mar 18, 2026

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 in 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 netip.AddrFrom16(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.

Note: this is a lossy conversion -- round-trip serialization will render the address as ::ffff:x.x.x.x rather than the original x.x.x.x.

Tests

  • Added "TCP6 with invalid address" test case (PROXY TCP6 not-an-ip ::1 1234 5678)
  • Added three new valid parse cases covering mixed address families in TCP6 headers:
    • PROXY TCP6 192.0.2.1 192.0.2.2 1234 5678 -- IPv4 src, IPv4 dst
    • PROXY TCP6 2001:db8::1 192.0.2.1 51512 22 -- IPv6 src, IPv4 dst (the nginx mixed case)
    • PROXY TCP6 192.0.2.1 2001:db8::1 51512 22 -- IPv4 src, IPv6 dst
  • All mixed-address test cases use skipWrite: true since round-trip serialization changes the address representation
  • Removed duplicate test fixtures that were identical to existing fixtureTCP4V1 and fixtureTCP6V1

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

@cmarker-gl
Copy link
Copy Markdown
Contributor Author

@pires can you take a look at this PR and let me know what you think?

@pires
Copy link
Copy Markdown
Owner

pires commented Mar 23, 2026

Hi @cmarker-gl sorry but I was AFK past week. I now have some work to attend to, and will come back to you soon.

Copy link
Copy Markdown
Owner

@pires pires left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all, thank you for your contribution. And then thank you for your patience, I have been immensely busy.

Please, fix the linter complain(s) and add tests for the following scenarios:

  • IPv4 src, IPv4 dst in TCP6: PROXY TCP6 192.0.2.1 192.0.2.2 1234 5678
  • IPv4 src, IPv6 dst in TCP6: the reversed mixed case

Comment thread v1_test.go
Comment thread v1_test.go Outdated
Comment thread v1_test.go
Comment thread v1.go Outdated
Comment thread v1.go
Comment thread v1.go
The remote address is logged intentionally in this example server.
@coveralls
Copy link
Copy Markdown

coveralls commented Apr 14, 2026

Coverage Status

Coverage is 95.717%cmarker-gl:cm-fix-v1-tcp6-ipv4-addr into pires:main. No base build found for pires:main.

- Add spec-departure and lossy-conversion comments per reviewer
- Refactor Is4() branch to use netip.AddrFrom16 for consistency
- Remove duplicate test fixtures (TCP4 simple, TCP6 loopback)
- Add TCP6 invalid address test case
- Add IPv4 src + IPv4 dst and IPv4 src + IPv6 dst test scenarios
@cmarker-gl
Copy link
Copy Markdown
Contributor Author

Thanks for the review! I've pushed two commits addressing your feedback:

Linter fix (41f2c96): Added a //nolint:gosec directive for the G706 finding in examples/httpserver/httpserver.go since logging the remote address is intentional in that example.

Review feedback (aa3ddcd): Applied all your suggestions (comments, netip.AddrFrom16 refactor, removed duplicate fixtures, reworked the invalid-address test for TCP6, added the two mixed-address test scenarios). Updated the PR description as well.

Copy link
Copy Markdown
Owner

@pires pires left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a ton for your contribution 🙇🏻

@pires pires merged commit 0cee3e4 into pires:main Apr 15, 2026
8 checks passed
@cmarker-gl
Copy link
Copy Markdown
Contributor Author

Appreciate the merge! Just need a version tag and release pushed to wrap this up. Let me know if you need anything from my end.

@pires
Copy link
Copy Markdown
Owner

pires commented Apr 15, 2026

🙇🏻‍♂️ https://github.com/pires/go-proxyproto/releases/tag/v0.12.0

rbqvq added a commit to rbqvq/go-proxyproto that referenced this pull request Apr 17, 2026
The standard `net.IP` implementation can not represent IPv4-mapped IPv6 addresses, `String()` on IPv4-mapped-IPv6 addresses will return IPv4 addresses, which may lead to formatting issues in v1 headers [1].

Migrate from `net.IP` to `netip.Addr` for IP state management to ensure `String()` outputs strictly follow the expected version format.

Link:
[1]: pires#167
rbqvq added a commit to rbqvq/go-proxyproto that referenced this pull request Apr 17, 2026
The standard `net.IP` implementation can not represent IPv4-mapped IPv6 addresses, `String()` on IPv4-mapped-IPv6 addresses will return IPv4 addresses, which may lead to formatting issues in v1 headers [1].

Migrate from `net.IP` to `netip.Addr` for IP state management to ensure `String()` outputs strictly follow the expected version format.

Link:
[1]: pires#167

Signed-off-by: Coia Prant <coiaprant@gmail.com>
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.

3 participants