翻牆路由器的原理與實現(轉)
原文鏈接:bit.ly/fqrouter
作者的部落格:https://fqrouter.tumblr.com
CC0 1.0 Universal
在法律允許的範圍內,本文作者放棄本文的一切著作權和鄰接權。
本文的發布後鏈接:https://docs.google.com/document/d/1mmMiMYbviMxJ-DhTyIGdK7OOg581LSD1CZV4XY1OMG8/pub
開篇
GFW 具有重大的社會意義。無論是正面的社會意義,還是負面的意義。無論你是討厭,還是憎恨。它都在那裡。在可以預見的將來,牆還會繼續存在。我們要學會如何與 其共存。我是一個死搞技術的,就是打算搞技術到死的那種人。當我讀到「西廂計畫」的部落格上的這麼一段話時,我被深深的觸動了。不是為了什麼政治目的,不是 為了什麼遠大理想,僅僅做為一個死搞技術的人顯擺自己的價值,我也必須做些什麼。部落格上的原話是這麼寫的:
gfwrev.blogspot.com |
作 為個搞技術的人,我們要干點瘋狂的事。如果我們不動手,我們就要被比我們差的遠的壞技術人員欺負。這太丟人了。眼前就是,GFW這個東西,之前是我們不抱 團,讓它猖狂了。現在咱們得湊一起,想出來一個辦法讓它鬱悶一下,不能老被欺負吧。要不,等到未來,後代會嘲笑我們這些沒用的傢伙,就象我們說別人「你怎 么不反抗?」 |
我 把翻牆看成一場我們與GFW之間的博弈,是一個不斷對抗升級的動態過程。目前整體的博弈態勢來講是GFW佔了絕對的上風。我們花費了大量的金錢(買VPS 買VPN),花費大量時間(學習各種翻牆技術),而GFW只需要簡單發幾個包,配幾個路由規則就可以讓你的心血都白費。
GFW 並不需要檢查所有的上下行流量中是不是有不和諧的內容,很多時候只需要檢查連接的前幾個包就可以判斷出是否要阻斷這個連接。為了規避這種檢查,我們就需要 把所有的流量都通過第三方代理,還要忍受不穩定,速度慢等各種各樣的問題。花費的是大量的研究的時間,切換線路的時間,找出是什麼導致不能用的時間,當然 還有伺服器的租用費用和帶寬費用。我的感覺是,這就像太極里的四兩撥千斤。GFW只需要付出很小的成本,就迫使了我們去付出很大的反封鎖成本,而且這種成 本好像是越來越高了。
這 場博弈的不公平之處在於,GFW擁有國家的資源和專業的團隊。而我們做為個體,願意花費在翻牆上的時間與金錢是非常有限的。在競爭激烈的北上廣深,每天辛 苦忙碌的白領們。翻牆無非是為了方便自己的工作而已。不可能在每天上下班從擁擠的地鐵中擠出來之後再去花費已經少得可憐的業餘時間去學習自己不是翻牆根本 不需要知道的名詞到底是什麼意思。於是乎,我們得過且過。不用Google也不會死,對不對。SSH加瀏覽器設置,搞一搞也就差不多能用就行啦。但是得過 且過也越來越不好過了。從最開始的HTTP代理,到後來的SOCKS代理,到最近的OpenVPN,一個個陣亡。普通人可以使用的方式越來越少。博弈的天 平遠遠不是平衡的,而是一邊倒。
|
GFW用技術的手段達到了四兩撥千斤的作用。難道技術上就沒有辦法用四兩撥千斤的方法重新扭轉這一邊倒的局面嗎? |
辦 法肯定是有的。我能想到的趨勢是兩個。第一步是用更複雜的技術,但是提供更簡單的使用方式。簡單的HTTP代理,SOCKS明文代理早已陣亡。接下來的斗 爭需要更複雜的工具。無論是ShadowSocks還是GoAgent都在向這個方向發展。技術越複雜,意味著普通人要學習要配置的成本就越高。每個人按 照文檔,在自己的PC上配置ABC的方式已經不能滿足下一階段的鬥爭需要了。我們需要提升手裡的武器,站在一個更高的平台上。
傳 統的配置方式的共同特點是終端配置。你需要在你的PC瀏覽器上,各種應用軟體里,手機上,平板電腦上做各種各樣的配置。這樣的終端配置的方式在過去是很方 便的。別人提供一個代理,你在瀏覽器里一設置就好用了。但是在連OpenVPN都被封了的今天,這種終端配置的方式就大大限制了我們的選擇。缺點是多方面 的:
-
翻牆的方式受到終端支持的限制。特別是手機和平板電腦,不ROOT不越獄的話,選擇就非常有限了。
-
終端種類繁多,掛一漏萬。提供翻牆的工具的人不可能有精力來測試支持所有種類的終端。
-
如果家裡有多個筆記本,還有手機等便攜設備使用起來就很不方便。躺在床上要刷Twitter的時候,才發現手機的里的OpenVPN帳號已經被封了,新的那個只配置在了電腦里。
-
最主流的終端是Windows的PC機。但是在Windows上控制底層網路的運作非常不方便。給翻牆工具的作者設置了一個更高的門檻。
-
終端一般處於家庭路由器的後面。大多數直穿的穿牆方式都很難在這種網路環境下工作正常。
我選擇的升級平台是路由器和手機。如果我們把翻牆實現在了家裡的路由器上,那麼以上的問題都可以得到很好的解決。
-
翻牆方式不再受到終端的限制。只要能接入路由器,就可以翻牆。
-
提供翻牆工具的人不需要測試所有的終端是不是支持。只需要保證幾個主流的路由器型號是被支持的就可以了。測試也可以更加充分。
-
多種終端可以同時共享一個路由器。無需重複配置。
-
路由器基於的Linux作業系統給翻牆工具的作者提供了極大的便利,新的工具可以更容易地被實現出來。
-
提供了一定的直穿的可能性。
而在路由器的系統的選擇上,OpenWRT是不二人選。
|
OpenWRT提供了完整的Linux系統,和良好的開發者支持。其主要缺陷就是太強大了,太靈活了所以並不適合普通用戶直接使用。但是可以效仿蘋果的作業系統,在複雜的底層上,我們可以包裝一個最簡單的用戶界面給小白使用。 |
把翻牆平台移動到路由器上來,硬體的發展也是一個很重要的因素。現在普通的路由器已經足夠強大,足夠可擴展到可以當作一台小電腦來使了。
|
售價129元的路由器就有400Mhz的CPU,32M記憶體。我的第一台PC是奔騰166MMX,記憶體也是32M。可以外接隨身碟和3G上網卡。兩個網口,還能踹口袋裡。 |
當然使用使用路由器做為翻牆平台並不是十全十美的。比如說要多一個設備,比如說不能便攜。但是最大的問題還是在軟體集成上。對於普通人來說,給路由器刷固件,寫腳本還是非常非常困難的事情。本文在後面會單獨討論使用路由器翻牆帶來的一些困難,以及如何去應對。
把翻牆平台提升到路由器上之後,更複雜的翻牆方式就變成了可能。有幾個方面:
-
對於傳統的SSH和VPN,在路由器上可以做得更智能更方便,比如SSH翻牆也可以免去瀏覽器的設置了。
-
可以支持一些自定義的翻牆方式,比如ShadowSocks等私有協議。
-
可以實現一些直穿的穿牆方式,比如TTL注入等。
-
可以實現自學習的智能選擇行為。如果發現被封了,自動把該IP切換為走代理。
這 是趨勢一,平台的提升。趨勢二是去中心化。我相信未來的趨勢肯定不是什麼境外敵對勢力出於不可告人的目的給我們提供翻牆方式。未來的趨勢是各自為戰的,公 開販賣的各種翻牆服務會被封殺殆盡。我們要確保的底線是,做為個人,在擁有一台國外伺服器,然後有一定技術能力的情況下,能夠穩定無憂的翻牆。
在我們能夠保證獨善其身的前提下,才有可能怎麼去達著兼善天下。才有可能以各自為圓心,把服務以P2P的方式擴散給親朋好友使用。即便是能夠有這樣的互助網路建立起來,也肯定是一種與中心化的,開源的實現。只有遍地開花,才能避免被連根拔起。
|
中心化的翻牆方式,特別是商業販賣的翻牆服務註定難逃被捕殺殆盡的命運。具有光明未來的翻牆方式必然是去中心化的,鬆散的,自組織的P2P的。 |
全面學習GFW
GFW會是一個長期的存在。要學會與之共存,必須先了解GFW是什麼。做為局外人,學習GFW有六個角度。漸進的來看分別是:
首先我們學習到的是WHAT和WHEN。比如說,你經常聽到人的議論是「昨天」,「github」被封了。其中的昨天就是WHEN,github就是WHAT。這是學習GFW的最天然,最樸素的角度。在這個方面做得非常極致的是一個叫做greatfire的網站。這個網站長期監控成千上萬個網站和關鍵詞。通過長期監控,不但可以掌握WHAT被封鎖了,還可以知道WHEN被封的,WHEN被解封的。
接下來的角度是WHO。比如說,「方校長」這個人名就經常和GFW同時出現。但是如果僅僅是掌握一個兩個人名,然後像某位同志那樣天天在twitter上 罵一遍那樣,除了把這個人名罵成名人之外,沒有什麼特別的積極意義。我更看好這篇文章「通過分析論文挖掘防火長城(GFW)的技術人員」的思路。通過網路 上的公開信息,掌握GFW的哪些方面與哪些人有關係,這些合作者之間又有什麼聯繫。除了大家猜測的將來可以鞭屍之外,對現在也是有積極的意義的。比如關注 這些人的研究動態和思想發展,可以猜測GFW的下一步發展方向。比如閱讀過去發表的論文,可以了解GFW的技術演進歷史,可以從歷史中找到一些技術或者管 理體制上的缺陷。
再接下來就是WHY了。github被封之後就常聽人說,github這樣的技術網站你封它幹啥?是什麼原因促成了一個網站的被封與解封的?我們做為局外 人,真正的原因當然是無從得知的。但是我們可以猜測。基於猜測,可以把不同網站被封,與網路上的輿情時間做關聯和分類。我們知道,方校長對於網路輿情監控 是有很深入研究的。有一篇論文(Whiskey, Weed, and Wukan on the World Wide Web: On Measuring Censors』 Resources and Motivations)專門討論監管者的動機的。觀測觸發被封的事件與實際被封之間的時間關係,也可以推測出一些有趣的現象。比如有人報 告,OpenVPN觸發的封連線埠和封IP這樣的事情一般都發生在中國的白天。也就是說,GFW背後不光是機器,有一些組件是血肉構成的。
剩下的兩個角度就是對如何翻牆穿牆最有價值的兩個角度了:HOW和WHERE。HOW是非常好理解的,就是在伺服器和客戶端兩邊抓包,看看一個正常的網路 通信,GFW做為中間人,分別給兩端在什麼時候發了什麼包或者過濾掉了什麼包。而這些GFW做的動作,無論是過濾還是發偽包又是如何干擾客戶端與伺服器之 間的正常通信的。WHERE是在知道了HOW之後的進一步發展,不但要了解客戶端與伺服器這兩端的情況,更要了解GFW是掛在兩端中間的哪一級路由器上做 干擾的。在了解到GFW的關聯路由器的IP的基礎上,可以根據不同的干擾行為,不同的運營商歸屬做分組,進一步了解GFW的整體部署情況。
整體上來說,對GFW的研究都是從WHAT和WHEN開始,讓偏人文的就去研究WHO和WHY,像我們這樣偏工程的就會去研究HOW和WHERE。以上就是全面了解GFW的主體脈絡。接下來,我們就要以HOW和WHERE這兩個角度去看一看GFW的原理。
GFW的原理
要 與GFW對抗不能僅僅停留在什麼不能訪問了,什麼可以訪問之類的表面現象上。知道youtube不能訪問了,對於翻牆來說並無幫助。但是知道GFW是如何 讓我們不能訪問youtube的,則對下一步的翻牆方案的選擇和實施具有重大意義。所以在討論如何翻之前,先要深入原理了解GFW是如何封的。
總的來說,GFW是一個分散式的入侵檢測系統,並不是一個嚴格意義上的防火牆。不是說每個出入國境的IP包都需要先經過GFW的首可。做為一個入侵檢測系 統,GFW把你每一次訪問facebook都看做一次入侵,然後在檢測到入侵之後採取應對措施,也就是常見的連接重置。整個過程一般話來說就是:
檢測有兩種方式。一種是人工檢測,一種是機器檢測。你去國新辦網站舉報,就是參與了人工檢測。在人工檢測到不和諧的網站之後,就會採取一些應對方式來防止 國內的網民訪問該網站。對於這類的封鎖,規避檢測就不是技術問題了,只能從GFW採取的應對方式上採取反制措施。另外一類檢測是機器檢測,其檢測過程又可 以再進一步細分:
重建
重建是指GFW從網路上監聽過往的IP包,然後分析其中的TCP協議,最後重建出一個完整的位元組流。分析是在這個重建的位元組流上分析具體的應用協議,比如HTTP協議。然後在應用協議中查找是不是有不和諧的內容,然後決定採用何種應對方式。
所以,GFW機器檢測的第一步就是重建出一個位元組流。那麼GFW是如何拿到原始的IP包的呢?真正的GFW部署方式,外人根本無從得知。據猜測,GFW是 部署在國家的出口路由器的旁路上,用「分光」的方式把IP包複製一份到另外一根光纖上,從而拿到所有進出國境的IP包。下圖引在 gfwrev.blogspot.com:
但是Google在北京有自己的機房。所以聰明的網友就使用Google的北京機房提供的GAE服務,用Goagent軟體達到高速翻牆的目的。但是有網友證實(https://twitter.com/chengr28/status/260970749190365184), 即便是北京的機房也會被骨幹網丟包。另外一個例子是當我們訪問被封的網站觸發連接重置的時候,往往收到兩個RST包,但是TTL不同。還有一個例子是對於 被封的IP,訪問的IP包還沒有到達國際出口就已經被丟棄。所以GFW應該在其他地方也部署有設備,據推測是在省級骨幹路由的位置。
對於GFW到底在哪這個話題,最近又有國外友人表達了興趣(https://github.com/mothran/mongol)。 其原理是基於一個IP協議的特性叫TTL。TTL是Time to Live的簡寫。IP包在沒經過一次路由的時候,路由器都會把IP包的TTL減去1。如果TTL到零了,路由器就不會再把IP包發給下一級路由。然後我們 知道GFW會在監聽到不和諧的IP包之後發回RST包來重置TCP連接。那麼通過設置不同的TTL就可以知道從你的電腦,到GFW之間經過了幾個路由器。 比如說TTL設置成9不觸發RST,但是10就觸發RST,那麼到GFW就是經過了10個路由器。另外一個IP協議的特性是當TTL耗盡的時候,路由器應 該發回一個TTL EXCEEDED的ICMP包,並把自己的IP地址設置成SRC(來源)。結合這兩點,就可以探測出IP包是到了IP地址為什麼的路由器之後才被GFW檢 測到。有了IP地址之後,再結合IP地址地理位置的資料庫就可以知道其地理位置。據說,得出的位置大概是這樣的:
但是這裡檢測出來的IP到底是GFW的還是骨幹路由器的?更有可能的是骨幹路由器的IP。GFW做為一個設備用「分光」的方式掛在主幹路由器旁邊做入侵檢 測。無論如何,GFW通過某種神奇的方式,可以拿到你和國外伺服器之間來往的所有的IP包,這點是肯定的。更嚴謹的理論研究有:Internet Censorship in China: Where Does the Filtering Occur?
GFW在擁有了這些IP包之後,要做一個艱難的決定,那就是到底要不要讓你和伺服器之間的通信繼續下去。GFW不能太過於激進,畢竟全國性的不能訪問國外 的網站是違反GFW自身存在價值的。GFW就需要在理解了IP包背後代表的含義之後,再來決定是不是可以安全的阻斷你和國外伺服器之間的連接。這種理解就 要建立了前面說的「重建」這一步的基礎上。大概用圖表達一下重建是在怎麼一回事:
重建需要做的事情就是把IP包1中的GET /inde和IP包2中的x.html H和IP包3中的TTP/1.1拼到一起變成GET /index.html HTTP/1.1。拼出來的數據可能是純文本的,也可能是二進位加密的協議內容。具體是什麼是你和伺服器之間約定好的。GFW做為竊聽者需要猜測才知道你 們倆之間的交談內容。對於HTTP協議就非常容易猜測了,因為HTTP的協議是標準化的,而且是未加密的。所以GFW可以在重建之後很容易的知道,你使用 了HTTP協議,訪問的是什麼網站。
重建這樣的位元組流有一個難點是如何處理巨大的流量?這個問題在這篇部落格(https://gfwrev.blogspot.tw/2010/02/gfw.html)中已經講得很明白了。其原理與網站的負載均衡器一樣。對於給定的來源和目標,使用一個HASH演算法取得一個節點值,然後把所有符合這個來源和目標的流量都往這個節點發。所以在一個節點上就可以重建一個TCP會話的單向位元組流。
最後為了討論完整,再提兩點。雖然GFW的重建發生在旁路上是基於分光來實現的,但並不代表整個GFW的所有設備都在旁路。後面會提到有一些GFW應對形 式必須是把一些GFW的設備部署在了主幹路由上,也就是GFW是要參與部分IP的路由工作的。另外一點是,重建是單向的TCP流,也就是GFW根本不在乎 雙向的對話內容,它只根據監聽到的一個方向的內容然後做判斷。但是監聽本身是雙向的,也就是無論是從國內發到國外,還是從國外發到國內,都會被重建然後加 以分析。所以一個TCP連接對於GFW來說會被重建成兩個位元組流。具體的證據會在後面談如何直穿GFW中詳細講解。
分析
分析是GFW在重建出位元組流之後要做的第二步。對於重建來說,GFW主要處理IP協議,以及上一層的TCP和UDP協議就可以了。但是對於分析來說,GFW就需要理解各種各樣的應用層的稀奇古怪的協議了。甚至,我們也可以自己發明新的協議。
總的來說,GFW做協議分析有兩個相似,但是不同的目的。第一個目的是防止不和諧內容的傳播,比如說使用Google搜尋了「不該」搜尋的關鍵字。第二個目的是防止使用翻牆工具繞過GFW的審查。下面列舉一些已知的GFW能夠處理的協議。
對於GFW具體是怎麼達到目的一,也就是防止不和諧內容傳播的就牽涉到對HTTP協議和DNS協議等幾個協議的明文審查。大體的做法是這樣的。
像 HTTP這樣的協議會有非常明顯的特徵供檢測,所以第一步就沒什麼好說的了。當GFW發現了包是HTTP的包之後就會按照HTTP的協議規則拆包。這個拆 包過程是GFW按照它對於協議的理解來做的。比如說,從HTTP的GET請求中取得請求的URL。然後GFW拿到這個請求的URL去與關鍵字做匹配,比如 查找Twitter是否在請求的URL中。為什麼有拆包這個過程?首先,拆包之後可以更精確的打擊,防止誤殺。另外可能預先做拆包,比全文匹配更節省資 源。其次,xiaoxia和liruqi同學的jjproxy的 核心就是基於GFW的一個HTTP拆包的漏洞,當然這個bug已經被修復了。其原理就是GFW在拆解HTTP包的時候沒有處理有多出來的\r\n這樣的情 況,但是你訪問的google.com卻可以正確處理額外的\r\n的情況。從這個例子中可以證明,GFW還是先去理解協議,然後才做關鍵字匹配的。關鍵 字匹配應該就是使用了一些高效的正則表達式演算法,沒有什麼可以討論的。
這 種拆解包的過程不止發生一次。比如HTTP代理和SOCKS代理,這兩種明文的代理都可以被GFW識別。GFW可以在識別到HTTP代理和SOCKS代理 之後,再拆解其內部的HTTP協議的正文。但是如果給數據加的套是GFW不能理解的,比如你把TCP包塞到了UDP包里,GFW就不能理解正文了,從而逃 過了關鍵字匹配的檢查。
目前已知的GFW會做的協議分析如下:
DNS查詢
GFW可以分析53連線埠的UDP協議的DNS查詢。如果查詢的網域匹配關鍵字則會被DNS劫持。可以肯定的是,這個匹配過程使用的是類似正則的機制,而不 僅僅是一個黑名單,因為子網域實在太多了。證據是:2012年11月9日下午3點半開始,防火長城對Google的泛網域 .google.com 進行了大面積的污染,所有以 .google.com 結尾的網域均遭到污染而解析錯誤不能正常訪問,其中甚至包括不存在的網域(來源https://zh.wikipedia.org/wiki/%E5%9F%9F%E5%90%8D%E5%8A%AB%E6%8C%81)
目前為止53連線埠之外的查詢也沒有被劫持。但是TCP的DNS查詢已經可以被TCP RST切斷了,表明了GFW具有這樣的能力,只是不屑於大規模部署。相關的研究論文有:
HTTP GET請求
GFW可以識別出HTTP協議,並且檢查GET的URL與HOST。如果匹配了關鍵字則會觸發TCP RST阻斷。前面提到了jjproxy使用的構造特殊的HTTP GET請求欺騙GFW的做法已經失效,現在GFW只要看到\r\n就直接TCP RST阻斷了(來源https://plus.google.com/u/0/108661470402896863593/posts/6U6Q492M3yY)。相關的研究論文有:
HTTP 內容全文過濾
GFW除了會分析上行的HTTP GET請求,對於HTTP返回的內容也會做全文過濾。相關的研究論文有:
SMTP 協議
因為有很多翻牆軟體都是以郵件索取下載地址的方式發布的,所以GFW有針對性的封鎖了SMTP協議,阻止這樣的郵件往來。
封鎖有三種表現方式(https://fqrouter.tumblr.com/post/43400982633/gfw-smtp),簡單概要的說就是看郵件是不是發往上了黑名單的郵件地址的(比如xiazai@upup.info就是一個上了黑名單的郵件地址),如果發現了就立馬用TCP RST包切斷連接。
電驢(ed2k)協議
GFW還會過濾電驢(ed2k)協議中的查詢內容。因為ed2k還有一個混淆模式,會加密往來的數據包,GFW會切斷所有使用混淆模式的ed2k連接,迫使客戶端使用明文與伺服器通訊(https://fqrouter.tumblr.com/post/43490772120/gfw-ed2k)。然後如果客戶端發起了搜尋請求,查找的關鍵字中包含敏感詞的話就會被用TCP RST包切斷連接。
對翻牆流量的分析識別
GFW 的第二個目的是封殺翻牆軟體。為了達到這個目的GFW採取的手段更加暴力。原因簡單,對於HTTP協議的封殺如果做不好會影響網際網路的正常運作,GFW與 網際網路是共生的關係,它不會做威脅自己存在的事情。但是對於tor這樣的幾乎純粹是為翻牆而存在的協議,只要檢測出來就是格殺勿論的了。GFW具體是如何 封殺各種翻牆協議的,我也不是很清楚,事態仍然在不斷更新中。但是舉兩個例子來證明GFW的高超技術。
第一個例子是GFW對TOR的自動封殺,體現了GFW盡最大努力去理解協議本身。根據這篇部落格(https://blog.torproject.org/blog/knock-knock-knockin-bridges-doors)。 使用中國的IP去連接一個美國的TOR網橋,會被GFW發現。然後GFW回頭(15分鐘之後)會親自假裝成客戶端,用TOR的協議去連接那個網橋。如果確 認是TOR的網橋,則會封當時的那個連線埠。換了連線埠之後,可以用一段時間,然後又會被封。這表現出了GFW對於協議的高超檢測能力,可以從國際出口的流量 中敏銳地發現你連接的TOR網橋。據TOR的同志說是因為TOR協議中的握手過程具有太明顯的特徵了。另外一點就表現了GFW的不辭辛勞,居然會自己偽裝 成客戶端過去連連看。
第二個例子表現了GFW根本不在乎加密的流量中的具體內容是不是有敏感詞。只要疑似翻牆,特別是提供商業服務給多個翻牆,就會被封殺。根據這個帖子(https://www.v2ex.com/t/55531),使用的ShadowSocks協議。預先部署密鑰,沒有明顯的握手過程仍然被封。據說是GFW已經升級為能夠機器識別出哪些加密的流量是疑似翻牆服務的。
關於GFW是如何識別與封鎖翻牆伺服器的,最近寫了一篇文章提出我的猜想,大家可以去看看:https://fqrouter.tumblr.com/post/45969604783/gfw。
總 結起來就是,GFW已經基本上完成了目的一的所有工作。明文的協議從HTTP到SMTP都可以分析然後關鍵字檢測,甚至電驢這樣不是那麼大眾的協議GFW 都去搞了。從原理上來說也沒有什麼好研究的,就是明文,拆包,關鍵字。GFW顯然近期的工作重心在分析網路流量上,從中識別出哪些是翻牆的流量。這方面的 研究還比較少,而且一個顯著的特徵是自己用沒關係,大規模部署就容易出問題。我目前沒有在GFW是如何封翻牆工具上有太多研究,只能是道聽途說了。
應對
GFW的應對措施是三步中最明顯的,因為它最直接。GFW的重建過程和協議分析的過程需要耐心的試探才能大概推測出GFW是怎麼實現的。但是GFW的應對 手段我們每天都可以見到,比如連接重置。GFW的應對目前可以感受到的只有一個目的就是阻斷。但是從廣義上來說,應對方式應該不限於阻斷。比如說記錄下日 志,然後做統計分析,秋後算賬什麼的也可以算是一種應對。就阻斷方式而言,其實並不多,那麼我們一個個來列舉吧。
封IP
一般常見於人工檢測之後的應對。還沒有聽說有什麼方式可以直接使得GFW的機器檢測直接封IP。一般常見的現象是GFW機器檢測,然後用TCP RST重置來應對。過了一段時間才會被封IP,而且沒有明顯的時間規律。所以我的推測是,全局性的封IP應該是一種需要人工介入的。注意我強調了全局性的 封IP,與之相對的是部分封IP,比如只對你訪問那個IP封個3分鐘,但是別人還是可以訪問這樣的。這是一種完全不同的封鎖方式,雖然現象差不多,都是 ping也ping不通。要觀摩的話ping twitter.com就可以了,都封了好久了。
其實現方式是把無效的路由黑洞加入到主幹路由器的路由表中,然後讓這些主幹網上的路由器去幫GFW把到指定IP的包給丟棄掉。路由器的路由表是動態更新 的,使用的協議是BGP協議。GFW只需要維護一個被封的IP列表,然後用BGP協議廣播出去就好了。然後國內主幹網上的路由器都好像變成了GFW的一份 子那樣,成為了幫凶。
如果我們使用traceroute去檢查這種被全局封鎖的IP就可以發現,IP包還沒有到GFW所在的國際出口就已經被電信或者聯通的路由器給丟棄了。這就是BGP廣播的作用了。
DNS劫持
這也是一種常見的人工檢測之後的應對。人工發現一個不和諧網站,然後就把這個網站的網域給加到劫持列表中。其原理是基於DNS與IP協議的弱點,DNS與 IP這兩個協議都不驗証伺服器的權威性,而且DNS客戶端會盲目地相信第一個收到的答案。所以你去查詢facebook.com的話,GFW只要在正確的 答案被返回之前搶答了,然後偽裝成你查詢的DNS伺服器向你發錯誤的答案就可以了。
TCP RST阻斷
TCP 協議規定,只要看到RST包,連接立馬被中斷。從瀏覽器里來看就是連接已經被重置。我想對於這個錯誤大家都不陌生。據我個人觀感,這種封鎖方式是GFW目 前的主要應對手段。大部分的RST是條件觸發的,比如URL中包含某些關鍵字。目前享受這種待遇的網站就多得去了,著名的有facebook。還有一些網 站,會被無條件RST。也就是針對特定的IP和連線埠,無論包的內容就會觸發RST。比較著名的例子是google plus。GFW在TCP層的應對是利用了IPv4協議的弱點,也就是只要你在網路上,就假裝成任何人發包。所以GFW可以很輕易地讓你相信RST確實是 Google發的,而讓Google相信RST是你發的。
封連線埠
GFW 除了自身主體是掛在骨幹路由器旁路上的入侵檢測設備,利用分光技術從這個骨幹路由器抓包下來做入侵檢測 (所謂 IDS),除此之外這個路由器還會被用來封連線埠 (所謂 IPS)。GFW在檢測到入侵之後可以不僅僅可以用TCP RST阻斷當前這個連接,而且利用骨幹路由器還可以對指定的IP或者連線埠進行從封連線埠到封IP,設置選擇性丟包的各種封禁措施。可以理解為骨幹路由器上具 有了類似「iptables」的能力(網路層和傳輸層的實時拆包,匹配規則的能力)。這個iptables的能力在CISCO路由器上叫做ACL Based Forwarding (ABF)。而且規則的部署是全國同步的,一台路由器封了你的連線埠,全國的掛了GFW的骨幹路由器都會封。一般這種封連線埠都是針對翻牆伺服器的,如果檢測 到伺服器是用SSH或者VPN等方式提供翻牆服務。GFW會在全國的出口骨幹路由上部署這樣的一條ACL規則,來封你這個伺服器+連線埠的下行數據包。也就 是如果包是從國外發向國內的,而且src(源ip)是被封的伺服器ip,sport(源連線埠)是被封的連線埠,那麼這個包就會被過濾掉。這樣部署的規則的特 點是,上行的數據包是可以被伺服器收到的,而下行的數據包會被過濾掉。
如果被封連線埠之後伺服器採取更換連線埠的應對措施,很快會再次被封。而且多次嘗試之後會被封IP。初步推斷是,封連線埠不是GFW的自動應對行為,而是採取黑名單加人工過濾地方式實現的。一個推斷的理由就是網友報道,封連線埠都是發生在白天工作時間。
在 進入了封連線埠階段之後,還會有繼發性的臨時性封其他連線埠的現象,但是這些繼發性的封鎖具有明顯的超時時間,觸發了之後(觸發條件不是非常明確)會立即被封 鎖,然後過了一段時間就自動解封。目前對於這一波封SSH/OPENVPN採用的以封連線埠為明顯特徵的封鎖方式研究尚不深入。可以參考我最近寫的一篇文 章:https://fqrouter.tumblr.com/post/45969604783/gfw
翻牆原理
前面從原理上講解了GFW的運作原理。翻牆的原理與之相對應,分為兩大類。第一類是大家普遍的使用的繞道的方式。IP包經由第三方中轉已加密的形式通過 GFW的檢查。這樣的一種做法更像「翻」牆,是從牆外繞過去的。第二類是找出GFW檢測過程的中一些BUG,利用這些BUG讓GFW無法知道準確的會話內 容從而放行。這種做法更像「穿」牆。曾經一起一時轟動的西廂計畫第一季就是基於這種方式的實現。
基於繞道法的翻牆方式無論是VPN還是SOCKS代理,原理都是類似的。都是以國外有一個代理伺服器為前提,然後你與代理伺服器通信,代理伺服器再與目標伺服器通信。
繞道法對於IP封鎖來說,因為最終的IP包是由代理伺服器在牆外發出的,所以國內骨幹路由封IP並不會產生影響。對於TCP重置來說,因為TCP重置是以入侵檢測為前提的,客戶端與代理之間的加密通信規避了入侵檢測,使得TCP重置不會被觸發。
但是對於反DNS污染來說,VPN和SOCKS代理卻有不同。基於VPN的翻牆方法,得到正確的DNS解析的結果需要設置一個國外的沒有被污染的DNS伺服器。然後發UDP請求去解析網域的時候,VPN會用繞道的方式讓UDP請求不被劫持地通過GFW。
但是SOCKS代理和HTTP代理這些更上層的代理協議則可以選擇不同的方式。因為代理與應用之間有更緊密的關係,應用程式比如瀏覽器可以把要訪問的服務 器的網域直接告訴本地的代理。然後SOCKS代理可以選擇不在本地做解析,直接把請求發給牆外的代理伺服器。在代理伺服器去與目標伺服器做連接的時候再在 代理伺服器上做DNS解析,從而避開了GFW的DNS劫持。
VPN與SOCKS代理的另外一個主要區別是應用程式是如何使用上代理去訪問國外的伺服器的。先來看不加代理的時候,應用程式是如何訪問網路的。
應用程式把IP包交給作業系統,作業系統會去決定把包用機器上的哪塊網卡發出去。VPN的客戶端對於作業系統來說就是一個虛擬出來的網卡。應用程式完全不用知道VPN客戶端的存在,作業系統甚至也不需要區分VPN客戶端與普通網卡的區別。
VPN客戶端在啟動之後會把作業系統的預設路由改成自己。這樣所有的IP包都會經由這塊虛擬的網卡發出去。這樣VPN就能夠再打包成加密的流量發出去(當然線路還是之前的電信線路),發回去的加密流量再解密拆包交還給作業系統。
SOCKS代理等應用層的代理則不同。其流量走不走代理的線路並不是由作業系統使用路由表選擇網卡來決定的,而是在應用程式里自己做的。也就是說,對於操 作系統來說,使用SOCKS代理的TCP連接和不使用SOCKS代理的TCP連接並沒有任何的不同。應用程式自己去選擇是直接與目標伺服器建立連接,還是 與SOCKS代理伺服器建立TCP連接,然後由SOCKS代理伺服器去建立第二個TCP連接,兩個TCP連接的數據由代理伺服器中轉。
繞道法的翻牆原理就是這些了,相對來說非常簡單。其針對的都是GFW的分析那一步,通過加密使得GFW無法分析出流量的原文從而讓GFW放行。但是GFW 最近的升級表明,GFW雖然無法解密這些加密的流量,但是GFW可以結合流量與其他協議特徵探測出這些流量是不是「翻牆」的,然後就直接暴力的切斷。繞道 法的下一步發展就是要從原理弄明白,GFW是如何分析出翻牆流量的,從而要麼降低自身的流量特徵避免上短名單被協議分析,或者通過混淆協議把自己偽裝成其 他的無害流量。
穿牆原理
實驗環境準備
穿牆比翻牆要複雜得多,但也有意思得多。本章節以實驗為主。實驗的設備是家庭用的路由器,我用的是水星4530R。需要有公網IP。刷的作業系統是OpenWRT Attitude Adjustment 12.09 rc-1版本。使用的包有:
-
NetfilterQueue(https://github.com/fqrouter/fqrouter 中有)
-
bind-dig
-
shadow
-
dpkt (不是OpenWRT的包,是python的 https://dpkt.googlecode.com/files/dpkt-1.7.tar.gz )
本文並不打算詳細講解實驗環境的設置。對於有OpenWRT編譯和刷機經驗的朋友可能可以按照我的敘述重建出實驗環境來。整個實驗的關鍵在於
-
公網上的ip地址
-
Linux
-
python
-
python訪問netfilter queue的庫
如果你有一台公網上的Linux機器,安裝了Python和Python的NetfilterQueue,也可以進行同樣的實驗。
如果你使用的是路由器,需要驗証你有公網ip。這個可以訪問ifconfig.me來證實。其次要保證路由器是OpenWRT的並且有足夠的空間安裝python-mini。到這裡基本上都和普通的OpenWRT刷機沒有什麼兩樣。重點在於:
安裝Python的NetfilterQueue
OpenWRT提供了NetfilterQueue的C的庫。但是使用C來做實驗太笨重了。所以我選擇了Python。但是Python的NetfilterQueue的庫沒有在OpenWRT中。下載https://github.com/fqrouter/fqrouter 解壓後可以得到一個名字叫fqrouter的目錄。然後給feeds.conf添加一行src-link fqrouter /opt/fqrouter/package。把/opt/fqrouter替換為你解壓的目錄。然後scripts/feeds update -a,再執行scripts/feeds install python-netfilterqueue就添加好了。然後在make menuconfig中選擇Languages=>Python=>python-netfilterqueue。
有了這個庫就賦予了我們使用Python任意抓包,修改包和發包的能力。在OpenWRT上,除了python沒有第二種腳本語言可以如此簡單地獲得這些能力。
安裝Python的dpkt
能夠抓取和發送IP包之後,第二個頭疼的問題是如何解析和構造任意的IP包。Python有一個庫叫dpkt可以幫我們很好地完成這項任務。這是我們選擇Python做實驗的第二個重要理由。
在路由器上直接下載https://dpkt.googlecode.com/files/dpkt-1.7.tar.gz,然後解壓縮,拷貝其中的dpkt目錄到/usr/lib/python2.7/site-packages下。
DNS劫持觀測
我們要做的第一個實驗是用python代碼觀測到DNS劫持的全過程。
應用層觀測
dig是DNS的客戶端,可以很方便地構造出我們想要的DNS請求。dig @8.8.8.8 twitter.com。可以得到相應如下
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 5494
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;twitter.com. IN A
;; ANSWER SECTION:
twitter.com. 4666 IN A 59.24.3.173
;; Query time: 110 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Sun Jan 13 13:22:10 2013
;; MSG SIZE rcvd: 45
可以很清楚地看到我們得到的錯誤答案59.24.3.173。
抓包觀測
使用iptables我們可以讓特定的IP包經過應用層的代碼,從而使得我們用python觀測DNS查詢過程提供了可能。代碼如下,保存文件名dns_hijacking_obversation.py(https://gist.github.com/4524294):
from netfilterqueue import NetfilterQueue
import subprocess
import signal
def observe_dns_hijacking(nfqueue_element):
print('packet past through me')
nfqueue_element.accept()
nfqueue = NetfilterQueue()
nfqueue.bind(0, observe_dns_hijacking)
def clean_up(*args):
subprocess.call('iptables -D OUTPUT -p udp --dst 8.8.8.8 -j QUEUE', shell=True)
subprocess.call('iptables -D INPUT -p udp --src 8.8.8.8 -j QUEUE', shell=True)
signal.signal(signal.SIGINT, clean_up)
try:
subprocess.call('iptables -I INPUT -p udp --src 8.8.8.8 -j QUEUE', shell=True)
subprocess.call('iptables -I OUTPUT -p udp --dst 8.8.8.8 -j QUEUE', shell=True)
print('running..')
nfqueue.run()
except KeyboardInterrupt:
print('bye')
執行python dns_hijacking_observation.py,再使用dig @8.8.8.8 twitter.com應該可以看到package past through me。這就說明DNS的請求和答案都經過了python代碼了。
上一步主要是驗証NetfilterQueue是不是工作正常。這一步則要靠dpkt的了。代碼如下,文件名相同(https://gist.github.com/4524299):
from netfilterqueue import NetfilterQueue
import subprocess
import signal
import dpkt
import traceback
import socket
def observe_dns_hijacking(nfqueue_element):
try:
ip_packet = dpkt.ip.IP(nfqueue_element.get_payload())
dns_packet = dpkt.dns.DNS(ip_packet.udp.data)
print(repr(dns_packet))
for answer in dns_packet.an:
print(socket.inet_ntoa(answer['rdata']))
nfqueue_element.accept()
except:
traceback.print_exc()
nfqueue_element.accept()
nfqueue = NetfilterQueue()
nfqueue.bind(0, observe_dns_hijacking)
def clean_up(*args):
subprocess.call('iptables -D OUTPUT -p udp --dst 8.8.8.8 -j QUEUE', shell=True)
subprocess.call('iptables -D INPUT -p udp --src 8.8.8.8 -j QUEUE', shell=True)
signal.signal(signal.SIGINT, clean_up)
try:
subprocess.call('iptables -I INPUT -p udp --src 8.8.8.8 -j QUEUE', shell=True)
subprocess.call('iptables -I OUTPUT -p udp --dst 8.8.8.8 -j QUEUE', shell=True)
print('running..')
nfqueue.run()
except KeyboardInterrupt:
print('bye')
執行python dns_hijacking_observation.py,再使用dig @8.8.8.8 twitter.com應該可以看到類似如下的輸出:
DNS(ar=[RR(type=41, cls=4096)], qd=[Q(name='twitter.com')], id=8613, op=288)
DNS(an=[RR(name='twitter.com', rdata=';\x18\x03\xad', ttl=19150)], qd=[Q(name='twitter.com')], id=8613, op=33152)
59.24.3.173
DNS(an=[RR(name='twitter.com', rdata='\xc7;\x95\xe6', ttl=27), RR(name='twitter.com', rdata='\xc7;\x96\x07', ttl=27), RR(name='twitter.com', rdata="\xc7;\x96'", ttl=27)], ar=[RR(type=41, cls=512)], qd=[Q(name='twitter.com')], id=8613, op=33152)
199.59.149.230
199.59.150.7
199.59.150.39
可以看到我們發出去了一個包,收到了兩個包。其中第一個收到的包是GFW發回來的錯誤答案,第二個包才是正確的答案。但是由於dig只取第一個返回的答案,所以我們實際看到的解析結果是錯誤的。
觀測劫持發生的位置
利用IP包的TTL特性,我們可以把TTL值從1開始遞增,直到我們收到錯誤的應答為止。結合TTL EXECEEDED ICMP返回的IP地址,就可以知道DNS請求是在第幾跳的路由器分光給GFW的。代碼如下(https://gist.github.com/4524927):
from netfilterqueue import NetfilterQueue
import subprocess
import signal
import dpkt
import traceback
import socket
import sys
DNS_IP = '8.8.8.8'
# source https://zh.wikipedia.org/wiki/%E5%9F%9F%E5%90%8D%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%BC%93%E5%AD%98%E6%B1%A1%E6%9F%93
WRONG_ANSWERS = {
'4.36.66.178',
'8.7.198.45',
'37.61.54.158',
'46.82.174.68',
'59.24.3.173',
'64.33.88.161',
'64.33.99.47',
'64.66.163.251',
'65.104.202.252',
'65.160.219.113',
'66.45.252.237',
'72.14.205.99',
'72.14.205.104',
'78.16.49.15',
'93.46.8.89',
'128.121.126.139',
'159.106.121.75',
'169.132.13.103',
'192.67.198.6',
'202.106.1.2',
'202.181.7.85',
'203.161.230.171',
'207.12.88.98',
'208.56.31.43',
'209.36.73.33',
'209.145.54.50',
'209.220.30.174',
'211.94.66.147',
'213.169.251.35',
'216.221.188.182',
'216.234.179.13'
}
current_ttl = 1
def locate_dns_hijacking(nfqueue_element):
global current_ttl
try:
ip_packet = dpkt.ip.IP(nfqueue_element.get_payload())
if dpkt.ip.IP_PROTO_ICMP == ip_packet['p']:
print(socket.inet_ntoa(ip_packet.src))
elif dpkt.ip.IP_PROTO_UDP == ip_packet['p']:
if DNS_IP == socket.inet_ntoa(ip_packet.dst):
ip_packet.ttl = current_ttl
current_ttl += 1
ip_packet.sum = 0
nfqueue_element.set_payload(str(ip_packet))
else:
if contains_wrong_answer(dpkt.dns.DNS(ip_packet.udp.data)):
sys.stdout.write('* ')
sys.stdout.flush()
nfqueue_element.drop()
return
else:
print('END')
nfqueue_element.accept()
except:
traceback.print_exc()
nfqueue_element.accept()
def contains_wrong_answer(dns_packet):
for answer in dns_packet.an:
if socket.inet_ntoa(answer['rdata']) in WRONG_ANSWERS:
return True
return False
nfqueue = NetfilterQueue()
nfqueue.bind(0, locate_dns_hijacking)
def clean_up(*args):
subprocess.call('iptables -D OUTPUT -p udp --dst %s -j QUEUE' % DNS_IP, shell=True)
subprocess.call('iptables -D INPUT -p udp --src %s -j QUEUE' % DNS_IP, shell=True)
subprocess.call('iptables -D INPUT -p icmp -m icmp --icmp-type 11 -j QUEUE', shell=True)
signal.signal(signal.SIGINT, clean_up)
try:
subprocess.call('iptables -I INPUT -p icmp -m icmp --icmp-type 11 -j QUEUE', shell=True)
subprocess.call('iptables -I INPUT -p udp --src %s -j QUEUE' % DNS_IP, shell=True)
subprocess.call('iptables -I OUTPUT -p udp --dst %s -j QUEUE' % DNS_IP, shell=True)
print('running..')
nfqueue.run()
except KeyboardInterrupt:
print('bye')
執行 dig +tries=30 +time=1 @8.8.8.8 twitter.com 可以得到類似下面的輸出:
=== 隱去 ===
=== 隱去 ===
=== 隱去 ===
219.158.100.166
219.158.11.150
* 219.158.97.30
* * 219.158.27.30
* 72.14.215.130
* 209.85.248.60
* 216.239.43.19
* * END
出現*號前面的那個IP就是掛了GFW的路由了。腳本只能執行一次,第二次需要重啟。另外同一個DNS不能被同時查詢,把8.8.8.8改成你沒有在用的DNS。這個腳本的一個「副作用」就是dig返回的答案是正確的了,因為錯誤的答案被丟棄了。
反向觀測
前面我們已經知道從國內請求國外的DNS伺服器大體是怎麼一個被劫持的過程了。接下來我們在國內搭建一個伺服器,從國外往國內發請求,看看是不是可以觀測到被劫持的現象。
把路由器的WAN口的防火牆打開。配置本地的dnsmasq為使用非標準連線埠代理查詢從而保證本地做dig查詢的時候可以拿到正確的結果。然後在國外的伺服器上執行
dig @國內路由器ip twitter.com
可以看到收到的答案是錯誤的。執行前面的路由跟蹤代碼,結果如下:
=== 隱去 ===
=== 隱去 ===
=== 隱去 ===
115.160.187.13
213.248.76.73
219.158.33.181
219.158.29.129
219.158.19.165
* 219.158.96.225
* * * 219.158.101.233
END
可以看到不但有DNS劫持,而且DNS劫持發生在非常靠近國內路由器的位置。這也證實了論文中提出的觀測結果。GFW並沒有嚴格地部署在出國境前第一眺的位置,而是更加靠前。並且是雙向的,至少DNS劫持是雙向經過實驗証實了。
通過避免GFW重建請求反DNS劫持
使用非標準連線埠
這個實驗就非常簡單了。使用53之外的連線埠查詢DNS,觀測是否有錯誤答案被返回。
dig @208.67.222.222 -p 5353 twitter.com
使用的DNS伺服器是OpenDNS,連線埠為5353連線埠。使用非標準連線埠的DNS伺服器不多,並不是所有的DNS伺服器都會提供非標準連線埠供查詢。結果如下:
; <<>> DiG 9.9.1-P3 <<>> @208.67.222.222 -p 5353 twitter.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 5367
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 8192
;; QUESTION SECTION:
;twitter.com. IN A
;; ANSWER SECTION:
twitter.com. 5 IN A 199.59.150.39
twitter.com. 5 IN A 199.59.148.82
twitter.com. 5 IN A 199.59.148.10
;; Query time: 194 msec
;; SERVER: 208.67.222.222#5353(208.67.222.222)
;; WHEN: Mon Jan 14 11:47:46 2013
;; MSG SIZE rcvd: 88
可見,非標準連線埠還是可以得到正確結果的。但是這種穿牆並不能被應用程式直接使用,因為幾乎所有的應用程式都不支持使用非標準連線埠查詢。有很多種辦法把連線埠變成53連線埠能用。
-
使用本地DNS伺服器轉發(dnsmasq,pdnsd)
-
用NetfilterQueue改寫IP包
-
用iptables改寫IP包:iptables -t nat -I OUTPUT --dst 208.67.222.222 -p udp --dport 53 -j DNAT --to-destination 208.67.222.222:5353
使用TCP查詢
這個實驗就更加簡單了,也是一條命令:
dig +tcp @8.8.8.8 twitter.com
GFW在日常是不屏蔽TCP的DNS查詢的,所以可以得到正確的結果。但是和非標準連線埠一樣,幾乎所有的應用程式都不支持使用TCP查詢。已知的TCP轉UDP方式是使用pdnsd或者unbound轉(https://otnth.blogspot.jp/2012/05/openwrt-dns.html?m=1)。
但是GFW現在不屏蔽TCP的DNS查詢並不代表GFW不能這麼干。做一個小實驗:
root@OpenWrt:~# dig +tcp @8.8.8.8 dl.dropbox.com
;; communications error to 8.8.8.8#53: connection reset
可以看到GFW是能夠知道你在查詢什麼的。與HTTP關鍵字過濾一樣,一旦發現查詢的內容不恰當,立馬就發RST包過來切斷連接。那麼為什麼GFW不審查 所有的TCP的DNS查詢呢?原因很簡單,用TCP查詢的絕對少數,尚不值得這麼去干。而且就算你能查詢到正確網域,GFW自認為還有HTTP關鍵字過濾 和封IP等後著守著呢,犯不著在DNS上卡這麼死。
使用單向代理
嚴格來說單向代理並不是穿牆,因為它仍然需要在國外有一個代理伺服器。使用代理伺服器把DNS查詢發出去,但是DNS查詢並不經由代理伺服器而是直接發回 客戶端。這樣的實現在目前有更好的反劫持的手段(比如非標準連線埠)的情況下並不是一個有實際意義的做法。但是對於觀測GFW的封鎖機制還是有幫助的。據報 道在敏感時期,對DNS不僅僅是劫持,而是直接丟包。通過單向代理可以觀測丟包是針對出境流量的還是入境流量的。
客戶端需要使用iptables把DNS請求轉給NetfilterQueue,然後用python代碼把DNS請求包裝之後發給中轉代理。對於應用程式來說,這個包裝的過程是透明的,它仍然認為請求是直接發給DNS伺服器的。
客戶端代碼如下,名字叫smuggler.py(https://gist.github.com/4531012):
from netfilterqueue import NetfilterQueue
import subprocess
import signal
import traceback
import socket
IMPERSONATOR_IP = 'x.x.x.x'
IMPERSONATOR_PORT = 19840
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
def smuggle_packet(nfqueue_element):
try:
original_packet = nfqueue_element.get_payload()
print('smuggled')
udp_socket.sendto(original_packet, (IMPERSONATOR_IP, IMPERSONATOR_PORT))
nfqueue_element.drop()
except:
traceback.print_exc()
nfqueue_element.accept()
nfqueue = NetfilterQueue()
nfqueue.bind(0, smuggle_packet)
def clean_up(*args):
subprocess.call('iptables -D OUTPUT -p udp --dst 8.8.8.8 --dport 53 -j QUEUE', shell=True)
signal.signal(signal.SIGINT, clean_up)
try:
subprocess.call('iptables -I OUTPUT -p udp --dst 8.8.8.8 --dport 53 -j QUEUE', shell=True)
print('running..')
nfqueue.run()
except KeyboardInterrupt:
print('bye')
伺服器端代碼如下,名字叫impersonator.py:
import socket
import dpkt.ip
def main_loop(server_socket, raw_socket):
21while True:
packet_bytes, from_ip = server_socket.recvfrom(4096)
packet = dpkt.ip.IP(packet_bytes)
dst = socket.inet_ntoa(packet.dst)
print('%s:%s => %s:%s' % (socket.inet_ntoa(packet.src), packet.data.sport, dst, packet.data.dport))
raw_socket.sendto(packet_bytes, (dst, 0))
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
server_socket.bind(('0.0.0.0', 19840))
raw_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
try:
raw_socket.setsockopt(socket.SOL_IP, socket.IP_HDRINCL, 1)
main_loop(server_socket, raw_socket)
finally:
raw_socket.close()
finally:
server_socket.close()
在路由器上運行的時候要把WAN的防火牆規則改為接受INPUT,否則進入的UDP包會因為沒有對應的出去的UDP包而被過濾掉。這是單向代理的一個缺陷,需要在牆上開洞。把防火牆整個打開是一種開洞的極端方式。後面專門討論單向代理的時候會有更多關於防火牆鑿洞的討論。
第二個運行的條件是伺服器所在的網路沒有對IP SPROOFING做過濾。伺服器實際上使用了和GFW發錯誤答案一樣的技術,就是偽造SRC地址。通過把SRC地址填成客戶端所在的IP地址,使得DNS查詢的結果不需要經過代理伺服器中裝直接到達客戶端。
通過丟棄錯誤答案反DNS劫持
使用iptables過濾
前兩種方式都是針對GFW的重建這一步。因為GFW沒有在日常的時候監聽所有UDP連線埠以及監聽TCP流量,所以非標準連線埠或者TCP的DNS查詢可以被 放行。選擇性丟包則針對的是GFW的應對措施。既然GFW發錯誤的答案回來,只要我們不認它給的答案,等正確的答案來就是了。有兩篇相關文檔
改寫成python腳本是這樣的(https://gist.github.com/4530465),實現來自於AntiDNSPoisoning:
import sys
import subprocess
# source https://zh.wikipedia.org/wiki/%E5%9F%9F%E5%90%8D%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%BC%93%E5%AD%98%E6%B1%A1%E6%9F%93
WRONG_ANSWERS = {
'4.36.66.178',
'8.7.198.45',
'37.61.54.158',
'46.82.174.68',
'59.24.3.173',
'64.33.88.161',
'64.33.99.47',
'64.66.163.251',
'65.104.202.252',
'65.160.219.113',
'66.45.252.237',
'72.14.205.99',
'72.14.205.104',
'78.16.49.15',
'93.46.8.89',
'128.121.126.139',
'159.106.121.75',
'169.132.13.103',
'192.67.198.6',
'202.106.1.2',
'202.181.7.85',
'203.161.230.171',
'207.12.88.98',
'208.56.31.43',
'209.36.73.33',
'209.145.54.50',
'209.220.30.174',
'211.94.66.147',
'213.169.251.35',
'216.221.188.182',
'216.234.179.13'
}
rules = ['-p udp --sport 53 -m u32 --u32 "4 & 0x1FFF = 0 && 0 >> 22 & 0x3C @ 8 & 0x8000 = 0x8000 && 0 >> 22 & 0x3C @ 14 = 0" -j DROP']
for wrong_answer in WRONG_ANSWERS:
hex_ip = ' '.join(['%02x' % int(s) for s in wrong_answer.split('.')])
rules.append('-p udp --sport 53 -m string --algo bm --hex-string "|%s|" --from 60 --to 180 -j DROP' % hex_ip)
try:
for rule in rules:
print(rule)
subprocess.call('iptables -I INPUT %s' % rule, shell=True)
print('running..')
sys.stdin.readline()
except KeyboardInterrupt:
print('bye')
finally:
for rule in reversed(rules):
subprocess.call('iptables -D INPUT %s' % rule, shell=True)
本地有了這些iptables規則之後就可以丟棄掉GFW發回來的錯誤答案,從而得到正確的解析結果。這個腳本用到了兩個iptables模組一個是 u32一個是string。這兩個內核模組不是所有的linux機器都有的。比如大部分的Android手機都沒有這兩個內核模組。所以上面的腳本適合內 核模組很容易安裝的場景,比如你的ubuntu pc。因為linux的內核模組與內核版本(每次編譯基本都不同)是一一對應的,所以不同的linux機器是無法共享同樣的內核模組的。所以基於內核模組 的方案天然地具有安裝困難的缺陷。
使用nfqueue過濾
對於沒有辦法自己安裝或者編譯內核模組的場景,比如最常見的Android手機,廠家不告訴你內核的具體版本以及編譯參數,普通用戶是沒有辦法重新編譯 linux內核的。對於這樣的情況,iptables提供了nfqueue,我們可以把內核模組做的ip過濾的工作交給用戶態(也就是普通的應用程式)來 完成。
CLEAN_DNS = '8.8.8.8'
RULES = []
for iface in network_interface.list_data_network_interfaces():
# this rule make sure we always query from the "CLEAN" dns
RULE_REDIRECT_TO_CLEAN_DNS = (
{'target': 'DNAT', 'iface_out': iface, 'extra': 'udp dpt:53 to:%s:53' % CLEAN_DNS},
('nat', 'OUTPUT', '-o %s -p udp --dport 53 -j DNAT --to-destination %s:53' % (iface, CLEAN_DNS))
)
RULES.append(RULE_REDIRECT_TO_CLEAN_DNS)
RULE_DROP_PACKET = (
{'target': 'NFQUEUE', 'iface_in': iface, 'extra': 'udp spt:53 NFQUEUE num 1'},
('filter', 'INPUT', '-i %s -p udp --sport 53 -j NFQUEUE --queue-num 1' % iface)
)
RULES.append(RULE_DROP_PACKET)
# source https://zh.wikipedia.org/wiki/%E5%9F%9F%E5%90%8D%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%BC%93%E5%AD%98%E6%B1%A1%E6%9F%93
WRONG_ANSWERS = {
'4.36.66.178',
'8.7.198.45',
'37.61.54.158',
'46.82.174.68',
'59.24.3.173',
'64.33.88.161',
'64.33.99.47',
'64.66.163.251',
'65.104.202.252',
'65.160.219.113',
'66.45.252.237',
'72.14.205.99',
'72.14.205.104',
'78.16.49.15',
'93.46.8.89',
'128.121.126.139',
'159.106.121.75',
'169.132.13.103',
'192.67.198.6',
'202.106.1.2',
'202.181.7.85',
'203.161.230.171',
'203.98.7.65',
'207.12.88.98',
'208.56.31.43',
'209.36.73.33',
'209.145.54.50',
'209.220.30.174',
'211.94.66.147',
'213.169.251.35',
'216.221.188.182',
'216.234.179.13',
'243.185.187.39'
}
def handle_nfqueue():
try:
nfqueue = NetfilterQueue()
nfqueue.bind(1, handle_packet)
nfqueue.run()
except:
LOGGER.exception('stopped handling nfqueue')
dns_service_status.error = traceback.format_exc()
def handle_packet(nfqueue_element):
try:
ip_packet = dpkt.ip.IP(nfqueue_element.get_payload())
dns_packet = dpkt.dns.DNS(ip_packet.udp.data)
if contains_wrong_answer(dns_packet):
# after the fake packet dropped, the real answer can be accepted by the client
LOGGER.debug('drop fake dns packet: %s' % repr(dns_packet))
nfqueue_element.drop()
return
nfqueue_element.accept()
dns_service_status.last_activity_at = time.time()
except:
LOGGER.exception('failed to handle packet')
nfqueue_element.accept()
def contains_wrong_answer(dns_packet):
if dpkt.dns.DNS_A not in [question.type for question in dns_packet.qd]:
return False # not answer to A question, might be PTR
for answer in dns_packet.an:
if dpkt.dns.DNS_A == answer.type:
resolved_ip = socket.inet_ntoa(answer['rdata'])
if resolved_ip in WRONG_ANSWERS:
return True # to find wrong answer
else:
LOGGER.info('dns resolve: %s => %s' % (dns_packet.qd[0].name, resolved_ip))
return False # if the blacklist is incomplete, we will think it is right answer
return True # to find empty answer
這個實現摘自fqrouter android版本的dns_service.py(https://github.com/fqrouter/fqrouter/blob/master/manager/dns_service.py)。 其原理是一樣的,過濾所有的DNS應答,如果發現是錯誤的答案就丟棄。因為是基於nfqueue的,所以只要linux內核支持nfqueue,而且 iptables可以添加nfqueue的target,就可以使用以上方式來丟棄DNS錯誤答案。目前已經成功在主流的android手機上運行該程 序,並獲得正確的DNS解析結果。另外,上面的實現利用iptables的重定向能力,達到了更換本機dns伺服器的目的。無論機器設置的dns伺服器是 什麼,通過上面的iptables規則,統統給你重定向到乾淨的DNS(8.8.8.8)。
自 此DNS穿牆的討論基本上就完成了。DNS劫持是所有GFW封鎖手段中最薄弱的一環,有很多種方法都可以穿過。如果不想寫代碼,用V2EX DNS的非標準連線埠是最容易的部署方式。如果願意部署代碼,用nfqueue丟棄錯誤答案是最可靠通用的方式,不依賴於特定的伺服器。
封IP觀測
觀測twitter.com
首先使用dig獲得twitter.com的ip地址:
root@OpenWrt:~# dig +tcp @8.8.8.8 twitter.com
; <<>> DiG 9.9.1-P3 <<>> +tcp @8.8.8.8 twitter.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 8015
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;twitter.com. IN A
;; ANSWER SECTION:
twitter.com. 7 IN A 199.59.149.230
twitter.com. 7 IN A 199.59.150.39
twitter.com. 7 IN A 199.59.150.7
根據前面的內容我們知道使用dns over tcp,大部分的網域解析都不會被干擾的。這裡得到了三個ip地址。先來測試199.59.149.230
root@OpenWrt:~# traceroute 199.59.149.230 -n
traceroute to 199.59.149.230 (199.59.149.230), 30 hops max, 38 byte packets
1 123.114.32.1 19.862 ms 4.267 ms 101.431 ms
2 61.148.163.73 920.148 ms 5.108 ms 3.868 ms
3 124.65.56.129 7.596 ms 7.742 ms 7.735 ms
4 123.126.0.133 5.310 ms 7.745 ms 7.573 ms
5 * * *
6 * * *
這個結果是最常見的。在骨幹路由器上,針對這個ip丟包了。這種封鎖方式就是最傳統的封IP方式,BGP路由擴散,現象就是針對上行流量的丟包。再來看199.59.150.39
root@OpenWrt:~# traceroute 199.59.150.39 -n
traceroute to 199.59.150.39 (199.59.150.39), 30 hops max, 38 byte packets
1 123.114.32.1 14.046 ms 20.322 ms 19.918 ms
2 61.148.163.229 7.461 ms 7.182 ms 7.540 ms
3 124.65.56.157 4.491 ms 3.342 ms 7.260 ms
4 123.126.0.93 6.715 ms 7.309 ms 7.438 ms
5 219.158.4.126 5.326 ms 3.217 ms 3.596 ms
6 219.158.98.10 3.508 ms 3.606 ms 4.198 ms
7 219.158.33.254 140.965 ms 133.414 ms 136.979 ms
8 129.250.4.107 132.847 ms 137.153 ms 134.207 ms
9 61.213.145.166 253.193 ms 253.873 ms 258.719 ms
10 199.16.159.15 257.592 ms 258.963 ms 256.034 ms
11 199.16.159.55 267.503 ms 268.595 ms 267.590 ms
12 199.59.150.39 266.584 ms 259.277 ms 263.364 ms
在 我撰寫的時候,這個ip還沒有被封。但是根據經驗,twitter.com享受了最高層次的GFW關懷,新的ip基本上最慢也是隔日被封的。不過通過這個 traceroute可以看到219.158.4.126其實就是那個之前搗亂的伺服器,包是在它手裡被丟掉的(嚴格來說並不一定是 219.158.4.126,因為ip包經過的路由對於不同的目標ip設置不同的連線埠都可能會不一樣)。再來看199.59.150.7
root@OpenWrt:~# traceroute 199.59.150.7 -n
traceroute to 199.59.150.7 (199.59.150.7), 30 hops max, 38 byte packets
1 123.114.32.1 11.379 ms 10.420 ms 23.008 ms
2 61.148.163.229 6.102 ms 6.777 ms 7.373 ms
3 61.148.153.61 5.638 ms 3.148 ms 3.235 ms
4 123.126.0.9 3.473 ms 3.306 ms 3.216 ms
5 219.158.4.198 2.839 ms !H * 6.136 ms !H
這次同樣是封IP,但是現象不同。通過抓包可以觀察到是什麼問題:
root@OpenWrt:~# tcpdump -i pppoe-wan host 199.59.150.7 or icmp -vvv
07:46:11.355913 IP (tos 0x0, ttl 251, id 0, offset 0, flags [none], proto ICMP (1), length 56)
219.158.4.198 > 123.114.40.44: ICMP host r-199-59-150-7.twttr.com unreachable, length 36
IP (tos 0x0, ttl 1, id 0, offset 0, flags [DF], proto UDP (17), length 38)
123.114.40.44.45021 > r-199-59-150-7.twttr.com.33449: UDP, length 10
原來219.158.4.198發回來了一個ICMP包,內容是地址不可到達(unreachable)。於是traceroute就在那裡斷掉了。
root@OpenWrt:~# iptables -I INPUT -p icmp --icmp-type 3 -j DROP
如果把unreachable類型的ICMP包丟棄掉,會發現ip包仍然過不去
root@OpenWrt:~# traceroute 199.59.150.7 -n
traceroute to 199.59.150.7 (199.59.150.7), 30 hops max, 38 byte packets
1 123.114.32.1 4.866 ms 3.165 ms 3.212 ms
2 61.148.163.229 3.107 ms 3.104 ms 3.270 ms
3 61.148.153.61 6.001 ms 7.246 ms 7.398 ms
4 123.126.0.9 7.840 ms 7.223 ms 7.443 ms
5 * * *
這次就和被丟包了是一樣的觀測現象了。
root@OpenWrt:~# iptables -L -v -n | grep icmp
3 168 DROP icmp -- * * 0.0.0.0/0 0.0.0.0/0 icmp type 3
同時,可以看到我們仍然是收到了icmp地址不可到達的包的,只是被我們drop掉了。
觀測被封ip的反向流量
之前的觀測中,被封的ip是ip包的dst。如果我們從國外往國內發包,其src是被封的ip,那麼ip包是否會被GFW過濾掉呢?
登錄到一台國外的vps上執行下面的python代碼
from scapy.all import *
send(IP(src="199.59.150.7", dst="123.114.40.44")/ICMP())
然後在國內的路由器(123.114.40.44)上執行抓包程序
root@OpenWrt:~# tcpdump -i pppoe-wan host 199.59.150.7 or icmp -vvv
tcpdump: listening on pppoe-wan, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
10:41:14.294671 IP (tos 0x0, ttl 50, id 1, offset 0, flags [none], proto ICMP (1), length 28)
r-199-59-150-7.twttr.com > 123.114.40.44: ICMP echo request, id 0, seq 0, length 8
10:41:14.294779 IP (tos 0x0, ttl 64, id 25013, offset 0, flags [none], proto ICMP (1), length 28)
123.114.40.44 > r-199-59-150-7.twttr.com: ICMP echo reply, id 0, seq 0, length 8
可 以看到,如果該ip是src而不是dst並不會被GFW過濾。這一行為有兩種可能:要麼GFW認為封dst就可以了,不屑於再封src了。另外一種可能是 GFW封twitter的IP用的是路由表擴散技術,而傳統的路由表是基於dst做路由判斷的(高級的路由器可以根據src甚至連線埠號做為路由的依據), 所以dst路由表導致的路由黑洞並不會影響該ip為src的情況。我相信是後者,但是GFW在封個人翻牆主機上所表現的實力(對大量的ip做精確到連線埠的 全國性丟包)讓我們相信,GFW很容易把封鎖變成雙向的。不過說實話,在這個硬實力的背後,靠的更多的是CISCO下一代骨幹網路由器的超強處理能力,而 不是GFW自身。
單向代理
因 為GFW對IP的封鎖是針對上行流量的,所以使得單向代理就可以突破封鎖。上行的IP包經過單向代理轉發給目標伺服器,下行的IP包直接由目標伺服器發回 給客戶端。代碼與DNS(UDP協議)的單向代理是一樣的。因為單向代理利用的是IP協議,所以TCP與UDP都是一樣的。除了單向代理,目前尚沒有其他 的辦法穿過GFW訪問被封的IP。這也是單純的穿牆法最大的缺陷,訪問不了twitter.com。
觀測HTTP關鍵詞過濾
未完待續 (請訂閱我的部落格或者twitter獲得持續更新:https://fqrouter.tumblr.com)
====================
SOCKS 代理因為需要應用程式主動建立一個與SOCKS代理伺服器的連接,這就要求應用程式對SOCKS代理有直接的支持。這種支持並不是存在所有的應用程式中, 雖然瀏覽器基本上都支持SOCKS代理,但是仍然有不少應用程式是不支持的。為了應對這種情況,就有一些工具來把SOCKS代理的支持加到不支持的應用程 序中,更確切的說是把應用程式的TCP連接劫持過來走代理。這種工具在Windows下有SocksCap,在Linux下有tproxy。它們的工作原 理都不是在網路這個層面的,而是在作業系統的API層面,使用的技術更像病毒而不是代理。
所以兩種繞道法的主要區別在於應用程式需不需要知道代理伺服器的存在。這其是就牽涉到了一個使用體驗的問題。一般來說,使用體驗分為以下幾個層面的需求:
-
有效,會不會仍然被牆擋住
-
速度,會不會影響上傳下載的傳輸率
-
穩定,會不會經常斷掉
-
通用,所有的應用程式都支持
-
設置簡單,不需要複雜的設置,不需要重複的設置
-
花費少,伺服器租用成本,設備購買成本,帶寬成本
這些考量後面我們要單獨討論。就繞道法而言,目前的主要矛盾是如何對抗GFW的檢測。VPN和SSH都被封得特別厲害。
本文鏈接:翻牆路由器的原理與實現(轉)
美博園文章均為「原創 - 首發」,請尊重辛勞撰寫,轉載請以上面完整鏈接註明來源!
軟體著作權歸原作者!個別轉載文,本站會註明為轉載。
網 友 留 言
2條評論 in “翻牆路由器的原理與實現(轉)”這裡是你留言評論的地方
如果有個駭客團隊把牆炸倒或推到多好,省得翻牆了;即使暫時推不倒也讓共匪震動震動。
@ 征東 :
那將亂成一團