A pod that calls its own Service through the external load balancer IP works on Syself Autopilot. That same call, hairpinning, times out on many other setups. It occurs when an ingress controller health-checks itself through the load balancer, or when a pod reaches a shared Service by its public address instead of the in-cluster Service name. It works here because the CCM sets one field correctly. This page explains the mechanism so you can recognize the rare case where it breaks.
Hairpinning is a pod connecting to the public load balancer IP, with the packet turning around to return to a pod on the same side. It resembles a client on the internet, but it never leaves:
```mermaid
flowchart LR
P["Pod"] -->|"1. connect to the Service's
external load balancer IP"| LB["Load balancer IP"]
LB -->|"2. packet turns around,
back into the cluster"| B["Backend pod"]
```
## The `ipMode` field decides the path
The CCM writes `status.loadBalancer.ingress[*].ipMode` on the Service, and its value chooses how hairpin traffic is routed.
**Without PROXY protocol, `ipMode` is `VIP`.** Cilium replaces kube-proxy here. It runs in the Linux kernel, intercepts traffic to the load balancer IP on the node itself, and routes it directly to a pod. The Hetzner load balancer is not involved. Hairpinning works.
**With PROXY protocol, `ipMode` is `Proxy`.** Cilium does not intercept the load balancer IP. Traffic from a pod travels to the real Hetzner load balancer, which routes it back into the cluster and adds the PROXY protocol header. Hairpinning works, and the header the backend expects is present.
The CCM sets `ipMode: Proxy` automatically when you set `uses-proxyprotocol: "true"`. You never set it manually.
## What breaks it
The failure is one specific mismatch: the load balancer sends PROXY protocol headers, but `ipMode` is not `Proxy`. In that case, Cilium bypasses the load balancer and routes directly to the pod, so the ingress controller receives no PROXY header at the start of the connection, misparses it, and drops it. The connecting pod experiences a timeout.
> [!NOTE]
> The platform CCM sets `ipMode` correctly, so you will not encounter this on a stock cluster. It can only happen if you replace or downgrade the CCM to a version that does not implement `ipMode: Proxy`.
## Verify it
Once the CCM has processed the Service, read the field:
```console
$ kubectl get svc -o jsonpath='{.status.loadBalancer.ingress[0].ipMode}'
Proxy
```
With PROXY protocol enabled, expect `Proxy`. Without it, expect `VIP`. If you set `load-balancer.hetzner.cloud/hostname` on the Service, the ingress entry carries only the hostname and this command returns nothing. If a hairpin call still times out, [debug the node's network path](/docs/hetzner/apalla/network/debug/debug-node-networking).