ARP協議與鄰居子系統剖析(基於 Linux-2.4.0已更新)


ARP協議與鄰居子系統剖析

學習過 TCP/IP 協議的同學都應該了解過 TCP/IP 五層網絡模型,如下圖:

image

從上圖可以看出,ARP協議 位於 TCP/IP 五層網絡模型的 網絡層。那么,ARP協議 的用途是什么呢?

ARP協議介紹

在局域網中(同一個路由器內),主機與主機之間需要通過 MAC 地址進行通訊。但由於 MAC 地址過於復雜,不容易被人類記憶。所以,人們更傾向於使用更容易記憶的 IP 地址。

但局域網只能使用 MAC 地址通訊,那有什么辦法可以通過主機的 IP 地址來獲取到主機的 MAC 地址呢?ARP協議 就應運而生。

ARP(Address Resolution Protocol) 即地址解析協議, 用於實現從 IP 地址到 MAC 地址的映射,即詢問目標IP對應的MAC地址。

ARP協議 通過廣播消息,向局域網的所有主機廣播 ARP請求消息,從而詢問主機的 IP 地址對應的 MAC 地址,如下圖:

image

如上圖所示,A主機要與B主機通信,但是只知道B主機的 IP 地址,所以這時可以向局域網廣播一條 ARP請求消息,用於詢問 IP 地址為 192.168.1.2 的主機所對應的 MAC 地址。

由於 ARP請求消息 是廣播消息,所以局域網的所有主機都會收到這條消息,但只有對應 IP 地址的主機才會回答這條消息。如上圖的B主機會回復一條 ARP應答消息,用於告訴A主機自己的 MAC 地址。

當A主機知道了B主機的 MAC 地址后,就能通過 MAC 地址與B主機進行通信了。

ARP協議頭部

每種網絡協議都有其協議頭部,用於本協議的通信,ARP協議 的頭部格式如下:

image

上圖是 ARP協議 頭部各個字段的信息,其代碼結構定義如下(路徑: /src/include/linux/if_arp.h):

struct arphdr
{
    unsigned short  ar_hrd;     /* format of hardware address   */
    unsigned short  ar_pro;     /* format of protocol address   */
    unsigned char   ar_hln;     /* length of hardware address   */
    unsigned char   ar_pln;     /* length of protocol address   */
    unsigned short  ar_op;      /* ARP opcode (command)         */

#if 0
   /* 下面部分沒有定義, 因為不同的鏈路層協議使用的地址格式不一定相同,
    * 所以下面只是以太網和IP協議的示例而已.
    *  Ethernet looks like this : This bit is variable sized however...
    */
    unsigned char       ar_sha[ETH_ALEN];   /* sender hardware address  */
    unsigned char       ar_sip[4];          /* sender IP address        */
    unsigned char       ar_tha[ETH_ALEN];   /* target hardware address  */
    unsigned char       ar_tip[4];          /* target IP address        */
#endif
}

從代碼可以看出,arphdr 結構的各個字段與上圖一一對應。下面說說各個字段的作用:

  • ar_hrd:硬件類型,如硬件類型是以太網,那么設置為 1

  • ar_pro:協議類型,由於 ARP 協議支持將多種不同協議地址轉換成 MAC 地址(如 IP 協議、AX.25 協議等),所以需要通過這個字段指明要轉換的協議類型。如 IP 協議設置為 0x0800

  • ar_hln:硬件地址長度,如以太網地址長度是 6。

  • ar_hln:協議地址長度,如 IP 地址長度是 4。

  • ar_op:操作碼,如 ARP請求 設置為 1,而 ARP應答 設置為 2。

下面的字段是不定長的,根據硬件類型和協議類型的改變而改變。

譬如:如果是硬件類型是以太網,並且協議類型是 IP 協議,那么 ar_sha 字段和 ar_tha 字段分別為 6 個字節,而 ar_sip 字段和 ar_tip 字段分別為 4 個字節。

鄰居子系統

首先說明一下什么是 鄰居:在同一局域網中,每一台主機都可以稱為其他主機的 鄰居。例如 Windows 系統可以在網絡中查看到同一局域網的鄰居主機,如下圖:

image

如上圖所示,每一台主機都可以稱為 鄰居節點

在 Linux 內核中,也把局域網中的每台主機稱為 鄰居節點,使用結構 neighbour 來描述,neighbour 結構定義如下:

struct neighbour
{
  struct neighbour    *next;      // 用於連接哈希表中相同哈希值的鄰居節點
  struct neigh_table  *tbl;       // 管理鄰居節點的鄰居表結構
  struct neigh_parms  *parms;     // 參數列表
  struct net_device   *dev;       // 可以與這個鄰居節點通信的設備
  unsigned long       used;       // 最后使用時間
  unsigned long       confirmed;  // 最后確認時間
  unsigned long       updated;    // 最后更新時間
  __u8                flags;      // 標志位
  __u8                nud_state;  // 鄰居節點所處於的狀態
  __u8                type;       // 類型
  __u8                dead;       // 是否已經失效
  atomic_t            probes;
  rwlock_t            lock;       // 鎖
                                  // 鄰居節點的硬件地址
  unsigned char       ha[(MAX_ADDR_LEN+sizeof(unsigned long)-1)&~(sizeof(unsigned long)-1)];
  struct hh_cache     *hh;
  atomic_t            refcnt;         // 引用計數器
  int                 (*output)(struct sk_buff *skb); // 發送數據給此鄰居節點的接口
  struct sk_buff_head arp_queue;      // 等待ARP回復的數據包列表(需要發送的數據包列表)
  struct timer_list   timer;          // 定時器
  struct neigh_ops    *ops;           // 操作方法列表
  u8                  primary_key[0]; // 要轉換成MAC地址的上層協議地址(如IP地址)
};

neighbour 結構中,比較重要的字段有:

  • ha:鄰居節點的硬件地址,因為與鄰居節點通信需要知道其硬件地址(MAC地址)。
  • output:向鄰居節點發送數據的接口,當要向鄰居節點發送數據時,使用這個接口把數據發送出去。
  • dev:輸出設備,如果向當前鄰居節點發送數據,需要通過這個設備來發送。
  • primary_key:要轉換成 MAC 地址的上層協議地址(如 IP 地址),由於上層協議不確定,所以這里設置 primary_key柔性數組(可變大小數組)(不了解柔性數組可以查閱相關的資料)。

如下圖所示:

image

所以,當本機需要向某一台 鄰居節點 主機發送數據時,首先需要通過上層協議地址與輸出設備查找對應的 neighbour 對象是否已經存在。如果存在,那么就使用這個 neighbour 對象。否則,就新創建一個 neighbour 對象,並初始化其各個字段。

查找鄰居節點信息

要查找一個 鄰居節點 的信息,可以通過調用 neigh_lookup() 函數來完成,其實現如下:

struct neighbour *
neigh_lookup(struct neigh_table *tbl, const void *pkey, struct net_device *dev)
{
  struct neighbour *n;
  u32 hash_val;
  int key_len = tbl->key_len;

  hash_val = tbl->hash(pkey, dev); // 通過IP地址與設備計算鄰居節點信息的哈希值

  read_lock_bh(&tbl->lock);

  // 通過設備和IP地址從鄰居節點哈希表中查找鄰居節點信息
  for (n = tbl->hash_buckets[hash_val]; n; n = n->next) {
    if (dev == n->dev && memcmp(n->primary_key, pkey, key_len) == 0) {
      neigh_hold(n);
      break;
    }
  }

  read_unlock_bh(&tbl->lock);

  return n; // 返回鄰居節點信息對象
}

neigh_lookup() 函數的參數含義如下:

  • tbl:鄰居節點管理表。
  • pkey:上層協議地址(如 IP 地址)。
  • dev:輸出設備對象。

neigh_lookup() 函數工作原理如下:

  • 首先通過上層協議地址與設備計算鄰居節點信息的哈希值。
  • 然后在鄰居節點哈希表中查找對應的鄰居節點信息,如果找到即返回鄰居節點信息,否則返回NULL。

創建鄰居節點信息

鄰居節點 信息不存在時,可以通過調用 neigh_create() 函數來創建,其實現如下:

struct neighbour *
neigh_create(struct neigh_table *tbl, const void *pkey, struct net_device *dev)
{
    struct neighbour *n, *n1;
    u32 hash_val;
    int key_len = tbl->key_len;
    int error;

    n = neigh_alloc(tbl); // 創建一個新的鄰居節點信息對象
    if (n == NULL)
        return ERR_PTR(-ENOBUFS);

    memcpy(n->primary_key, pkey, key_len); // 復制上層協議地址到 primary_key 字段

    n->dev = dev; // 綁定輸出設備
    dev_hold(dev);

    // 對於ARP協議會調用 arp_constructor() 函數
    if (tbl->constructor && (error = tbl->constructor(n)) < 0) {
        neigh_release(n);
        return ERR_PTR(error);
    }
    ...
    hash_val = tbl->hash(pkey, dev);

    write_lock_bh(&tbl->lock);
    ...
    // 把鄰居節點信息添加到鄰居節點信息管理哈希表中
    n->next = tbl->hash_buckets[hash_val];
    tbl->hash_buckets[hash_val] = n;
    n->dead = 0;
    neigh_hold(n);

    write_unlock_bh(&tbl->lock);

    return n;
}

neigh_create() 函數的工作原理如下:

  • 調用 neigh_alloc() 函數向內存申請一個新的鄰居節點信息對象。
  • 把上層協議地址(如 IP 地址)復制到 primary_key 字段中。
  • 綁定輸出設備到 dev 字段。
  • 調用鄰居節點信息管理哈希表的 constructor() 方法來初始化鄰居節點信息對象,對於 ARP協議 這里調用的是 arp_constructor() 函數。
  • 把新創建的鄰居節點信息對象添加到鄰居節點信息管理哈希表中。

對於 arp_constructor() 函數,主要是用於初始化鄰居節點信息對象的操作方法列表和 output 字段,如下:

static struct neigh_ops arp_generic_ops = {
    AF_INET,                  // family
    NULL,                     // destructor
    arp_solicit,              // solicit
    arp_error_report,         // error_report
    neigh_resolve_output,     // output
    neigh_connected_output,   // connected_output
    dev_queue_xmit,           // hh_output
    dev_queue_xmit            // queue_xmit
};

static int arp_constructor(struct neighbour *neigh)
{
    u32 addr = *(u32*)neigh->primary_key;
    struct net_device *dev = neigh->dev;
    ...
    neigh->type = inet_addr_type(addr);
    ...
    if (dev->hard_header_cache)
        neigh->ops = &arp_hh_ops;
    else
        neigh->ops = &arp_generic_ops;

    if (neigh->nud_state & NUD_VALID)
        neigh->output = neigh->ops->connected_output;
    else
        neigh->output = neigh->ops->output;

    return 0;
}

arp_constructor() 函數的實現可以知道,鄰居節點信息對象的ops 字段被設置為 arp_generic_ops,而 output 字段會被設置為 neigh_resolve_output() 函數(當鄰居節點信息對象不可用時)或者 neigh_connected_output() 函數(當鄰居節點信息對象可用時)。

所以,一個新創建的 鄰居節點信息對象 各個字段的值大概如下圖所示:

image

由於此時還不知道鄰居節點的 MAC 地址,所以其 ha 字段的值為 0。

向鄰居節點發送數據

當向鄰居節點發送數據時,需要調用鄰居節點信息對象的 output 接口。根據前面的分析,output 接口被設置為 neigh_resolve_output() 函數。我們來分析一下 neigh_resolve_output() 函數的實現:

int neigh_resolve_output(struct sk_buff *skb)
{
    struct dst_entry *dst = skb->dst;
    struct neighbour *neigh;
    ...
    if (neigh_event_send(neigh, skb) == 0) {
        int err;
        struct net_device *dev = neigh->dev;

        if (dev->hard_header_cache && dst->hh == NULL) {
            ...
        } else {
            read_lock_bh(&neigh->lock);
            err = dev->hard_header(skb, dev, ntohs(skb->protocol), 
                                   neigh->ha, NULL, skb->len);
            read_unlock_bh(&neigh->lock);
        }
        if (err >= 0)
            return neigh->ops->queue_xmit(skb);
        kfree_skb(skb);
        return -EINVAL;
    }
    return 0;
    ...
}

neigh_resolve_output() 函數主要完成三件事:

  • 調用 neigh_event_send() 函數發送一個查詢鄰居節點 MAC 地址的 ARP 請求。
  • 調用設備的 hard_header() 方法設置數據包的目標 MAC 地址(如果鄰居節點的 MAC 地址已經獲取到)。
  • 如果數據包的目標 MAC 地址設置成功,調用鄰居節點信息對象的 queue_xmit() 方法把數據發送出去(對於 ARP 協議,queue_xmit() 方法對應的是 dev_queue_xmit() 函數)。

我們再來看看 neigh_event_send() 函數怎么發送 ARP 請求:

int __neigh_event_send(struct neighbour *neigh, struct sk_buff *skb)
{
    write_lock_bh(&neigh->lock);
    if (!(neigh->nud_state & (NUD_CONNECTED|NUD_DELAY|NUD_PROBE))) {
        if (!(neigh->nud_state & (NUD_STALE|NUD_INCOMPLETE))) {
            if (neigh->parms->mcast_probes + neigh->parms->app_probes) {
                ...
                neigh->nud_state = NUD_INCOMPLETE;
                ...
                neigh->ops->solicit(neigh, skb); // 發送查詢鄰居節點MAC地址的ARP請求
                ...
            } else {
                ...
            }
        }

        if (neigh->nud_state == NUD_INCOMPLETE) {
            if (skb) {
                ...
                __skb_queue_head(&neigh->arp_queue, skb); // 把數據包添加到arp_queue隊列中
            }
            write_unlock_bh(&neigh->lock);
            return 1;
        }
        ...
    }
    write_unlock_bh(&neigh->lock);
    return 0;
}

__neigh_event_send() 函數主要完成兩個工作:

  • 首先調用鄰居節點信息對象的 solicit() 方法發送一個 ARP 請求,從前面的分析可知,solicit() 方法會被設置為 arp_solicit() 函數。
  • 然后把要發送的數據包添加到鄰居節點信息對象的 arp_queue 隊列中,等待獲取到鄰居節點 MAC 地址后重新發送這個數據包。

鄰居節點信息對象的 arp_queue 隊列用於緩存等待發送的數據包,如下圖:

image

發送 ARP 請求

通過前面的分析可知,當向鄰居節點發送數據時,如果還不知道鄰居節點的 MAC 地址,那么首先會調用 arp_solicit() 函數發送一個 ARP請求 來獲取鄰居節點的 MAC 地址,其實現如下:

static void arp_solicit(struct neighbour *neigh, struct sk_buff *skb)
{
    u32 saddr;                               // 源IP地址(本地IP地址)
    u8 *dst_ha = NULL;                       // 接收ARP請求目標MAC地址(發廣播信息設置為NULL)
    struct net_device *dev = neigh->dev;     // 輸出設備
    u32 target = *(u32*)neigh->primary_key;  // 要查詢的鄰居節點IP地址
    ...

    if (skb && inet_addr_type(skb->nh.iph->saddr) == RTN_LOCAL)
        saddr = skb->nh.iph->saddr;
    else
        saddr = inet_select_addr(dev, target, RT_SCOPE_LINK);
    ...

    arp_send(ARPOP_REQUEST, ETH_P_ARP, target, dev, saddr,
             dst_ha, dev->dev_addr, NULL);
    ...
}

arp_solicit() 函數最終會調用 arp_send() 函數發送一個 ARP 請求,我們來分析一下 arp_send() 函數的實現:

void arp_send(int type, int ptype, u32 dest_ip,
              struct net_device *dev, u32 src_ip,
              unsigned char *dest_hw, unsigned char *src_hw,
              unsigned char *target_hw)
{
    struct sk_buff *skb;
    struct arphdr *arp;
    unsigned char *arp_ptr;
    ...
    // 申請一個數據包對象
    skb = alloc_skb(sizeof(struct arphdr) + 2*(dev->addr_len+4)
                        + dev->hard_header_len + 15, GFP_ATOMIC);
    ...

    // ARP請求頭部
    arp = (struct arphdr *)skb_put(skb, sizeof(struct arphdr) + 2*(dev->addr_len+4));

    skb->dev = dev;
    skb->protocol = __constant_htons(ETH_P_ARP);

    if (src_hw == NULL)
        src_hw = dev->dev_addr;
    if (dest_hw == NULL)
        dest_hw = dev->broadcast;

    // 下面設置ARP頭部的各個字段信息
    ...
    switch (dev->type) {
    default:
        arp->ar_hrd = htons(dev->type);            // 設置硬件類型
        arp->ar_pro = __constant_htons(ETH_P_IP);  // 設置上層協議類型為IP協議
        break;
    ...
    }

    arp->ar_hln = dev->addr_len;            // 設置硬件地址長度
    arp->ar_pln = 4;                        // 設置上層協議地址長度
    arp->ar_op = htons(type);               // ARP請求操作碼類型

    arp_ptr = (unsigned char *)(arp + 1);

    memcpy(arp_ptr, src_hw, dev->addr_len); // 復制源MAC地址(本機MAC地址)

    arp_ptr += dev->addr_len;
    memcpy(arp_ptr, &src_ip,4);             // 復制源IP地址(本機IP地址)

    // 復制目標MAC地址(對於查詢請求設置為0)
    arp_ptr += 4;
    if (target_hw != NULL)
        memcpy(arp_ptr, target_hw, dev->addr_len);
    else
        memset(arp_ptr, 0, dev->addr_len);

    // 復制目標IP地址
    arp_ptr += dev->addr_len;
    memcpy(arp_ptr, &dest_ip, 4);
    ...
    dev_queue_xmit(skb); // 把數據發送出去
    return;
}

arp_send() 函數的工作也比較清晰:

  • 首先調用 alloc_skb() 函數申請一個數據包對象。
  • 然后設置 ARP 頭部各個字段的信息。
  • 調用 dev_queue_xmit() 函數把數據發送出去。

處理 ARP 回復

當鄰居節點回復 MAC 地址查詢 ARP 請求時,本機需要處理此 ARP 回復。本機通過 arp_rcv() 函數來處理 ARP 回復,代碼如下:

int arp_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt)
{
    struct arphdr *arp = skb->nh.arph;
    unsigned char *arp_ptr = (unsigned char *)(arp+1);
    struct rtable *rt;
    unsigned char *sha, *tha;
    u32 sip, tip;
    u16 dev_type = dev->type;
    int addr_type;
    struct in_device *in_dev = in_dev_get(dev);
    struct neighbour *n;
    ...
    // 從ARP回復中獲取數據
    sha = arp_ptr;             // 源MAC地址(鄰居節點的MAC地址)
    arp_ptr += dev->addr_len;
    memcpy(&sip, arp_ptr, 4);  // 源IP地址(鄰居節點的IP地址)

    arp_ptr += 4;
    tha = arp_ptr;             // 目標MAC地址(本機的MAC地址)

    arp_ptr += dev->addr_len;
    memcpy(&tip, arp_ptr, 4);  // 目標IP地址(本機的IP地址)
    ...

    n = __neigh_lookup(&arp_tbl, &sip, dev, 0); // 查找鄰居節點信息對象
    if (n) {
        int state = NUD_REACHABLE;
        int override = 0;

        if (jiffies - n->updated >= n->parms->locktime)
            override = 1;

        if (arp->ar_op != __constant_htons(ARPOP_REPLY) 
            || skb->pkt_type != PACKET_HOST)
            state = NUD_STALE;

        neigh_update(n, sha, state, override, 1); // 更新鄰居節點信息對象
        neigh_release(n);
    }
    ...
    return 0;
}

arp_rcv() 函數主要完成以下工作:

  • 通過從 ARP 回復中獲取到鄰居節點的 MAC 地址和 IP 地址。
  • 通過鄰居節點的 IP 地址和輸入設備來查找對應的鄰居節點信息對象。
  • 如果鄰居節點信息對象已經存在,那么調用 neigh_update() 函數更新鄰居節點信息對象的數據。

我們來看看 neigh_update() 函數怎么更新鄰居節點信息對象的數據:

int neigh_update(struct neighbour *neigh, const u8 *lladdr, u8 new, int override, int arp)
{
    u8 old;
    int err;
    int notify = 0;
    struct net_device *dev = neigh->dev;
    ...
    old = neigh->nud_state;    // 更新前鄰居節點信息對象的狀態
    ...
    neigh->nud_state = new;    // 更新鄰居節點信息對象的狀態
    if (lladdr != neigh->ha) { // 更新鄰居節點信息對象的MAC地址
        memcpy(&neigh->ha, lladdr, dev->addr_len);
        ...
    }
    ...
    if (!(old&NUD_VALID)) {
        struct sk_buff *skb;

        // 如果 arp_queue 隊列中有等待發送的數據包, 現在可以把這些數據包發送出去
        while (neigh->nud_state & NUD_VALID &&
               (skb = __skb_dequeue(&neigh->arp_queue)) != NULL) 
        {
            struct neighbour *n1 = neigh;
            write_unlock_bh(&neigh->lock);
            ...
            n1->output(skb);
            write_lock_bh(&neigh->lock);
        }
        skb_queue_purge(&neigh->arp_queue);
    }
    ...
    return err;
}

neigh_update() 函數主要完成以下工作:

  • 更新鄰居節點信息對象的 ha 字段(也就是 MAC 地址)為 ARP 回復中獲得的鄰居節點 MAC 地址。
  • 如果鄰居節點對象的 arp_queue 隊列不為空,說明有等待發送的數據包,此時調用鄰居節點信息的 output() 接口把這些數據發送出去。從上下文可知,此時的 output() 還是為 neigh_resolve_output() 函數。但由於鄰居節點的 MAC 地址已經獲得,所以不會再發送 ARP 請求。而是調用設備的 hard_header() 接口設置數據包的目標 MAC 地址,然后 調用 dev_queue_xmit() 函數把數據包發送出去。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM