Proxy vs VPN vs NAT vs load balancer
Four middleboxes compared by OSI layer, what they rewrite, whether they terminate connections, and what the origin server actually sees as the client IP.
Key points
- The dividing test is connection termination. A proxy is an endpoint twice; NAT, DSR load balancers and routed VPNs rewrite or encapsulate packets in flight.
- A NAT gateway cannot return
502, because it has no application-layer voice. If a box can answer in the protocol's own language, it terminated the connection. - An L4 load balancer is a proxy in proxy mode (HAProxy
mode tcp, IPVS NAT with a userspace relay) and is not one in direct server return. - NAT preserves the client's TCP fingerprint and IP TTL behaviour; a proxy replaces both with its own stack's, which changes what the origin can infer.
A proxy terminates the client's connection and opens a new one to the destination. A NAT gateway rewrites addresses and ports on packets that pass through it without ever becoming an endpoint. A VPN encapsulates whole IP packets inside an encrypted tunnel and delivers them into another network, again without terminating the inner connection. A load balancer is a distribution policy, not a layer: it is a proxy when it terminates connections and it is a packet forwarder when it does not. Those four descriptions are the whole distinction, and every practical difference (what the origin sees, what breaks, what can be logged) follows from them.
The one test that separates them#
Ask: can the box answer in the protocol's own language?
A reverse proxy can return 502 Bad Gateway because it holds a live HTTP connection to the client and can compose a response. A NAT gateway cannot; if the destination is unreachable, the best it can do is emit an ICMP error or drop the packet, and the client's own TCP stack reports the failure. A VPN concentrator is in the same position: it moves packets, so the failure the application sees is a timeout or a connection reset originated by the real endpoint.
The corollary is diagnostic: an HTML error page, a 503, or an unexpected TLS certificate means something on the path terminated. A bare Connection timed out or Connection reset by peer means the failure came from an endpoint and the middleboxes were forwarding.
Full comparison#
| Forward/reverse proxy (L7) | L4 proxy / LB in proxy mode | LB in DSR or NAT mode | NAT gateway | Routed VPN | |
|---|---|---|---|---|---|
| Layer | 7 | 4 | 3 to 4 | 3 to 4 | 3 (tunnelled) |
| Terminates connections | Yes, two independent ones | Yes, two independent TCP sessions | No, one TCP session end to end | No | No, the inner connection is untouched |
| What is rewritten | The whole message: request line, headers, sometimes body | Nothing in the payload; new IP/TCP headers on the upstream side | Destination MAC (DSR) or destination IP/port (NAT mode) | Source IP and port, plus checksums | Nothing inner; an outer IP/UDP/ESP header is added |
| Client IP at the origin | The proxy's egress IP | The proxy's egress IP | The real client IP | The NAT device's public IP | The VPN's assigned inner IP, or the exit NAT's IP |
| How the real client IP is recovered | X-Forwarded-For / Forwarded headers | PROXY protocol header, or IP_TRANSPARENT spoofing | Not needed | Not recoverable without external correlation | Not needed inside the tunnel |
| Origin can observe | Full request content, the proxy's TLS stack, the proxy's TCP fingerprint | Ciphertext, the proxy's TCP fingerprint | The client's real TCP fingerprint and TLS stack | The client's TCP fingerprint, the NAT's address | The client's real stack, from the inner address |
| IP TTL seen at the origin | Reset by the proxy's own stack | Reset | Decremented once per hop | Decremented | Inner TTL decremented once by the tunnel |
| Can enforce content policy | Yes | Only on SNI/ALPN | No | No | No |
| Retries a failed backend | Yes, per request | Only before bytes are relayed | No | No | No |
| Typical software | nginx, HAProxy mode http, Envoy, Squid, Caddy | HAProxy mode tcp, nginx stream, Envoy TCP proxy | IPVS in DR mode, ECMP/Maglev forwarders | Linux MASQUERADE, cloud NAT gateway, CGNAT | WireGuard, IPsec, OpenVPN |
| Signature failure | 502, 504, header rewriting bugs | Half-open connections, pool exhaustion | Asymmetric routing, health-check blind spots | Port exhaustion, no inbound reachability | MTU/PMTU black holes, split-tunnel DNS leaks |
Why a NAT gateway is not a proxy#
Network address translation (RFC 3022) maintains a table mapping (inside IP, inside port) to (outside IP, outside port) and rewrites the relevant fields on each packet, then fixes the IP and TCP/UDP checksums. On Linux this table is nf_conntrack, and you can read it directly:
conntrack -L -p tcp --dport 443 | head -1tcp 6 431999 ESTABLISHED src=10.0.3.17 dst=93.184.216.34 sport=51422 dport=443 \
src=93.184.216.34 dst=203.0.113.9 sport=443 dport=51422 [ASSURED] mark=0 use=1That is one connection described from two vantage points. The TCP sequence numbers, window scaling, timestamps and the TLS handshake are all the client's; the gateway only edited addressing fields. There is no second socket, no buffer, no timeout of its own beyond the conntrack idle expiry (the Linux default for established TCP is 432000 seconds, which is where the 431999 above comes from).
Compare a proxy handling the same request. On the proxy host there are two sockets:
ss -tnp state established '( sport = :8080 or dport = :443 )'Recv-Q Send-Q Local Address:Port Peer Address:Port
0 0 10.0.1.5:8080 10.0.3.17:51422 users:(("nginx",pid=812,fd=13))
0 0 10.0.1.5:44118 93.184.216.34:443 users:(("nginx",pid=812,fd=14))Two file descriptors, two sequence-number spaces, two independent congestion windows. That is the structural difference, and it explains the behavioural ones: the proxy can hold the client waiting while it retries a second upstream, can return an error page, can rewrite headers, and can be the bottleneck. NAT can do none of those things and also cannot be the bottleneck in the same way, since its per-flow cost is a hash table entry.
Why an L4 load balancer is sometimes a proxy#
"L4 load balancer" covers two implementations that behave completely differently at the endpoint.
Proxy mode. HAProxy in mode tcp, nginx's stream module and Envoy's TCP proxy all accept the client's TCP connection, then open a separate connection to a chosen backend and copy bytes. This is a proxy by every criterion: two sockets, its own timeouts, its own connection to the backend. The backend sees the load balancer's IP. To recover the client address you either prepend a PROXY protocol header (send-proxy or send-proxy-v2 in HAProxy) or use transparent-proxy address spoofing (source 0.0.0.0 usesrc clientip, which needs IP_TRANSPARENT and routing that returns the backend's replies through the load balancer). The PROXY protocol decoder is useful when you need to confirm what a v2 header actually contains.
Forwarding mode. IPVS in direct routing (DR) mode rewrites only the destination MAC address; the backend has the virtual IP configured on a loopback alias, answers the client directly, and the load balancer never sees the return traffic. ECMP and Maglev-style forwarders behave similarly. Here the backend sees the true client IP with zero configuration, the load balancer has no per-connection buffers, but it also cannot retry, cannot rewrite, cannot terminate TLS and gets no visibility into responses.
The decision rule follows directly:
| Requirement | Choose |
|---|---|
| Need per-request routing, retries, header manipulation, TLS termination | L7 proxy |
| Need TLS passthrough but multiple backends on one IP:port | L4 proxy with SNI routing |
| Need the real client IP at the backend with no application change, at very high packet rates | Forwarding mode (DSR/ECMP) |
| Need the real client IP and connection-level control | L4 proxy plus PROXY protocol |
Cloud load balancers straddle this. An application load balancer always terminates and appends X-Forwarded-For. A network load balancer's behaviour depends on a client-IP-preservation attribute on the target group, so check the attribute rather than assuming; when preservation is off, the PROXY protocol option is the supported way to carry the address.
Where a VPN sits#
A VPN is an encapsulation, not an intermediation. WireGuard, IPsec ESP and OpenVPN take an entire IP packet, encrypt it, and put it inside a new outer packet addressed to the tunnel peer. The inner TCP connection runs unchanged from the client's stack to the destination's stack. That has three consequences worth internalising:
- It is protocol-agnostic and application-blind. A VPN carries DNS, ICMP, SMB and QUIC as happily as HTTP, and cannot make decisions about any of them, because it never parses them. A proxy is the opposite: protocol-aware, therefore selective.
- MTU becomes your problem. The outer header eats payload space.
wg-quicksets the interface MTU to the underlying device's MTU minus 80 bytes, which is why 1420 is the familiar WireGuard number on a 1500-byte path. If Path MTU Discovery is broken by an ICMP-filtering firewall, you get the classic signature: handshakes succeed, small requests work, and any response larger than roughly one segment hangs forever. TCP MSS clamping (iptables -t mangle -A FORWARD -p tcp --syn -j TCPMSS --clamp-mss-to-pmtu) is the standard mitigation. - The IP the origin sees depends on what is at the tunnel exit, not on the VPN itself. Full-tunnel VPNs almost always NAT at the exit, so the origin sees the exit gateway's address. This is why "a VPN hides my IP" and "a proxy hides my IP" produce the same observable result via completely different mechanisms.
VPNs also scope by route, not by destination name. A split tunnel sends selected prefixes into the tunnel; the analogous proxy control is the no_proxy environment variable, which is per-application and hostname-based, with entirely different matching semantics. Corporate laptops routinely sit inside a split tunnel and point at a proxy, with only one of the two exclusion lists kept current.
Client IP consequences, one by one#
| Middlebox | What the origin's socket shows | What to configure |
|---|---|---|
| L7 reverse proxy | The proxy's egress IP | Append X-Forwarded-For/Forwarded at the edge, strip inbound copies, define the trust boundary |
| Chained L7 proxies (CDN then ingress) | The last proxy's IP | Count hops from the right; the leftmost entry is attacker-controlled |
| L4 proxy | The proxy's egress IP, and no header exists to fix it | PROXY protocol v2, or IP_TRANSPARENT spoofing |
| DSR load balancer | The real client IP | Nothing |
| NAT gateway | The NAT's public IP, shared by every client behind it | Nothing recovers it; do not use IP for identity |
| CGNAT | One address shared by thousands of subscribers | Rate limiting by IP will punish unrelated users |
| VPN with exit NAT | The VPN exit IP | Same as NAT |
Two rules fall out of this table. First, never treat a source IP as an identity unless you control every hop between the client and the socket. Second, the header chain is only as trustworthy as your trusted-proxy list: an inbound X-Forwarded-For from an untrusted peer is user input. Work a specific chain through the client IP resolver and read client IP spoofing through proxies before writing allow-list logic against it.
Failure modes#
- Intermittent connection failures behind a NAT gateway. One NAT address offers roughly 64k source ports per destination tuple, and a workload opening many short-lived connections to a single destination exhausts them. Symptom: sporadic
connection timed outunder load with no server-side log entry. Fix: more egress addresses, connection reuse, or a proxy with an upstream keep-alive pool. - Health checks pass but real traffic fails, DSR mode. The probe reaches the backend's real address while clients arrive on the loopback-configured virtual IP, so a missing
arp_ignore/arp_announcesetting stays invisible to health checking. - TLS handshake succeeds, first large response never arrives, over a VPN. PMTU black hole. Confirm with
ping -M do -s 1400across the tunnel and clamp MSS. - All users rate-limited together after inserting a proxy. The application is keying limits on the socket peer address, which is now one proxy IP. Fix at the application, using a resolved client IP with an explicit trusted proxy configuration.
502appears where a timeout used to. Expected after replacing a forwarder with a proxy: the proxy now converts upstream failures into HTTP statuses. See 502 vs 503 vs 504 for reading them.- Traceroute stops at the middlebox. A proxy terminates, so a TCP traceroute to the destination port reaches only the proxy. This is a useful positive signal rather than a fault.
Frequently asked questions#
Is a VPN just a proxy that encrypts everything?#
No. A VPN encapsulates IP packets and routes them, leaving the inner TCP or QUIC connection intact from the client's stack to the destination's. A proxy terminates the connection and creates a new one, so it can see, alter and refuse individual requests. The practical difference is scope and control: a VPN applies to whatever the routing table sends into it, regardless of protocol, while a proxy applies per application and per protocol and can enforce content policy.
Does NAT hide my IP address the way a proxy does?#
The origin sees the NAT device's public address instead of your private one, so superficially yes. But NAT preserves your TCP and TLS characteristics exactly, offers no application-layer control, and shares one address across everyone behind it. It is address multiplexing, not intermediation, and it is not a privacy mechanism.
Is a reverse proxy a load balancer?#
A reverse proxy becomes a load balancer as soon as it distributes across more than one upstream, which nginx, HAProxy, Envoy, Caddy and Traefik all do. The terms describe different aspects of the same box: "reverse proxy" describes the role relative to the origin, "load balancer" describes the selection policy across backends. A load balancer that forwards packets without terminating is not a reverse proxy.
Why does my backend see the load balancer's IP with one product and the client's IP with another?#
Because they use different forwarding models. Terminating products (application load balancers, HAProxy in mode tcp or mode http) open a new connection from their own address. Forwarding products (direct server return, ECMP-based designs, and network load balancers with client IP preservation enabled) leave the source address intact. Check the specific attribute rather than the product category, since the same product can do both depending on configuration.
Can I put a proxy and a VPN in the same path?#
Yes, and it is a common corporate arrangement: a split-tunnel VPN routes internal prefixes, and an explicit proxy handles internet-bound HTTP. The failure to plan for is that the two use unrelated exclusion mechanisms, VPN routes versus no_proxy hostname matching, so a host can be reachable by routing yet still sent to a proxy that cannot reach it. Verify the hostname list with the no_proxy tester rather than reasoning about it.
Which one preserves end-to-end TLS?#
VPNs, NAT and forwarding-mode load balancers all preserve end-to-end TLS, because the TLS session is negotiated between the client and the origin. L4 proxies preserve it too when configured for passthrough, since they relay ciphertext. L7 proxies do not: they terminate TLS, and any encryption to the upstream is a second, separate session, which is the distinction covered in TLS termination, passthrough and re-encryption.
Primary sources#
Every normative claim on this page is checked against the specification or the vendor documentation listed here. Where behaviour is version dependent, the version is named in the text.
- RFC 3022: Traditional IP Network Address Translator (Traditional NAT)
- RFC 6888: Common Requirements for Carrier-Grade NATs (CGNs)
- RFC 4301: Security Architecture for the Internet Protocol
- RFC 9110: HTTP Semantics, section 3.7 Intermediaries
- The PROXY protocol specification
- Linux Virtual Server: how virtual server works
- HAProxy configuration manual: mode, source, send-proxy
- WireGuard: next generation kernel network tunnel
Found something wrong, or behaviour that differs on your version? Report it with the version number and a primary source. Anything substantive is fixed in the page and logged on the corrections page. See editorial standards for how pages are researched, sourced and reviewed.