1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        LVS原理與實現(xiàn) - 實現(xiàn)篇

        共 18106字,需瀏覽 37分鐘

         ·

        2021-01-27 07:15

        在上一篇文章中,我們主要介紹了?LVS?的原理,接下來我們將會介紹?LVS?的代碼實現(xiàn)。

        本文使用的內(nèi)核版本是:2.4.23,而 LVS 的代碼在路徑:?/src/net/ipv4/ipvs?中。

        Netfilter

        在介紹?LVS?的實現(xiàn)前,我們需要了解以下?Netfilter?這個功能,因為?LVS?的實現(xiàn)使用了?Netfilter?的功能。

        Netfilter:顧名思義就是網(wǎng)絡(luò)過濾器(Network Filter),是 Linux 系統(tǒng)特有的網(wǎng)絡(luò)子系統(tǒng),用于過濾或修改進出內(nèi)核協(xié)議棧的網(wǎng)絡(luò)數(shù)據(jù)包。一般可以用來實現(xiàn)網(wǎng)絡(luò)防火墻功能,其中?iptables?就是基于?Netfilter?實現(xiàn)的。

        Linux 內(nèi)核處理進出網(wǎng)絡(luò)協(xié)議棧的數(shù)據(jù)包分為5個不同的階段,Netfilter?通過這5個階段注入鉤子函數(shù)(Hooks Function)來實現(xiàn)對數(shù)據(jù)包的過濾和修改。如下圖的藍色方框所示:


        這5個階段分為:

        • PER_ROUTING路由前階段,發(fā)生在內(nèi)核對數(shù)據(jù)包進行路由判決前。

        • LOCAL_IN本地上送階段,發(fā)生在內(nèi)核通過路由判決后。如果數(shù)據(jù)包是發(fā)送給本機的,那么就把數(shù)據(jù)包上送到上層協(xié)議棧。

        • FORWARD轉(zhuǎn)發(fā)階段,發(fā)生在內(nèi)核通過路由判決后。如果數(shù)據(jù)包不是發(fā)送給本機的,那么就把數(shù)據(jù)包轉(zhuǎn)發(fā)出去。

        • LOCAL_OUT本地發(fā)送階段,發(fā)生在對發(fā)送數(shù)據(jù)包進行路由判決之前。

        • POST_ROUTING路由后階段,發(fā)生在對發(fā)送數(shù)據(jù)包進行路由判決之后。

        當向?Netfilter?的這5個階段注冊鉤子函數(shù)后,內(nèi)核會在處理數(shù)據(jù)包時,根據(jù)所在的不同階段來調(diào)用這些鉤子函數(shù)對數(shù)據(jù)包進行處理。向?Netfilter?注冊鉤子函數(shù)可以通過函數(shù)?nf_register_hook()?來進行,nf_register_hook()?函數(shù)的原型如下:

        int nf_register_hook(struct nf_hook_ops *reg);

        其中參數(shù)?reg?是類型為?struct nf_hook_ops?結(jié)構(gòu)的指針,struct nf_hook_ops?結(jié)構(gòu)的定義如下:

        struct nf_hook_ops{    struct list_head list;    nf_hookfn *hook;    int pf;    int hooknum;    int priority;};

        struct nf_hook_ops?結(jié)構(gòu)各個字段的作用如下:

        • list:用于連接同一階段中所有相同的鉤子函數(shù)列表。

        • hook:鉤子函數(shù)指針。

        • pf:協(xié)議類型,因為?Netfilter?可以用于不同的協(xié)議,如 IPV4 和 IPV6 等。

        • hooknum:所處的階段,也就是上面所說的5個不同的階段。

        • priority:優(yōu)先級,值越大優(yōu)先級約小。

        所以要使用?Netfilter?對網(wǎng)絡(luò)數(shù)據(jù)包進行處理,只需要編寫好處理數(shù)據(jù)包的鉤子函數(shù),然后通過調(diào)用?nf_register_hook()?函數(shù)向?Netfilter?注冊即可。

        另外,鉤子函數(shù)?nf_hookfn?的原型如下:

        typedef unsigned int nf_hookfn(unsigned int hooknum, struct sk_buff **skb,     const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *));

        其參數(shù)說明如下:

        • hooknum:所處的階段,也就是上面所說的5個不同的階段。

        • skb:要處理的數(shù)據(jù)包。

        • in:輸入設(shè)備。

        • out:輸出設(shè)備。

        • okfn:如果鉤子函數(shù)執(zhí)行成功,即調(diào)用這個函數(shù)完成對數(shù)據(jù)包的后續(xù)處理工作。

        Netfilter?相關(guān)的知識點就介紹到這里,以后有機會會詳解講解?Netfilter?的原理和現(xiàn)實。

        LVS 實現(xiàn)

        前面我們主要簡單介紹了?Netfilter?的使用,接下來我們將要分析?LVS?的代碼實現(xiàn)。

        1. 鉤子函數(shù)注冊

        LVS?主要通過向?Netfilter?的3個階段注冊鉤子函數(shù)來對數(shù)據(jù)包進行處理,如下圖:


        • 在?LOCAL_IN?階段注冊了?ip_vs_in()?鉤子函數(shù)。

        • 在?FORWARD?階段注冊了?ip_vs_out()?鉤子函數(shù)。

        • 在?POST_ROUTING?階段注冊了?ip_vs_post_routing()?鉤子函數(shù)。

        我們在 LVS 的初始化函數(shù)?ip_vs_init()?可以找到這些鉤子函數(shù)的注冊代碼,如下:

        static struct nf_hook_ops ip_vs_in_ops = {    { NULL, NULL },    ip_vs_in, PF_INET, NF_IP_LOCAL_IN, 100};
        static struct nf_hook_ops ip_vs_out_ops = { { NULL, NULL }, ip_vs_out, PF_INET, NF_IP_FORWARD, 100};
        static struct nf_hook_ops ip_vs_post_routing_ops = { { NULL, NULL }, ip_vs_post_routing, PF_INET, NF_IP_POST_ROUTING, NF_IP_PRI_NAT_SRC-1};
        static int __init ip_vs_init(void){ int ret; ... ret = nf_register_hook(&ip_vs_in_ops); ... ret = nf_register_hook(&ip_vs_out_ops); ... ret = nf_register_hook(&ip_vs_post_routing_ops); ... return ret;}
        • LOCAL_IN?階段:在路由判決之后,如果發(fā)現(xiàn)數(shù)據(jù)包是發(fā)送給本機的,那么就調(diào)用?ip_vs_in()?函數(shù)對數(shù)據(jù)包進行處理。

        • FORWARD?階段:在路由判決之后,如果發(fā)現(xiàn)數(shù)據(jù)包不是發(fā)送給本機的,調(diào)用?ip_vs_out()?函數(shù)對數(shù)據(jù)包進行處理。

        • POST_ROUTING?階段:在發(fā)送數(shù)據(jù)前,需要調(diào)用?ip_vs_post_routing()?函數(shù)對數(shù)據(jù)包進行處理。

        2. LVS 角色介紹

        在介紹這些鉤子函數(shù)之前,我們先來了解一下?LVS?中的四個角色。如下:

        • ip_vs_service:服務(wù)配置對象,主要用于保存 LVS 的配置信息,如 支持的?傳輸層協(xié)議虛擬IP?和?端口?等。

        • ip_vs_dest:真實服務(wù)器對象,主要用于保存真實服務(wù)器 (Real-Server) 的配置,如?真實IP端口?和?權(quán)重?等。

        • ip_vs_scheduler:調(diào)度器對象,主要通過使用不同的調(diào)度算法來選擇合適的真實服務(wù)器對象。

        • ip_vs_conn:連接對象,主要為了維護相同的客戶端與真實服務(wù)器之間的連接關(guān)系。這是由于 TCP 協(xié)議是面向連接的,所以同一個的客戶端每次選擇真實服務(wù)器的時候必須保存一致,否則會出現(xiàn)連接中斷的情況,而連接對象就是為了維護這種關(guān)系。

        各個角色之間的關(guān)系如下圖所示:


        從上圖可以看出,ip_vs_service?對象的?destinations?字段用于保存?ip_vs_dest?對象的列表,而?scheduler?字段指向了一個?ip_vs_scheduler?對象。

        ip_vs_scheduler?對象的?schedule?字段指向了一個調(diào)度算法函數(shù),通過這個調(diào)度函數(shù)可以從?ip_vs_service?對象的?ip_vs_dest?對象列表中選擇一個合適的真實服務(wù)器。

        那么,ip_vs_service?對象和?ip_vs_dest?對象的信息怎么來的呢?答案是通過用戶配置創(chuàng)建。例如可以通過下面的命令來創(chuàng)建?ip_vs_service?對象和?ip_vs_dest?對象:

        node1?>?$?ipvsadm?-A?-t?node1:80?-s?wrrnode1?>?$?ipvsadm?-a?-t?node1:80?-r?node2?-m?-w?3node1?>?$?ipvsadm?-a?-t?node1:80?-r?node3?-m?-w?5

        第一行用于創(chuàng)建一個?ip_vs_service?對象,而第二和第三行用于向?ip_vs_service?對象添加?ip_vs_dest?對象到?destinations?列表中。關(guān)于 LVS 的配置這里不作詳細介紹,讀者可以參考其他關(guān)于 LVS 配置的資料。

        ip_vs_service 對象創(chuàng)建

        我們來看看 LVS 源碼是怎么創(chuàng)建一個?ip_vs_service?對象的,創(chuàng)建?ip_vs_service?對象通過?ip_vs_add_service()?函數(shù)完成,如下:

        static intip_vs_add_service(struct ip_vs_rule_user *ur, struct ip_vs_service **svc_p){    int ret = 0;    struct ip_vs_scheduler *sched;    struct ip_vs_service *svc = NULL;
        sched = ip_vs_scheduler_get(ur->sched_name); // 根據(jù)調(diào)度器名稱獲取調(diào)度策略對象 ... // 申請一個 ip_vs_service 對象 svc = (struct ip_vs_service *)kmalloc(sizeof(struct ip_vs_service), GFP_ATOMIC); ... memset(svc, 0, sizeof(struct ip_vs_service)); // 設(shè)置 ip_vs_service 對象的各個字段 svc->protocol = ur->protocol; // 協(xié)議 svc->addr = ur->vaddr; // 虛擬IP svc->port = ur->vport; // 虛擬端口 svc->fwmark = ur->vfwmark; // 防火墻標記 svc->flags = ur->vs_flags; // 標志位 svc->timeout = ur->timeout * HZ; // 超時時間 svc->netmask = ur->netmask; // 網(wǎng)絡(luò)掩碼
        INIT_LIST_HEAD(&svc->destinations); svc->sched_lock = RW_LOCK_UNLOCKED; svc->stats.lock = SPIN_LOCK_UNLOCKED;
        ret = ip_vs_bind_scheduler(svc, sched); // 綁定調(diào)度器 ... ip_vs_svc_hash(svc); // 添加到ip_vs_service對象的hash表中 ... *svc_p = svc; return 0;}

        先說明一下,參數(shù)?ur?是用戶通過命令行配置的規(guī)則信息。上面的代碼主要完成以下幾個工作:

        • 通過調(diào)用?ip_vs_scheduler_get()?函數(shù)來獲取一個?ip_vs_scheduler?(調(diào)度器) 對象。

        • 然后申請一個?ip_vs_service?對象并且根據(jù)用戶的配置設(shè)置其各個參數(shù),并且把調(diào)度器對象綁定這個?ip_vs_service?對象。

        • 最后把?ip_vs_service?對象添加到?ip_vs_service?對象的全局哈希表中(這是由于可以創(chuàng)建多個?ip_vs_service?對象,這些對象通過一個全局哈希表來存儲)。

        ip_vs_dest 對象創(chuàng)建

        創(chuàng)建?ip_vs_dest?對象通過?ip_vs_add_dest()?函數(shù)完成,代碼如下:

        static int ip_vs_add_dest(struct ip_vs_service *svc, struct ip_vs_rule_user *ur){    struct ip_vs_dest *dest;    __u32 daddr = ur->daddr; // 目的IP    __u16 dport = ur->dport; // 目的端口    int ret;    ...    // 調(diào)用 ip_vs_new_dest() 函數(shù)創(chuàng)建一個 ip_vs_dest 對象    ret = ip_vs_new_dest(svc, ur, &dest);    ...    // 把 ip_vs_dest 對象添加到 ip_vs_service 對象的 destinations 列表中    list_add(&dest->n_list, &svc->destinations);     svc->num_dests++;
        /* 調(diào)用調(diào)度器的 update_service() 方法更新 ip_vs_service 對象 */ svc->scheduler->update_service(svc); ... return 0;}

        ip_vs_add_dest()?函數(shù)主要通過調(diào)用?ip_vs_new_dest()?創(chuàng)建一個?ip_vs_dest?對象,然后將其添加到?ip_vs_service?對象的?destinations?列表中。我們來看看?ip_vs_new_dest()?函數(shù)的實現(xiàn):

        static intip_vs_new_dest(struct ip_vs_service *svc,               struct ip_vs_rule_user *ur,               struct ip_vs_dest **destp){    struct ip_vs_dest *dest;    ...    *destp = dest = (struct ip_vs_dest*)kmalloc(sizeof(struct ip_vs_dest), GFP_ATOMIC);    ...    memset(dest, 0, sizeof(struct ip_vs_dest));    // 設(shè)置 ip_vs_dest 對象的各個字段    dest->protocol = svc->protocol; // 協(xié)議    dest->vaddr = svc->addr;        // 虛擬IP    dest->vport = svc->port;        // 虛擬端口    dest->vfwmark = svc->fwmark;    // 虛擬網(wǎng)絡(luò)掩碼    dest->addr = ur->daddr;         // 真實IP    dest->port = ur->dport;         // 真實端口
        atomic_set(&dest->activeconns, 0); atomic_set(&dest->inactconns, 0); atomic_set(&dest->refcnt, 0);
        INIT_LIST_HEAD(&dest->d_list); dest->dst_lock = SPIN_LOCK_UNLOCKED; dest->stats.lock = SPIN_LOCK_UNLOCKED; __ip_vs_update_dest(svc, dest, ur); ... return 0;}

        ip_vs_new_dest()?函數(shù)的實現(xiàn)也比較簡單,首先通過調(diào)用?kmalloc()?函數(shù)申請一個?ip_vs_dest?對象,然后根據(jù)用戶配置的規(guī)則信息來初始化?ip_vs_dest?對象的各個字段。

        ip_vs_scheduler 對象

        ip_vs_scheduler?(調(diào)度器) 對象用于從?ip_vs_service?對象的?destinations?列表中選擇一個合適的?ip_vs_dest?對象,其定義如下:

        struct ip_vs_scheduler {    struct list_head    n_list;     // 連接所有調(diào)度策略    char                *name;      // 調(diào)度策略名稱    atomic_t            refcnt;     // 應(yīng)用計數(shù)器    struct module       *module;    // 模塊對象(如果是通過模塊引入的)
        int (*init_service)(struct ip_vs_service *svc); // 用于初始化服務(wù) int (*done_service)(struct ip_vs_service *svc); // 用于停止服務(wù) int (*update_service)(struct ip_vs_service *svc); // 用于更新服務(wù)
        // 用于獲取一個真實服務(wù)器對象 (Real-Server) struct ip_vs_dest *(*schedule)(struct ip_vs_service *svc, struct iphdr *iph);};

        ip_vs_scheduler?對象的各個字段都在注釋說明了,其中?schedule?字段是一個函數(shù)的指針,其指向一個調(diào)度函數(shù),用于從?ip_vs_service?對象的?destinations?列表中選擇一個合適的?ip_vs_dest?對象。

        我們可以通過一個最簡單的調(diào)度模塊(輪詢調(diào)度模塊)來分析?ip_vs_scheduler?對象的工作原理(文件路徑:/net/ipv4/ipvs/ip_vs_rr.c):

        static struct ip_vs_scheduler ip_vs_rr_scheduler = {    {0},                /* n_list */    "rr",               /* name */    ATOMIC_INIT(0),     /* refcnt */    THIS_MODULE,        /* this module */    ip_vs_rr_init_svc,  /* service initializer */    ip_vs_rr_done_svc,  /* service done */    ip_vs_rr_update_svc,/* service updater */    ip_vs_rr_schedule,  /* select a server from the destination list */};

        首先輪詢調(diào)度模塊定義了一個?ip_vs_scheduler?對象,其中?schedule?字段設(shè)置為?ip_vs_rr_schedule()?函數(shù)。我們來看看?ip_vs_rr_schedule()?函數(shù)的實現(xiàn):

        static struct ip_vs_dest *ip_vs_rr_schedule(struct ip_vs_service *svc, struct iphdr *iph){    register struct list_head *p, *q;    struct ip_vs_dest *dest;
        write_lock(&svc->sched_lock); p = (struct list_head *)svc->sched_data; // 最后一次被調(diào)度的位置 p = p->next; q = p; // 遍歷 destinations 列表 do { if (q == &svc->destinations) { q = q->next; continue; } dest = list_entry(q, struct ip_vs_dest, n_list); // 找到一個權(quán)限值大于 0 的 ip_vs_dest 對象 if (atomic_read(&dest->weight) > 0) goto out; q = q->next; } while (q != p); write_unlock(&svc->sched_lock);
        return NULL;
        out: svc->sched_data = q; // 設(shè)置最后一次被調(diào)度的位置 ... return dest;}

        ip_vs_rr_schedule()?函數(shù)是輪詢調(diào)度算法的實現(xiàn),其實現(xiàn)原理如下:

        • ip_vs_service?對象的?sched_data?字段保存了最后一次調(diào)度的位置,所以每次調(diào)度時都是從這個字段讀取到最后一次調(diào)度的位置。

        • 從最后一次調(diào)度的位置開始遍歷,找到一個權(quán)限值(weight)大于 0 的?ip_vs_dest?對象。

        • 如果找到就把?ip_vs_service?對象的?sched_data?字段設(shè)置為最后被選擇的?ip_vs_dest?對象的位置。

        其原理可以通過以下圖片說明:


        上圖描述的原理還是比較簡單,首先從?sched_data?處開始遍歷,查找一個合適的?ip_vs_dest?對象,然后更新?sched_data?的位置。

        另外,由于?LVS?可以存在多種不同的調(diào)度對象(提供不同的調(diào)度算法),所以?LVS?把這些調(diào)度對象通過一個鏈表(ip_vs_schedulers)存儲起來,而這些調(diào)度對象可以通過調(diào)度對象的名字(name?字段)來查詢。

        可以通過調(diào)用?register_ip_vs_scheduler()?函數(shù)向?LVS?注冊調(diào)度對象,而通過調(diào)用?ip_vs_scheduler_get()?函數(shù)來獲取指定名字的調(diào)度對象,這兩個函數(shù)的實現(xiàn)比較簡單,這里就不作詳細介紹了。

        ip_vs_conn 對象

        ip_vs_conn?對象用于維護?客戶端?與?真實服務(wù)器?之間的關(guān)系,為什么需要維護它們之間的關(guān)系?原因是?TCP協(xié)議?面向連接的協(xié)議,所以每次調(diào)度都必須選擇相同的真實服務(wù)器,否則連接就會失效。


        如上圖所示,剛開始時調(diào)度器選擇了?Real-Server(1)?服務(wù)器進行處理客戶端請求,但第二次調(diào)度時卻選擇了?Real-Server(2)?來處理客戶端請求。

        由于?TCP協(xié)議?需要客戶端與服務(wù)器進行連接,但第二次請求的服務(wù)器發(fā)生了變化,所以連接狀態(tài)就失效了,這就為什么?LVS?需要維持客戶端與真實服務(wù)器連接關(guān)系的原因。

        LVS?通過?ip_vs_conn?對象來維護客戶端與真實服務(wù)器之間的連接關(guān)系,其定義如下:

        struct ip_vs_conn {    struct list_head    c_list;     /* 用于連接到哈希表 */
        __u32 caddr; /* 客戶端IP地址 */ __u32 vaddr; /* 虛擬IP地址 */ __u32 daddr; /* 真實服務(wù)器IP地址 */ __u16 cport; /* 客戶端端口 */ __u16 vport; /* 虛擬端口 */ __u16 dport; /* 真實服務(wù)器端口 */ __u16 protocol; /* 協(xié)議類型(UPD/TCP) */ ... /* 用于發(fā)送數(shù)據(jù)包的接口 */ int (*packet_xmit)(struct sk_buff *skb, struct ip_vs_conn *cp); ...};

        ip_vs_conn?對象各個字段的作用都在注釋中進行說明了,客戶端與真實服務(wù)器的連接關(guān)系就是通過?協(xié)議類型、客戶端IP、客戶端端口、虛擬IP?和?虛擬端口?來進行關(guān)聯(lián)的,也就是說根據(jù)這五元組能夠確定一個?ip_vs_conn?對象。

        另外,在《原理篇》我們說過,LVS 有3中運行模式:NAT模式、DR模式?和?TUN模式。而對于不同的運行模式,發(fā)送數(shù)據(jù)包的接口是不一樣的,所以?ip_vs_conn?對象的?packet_xmit?字段會根據(jù)不同的運行模式來選擇不同的發(fā)送數(shù)據(jù)包接口,綁定發(fā)送數(shù)據(jù)包接口是通過?ip_vs_bind_xmit()?函數(shù)完成,如下:

        static inline void ip_vs_bind_xmit(struct ip_vs_conn *cp){    switch (IP_VS_FWD_METHOD(cp)) {    case IP_VS_CONN_F_MASQ:                     // NAT模式        cp->packet_xmit = ip_vs_nat_xmit;        break;    case IP_VS_CONN_F_TUNNEL:                   // TUN模式        cp->packet_xmit = ip_vs_tunnel_xmit;        break;    case IP_VS_CONN_F_DROUTE:                   // DR模式        cp->packet_xmit = ip_vs_dr_xmit;        break;    ...    }}

        一個客戶端請求到達?LVS?后,Director服務(wù)器?首先會查找客戶端是否已經(jīng)與真實服務(wù)器建立了連接關(guān)系,如果已經(jīng)建立了連接,那么直接使用這個連接關(guān)系。否則,通過調(diào)度器對象選擇一臺合適的真實服務(wù)器,然后創(chuàng)建客戶端與真實服務(wù)器的連接關(guān)系,并且保存到全局哈希表?ip_vs_conn_tab?中。流程圖如下:


        上面對?LVS?各個角色都進行了介紹,下面開始講解?LVS?對數(shù)據(jù)包的轉(zhuǎn)發(fā)過程。

        3. 數(shù)據(jù)轉(zhuǎn)發(fā)

        因為?LVS?是一個負載均衡工具,所以其最重要的功能就是對數(shù)據(jù)的調(diào)度與轉(zhuǎn)發(fā), 而對數(shù)據(jù)的轉(zhuǎn)發(fā)是在前面介紹的?Netfilter?鉤子函數(shù)進行的。

        對數(shù)據(jù)的轉(zhuǎn)發(fā)主要是通過?ip_vs_in()?和?ip_vs_out()?這兩個鉤子函數(shù):

        • ip_vs_in()?運行在?Netfilter?的?LOCAL_IN?階段。

        • ip_vs_out()?運行在?Netfilter?的?FORWARD?階段。

        FORWARD?階段發(fā)送在數(shù)據(jù)包不是發(fā)送給本機的情況,但是一般來說數(shù)據(jù)包都是發(fā)送給本機的,所以對于?ip_vs_out()?這個函數(shù)的實現(xiàn)就不作介紹,我們主要重點分析?ip_vs_in()?這個函數(shù)。

        ip_vs_in() 鉤子函數(shù)

        有了前面的知識點,我們對?ip_vs_in()?函數(shù)的分析就不那么困難了。下面我們分段對?ip_vs_in()?函數(shù)進行分析:

        static unsigned intip_vs_in(unsigned int hooknum,         struct sk_buff **skb_p,         const struct net_device *in,         const struct net_device *out,         int (*okfn)(struct sk_buff *)){    struct sk_buff *skb = *skb_p;    struct iphdr *iph = skb->nh.iph; // IP頭部    union ip_vs_tphdr h;    struct ip_vs_conn *cp;    struct ip_vs_service *svc;    int ihl;    int ret;    ...    // 因為LVS只支持TCP和UDP    if (iph->protocol != IPPROTO_TCP && iph->protocol != IPPROTO_UDP)        return NF_ACCEPT;
        ihl = iph->ihl << 2; // IP頭部長度
        // IP頭部是否正確 if (ip_vs_header_check(skb, iph->protocol, ihl) == -1) return NF_DROP;
        iph = skb->nh.iph; // IP頭部指針 h.raw = (char*)iph + ihl; // TCP/UDP頭部指針

        上面的代碼主要對數(shù)據(jù)包的?IP頭部?進行正確性驗證,并且將?iph?變量指向?IP頭部,而?h?變量指向?TCP/UDP?頭部。

            // 根據(jù) "協(xié)議類型", "客戶端IP", "客戶端端口", "虛擬IP", "虛擬端口" 五元組獲取連接對象    cp = ip_vs_conn_in_get(iph->protocol, iph->saddr,                           h.portp[0], iph->daddr, h.portp[1]);
        // 1. 如果連接還沒建立 // 2. 如果是TCP協(xié)議的話, 第一個包必須是syn包, 或者UDP協(xié)議。 // 3. 根據(jù)協(xié)議、虛擬IP和虛擬端口查找服務(wù)對象 if (!cp && (h.th->syn || (iph->protocol != IPPROTO_TCP)) && (svc = ip_vs_service_get(skb->nfmark, iph->protocol, iph->daddr, h.portp[1]))) { ... // 通過調(diào)度器選擇一個真實服務(wù)器 // 并且創(chuàng)建一個新的連接對象, 建立真實服務(wù)器與客戶端連接關(guān)系 cp = ip_vs_schedule(svc, iph); ... }

        上面的代碼主要完成以下幾個功能:

        • 根據(jù)?協(xié)議類型、客戶端IP、客戶端端口虛擬IP?和?虛擬端口?五元組,然后調(diào)用?ip_vs_conn_in_get()?函數(shù)獲取連接對象。

        • 如果連接還沒建立,那么就調(diào)用?ip_vs_schedule()?函數(shù)調(diào)度一臺合適的真實服務(wù)器,然后創(chuàng)建一個連接對象,并且建立真實服務(wù)器與客戶端之間的連接關(guān)系。

        我們來分析一下?ip_vs_schedule()?函數(shù)的實現(xiàn):

        static struct ip_vs_conn *ip_vs_schedule(struct ip_vs_service *svc, struct iphdr *iph){    struct ip_vs_conn *cp = NULL;    struct ip_vs_dest *dest;    const __u16 *portp;    ...    portp = (__u16 *)&(((char *)iph)[iph->ihl*4]); // 指向TCP或者UDP頭部    ...    dest = svc->scheduler->schedule(svc, iph); // 通過調(diào)度器選擇一臺合適的真實服務(wù)器    ...    cp = ip_vs_conn_new(iph->protocol,                      // 協(xié)議類型                        iph->saddr,                         // 客戶端IP                        portp[0],                           // 客戶端端口                        iph->daddr,                         // 虛擬IP                        portp[1],                           // 虛擬端口                        dest->addr,                         // 真實服務(wù)器的IP                        dest->port ? dest->port : portp[1], // 真實服務(wù)器的端口                        0,                                  // flags                        dest);    ...    return cp;}

        ip_vs_schedule()?函數(shù)的主要工作如下:

        • 首先通過調(diào)用調(diào)度器(ip_vs_scheduler?對象)的?schedule()?方法從?ip_vs_service?對象的?destinations?鏈表中選擇一臺真實服務(wù)器(ip_vs_dest?對象)

        • 然后調(diào)用?ip_vs_conn_new()?函數(shù)創(chuàng)建一個新的?ip_vs_conn?對象。

        ip_vs_conn_new()?主要用于創(chuàng)建?ip_vs_conn?對象,并且根據(jù)?LVS?的運行模式為其選擇正確的數(shù)據(jù)發(fā)送接口,其實現(xiàn)如下:

        struct ip_vs_conn *ip_vs_conn_new(int proto,                   // 協(xié)議類型               __u32 caddr, __u16 cport,    // 客戶端IP和端口               __u32 vaddr, __u16 vport,    // 虛擬IP和端口               __u32 daddr, __u16 dport,    // 真實服務(wù)器IP和端口               unsigned flags, struct ip_vs_dest *dest){    struct ip_vs_conn *cp;
        // 創(chuàng)建一個 ip_vs_conn 對象 cp = kmem_cache_alloc(ip_vs_conn_cachep, GFP_ATOMIC); ... // 設(shè)置 ip_vs_conn 對象的各個字段 cp->protocol = proto; cp->caddr = caddr; cp->cport = cport; cp->vaddr = vaddr; cp->vport = vport; cp->daddr = daddr; cp->dport = dport; cp->flags = flags; ... ip_vs_bind_dest(cp, dest); // 將 ip_vs_conn 與真實服務(wù)器對象進行綁定 ... ip_vs_bind_xmit(cp); // 綁定一個發(fā)送數(shù)據(jù)的接口 ... ip_vs_conn_hash(cp); // 把 ip_vs_conn 對象添加到連接信息表中
        return cp;}

        ip_vs_conn_new()?函數(shù)的主要工作如下:

        • 創(chuàng)建一個新的?ip_vs_conn?對象,并且設(shè)置其各個字段的值。

        • 調(diào)用?ip_vs_bind_dest()?函數(shù)將?ip_vs_conn?對象與真實服務(wù)器對象(ip_vs_dest?對象)進行綁定。

        • 根據(jù)?LVS?的運行模式,調(diào)用?ip_vs_bind_xmit()?函數(shù)為連接對象選擇一個正確的數(shù)據(jù)發(fā)送接口,ip_vs_bind_xmit()?函數(shù)在前面已經(jīng)介紹過。

        • 調(diào)用?ip_vs_conn_hash()?函數(shù)把新創(chuàng)建的?ip_vs_conn?對象添加到全局連接信息哈希表中。

        我們接著分析?ip_vs_in()?函數(shù):

            if (cp->packet_xmit)        ret = cp->packet_xmit(skb, cp); // 把數(shù)據(jù)包轉(zhuǎn)發(fā)出去    else {        ret = NF_ACCEPT;    }    ...    return ret;}

        ip_vs_in()?函數(shù)的最后部分就是通過調(diào)用數(shù)據(jù)發(fā)送接口把數(shù)據(jù)包轉(zhuǎn)發(fā)出去,對于?NAT模式?來說,數(shù)據(jù)發(fā)送接口就是?ip_vs_nat_xmit()。

        數(shù)據(jù)發(fā)送接口:ip_vs_nat_xmit()

        接下來,我們對?NAT模式?的數(shù)據(jù)發(fā)送接口?ip_vs_nat_xmit()?進行分析。由于?ip_vs_nat_xmit()?函數(shù)的實現(xiàn)比較復雜,所以我們通過分段來分析:

        static int ip_vs_nat_xmit(struct sk_buff *skb, struct ip_vs_conn *cp){    struct rtable *rt;      /* Route to the other host */    struct iphdr  *iph;    union ip_vs_tphdr h;    int ihl;    unsigned short size;    int mtu;    ...    iph = skb->nh.iph;                // IP頭部    ihl = iph->ihl << 2;              // IP頭部長度    h.raw = (char*) iph + ihl;        // 傳輸層頭部(TCP/UDP)    size = ntohs(iph->tot_len) - ihl; // 數(shù)據(jù)長度    ...    // 找到真實服務(wù)器IP的路由信息    if (!(rt = __ip_vs_get_out_rt(cp, RT_TOS(iph->tos))))         goto tx_error_icmp;    ...    // 替換新路由信息    dst_release(skb->dst);    skb->dst = &rt->u.dst;

        上面的代碼主要完成兩個工作:

        • 調(diào)用?__ip_vs_get_out_rt()?函數(shù)查找真實服務(wù)器 IP 對應(yīng)的路由信息對象。

        • 把數(shù)據(jù)包的舊路由信息替換成新的路由信息。

        我們接著分析:

            iph->daddr = cp->daddr; // 修改目標IP地址為真實服務(wù)器IP地址    h.portp[1] = cp->dport; // 修改目標端口為真實服務(wù)器端口    ...    // 更新UDP/TCP頭部的校驗和    if (!cp->app && (iph->protocol != IPPROTO_UDP || h.uh->check != 0)) {        ip_vs_fast_check_update(&h, cp->vaddr, cp->daddr, cp->vport,                                cp->dport, iph->protocol);
        if (skb->ip_summed == CHECKSUM_HW) skb->ip_summed = CHECKSUM_NONE;
        } else { switch (iph->protocol) { case IPPROTO_TCP: h.th->check = 0; h.th->check = csum_tcpudp_magic(iph->saddr, iph->daddr, size, iph->protocol, csum_partial(h.raw, size, 0)); break;
        case IPPROTO_UDP: h.uh->check = 0; h.uh->check = csum_tcpudp_magic(iph->saddr, iph->daddr, size, iph->protocol, csum_partial(h.raw, size, 0)); if (h.uh->check == 0) h.uh->check = 0xFFFF; break; }
        skb->ip_summed = CHECKSUM_UNNECESSARY; }

        上面的代碼完成兩個工作:

        • 修改目標IP地址和端口為真實服務(wù)器IP地址和端口。

        • 更新?UDP/TCP 頭部?的校驗和(checksum)。

        我們接著分析:

            ip_send_check(iph); // 計算IP頭部的校驗和    ...    skb->nfcache |= NFC_IPVS_PROPERTY;
        ip_send(skb); // 把包發(fā)送出去 ... return NF_STOLEN; // 讓其他 Netfilter 的鉤子函數(shù)放棄處理該包}

        上面的代碼完成兩個工作:

        • 調(diào)用?ip_send_check()?函數(shù)重新計算數(shù)據(jù)包的?IP頭部?校驗和。

        • 調(diào)用?ip_send()?函數(shù)把數(shù)據(jù)包發(fā)送出去。

        這樣,數(shù)據(jù)包的目標IP地址和端口被替換成真實服務(wù)器的IP地址和端口,然后被發(fā)送到真實服務(wù)器處。至此,NAT模式?的分析已經(jīng)完畢。下面我們來總結(jié)一下整個流程:

        • 當數(shù)據(jù)包進入到?Director服務(wù)器?后,會被?LOCAL_IN階段?的?ip_vs_in()?鉤子函數(shù)進行處理。

        • ip_vs_in()?函數(shù)首先查找客戶端與真實服務(wù)器的連接是否存在,如果存在就使用這個真實服務(wù)器。否則通過調(diào)度算法對象選擇一臺最合適的真實服務(wù)器,然后建立客戶端與真實服務(wù)器的連接關(guān)系。

        • 根據(jù)運行模式來選擇發(fā)送數(shù)據(jù)的接口(如?NAT模式?對應(yīng)的是?ip_vs_nat_xmit()?函數(shù)),然后把數(shù)據(jù)轉(zhuǎn)發(fā)出去。

        • 轉(zhuǎn)發(fā)數(shù)據(jù)時,首先會根據(jù)真實服務(wù)器的IP地址更新數(shù)據(jù)包的路由信息,然后再更新各個協(xié)議頭部的信息(如IP地址、端口和校驗和等),然后把數(shù)據(jù)發(fā)送出去。

        總結(jié)

        本文主要分析 LVS 的實現(xiàn)原理,但由于本人能力有限,并且很多細節(jié)沒有分析,所以有問題可以通過評論指出。另外,本文只介紹了?NAT模式?的原理,還有?DR模式?和?TUN模式?的沒有分析,有興趣可以自行閱讀源碼


        瀏覽 128
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            欧美一区二区三区不卡 | 青青草成人在线播放 | 亚洲中文字幕第一页 | 亚洲AV日韩AV永久无码网站 | 狠狠久久| 别柔我的奶头一区二区三区 | 女人做爰全过程免费观看美女臀位 | 黄免费 | 99热在线观看免费 | 天堂网av在线 |