1. 程式人生 > >Linux如何實現鏡像端口

Linux如何實現鏡像端口

希望 新的 getopt author 的人 art 配置 額外 網絡

在所有高端型號,大多數中端型號以及部分低端型號的交換機/路由器上,都可以配置一個或者多個鏡像端口,它是流量分析的利器。然而,Linux上沒有現成的技術可以實現鏡像端口,當然,我指的不是Linux 3.x(x是幾,忘了)以上的內核,這些內核已經支持了鏡像,但不夠好。起碼2.6.35的內核是不能支持的,那麽Linux實現的軟交換機屬於哪個檔次呢?關鍵是,很多高端的網絡產品也是基於Linux實現的,沒有鏡像口怎麽能行,即使在不使用Linux bridge的情況下,也希望能有一個技術實現鏡像端口。
我相信,並且確信,很多產品都已經實現了這個技術,它事實上很簡單,多年以前,我自己在學習華為網絡技術的時候,也曾在Linux寫了一個支持鏡像的內核模塊,雖然那是在網上找的人家實現的半片子代碼改的。現如今,在我可以很不謙虛地說自己已經很精通Netfilter以及Linux IP路由的時候,決定給出一個基於Netfilter的實現,Netfilter就在那,它幾乎可以擴展任何協議棧的東西,甚至重寫整個協議棧...多年以來,關於這個Linux如何實現鏡像端口的討論很多很多,也催生了不少愛美之士的不斷嘗試和修正,對我個人來講,第一次涉足這個話題是在2009年,雖然在學習Cisco技術的時候也搞過,但畢竟不是任務化的,只是說我對Cisco技術是學而不考-太貴,因此可以有大把本應該用於考試準備的時間用來學習Linux,特別是把Cisco的特性實現在Linux上,說句題外話,我之所以對Cisco和Linux的網絡技術能同時掌握,和學而不考有很大的關系,然而對於求職,那就是另外一回事了...
在給出代碼之前,我先給出一個只依靠配置就可以完成的實現,然後說一下它的缺點。事實上,僅僅依靠brctl命令或者sysfs,echo就可以實現一個鏡像端口,具體做法就是:
1.確定你的鏡像端口,比如eth5;
2.將實際數據通過的端口,比如eth0和鏡像端口綁成一個bridge;
3.調用brctl的setageing命令將老化時間設置為0,這就模擬了一個2端口的hub;
4.所有數據端口eth0發出的包都會發往eth5
...

但是!但是每一個物理接口只能屬於一個bridge,這就意味著你只能通過上述的方式捕獲一個方向的數據,不得不使用另外的一個鏡像口使用相同的辦法捕獲另外一個方向的數據,然後再把這兩個鏡像口接在一個switch上,在此switch上合二為一,這種方式,還是,太硬了!
那麽,軟件做法有沒有呢?有的,我多年前實現的那個就是,大體想法就是註冊一個ETH_P_ALL類型的packet_type,類似tcpdump抓包那樣捕獲數據包,然後在內核模塊中調用dev_queue_xmit將其發送到你定義的鏡像端口,具體定義方式需要通過字符設備的ioctl,procfs等方式來定義。這種方式比較常規,工作地比較好,並且可以從諸如tap等虛擬網卡將流量鏡像給進程而不是線纜那頭的審計設備。然而,還是太硬了,在你通過BPF語法過濾數據包之前,流量已經被ETH_P_ALL截取了...事實上,並不是所有的流量都需要被鏡像!BPF雖然強大,但是依靠中間層進行解析翻譯,門檻太高,我相信,一條iptables規則和一條等價的BPF規則放在那,能看懂前者的占絕大多數,看不懂後者占絕大多數,過於靈活就是不靈活,給你一本新華字典,所有字都在裏面,你讀十遍也不如讀一遍《古文觀止》...這個可以從香農的信息論中得到證明。

xt_TEE的實現

在xtables-addons中,已經有了一個xt_TEE的實現,在其manual中,有一個一目了然的配置:
-t mangle -A PREROUTING -i eth0 -j TEE --gateway 2001:db8::1
即將數據包克隆一份,然後發往一個IP地址,該IP地址可以配置。我為何覺得它不好呢?第一,我認為依基於IP而不是基於端口來鏡像數據包可能需要額外太多的配置,比如你事先要有一個接收端的明確IP地址;第二我覺得它的實現不是很好,它的實現阻礙了原始數據包的快速通過,而我比較傾向於用“下半部”的思想解決克隆包的發送問題,即先將其排入一個隊列,然後讓系統調度其發送,而不是強制在代碼中調用發送代碼。除了這兩點,TEE的實現真的不錯。看了TEE的實現之後,我在想,為何:
-j TEE --dev ethX,ethY,ethZ

這種設置就不行呢?當然,肯定不行,因為TEE target沒有--dev參數,可是為何沒有人實現呢?...難道僅僅是內核缺少由dev自動封裝以太頭的接口?也許是吧,畢竟,所有的dev_queue_xmit調用都是從路由層一路下來的...
現在該給出我最新的實現了。這個實現很簡單,和TEE一樣,寫了一個新的iptables target,即CLONE。克隆一個數據包並且打上標簽,然後如何處理該數據包呢?很顯然是根據標簽來查找策略路由表了,你可以在策略路由表中將所有克隆的數據包發到任何一個網卡中,這不就是鏡像口的含義麽?
要說明的是,雖然你可以通過reroute的方式將帶有標簽的克隆數據包發往一個網卡,但是由於網卡在發包前需要對目標或者對下一跳進行ARP,那麽可能導致由於ARP沒有回應而發包失敗,幸運的是,ifconfig命令可以禁用網卡的ARP,這不正是為鏡像端口準備的麽??

前傳

起初,寫這個模塊的目標並不是為了做鏡像端口,而是為了將一個數據包復制兩份,僅此而已,其實本意就是一個Netfilter實現的抓包模塊,和使用pcap抓包相比,它的優勢在於可以去除很多不相關數據包的幹擾,它只能抓取確實是發往本機的數據包,雖然這也許違背的抓包的原本的意義,但是那只是一個詞匯而已!我以及很多人大多數情況下抓包並不是為了嗅探別人的數據,而是為了解決和自己相關的問題,這就需要過濾掉那些不小心到來的由於交換機MAC映射到期導致的發往所有端口的數據,而這需要寫一大堆tcpdump規則。
使用Netfilter配合iptables來做這件事,優勢在於不需要把全部的規則寫在一條命令裏面,你完全可以在PREROUTING的mangle表用mark過濾掉那些你不感興趣的包,然後在FORWARD的filter上對感興趣的包實施包克隆,然後將克隆到的數據包通過策略路由發往任何你希望它到達的地方。你可以再寫一個模塊,用以決定是對包進行完全的記錄呢,還是對僅僅像LOG target那樣只記錄協議元數據-這很重要,大多數時候,我們並不關心載荷內容,除非你做深度分析。
總之,我不喜歡那種包攬一切的程序,抓包也是如此,一個ETH_P_ALL將所有數據不問青紅皂白全部截取,這是不合適的,當然它更加符合抓包的原本含義,但是誰在乎呢?也許是UNIX哲學在作崇,但也只是也許而已。實證主義並不在任何地方都有效。
在我的實現中,和TEE的實現不同,我只是克隆數據包,然後為其打上一個標簽,至於說接下來怎麽做,後續的HOOK來決定,你甚至都可以用我的CLONE target和TEE target結合在一起,形成一個packet fork炸彈。
下面給出實現,註意,該實現不能做到包嗅探!

實現

本實現由4部分,其中包含一個內核模塊文件,一個用戶態的iptables庫文件,一個結構體定義頭文件,一套Makefile。代碼完全按照xtables-addons的規範制作。
結構體定義頭文件:xt_CLONE.h

#ifndef _LINUX_NETFILTER_XT_CLONEMARK_H
#define _LINUX_NETFILTER_XT_CLONEMARK_H 1

struct xt_clonemark_tginfo {
        __u32 mark;
};

#endif /* _LINUX_NETFILTER_XT_CLONEMARK_H */



內核模塊:xt_CLONE.c
/*
 *      This program is free software; you can redistribute it and/or
 *      modify it under the terms of the GNU General Public License; either
 *      version 2 of the License, or any later version, as published by the
 *      Free Software Foundation.
 */

#include <linux/module.h>
#include <linux/skbuff.h>
#include <linux/netfilter/x_tables.h>
#include <net/ip6_route.h>
#include "xt_CLONE.h"
#include <net/ip.h>
#include "compat_xtables.h"

struct sk_buff_head clq;
static struct tasklet_struct clone_xmit_tasklet;


static void clone_xmit_work(unsigned long data)
{
        struct sk_buff_head *pclq = (struct sk_buff_head *)data;
        struct net_device *old_dev = NULL;
        struct net_device *new_dev = NULL;
        do {
                struct sk_buff * skb = skb_dequeue_tail(pclq);
                old_dev = skb_dst(skb)->dev;
                if (ip_route_me_harder(&skb, RTN_UNSPEC)) {
                        kfree_skb(skb);
                }
                new_dev = skb_dst(skb)->dev;
                if (old_dev != new_dev) {
                        ip_local_out(skb);
                } else {
                        kfree_skb(skb);
                }
        } while (!skb_queue_empty(pclq));
}


static unsigned int
clone_tg6(struct sk_buff **poldskb, const struct xt_action_param *par)
{
        // TODO
        return XT_CONTINUE;;
}

static unsigned int
clone_tg4(struct sk_buff **poldskb, const struct xt_action_param *par)
{
        const struct xt_clonemark_tginfo *markinfo = par->targinfo;
        struct sk_buff *newskb;
        __u32 mark;
        __u32 qlen;

        qlen = skb_queue_len (&clq);
        // 控制總量!
        if (qlen > 1000/*sysctl參數控制*/) {
                return XT_CONTINUE;
        }
        mark = markinfo->mark;
        newskb = pskb_copy(*poldskb, GFP_ATOMIC);
        if (newskb == NULL)
                return XT_CONTINUE;

        // 在FORWARD鏈上做的目的是可以放心reroute,關鍵在re前綴
//      skb_dst_drop(newskb);

        // 丟棄連接跟蹤,但是要為之初始化一個notrack的偽連接跟蹤
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
#include <net/netfilter/nf_conntrack.h>
        nf_conntrack_put(newskb->nfct);
        newskb->nfct = &nf_conntrack_untracked.ct_general;
        newskb->nfctinfo = IP_CT_NEW;
        nf_conntrack_get(newskb->nfct);
#endif
        newskb->mark = mark;
        skb_queue_head(&clq, newskb);
        tasklet_schedule(&clone_xmit_tasklet);

        return XT_CONTINUE;
}

static struct xt_target clone_tg_reg[] __read_mostly = {
        {
                .name       = "CLONE",
                .revision   = 0,
                .family     = NFPROTO_IPV6,
                .table      = "filter",
                .target     = clone_tg6,
                .targetsize = sizeof(struct xt_clonemark_tginfo),
                .me         = THIS_MODULE,
        },
        {
                .name       = "CLONE",
                .revision   = 0,
                .family     = NFPROTO_IPV4,
                .table      = "filter",
                .target     = clone_tg4,
                .targetsize = sizeof(struct xt_clonemark_tginfo),
                .me         = THIS_MODULE,
        },
};

static int __init clone_tg_init(void)
{
        skb_queue_head_init(&clq);
        tasklet_init(&clone_xmit_tasklet, clone_xmit_work, (unsigned long)&clq);
        return xt_register_targets(clone_tg_reg, ARRAY_SIZE(clone_tg_reg));
}

static void __exit clone_tg_exit(void)
{
        tasklet_kill(&clone_xmit_tasklet);
        return xt_unregister_targets(clone_tg_reg, ARRAY_SIZE(clone_tg_reg));
}

module_init(clone_tg_init);
module_exit(clone_tg_exit);
MODULE_AUTHOR("Wangran <[email protected]>");
MODULE_DESCRIPTION("Xtables: CLONE packet target");
MODULE_LICENSE("GPL");
MODULE_ALIAS("ip6t_CLONE");
MODULE_ALIAS("ipt_CLONE");



iptables模塊:libxt_CLONE.c

/*
 *      This program is free software; you can redistribute it and/or
 *      modify it under the terms of the GNU General Public License; either
 *      version 2 of the License, or any later version, as published by the
 *      Free Software Foundation.
 */
#include <stdio.h>
#include <getopt.h>
#include <xtables.h>
#include "xt_CLONE.h"
#include "compat_user.h"

enum {
        FL_MARK_USED     = 1 << 0,
};

static const struct option clonemark_tg_opts[] = {
        {.name = "mark",     .has_arg = true, .val = ‘1‘},
        {NULL},
};

static void clonemark_tg_init(struct xt_entry_target *t)
{
        struct xt_clonemark_tginfo *info = (void *)t->data;
        info->mark = ~0U;
}

static void clone_tg_help(void)
{
        printf("CLONE --mark mark\n\n");
}

static int clone_tg_parse(int c, char **argv, int invert, unsigned int *flags,
                         const void *entry, struct xt_entry_target **target)
{
        struct xt_clonemark_tginfo *info = (void *)(*target)->data;
        unsigned int n;
        switch (c) {
        case ‘1‘:
                xtables_param_act(XTF_ONLY_ONCE, "CLONE", "--mark", *flags & FL_MARK_USED);
                xtables_param_act(XTF_NO_INVERT, "CLONE", "--mark", invert);
                if (!xtables_strtoui(optarg, NULL, &n, 0, ~0U))
                        xtables_param_act(XTF_BAD_VALUE, "CLONE", "--mark", optarg);
                info->mark = n;
                *flags |= FL_MARK_USED;
                return true;
        }
        return false;
}

static void clone_tg_check(unsigned int flags)
{
        //TODO
}

static void
clonemark_tg_save(const void *entry, const struct xt_entry_target *target)
{
        const struct xt_clonemark_tginfo *info = (const void *)target->data;
        printf(" --mark 0x%x ", (__u32)info->mark);
}

static struct xtables_target clone_tg_reg = {
        .version       = XTABLES_VERSION,
        .name          = "CLONE",
        .family        = NFPROTO_UNSPEC,
        .size          = XT_ALIGN(sizeof(struct xt_clonemark_tginfo)),
        .userspacesize = XT_ALIGN(sizeof(struct xt_clonemark_tginfo)),
        .init          = clonemark_tg_init,
        .save          = clonemark_tg_save,
        .help          = clone_tg_help,
        .parse         = clone_tg_parse,
        .final_check   = clone_tg_check,
        .extra_opts    = clonemark_tg_opts,
};

static __attribute__((constructor)) void clone_tg_ldr(void)
{
        xtables_register_target(&clone_tg_reg);
}


編譯
建議編譯時將c代碼全部放入xtables-addons的extensions目錄,然後修改該目錄下的Kbuild文件,加入以下一行:
obj-${build_CLONE} += xt_CLONE.o
修改該目錄下的Mbuild文件,加入下面一行:
obj-${build_CLONE} += libxt_CLONE.so
修改該目錄上級目錄的mconfig文件,加入下面一行:
build_CLONE=m
在extensions目錄下執行make && make install即可,

說明

為何要在filter表做呢?因為filter表都在路由之後執行,這是為了調用reroute接口函數ip_route_me_harder的方便,該函數導出為一個內核接口,可以直接調用。在這麽做之前,我嘗試過直接調用ip_queue_xmit函數,然而發現只有在本機出發的包才會經過該路徑,因此需要為skb綁定一個socket才可以,而這無疑是工作量加大了;後來,我想到了直接調用ip_rcv_finish函數,可以該函數並未導出,需要在加載模塊前先去procfs裏面查一下該函數的地址,然後傳入模塊,這種做法並不標準;再往後,自然而然就是調用ip_route_me_harder接口函數了,然而該函數需要skb已經有了一個dst_entry(這很正常,reroute中的re前綴表明skb已經被路由過一次了),因此必然要在路由之後調用,那麽顯然處理位置就落到了Netfilter的HOOK點和路由構成的馬鞍面的中間位置了,只能在filter表來做,重新路由之後,直接調用ip_local_out從第三層發出即可。
此時又有問題了,既然已經重路由了,為了不直接從第二層路由結果的dev中發出呢,也就是調用dev_queue_xmit函數。實際上是完全可以的,然而工作量也會加大,比如你要自行增加MAC頭封裝等。在一個成型的實現中,所有的封裝都必須由協議棧本身來完成,即調用協議棧的函數,因為協議棧本身就是幹這個的,決不要在自己的代碼中實現,如果你覺得自己可以實現一個更妙的,那就直接改掉協議棧。

局限

該實現還是有一定局限的,畢竟該實現的做法太高層,它會改變數據包的MAC頭,但是這對於針對應用層內容的深度解析,無所謂了。另外需要註意的是,需要在本機做三件工作,第一就是設置CLONE規則及確定mark,第二是根據mark設置策略路由,第三就是將策略路由指向的出口設備的arp禁用掉。除了本機做的工作之外,還要在接收鏡像數據的機器的接收接口上開啟混雜模式。
畢竟這只是一個試驗,並非成型的解決方案,能做到這一點我已經很滿足了。

再分享一下我老師大神的人工智能教程吧。零基礎!通俗易懂!風趣幽默!還帶黃段子!希望你也加入到我們人工智能的隊伍中來!https://blog.csdn.net/jiangjunshow

Linux如何實現鏡像端口