Linux下的 PPPoE (整理,总结)

一、 PPPoE 整体实现框架

图1:整体框架

  • PPPoE 程序
    首先PPPoE完成PPPoE的发现阶段(即相互通知MAC地址),接着这个程序负责发送/接收所有通过ppp网络接口(如ppp0)的数据包。
    在会话阶段,PPPoE从网口和stdin接收数据,向stdout发送数据。(PPPD程序在启动PPPoE程序时,将PPPoE程序的stdin和stdout都重定向到PTY的主设备)
  • PPPD 程序
    与PPPoE 程序配合起来完成拨号上网的协商与维护。
  • /dev/ppp
    创建了ppp设备后,PPP过程的数据包经过协议栈的分类,会被传送到该接口的队列内。PPPD从该接口读取PPP过程的数据包,然后交给相应的协议栈处理。
    对于响应的数据包同样写入该设备,设备内会将数据包交给协议栈然后转发出去。
  • socket
    PPPoE的会话与发现阶段数据包对应的以太网类型分别为0x8863和0x8864,内核中定义了这两种类型的socket。
  • PPP协议栈
    主要负责PPP层的封装、压缩与解压。同时,它还对普通数据包和PPP过程的数据包进行了分流,将普通数据包提交给TCP/IP协议栈,而将PPP过程的数据包交给/dev/ppp设备队列中,等待PPPD去收取处理。
  • PTY设备
    串行设备,PPP内核协议栈与PPPoE应用程序的中转站。因为PPP协议早多运行在串行链路上,所以在Linux内核中PPP协议栈与串行设备结合紧密。
    伪终端的使用是成对出现的,分为 master 和 slaver 。写入主设备的信息,可以从从设备上读出;写入从设备的信息,可以从主设备读出。

二、 PPPoE的实现

  • 客户端
    rp-pppoe
  • PPPoE 背景
    传统的PPP连接是基于点到点的,而在以太网是muti-accesss,即在以太网中的任一节点可以访问其他节点。以太网中的Frame包含着目的节点的MAC地址以找到目的节点。 所以在转换PPP frame之前,两个通信节点必须事先知道对方的MAC地址。
  • PPPoE的两个阶段
    DISCOVERY阶段:以太网中的节点交换通知MAC地址,并建立一个Session ID供后续的包交换使用。处理函数为discovery()
    SESSION阶段:当节点之间知道了相互的MAC地址后,就进入了SESSION阶段。处理函数为session()

三、 PPP的实现

PPP协商过程

图2: PPP协商过程

  • PPP主要由两类协议组成:链路控制协议族(LCP)和网络控制协议族(NCP)
    LCP用于建立、拆除和监控PPP数据链路。
    NCP主要用于协商在该数据链路上所传输的数据包的格式与类型。
    同时,PPP还提供了用于网络安全方面的验证协议族(PAP和CHAP)。
  • PPPD的源代码实现
    在pppd里,每种协议实现都在独立的c文件中,它们通常要实现protent接口(该接口主要用于处理数据包)和fsm_callbacks接口(该接口主要用于状态机的状态切换)。
    数据包的接收是由main.c:get_input统一处理的,然后根据协议类型分发到具体的协议实现上。
    数据包的发送是协议实现者根据需要调用output函数完成的。

四、 疑问 

1. PPPD 程序中都是从/dev/ppp设备文件接收和发送数据,/dev/ppp和tty是如何关联的?

首先介绍一下tty discipline 。终端设备驱动架构一般分为三层。其中tty discipline是用来对接收或者发送的数据进行预处理。

图3:终端设备驱动架构

接下来就是pppd中,/dev/ppp 与tty 关联的代码片段


 
 
  1. /*
  2. * pppd/src/sys-linux.c
  3. */
  4. int tty_establish_ppp (int tty_fd)
  5. {
  6. ......
  7. ......
  8. // 设置线路规程为PPP Discipline
  9. // 同时,tty驱动会回调ppp_ldisc的函数ppp_asynctty_open() 来使tty_fd 与channel_index 建立联系。
  10. ppp_disc = (new_style_driver && sync_serial)? N_SYNC_PPP: N_PPP;
  11. if (ioctl(tty_fd, TIOCSETD, &ppp_disc) < 0) {
  12. if ( ! ok_error (errno) ) {
  13. error( "Couldn't set tty to PPP discipline: %m");
  14. return -1;
  15. }
  16. }
  17. ret_fd = generic_establish_ppp(tty_fd);
  18. ......
  19. ......
  20. }
  21. int generic_establish_ppp (int fd)
  22. {
  23. int x;
  24. if (new_style_driver) {
  25. int flags;
  26. /* Open an instance of /dev/ppp and connect the channel to it */
  27. if (ioctl(fd, PPPIOCGCHAN, &chindex) == -1) {
  28. error( "Couldn't get channel number: %m");
  29. goto err;
  30. }
  31. dbglog( "using channel %d", chindex);
  32. // 打开/dev/ppp设备
  33. fd = open( "/dev/ppp", O_RDWR);
  34. if (fd < 0) {
  35. error( "Couldn't reopen /dev/ppp: %m");
  36. goto err;
  37. }
  38. ( void) fcntl(fd, F_SETFD, FD_CLOEXEC);
  39. // 建立/dev/ppp设备文件与channel_index的联系
  40. // 这时向该设备文件写入数据,PPP内核驱动就会知道要使用到的tty设备
  41. if (ioctl(fd, PPPIOCATTCHAN, &chindex) < 0) {
  42. error( "Couldn't attach to channel %d: %m", chindex);
  43. goto err_close;
  44. }
  45. ......
  46. ......
  47. }
2. pppd 创建网络接口(如ppp0)如何与tty相关联?

pppd在make_ppp_unit函数中调用ioctrl(PPPIOCNEWUNIT)创建一个网络接口(如ppp0),内核中的PPP协议模块在处理PPPIOCNEWUNIT时,调用register_netdev向内核注册ppp的网络接口,该网络接口的传输函数指向ppp_start_xmit。


 
 
  1. /*
  2. * pppd/src/sys-linux.c
  3. */
  4. int generic_establish_ppp (int fd)
  5. {
  6. ......
  7. ......
  8. if (!looped && !multilink) {
  9. /*
  10. * Create a new PPP unit.
  11. */
  12. if (make_ppp_unit() < 0)
  13. goto err_close;
  14. }
  15. if (looped)
  16. modify_flags(ppp_dev_fd, SC_LOOP_TRAFFIC, 0);
  17. if (!multilink) {
  18. add_fd(ppp_dev_fd);
  19. // 该fd = tty_fd
  20. // 通过ifunit 与 tty设备建立连接,当应用程序通过网络接口(比如ppp0)发送数据时
  21. // PPP内核驱动就知道最终数据要写往哪个tty设备
  22. if (ioctl(fd, PPPIOCCONNECT, &ifunit) < 0) {
  23. error( "Couldn't attach to PPP unit %d: %m", ifunit);
  24. goto err_close;
  25. }
  26. }
  27. ......
  28. ......
  29. }
  30. static int make_ppp_unit()
  31. {
  32. int x, flags;
  33. if (ppp_dev_fd >= 0) {
  34. dbglog( "in make_ppp_unit, already had /dev/ppp open?");
  35. close(ppp_dev_fd);
  36. }
  37. // 打开设备文件
  38. ppp_dev_fd = open( "/dev/ppp", O_RDWR);
  39. if (ppp_dev_fd < 0)
  40. fatal( "Couldn't open /dev/ppp: %m");
  41. flags = fcntl(ppp_dev_fd, F_GETFL);
  42. if (flags == -1
  43. || fcntl(ppp_dev_fd, F_SETFL, flags | O_NONBLOCK) == -1)
  44. warn( "Couldn't set /dev/ppp to nonblock: %m");
  45. ifunit = req_unit;
  46. // 创建一个网络接口(如ppp0)
  47. // 调用register_netdev向内核注册ppp的网络接口,该网络接口的传输函数指向ppp_start_xmit。
  48. x = ioctl(ppp_dev_fd, PPPIOCNEWUNIT, &ifunit);
  49. if (x < 0 && req_unit >= 0 && errno == EEXIST) {
  50. warn( "Couldn't allocate PPP unit %d as it is already in use", req_unit);
  51. ifunit = -1;
  52. x = ioctl(ppp_dev_fd, PPPIOCNEWUNIT, &ifunit);
  53. }
  54. if (x < 0)
  55. error( "Couldn't create new ppp unit: %m");
  56. return x;
  57. }
3. 普通应用程序通过socket接口发送TCP/IP数据包,这些TCP/IP数据包如何流经PPP协议处理模块,然后通过串口发送出去呢?

当应用程序发送数据时,内核根据IP地址和路由表,找到ppp网络接口,然后调用ppp_start_xmit函数,此时控制就转移到PPP协议处理模块了。ppp_start_xmit调用函数ppp_xmit_process去发送队列中的所有数据包,ppp_xmit_process又调用ppp_send_frame去发送单个数据包,ppp_send_frame根据设置,调用压缩等扩展处理之后,又经ppp_push调用pch->chan->ops->start_xmit发送数据包。

pch->chan->ops->start_xmit是什么?它就是具体的传输方式了,比如说对于串口发送方式,则是ppp_async.c: ppp_asynctty_open中注册的ppp_async_send函数,ppp_async_send经ppp_async_push函数调用tty->ops->write把数据发送串口。

4. 普通应用程序接收数据的情形又是如何的?

ppp_async.c在初始化时(ppp_async_init),调用tty_register_ldisc向tty注册了行规程处理接口,也就是一组回调函数,当串口tty收到数据时,它就会回调ppp_ldisc的ppp_asynctty_receive函数接收数据。ppp_asynctty_receive调用ppp_async_input把数据buffer转换成sk_buff,并放入接收队列ap->rqueue中。


ppp_async另外有一个tasklet(ppp_async_process)专门处理接收队列ap->rqueue中的数据包,ppp_async_process一直挂在接收队列ap->rqueue上,一旦被唤醒,它就调用ppp_input函数让PPP协议处理模块处理该数据包。

在ppp_input函数中,数据被分成两路,一路是控制协议数据包,放入pch->file.rqb队列,交给pppd处理。另外一路是用户数据包,经ppp_do_recv/ppp_receive_frame进行PPP处理之后,再由netif_rx提交给上层协议处理,最后经socket传递到应用程序。

五、 附录

1. 内核源代码

kernel/drivers/net/ppp/ppp_async.c

kernel/drivers/net/ppp/ppp_generic.c

kernel/drivers/tty/tty_io.c

kernel/drivers/tty/tty_ldisc.c

kernel/drivers/tty/tty_ioctl.c

2. 参考

https://zh.wikipedia.org/wiki/PPPoE

http://en.wikipedia.org/wiki/Point-to-point_protocol

http://en.wikipedia.org/wiki/Line_discipline

http://blog.csdn.net/absurd/article/details/1596496

http://blog.csdn.net/xuxinyl/article/details/6536281

http://blog.csdn.net/moazhen/article/details/1096849

http://www.linusakesson.net/programming/tty/


 

 

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值