Hairpin NAT
Hairpin NAT (disebut juga NAT loopback atau NAT reflection) adalah masalah klasik ketika perangkat di dalam LAN mencoba mengakses server internal lewat IP publik (atau domain yang menunjuk ke IP publik), padahal server itu berada di jaringan yang sama. Dari internet berjalan normal, dari dalam LAN koneksi menggantung.
Halaman ini memakai kasus nyata: node OKD (192.168.90.30) gagal mengakses registry.example.com. Lihat juga Install OKD Single Node (SNO) di Proxmox.
Topologi
| Komponen | Alamat | Peran |
|---|---|---|
| MikroTik (WAN) | 203.0.113.10 |
IP publik, NAT 80/443 ke proxy |
| MikroTik (LAN) | 192.168.90.1 |
Gateway subnet LAB |
| nginx proxy | 192.168.90.100 |
Terminasi HTTPS (Let's Encrypt), reverse proxy berdasarkan nama domain |
| Registry backend | 192.168.90.25:5001 |
Docker Distribution (HTTP, auth) |
| Klien (node OKD) | 192.168.90.30 |
Ingin menarik image dari registry |
DNS publik (Cloudflare, DNS only): registry.example.com → 203.0.113.10
Gejala
- Dari internet:
https://registry.example.comnormal. - Dari dalam LAN: koneksi menggantung tanpa error yang jelas.
$ getent hosts registry.example.com
203.0.113.10 registry.example.com
$ curl -sI -v https://registry.example.com/v2/
* Host registry.example.com:443 was resolved.
* IPv4: 203.0.113.10
* Trying 203.0.113.10:443...
(menggantung)
Tidak ada connection refused, tidak ada error sertifikat; hanya diam. Ini ciri khas hairpin NAT.
Penyebab
Alur paket yang gagal
1. Klien 192.168.90.30 ingin ke 203.0.113.10:443
"203.0.113.10 bukan subnet-ku" → kirim ke gateway MikroTik
SYN src 192.168.90.30 → dst 203.0.113.10
2. MikroTik: aturan dst-nat 443 → proxy
SYN src 192.168.90.30 → dst 192.168.90.100 (alamat tujuan diganti)
3. Proxy melihat pengirim 192.168.90.30, satu subnet dengannya
→ balas LANGSUNG ke klien lewat switch, TIDAK lewat MikroTik
SYN-ACK src 192.168.90.100 → dst 192.168.90.30
4. Klien: "Aku mengirim SYN ke 203.0.113.10, kenapa yang membalas 192.168.90.100?"
→ balasan tidak cocok dengan koneksi mana pun, dibuang
→ klien terus menunggu → koneksi menggantung
Inti masalah
NAT hanya berfungsi bila paket pergi dan paket pulang melewati router yang sama, supaya router bisa menerjemahkan alamat balik. Pada langkah 3, balasan mengambil jalan pintas di dalam subnet, sehingga MikroTik tidak sempat mengubah alamat sumber balasan dari 192.168.90.100 kembali menjadi 203.0.113.10.
Kenapa dari internet tidak bermasalah?
Klien di internet tidak satu subnet dengan proxy. Balasan proxy pasti dikirim ke gateway (MikroTik), sehingga MikroTik menerjemahkan alamatnya dengan benar.
Cara membuktikan
Di server tujuan (proxy), tangkap paket dari klien:
sudo tcpdump -ni any host 192.168.90.30 and port 443
Saat klien menjalankan curl, terlihat SYN masuk dari 192.168.90.30 dan proxy membalas SYN-ACK langsung ke 192.168.90.30, tetapi ACK dari klien tidak pernah datang.
Uji jalur langsung tanpa NAT dengan --resolve (memaksa nama domain ke IP tertentu):
curl -sI --max-time 5 --resolve registry.example.com:443:192.168.90.100 \
https://registry.example.com/v2/ | head -3
Bila ini menjawab (misalnya HTTP/1.1 401 Unauthorized dari nginx), server sehat dan masalahnya memang hairpin NAT.
Catatan: pastikan IP yang dipakai adalah server yang benar-benar melayani HTTPS. Pada kasus ini, 192.168.90.25 (backend registry) menolak port 443 karena hanya melayani HTTP di port 5001; HTTPS dilayani oleh proxy 192.168.90.100.
curl -sI --max-time 5 http://192.168.90.25:5001/v2/ | head -3 # backend, HTTP
Solusi 1: Split DNS (dipakai)
Ide: perangkat di dalam LAN diberi jawaban DNS berbeda, yaitu langsung IP internal server. Disebut juga split-horizon DNS.
Klien 192.168.90.30 ──(langsung, satu subnet, tanpa NAT)──► Proxy 192.168.90.100
◄──────────────── balasan ─────────────
Di dnsmasq bastion (/etc/dnsmasq.d/okd.conf):
address=/registry.example.com/192.168.90.100
sudo systemctl restart dnsmasq
getent hosts registry.example.com # → 192.168.90.100
curl -sI https://registry.example.com/v2/ | head -3
Kenapa HTTPS tetap valid? Sertifikat dicocokkan dengan nama, bukan IP. Klien tetap meminta registry.example.com (lewat SNI dan header Host), sehingga nginx menyajikan sertifikat Let's Encrypt untuk nama itu lalu meneruskan ke backend 192.168.90.25:5001. Yang berubah hanya jalur jaringannya.
Kelebihan:
- Tidak mengubah konfigurasi router.
- Trafik langsung di dalam subnet (lebih efisien).
- Proxy melihat IP asli klien di log.
Kekurangan:
- Hanya berlaku untuk perangkat yang memakai DNS tersebut (di sini: node OKD, bastion, VM yang DNS-nya diarahkan ke bastion, dan klien WireGuard dengan
DNS = 192.168.90.29). - Setiap subdomain yang perlu diakses dari dalam harus ditambahkan (atau pakai wildcard
address=/example.com/192.168.90.100bila semua subdomain memang dilayani proxy yang sama).
Solusi 2: Hairpin NAT di MikroTik
Ide: untuk trafik LAN yang di-NAT balik ke LAN, MikroTik juga mengganti alamat sumber menjadi IP router sendiri. Akibatnya server terpaksa membalas ke MikroTik, sehingga terjemahan alamat bisa dibalik dengan benar.
1. Klien → MikroTik : src 192.168.90.30 → dst 203.0.113.10 2. MikroTik → Proxy : src 192.168.90.1 → dst 192.168.90.100 (dst-nat + masquerade) 3. Proxy → MikroTik : src 192.168.90.100 → dst 192.168.90.1 (balas ke router) 4. MikroTik → Klien : src 203.0.113.10 → dst 192.168.90.30 (diterjemahkan balik)
Syarat 1: aturan dst-nat harus cocok untuk trafik dari LAN
Banyak aturan port forward ditulis dengan in-interface=<WAN>, sehingga hanya berlaku untuk trafik dari internet. Untuk hairpin, aturan perlu mencocokkan alamat tujuan IP publik:
/ip firewall nat
add chain=dstnat dst-address=203.0.113.10 protocol=tcp dst-port=80,443 \
action=dst-nat to-addresses=192.168.90.100 comment="port forward web"
Bila IP publik dinamis, pilihan umum adalah dst-address-type=local dikombinasikan dengan pembatas lain (misalnya dst-address-list berisi IP publik yang diperbarui skrip), agar tidak ikut menangkap trafik ke IP router lainnya.
Syarat 2: masquerade untuk trafik hairpin
/ip firewall nat
add chain=srcnat src-address=192.168.90.0/24 dst-address=192.168.90.100 \
protocol=tcp dst-port=80,443 out-interface=<interface-LAN> \
action=masquerade comment="hairpin NAT"
Aturan ini harus berada di atas aturan masquerade umum ke internet (urutan aturan NAT dievaluasi dari atas).
Sesuaikan nama interface, subnet, dan alamat dengan konfigurasi sebenarnya sebelum dipakai. Cek aturan yang ada (hanya membaca):
/ip firewall nat print
Kelebihan:
- Berlaku untuk semua perangkat di LAN, tanpa mengubah DNS mereka.
- Satu aturan mencakup semua subdomain yang diteruskan ke proxy.
Kekurangan:
- Mengubah konfigurasi router.
- Trafik internal bolak-balik melewati router (beban tambahan, terutama untuk transfer besar seperti push image).
- Proxy melihat IP router sebagai pengirim, sehingga log dan pembatasan berbasis IP klien kurang berguna.
Perbandingan
| Split DNS | Hairpin NAT | |
|---|---|---|
| Ubah router | Tidak | Ya |
| Cakupan | Perangkat yang memakai DNS internal | Semua perangkat LAN |
| Jalur trafik | Langsung (efisien) | Lewat router |
| IP klien di log server | Asli | IP router |
| Cocok untuk | Lab, server, klien VPN | LAN umum dengan banyak perangkat |
Catatan: jangan pakai Cloudflare proxy (awan oranye) untuk registry
Mengaktifkan proxy Cloudflare juga "menghindari" hairpin NAT, karena trafik keluar ke Cloudflare lalu masuk kembali dari IP Cloudflare. Namun untuk container registry ini tidak disarankan: trafik internal jadi keluar-masuk internet, dan batas ukuran body request pada paket gratis Cloudflare dapat membuat push layer image yang besar gagal.
Ringkasan
- Hairpin NAT terjadi saat klien dan server berada di subnet yang sama, tetapi klien mengakses server lewat IP publik.
- Gejala: koneksi menggantung tanpa error;
curlberhenti di Trying <IP publik>. - Bukti:
curl --resolveke IP internal berhasil;tcpdumpmenunjukkan server membalas langsung ke klien. - Solusi: split DNS (jawab nama dengan IP internal) atau hairpin NAT di router (masquerade agar balasan lewat router).