linux下vlan的實現分析(上)

一. VLAN的核心概念
    1. 劃分VLAN的核心目的只有一個:分割廣播域。
       通過VLAN對廣播域進行合理分割之後,一是可以縮小ARP攻擊的範圍,從而提高網絡的安全性;二是可以縮小廣播域的大小,從而提高網絡的性能。

       所以要注意的是,劃分VLAN的目的中根本沒有隔離不同VLAN用戶互訪這一說法,這只是劃分VLAN之後的一種應用,不然使用三層設備實現不同VLAN
       間互訪就成了多此一舉。

    2. 單獨的一個VLAN模擬了一個常規的以太網交換機,因此VLAN實際上就是將一臺物理交換機分割成了多臺邏輯交換機,變相也節省了物理設備。
       站在二層的角度,不同VLAN間是無法通信的,VLAN間想要進行通信必須有三層參與。
       
    3. 本地VLAN的實現方式有很多種,常見的有基於端口的VLAN、基於MAC地址的VLAN、基於IP地址的VLAN、基於用戶的VLAN等,這裏略過。
    
    4. 跨設備的VLAN,標準的實現方法主要就是802.1q,通過VID來識別不同的VLAN,本文主要就是記錄了802.1q的相關內容。

    5. 802.1q的應用原則:通常在上行鏈路的第一臺VLAN交換機打上tag,在下行鏈路的最後一臺VLAN交換機去掉tag;
                         基於802.1q的VLAN劃分既要合理,又要儘量簡單,原則就是隻有當一個數據幀不打tag就不能區分屬於哪個VLAN時纔打上tag,
                         能去掉時儘早去掉。


二. Linux中VLAN的實現原理
    Linux中跟VLAN相關的代碼主要位於net/8021q目錄中。
    Linux中的VLAN是一種特殊的虛擬設備,所有的VLAN設備必須依賴於它的宿主設備才能存在。

    1. VLAN模塊涉及的主要結構

        /* 以下定義了整個VLAN模塊公用的私有空間(當然公用的前提是同一個網絡命名空間下)
         * 具體就是記錄了有關VLAN的proc文件系統信息
         */
        struct vlan_net {
            struct proc_dir_entry *proc_vlan_dir;   // proc文件系統中VLAN的頂層目錄節點
            struct proc_dir_entry *proc_vlan_conf;  // proc文件系統中VLAN的配置文件節點
            unsigned short name_type;               // vlan設備名字顯示風格,默認就是eth0.10的風格
        }

        /* ioctl調用SIOCGIFVLAN / SIOCSIFVLAN這兩條命令時傳入的參數結構
         * 用戶空間和kernel中都使用到本結構
         */
        struct vlan_ioctl_args {
            int cmd;    // 具體vlan命令
            char device1[24];       /* 用戶傳入的設備名,用於內核查找實際對應的設備
                                       調用ADD_VLAN_CMD時,該參數傳入的是vlan的宿主設備名
                                       調用SET_VLAN_NAME_TYPE_CMD時,該參數不用填
                                       調用其他命令時,該參數傳入的是vlan設備名
                                    */
            union {
                char device2[24];   // 用於返回vlan設備名
                int VID;
                unsigned int skb_priority;
                unsigned int name_type;
                unsigned int bind_type;
                unsigned int flag;  // 注意點,想要開啓某功能時需要同時設置vlan_qos爲非0
            }u;
            short vlan_qos;
        }

        /* 定義了每個VLAN設備附屬的私有空間結構
         * 每個VLAN設備都有一個私有空間vlan_dev_priv,同時整個VLAN模塊的私有空間vlan_net是所有VLAN設備共有的
         */
        struct vlan_dev_priv {
            unsigned int                nr_ingress_mappings;
            u32                            ingress_priority_map[8];
            unsigned int                nr_egress_mappings;
            struct vlan_priority_tci_mapping    *egress_priority_map[16];

            __be16                    vlan_proto; // vlan協議類型(大端)
            u16                        vlan_id;    // vlan id
            u16                        flags;      // VLAN_FLAG_* 用來記錄該vlan設備的幾個特徵標誌,默認總是設置了REORDER_HDR

            struct net_device            *real_dev;                  // 指向宿主設備
            unsigned char                real_dev_addr[ETH_ALEN];    // 宿主設備mac

            struct proc_dir_entry                *dent;
            struct vlan_pcpu_stats __percpu        *vlan_pcpu_stats;
        #ifdef CONFIG_NET_POLL_CONTROLLER
            struct netpoll                *netpoll;
        #endif
            unsigned int                nest_level;
        }

        /* 定義一個記錄vlan設備信息的結構,保存了一個宿主設備上綁定的所有vlan設備的信息
         * 這個結構是宿主設備用來管理其下轄的所有vlan設備的
         */
        struct vlan_info {
            struct net_device   *real_dev;  // 指向宿主設備
            struct vlan_group   grp;        // vlan組,記錄了所有綁定在該宿主設備上的vlan設備
            struct list_head    vid_list;   // vlan id的鏈表頭
            unsigned int        nr_vids;    // 記錄了該vlan id鏈表中節點數量
            struct rcu_head     rcu;
        }

        /* 定義同一個宿主設備下的vlan組模型
         * 存儲的vlan設備呈現四維結構:VLAN_PROTO_NUM * VLAN_GROUP_ARRAY_SPLIT_PARTS * VLAN_GROUP_ARRAY_PART_LEN * struct net_device指針
         */
        struct vlan_group {
            unsigned int        nr_vlan_devs;   // 記錄了該vlan組中包含的vlan設備數量
            struct hlist_node    hlist;    
            struct net_device **vlan_devices_arrays[VLAN_PROTO_NUM]
                                   [VLAN_GROUP_ARRAY_SPLIT_PARTS];  // 記錄了該vlan組中包含的所有vlan設備
        }

    2. VLAN模塊的初始化
        int vlan_net_id;  // 用於記錄vlan模塊在所有網絡命名空間中的ID號(註冊到網絡命名空間時分配)

        // 以下是整個VLAN模塊的初始化入口
        int __init vlan_proto_init(void)
        {
            // 將vlan模塊註冊到每一個網絡命名空間,注意就是在這裏完成了模塊私有空間的申請,對於VLAN來說就是vlan_net,並且執行了vlan_init_net
            register_pernet_subsys(&vlan_net_ops);
            // 向通知鏈中註冊一個vlan事件通知塊
            register_netdevice_notifier(&vlan_notifier_block);
            // 初始化操作vlan用的netlink接口
            vlan_netlink_init();
            // 設置用於vlan操作的ioctl鉤子函數
            vlan_ioctl_set(vlan_ioctl_handler);
        }
        小結:以上4步操作基本是加載一個頂層網絡模塊的通用格式,
              而模塊特有的那部分初始化通常就是放在pernet_operations->init指向的函數中,以VLAN爲例,就是vlan_init_net

        // 以下就是VLAN模塊特有的那部分初始化
        int __net_init vlan_init_net(struct net *net)
        {
            // 根據vlan_net_id索引對應的私有空間,也就是vlan_net
            struct vlan_net *vn = net_generic(net, vlan_net_id);
            // 設置vlan設備名的默認顯示風格,類似 eth0.10
            vn->name_type = VLAN_NAME_TYPE_RAW_PLUS_VID_NO_PAD;
            // 在proc文件系統中創建vlan接口(/proc/net/vlan)
            vlan_proc_init(net);
        }
        int __net_init vlan_proc_init(struct net *net)
        {
            // 根據vlan_net_id索引得到對應的vlan_net結構
            struct vlan_net *vn = net_generic(net, vlan_net_id);
            // proc文件系統下創建/proc/net/vlan目錄
            vn->proc_vlan_dir = proc_net_mkdir(net, name_root, net->proc_net);
            // proc文件系統下創建/proc/net/vlan/config文件(文件屬性:普通文件 + 可讀可寫)
            vn->proc_vlan_conf = proc_create(name_conf, S_IFREG|S_IRUSR|S_IWUSR,vn->proc_vlan_dir, &vlan_fops);
        }
        小結:這部分VLAN特有的初始化,具體主要就是創建了proc文件系統下的VLAN節點

    3. VLAN模塊的功能管理
        對VLAN模塊的功能管理主要基於2種接口方式:ioctl和netlink

        3.1 ioctl方式分析
            VLAN模塊初始化過程中註冊了對應的ioctl鉤子函數vlan_ioctl_hook,
            這樣,當用戶空間執行ioctl調用SIOCGIFVLAN/SIOCSIFVLAN這兩條命令時,對應的vlan_ioctl_handler將被執行
            int vlan_ioctl_handler(struct net *net, void __user *arg)
            {
                // 將ioctl的vlan傳入參數從用戶空間拷貝到內核
                copy_from_user(&args, arg, sizeof(struct vlan_ioctl_args));
                switch (args.cmd)
                {
                    case SET_VLAN_INGRESS_PRIORITY_CMD:
                    case SET_VLAN_EGRESS_PRIORITY_CMD:
                    case SET_VLAN_FLAG_CMD:
                    case ADD_VLAN_CMD:
                    case DEL_VLAN_CMD:
                    case GET_VLAN_REALDEV_NAME_CMD:
                    case GET_VLAN_VID_CMD:
                        // 根據用戶傳入的設備名查找對應的設備管理塊
                        dev = __dev_get_by_name(net, args.device1);
                        // 以上這些命令中除了ADD_VLAN_CMD,必須確保device1傳入的設備名指的是vlan設備
                        if (args.cmd != ADD_VLAN_CMD && !is_vlan_dev(dev))
                            goto out;
                }       
                switch (args.cmd)
                {
                    case SET_VLAN_INGRESS_PRIORITY_CMD: // 暫略
                        break;
                    case SET_VLAN_EGRESS_PRIORITY_CMD:  // 暫略
                        break;
                    case SET_VLAN_FLAG_CMD:
                        // 設置vlan標誌位命令的用戶需要進行管理員級別權限測試
                        if (!ns_capable(net->user_ns, CAP_NET_ADMIN))
                            break;
                        // 如果傳入的vlan設備名風格有效則更新
                        if ((args.u.name_type >= 0) &&(args.u.name_type < VLAN_NAME_TYPE_HIGHEST))
                        {
                            struct vlan_net *vn;
                            vn = net_generic(net, vlan_net_id);
                            vn->name_type = args.u.name_type;
                        }
                        break;
                    case ADD_VLAN_CMD:
                        // 添加vlan設備的用戶需要進行管理員級別權限測試
                        if (!ns_capable(net->user_ns, CAP_NET_ADMIN))
                            break;
                        // 在宿主設備上註冊一個新的vlan設備
                        register_vlan_device(dev, args.u.VID);
                        break;
                    case DEL_VLAN_CMD:
                        // 刪除vlan設備的用戶需要進行管理員級別權限測試
                        if (!ns_capable(net->user_ns, CAP_NET_ADMIN))
                            break;
                        // 註銷vlan設備
                        unregister_vlan_dev(dev, NULL);
                        break;
                    case GET_VLAN_REALDEV_NAME_CMD:
                        // 獲取vlan設備對應的宿主設備名
                        vlan_dev_get_realdev_name(dev, args.u.device2);
                        // 拷貝回用戶空間
                        copy_to_user(arg, &args,sizeof(struct vlan_ioctl_args));
                        break;
                    case GET_VLAN_VID_CMD:
                        // 獲取該vlan設備的vid
                        args.u.VID = vlan_dev_vlan_id(dev);
                        // 拷貝回用戶空間
                        copy_to_user(arg, &args,sizeof(struct vlan_ioctl_args));
                        break;
                }
            }
            
            3.1.1 VLAN設備的創建
                /* 在宿主設備上註冊一個新的VLAN設備
                 * @real_dev - 宿主設備(一般就是真實的網卡,但對交換機來說,可能是DSA中創建的虛擬端口)
                 * @vlan_id  - 要創建的VLAN設備的ID
                 */
                int register_vlan_device(struct net_device *real_dev, u16 vlan_id)
                {
                    // 首先獲取宿主設備所在的網絡命名空間
                    struct net *net = dev_net(real_dev);
                    // 然後根據vlan_net_id在索引得到對應的私有空間,也就是vlan_net
                    struct vlan_net *vn = net_generic(net, vlan_net_id);
                    // vlan id號不能超過4096
                    if (vlan_id >= VLAN_VID_MASK)
                        return -ERANGE;
                    // 檢查宿主設備是否支持vlan協議以及要創建的vlan id在該設備上是否已經存在
                    vlan_check_real_dev(real_dev, htons(ETH_P_8021Q), vlan_id);

                    // 根據vlan設備不同的顯示風格生成相應的vlan設備名
                    switch(vn->name_type)
                    {
                        case VLAN_NAME_TYPE_RAW_PLUS_VID_NO_PAD:
                            snprintf(name, IFNAMSIZ, "%s.%i", real_dev->name, vlan_id);
                            break;
                        ...
                    }

                    // 創建一個vlan設備,並執行vlan_setup完成基本的初始化
                    new_dev = alloc_netdev(sizeof(struct vlan_dev_priv), name, vlan_setup);
                    // 將新創建的vlan設備關聯到當前的網絡命名空間
                    dev_net_set(new_dev, net);
                    new_dev->mtu = real_dev->mtu;   // 將宿主設備的mtu繼承到vlan設備
                    new_dev->priv_flags |= (real_dev->priv_flags & IFF_UNICAST_FLT);    // 如果宿主設備支持單播過濾功能,則同樣繼承到vlan設備

                    // 初始化vlan設備附屬的私有空間vlan_dev_priv
                    vlan = vlan_dev_priv(new_dev);
                    vlan->vlan_proto = htons(ETH_P_8021Q);  
                    vlan->vlan_id = vlan_id;
                    vlan->real_dev = real_dev;
                    vlan->dent = NULL;
                    vlan->flags = VLAN_FLAG_REORDER_HDR;

                    new_dev->rtnl_link_ops = &vlan_link_ops;// 註冊netlink接口操作集合用於管理該vlan設備

                    // 進一步註冊該vlan設備
                    register_vlan_dev(new_dev);
                }

                /* 每個新創建的vlan設備初始化回調函數
                 */
                void vlan_setup(struct net_device *dev)
                {
                    // 爲vlan設備設置一些鏈路層基本參數
                    ether_setup(dev);
                    dev->priv_flags     |= IFF_802_1Q_VLAN; // 表明這是一個vlan設備
                    dev->priv_flags     &= ~(IFF_XMIT_DST_RELEASE | IFF_TX_SKB_SHARING);
                    dev->tx_queue_len   = 0;

                    dev->netdev_ops        = &vlan_netdev_ops;     // 註冊vlan設備管理操作回調函數集
                    dev->destructor        = vlan_dev_free;        // 註冊vlan設備註銷回調函數
                    dev->ethtool_ops    = &vlan_ethtool_ops;    // 註冊使用ethtool工具操作vlan設備的回調函數集

                    memset(dev->broadcast, 0, ETH_ALEN);        // 清零廣播mac
                }

                /* 進一步註冊該vlan設備
                 */
                int register_vlan_dev(struct net_device *dev)
                {
                    struct vlan_dev_priv *vlan = vlan_dev_priv(dev);    // 獲取vlan設備附屬的私有空間
                    struct net_device *real_dev = vlan->real_dev;       // 獲取宿主設備
                    u16 vlan_id = vlan->vlan_id;    // 獲取vlan id
                    
                    // 將指定vlan協議ID的vlan id添加到宿主設備的vlan_info管理塊中
                    vlan_vid_add(real_dev, vlan->vlan_proto, vlan_id);
                    // 確保程序運行到這裏時宿主設備的vlan_info成員不再爲空        
                    vlan_info = rtnl_dereference(real_dev->vlan_info);
                    // 對四維結構的vlan組模型在第三維度上進行預分配
                    vlan_group_prealloc_vid(grp, vlan->vlan_proto, vlan_id);
                    // 設置該vlan設備的嵌套級別
                    vlan->nest_level = dev_get_nest_level(real_dev, is_vlan_dev) + 1;
                    // 註冊該網絡設備到內核中,註冊結果會從通知鏈中反饋
                    register_netdevice(dev);
                    // 將vlan設備加入宿主設備的upper鄰接鏈表中
                    netdev_upper_dev_link(real_dev, dev);
                    // 宿主設備的引用計數加1
                    dev_hold(real_dev);
                    // 從宿主設備繼承一些狀態(比如設備的鏈路狀態)
                    netif_stacked_transfer_operstate(real_dev, dev);
                    // 將vlan設備加入lweventlist通知鏈
                    linkwatch_fire_event(dev);
                    // 最後一步,將指定的vlan設備記錄到四維vlan組模型中
                    vlan_group_set_device(grp, vlan->vlan_proto, vlan_id, dev);
                    grp->nr_vlan_devs++;
                }

        3.2 netlink方式分析
            VLAN模塊初始化過程中還通過vlan_netlink_init註冊了一組netlink接口vlan_link_ops用於操作VLAN模塊

            3.2.1 VLAN設備的創建
                /* 註冊並配置一個新的vlan設備
                 * @src_net     - 所處的網絡命名空間
                 * @dev         - 新創建的網絡設備
                 * @tb[]        - ?
                 * @data[]      - ?
                 *
                 * 備註:對應ioctl接口的函數register_vlan_device,主要的區別在於調用本函數前vlan設備管理塊已經被創建
                 */
                int vlan_newlink(struct net *src_net, struct net_device *dev,struct nlattr *tb[], struct nlattr *data[])
                {
                    struct vlan_dev_priv *vlan = vlan_dev_priv(dev);
                    // 檢查是否有傳入IFLA_VLAN_ID屬性
                    if (!data[IFLA_VLAN_ID])
                        return -EINVAL;
                    // 檢查是否有傳入IFLA_LINK屬性
                    if (!tb[IFLA_LINK])
                        return -EINVAL;

                    // 從IFLA_LINK屬性中獲取宿主設備接口序號,再根據接口序號索引得到對應的宿主設備
                    real_dev = __dev_get_by_index(src_net, nla_get_u32(tb[IFLA_LINK]));
                    // 如果傳入了IFLA_VLAN_PROTOCOL屬性則從中獲取vlan的協議類型,否則採用缺省的vlan協議類型
                    if (data[IFLA_VLAN_PROTOCOL])
                        proto = nla_get_be16(data[IFLA_VLAN_PROTOCOL]);
                    else
                        proto = htons(ETH_P_8021Q);

                    // 初始化vlan設備附屬的私有空間vlan_dev_priv
                    vlan->vlan_proto = proto;
                    vlan->vlan_id    = nla_get_u16(data[IFLA_VLAN_ID]);
                    vlan->real_dev   = real_dev;
                    vlan->flags  = VLAN_FLAG_REORDER_HDR;

                    // 檢查宿主設備是否支持vlan協議以及要創建的vlan id在該設備上是否已經存在
                    vlan_check_real_dev(real_dev, vlan->vlan_proto, vlan->vlan_id);
                    // 如果沒有傳入IFLA_MTU屬性則從宿主設備繼承mtu值,否則意味着使用了自定義的mtu值,這時候需要確保自定義值不大於宿主設備上的mtu值
                    if (!tb[IFLA_MTU])
                        dev->mtu = real_dev->mtu;
                    else if (dev->mtu > real_dev->mtu)
                        return -EINVAL;

                    // 該vlan設備在這裏繼續處理來自用戶空間設置的參數
                    vlan_changelink(dev, tb, data);

                    // 進一步註冊該vlan設備
                    return register_vlan_dev(dev);
                }
   
發佈了61 篇原創文章 · 獲贊 15 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章