Linux NAT üzerinden ping’i nasıl işler?
(devnonsense.com)- IPv4 NAT, yanıt hedefini genellikle TCP/UDP portları ile ayırt eder; ancak
pingin ICMP echo’sunda port olmadığı için kritik nokta, Linux’un eşleme anahtarı olarak hangi değeri kullandığıdır - Deneyde ağ ad alanları kullanılarak
client1,client2,natbox,serveroluşturuluyor veiptablesMASQUERADE ile 192.168.99.0/24’ten 10.0.100.0/24’e NAT yapılan yapı yeniden üretiliyor - RFC 792 ile paket yakalama sonuçları karşılaştırıldığında, ICMP echo’nun Identifier ve Sequence Number alanlarının istek-yanıt eşleştirmesinde kullanıldığı; Linux’un ICMP
SOCK_DGRAMyolunda ise soketin local port değerinin ID olarak girdiği görülüyor - İki istemci aynı ICMP ID
999değerini kullandığında netfilter çakışmayı önlemek için bir tarafın ICMP ID’sini rastgele bir değere değiştiriyor; yanıtta ise bunu asıl istemci IP’sine ve ID’sine geri döndürüyor - Linux NAT, portu olmayan ICMP’de bile conntrack tuple’ında özgün ve yanıt yönü durumunu saklıyor ve ICMP ID’yi değiştirilebilir bir anahtar olarak kullanarak yanıtı doğru iç makineye eşliyor
Deney ortamı ve NAT yapılandırması
- Tek bir Linux makinesinde birden fazla cihazı taklit etmek için ağ ad alanları kullanılıyor
- İki istemci, NAT yönlendiricisi rolündeki
natboxve sunucu ayrı ad alanları olarak oluşturulup özel ağ ile sunucu tarafındaki ağ ayrılıyorclient1:192.168.99.1/24client2:192.168.99.2/24natboxiç arayüzü:192.168.99.3/24natboxdış arayüzü:10.0.100.1/24server:10.0.100.2/24
- Fedora 38 Server VM ve Linux kernel 6.2.9 üzerinde
ip,iptables,tcpdumpvb. root olarak çalıştırılıyor - İki istemci
br0bridge’ine bağlanıyor;natboxise sırasıyla bridge’e ve sunucu tarafındaki veth pair’e bağlanıyor - İstemcilerin varsayılan rotası
192.168.99.3olarak ayarlanarak sunucuya giden trafiğinnatboxüzerinden geçmesi sağlanıyor natboxüzerindenet.ipv4.ip_forward=1ile paket yönlendirme açılıyor veiptablesınnattablosundakiPOSTROUTINGzincirine MASQUERADE kuralı ekleniyorip netns exec natbox iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
Paket yakalamada görülen ICMP NAT
client1veserverad alanlarındatcpdump -n icmpile ICMP paketleri yakalanıyor- İstemci tarafında
192.168.99.1 > 10.0.100.2echo request ve10.0.100.2 > 192.168.99.1echo reply görünüyor - Sunucu tarafında aynı isteğin kaynak IP’sinin
10.0.100.1olarak değiştiği görülüyor; bu, NAT’ın kaynak adresininatboxın dış IP’siyle yeniden yazdığını doğruluyor - Farklı istemcilerden gelen ICMP istekleri farklı id alanlarına sahip
- Örnekte
client1için ID31428 client2için ID33391
- Örnekte
- Bu gözlem,
natboxın ICMP yanıtlarını iç istemcilere geri gönderirken ID alanından yararlanabileceğini gösteriyor
RFC 792 ve ping’in ICMP ID’si
- ICMP, 1981’de yayımlanan RFC 792 ile tanımlanmış eski bir protokoldür
- ICMP echo ve echo reply mesajlarında Type, Code, Checksum, Identifier, Sequence Number ve Data alanları bulunur
- Type, echo request ile echo reply’ı ayırt eder
- echo request’in Type değeri
8 - RFC’den alıntılanan bölümde echo reply
1olarak yazılmıştır - Code
0dır
- echo request’in Type değeri
- RFC 792, Identifier ve Sequence Number’ın echo request ile reply’ı eşleştirmede kullanılabileceğini açıklar
- Identifier, TCP/UDP’deki port gibi oturum tanımlama için kullanılabilir; Sequence Number ise her echo request’te artırılabilir
- RFC, ID’nin gerçekte nasıl seçileceğini belirtmediğinden, uygulama olan
pingin kaynak kodunu incelemek gerekir
iputils ping’de ID’nin belirlenme biçimi
pingkomutu iputils paketinde yer alırping4_send_probeyakınındaki yorumlar, ICMP echo request oluşturulurken ID alanının rastgele sayı olduğunu ve Sequence Number’ın artan bir tamsayı olduğunu açıklarpingiçindestruct ping_rtsyapısınınidentalanı bulunur- Varsayılan değer
-1dir - CLI seçeneği
-eile0ileIDENTIFIER_MAXolan0xFFFFarasındaki bir değerle üzerine yazılabilir
- Varsayılan değer
rts->ident == -1iseping,SOCK_DGRAMtipi veIPPROTO_ICMPprotokolüyle bir sokete bind eder- Linux’un
IPPROTO_ICMPsoket açıklamasına göre ICMP header’ısend()sırasında denetlenip düzenlenir ve id, soketin local port numarası olarak ayarlanır pingsource port belirtmezse Linux kernel’ın boş bir portu rastgele seçtiği ve bu portun ICMP paketinin ID’si olarak kullanıldığı anlaşılıyor
Aynı ICMP ID çakıştığında
- İki istemcide
ping -e 999kullanılarak aynı ICMP ID 999 ile sunucuya ping gönderiliyor - Sunucu yakalama sonucunda, bir istemcinin isteği ID
999değerini korurken diğer istemcinin isteği ID30218olarak değişiyor - NAT cihazı, aynı dış IP ve aynı ICMP ID kombinasyonunun çakışmaması için taraflardan birinin ID’sini değiştiriyor
- Çakışma işlemenin nerede yapıldığını bulmak için Linux’un
net/netfilterdizininde ICMPidalanını kullanan kod inceleniyor
netfilter, conntrack ve NAT’ın rolü
iptableskurallarını uygulayan kernel alt sistemi netfilterdır- MASQUERADE kuralı NAT yaptığı için ICMP NAT uygulaması da netfilter içindedir
nf_nat_core.ciçindekinf_nat_setup_info,get_unique_tupleı çağırır; bu da yenidennf_nat_l4proto_unique_tupleyoluna gidernf_nat_l4proto_unique_tupleiçindeIPPROTO_ICMPcase’i vardır vetuple->src.u.icmp.idalanına başvururnf_nat_proto.ciçindekinf_nat_manip_pkt,nf_nat_ipv4_manip_pktvel4proto_manip_pktüzerinden giderek ICMP olduğundaicmp_manip_pkti çağırıricmp_manip_pkt, gerçek ICMP ID’yihdr->un.echo.id = tuple->src.u.icmp.idile pakete yazar
conntrack tuple’ında ICMP’nin temsil edilme biçimi
- netfilter’daki connection yalnızca TCP bağlantısını ifade etmez; UDP veya ICMP gibi bağlantısız protokollerde de giden paketlerle gelen paketleri ilişkilendiren durumu ifade eder
nf_conniçindetuplehash[IP_CT_DIR_MAX]bulunurIP_CT_DIR_ORIGINAL: giden paket yönüIP_CT_DIR_REPLY: gelen yanıt yönü
- Her
nf_conntrack_tuple_hash, connection’ı tanımlayan birnf_conntrack_tupleiçerir - Tuple, değiştirilebilir
srcve değişmezdstolarak ikiye ayrılırsrciçinde IP adresi ve protokole özgü alanlar bulunur- ICMP’nin protokole özgü alanı
__be16 iddir dstiçinde değiştirilmeyen IP adresi ile ICMPtypevecodebulunur
- NAT, giden paketi nasıl değiştirdiğini connection içinde saklar ve yanıt paketinde bu değişikliği geri alır
ICMP ID’sini seçen kod yolu
natboxbir ICMP echo aldığındanf_nat_setup_infoyeni bir connection oluşturur ve kaynak IP ile ICMP ID’nin değiştirilip değiştirilmeyeceğine karar verir- Sonrasında her ICMP paketi için
nf_nat_manip_pkt, connection’da saklanan değerlere göre kaynak veya hedef IP ile ICMP ID’yi ayarlar get_unique_tuple, kullanılabilir NAT tuple’ını seçen temel yoldurfind_best_ips_protokaynak IP adresini yeniden yazarnf_nat_used_tuple, tuple’ın zaten kullanımda olup olmadığını kontrol eder; kullanımda değilse mevcut tuple’ı olduğu gibi döndürür- Bu nedenle iki istemcinin ICMP ID’leri farklıysa NAT’lanmış paketlerde de ID korunur
- Tuple zaten kullanılıyorsa
nf_nat_l4proto_unique_tupleçağrılır ve protokole özgü NAT yapılır
- ICMP’de
tuple->src.u.icmp.id, NAT hedef anahtarı olarak seçilir find_free_id,get_random_u16()ile rastgele bir ID üretir, geçerli ICMP ID aralığına göre ayarlar ve kullanımda olup olmadığını denetler- Varsayılan ID aralığı tüm ID aralığıdır;
iptablesMASQUERADE kuralında--to-portsile100-200gibi bir aralık belirtilebilir - Kullanılmayan bir tuple bulunamazsa yinelenen ID connection’da kalır ve daha sonra
__nf_conntrack_confirmyinelenmeyi algılayarak paketi drop eder
bpftrace ile kernel davranışının doğrulanması
- Anlaşılan netfilter davranışını doğrulamak için
bpftracekullanılıyor - İzlenen kernel fonksiyonları
nf_nat_setup_infovenf_nat_manip_pkttir kprobe, fonksiyon çağrısı anını;kretprobeise fonksiyon dönüş anını izlerkretprobeiçinde fonksiyon argümanlarına doğrudan erişilemediğinden, girişte argümanlar bir BPF map’e kaydedilip dönüşte yeniden okunurstruct sk_buff, Linux kernel’ın paketleri temsil ettiği veri yapısıdırbswap, ağ bayt sıralaması olan big endian’ı little endian’a çevirmek için kullanılırntop, IP adreslerini string’e dönüştürür- Güncel Linux kernel’larındaki BPF Type Format (BTF) sayesinde BPF programı,
sk_buff,nf_conngibi kernel veri yapılarına header include etmeden başvurabilir - Bu
bpftraceprogramı Linux kernel 6.2.9’da test edilmiştir; diğer kernel sürümlerinde çalışıp çalışmayacağı farklı olabilir
İzleme sonuçları ve sonuç
- İki istemci aynı ICMP ID
999ile ping gönderdiğindenf_nat_setup_infoher istemci için bir kez çağrılır - İlk istemci
192.168.99.1için hem original tuple hem reply tuple ICMP ID999değerini korur - İkinci istemci
192.168.99.2için reply tuple’ın ICMP ID’si32809olarak yeniden yazılır nf_nat_manip_pkt, echo request’teNF_NAT_MANIP_SRCile kaynak IP’yi10.0.100.1olarak değiştirir; yanıtta iseNF_NAT_MANIP_DSTile hedef IP’yi asıl istemci IP’sine geri döndürür- Yanıt paketinin ICMP ID’si de NAT’lanmış değerden istemcinin gönderdiği asıl değere geri yüklenir
- Varsayılan ICMP conntrack timeout değeri
/proc/sys/net/netfilter/nf_conntrack_icmp_timeoutiçinde görülebilir; gözlemlenen varsayılan değer 30 saniyedir - İstemci 30 saniyeden uzun süre paket göndermezse bir sonraki ping’de
nf_nat_setup_infoyeniden çağrılır - Linux’un ping NAT davranışı Netfilter Hacking HOWTO’da da belgelenmiştir; işin özü conntrack tuple’ları ve ICMP ID’nin yeniden yazılmasıdır
1 yorum
Hacker News yorumları
Sunucu başladığında sabit 3.3.3.3 adresine sabit bir ICMP echo request paketi göndermeye başlar ve bu paketin geri dönmemesi beklenir.
3.3.3.3 erişilebilir bir host da değildir, spoof edilecek hedef de değildir. Bunun yerine istemci bağlanmaya çalışırken sunucunun IP’sini bildiği için sunucuya bir ICMP Time Exceeded paketi gönderir. Bu ICMP paketinin içinde, sunucunun 3.3.3.3’e gönderdiği “orijinal” sabit paket yer alır ve bu hardcoded paket pwnat’ın tanımlayıcısı işlevini görür.
İstemci, internetteki bir hop gibi davranarak sunucuya orijinal “ICMP echo request”inin iletilemediğini bildiriyor sayılır. NAT, ICMP Time Exceeded içindeki paketin sunucunun gönderdiği paketle eşleştiğini görür ve NAT arkasındaki sunucuya iletir; bu sırada istemcinin tam IP header’ı da dahil edildiğinden sunucu istemci IP adresini öğrenebilir.
Bu aracın temel davranışı daha sonra istemci ile sunucu arasında bir UDP tüneli oluşturmaktır.
Ancak hızlıca baktığım kadarıyla NAT’ın UDP kaynak portunu yeniden yazmadığını varsayıyor gibi görünüyor; bu yüzden tüm router’larda çalışmayabilir. WebRTC vb. yerlerde kullanılan STUN daha gelişmiş teknikler uygular, ama yine de çalışmadığı durumlarda relay olan TURN kullanmak gerekir.
Aynı sorunun 3.3.3.3 ping numarası için de geçerli olma ihtimali yüksek. Yazıdaki gibi NAT ping tanımlayıcısını yeniden yazarsa bu numara bozulur.
Yanıt geldiğinde router, bu benzersiz ID değerini kullanarak yanıtı yerel ağdaki doğru cihaza iletir.
Tek bir bilgisayar ve Wireshark/tcpdump ile bunu doğrulamak mümkün.
Yazının kendisi iyi ve ağ konusunda hiç anlayışı olmayan biri için ufuk açıcı olabilir. Ama özünde, doğrudan düşünmekten çok düzgün bir ağ laboratuvarı kurup kaynak koda dalma yöntemine daha yakın görünüyor.
Bu tarz elle takip edilebilen örnekler gerçekten çok değerli ve bizzat denemeyi düşünüyorum.
Bu konuda gerçekten iyi anladığım neredeyse tek diğer yazı Tailscale’in şu yazısıydı. Çok sayıda “açıklanmış örnek” olduğu için bütünün nasıl birbirine oturduğu netleşiyor.
https://tailscale.com/blog/how-nat-traversal-works/
Tesadüfen bu hafta sonu OpenWRT router’ımda transparent proxy açmak için Netfilter ile boğuştum.
Netfilter’a bakarken temel başvuru olarak kullanılabilecek kaynaklar https://wiki.nftables.org/wiki-nftables/index.php/Main_Page ve https://www.netfilter.org/projects/nftables/manpage.html.
Ama ICMP echo request’te ID vardır ve fiilen kaynak port numarasıyla aynı rolü oynar.
ICMP echo’yu doğru şekilde NAT’lemek için, UDP’nin kaynak portunu yeniden eşler gibi ID’yi de iki yönde yeniden eşlemek gerekir.
Çünkü NAT arkasındaki makine aynı anda iki host’tan ping alırsa ve iki host tesadüfen aynı istek numarasını kullanırsa belirsizlik oluşur.
Diğer olasılık, tanımlayıcıyı yeniden yazmayıp her ID’ye bağlı uzak makine listesini tutmaktır. ID çakışırsa listede birden fazla uzak IP adresi olur; NAT arkasındaki makineden yanıt gelince NAT listeden birini seçip yanıtı o makineye gönderir ve girdiyi kaldırır.
IPv6 olsaydı ağdaki tüm node’ları değiştirmem ve dahili DNS’i de güncellemem gerekecekti.
Teoride yanımda taşıyabileceğim kendi /48’ime sahip olabilirim, ama yeni ISP’nin bunu advertise etmesi gerekir; mevcut ISP bunu yapsa bile yaygın bir şey değil.
Bir hafta önce telefon hattı koptuğunda 5G MiFi’ı çıkarıp WAN bağlantısını oraya taşıdım ve ilgili interface’te basitçe masquerade uygulamak yeterli oldu. Sinyal zayıf olduğu için iyi değildi ama çalıştı.
Sorun şu ki IPv6 yapsam bile hâlâ dual stack işletmem ya da kirli NAT soyutlamasını kullanmam gerekiyor. Bana faydası olmadan sadece iş yükünü artırıyor.
İş tarafında da aynı durum var. Dahili 172.16/12 subnet’leri kullanan araçlar birbirine bağlanıyor ve routing yapıyor; çeşitli VPN bağlantılarıyla dış dünyaya erişiyor. Sık sık yer altında park edildikleri için sinyal neredeyse hiç olmuyor ve birçok yöntemden en az birinin çalışmasını uman bir yapı var.
IPv6’ya geçilirse /48’leri yeniden taşımak gerekecek. Üstelik bu araçlar interneti çeşitli spor stadyumlarından alıyor; bunların çoğu MITM/443’ü kapatmakta ya da UDP engelini kaldırmakta bile zorlanıyor. Cumartesi sabah 10’da gelip 2 saat sonra çalışması gereken bir ortamda bu işlemez
Çift yığına geçip iş yükünü ve riski ikiye katlamanın ne gibi ticari faydası olduğunu bilmiyorum
https://en.wikipedia.org/wiki/Lindy_effect
https://stackoverflow.com/questions/31857419/how-to-send-a-m...
Ne yazık ki ping’i işletim sistemi işlediği için eş IP’deki uygulamalar mesajları okuyamıyor
Bu tür servislerin bazılarına kullanıcı alanı hook’ları sağlayıp iki taraf da NAT arkasındayken gerçek P2P’yi mümkün kılmanın zamanı gelmedi mi diye düşünüyorum. En azından salt okunur bir olay akışı gibi bir şey bile olsa. Artık bunu engelleyen bariyerlerin hepsi yapaymış gibi geliyor
Yine de ping kullanan veri sızdırma stratejileri veya başka iletişim yöntemleri gördüm. Günümüzde çoğu güvenlik duvarının varsayılan ayarları ping dâhil tüm ICMP’yi sessizce düşürdüğü için P2P amacıyla neredeyse imkânsız gibi görünüyor
idfiilen(sport, dport)ile eşdeğer görünüyor, ama 16 bit olduğu için alanı 32 bitten çok daha küçükAncak NAT hole punching’in temel sorunu, bağlantıyı kurmak için iki uçta da etkinlik gerekmesi değil mi? Bu yüzden T’ye, S düğümünün onunla konuşmak istediğini bildirecek bir koordinasyon sunucusu her zaman gerekir
Yine de düşünmeye değer. ICMP yönlendirme mesajlarıyla, örneğin unreachable veya TTL expired gibi mesajlarla bir yol olup olmadığını merak ediyorum. Bir IP’ye traceroute yaptığınızda rastgele başka IP’lerden paketler geri alırsınız ve bu genellikle NAT’tan geçer
Gelen bağlantı almak isteyen T ana makinesinin rastgele bir “kukla” IP adresi seçtiğini,
(yönlendirici IP’si, kukla IP)çiftini kendi tanımlayıcısı olarak yayımladığını ve düzenli olarak o kukla IP’ye paket gönderdiğini hayal edebiliriz. T ile konuşmak isteyen S ana makinesi, o kukla adresle ilgili ICMP TTL-expired mesajını T’nin yönlendiricisine gönderebilir; yönlendirici de bunu görüp T’ye iletebilirElbette bu, ICMP alanındaki IP adresinin IP başlığındaki adres gibi ingress filtrelemeye tabi tutulup tutulmadığına bağlı
Düzenleme: Bu fikrin bir uygulamasına işaret eden bir üst düzey yorum zaten gelmiş
[1] - https://github.com/yarrick/pingfs
GitHub’da belirli bir commit hash’i, dosya adı ve satır numarası kombinasyonuna bağlayabilirsiniz; ama kod tabanı büyük ölçüde değişirse pek faydalı olmaz. git.blender.org gibi daha az kullanılan git web görüntüleyicilerinde de pek iyi çalışmadı
https://elixir.bootlin.com/linux/latest/source