1. 程式人生 > >Linux基於mark的策略路由以及nf conntrack RELATED

Linux基於mark的策略路由以及nf conntrack RELATED

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow

也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!

                       

談到什麼是意義,話題總顯得很大,近日每晚都和老城裡的朋友聊老城的文化,老城的老房子,老城的叫賣聲,老城的方言…進行了很多的思考,也挺充實。至於技術方面,也有跟朋友以及前同事聊過,這些都是意義。又到了週末,早早起來寫一篇技術總結,至於老城的話題,我會在朋友圈零零散散地寫。

本文關鍵詞:Linux策略路由,nf_conntrack,socket,路由快取

再談“哪裡來的回哪裡”

當人們部署雙線伺服器時,比如一根線接電信,一根線接聯通,人們當然不希望同一個流的流量被跨運營商路由,一個自然而然的需求就是哪裡來的回哪裡,為了實現這個需求,一般採用的技術是策略路由(Policy Routing),基於Linux,我們可以通過下面的正規化來實現:

iptables -t mangle -A PREROUTING -j CONNMARK --restore-markiptables -t mangle -A PREROUTING -m mark ! --mark 0
-j ACCEPTiptables -t mangle -A PREROUTING -i XXX .... -j MARK --set-mark Xiptables -t mangle -A PREROUTING -m mark ! --mark 0 -j CONNMARK --save-markiptables -t mangle -A OUTPUT -j CONNMARK --restore-markip rule add fwmark X table Xip route add $net/$mask via $gw dev $device table X
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

這樣,我們就可以通過不同的標籤來識別不同線路的流量,從而通過策略路由配置多個不同的預設閘道器。

  以上這些在Linux運維圈子裡幾乎成了一個典型的認知,但是仔細看上述的正規化,好像少了點什麼…


從一個錯誤的分析說起

如果我通過電信的線路去telnet一個在伺服器上並不存在的埠,伺服器會產生一條destination port unreachable的ICMP錯誤資訊,然而這條ICMP資訊嚴格來講並不屬於這個telnet引發的TCP流,因此貌似正規化中的以下規則並不會起作用:

iptables -t mangle -A OUTPUT -j CONNMARK --restore-mark
   
  • 1

因此,後面的策略路由便不會被命中,最終這條ICMP報錯資訊並不一定會通過電信的線路返回,這完全不符合我們的預期,怎麼辦?

  事情將在此反轉。

  如果你親自試一下,並且加入下面的規則除錯:

iptables -A OUTPUT -m mark ! --mark 0x0 -j LOG
   
  • 1

發現ICMP訊息也被打上了標籤,也就是說,我們上述的擔心並不真的存在!難道上面的邏輯分析哪裡不對嗎?

  在知識構成有缺失的時候,邏輯分析非常不靠譜,此時實際動手試一下更顯的真實!我之所以強調這句話,是因為這是我2013年時面對上述問題最終解決後的總結,它對我十分有用。

  上面看似嚴謹的邏輯分析之所以是錯的,是因為我當時根本就不知道nf_conntrack實現的細節,更不曉得什麼是RELATED狀態…這個ICMP錯誤訊息雖然不屬於telnet引發的TCP流,但它跟該TCP流卻是RELATED的,而RELATED的流會繼承原始流的conntrack結構體表項,這就是問題的根本,如果缺失了這個細節,就會帶來錯誤的判斷。

  在這裡分析RELATED實現的細節會顯得喧賓奪主而不合時宜,我會在本文的附錄中給出詳細的解釋。

  在初步理解了RELATED狀態的流域引發它們的原始流之間的關係後,我接下來給出另一個正規化。


另一個正規化-把ICMP錯誤資訊引流到固定的地方

不要回答,但要審計!“不與陌生人說話”是資訊保安領域的最高要求之一,讀過《三體-黑暗森林》的應該有所體會,保持沉默永遠是最安全的。

  如果有人偽造源IP地址發動了針對你的伺服器的攻擊,你難道真的要給這個偽造的源回覆一條ICMP報錯訊息嗎?不,這麼做相當於你給陌生人說話了,偽造源的攻擊者正等著收到你的ICMP資訊呢,至少他可以通過IP頭的TTL欄位知道你離他真正有多遠吧…因此最好不要回答!

  然而,我們卻可以把這個ICMP報錯訊息傳送到我們自己特定的審計伺服器裡,便於審計服務進行離線的模式分析,所以說這個ICMP訊息某種意義上還是有用的。實現這個需求的方案依然是策略路由,和哪裡來的回哪裡正規化不同的是,這個新的正規化只需要標記由於錯誤的IP報文引發的ICMP報錯訊息報文即可,並不需要標記原始的資料包,我先把正規化列如下:

# 連線的第一個包記錄mark到conntrackiptables -t mangle -A PREROUTING -m state --state NEW -j MARK --set-mark X# 短路規則iptables -t mangle -A PREROUTING -m mark --mark 0 -j ACCEPTiptables -t mangle -A PREROUTING -j CONNMARK --save-mark # 待將mark儲存在conntrack中的任務完成後,解除mark,不影響原始流iptables -t mangle -A PREROUTING -j MARK --set-mark 0# 如果是ICMP related包,就先restore標籤,這裡會恢復mark標籤到skbiptables -t mangle -A OUTPUT -p icmp -j CONNMARK --restore-mark# 配置策略路由ip rule add fwmark X table X# 審計伺服器的路由ip route add $net/$mask via $gw dev $device table X
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

嗯,以上的正規化可以完美做到將發生的ICMP錯誤訊息路由給審計伺服器,值得注意的是,在部署審計伺服器的時候,最好將其部署在獨立的區域,即通過伺服器的某個網口僅能夠到達審計伺服器

  雖然,我們完成了任務,然而,當需求滿足了的時候,便要考慮效能優化了。好吧,又一次,我們遇到了conntrack,不可避免地,有人要問,“如果不用conntrack,如何滿足上述兩個正規化中滿足的需求,我真是受夠了conntrack了,能不用它就不用它”,這裡先給出答案,完全可以!誠然,說conntrack不好的人可能並不是真的知道conntrack到底差在哪裡,更別說它為什麼差勁了,大多數情況,這些人都是聽別人說的,因此這並不能作為不用conntrack的理由,這就好比說別人說奧迪燒機油不要買,我聽了之後就徹底看扁這個品牌了,不,不是這樣,在說一個東西不好之前,你首先要了解它。


conntrack的毛病到底在哪兒

我可以客觀的說,conntrack完全沒毛病,說它有效能問題完全是胡扯,因為既然你想到了用conntrack幫你解決的問題,那就說明你還沒有到拼效能的時候,如果真到了拼效能的地步,別說conntrack,連整個核心協議棧都需要被繞過去,君不見諸多DPI(深度包解析)平臺,有哪個是基於傳統的Linux核心協議棧的,一般的思路難道不是眾核平臺結合DPDK嗎?

  我個人認識到以上這一點是經歷了一個比較久的過程的,起初我也嘗試著用conntrack來完成一個諸如短路資訊分析和記錄這種事,後來我發現使用nf_queue來將skb上推到使用者態,在使用者態處理後再注入會更方便也更穩定,這個時候conntrack又退回到它本來的位置了,在接下來的優化中,我發現幾個鎖的開銷是繞不開的,在一番深思熟慮之後,我為conntrack加入了percpu的cache,可以參見:
一個Netfilter nf_conntrack流表查詢的優化-為conntrack增加一個per cpu cachehttp://blog.csdn.net/dog250/article/details/47193113
效能得到了大幅提升,然而此時的瓶頸成了路由查表或者socket查表…總之,如果追求極致的效能,Linux核心協議棧可能是無法勝任的,在2014年初的時候,我接觸到了Tilera平臺,跟DPDK一樣的原理,只是沒有後者通用,基於這些平臺的解決方案無一例外地都是繞過傳統的核心協議棧,直接從網絡卡中把資料包拉到使用者態,充分利用核數越來越多的CPU,這是一種新的模式,它解決了傳統協議棧無法解決的在多核平臺上鎖的問題。做個比喻,DPDK的方式不僅僅是一輛代步的質量好的轎車,而是一輛不以代步為目的的專用跑車,可能它很便宜,但它仍然是跑車。

  簡簡單單說conntrack效能差,就跟說寶馬3系比奧迪A4L要好一樣無聊。插曲在這裡說正合適。

  近期老婆要換車,主要在寶馬3系/4系和奧迪A4L/A5之間選擇,大多數人千篇一律地說什麼寶馬操控上要比奧迪好什麼什麼的,我相信除了極少數人,絕大多數人都是聽別人說的,後來老婆的一個懂車的朋友推薦買奧迪,並說了句實話,“前驅或四驅或更穩一些,應對路況的突變或者雨雪天會更好,至於操控?!你以為你開賽車玩漂移啊?!”,是的,也就30萬,40萬的車,還沒到拼百公里加速的層次,就是個普通的質量好一些的代步工具而已,這個價位的車子主要功能就是代步,而不是品玩,簡單點說,它們都是差不多的,對手車之間差別不可能太大,人人也都不是傻子,安全舒適好看要比比拼那些相差毫釐的引數更重要。所以買哪個還看自己的品牌認同感,以及看哪個順眼了。我不懂車,還沒我老婆懂,關於換車的事,我就不摻和了,但我懂Linux協議棧和內嵌的nf_conntrack,下文中我就說說conntrack如果有問題,那麼它的問題到底在哪裡。

  和核心協議棧其它的部分一樣,影響conntrack機制效能的罪魁禍首,簡單點說就是該機制的實現中內建的1把全域性自旋鎖(請注意,自旋鎖問題並不是conntrack機制獨有的!但凡多核平臺,這就是個惡魔),即nf_conntrack_lock,該自旋鎖在3個地方會被lock,我分別說。

 

1. conntrack表項初始化時

當一個數據包進入協議棧並且沒有關聯任何conntrack表項時,會為其初始化一個conntrack表項結構體,雖然它還沒有必要被立即confirm,然而還是會被置入一個連結串列,整個過程大致如下:

init_conntrack(){    spin_lock_bh(&nf_conntrack_lock);    // 這個expect機制我會在附錄裡詳述,這裡僅僅瞭解到所有expect流都在一個全域性連結串列中即可    exp = nf_ct_find_expectation(net, zone, tuple);    if (exp) {        __set_bit(IPS_EXPECTED_BIT, &ct->status);        ct->master = exp->master;    }    // 為了管理方便,所有剛剛初始化的conntrack結構體都要置入一個叫做unconfirmed的連結串列中    hlist_nulls_add_head_rcu(&ct->tuplehash[IP_CT_DIR_ORIGINAL].hnnode,                &net->ct.unconfirmed);    spin_unlock_bh(&nf_conntrack_lock);}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
 

2. conntrack表項被confirm時

記住,unconfirmed連結串列僅僅是為了讓一個conntrack結構體可被追溯,而不至於脫離管理而遊離,當它最終被confirm的時候,就意味著它要加入全域性conntrack連結串列了,此時便可以將它從unconfirmed連結串列中安全摘除了。在confirm例程中,大致的邏輯如下:

__nf_conntrack_confirm(){    spin_lock_bh(&nf_conntrack_lock);    // 從unconfirmed連結串列摘除    hlist_nulls_del_rcu(&ct->tuplehash[IP_CT_DIR_ORIGINAL].hnnode);    // 加入全域性的confirm連結串列    __nf_conntrack_hash_insert(ct, hash, repl_hash);    spin_unlock_bh(&nf_conntrack_lock);}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
 

3. conntrack銷燬刪除的時候

這個不多說,非常顯然。
—————————————
以上幾處需要lock/unlock自旋鎖的地方意味著什麼呢?

  首先只有conntrack結構體在初始化建立或者confirm的時候,才會lock/unlock這把自旋鎖,如果說當前系統中的連線都是既有的且穩定的時候,這把自旋鎖根本就不會被觸動,因此執行流便根本不會落入它所帶來的序列化瓶頸區域,這就意味著這種情況下conntrack機制的瓶頸是不存在的,至少說是不嚴重的,懂了嗎?

  那麼什麼時候使用conntrack才會影響效能?
  很簡單,有大量連線新建或刪除的時候,比如說遭遇了DDoS攻擊的時候,比如說大量短連結的時候,比如說同時大量TCP timewait連線的時候…對於DDoS情況,我在下面的文章中已經給出了一些應對措施:
SYNPROXY抵禦DDoS攻擊的原理和優化http://blog.csdn.net/dog250/article/details/77920696

對於另外的情況,也是有很多方案可化解的,比如汗牛充棟的解決TCP timewait的方案,比如HTTP協議將短連結聚合成長連線的方案(多個HTTP請求重用單獨的TCP連線)…只要我們避免了這類情況,就不必擔心conntrack帶來的效能損耗,然而,理想歸理想,你永遠也不能預料什麼時候會有一個什麼樣的資料包到達你的伺服器,就像你永遠不能預料什麼時候有什麼人會敲你家的門一樣,所以說,這個全域性自旋鎖的開銷平均下來是非常可觀的。Netfilter開發社群的猛士當然知道這個問題,當廣大使用者正在糾結於conntrack全域性鎖如何在應用層面避開的時候,社群的核心開發者們早就在機制層面給予了優化,這是一件多麼幸運的事情。

  我們來看看優化的細節,告訴大家,Linux 3.10核心還沒有這個優化,但是發現4.3往後的核心就有了(我沒有具體確認從哪個版本開始引入了這個優化,手頭上有一個4.3版本的核心,確認了一下,這個優化已經被引入),所以還是那句話,儘量升級你的核心到最高版本吧。

  優化的本質在於自旋鎖的細粒度拆分,仔細想想,unconfirmed連結串列需要全域性鎖嗎?它只是為了確保conntrack結構體不要脫繮,因此只需要本地儲存即可,沒必要搞成全域性的。說再多不如看程式碼。所以說新的優化版本邏輯如下:

init_conntrack(){    if (net->ct.expect_count) { // 這裡加了一個條件,確保只有在確實需要查詢expect連結串列的時候才會鎖定額外的全域性自旋鎖,這是另一個優化        // 注意,這裡仍然有個全域性鎖,但是卻不是每次都必進的分支,因為新增了expect_count這個條件變數        spin_lock(&nf_conntrack_expect_lock);        if (exp) {            __set_bit(IPS_EXPECTED_BIT, &ct->status);            ct->master = exp->master;        }               spin_unlock(&nf_conntrack_expect_lock);    }    // 注意,以下的inline函式將全域性的spinlock分解成了區域性的percpu spinlock    {        ct->cpu = smp_processor_id();        pcpu = per_cpu_ptr(nf_ct_net(ct)->ct.pcpu_lists, ct->cpu);        // 僅僅鎖定本地的自旋鎖        spin_lock(&pcpu->lock);        // 僅僅將conntrack加入到本地的unconfirmed連結串列中        hlist_nulls_add_head(&ct->tuplehash[IP_CT_DIR_ORIGINAL].hnnode,                 &pcpu->unconfirmed);        spin_unlock(&pcpu->lock);    }}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

可以看到,unconfirmed連結串列成了percpu本地連結串列了,因此自旋鎖的鎖定粒度大大減小了,這並不影響其它的執行邏輯,比如當需要dump所有的unconfirmed表項時,完全可以先鎖定全域性的自旋鎖再獲取,要知道,全域性自旋鎖是可以囊含本地自旋鎖的。好了,我們再看下優化後的confirm例程:

__nf_conntrack_confirm(){    {        // 從conntrack的cpu欄位獲取當初它加入的本地cpu(conntrack結構體中保留cpu欄位是個創舉)        pcpu = per_cpu_ptr(nf_ct_net(ct)->ct.pcpu_lists, ct->cpu);        spin_lock(&pcpu->lock);        // 從conntrack當初加入的cpu的本地unconfirmed連結串列刪除        hlist_nulls_del_rcu(&ct->tuplehash[IP_CT_DIR_ORIGINAL].hnnode);        spin_unlock(&pcpu->lock);    }    {        // 全域性連結串列的自旋鎖也分解成了orig以及reply兩個方向的鎖        spin_lock(nf_conntrack_locks_orig);        spin_lock(nf_conntrack_locks_reply);        __nf_conntrack_hash_insert(ct, hash, repl_hash);        spin_unlock(nf_conntrack_locks_reply);        spin_unlock(nf_conntrack_locks_orig);    }}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

通過以上的事實以及針對這些事實的論述,會發現影響conntrack效能的就是自旋鎖,然而隨著自旋鎖在不斷的細粒度化分解,這些問題將越來越不是問題,我相信未來追究會解決所有關於conntrack的效能問題的。

  也許你會說,除了連結串列插入,刪除的自旋鎖開銷,難道不得不做的查詢行為沒有開銷嗎?哈哈,開銷肯定是有的,但是你難道就不會變通一下嗎?查什麼不是查,就算你避開了查conntrack連結串列,難道你能避開查路由表或者socket連結串列嗎?所以說,幹嘛不把conntrack當成一個cache呢?把路由表以及socket表的查詢結果都儲存在conntrack表項中,這樣就可以只查conntrack連結串列了,順帶取出的還有路由(轉發時)以及socket(本地接收時),這個優化我親自實現過,效果非常好,並且conntrack是所有包括路由表和socket連結串列在內第一個被查詢的,所有這樣的優化非常容易實現,沒有實際操作過的人就不要人云亦云地詬病conntrack了,先試試我的方案再說!Together with L4_early_demux!

  看到conntrack在持續優化,我就欣慰了,因為這樣我就可以將下文的方案作為另一種稍微好的做法,而不至於作為不得已的退避手段了。好了,接下來讓我們看一下如何不使用conntrack來實現‘哪裡來哪裡去’


不使用conntrack實現“哪裡來哪裡去”正規化

接著上一節的最後論調繼續說。conntrack連線跟蹤記錄了一個五元組,而對於一個伺服器而言,一個socket在全部意義上就扮演了conntrack的角色,因此在伺服器上,即那些非轉發裝置上,可以讓socket替代conntrack表項來儲存一些必要的資訊。在這些資訊中,對於策略路由而言最關鍵的資訊就是mark!

  昔日,我們用iptables為匹配的skb打上mark,然後將mark儲存在conntrack結構體中,此後對於reply方向的包就可以將conntrack的mark給restore到skb中,對於伺服器而言,我們完全可以把匹配的skb的mark儲存在socket中,更加輕鬆的是,一旦socket有了mark,只要是本socket發出的skb,就會被自動打上相應的socket的mark,根本連iptables的restore-mark規則都不需要!

  這簡直是如魚得水,全程沒有使用conntrack,讓某些詬病conntrack的吃瓜群眾放了心。

  在具體實現上,上一節同樣說過,查什麼不是查,只要保證查一次即可,既然在early demux以及L4 recv函式中都能查socket,那麼我們自己寫一個iptables的target,在該target中實現如下的邏輯豈不是妙哉:

sk = nf_tproxy_get_sock_v4(net, skb, hp, iph->protocol, iph->saddr, laddr, hp->source, lport, skb->dev, NFT_LOOKUP_LISTENER);if (sk) {    sk->sk_mark = mark_value;}
   
  • 1
  • 2
  • 3
  • 4

請注意,mark並沒有為skb來打,而是直接打給了socket,這是為了該socket往外發包時,這些資料包會自帶該mark!這裡的socket就完全扮演了conntrack的角色。

  如此一來,只要當進入的資料包匹配了iptables的規則,那麼既定的mark就會被打入socket,然後該socket在發包的時候,會自動繼承這個mark,從而去匹配特定於mark的策略路由表等。

  這是一種實現“哪裡來哪裡去”正規化的良好手段,然而對於實現第二個正規化,即為ICMP錯誤訊息設定策略路由這種事就不好辦了,因為ICMP訊息的傳送完全是核心來自動完成的,並不是通過某個socket來發送的。這個並沒有好的手段來實現,因此必然要修改程式碼才能實現,我側重於修改icmp_send函式:

void icmp_send(struct sk_buff *skb_in, int type, int code, __be32 info){    ...    // 這裡在查路由表時並沒有體現出mark的意義    rt = icmp_route_lookup(net, &fl4, skb_in, iph, saddr, tos, type, code, &icmp_param);    ...}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

因此我只需在icmp_route_lookup增加mark就好了,也比較簡單,就是取skb_in的mark欄位作為引數傳入即可…

  本來我正準備改程式碼並編譯呢,在此之前,我看了4.9/4.14的程式碼…所有這些都已經實現了:

void icmp_send(struct sk_buff *skb_in, int type, int code, __be32 info){    ...    mark = IP4_REPLY_MARK(net, skb_in->mark);    sk->sk_mark = mark;    rt = icmp_route_lookup(net, &fl4, skb_in, iph, saddr, tos, mark,                   type, code, &icmp_param);    ...}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

程式碼就不用註釋了。關鍵在於IP4_REPLY_MARK巨集:

#define IP4_REPLY_MARK(net, mark) \    ((net)->ipv4.sysctl_fwmark_reflect ? (mark) : 0)
   
  • 1
  • 2

啊哈,抓住了大魚!就是sysctl_fwmark_reflect,sysctl引數名是net.ipv4.fwmark_reflect,是不是在高版本中我只需要把net.ipv4.fwmark_reflect設定成1就OK了呢?答案顯然是肯定的。

  因此,使用sysctl的fwmark_reflect引數來實現第二正規化可以完美解決!那麼第一正規化呢?顯然剛才我們就知道,第一正規化可以通過重新編寫一個target模組來解決,而不是用引數來解決,但是既然可以用引數解決ICMP訊息的標記問題,是不是也有引數能滿足第一正規化的需求呢?即便沒有,我覺得應該也不難,自己實現的話,最多也就一天吧,原因很簡單,只需要把conntrack中轉交mark的邏輯置於socket中即可。無比幸運的是,就連這也都不需要我自己來做了,系統針對TCP早就有了這樣的支援機制,這就是net.ipv4.tcp_fwmark_accept所起的作用。


net.ipv4.tcp_fwmark_accept引數實現“哪裡來哪裡去”正規化

我先給出正規化的配置方法吧,列如下:

# 開啟mark轉交功能sysctl -w net.ipv4.tcp_fwmark_accept=1# 僅僅為新入的SYN包來進行標記,後續包無需標記,因為fwmark_accept會將mark賦值給socketiptables -t mangle -A PREROUTING -i $dev1 -p tcp --syn -j MARK --set-mark $mark_dev1 iptables -t mangle -A PREROUTING -i $dev2 -p tcp --syn -j MARK --set-mark $mark_dev2# 配置策略路由ip rule add fwmark $mark_dev1 tab dev1ip rule add fwmark $mark_dev1 tab dev1# 分別新增路由項ip route add default via $gateway1 dev $dev1 tab dev1ip route add default via $gateway2 dev $dev2 tab dev2
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

我打算本節就此打住,但是還是忍不住想再多說一點。

  tcp_fwmark_accept這個引數帶來的功能是如何實現的呢?非常簡單,TCP連線在初始化一個request socket的時候,會呼叫以下函式來為這個將來的客戶端socket選擇一個mark:

static inline u32 inet_request_mark(const struct sock *sk, struct sk_buff *skb){    if (!sk->sk_mark && sock_net(sk)->ipv4.sysctl_tcp_fwmark_accept)        return skb->mark;    return sk->sk_mark;}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

可見,skb上的mark在tcp_fwmark_accept引數啟用的情況下成為首選。

  本節畢!


socket上的路由cache

很久以前,在我想到將路由查詢的結果放在conntrack結構體中的同時,我就想到在作為伺服器的場景下,把路由查詢結果放在socket中了,請看下面的連結:
Linux核心協議棧的socket查詢快取路由機制http://blog.csdn.net/dog250/article/details/42609663
在Linux的連線跟蹤(nf_conntrack)中快取私有資料省去每次查詢http://blog.csdn.net/dog250/article/details/42814563
悲哀!作為伺服器,Top 1卻是fib_table_lookuphttp://blog.csdn.net/dog250/article/details/51289489
但其實,Linux在實現四層協議時,天然地就支援一個路由cache。對於TCP而言,系統會將SYN-ACK的結果路由快取到socket中,這是通過下面的程式碼實現的:

tcp_v4_syn_recv_sock(){    // 接上一節,在tcp_fwmark_accept開啟時,req中便已經有了mark了,會影響接下來的路由查詢    dst = inet_csk_route_child_sock(sk, newsk, req);    // 快取路由到socket!    sk_setup_caps(newsk, dst);}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

非常乾淨直接的一個邏輯,但是同樣要考慮到的是,既然是快取,就有什麼時候過期刪除的問題,TCP和IP很好的處理了這一點,在使用這個路由快取的時候,會進行判斷,我們來看下ip_queue_xmit函式:

ip_queue_xmit(){    rt = skb_rtable(skb);    if (rt) // 這是已經路由好的資料包        goto packet_routed;    // 這裡是重點,在使用socket的路由cache前,首先要check一下。    rt = (struct rtable *)__sk_dst_check(sk, 0);    if (!rt) {}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

有兩種情況會導致socket的路由快取失效:
1. 系統路由表發生了變動
  這是很好理解的,在實現上也很精妙。系統維護了一個全域性計數器,每次路由表變動時都會遞增,而每一個路由快取都有一個ID欄位,其值就等於當前的全域性計數器的值,在使用路由快取前發現快取的ID和全域性計數器不一致,便可將快取廢了,說明路由表發生了變化,要重新路由了。
2. TCP連線發生了超時重傳
  一般情況下,除了尾部丟包和鏈路完全堵死,TCP在丟包時都會觸發快速重傳機制的,一旦發生超時重傳,意味著發生了嚴重的事情,下層的IP層路由發生了變化只是原因之一,但是發生了嚴重事情後的行為需要更加保守,所以這個時候廢除socket的路由快取是一個好主意。

附錄

附1:RELATED實現的細節

什麼是RELATED流?
  所謂的RELATED流並不是自發產生的流,而是由別的流引起的流,典型地,我將RELATED流分為兩類:
1. 可預期的帶內流
這類的典型例子就是FTP協議,在一個連線中發起另一個連線,另外還有H.323協議,SIP協議等等,也屬於這類。也就是說通過解析原始的資料包就能知道後面會有什麼樣的流通過,換句話說就是這些RELATED流是可預期的
2. 不可預期的帶外控制流
如果一個數據包在某跳發生了路由不可達事件,當前節點會向源端傳送一個ICMP訊息,鑑於IP是無連線無狀態的,對於它已經經過的所有節點而言,這個ICMP完全是不可預期的,只能通過分析這個ICMP資料包的內容來判斷它和哪個流相關聯

  以上兩類都屬於RELATED流,那麼Linux是如何處理它們的呢?

  我們先看資料包進入conntrack處理邏輯後第一個要乾的事,即nf_conntrack_in裡面發生的事:

nf_conntrack_in(){    l4proto = __nf_ct_l4proto_find(pf, protonum);    // 首先呼叫特定四層協議的error函式    if (l4proto->error != NULL) {        ret = l4proto->error(net, tmpl, skb, dataoff, pf, hooknum);        if (ret <= 0) {            return ret;        }    }    // 然後再關聯conntrack表項    ret = resolve_normal_ct(net, tmpl, skb, dataoff, pf, protonum,                l3proto, l4proto);    ...}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

從error回撥的呼叫來看,如果收到的資料包是一個ICMP包且ICMP協議有error回撥的話,它是一定會被呼叫的。ICMP當然有error回撥:

icmp_error(){    struct nf_conntrack_tuple innertuple...    innertuple = ICMP包內容中解析出來的引發它的原始資料包的元組    ctinfo = IP_CT_RELATED;    h = nf_conntrack_find_get(net, zone, &innertuple);    // 關聯RELATED包到原始的conntrack表項    nf_ct_set(skb, nf_ct_tuplehash_to_ctrack(h), ctinfo);}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

這意味著,對於ICMP錯誤訊息而言,並不產生新的conntrack表項,而是重用引發它的原始conntrack表項,但這並不是RELATED的全部,對於可預期的RELATED流而言,事情遠沒有這麼簡單。

  假設來了一個新的流,併為其建立了一個新的conntrack表項,核心是怎麼知道這個新的流是真正獨立的新流還是由前面某個舊的流引發的預期中的流呢?

  為了實現上述的判斷,系統需要把所有預期中的流全部儲存到一個容器中,然後每到來一個新流都會去檢索這個預期流容器,只要能在這個容器中被檢索到,就為其打上RELATED標籤,說明這個流並不是獨立的。新流的檢索邏輯如下:

init_conntrack(){    ... // 省略例行建立部分,這裡只關注expect流    if (net->ct.expect_count) {        // 全域性自旋鎖        spin_lock(&nf_conntrack_expect_lock);        // 檢索預期中的流表        exp = nf_ct_find_expectation(net, zone, tuple);        if (exp) {            // 打上RELATED標籤,過程省略            ...        }        spin_unlock(&nf_conntrack_expect_lock);    }    ...}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

雖然在理論上,每到來一個新流都要去檢索預期表,但是核心在這裡做了一個大大的優化,如果你知道你的預期表是空的,你還要去檢索它嗎?雖然查一個空表也不費什麼事兒,但問題的關鍵在於這個全域性的自旋鎖,在多核環境下,100個CPU同時鎖一下再釋放,玩呢?!所以說,這個優化在於,如果沒有查表的必要,就不去查了。這個優化依託於這樣一個事實,即核心在往預期流表中新增或者刪除一個項時,它是知道這件事的,也就是說,核心知道預期表的當前專案數量,如果專案數量為0,那便可以避開全域性自旋鎖了!

  擼一下程式碼,就知道expect_count這個變數在nf_ct_expect_insert(當特定協議【比如FTP】的helper回撥中發現了一個可以預期的流時,會建立一條預期流項,插入到一個expect流表中)中遞增,這正是新建一個預期表項的時刻。

  這個優化太有深意了,幾乎是一個通用的正規化!
  言歸正傳,但也沒幾句話了,在預期的流和不可預期的流都被打上了RELATED標識後,就可以用以下的iptables規則識別了:

iptables -t mangle -A PREROUTING -m state --state RELATED ...
   
  • 1

以上就是RELATED機制實現的全部了。

附2:local路由表可以新增刪除啦

曾經,我抱怨過一個事實,那就是資料包到達,首先要無條件判斷的就是這個包是不是發給本機的,如果是發給本機的,除非用iptables重新折騰它,便被本機無條件接收而根本就沒有機會去查策略路由表。

  但是現在,事情起了變化,local表現在可以刪除和重新添加了:

ip ru del pref 0 tab local
   
  • 1

從而,路由表排序可以變成下面的樣子:

10:     from all fwmark 0x7b lookup 123 32765:  from all lookup local 32766:  from all lookup main 32767:  from all lookup default 
   
  • 1
  • 2
  • 3
  • 4

這不知給多少玩家帶來了福音啊!

後記

通過世俗的方式,將宿命轉化為連續,把偶然轉化為意義!

           

給我老師的人工智慧教程打call!http://blog.csdn.net/jiangjunshow

這裡寫圖片描述