PPPoE收發包過程分析

1. ppp0設備

路由器中的LAN/WAN口都是通過以太網(ether)設備來收發包的,而在WAN口進行了PPPoE撥號後,ifconfig會發現多出一個ppp0,這個設備是幹什麼用的呢?
實際上,這個設備是內核的ppp模塊爲方便pppoe等協議收發包用的,有了這個設備,你就可以將路由表改一改,將發往WAN口的數據包從ppp0(而不是eth1)發出去,內核協議棧會按照PPP協議相關的設置將這個包發出去。

1.1 創建ppp0

創建ppp設備是在內核的drivers/net/ppp_generic.c中:

ppp_create_interface(struct net *net, int unit, int *retp);

函數里根據第二個參數unit來決定ppp%d如何賦值。

而相應的,在用戶程序pppd的sys-linux.c中,make_ppp_unit()函數通過ioctl進入內核來調用ppp_create_interface():

... ...
ioctl(ppp_dev_fd, PPPIOCNEWUNIT, &ifunit);
... ...

其中的ppp_dev_fd指的是/dev/ppp設備。
pppd中將eth1(WAN口設備名)賦值給全局變量devnam,在PPPOEInitDevice()函數初始化全局結構體conn,在PPPOEConnectDevice()函數中,如果還沒有session,就進行discovery階段,調用函數discovery(conn),如果有了session,則創建用於session的socket:

... ...
conn->sessionSocket = socket(AF_PPPOX, SOCK_STREAM, PX_PROTO_OE);
... ...

並調用connect(conn->sessionSocket, (struct sockaddr *) &sp, sizeof(struct sockaddr_pppox));
注意,第二個參數不是sockaddr_in類型,而是sockaddr_pppox類型,這個結構中需要包含sid,dev,peeraddr等。
該connect()進而調用內核中爲AF_PPPOX註冊的pppoe_connect()函數,這個函數會將pppox_sk(sk)->pppoe_dev賦值爲要綁定的設備,即eth1。
所以,在發現階段,不需要ppp0也不需要綁定eth1。實際上,在有session id後,才創建的ppp0。

2. 收包

收包時在netif_receive_skb()根據協議類型(0x8863/0x8864)進入pppoe的收包函數:
1. 先看ptype_all是否可處理該skb。
2. 如果skb的設備橋在某個br上,就進入handle_bridge()處理。
3. 看ptype_base[]是否可處理,根據不同的協議找處理函數,如ip_rcv、arp_rcv等。pppoe初始化的時候註冊好了會話階段(協議類型ETH_P_PPP_SES)的處理函數pppoe_rcv(),而發現階段的收包函數爲pppoe_disc_rcv()(協議類型ETH_P_PPP_DISC)。

在內核中pppoe收包模塊主要處理session階段的包,上面說過的discovery包的收包函數pppoe_disc_rcv(),只是爲了檢查PADT包,其他包直接返回success到netif_receive_skb()使數據包繼續走協議棧,而由於discovery包的收包socket是通過PF_PACKET、SOCK_RAW創建創建的,所以數據包直接來到用戶態交給pppd處理。
值得一提的是,在以太網驅動收到數據包之後,會將skb->data跳過MAC頭,再交給netif_receive_skb()。其中剝去14字節MAC頭的函數爲eth_type_trans(),它除了剝去MAC頭,還檢查目的地址是否爲設備的地址,例如如果不是並且沒開啓混雜模式,就將skb->pkt_type設置爲PACKET_OTHERHOST。

收包函數pppoe_rcv()將skb->data後移過pppoe頭部,即struct pppoe_hdr大小,然後調用sk_receive_skb(),這個函數實現如下:

    if (!sock_owned_by_user(sk)) {
        rc = sk_backlog_rcv(sk, skb); //繼續調用sk->sk_backlog_rcv()即pppoe_rcv_core()。
    } else {
        sk_add_backlog(sk, skb); //將數據包放入sk_backlog隊列
    }

一般都會走到pppoe_rcv_core(),進而調用ppp_input(),然後可能繼續走ppp_do_recv()收包,或者直接skb_queue_tail()等待應用層的socket來recv()。
例如,LCP包就是走的skb_queue_tail(),所以pppd中收到的包是沒有MAC頭和pppoe頭的。

pppd中,main() -> get_input()去不停的讀socket上的數據,然後調用相應pppoe類型的回調函數,這些回調函數都註冊在一系列的struct protent結構體中,如LCP包就是全局的lcp_protent,對LCP的處理主要可關注其中的有限狀態機(Finite State Machine),pppd進程和lcp相關的選項在lcp_option_list中列出,如noaccomp和nopcomp都是和lcp相關的。

3. 發包

我所看到的發包方式有如下幾種:
1. 直接通過/dev/ppp字符設備,該設備在ppp_init()函數被創建,並綁定ops方法集ppp_device_fops。在用戶態可以通過這個文件來read/write數據進而接收/發送數據:
ppp_write() -> ppp_channel_push() -> pppoe_xmit().
2. 通過網絡設備ppp0,該網絡設備在ppp_create_interface()中被創建,綁定ops方法爲ppp_netdev_ops,在本地發包或轉發時選擇路由如果選中ppp0就會通過該設備的xmit發包:
ppp_start_xmit() -> ppp_xmit_process() -> ppp_push() -> pppoe_xmit().
3. 通過socket方法如pppoe_sendmsg()等函數來收發包。在創建family爲AF_PPPOX,protocol爲PX_PROTO_OE的socket時,收發數據走這裏。
4. 通過PF_PACKET、SOCK_RAW創建的socket在pppd構造好整個數據包,不經過內核的PPP模塊。在發送discovery階段的數據包時使用這種方法。

pppoe模塊的初始化可參見pppoe_init()函數。

上面提到的pppoe的發包函數pppoe_xmit()只是一個封裝,實際調用了__pppoe_xmit(),其過程爲:
1. 給skb->data加上pppoe頭,即struct pppoe_hdr,併爲其賦值。
2. skb->dev = pppox_sk(sk)->pppoe_dev;
3. dev_hard_header()爲數據包添加hardaddr頭部,例如以太網設備就是將skb->data前移14字節,並填充MAC頭。調用的函數爲操作hw地址信息函數集的dev->header_ops->create方法,如以太網協議的操作函數集是eth_header_ops,其create方法爲eth_header()。也就是說,數據包給eth0/eth1之前,要帶上MAC頭(在最開始講到ppp0是和實際的以太網物理設備綁定過的,因此可以找到實際發包設備)。
4. dev_queue_xmit(skb)交給skb->dev的發包函數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章