Jump to content

Hairpin NAT

From Haidhi Wiki

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

$ 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.100 bila 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; curl berhenti di Trying <IP publik>.
  • Bukti: curl --resolve ke IP internal berhasil; tcpdump menunjukkan server membalas langsung ke klien.
  • Solusi: split DNS (jawab nama dengan IP internal) atau hairpin NAT di router (masquerade agar balasan lewat router).