This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+90 -17
View File
@@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"fmt"
"math"
"net"
"net/netip"
"strconv"
@@ -13,8 +14,29 @@ import (
const (
crlf = "\r\n"
separator = " "
// v1ProtoTCP4 and v1ProtoTCP6 are the PROXY protocol v1 transport-protocol
// tokens for TCP over IPv4 and IPv6 respectively (net has no constants for
// these textual identifiers).
v1ProtoTCP4 = "TCP4"
v1ProtoTCP6 = "TCP6"
)
// V1AcceptIPv4InTCP6 permits plain IPv4 literals in the address fields of a v1
// TCP6 line, promoting them to v4-mapped IPv6 (::ffff:x.x.x.x). Some proxies
// (notably the nginx OSS stream module) emit such lines when the client and
// backend address families differ. The spec forbids them — "the advertised
// protocol family dictates what format to use" — so this compatibility mode is
// disabled by default.
//
// Note the promotion is lossy: round-trip serialization renders the address as
// "::ffff:x.x.x.x" rather than the original "x.x.x.x".
//
// Like DefaultReadHeaderTimeout, this is a package-level variable to keep it
// easy to override. Set it at program init; it must not be modified
// concurrently with parsing.
var V1AcceptIPv4InTCP6 = false
func initVersion1() *Header {
header := new(Header)
header.Version = 1
@@ -71,7 +93,7 @@ func parseVersion1(reader *bufio.Reader) (*Header, error) {
for {
b, err := reader.ReadByte()
if err != nil {
return nil, fmt.Errorf(ErrCantReadVersion1Header.Error()+": %v", err)
return nil, fmt.Errorf("%w: %w", ErrCantReadVersion1Header, err)
}
buf = append(buf, b)
if b == '\n' {
@@ -95,8 +117,15 @@ func parseVersion1(reader *bufio.Reader) (*Header, error) {
return nil, ErrLineMustEndWithCrlf
}
// Check full signature.
// Check full signature. Read dispatches to this parser after peeking only
// the first 5 bytes ("PROXY"), so the first token must still be checked to
// be exactly "PROXY": per spec the line starts with "PROXY" followed by
// exactly one space, and anything else (e.g. "PROXYjunk TCP4 ...") is not a
// v1 header.
tokens := strings.Split(string(buf[:len(buf)-2]), separator)
if tokens[0] != "PROXY" {
return nil, ErrCantReadVersion1Header
}
// Expect at least 2 tokens: "PROXY" and the transport protocol.
if len(tokens) < 2 {
@@ -106,9 +135,9 @@ func parseVersion1(reader *bufio.Reader) (*Header, error) {
// Read address family and protocol
var transportProtocol AddressFamilyAndProtocol
switch tokens[1] {
case "TCP4":
case v1ProtoTCP4:
transportProtocol = TCPv4
case "TCP6":
case v1ProtoTCP6:
transportProtocol = TCPv6
case "UNKNOWN":
transportProtocol = UNSPEC // doesn't exist in v1 but fits UNKNOWN
@@ -116,8 +145,11 @@ func parseVersion1(reader *bufio.Reader) (*Header, error) {
return nil, ErrCantReadAddressFamilyAndProtocol
}
// Expect 6 tokens only when UNKNOWN is not present.
if transportProtocol != UNSPEC && len(tokens) < 6 {
// Expect exactly 6 tokens when UNKNOWN is not present. The spec's TCP4/TCP6
// line is "PROXY <proto> <src> <dst> <sport> <dport>"; trailing tokens are
// not permitted. (UNKNOWN is handled leniently below, per spec: the receiver
// must ignore anything up to the CRLF.)
if transportProtocol != UNSPEC && len(tokens) != 6 {
return nil, ErrCantReadAddressFamilyAndProtocol
}
@@ -170,9 +202,9 @@ func (header *Header) formatVersion1() ([]byte, error) {
var proto string
switch header.TransportProtocol {
case TCPv4:
proto = "TCP4"
proto = v1ProtoTCP4
case TCPv6:
proto = "TCP6"
proto = v1ProtoTCP6
default:
// Unknown connection (short form)
return []byte("PROXY UNKNOWN" + crlf), nil
@@ -184,16 +216,35 @@ func (header *Header) formatVersion1() ([]byte, error) {
return nil, ErrInvalidAddress
}
sourceIP, destIP := sourceAddr.IP, destAddr.IP
// A hand-built header can carry any int; the spec requires ports in the
// decimal range 0..65535, so validate before serializing (mirrors the v2
// port check in formatVersion2).
if sourceAddr.Port < 0 || sourceAddr.Port > math.MaxUint16 || destAddr.Port < 0 || destAddr.Port > math.MaxUint16 {
return nil, ErrInvalidPortNumber
}
// netip.Addr (not net.IP) is used here so String() honors the address family
// declared by TransportProtocol. AddrFromSlice reports ok=false when the slice
// is nil (e.g. To4() on an IPv6-only address), which the guard below rejects.
var sourceIP, destIP netip.Addr
switch header.TransportProtocol {
case TCPv4:
sourceIP = sourceIP.To4()
destIP = destIP.To4()
sourceIP, sourceOK = netip.AddrFromSlice(sourceAddr.IP.To4())
destIP, destOK = netip.AddrFromSlice(destAddr.IP.To4())
case TCPv6:
sourceIP = sourceIP.To16()
destIP = destIP.To16()
// Use netip.Addr instead of net.IP to guarantee an Is6() address; i.e. a
// v4-mapped IP in a TCP6 header serializes as ::ffff:1.2.3.4 instead of
// net.IP.String()'s collapsed 1.2.3.4.
sourceIP, sourceOK = netip.AddrFromSlice(sourceAddr.IP.To16())
destIP, destOK = netip.AddrFromSlice(destAddr.IP.To16())
default:
// Unreachable today: the proto switch at the top of this function already
// returns for anything other than TCPv4/TCPv6. Kept so a future protocol
// can't fall through with zero-value IPs while sourceOK/destOK still hold
// from the type assertion above.
return nil, ErrInvalidAddress
}
if sourceIP == nil || destIP == nil {
if !sourceOK || !destOK {
return nil, ErrInvalidAddress
}
@@ -215,16 +266,32 @@ func (header *Header) formatVersion1() ([]byte, error) {
}
func parseV1PortNumber(portStr string) (int, error) {
port, err := strconv.Atoi(portStr)
if err != nil || port < 0 || port > 65535 {
// Per spec, a v1 port is 1..5 decimal digits in the range 0..65535, with no
// leading zero and no sign. ParseUint with bitSize 16 enforces all of that
// (it rejects empty strings, signs, non-digits, and values above 65535)
// except the leading zero ("080"), which the spec forbids and which creates
// ambiguity for anything downstream that re-parses the address.
if len(portStr) > 1 && portStr[0] == '0' {
return 0, ErrInvalidPortNumber
}
return port, nil
port, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return 0, fmt.Errorf("%w: %w", ErrInvalidPortNumber, err)
}
return int(port), nil
}
func parseV1IPAddress(protocol AddressFamilyAndProtocol, addrStr string) (net.IP, error) {
addr, err := netip.ParseAddr(addrStr)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrInvalidAddress, err)
}
// netip accepts zoned literals ("fe80::1%eth0"). The spec's address grammar
// does not (hex digits and colons only), and net.IP cannot carry a zone, so
// accepting one would silently forward an address the sender never wrote.
// Per spec, "any sequence which does not exactly match the protocol must be
// discarded".
if addr.Zone() != "" {
return nil, ErrInvalidAddress
}
@@ -237,6 +304,12 @@ func parseV1IPAddress(protocol AddressFamilyAndProtocol, addrStr string) (net.IP
if addr.Is6() || addr.Is4In6() {
return net.IP(addr.AsSlice()), nil
}
// Plain IPv4 in a TCP6 line is spec-invalid but emitted by some proxies;
// see V1AcceptIPv4InTCP6 for the compatibility trade-off.
if V1AcceptIPv4InTCP6 && addr.Is4() {
mapped := netip.AddrFrom16(addr.As16())
return net.IP(mapped.AsSlice()), nil
}
}
return nil, ErrInvalidAddress