Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+125
-122
@@ -1,162 +1,165 @@
|
||||
# go-proxyproto
|
||||
|
||||
[](https://github.com/pires/go-proxyproto/actions)
|
||||
[](https://coveralls.io/github/pires/go-proxyproto?branch=master)
|
||||
[](https://coveralls.io/github/pires/go-proxyproto?branch=main)
|
||||
[](https://goreportcard.com/report/github.com/pires/go-proxyproto)
|
||||
[](https://pkg.go.dev/github.com/pires/go-proxyproto?tab=doc)
|
||||
[](https://pkg.go.dev/github.com/pires/go-proxyproto)
|
||||
|
||||
|
||||
A Go library implementation of the [PROXY protocol, versions 1 and 2](https://www.haproxy.org/download/2.3/doc/proxy-protocol.txt),
|
||||
which provides, as per specification:
|
||||
> (...) a convenient way to safely transport connection
|
||||
> information such as a client's address across multiple layers of NAT or TCP
|
||||
> proxies. It is designed to require little changes to existing components and
|
||||
> to limit the performance impact caused by the processing of the transported
|
||||
> information.
|
||||
A Go library implementation of the
|
||||
[HAProxy PROXY protocol specification 3.4](https://www.haproxy.org/download/3.4/doc/proxy-protocol.txt).
|
||||
It covers protocol versions 1 (text) and 2 (binary), carrying the original
|
||||
client connection information across NAT and proxy layers.
|
||||
|
||||
This library is to be used in one of or both proxy clients and proxy servers that need to support said protocol.
|
||||
Both protocol versions, 1 (text-based) and 2 (binary-based) are supported.
|
||||
Use it on either side of a PROXY-aware hop: 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;
|
||||
UDP uses packet helpers because the spec requires a header in every datagram.
|
||||
|
||||
## Installation
|
||||
|
||||
```shell
|
||||
$ go get -u github.com/pires/go-proxyproto
|
||||
$ go get github.com/pires/go-proxyproto
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
The fastest way to get started is the runnable programs under
|
||||
[`examples/`](examples) and the API examples on
|
||||
[pkg.go.dev](https://pkg.go.dev/github.com/pires/go-proxyproto#pkg-examples):
|
||||
|
||||
| Goal | Where to look |
|
||||
| ---- | ------------- |
|
||||
| Minimal client | [`examples/client`](examples/client) |
|
||||
| Minimal server | [`examples/server`](examples/server) |
|
||||
| HTTP server | [`examples/httpserver`](examples/httpserver) |
|
||||
| Server + client over TLS (PROXY header before TLS) | [`examples/tlsserver`](examples/tlsserver), [`examples/tlsclient`](examples/tlsclient) |
|
||||
| UDP receiver and sender | [`examples/udpserver`](examples/udpserver), [`examples/udpclient`](examples/udpclient) |
|
||||
| UDP `net.PacketConn` wrapper pattern | [`examples/udppacketconn`](examples/udppacketconn) |
|
||||
| `Listener`, `NewConn`, `Read`, UDP, and TLS API examples | [Package examples](https://pkg.go.dev/github.com/pires/go-proxyproto#pkg-examples) |
|
||||
|
||||
## Usage
|
||||
|
||||
### Client
|
||||
Use the runnable examples above for complete programs. The core API shape is
|
||||
small.
|
||||
|
||||
### Client side
|
||||
|
||||
```go
|
||||
package main
|
||||
header := proxyproto.HeaderProxyFromAddrs(1, sourceAddr, destinationAddr)
|
||||
_, err := header.WriteTo(conn) // write the PROXY header before application data
|
||||
```
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
See [`examples/client`](examples/client) for a complete TCP client.
|
||||
|
||||
proxyproto "github.com/pires/go-proxyproto"
|
||||
)
|
||||
### Server side
|
||||
|
||||
func chkErr(err error) {
|
||||
if err != nil {
|
||||
log.Fatalf("Error: %s", err.Error())
|
||||
}
|
||||
}
|
||||
```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.
|
||||
```
|
||||
|
||||
func main() {
|
||||
// Dial some proxy listener e.g. https://github.com/mailgun/proxyproto
|
||||
target, err := net.ResolveTCPAddr("tcp", "127.0.0.1:2319")
|
||||
chkErr(err)
|
||||
See [`examples/server`](examples/server) for a complete TCP server. For HTTP/1
|
||||
and HTTP/2, see [`examples/httpserver`](examples/httpserver), which uses
|
||||
[`helper/http2`](helper/http2) so one server can accept proxied HTTP/1 and HTTP/2
|
||||
connections.
|
||||
|
||||
conn, err := net.DialTCP("tcp", nil, target)
|
||||
chkErr(err)
|
||||
> [!WARNING]
|
||||
> The zero-value configuration requires the PROXY header but still honors it
|
||||
> from any peer. It is not safe for listeners reachable by untrusted clients
|
||||
> without a trusted-source policy. See [Security](#security).
|
||||
|
||||
defer conn.Close()
|
||||
## Security
|
||||
|
||||
// Create a proxyprotocol header or use HeaderProxyFromAddrs() if you
|
||||
// have two conn's
|
||||
header := &proxyproto.Header{
|
||||
Version: 1,
|
||||
Command: proxyproto.PROXY,
|
||||
TransportProtocol: proxyproto.TCPv4,
|
||||
SourceAddr: &net.TCPAddr{
|
||||
IP: net.ParseIP("10.1.1.1"),
|
||||
Port: 1000,
|
||||
},
|
||||
DestinationAddr: &net.TCPAddr{
|
||||
IP: net.ParseIP("20.2.2.2"),
|
||||
Port: 2000,
|
||||
},
|
||||
}
|
||||
// After the connection was created write the proxy headers first
|
||||
_, err = header.WriteTo(conn)
|
||||
chkErr(err)
|
||||
// Then your data... e.g.:
|
||||
_, err = io.WriteString(conn, "HELO")
|
||||
chkErr(err)
|
||||
The PROXY header replaces what your application sees as the client address, so
|
||||
whoever is allowed to send one can spoof their origin. The spec (section 2)
|
||||
says receivers "MUST not try to guess" whether the header is present, and
|
||||
requires access filtering so only trusted proxies can use the protocol.
|
||||
|
||||
Important stream defaults and policies:
|
||||
|
||||
- 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`, so
|
||||
header presence is never guessed.
|
||||
- `REQUIRE` still honors headers from any peer. Use it only when the listener
|
||||
is reachable exclusively by trusted proxies, such as a private network
|
||||
segment behind your load balancer.
|
||||
- Deployments that need historical optional-header behavior can restore it
|
||||
process-wide with `proxyproto.DefaultPolicy = proxyproto.USE`, per
|
||||
connection with `WithPolicy(USE)`, or per listener with a policy returning
|
||||
`USE`.
|
||||
- For exposed listeners, restrict the senders with `TrustProxyHeaderFrom` or
|
||||
`TrustProxyHeaderFromRanges`. Trusted peers must send a header; untrusted
|
||||
peers are dropped by `Accept`. For example:
|
||||
|
||||
```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")),
|
||||
}
|
||||
```
|
||||
|
||||
### Server
|
||||
For UDP, `ParseUDPDatagram` has no built-in trusted-source policy because it
|
||||
only sees packet bytes. Check the `net.PacketConn.ReadFrom` sender address
|
||||
yourself before trusting `header.SourceAddr`.
|
||||
|
||||
```go
|
||||
package main
|
||||
Related knobs are documented in the
|
||||
[package docs](https://pkg.go.dev/github.com/pires/go-proxyproto):
|
||||
`ReadHeaderTimeout` (default 10s), `MaxV2HeaderSize` (default 4KiB),
|
||||
`V1AcceptIPv4InTCP6` (default off), and `Listener.ValidateHeader`.
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
## UDP
|
||||
|
||||
proxyproto "github.com/pires/go-proxyproto"
|
||||
)
|
||||
The spec requires the header and proxied payload in the same UDP datagram, and
|
||||
the receiver must parse the header independently for every datagram. `Listener`
|
||||
and `Conn` cannot provide those semantics; use `ParseUDPDatagram` and
|
||||
`Header.FormatUDPDatagram` with your own `net.PacketConn`. Headers with
|
||||
`UDPv4`/`UDPv6` families carried over stream connections describe the proxied
|
||||
protocol and remain fully supported.
|
||||
|
||||
func main() {
|
||||
// Create a listener
|
||||
addr := "localhost:9876"
|
||||
list, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
log.Fatalf("couldn't listen to %q: %q\n", addr, err.Error())
|
||||
}
|
||||
- [`examples/udpserver`](examples/udpserver) and
|
||||
[`examples/udpclient`](examples/udpclient) show direct per-datagram parsing
|
||||
and formatting.
|
||||
- [`examples/udppacketconn`](examples/udppacketconn) sketches a
|
||||
`net.PacketConn` wrapper with reply routing through the relaying proxy.
|
||||
- [`ExampleParseUDPDatagram`](https://pkg.go.dev/github.com/pires/go-proxyproto#example-ParseUDPDatagram)
|
||||
and
|
||||
[`ExampleHeader_FormatUDPDatagram`](https://pkg.go.dev/github.com/pires/go-proxyproto#example-Header.FormatUDPDatagram)
|
||||
show the same APIs as package examples.
|
||||
|
||||
// Wrap listener in a proxyproto listener
|
||||
proxyListener := &proxyproto.Listener{Listener: list}
|
||||
defer proxyListener.Close()
|
||||
The library deliberately does not export a `net.PacketConn` wrapper. 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.
|
||||
|
||||
// Wait for a connection and accept it
|
||||
conn, err := proxyListener.Accept()
|
||||
defer conn.Close()
|
||||
### TLS
|
||||
|
||||
// Print connection details
|
||||
if conn.LocalAddr() == nil {
|
||||
log.Fatal("couldn't retrieve local address")
|
||||
}
|
||||
log.Printf("local address: %q", conn.LocalAddr().String())
|
||||
When combining PROXY protocol with TLS, match the wrapper order to the upstream
|
||||
order. If the header is sent in cleartext before the handshake, put proxyproto
|
||||
inside TLS: `tls.NewListener(&proxyproto.Listener{Listener: l}, tlsConfig)`.
|
||||
If the header is sent inside the TLS session, decrypt first:
|
||||
`&proxyproto.Listener{Listener: tls.NewListener(l, tlsConfig)}`. In both cases
|
||||
`conn.RemoteAddr()` reports the client carried by the PROXY header.
|
||||
|
||||
if conn.RemoteAddr() == nil {
|
||||
log.Fatal("couldn't retrieve remote address")
|
||||
}
|
||||
log.Printf("remote address: %q", conn.RemoteAddr().String())
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP Server
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pires/go-proxyproto"
|
||||
)
|
||||
|
||||
func main() {
|
||||
server := http.Server{
|
||||
Addr: ":8080",
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", server.Addr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
proxyListener := &proxyproto.Listener{
|
||||
Listener: ln,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
defer proxyListener.Close()
|
||||
|
||||
server.Serve(proxyListener)
|
||||
}
|
||||
```
|
||||
Runnable code lives in [`examples/tlsserver`](examples/tlsserver) and
|
||||
[`examples/tlsclient`](examples/tlsclient). Package examples show both
|
||||
orderings.
|
||||
|
||||
## Special notes
|
||||
|
||||
### AWS
|
||||
|
||||
AWS Network Load Balancer (NLB) does not push the PPV2 header until the client starts sending the data. This is a problem if your server speaks first. e.g. SMTP, FTP, SSH etc.
|
||||
|
||||
By default, NLB target group attribute `proxy_protocol_v2.client_to_server.header_placement` has the value `on_first_ack_with_payload`. You need to contact AWS support to change it to `on_first_ack`, instead.
|
||||
|
||||
Just to be clear, you need this fix only if your server is designed to speak first.
|
||||
AWS Network Load Balancer (NLB) does not send the PROXY v2 header until the
|
||||
client sends payload: the target group attribute
|
||||
`proxy_protocol_v2.client_to_server.header_placement` defaults to
|
||||
`on_first_ack_with_payload`. Server-first protocols such as SMTP, FTP, and SSH
|
||||
fail in that mode; contact AWS support to change the attribute to
|
||||
`on_first_ack` so the header arrives before the backend speaks.
|
||||
Reference in new issue
Block a user