Symptoms
- Clients through the Network Load Balancer (NLB) time out after enabling preserve source IP on the backend set; with it off, everything works.
- Clients in the same subnet or VCN as the backends fail; clients from other networks work.
- Backends see the real client IP but the health checks are fine, so the NLB shows all backends healthy.
What changes when the client IP is preserved
| Setting | Backend sees | Reply goes to |
|---|---|---|
| Source IP preservation off | The NLB's IP | The NLB, which returns it to the client |
| Source IP preservation on | The real client IP | Routed towards the client IP by the backend's normal routing; it must pass back through the NLB to be translated correctly |
When the client is in a remote network, the reply follows the subnet route table back through the path the NLB handles, and it works. When the client is in the same subnet (or reachable directly), the backend replies straight to the client, bypassing the NLB. The client sent its SYN to the NLB IP but gets a SYN-ACK from the backend IP, and drops it. This is the hairpin problem.
Step 1: check the NLB and backend set configuration
NLB=ocid1.networkloadbalancer.oc1.eu-frankfurt-1.aaaa...
oci nlb network-load-balancer get --network-load-balancer-id $NLB \
--query 'data.{private:"is-private",preserveSrcDst:"is-preserve-source-destination",symmetric:"is-symmetric-hash-enabled",subnet:"subnet-id"}'
oci nlb backend-set list --network-load-balancer-id $NLB \
--query 'data.items[].{name:name,preserveSource:"is-preserve-source",policy:policy}' --output table
oci nlb backend-health get --network-load-balancer-id $NLB \
--backend-set-name bs-app --backend-name 10.0.2.10:443 2>/dev/null
is-preserve-source on the backend set is the per-set client IP preservation flag. is-preserve-source-destination and symmetric hashing on the NLB itself are for transparent ("bump in the wire") designs with network appliances as backends.
Step 2: find where the clients are
List the failing client IPs (from app or client logs) and compare with:
- The backend subnets' CIDRs.
- The NLB's subnet.
- Whether a route sends traffic between them via a gateway or locally within the VCN.
If failing clients are in the same VCN subnet as the backends, it's the hairpin case.
Step 3: fix the hairpin case
Options, from simplest:
| Option | Effect |
|---|---|
| Turn off source IP preservation for the backend set used by internal clients | Backend sees the NLB IP; replies go back via the NLB. Lose client IP for those flows |
| Put clients and backends in different subnets with routing that brings replies back through the expected path | Removes the direct path |
| Internal clients call backends directly (or via a separate NLB/backend set without preservation) | Separate entry for internal and external traffic |
| Use proxy protocol / X-Forwarded-For with the flexible Load Balancer (L7) when only HTTP(S) is needed | Client IP carried in headers, no routing tricks |
Step 4: security rules with preserved client IPs
With preservation on, backends receive packets from the client IPs, not from the NLB subnet. Security lists and NSGs on the backend subnet must allow the client ranges on the service port, and stateless rules (if you use them) must allow the return traffic too.
oci network nsg rules list --nsg-id <backend-nsg> \
--query 'data[?direction==`INGRESS`].{src:source,proto:protocol,ports:"tcp-options"."destination-port-range"}' --output table
Health checks come from the NLB, so they still succeed even when client traffic is blocked. That's why "all backends healthy, clients failing" is typical.
Tip
Prefer stateful NSG rules for backends behind an NLB. Stateless rules need explicit rules for every return flow, which is where half-open connections come from.
Step 5: test both paths
From an external client and from a client in the backend subnet:
curl -v --connect-timeout 5 https://10.0.1.50/health # NLB private IP
On a backend, capture to see who sends what:
sudo tcpdump -ni ens3 port 443 and host <client-ip>
A SYN from the client IP followed by a SYN-ACK leaving towards it, but no ACK coming back, confirms the client dropped the reply because it came from the wrong source.
Step 6: transparent mode with network appliances
If the NLB load-balances across firewalls (preserve source and destination), both directions of each flow must hit the same appliance. Enable symmetric hashing on the NLB and route return traffic through the same NLB, as described in OCI's network firewall/NLB reference architectures. Otherwise the same asymmetry problem appears between appliances.
Prevent it next time
- Decide per backend set whether you need the client IP, and why.
- Keep internal consumers on a separate path from external ones.
- Allow client CIDRs explicitly in backend NSGs when preservation is on, and document them.
- Add a synthetic check from inside the VCN as well as from outside.