Proxy Protocol (HAProxy): Preserve Client IP Through Load Balancers
The PROXY protocol is a networking standard developed by HAProxy Technologies that solves a critical problem: when traffic passes through load balancers and reverse proxies, the original client IP address is lost. The PROXY protocol adds a small header to each connection containing the original source and destination IP addresses, allowing backend servers to see the real client IP even through multiple proxy layers.
The Problem
When a load balancer or reverse proxy sits between clients and backend servers, the backend only sees the proxy’s IP address — not the client’s:
Without PROXY Protocol:
Client (73.162.45.120)
|
v
Load Balancer (10.0.1.100)
|
v
Backend Server
| Sees connection from: 10.0.1.100 (load balancer IP)
| Client IP: UNKNOWN
| Problem: logging, rate limiting, geo-targeting all brokenExisting Solutions and Their Limitations
| Solution | How It Works | Limitation |
|---|---|---|
| X-Forwarded-For header | HTTP header with client IP | HTTP only, can be spoofed |
| X-Real-IP header | Single header with client IP | HTTP only, not standardized |
| PROXY protocol | TCP-level header prepended | Works for any TCP protocol |
X-Forwarded-For only works for HTTP/HTTPS traffic and is easily spoofed by malicious clients. The PROXY protocol works at the TCP level, supporting any protocol (HTTP, SMTP, database connections, etc.).
How PROXY Protocol Works
Version 1 (Human-Readable)
PROXY protocol v1 header format:
PROXY TCP4 <source_ip> <dest_ip> <source_port> <dest_port>\r\n
Example:
PROXY TCP4 73.162.45.120 10.0.1.100 56789 443\r\n
<actual TCP data follows immediately>
Fields:
- PROXY → Protocol identifier
- TCP4/TCP6 → IPv4 or IPv6
- 73.162.45.120→ Original client IP
- 10.0.1.100 → Original destination IP
- 56789 → Client source port
- 443 → Destination portWith PROXY Protocol v1:
Client (73.162.45.120:56789)
|
v
Load Balancer
| Prepends: "PROXY TCP4 73.162.45.120 10.0.1.100 56789 443\r\n"
v
Backend Server
| Reads PROXY header first
| Knows client IP: 73.162.45.120
| Processes remaining data normallyVersion 2 (Binary)
PROXY protocol v2 header format (binary, more efficient):
Byte 0-11: Signature (\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A)
Byte 12: Version (2) + Command (PROXY=1 or LOCAL=0)
Byte 13: Address family + Transport protocol
Byte 14-15: Length of address data
Byte 16+: Address data (source IP, dest IP, source port, dest port)
Optional: TLV (Type-Length-Value) extensions
Extensions include:
- PP2_TYPE_ALPN → TLS ALPN negotiated protocol
- PP2_TYPE_AUTHORITY → SNI hostname
- PP2_TYPE_SSL → TLS version, cipher, client cert info
- PP2_TYPE_UNIQUE_ID → Unique connection identifierHAProxy Configuration
Basic Setup
# haproxy.cfg
global
log /dev/log local0
maxconn 4096
defaults
mode tcp
timeout connect 5s
timeout client 30s
timeout server 30s
# Frontend — accept client connections
frontend web_frontend
bind *:443 ssl crt /etc/ssl/certs/server.pem
default_backend web_servers
# Backend — send to servers WITH proxy protocol
backend web_servers
balance roundrobin
server web1 10.0.2.10:8080 send-proxy-v2
server web2 10.0.2.11:8080 send-proxy-v2
server web3 10.0.2.12:8080 send-proxy-v2Receiving PROXY Protocol
# HAProxy as a backend that receives PROXY protocol from another LB
frontend from_upstream_lb
bind *:8080 accept-proxy
# Now $src contains the ORIGINAL client IP, not the upstream LB
default_backend applicationMixed Mode (Some Connections with PP, Some Without)
frontend mixed_frontend
bind *:80
bind *:8080 accept-proxy # Only this port expects PROXY protocol
# Use src for logging — will show real client IP when accept-proxy is used
log-format "%ci:%cp -> %fi:%fp [%t] %ft %b/%s %Tq/%Tw/%Tc/%Tr/%Tt %ST %B %CC"Nginx Configuration
Receiving PROXY Protocol
# /etc/nginx/nginx.conf
stream {
server {
listen 443 ssl proxy_protocol;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
# $proxy_protocol_addr contains the real client IP
proxy_pass backend_servers;
}
}
http {
# For HTTP backends receiving PROXY protocol
server {
listen 8080 proxy_protocol;
# Set real IP from PROXY protocol
set_real_ip_from 10.0.0.0/8; # Trust the load balancer subnet
real_ip_header proxy_protocol;
# Now $remote_addr is the real client IP
location / {
proxy_pass http://app_server;
proxy_set_header X-Real-IP $proxy_protocol_addr;
proxy_set_header X-Forwarded-For $proxy_protocol_addr;
}
}
}Sending PROXY Protocol
stream {
upstream backend {
server 10.0.2.10:8080;
server 10.0.2.11:8080;
}
server {
listen 443 ssl;
proxy_pass backend;
proxy_protocol on; # Send PROXY protocol to backend
}
}AWS Integration
Application Load Balancer (ALB)
ALB does not support PROXY protocol — it uses X-Forwarded-For instead. Use NLB for PROXY protocol.
Network Load Balancer (NLB)
NLB Configuration:
1. Create target group
2. Enable "Proxy protocol v2" in target group attributes
3. Configure backend instances to accept PROXY protocol
AWS CLI:
aws elbv2 modify-target-group-attributes \
--target-group-arn arn:aws:elasticloadbalancing:... \
--attributes Key=proxy_protocol_v2.enabled,Value=trueECS/Fargate with NLB
{
"targetGroupAttributes": [
{
"key": "proxy_protocol_v2.enabled",
"value": "true"
}
]
}Application-Level Handling
Python — Reading PROXY Protocol Header
import socket
import struct
def read_proxy_protocol_v1(conn):
"""Parse PROXY protocol v1 header from connection"""
data = b""
while b"\r\n" not in data:
data += conn.recv(1)
header = data.decode().strip()
parts = header.split()
if parts[0] != "PROXY":
raise ValueError("Not a PROXY protocol header")
return {
"protocol": parts[1], # TCP4 or TCP6
"source_ip": parts[2], # Client IP
"dest_ip": parts[3], # Original destination
"source_port": int(parts[4]),
"dest_port": int(parts[5]),
}
def read_proxy_protocol_v2(conn):
"""Parse PROXY protocol v2 header"""
signature = conn.recv(12)
expected = b'\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A'
if signature != expected:
raise ValueError("Invalid PROXY protocol v2 signature")
ver_cmd = struct.unpack('B', conn.recv(1))[0]
fam_proto = struct.unpack('B', conn.recv(1))[0]
length = struct.unpack('!H', conn.recv(2))[0]
addr_data = conn.recv(length)
# Parse IPv4 addresses
if (fam_proto >> 4) == 1: # AF_INET
src_ip = socket.inet_ntoa(addr_data[0:4])
dst_ip = socket.inet_ntoa(addr_data[4:8])
src_port = struct.unpack('!H', addr_data[8:10])[0]
dst_port = struct.unpack('!H', addr_data[10:12])[0]
return {
"source_ip": src_ip,
"dest_ip": dst_ip,
"source_port": src_port,
"dest_port": dst_port,
}Go — PROXY Protocol Library
package main
import (
"fmt"
"net"
proxyproto "github.com/pires/go-proxyproto"
)
func main() {
listener, _ := net.Listen("tcp", ":8080")
// Wrap listener to parse PROXY protocol
proxyListener := &proxyproto.Listener{Listener: listener}
for {
conn, _ := proxyListener.Accept()
// conn.RemoteAddr() now returns the REAL client IP
// (from PROXY protocol header, not the load balancer IP)
fmt.Printf("Real client IP: %s\n", conn.RemoteAddr())
conn.Close()
}
}When to Use PROXY Protocol
| Scenario | Use PROXY Protocol? | Alternative |
|---|---|---|
| HTTP behind load balancer | Optional | X-Forwarded-For works |
| HTTPS behind LB (TCP mode) | Yes | Cannot use XFF in TCP mode |
| Non-HTTP protocols (SMTP, DB) | Yes | Only option |
| Multi-layer proxy chain | Yes | XFF can be spoofed |
| Cloud NLB to backend | Yes | Recommended by AWS |
| CDN to origin | Depends | Most CDNs use XFF |
Frequently Asked Questions
What is the difference between PROXY protocol and X-Forwarded-For?
X-Forwarded-For is an HTTP header, so it only works with HTTP/HTTPS traffic. The PROXY protocol works at the TCP level, supporting any protocol (email, databases, custom protocols). X-Forwarded-For can also be spoofed by clients, while PROXY protocol headers are added by trusted infrastructure components and are harder to forge.
Can PROXY protocol be used with forward proxies?
The PROXY protocol is designed for reverse proxy/load balancer scenarios where you control both the proxy and the backend. It is not typically used with forward proxies (like web scraping proxies) because the target website does not expect or support PROXY protocol headers. Forward proxies use the standard HTTP proxy protocol or SOCKS5 instead.
Does PROXY protocol add latency?
The overhead is negligible — the v1 header is a single text line (under 100 bytes), and v2 is a compact binary header (16-52 bytes). The header is sent once per connection, not per request. The parsing time is microseconds. There is no measurable latency impact.
What happens if a backend receives PROXY protocol but is not configured for it?
The backend will interpret the PROXY protocol header as the beginning of the actual request data, causing parsing errors. For HTTP servers, the request will appear malformed and return a 400 Bad Request error. Always ensure both sender and receiver agree on whether PROXY protocol is enabled.
Is PROXY protocol v2 always better than v1?
v2 is more efficient (binary format, faster parsing) and supports extensions (TLS info, unique IDs). However, v1 is human-readable and easier to debug. For production systems, v2 is recommended. For debugging and development, v1 is convenient. Most modern software supports both versions. Learn more about proxy types in our reverse proxy guide.
Conclusion
The PROXY protocol is an essential infrastructure component for any deployment using load balancers or reverse proxies. It preserves the original client IP address at the TCP level, enabling accurate logging, rate limiting, and security policies regardless of how many proxy layers sit between clients and backends. Use v2 for production deployments, configure both sender and receiver consistently, and always restrict PROXY protocol acceptance to trusted sources.
For related topics, see our guides on reverse proxies and proxy authentication methods.
- Datacenter vs Residential Proxies: Complete Comparison
- Docker Proxy Setup: Configure Containers to Use Proxies
- Anti-Bot Detection Glossary: 50+ Terms Defined
- Anti-Bot Terminology Glossary: Complete A-Z Reference 2026
- Backconnect Proxies Deep Dive: Architecture and Real-World Performance
- Best Proxies in Southeast Asia: Singapore, Thailand, Indonesia, Philippines
- Datacenter vs Residential Proxies: Complete Comparison
- Docker Proxy Setup: Configure Containers to Use Proxies
- Anti-Bot Detection Glossary: 50+ Terms Defined
- Anti-Bot Terminology Glossary: Complete A-Z Reference 2026
- Backconnect Proxies Deep Dive: Architecture and Real-World Performance
- Best Proxies in Southeast Asia: Singapore, Thailand, Indonesia, Philippines
- Datacenter vs Residential Proxies: Complete Comparison
- Docker Proxy Setup: Configure Containers to Use Proxies
- 403 Forbidden Error: What It Means & How to Fix It
- 407 Proxy Authentication Required: Fix Guide
- Anti-Bot Detection Glossary: 50+ Terms Defined
- Anti-Bot Terminology Glossary: Complete A-Z Reference 2026
Related Reading
- Datacenter vs Residential Proxies: Complete Comparison
- Docker Proxy Setup: Configure Containers to Use Proxies
- 403 Forbidden Error: What It Means & How to Fix It
- 407 Proxy Authentication Required: Fix Guide
- Anti-Bot Detection Glossary: 50+ Terms Defined
- Anti-Bot Terminology Glossary: Complete A-Z Reference 2026