Linux 內核角度分析 tcpdump 原理 -1-

一、tcpdump 的用途

tcpdump 是 Linux 系統抓包工具,tcpdump 基於 libpcap 庫,根據使用者的定義對網絡上的數據包進行截獲,tcpdump 可以將網絡中傳送的數據包中的 "頭" 完全截獲下來提供分析,支持針對網絡層、協議、主機、網絡或端口的過濾,並提供 and、or、not 等邏輯語句來幫助去掉無用的信息。通過 tcpdump 可以分析很多網絡行爲,比如丟包重傳、詳細報文、tcp 分組等,總之通過 tcpdunp 可以爲各種網絡問題進行排查, 可以在服務器上將捕獲的數據包信息以 pcap 文件保存下來,通過 wireshark 打開,更直觀地分析。

tcpdump 是基於 libpcap 庫的,數據包的過濾是基於 BPF(tcpdump 依附標準的 bpf 機器來運行,tcpdump 過濾規則會被轉化爲一段 bpf 指令並加載到內核中的 bpf 虛擬機器上執行),使用 bpf 虛擬機的 tcpdump 完美解決了包過濾問題。總之 tcpdump 使用 libpcap 這種鏈路層旁路處理的形式進行包捕獲,使用 bpf 機制實現對包的完美過濾。

更多介紹和使用可以從官網:

http://www.tcpdump.org/index.html#documentation 查閱到。

下面將先介紹 libpcap(第二章),對 libpcap 有一個瞭解,然後再從 Linux 內核角度分析 tcpdunp 的抓包原理 (第三章)。

二、libpcap 簡單介紹

libpcap(Packet Capture Library),即數據包捕獲函數庫,是 Unix/Linux 平臺下的網絡數據包捕獲函數庫,獨立於系統的用戶層包捕獲的 API 接口,爲底層網絡監測提供了一個可移植的框架。

利用 libpcap 函數庫開發應用程序的基本步驟:

  1. 捕獲各種數據包,例如:網絡流量統計。

  2. 過濾網絡數據包,例如:過濾掉本地上的一些數據,類似防火牆。

  3. 分析網絡數據包,例如:分析網絡協議,數據的採集。

  4. 存儲網絡數據包,例如:保存捕獲的數據以爲將來進行分析。

libpcap 庫在 linux 上的安裝過程

sudo apt-get insatll flex
sudo apt-get install bison
wget -c http://www.tcpdump.org/release/libpcap-1.7.4.tar.gz 
cd libpcap-1.7.4
./congigure
sudo make
sudo make install

測試:

//demo:查找當前系統的可用網絡設備
#include <stdio.h>
#include <pcap.h>

int main(int argc, char *argv[])
{
    char *dev,errbuf[1024];
   
    dev=pcap_lookupdev(errbuf);//函數用來查找網絡設備

    if(dev==NULL){
        printf("%s\n",errbuf);
        return 0;
    }

    printf("Device: %s\n", dev);
    return 0;
}

編譯:

#注意編譯時加上:-lpcap
gcc test.c -o test -lpcap

報錯提醒:

error while loading shared libraries: libpcap.so.1: cannot open shared object file: No such file or directory

解決:

  1. 執行 locate libpcap.so.1   ,  查看 libpcap.so.1 在系統中的路徑  ,  顯示爲 :  /usr/local/lib/libpcap.so.1.2.1

  2. 以管理員權限打開編輯 /etc/ld.so.conf 文件, 末尾新一行追加  /usr/local/lib ,  /usr/local/lib 爲 libpcap.so.1.7.4 所在目錄, 保存退出

  3. 以管理員權限執行 ldconfig(如果不支持改命令用 whereis ldconfig 查看並設置環境變量) 命令,

  4. 成功

幾個重要的 API

/*errbuf:存放出錯信息字符串,宏定義PCAP_ERRBUF_SIZE爲錯誤緩衝區大小
返回值爲設備名(第一個合適的網絡接口的字符串指針)
*/
char *pcap_lookupdev(char *errbuf)
/*獲取指定網卡的ip地址,子網掩碼
device:網絡設備名
netp:存放ip地址的指針
maskp:存放子網掩碼的指針
errbuf:存放出錯信息*/
int pcap_lookupnet(char *device,bpf_u_int32 *netp,bpf_u_int32 *maskp,char *errbuf  );
/*
device:網絡接口名字
snaplen:數據包大小,最大爲65535字節
promise:“1” 代表混雜模式,其它非混雜模式。什麼爲混雜模式
to_ms:指定需要等待的毫秒數,超過這個數值後,獲取數據包的函數就會立即返回(這個函數不會阻塞,後面的抓包函數纔會阻塞)。0 表示一直等待直到有數據包到來。

ebuf:存儲錯誤信息。
返回值:pcap_t類型指針,後面的所有操作都要用這個指針

    */
pcap_t *pcap_open_live(const char *device,int snaplen,int promisc,int to_ms,char *ebuf );
/*
p是嗅探器回話句柄
fp是一個bpf_program結構的指針,在pcap_compile()函數中被賦值。
str:指定的過濾條件
optimize參數控制結果代碼的優化。
netmask參數指定本地網絡的網絡掩碼
*/
int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask)
/*
p:pcap_open_live() 返回的 pcap_t 類型的指針
fp:pcap_compile() 的第二個參數
*/
int pcap_setfilter( pcap_t * p,  struct bpf_program * fp );

三、tcpdump 實現抓包原理剖析

使用 strace 追蹤

strace tcpdump tcp port 80

可以看到 tcpdump 抓包創建的的套接字類型 AF_PACKET

在 libpcap 庫源碼中也可以看到有調用 socket 系統調用:

tatic int
pcap_can_set_rfmon_linux(pcap_t *handle)
{
     ...
 
 sock_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
 if (sock_fd == -1) {
  (void)snprintf(handle->errbuf, PCAP_ERRBUF_SIZE,
      "socket: %s", pcap_strerror(errno));
  return PCAP_ERROR;
 }
 ...
}

AF_PACKET 和 socket 應用結合一般都是用於抓包分析, packet 套接字提供的是 L2 的抓包能力。

socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))

系統調用:

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
 ......

 retval = sock_create(family, type, protocol, &sock);
 if (retval < 0)
  return retval;

 return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}

socket 創建函數:

int sock_create(int family, int type, int protocol, struct socket **res)
{
 return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
}

調用:__sock_create

sock_create 函數主要就是創建了 socket . 同時根據之前 PF_PACKET 模塊註冊到全局變量 net_families 。 找到 af_packet.c 中初始化的 static const struct net_proto_family packet_family_ops。而 sock_create 函數中 err = pf->create(net, sock, protocol, kern); 最終就會調用 packet_family_ops 裏的 packet_create

int __sock_create(struct net *net, int family, int type, int protocol,
    struct socket **res, int kern)
{
 int err;
 struct socket *sock;
 const struct net_proto_family *pf;
    ......
 
 sock = sock_alloc();//分配socket結構空間
 if (!sock) {
  net_warn_ratelimited("socket: no more sockets\n");
  return -ENFILE; /* Not exactly a match, but its the
       closest posix thing */
 }

 sock->type = type;//記錄socket類型

#ifdef CONFIG_MODULES

 if (rcu_access_pointer(net_families[family]) == NULL)
  request_module("net-pf-%d", family);
#endif

 rcu_read_lock();
 pf = rcu_dereference(net_families[family]);//根據family協議簇找到註冊的(PF_PACKET)協議族操作表
 err = -EAFNOSUPPORT;
 if (!pf)
  goto out_release;

 if (!try_module_get(pf->owner))
  goto out_release;
 rcu_read_unlock();

 err = pf->create(net, sock, protocol, kern);//執行該協議族(PF_PACKET)的創建函數
    ......
 
}

Linux 內核中定義了 net_proto_family 結構體,用來指明不同的協議族對應的 socket 創建函數,family 字段是協議族的類型,create 是創建 socket 的函數,如下是 PF_PACKET 對應結構體。

static const struct net_proto_family packet_family_ops = {
 .family = PF_PACKET,
 .create = packet_create,
 .owner = THIS_MODULE,
};

找到 AF_PACKET 協議族對應的 create 函數:可以看到po->prot_hook.func = packet_rcv;po->prot_hook 其實 packet_type,packet_type 結構體: packet_type 結構體第一個 type 很重要,對應鏈路層中 2 個字節的以太網類型。而 dev.c 鏈路層抓取的包上報給對應模塊,就是根據抓取的鏈路層類型,然後給對應的模塊處理,例如 socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL)); ETH_P_ALL 表示所有的底層包都會給到 PF_PACKET 模塊的處理函數,這裏處理函數就是 packet_rcv 函數。

設置了回調函數: packet_rcv, 並通過 register_prot_hook(sk) 完成了註冊,其中註冊過程將再下面分析

static int packet_create(struct net *net, struct socket *sock, int protocol,
    int kern)
{
 struct sock *sk;
 struct packet_sock *po;
 __be16 proto = (__force __be16)protocol; /* weird, but documented */
 int err;
  ......
       po = pkt_sk(sk); 
 sk->sk_family = PF_PACKET;//設置sk協議族爲PF_PACKET
 po->num = proto;  //數據包的類型ETH_P_ALL
 po->xmit = dev_queue_xmit;

 err = packet_alloc_pending(po);
 if (err)
  goto out2;

 packet_cached_dev_reset(po);

 sk->sk_destruct = packet_sock_destruct;
 sk_refcnt_debug_inc(sk);

 /*
  * Attach a protocol block
  */

 spin_lock_init(&po->bind_lock);
 mutex_init(&po->pg_vec_lock);
    .....
 po->rollover = NULL;
 po->prot_hook.func = packet_rcv;//設置回調函數

 if (sock->type == SOCK_PACKET)
  po->prot_hook.func = packet_rcv_spkt;

 po->prot_hook.af_packet_priv = sk;

 if (proto) {
  po->prot_hook.type = proto;
  register_prot_hook(sk);//將這個socket掛載到ptype_all連接串列上
 }
  ......
}


//packet_sock結構體
struct packet_sock {
 /* struct sock has to be the first member of packet_sock */
 struct sock  sk;
 ......
 struct net_device __rcu *cached_dev;
 int   (*xmit)(struct sk_buff *skb);
 struct packet_type prot_hook ____cacheline_aligned_in_smp;//packet_create函數中通過該字段進行下一步的設置:po->prot_hook
};



/*po->prot_hook其實packet_type,packet_type結構體:
數據包完成鏈路層的處理後,需要提交給協議棧上層繼續處理,每個packet_type結構就是數據包的一個可能去向
packet_type 結構體第一個type 很重要,對應鏈路層中2個字節的以太網類型。而dev.c 鏈路層抓取的包上報給對應模塊,就是根據抓取的鏈路層類型,然後給對應的模塊處理,例如socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL)); ETH_P_ALL表示所有的底層包都會給到PF_PACKET 模塊的處理函數,這裏處理函數就是packet_rcv 函數。
*/
struct packet_type {
 __be16   type; /* type指定了協議的標識符,標記了packet_type收取什麼類型的數據包,處理程序func會使用該標識符 ,保存了三層協議類型,ETH_P_IP、ETH_P_ARP等等*/
 struct net_device *dev; /* NULL指針表示該處理程序對系統中所有網絡設備都有效    */
    /* func:packet_create函數通過該字段設置的回調函數:po->prot_hook.func = packet_rcv;
    func是該結構的主要成員,它是一個指向網絡層函數的指針,ip層處理時掛載的是ip_rcv
    */
 int   (*func) (struct sk_buff *,
      struct net_device *,
      struct packet_type *,
      struct net_device *);
 bool   (*id_match)(struct packet_type *ptype,
         struct sock *sk);
 void   *af_packet_priv;
 struct list_head list;
};

展開註冊函數 register_prot_hook(sk)

static void register_prot_hook(struct sock *sk)
{
 struct packet_sock *po = pkt_sk(sk);

 if (!po->running) {
  if (po->fanout)
   __fanout_link(sk, po);
  else
   dev_add_pack(&po->prot_hook);//將pacekt_type放到ptype_all鏈表上。

  sock_hold(sk);
  po->running = 1;
 }
}

//ptype_all鏈表:
struct list_head ptype_all __read_mostly;//全局變量

展開 dev_add_pack

//將pacekt_type放到ptype_all鏈表上。
void dev_add_pack(struct packet_type *pt)
{
 struct list_head *head = ptype_head(pt);//獲取ptype_all鏈表

 spin_lock(&ptype_lock);
 list_add_rcu(&pt->list, head);//將po->prot_hook掛載到ptype_all鏈表
 spin_unlock(&ptype_lock);
}


//獲取ptype_all鏈表
static inline struct list_head *ptype_head(const struct packet_type *pt)
{
 if (pt->type == htons(ETH_P_ALL))//type爲ETH_P_ALL時,則掛在ptype_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];////否則,掛在ptype_base[type&15]上面
}

綜上:tcpdump 在剛開始工作時創建了 PF_PACKET 套接字,並在全局的 ptype_all 中掛載了該套接字的 pt(packet_type *pt), 其中 pt 的字段 func 設置了相應的回調函數 packet_rcv(後面將分析該函數),到此 tcpdump 抓包的 socket(AF_PACKET) 創建完成,相應的準備工作完成。

網絡收包時 tcpdump 進行抓包

函數調用關係

調用關係:netif_receive_skb-->netif_receive_skb-->netif_receive_skb_internal->__netif_receive_skb-->__netif_receive_skb_core

核心函數__netif_receive_skb_core

static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
 ......
 //遍歷ptype_all,cpdump在創建socket時已將其packet_type掛載到了遍歷ptype_all
 list_for_each_entry_rcu(ptype, &ptype_all, list) {
  if (pt_prev)
   ret = deliver_skb(skb, pt_prev, orig_dev);//deliver函數回調用paket_type.func(),也就是packet_rcv 
  pt_prev = ptype;
 }

   ......
}

__netif_receive_skb_core 函數在遍歷 ptype_all 時,同時也執行了deliver_skb(skb, pt_prev, orig_dev);deliver 函數調用了 paket_type.func(),也就是 packet_rcv ,如下源碼所示:

static inline int deliver_skb(struct sk_buff *skb,
         struct packet_type *pt_prev,
         struct net_device *orig_dev)
{
 if (unlikely(skb_orphan_frags_rx(skb, GFP_ATOMIC)))
  return -ENOMEM;
 refcount_inc(&skb->users);
 return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);//調用tcpdump掛載的packet_rcv 函數
}

下面將展開 packet_rcv 函數進行分析; 函數接收到鏈路層網口的數據包後,會根據應用層設置的 bpf 過濾數據包,符合要求的最終會加到 struct sock sk 的接收緩存中。使用 BPF 過濾過程將在後面進行分析。

static int packet_rcv(struct sk_buff *skb, struct net_device *dev,
        struct packet_type *pt, struct net_device *orig_dev)
{
     ......
  if (sk->sk_type != SOCK_DGRAM)// 當 SOCK_DGRAM類型的時候,會截取掉鏈路層的數據包,從而返回給應用層的數據包是不包含鏈路層數據的
   skb_push(skb, skb->data - skb_mac_header(skb));
  else if (skb->pkt_type == PACKET_OUTGOING) {
   /* Special case: outgoing packets have ll header at head */
   skb_pull(skb, skb_network_offset(skb));
  }
 }
     ......
    //最後將底層網口符合應用層的數據複製到接收緩存隊列中
         
 res = run_filter(skb, sk, snaplen);   //將用戶指定的過濾條件使用BPF進行過濾
    ......
 spin_lock(&sk->sk_receive_queue.lock);
 po->stats.stats1.tp_packets++;
 sock_skb_set_dropcount(sk, skb);
 __skb_queue_tail(&sk->sk_receive_queue, skb);//將skb放到當前的接收隊列中
 spin_unlock(&sk->sk_receive_queue.lock);
 sk->sk_data_ready(sk);
 return 0;
    ......

}

綜上一旦關聯上鍊路層抓到的包就會 copy 一份給上層接口(即 PF_PACKET 註冊的回調函數 packet_rev). 而回調函數會根據應用層設置的 bpf 過濾數據包,最終放入接收緩存的數據包肯定是符合應用層想截取的數據。因此最後一步 recvfrom 也就是從接收緩存的數據包 copy 給應用層,如下源碼:

static int packet_recvmsg(struct socket *sock, struct msghdr *msg, size_t len,
     int flags)
{
    ......
        
 skb = skb_recv_datagram(sk, flags, flags & MSG_DONTWAIT, &err);//從接收緩存中接收數據

 ......
        
 err = skb_copy_datagram_msg(skb, 0, msg, copied);//將最終的數據copy到用戶空間
    ......
 
}

到這,網絡接收數據包時的抓包過程就結束了

網絡發包時 tcpdump 進行抓包

Linux 協議棧中提供的報文發送函數有兩個,一個是鏈路層提供給網絡層的發包函數 dev_queue_xmit(), 另一個就說軟中斷髮吧包函數之間調用的 sch_direct_xmit(), 這兩個函數最終都會調用 dev_hard_start_xmit()

struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev,
        struct netdev_queue *txq, int *ret)
{
    ......
   while (skb) {
  struct sk_buff *next = skb->next;

  skb->next = NULL;
  rc = xmit_one(skb, dev, txq, next != NULL);//調用xmit_one來發送一個到多個數據包
  ......
}

xmit_one(): 發送一個到多個數據包

static int xmit_one(struct sk_buff *skb, struct net_device *dev,
      struct netdev_queue *txq, bool more)
{   
    ......

 if (!list_empty(&ptype_all) || !list_empty(&dev->ptype_all))
  dev_queue_xmit_nit(skb, dev);//通過調用dev_queue_xmit這個網絡設備接口層函數發送給driver,
    ......
}

dev_queue_xmit_nit(): 將數據包發送給 driver

void dev_queue_xmit_nit(struct sk_buff *skb, struct net_device *dev)
{
 ......
 list_for_each_entry_rcu(ptype, ptype_list, list) {
  /* Never send packets back to the socket
   * they originated from - MvS (miquels@drinkel.ow.org)
   */
  if (skb_loop_sk(ptype, skb))
   continue;

  if (pt_prev) {
   deliver_skb(skb2, pt_prev, skb->dev);
   pt_prev = ptype;
   continue;
  }
    ......
}

在遍歷 ptype_all 時,同時也執行了deliver_skb(skb, pt_prev, orig_dev);deliver 函數調用了 paket_type.func(),也就是 packet_rcv ,如下源碼所示:

static inline int deliver_skb(struct sk_buff *skb,
         struct packet_type *pt_prev,
         struct net_device *orig_dev)
{
 if (unlikely(skb_orphan_frags_rx(skb, GFP_ATOMIC)))
  return -ENOMEM;
 refcount_inc(&skb->users);
 return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);//調用tcpdump掛載的packet_rcv 函數
}

下面的流程就和網絡收包時 tcpdump 進行抓包一樣了 (packet_rcv 函數中會將用戶設置的過濾條件,通過 BPF 進行過濾,並將過濾的數據包添加到接收隊列中, 應用層在 libpcap 庫中調用 recvfrom 。 PF_PACKET 協議簇模塊調用 packet_recvmsg 將接收隊列中的數據 copy 應用層)

tcpdump 進行抓包的內核流程梳理

總結

本文主要從 tcpdump 抓包時調用的 libpcap 庫開始梳理,從 socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)) 進入系統調用,再從內核角度對 Tcpdump 在收包和發包的流程分析了一遍,其實還有一個重點:BPF 的過濾過程,如下源碼所示:run_filter(skb, sk, snaplen),下次文章將對 BPF 的過濾過程進行一些分析。

static int packet_rcv(struct sk_buff *skb, struct net_device *dev,
        struct packet_type *pt, struct net_device *orig_dev)
{
  ......
         
 res = run_filter(skb, sk, snaplen);   //將用戶指定的過濾條件使用BPF進行過濾
    ......
 __skb_queue_tail(&sk->sk_receive_queue, skb);//將skb放到當前的接收隊列中
    ......

}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/aaXcfAst_fFs3GB0VfittA