package proxyprotoimport ()const ( crlf = "\r\n" separator = " ")func initVersion1() *Header { := new(Header) .Version = 1// Command doesn't exist in v1 .Command = PROXYreturn}func parseVersion1( *bufio.Reader) (*Header, error) {//The header cannot be more than 107 bytes long. Per spec: // // (...) // - worst case (optional fields set to 0xff) : // "PROXY UNKNOWN ffff:f...f:ffff ffff:f...f:ffff 65535 65535\r\n" // => 5 + 1 + 7 + 1 + 39 + 1 + 39 + 1 + 5 + 1 + 5 + 2 = 107 chars // // So a 108-byte buffer is always enough to store all the line and a // trailing zero for string processing. // // It must also be CRLF terminated, as above. The header does not otherwise // contain a CR or LF byte. // // ISSUE #69 // We can't use Peek here as it will block trying to fill the buffer, which // will never happen if the header is TCP4 or TCP6 (max. 56 and 104 bytes // respectively) and the server is expected to speak first. // // Similarly, we can't use ReadString or ReadBytes as these will keep reading // until the delimiter is found; an abusive client could easily disrupt a // server by sending a large amount of data that do not contain a LF byte. // Another means of attack would be to start connections and simply not send // data after the initial PROXY signature bytes, accumulating a large // number of blocked goroutines on the server. ReadSlice will also block for // a delimiter when the internal buffer does not fill up. // // A plain Read is also problematic since we risk reading past the end of the // header without being able to easily put the excess bytes back into the reader's // buffer (with the current implementation's design). // // So we use a ReadByte loop, which solves the overflow problem and avoids // reading beyond the end of the header. However, we need one more trick to harden // against partial header attacks (slow loris) - per spec: // // (..) The sender must always ensure that the header is sent at once, so that // the transport layer maintains atomicity along the path to the receiver. The // receiver may be tolerant to partial headers or may simply drop the connection // when receiving a partial header. Recommendation is to be tolerant, but // implementation constraints may not always easily permit this. // // We are subject to such implementation constraints. So we return an error if // the header cannot be fully extracted with a single read of the underlying // reader. := make([]byte, 0, 107)for { , := .ReadByte()if != nil {returnnil, fmt.Errorf("%w: %w", ErrCantReadVersion1Header, ) } = append(, )if == '\n' {// End of header foundbreak }iflen() == 107 {// No delimiter in first 107 bytesreturnnil, ErrVersion1HeaderTooLong }if .Buffered() == 0 {// Header was not buffered in a single read. Since we can't // differentiate between genuine slow writers and DoS agents, // we abort. On healthy networks, this should never happen.returnnil, ErrCantReadVersion1Header } }// Check for CR before LF.iflen() < 2 || [len()-2] != '\r' {returnnil, ErrLineMustEndWithCrlf }// Check full signature. := strings.Split(string([:len()-2]), separator)// Expect at least 2 tokens: "PROXY" and the transport protocol.iflen() < 2 {returnnil, ErrCantReadAddressFamilyAndProtocol }// Read address family and protocolvarAddressFamilyAndProtocolswitch [1] {case"TCP4": = TCPv4case"TCP6": = TCPv6case"UNKNOWN": = UNSPEC// doesn't exist in v1 but fits UNKNOWNdefault:returnnil, ErrCantReadAddressFamilyAndProtocol }// Expect 6 tokens only when UNKNOWN is not present.if != UNSPEC && len() < 6 {returnnil, ErrCantReadAddressFamilyAndProtocol }// When a signature is found, allocate a v1 header with Command set to PROXY. // Command doesn't exist in v1 but set it for other parts of this library // to rely on it for determining connection details. := initVersion1()// Transport protocol has been processed already. .TransportProtocol = // When UNKNOWN, set the command to LOCAL and return earlyif .TransportProtocol == UNSPEC { .Command = LOCALreturn , nil }// Otherwise, continue to read addresses and ports , := parseV1IPAddress(.TransportProtocol, [2])if != nil {returnnil, } , := parseV1IPAddress(.TransportProtocol, [3])if != nil {returnnil, } , := parseV1PortNumber([4])if != nil {returnnil, } , := parseV1PortNumber([5])if != nil {returnnil, } .SourceAddr = &net.TCPAddr{IP: ,Port: , } .DestinationAddr = &net.TCPAddr{IP: ,Port: , }return , nil}func ( *Header) () ([]byte, error) {// As of version 1, only "TCP4" ( \x54 \x43 \x50 \x34 ) for TCP over IPv4, // and "TCP6" ( \x54 \x43 \x50 \x36 ) for TCP over IPv6 are allowed.varstringswitch .TransportProtocol {caseTCPv4: = "TCP4"caseTCPv6: = "TCP6"default:// Unknown connection (short form)return []byte("PROXY UNKNOWN" + crlf), nil } , := .SourceAddr.(*net.TCPAddr) , := .DestinationAddr.(*net.TCPAddr)if ! || ! {returnnil, ErrInvalidAddress } , := .IP, .IPswitch .TransportProtocol {caseTCPv4: = .To4() = .To4()caseTCPv6: = .To16() = .To16() }if == nil || == nil {returnnil, ErrInvalidAddress } := bytes.NewBuffer(make([]byte, 0, 108)) .Write(SIGV1) .WriteString(separator) .WriteString() .WriteString(separator) .WriteString(.String()) .WriteString(separator) .WriteString(.String()) .WriteString(separator) .WriteString(strconv.Itoa(.Port)) .WriteString(separator) .WriteString(strconv.Itoa(.Port)) .WriteString(crlf)return .Bytes(), nil}func parseV1PortNumber( string) (int, error) { , := strconv.Atoi()if != nil {return0, fmt.Errorf("%w: %w", ErrInvalidPortNumber, ) }if < 0 || > 65535 {return0, ErrInvalidPortNumber }return , nil}func parseV1IPAddress( AddressFamilyAndProtocol, string) (net.IP, error) { , := netip.ParseAddr()if != nil {returnnil, fmt.Errorf("%w: %w", ErrInvalidAddress, ) }switch {caseTCPv4:if .Is4() {returnnet.IP(.AsSlice()), nil }caseTCPv6:// Some proxies (notably nginx OSS stream module) emit plain IPv4 // addresses in TCP6 headers when the backend is IPv4 but the client // is IPv6. Promote to IPv4-mapped IPv6 for interoperability. // // This is an intentional departure from the PROXY protocol v1 spec, // which states that addresses in a TCP6 line must be in IPv6 format.if .Is6() || .Is4In6() {returnnet.IP(.AsSlice()), nil }// ATTENTION: 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".if .Is4() { := netip.AddrFrom16(.As16())returnnet.IP(.AsSlice()), nil } }returnnil, ErrInvalidAddress}
The pages are generated with Goldsv0.8.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.