# go-proxyproto moves the real client address through your load balancer

> go-proxyproto is a Go implementation of the HAProxy PROXY protocol, versions 1 and 2, so a service behind a proxy can see the original client address instead of the proxy's. Its default policy refuses connections without a header, and its UDP story deliberately stops short of a PacketConn wrapper.

**pires/go-proxyproto** — A Go library implementation of the PROXY protocol, versions 1 and 2.

- Repository: https://github.com/pires/go-proxyproto
- Stars: 624 · Forks: 127
- Language: Go
- License: Apache-2.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/pires-go-proxyproto

## The problem is one NAT hop too many

The PROXY protocol exists because a load balancer breaks the one piece of information a service usually needs. When a TCP connection passes through a proxy, the backend sees the proxy's address, not the client's, so the application cannot log the real source, rate limit per client, or apply per client access rules.

go-proxyproto is a Go library implementation of the HAProxy PROXY protocol specification 3.4, covering protocol versions 1, the text form, and 2, the binary form. The readme's description of the job is that the protocol carries the original client connection information across NAT and proxy layers.

The design point is that the header is a preamble to the connection rather than something inside the application protocol. A TCP proxy that supports it writes a short header describing the real source and destination before passing the payload through, and the server reads the header, strips it, and then hands the application a connection whose RemoteAddr reports the original client. That last part is the whole trick, because from the application's point of view nothing else changes.

It is a small library with a small surface. The readme describes the API shape as small: send headers with Header.WriteTo or Header.FormatUDPDatagram, and receive them with Listener, NewConn, Read or ParseUDPDatagram. The stream APIs are for TCP and Unix streams, while UDP uses the packet helpers because the specification requires a header in every datagram.

## The client side writes a header before any payload

The client side is two lines. You build a header from a source and a destination address, then write it to the connection before any application data goes out.

```go
header := proxyproto.HeaderProxyFromAddrs(1, sourceAddr, destinationAddr)
_, err := header.WriteTo(conn) // write the PROXY header before application data
```

The version argument is 1 there, which selects the text form of the protocol. The library covers version 2 as well, the binary form, and the readme points at the runnable programs under the examples directory as the fastest way in, with a minimal client and a minimal server among them.

The ordering constraint is the thing to get right. The header has to be the first thing on the wire, before the protocol handshake, and the comment in the snippet says as much. There is no way to add it later, because a server that expects it is reading the first bytes of the connection.

For a project already using this library, the upgrade path is the Go module. The readme states that Go 1.26.9 or later is required and that CI tests Go 1.26 and 1.27, and the module file pins the language version and depends on golang.org/x/net for the HTTP/2 helper.

```shell
$ go get github.com/pires/go-proxyproto
```

## The server side is a Listener wrapper, and its default is strict

On the receiving side you wrap your existing listener and accept from the wrapper.

```go
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.
```

The default policy matters more than anything else in this library, and it is called out in a warning block near the top of the readme. With no policy configured, Listener and NewConn use proxyproto.DefaultPolicy, which is REQUIRE. A connection that does not open with a PROXY header fails its first I/O with ErrNoProxyProtocol.

That is a deliberate reading of the specification, which says receivers must not try to guess whether the header is present. A receiver that accepted a connection with no header would have to decide whether to treat the peer as the client, and that decision is guesswork an attacker can influence.

But REQUIRE alone is not a trust boundary, and the readme says so in plain words: REQUIRE still honors headers from any peer, so use it only when the listener is reachable exclusively by trusted proxies, such as a private network segment behind your load balancer. The zero-value configuration requires the header but still honors it from any peer, which makes it unsafe for listeners reachable by untrusted clients without a trusted-source policy.

If you want the older, optional-header behaviour, the readme lists three ways to get it: set proxyproto.DefaultPolicy to proxyproto.USE process-wide, use WithPolicy(USE) per connection, or supply a policy returning USE per listener.

## Exposed listeners need a trusted source list

For a listener that anyone can reach, the library gives you the tool to say who is allowed to speak the protocol at all. The mechanism is a ConnPolicy on the listener, and the readme's example shows the shape.

```go
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")),
}
```

TrustProxyHeaderFrom takes a single address, TrustProxyHeaderFromRanges takes CIDR ranges, and PolicyFromRanges lets you spell out two policies when traffic comes from a mix of sources, one policy for the matched ranges and another for everything else. In each case the effect on untrusted peers is that Accept drops them, rather than passing on a connection whose client address came from an attacker.

The reason this matters is stated plainly at the top of the security section. The PROXY header replaces what your application sees as the client address, so whoever is allowed to send one can spoof their origin. The specification requires access filtering so that only trusted proxies can use the protocol, and this library gives you the filter.

The other knobs are documented in the package docs and are worth knowing about: ReadHeaderTimeout with a default of 10 seconds, MaxV2HeaderSize with a default of 4KiB, V1AcceptIPv4InTCP6 which is off by default, and Listener.ValidateHeader.

## UDP is per datagram, and the library declines to wrap PacketConn

The UDP story is where this library is unusually explicit about what it will not do, and the reason is technical.

The specification requires the header and the proxied payload to travel in the same datagram, and the receiver must parse the header independently for every datagram. A stream oriented Listener and Conn cannot provide that, so the readme points you at ParseUDPDatagram and Header.FormatUDPDatagram with your own net.PacketConn.

The readme also states that the library deliberately does not export a net.PacketConn wrapper, and explains why. A wrapper that returns the client address from the header also needs an application owned client to proxy flow table for replies, including bounds, expiry and spoofing policy. In other words, a correct implementation has to answer questions about how long a mapping lives and who is allowed to use it, and those are application decisions, not library ones.

The examples make the same point from the other side. There is a UDP receiver and sender example showing direct per datagram parsing and formatting, and a separate example that sketches a net.PacketConn wrapper with reply routing through the relaying proxy, which is the pattern you would have to build yourself if you want that convenience.

One asymmetry is worth noting for stream connections: headers with UDPv4 and UDPv6 families carried over a stream describe the proxied protocol, and the readme says they remain fully supported. So the address family recorded in the header and the transport carrying it are separate concerns, and a TCP connection can legitimately carry a header that names a UDP protocol.

## TLS ordering and an HTTP/2 helper with a deprecation

Combining the PROXY protocol with TLS is a question of order, and the readme's rule is that the wrapper order has to match the upstream order.

If the header is sent in cleartext before the handshake, then proxyproto goes inside TLS. The other order, TLS first and then the header inside the encrypted channel, means the proxy protocol wrapper goes outside. This matters because the load balancer and the server have to agree, and the readme describes the TLS examples in the repository as a server and client over TLS with the PROXY header before TLS.

For HTTP, the helper package covers a case that is otherwise awkward. The helper/http2 package lets one server accept proxied HTTP/1 and HTTP/2 connections, and the readme recommends the newer constructor for new integrations.

```go
Use `helper/http2.NewServerWithConfig(server, config)` with
```

with a *net/http.HTTP2Config argument. The existing NewServer constructor remains available for compatibility with golang.org/x/net/http2.Server, and is marked deprecated. So the project is moving from the x/net flavoured server to the standard library's own HTTP/2 configuration, and the older path is kept rather than removed.

The repository itself is organised by protocol and concern, which makes it easy to see the scope: v1.go and v2.go for the two protocol versions, header.go for the header type, tlv.go and a tlvparse directory for the type length value encoding of version 2, udp.go for the packet helpers, policy.go for the trust policies, protocol.go and addr_proto.go for the shared plumbing, plus a fuzz test file and a set of example test files.

## Conclusion

go-proxyproto fits a Go service that sits behind a load balancer which speaks the PROXY protocol and needs the original client address for logging, rate limiting or access control. It is the wrong choice if your proxy only sets an application header, if you need UDP with automatic reply routing, or if you are stuck on an older Go release. Check first that your load balancer can emit the header in the format you configure, that the listener is only reachable by trusted proxies if you keep the default REQUIRE policy, and that the TLS ordering matches how the header is sent in front of the handshake.

## FAQ

### How do I add a PROXY protocol header to a Go client connection?

Build the header with proxyproto.HeaderProxyFromAddrs, passing the protocol version and the source and destination addresses, then call header.WriteTo(conn) before writing any application data.

### What happens when a client connects without a PROXY header?

With the default policy, which is REQUIRE, the connection fails its first I/O with ErrNoProxyProtocol. The default exists because the specification says receivers must not try to guess whether the header is present.

### How do I only trust PROXY headers from my load balancer?

Set a ConnPolicy on the listener, using TrustProxyHeaderFrom for a single address, TrustProxyHeaderFromRanges for CIDR ranges, or PolicyFromRanges for mixed traffic. Connections from untrusted sources are dropped by Accept.

### Does go-proxyproto support UDP?

Yes, but through the packet helpers ParseUDPDatagram and Header.FormatUDPDatagram with your own net.PacketConn, because the spec requires a header in every datagram. The library deliberately does not export a net.PacketConn wrapper, since reply routing needs an application owned flow table.

## Sources

- [Issues](https://github.com/pires/go-proxyproto/issues)
- [License: Apache-2.0](https://github.com/pires/go-proxyproto/blob/main/LICENSE)
- [pires/go-proxyproto on GitHub](https://github.com/pires/go-proxyproto)
- [README](https://github.com/pires/go-proxyproto/blob/main/README.md)
- [Releases](https://github.com/pires/go-proxyproto/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/pires-go-proxyproto
