socket有一個IP_TRANSPARENT選項,其含義就是可以使一個伺服器程式偵聽所有的IP位址,哪怕不是本機的IP位址,這個特性在實作透明代理伺服器時十分有用,而其使用也很簡單:
int opt =1;
setsockopt(server_socket,SOL_IP, IP_TRANSPARENT,&opt,sizeof(opt));
TCP可以綁定0.0.0.0,這個都知道,那麼到底用哪一個位址何時确定呢?答案是“根據連接配接源的位址反向做路由查找後确定的”。如果有一個位址A連接配接該伺服器,那麼在伺服器收到syn後,就會查找目的位址為A的路由,進而确定源位址,然而如果不設定IP_TRANSPARENT選項,則這個被連接配接的位址必須在local路由表中被找到,否則一切都免談。
是以如果我有一個沒有設定IP_TRANSPARENT選項的TCP伺服器綁定了0.0.0.0這個位址,端口綁定到80,我想這個伺服器截獲經過此地通路56.56.56.56:80的流量,怎麼辦?很簡單,知道了TCP源位址選擇的原理之後,我們隻需要設定下面的路由即可:
ip route add local 56.56.56.56 dev lo tab local
這樣一來,所有通路56.56.56.56這個位址的流量在經過本機時,都會進入local_in,因為它在local表中找到了路由。但是本機沒有56.56.56.56這個位址,本機的80端口伺服器回複syn-ack的時候,執行反向路由查找,在local表中找到了56.56.56.56的路由,進而成功傳回,最終連接配接成功在A和56.56.56.56:80之間建立。
然而思考一下,以上雖然圓滿完成了任務,但是如果有N多個目的位址,豈不是要設定N多位址在local路由表?有沒有什麼辦法隻設定很少的規則就能截獲所有到達80端口的流量呢?有的,那就是在代理伺服器的socket上設定IP_TRANSPARENT選項。
如果你想操作路由查找的過程,還是要使用政策路由。針對上述的需求,有以下配置:
可選配置1:僅僅針對網卡
ip rule add iif $流量進入的網卡 tab proxy
可選配置2:針對更複雜的五元組資訊
iptables -t mangle -A PREROUTING (為特定端口的流量打上mark)
ip rule add fwmark (上述mark) iif $流量進入的網卡 tab proxy
ip route add local 0.0.0.0/0(或者直接寫default) dev lo tab proxy
注意:增加路由表項的時候,一定要注意local這個type,這是一個路由類型,凡這個類型的路由項,一旦有流量被比對,所有的流量統統送到本地進行處理。
值得注意的是,上述配置中并不需要将路由表項添加到local表中,這是因為我們設定了IP_TRANSPARENT選項,具體的限制請參考Linux核心代碼net/ipv4/route.c中的ip_route_output_slow函數:
綜上所述,配置完成了。我們可以看到,Linux對路由的處理是怎樣進行的,如果想深入地了解它,還是需要深入了解一下iproute2工具,它提供了一個了解問題的表象。然而正如Linux其它子系統總是為發生一些讓人費解的行為一樣,網絡子系統這類行為更多,一些是遵循了RFC或者IEEE等相關标準的建議,另一些則是Linux自身的實作技巧,對于本文的情況,有以下的主題:
如果你的本機eth1的位址是4.4.4.1/24,你想将過路流量導入到本機應用層,一個很直覺但是不可用的配置是:
ip route add 1.2.3.4 via 4.4.4.1
然而當去往1.2.3.4的流量經過本機的時候,使用route -C檢視cache,發現其預設網關還是本機的預設網關,并不是你訓示的4.4.4.1,如果不了解這一點,還是需要看一下代碼是如何執行的。當去往1.2.3.4的流量經過時,很顯然會比對到上述配置的路由,然而核心在真正使用該路由前需要對該路由進行一些微調,最終生成的路由cache則是真正可用的路由項,起初比對完成後會将路由cache項的rt_gateway直接初始化為目的位址,也就是1.2.3.4:
然後在rt_set_nexthop中會判斷是繼續使用目的位址直接轉發還是使用你在路由表中配置的那個預設網關:
很顯然,隻有當FIB_RES_NH(*res).nh_scope == RT_SCOPE_LINK為真時,才會使用你配置的預設網關4.4.4.1。
接下來就是讓FIB_RES_NH(*res).nh_scope == RT_SCOPE_LINK為真,于是設定下列路由表項:
ip route add 1.2.3.4/32 scope global via 4.4.4.1 dev eth1 onlink
scope為global是被迫的,因為這是規定,下一跳必須比目的地更近才可以,是以其scope一定要比下一跳的scope更小。這樣還是不行,因為還有一個限制,那就是你的下一跳網關的位址必須是unicast的:
由于inet_addr_type傳回unicast需要保證位址不在local表中命中:
那麼下一步則将4.4.4.1這條路由從local表中删除:
ip rou del 4.4.4.1/24 tab local
OK,這下可以了,然而卻在本機發送資料到1.2.3.4的時候失敗了,這是因為源位址選擇時出了問題,此時4.4.4.1這個位址已經不在local中了。上述失敗以telnet 1.2.3.4 6666為例,在TCP進行connect的時候,在真正進入路由子產品之前,首先要調用ip_route_connect确定源位址,然而在該connect流量進入路由子產品的時候,源位址已經确定了,為4.4.4.1,然而該位址已經不在local路由表中存在,可是output路由查找在确定了源位址的情況下需要源位址在local表中或者源socket設定了IP_TRANSPARENT選項,我們知道标準的telnet是沒有這個選項的,是以會傳回:
telnet: Unable to connect to remote host: Invalid argument
這個錯誤。是以需要如下:
ip route add 1.2.3.4/32 scope global via 4.4.4.1 dev eth1 onlink src 7.7.7.7
其中7.7.7.7是臨時添加到eth1上的另外一個位址,顯然7.7.7.7是在local表中存在的。
看到這裡,其實還有更簡單的方式,前面的那段if (nh->nh_flags&RTNH_F_ONLINK)代碼是fib_check_nh的一部分,該部分執行的是設定了onlink标志的邏輯,如果不設定呢?要知道我們要做的隻是:
1.inet_addr_type(net, nh->nh_gw) == RTN_UNICAST
2.cfg->fc_scope < RT_SCOPE_LINK
于是看一下fib_check_nh的第二部分:
成功,不再報錯,然而資料也沒有到達應用層。
廢了這麼大的力氣終于将流量導入4.4.4.1了,然而真的能發往本機嗎?如果你試驗一下,将會發現核心直接在4.4.4.1的那個網卡上arp這個4.4.4.1位址:
ARP, Request who-has 4.4.4.1 tell 4.4.4.1, length 28
核心從來不會為你的預設網關再次進行路由查找,而僅僅根據路由的scope以及下一跳網關的scope直接進行位址解析,在本例中4.4.4.1是link的,那麼很顯然會直接arp,因為核心相信4.4.4.1在同鍊路層(link)的其它地方。
雖然我們都不想使用複雜的iptables,使用純路由被看作是一種妙招,路由的blackhole/unreachable以及任意引導任意流量被看作是網絡的必殺技。然而路由僅僅工作在網絡層,如果你需要更高層的參數參與過濾和引導,你不得不使用其它的手段,在Linux上使用iptables工具可以完成。當然并不是說你必須使用其防火牆和NAT功能,哪怕打個mark不也很好嗎?iptables是一個工具鍊,它可以通過mark和政策路由互動。
本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1268969