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的发包函数。

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