go-proxyproto

repository·main·Indexed 20 days ago

https://github.com/pires/go-proxyproto

A Go implementation of the HAProxy PROXY protocol (v1 and v2) used to pass original client connection information (IP, port) through proxies and load balancers to backend applications. It provides tools for implementing PROXY protocol clients and servers, including a wrapped net.Listener, UDP datagram parsing, and trusted-source policies to secure listeners against untrusted networks.

Tokens
8.1K
Snippets
27
Records
37
Agent score
70%

What's inside go-proxyproto

  1. Handle PROXY protocol with UDP

    main

    The PROXY protocol for UDP requires a header in every datagram. Because net.Listener and net.Conn are designed for continuous streams, they cannot be used for UDP. Instead, use proxyproto.ParseUDPDatagram to read headers and Header.FormatUDPDatagram to prepare outgoing packets.

    Note: ParseUDPDatagram does not have a built-in trusted-source policy. You must manually verify the sender's address from net.PacketConn.ReadFrom before trusting the address returned by the header.

  2. Secure a listener with trusted-source policies

    main

    By default, proxyproto.Listener uses proxyproto.DefaultPolicy (which is REQUIRE). This requires a header but accepts it from any peer, which is unsafe for untrusted networks.

    To restrict header acceptance to specific trusted proxies, use ConnPolicy with TrustProxyHeaderFrom (for a single IP) or TrustProxyHeaderFromRanges (for CIDR ranges). For mixed traffic, use PolicyFromRanges to define different behaviors for matched and unmatched sources.

    proxyListener := &proxyproto.Listener{
    	Listener: ln,
    	// Connections from the load balancer must open with a PROXY header
    	// (REQUIRE); connections from any other source are dropped.
    	// For CIDR ranges, use TrustProxyHeaderFromRanges([]string{"10.0.0.0/24"}).
    	// For mixed traffic (e.g. optional headers from some sources), spell the
    	// two policies out with PolicyFromRanges(ranges, matched, unmatched).
    	ConnPolicy: proxyproto.TrustProxyHeaderFrom(net.ParseIP("10.0.0.10")),
    }
  3. Configure PROXY protocol with TLS

    main

    When using TLS, the order of wrapping depends on when the PROXY header is sent:

    1. Header before TLS (Cleartext): Wrap the PROXY listener inside the TLS listener. tls.NewListener(&proxyproto.Listener{Listener: l}, tlsConfig)

    2. Header inside TLS (Encrypted): Wrap the TLS listener inside the PROXY listener. &proxyproto.Listener{Listener: tls.NewListener(l, tlsConfig)}

  4. Configure connection policies for PROXY headers

    main

    The proxyproto package uses Policy and ConnPolicyFunc to determine how to handle PROXY protocol headers during a connection. You can define whether to trust, require, ignore, or reject headers based on the connection's source (upstream) or destination (downstream) addresses.

    Available Policies

    PolicyBehavior
    USEUse the address provided in the PROXY header.
    IGNOREAccept the connection but ignore the address in the PROXY header.
    REJECTReject the connection if a PROXY header is present.
    REQUIREReject the connection if a PROXY header is not present.
    SKIPAccept the connection without requiring a header. On a Listener, this short-circuits Accept and returns the raw connection. On a Conn, the header is consumed but discarded.

    Error Handling

    When a ConnPolicyFunc returns an error:

    • An error wrapping ErrInvalidUpstream denies only that specific connection while allowing the Listener.Accept loop to continue.
    • Any other error returned by the policy is returned by Accept itself, which typically stops the caller's accept loop.
    const (
    	USE    Policy = iota // Use address from PROXY header
    	IGNORE                // Ignore address from PROXY header, but accept connection
    	REJECT                // Reject connection when PROXY header is sent
    	REQUIRE               // Require connection to send PROXY header, reject if not present
    	SKIP                  // Skip PROXY header requirements
    )
  5. Configure PROXY protocol policies in Listener

    main

    The Listener uses policies to decide how to treat connections based on the presence and validity of PROXY protocol headers. You can provide either Policy or ConnPolicy (but not both).

    • Policy: A function that takes the net.Addr of the upstream (the proxy) and returns a ProxyHeaderPolicy.
    • ConnPolicy: A function that takes ConnPolicyOptions (containing both Upstream and Downstream addresses) and returns a ProxyHeaderPolicy.

    Policy Error Handling:

    • If a policy returns ErrInvalidUpstream, the Listener.Accept() method will close the connection and continue listening for the next connection.
    • If a policy returns any other error, Accept() will return that error, typically stopping the caller's accept loop.

    Common Policies (implied by usage):

    • REQUIRE: Connections must start with a PROXY header.
    • SKIP: Connections are treated as regular connections without PROXY headers.
    • REJECT: Connections attempting to send a PROXY header are rejected (returns ErrSuperfluousProxyHeader).
  6. Handle ErrInvalidUpstream for resilient listeners

    main

    When implementing a custom Policy or ConnPolicy for a proxyproto.Listener, you should wrap address-classification failures in ErrInvalidUpstream.

    This allows the Listener.Accept() loop to distinguish between a single unclassifiable peer (which should be ignored) and a fatal error that should stop the entire server. If your policy returns ErrInvalidUpstream, the listener will close that specific connection and keep listening for others.

    var ErrInvalidUpstream = fmt.Errorf("proxyproto: upstream connection address not trusted for PROXY information")
  7. Configure AWS Network Load Balancer (NLB) for PROXY protocol

    main

    AWS NLB may not send the PROXY v2 header until the client sends payload (default: on_first_ack_with_payload). This causes failures for server-first protocols like SMTP, FTP, or SSH.

    To fix this, change the target group attribute proxy_protocol_v2.client_to_server.header_placement to on_first_ack so the header arrives before the backend begins communication.

  8. Implement a PROXY protocol server

    main

    To receive PROXY protocol headers on a server, wrap your existing net.Listener with &proxyproto.Listener{}. When you call Accept(), the returned connection will have its RemoteAddr() populated with the client address extracted from the PROXY header.

    proxyListener := &proxyproto.Listener{Listener: ln}
    conn, err := proxyListener.Accept()
    // Connections must open with a PROXY header (the default policy is REQUIRE);
    // conn.RemoteAddr() then reports the client address from that header.
  9. Implement a PROXY protocol client

    main

    To send PROXY protocol headers from a client, create a header using proxyproto.HeaderProxyFromAddrs and write it to the connection using Header.WriteTo before sending any application data.

    header := proxyproto.HeaderProxyFromAddrs(1, sourceAddr, destinationAddr)
    _, err := header.WriteTo(conn) // write the PROXY header before application data
  10. Set the global DefaultPolicy

    main

    The DefaultPolicy variable determines the behavior when no explicit policy is configured. By default, it is set to REQUIRE to comply with the PROXY protocol specification.

    If you are migrating a deployment that relies on the older behavior where the PROXY header was optional, you can set this at program initialization:

    proxyproto.DefaultPolicy = proxyproto.USE

    Warning: This is a package-level variable. It must be set during init() or at the start of your program and must not be modified concurrently while accepting connections.

    proxyproto.DefaultPolicy = proxyproto.USE
  11. Extract address information from a Header

    main

    Once a Header is parsed, you can extract connection details using type-specific methods. These methods return a boolean ok to indicate if the header actually contains that type of information.

    • TCPAddrs(): Returns (*net.TCPAddr, *net.TCPAddr, bool) for stream-based TCP headers.
    • UDPAddrs(): Returns (*net.UDPAddr, *net.UDPAddr, bool) for datagram-based UDP headers.
    • UnixAddrs(): Returns (*net.UnixAddr, *net.UnixAddr, bool) for UNIX-based headers.
    • IPs(): Returns (net.IP, net.IP, bool) for TCP/UDP headers.
    • Ports(): Returns (int, int, bool) for TCP/UDP headers.
    if src, dest, ok := header.TCPAddrs(); ok {
        fmt.Printf("Source: %s, Dest: %s\n", src, dest)
    }
    
    if srcIP, destIP, ok := header.IPs(); ok {
        fmt.Printf("Source IP: %s, Dest IP: %s\n", srcIP, destIP)
    }