和同事討論UDP打洞技術,後做了一個簡單的實驗,由於Windows上設定NAT以及察看其原理太麻煩或者根本就不可能,於是還是使用Linux做了實驗,發現基於Linux ip_conntrack這種對稱NAT也能很簡單的實現UDP穿越,實驗很簡單,並且這種UDP穿越還不需要公網伺服器的協助(因為它們對於對稱NAT或者基於連線的NAT根本幫不上什麼忙),很實用。在展示實驗之前,首先要明白以下的知識點。
1.Linux的NAT是基於ip_conntrack的這一點說明僅僅針對五元組決定的一個連線的第一個資料包進行NAT規則的查詢和匹配並存入ip_conntrack結構體,後續的包自動應用ip_conntrack結構體中的NAT資訊進行NAT。 由於五元組標示了一個連線,因此: 1).即使是同一個內網主機發起連線,最終很大程度上也不一定能被轉換成同一個(IP地址,協議埠)對2).目的地址不同,就不是同一個連線,因此絕不可能做到像cone NAT那樣的偷樑換柱的效果
2.Linux目前還沒有實現cone NAT的模組Linux的NAT是處於Netfilter模組中的,標準協議棧中沒有任何相關支援,那麼只要Netfilter不支援cone NAT,Linux NAT就別指望能被常規方式打洞。
3.Linux的NAT盡力不改變連線的源埠Linux的NAT是透過iptables工具配置的,對於隱藏內網主機這種型別的NAT是iptables的SNAT實現的,如果你man iptables將會發現: --to-source [ipaddr[-ipaddr]][:port[-port]] which can specify a single new source IP address, an inclusive range of IP addresses, and optionally, a port range (which is only valid if the rule also specifies -p tcp or -p udp). If no port range is specified, then source ports below 512 will be mapped to other ports below 512: those between 512 and 1023 inclusive will be mapped to ports below 1024, and other ports will be mapped to 1024 or above. Where possible, no port alteration will occur. 注意最後一句,Linux盡力不改變連線的源埠,除非和另一個tuple相沖突,我們知道一個tuple就是一個五元組。 這一點正是給了我們一點啟示,那就是可以透過嘗試不同埠,直到找到沒有被改變源埠的那次連線。而印證這一點也很容易,那就是打洞成功。這也從反方面說明沒有必要存在外部伺服器了,因為它根本幫不上什麼忙。
【文章福利】需要C/C++ Linux伺服器架構師學習資料加群812855908(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等)
4.TCP很難穿越Linux這種對稱NAT這是為什麼呢?起初,我以為可以採用偽造ACK包的方式進行穿越,然而實驗沒有成功,後來看了原始碼發現ip_conntrack對TCP的狀態機,序列號範圍進行了嚴格的審查,凡是不透過的...怎樣呢?ip_conntrack沒有權力丟棄它,而是直接return ACCEPT了,這下就根本別指望能購匹配到任何既有的conntrack了,比如以下的場景:
A-模擬公網主機10.16.0.1;
N-模擬NAT主機10.16.0.254;B-N後面的主機;
最終目標:A連線B
配置:A上丟棄到來的TCP 6667資料包
行為:
1).B繫結埠6667連線A的埠66672).連線順利透過N,在N上留下了: tcp 6 52 SYN_SENT src=172.16.0.35 dst=10.16.0.1 sport=6667 dport=6667 packets=3 bytes=180 [UNREPLIED] src=10.16.0.1 dst=10.16.0.254 sport=6667 dport=6667 packets=0 bytes=0 mark=0 secmark=0 use=1 一條UNREPLIED的conntrack 3).此時B反客為主,停掉客戶端程式,啟動監聽同一埠的伺服器;4).A以埠6667連線N的66675).SYN資料包到達N,被N發回了reset,實驗失敗。 這個原理很簡單,如果看一下ip_conntrack_in這個ip_conntrack的入口函式,將會發現一個下面的邏輯:
ret = proto->packet(ct, *pskb, ctinfo);if (ret < 0) { /* Invalid: inverse of the return code tells * the netfilter core what to do*/ nf_conntrack_put((*pskb)->nfct); (*pskb)->nfct = NULL; CONNTRACK_STAT_INC(invalid); //如果發現狀態或者序列號有問題,直接返回,跳出Netfilter的當前HOOK點 return -ret;}...if (set_reply) set_bit(IPS_SEEN_REPLY_BIT, &ct->status);
packet回撥函式實際上就是tcp_packet函式,裡面有複雜的狀態機檢查,序列號檢查等邏輯,因此出錯後直接返回,進而即使找到了與之相匹配的/proc/net/ip_conntrack檔案中的連線,由於無法繼續執行下面的邏輯,所以始終不會將對應連線的UNREPLIED字眼抹去,資料包繼續進入協議棧的上層,最終發往N本地,由於N沒有監聽6667,因此reset。
Linux NAT的這種行為說明很難用常規的方式去穿越.
實驗簡述理解了上述的知識點之後,如何進行實驗就很簡單了,我們只需要3臺機器來模擬,拓撲場景和上述4中的一模一樣,只是將TCP換成了UDP,很容易就成功了,如果不成功怎麼辦呢?如果不成功一定是因為在NAT的時候源埠被改掉了,那麼我們只需要再多試幾個埠,直到找到那個不被修改掉的埠為止。對於TCP,由於還沒有找到什麼方法,只有放棄了... 如果希望驗證兩臺同在NAT後面的機器的連通性,原理一樣,由於沒有那麼多機器,作罷了。
註解:關於cone NATcone NAT實際上是一種主要以“節約IP地址”為目標的NAT裝置,它們並沒有維護各個連線的狀態,也不採取任何安全策略,是雙向的,對於full cone裝置,一臺內部主機會被對映為一個特定的地址,埠對,而不管它訪問什麼目標,從任何目標到達full cone NAT裝置的對映後端口的都能被該NAT裝置轉發至內網。 也就是說,所謂的cone NAT正如其名字一樣,是一把錐子形狀的,從NAT裝置向遠方輻散開來,一個IP地址,埠對對應多個目標地址,埠對。很顯然,這種NAT肯定不是基於連線實現的,而是基於“每包”來實現的。為了提高安全性,還是需要使用對稱的NAT或者不管它是什麼型別的NAT,只要是基於連線來實現的就行,正如Linux的實現一樣,如果以地址,埠對來看,它確實有時是錐子形的,然而如果以連線session來看,它就是對稱的,這種NAT的穿越是很難的。