Add PROXY protocol v1/v2 support for multi-hop deployments
New internal/proxyproto package implements PROXY protocol parsing and writing without buffering past the header boundary (reads exact byte counts so the connection is correctly positioned for SNI extraction). Parser: auto-detects v1 (text) and v2 (binary) by first byte. Parses TCP4/TCP6 for both versions plus v2 LOCAL command. Enforces max header sizes and read deadlines. Writer: generates v2 binary headers for IPv4 and IPv6 with PROXY command. Server integration: - Receive: when listener.ProxyProtocol is true, parses PROXY header before firewall check. Real client IP from header is used for firewall evaluation and logging. Malformed headers cause RST. - Send: when route.SendProxyProtocol is true, writes PROXY v2 header to backend before forwarding the ClientHello bytes. Tests cover v1/v2 parsing, malformed rejection, timeout, round-trip write+parse, and five server integration tests: receive with valid header, receive with garbage, send verification, send-disabled verification, and firewall evaluation using the real client IP. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"git.wntrmute.dev/kyle/mc-proxy/internal/config"
|
||||
"git.wntrmute.dev/kyle/mc-proxy/internal/firewall"
|
||||
"git.wntrmute.dev/kyle/mc-proxy/internal/proxy"
|
||||
"git.wntrmute.dev/kyle/mc-proxy/internal/proxyproto"
|
||||
"git.wntrmute.dev/kyle/mc-proxy/internal/sni"
|
||||
)
|
||||
|
||||
@@ -266,6 +267,20 @@ func (s *Server) handleConn(ctx context.Context, conn net.Conn, ls *ListenerStat
|
||||
}
|
||||
addr := addrPort.Addr()
|
||||
|
||||
// Parse PROXY protocol header if enabled on this listener.
|
||||
if ls.ProxyProtocol {
|
||||
hdr, err := proxyproto.Parse(conn, time.Now().Add(5*time.Second))
|
||||
if err != nil {
|
||||
s.logger.Debug("PROXY protocol parse failed", "addr", addr, "error", err)
|
||||
return
|
||||
}
|
||||
if hdr.Command == proxyproto.CommandProxy {
|
||||
addr = hdr.SrcAddr.Addr()
|
||||
addrPort = hdr.SrcAddr
|
||||
s.logger.Debug("PROXY protocol", "real_addr", addr, "peer_addr", remoteAddr)
|
||||
}
|
||||
}
|
||||
|
||||
if s.fw.Blocked(addr) {
|
||||
s.logger.Debug("blocked by firewall", "addr", addr)
|
||||
return
|
||||
@@ -289,12 +304,12 @@ func (s *Server) handleConn(ctx context.Context, conn net.Conn, ls *ListenerStat
|
||||
s.logger.Error("L7 mode not yet implemented", "hostname", hostname)
|
||||
return
|
||||
default:
|
||||
s.handleL4(ctx, conn, ls, addr, hostname, route, peeked)
|
||||
s.handleL4(ctx, conn, addr, addrPort, hostname, route, peeked)
|
||||
}
|
||||
}
|
||||
|
||||
// handleL4 handles an L4 (passthrough) connection.
|
||||
func (s *Server) handleL4(ctx context.Context, conn net.Conn, _ *ListenerState, addr netip.Addr, hostname string, route RouteInfo, peeked []byte) {
|
||||
func (s *Server) handleL4(ctx context.Context, conn net.Conn, addr netip.Addr, clientAddrPort netip.AddrPort, hostname string, route RouteInfo, peeked []byte) {
|
||||
backendConn, err := net.DialTimeout("tcp", route.Backend, s.cfg.Proxy.ConnectTimeout.Duration)
|
||||
if err != nil {
|
||||
s.logger.Error("backend dial failed", "hostname", hostname, "backend", route.Backend, "error", err)
|
||||
@@ -302,6 +317,15 @@ func (s *Server) handleL4(ctx context.Context, conn net.Conn, _ *ListenerState,
|
||||
}
|
||||
defer backendConn.Close()
|
||||
|
||||
// Send PROXY protocol v2 header to backend if configured.
|
||||
if route.SendProxyProtocol {
|
||||
backendAddrPort, _ := netip.ParseAddrPort(backendConn.RemoteAddr().String())
|
||||
if err := proxyproto.WriteV2(backendConn, clientAddrPort, backendAddrPort); err != nil {
|
||||
s.logger.Error("writing PROXY protocol header", "hostname", hostname, "error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Debug("proxying", "addr", addr, "hostname", hostname, "backend", route.Backend)
|
||||
|
||||
result, err := proxy.Relay(ctx, conn, backendConn, peeked, s.cfg.Proxy.IdleTimeout.Duration)
|
||||
|
||||
Reference in New Issue
Block a user