使用 nlmon 驅動抓取 netlink 報文的原理
前言
在 如何抓取 netlink 報文 這篇部落格中,我描述了使用 nlmon 驅動建立虛擬 tap 口抓取 netlink 報文的過程,在這篇文章中,我探討下這一過程背後的原理。
nlmon 驅動
nlmon 驅動原始碼位於核心原始碼樹中 drivers/net/ 目錄中,由單個原始檔——nlmon.c 組成,其程式碼長度只有 150 多行。
與普通的驅動程式碼一樣,它也有一個初始化與解初始化函式,這兩個函式分別實現的功能是在核心中註冊一個 rtnl_link_ops 結構,解除註冊的 rtnl_link_ops 結構。
rtnetlink link_ops 連結串列
這裡首先需要指明的是,在 rtnetlink.c 中建立了一個 link_ops 連結串列,這個連結串列將所有的 rtnl_link_ops 以 find 為唯一識別符號連結起來。
這裡描述的 rtnl_link_ops 是什麼東西呢?
這裡的 rtnl_link_ops 是 net device 中 netlink 相關操作的方法,在每個 struct net 結構體中有一個指向 rtnl_link_ops 的指標,用以例項化不同的 rtnl_link_ops。
struct net_device 中的相關定義如下:
const struct rtnl_link_ops *rtnl_link_ops;
這裡我需要說明兩點內容:
- rtnl_link_register 函式與 rtnl_link_unregister 函式都涉及到了對 link_ops 的操作。
- rtnl_link_ops 通過 struct net 中的 rtnl_link_ops 指標與一個 netdev 關聯起來,註冊一個 rtnl_link_ops 並不涉及與 netdev 的關聯,只需要在 link_ops 連結串列中新增一個節點就行,而當刪除一個 rtnl_link_ops 時,就需要對 netdev 中使用到待刪除的 rtnl_link_ops 的網路裝置進行相應處理。
這裡也說明其實 rtnl_link_ops 類似於一個框架性的功能,netdev 是它的客戶,它本身的註冊類似於擴充套件功能的行為,對 netdev 客戶的正在使用的功能沒有影響,但是當解註冊時就需要考慮可能正在被一個 netdev 客戶使用的場景。
下面我分別描述下 rtnl_link_ops 的註冊與解註冊相關函式的原理。
rtnl_link_register 函式
rtnl_link_register 函式的主要邏輯及如下:
- 使用 rtnl_link_ops 中的 find 欄位檢索 link_ops 連結串列,如果存在則返回 -EEXIST
- find 欄位檢索 link_ops 連結串列發現待鏈入的 ops 不存在則將傳入的 ops 鏈入到 link_ops 中。
這裡核心還對傳入的 rtnl_link_ops 中的函式指標進行了合法值校驗,當 setup 函式值非空而 dellink 為空的時候,核心將 dellink 設定為 unregister_netdevice_queue 函式。
同時需要注意的是 link_ops 是一個共享的資料結構,對它的修改需要進行序列化處理。
可以看到,在 rtnl_link_register 中對 link_ops 連結串列的修改是在佔用了 rtnl 鎖的條件下執行的,執行完成後釋放 rtnl 鎖就完成了註冊的完整過程。
rtnl_link_unregister 函式
相較 rtnl_link_register 而言,unregister 函式做了更多的事情,從這個 unregister 函式中我們可以看到一些更內部的原理。
其主要執行邏輯如下:
-
獲取 pernet_ops_rwsem 訊號量,就獲取此訊號量是為了消除與 setup_net 及 cleanup_net 的競爭條件。(具體的場景目前我並不清楚!)
-
呼叫 rtnl_lock_unregistering_all
rtnl_lock_unregisering_all 函式值得研究,其程式碼如下:
/* Return with the rtnl_lock held when there are no network
* devices unregistering in any network namespace.
*/
static void rtnl_lock_unregistering_all(void)
{
struct net *net;
bool unregistering;
DEFINE_WAIT_FUNC(wait, woken_wake_function);
add_wait_queue(&netdev_unregistering_wq, &wait);
for (;;) {
unregistering = false;
rtnl_lock();
/* We held write locked pernet_ops_rwsem, and parallel
* setup_net() and cleanup_net() are not possible.
*/
for_each_net(net) {
if (net->dev_unreg_count > 0) {
unregistering = true;
break;
}
}
if (!unregistering)
break;
__rtnl_unlock();
wait_woken(&wait, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
remove_wait_queue(&netdev_unregistering_wq, &wait);
}
上述函式,當沒有任何一個 net namespace 中的 netdev 處於 unregistering 狀態時它會直接返回並佔有 rtnl_lock,當檢測到有 netdev 處於 unregistering 狀態時,它會設定 timeout 並將當前執行緒掛起等待,此函式會等待所有 namespace 中的即將 unregistering 的 netdev 事件完成後返回並佔有 rtnl_lock。在這一過程完成後此函式會將當前程式從 netdev_unregistering_wq 等待佇列中移除
這裡需要注意的是在 for_each_net 中通過判斷 dev_unreg_count 的值來判斷是否有 netdev 待釋放,實際上 netdev 的釋放過程使用了類似延後釋放的機制,真正釋放是在呼叫了 netdev_run_todo 後完成的,在 netdev_run_todo 中還會喚醒等待 netdev_unregistering 事件的程式,與喚醒相關的程式碼如下:
wake_up(&netdev_unregistering_wq);
wak_up 最終會呼叫 __wake_up_common 來執行預先註冊的喚醒事件函式,實際上真正將掛起到等待佇列中的程式狀態也是在這個函式中完成的。
上面提到的喚醒事件函式是 wait_queue_entry 結構體中的 func 函式指標。wait_queue_entry 結構體內容如下:
struct wait_queue_entry {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head entry;
};
- 呼叫 __rtnl_link_unregister
此函式遍歷所有的 netdev 結構,並使用 __rtnl_kill_links 來呼叫 netdev 中使用了傳入的 rtnl_link_ops 的 dellink 函式,然後呼叫 unregister_netdevice_many 來 unregister 這些相關的 netdev。最終將 ops 從註冊連結串列中移除就完成了所有的過程。
- 釋放 pernet_ops_rwsem 訊號量
nlmon_link_ops
nlmon_link_ops 是一個 rtnl_link_ops 的例項,它通過呼叫上文描述的 rtnl_link_register 函式來完成工作。
rtnl_link_ops 中的 setup 函式在 nlmon 驅動中有自己的實現,其程式碼如下:
static void nlmon_setup(struct net_device *dev)
{
dev->type = ARPHRD_NETLINK;
dev->priv_flags |= IFF_NO_QUEUE;
dev->netdev_ops = &nlmon_ops;
dev->ethtool_ops = &nlmon_ethtool_ops;
dev->needs_free_netdev = true;
dev->features = NETIF_F_SG | NETIF_F_FRAGLIST |
NETIF_F_HIGHDMA | NETIF_F_LLTX;
dev->flags = IFF_NOARP;
/* That's rather a softlimit here, which, of course,
* can be altered. Not a real MTU, but what is to be
* expected in most cases.
*/
dev->mtu = NLMSG_GOODSIZE;
dev->min_mtu = sizeof(struct nlmsghdr);
}
這個 nlmon_setup 函式程式碼對 netdev 中的多個欄位進行了設定,其中需要注意的是 netdev_ops 與 ethtool_ops,這兩個欄位表明 nlmon 實現了一組虛擬網路卡驅動。
nlmon 驅動中 netdev_ops 結構體程式碼如下:
static const struct net_device_ops nlmon_ops = {
.ndo_init = nlmon_dev_init,
.ndo_uninit = nlmon_dev_uninit,
.ndo_open = nlmon_open,
.ndo_stop = nlmon_close,
.ndo_start_xmit = nlmon_xmit,
.ndo_get_stats64 = nlmon_get_stats64,
};
這裡 ndo_open 與 ndo_stop 是 ifconfig up、ifconfig down 最終呼叫到的函式介面。
這裡我著重描述下 nlmon_open 函式的執行過程。首先貼上函式的程式碼:
static int nlmon_open(struct net_device *dev)
{
struct nlmon *nlmon = netdev_priv(dev);
nlmon->nt.dev = dev;
nlmon->nt.module = THIS_MODULE;
return netlink_add_tap(&nlmon->nt);
}
這裡呼叫了 netlink_add_tap 介面,這個介面可以理解為建立了一個 netlink 型別的 tap 口,tcpdump 抓取 netlink 訊息實際上就是從這個 tap 口的接收與傳送佇列中獲取 netlink 資料包的。
使用者態傳送 netlink 到核心態以及核心態傳送 netlnk 到使用者態,報文都會複製到這個註冊的 netlink tap 口中,這樣 tcpdump 就能夠從這個 netlink tap 口中抓取到 netlink 報文了。
可以看到在 netlink 傳送與接收的介面中都有呼叫 netlink_deliver_tap、 netlink_deliver_tap_kernel 來投遞訊息到 netlink tap 口中。
tcpdump 抓取 netlink 包的原理
上面已經大致描述完了使用 nlmon 驅動抓取 netlink 報文的原理,不過對與 tcpdump 如何從核心抓取報文卻沒有進行描述,這裡簡單的提一提。
tcpdump 首先建立一個 AF_PACKET 型別的 socket,這個 socket 有自己獨立的 proto_os 操作。然後 tcpdump 通過 ioctl 獲取 nlmon0 網路介面的 ifindex,這個 ifindex 被用來獲取 net_device 結構。
tcpdump 的鉤子函式在 af_packet 協議操作的 bind 函式中 hook 到對應的 net_device 結構中,在這個結構中新增了一個協議。
核心程式碼是呼叫 register_prot_hook 函式,此函式中主要通過 dev_add_pack 來完成工作。
dev_add_pack 函式程式碼如下:
void dev_add_pack(struct packet_type *pt)
{
struct list_head *head = ptype_head(pt);
spin_lock(&ptype_lock);
list_add_rcu(&pt->list, head);
spin_unlock(&ptype_lock);
}
這裡需要注意 ptype_head 函式,其程式碼如下:
static inline struct list_head *ptype_head(const struct packet_type *pt)
{
if (pt->type == htons(ETH_P_ALL))
return pt->dev ? &pt->dev->ptype_all : &ptype_all;
else
return pt->dev ? &pt->dev->ptype_specific :
&ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}
這裡 pt->dev 就是待 hook 的 dev,在呼叫此函式前已經設定了此欄位,這裡的目標就是 nlmon0 的 net_device 結構體。
可以看到 dev_add_pack 實際上是修改 dev->ptype_all、dev->ptype_specific 連結串列的內容。
底層介面在收到包後要進行協議棧分發,這時候就會訪問 ptype_all 與 ptype_specific 連結串列,將報文 deliver 到連結串列中註冊的協議中,這裡的註冊的 af_packet 協議也會被推送報文,這樣 af_packet 協議就能夠得到一份報文的拷貝,並傳遞給抓包模組,就完成了抓包的過程。
與上面描述相關的程式碼如下:
static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc,
struct packet_type **ppt_prev)
{
list_for_each_entry_rcu(ptype, &ptype_all, list) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
總結
本文對 nlmon 驅動以及 tcpdump 抓取 netlink 報文的工作原理進行了描述,儘管 nlmon 驅動的程式碼內容很少,但是其依賴的函式介面卻不容小覷,同時也必須指出的是 tcpdump 工具的原理也比表面上看上更復雜一些,不過對其原理進行研究有助於提高我對協議棧工作原理的瞭解。
這篇文章發出,算上私密的兩篇文章,就完成了 200 篇文章的目標,這個目標的實現值得慶祝,同時也意味著我能夠挑戰更高的目標,我想這也是不成為問題的。
相關文章
- Linux 下使用 NetLink 檢測裝置的熱插拔Linux
- Binder驅動的使用
- STM32驅動LCD原理
- 甲骨文:AI驅動的復興之路AI
- 爬蟲原理與資料抓取爬蟲
- 一文讀懂 MongoDB驅動程式 APIMongoDBAPI
- 使用代理抓取網頁的原因網頁
- 爬蟲抓取網頁資料原理爬蟲網頁
- 如何使用 Github Actions 自動抓取每日必應桌布?Github
- Laravel驅動管理類Manager的分析和使用Laravel
- 使用Spring Boot的消費者驅動合同Spring Boot
- 基於顯揚科技3D機器視覺的驅動輪抓取上料系統3D視覺
- MySQL的驅動表與被驅動表MySql
- nvidia驅動安裝過程中報已有nouveau驅動錯誤解決
- Charles 抓取移動裝置資料包基本使用教程
- 事件驅動的微服務-事件驅動設計事件微服務
- 測試驅動開發(TDD)總結——原理篇
- 這一次搞懂SpringBoot核心原理(自動配置、事件驅動、Condition)Spring Boot事件
- 驅動精靈是幹嘛的 驅動精靈怎麼安裝驅動
- linux驅動之LED驅動Linux
- extcon驅動及其在USB驅動中的應用
- 移動端使用rem原理REM
- colly 自動抓取資訊
- C# 使用特性的方式封裝報文C#封裝
- 轉發 安裝 scount 的 es 驅動,報錯解決
- Qt中使用TCP接收報文QTTCP
- 手把手教你玩轉藍芽模組(原理+驅動)藍芽模組
- NodeJS使用PhantomJs抓取網頁NodeJS網頁
- win10系統升級顯示卡驅動的方法【圖文】Win10
- 專訪文青松|AI時代的教育革新:深度融合,驅動未來AI
- 新字元驅動框架驅動LED字元框架
- DDD領域驅動最全詳解(圖文全面總結)
- 【linux】驅動-8-一文解決裝置樹Linux
- Laravel 優化 Auth 使用快取驅動Laravel優化快取
- 何時使用領域驅動設計
- 印表機驅動程式無法使用怎麼解決 印表機驅動程式無法使用w10
- 既支援時間驅動又支援事件驅動,TDengine3.0流式計算的學習使用心得事件
- UiBot無法抓取Chrome元素和資料抓取工具無法使用的解決方案UIChrome