前言:
PPP(Point to Point Protocol)协议是一种广泛使用的数据链路层协议,在国内广泛使用的宽带拨号协议PPPoE其基础就是PPP协议,此外和PPP相关的协议PPTP,L2TP也常应用于VPN虚拟专用网络。随着智能手机系统Android的兴起,PPP协议还被应用于GPRS拨号,3G/4G数据通路的建立,在嵌入式通信设备及智能手机中有着广泛的应用基础。本文主要分析Linux中PPP协议实现的关键代码和基本数据收发流程,对PPP协议的详细介绍请自行参考RFC和相关协议资料。
模块组成:
上图为PPP模块组成示意图,包括:
PPPD:PPP用户态应用程序。
PPP驱动:PPP在内核中的驱动部分,kernel源码在/drivers/net/下的ppp_generic.c, slhc.c。
PPP线路规程*:PPP TTY线路规程,kernel源码在/drivers/net/下的ppp_async.c, ppp_synctty.c,本文只考虑异步PPP。
TTY核心:TTY驱动,线路规程的通用框架层。
TTY驱动:串口TTY驱动,和具体硬件相关,本文不讨论。
说明:本文引用的pppd源码来自于android 2.3源码包,kernel源码版本为linux-2.6.18。
Linux中PPP实现主要分成两大部分:PPPD和PPPK。PPPD是用户态应用程序,负责PPP协议的具体配置,如MTU、拨号模式、认证方式、认证所需用户名/密码等。 PPPK指的是PPP内核部分,包括上图中的PPP驱动和PPP线路规程。PPPD通过PPP驱动提供的设备文件接口/dev/ppp来对PPPK进行管理控制,将用户需要的配置策略通过PPPK进行有效地实现,并且PPPD还会负责PPP协议从LCP到PAP/CHAP认证再到IPCP三个阶段协议建立和状态机的维护。因此,从Linux的设计思想来看,PPPD是策略而PPPK是机制;从数据收发流程看,所有控制帧(LCP,PAP/CHAP/EAP,IPCP/IPXCP等)都通过PPPD进行收发协商,而链路建立成功后的数据报文直接通过PPPK进行转发,如果把Linux当做通信平台,PPPD就是Control
Plane而PPPK是DataPlane。
在Linux中PPPD和PPPK联系非常紧密,虽然理论上也可以有其他的应用层程序调用PPPK提供的接口来实现PPP协议栈,但目前使用最广泛的还是PPPD。PPPD的源码比较复杂,支持众多类UNIX平台,里面包含TTY驱动,字符驱动,以太网驱动这三类主要驱动,以及混杂了TTY,PTY,Ethernet等各类接口,导致代码量大且难于理解,下文我们就抽丝剥茧将PPPD中的主干代码剥离出来,遇到某些重要的系统调用,我会详细分析其在Linux内核中的具体实现。
源码分析:
PPPD的主函数main:
第一阶段:
pppd/main.c -> main(): |
…… new_phase(PHASE_INITIALIZE); //PPPD中的状态机,目前是初始化阶段 /* * Initialize magic number generator now so that protocols may * use magic numbers in initialization. */ magic_init();
/* * Initialize each protocol. */ for(i=0;(protp=protocols[i])!= (*protp->init)(0); //初始化协议数组中所有协议
/* * Initialize the default channel. */ tty_init(); //channel初始化,默认就是全局的tty_channel,里面包括很多TTY函数指针 if(!options_from_file(_PATH_SYSOPTIONS,!privileged,0,1)//解析/etc/ppp/options中的参数 ||!options_from_user() ||!parse_args(argc-1,argv+1)) //解析PPPD命令行参数 exit(EXIT_OPTION_ERROR); devnam_fixed=1; /*
/* * Work out the device name, if it hasn't already been specified, * and parse the tty's options file. */ if(the_channel->process_extra_options) (*the_channel->process_extra_options)(); //实际上是调用tty_process_extra_options解析TTY if(!ppp_available()){ //检测/dev/ppp设备文件是否有效 option_error("%s",no_ppp_msg); exit(EXIT_NO_KERNEL_SUPPORT); } /* * Check that the options given are valid and consistent. */ check_options(); //检查选项参数 if(!sys_check_options()) //检测系统参数,比如内核是否支持Multilink等 exit(EXIT_OPTION_ERROR); auth_check_options(); //检查认证相关的参数 #ifdef HAVE_MULTILINK mp_check_options(); #endif for(i=0;(protp=protocols[i])!= if(protp->check_options!= (*protp->check_options)(); //检查每个控制协议的参数配置 if(the_channel->check_options) (*the_channel->check_options)(); //实际上是调用tty_check_options检测TTY参数
…… /* * Detach ourselves from the terminal, if required, * and identify who is running us. */ if(!nodetach&&!updetach) detach(); //默认放在后台以daemon执行,也可配置/etc/ppp/option中的nodetach参数放在前台执行 …… syslog(LOG_NOTICE,"pppd %s started by %s, script_setenv("PPPLOGNAME",p,0);
if(devnam[0]) script_setenv("DEVICE",devnam,1); slprintf(numbuf,sizeof(numbuf),"%d",getpid()); script_setenv("PPPD_PID",numbuf,1);
setup_signals(); //设置信号处理函数
create_linkpidfile(getpid()); //创建PID文件
waiting=0;
/* * If we're doing dial-on-demand, set up the interface now. */ if(demand){ //以按需拨号方式运行,可配置 /* * Open the loopback channel and set it up to be the ppp interface. */ fd_loop=open_ppp_loopback(); //详见下面分析 set_ifunit(1); //设置IFNAME环境变量为接口名称如ppp0 /* * Configure the interface and mark it up, etc. */ demand_conf(); } (第二阶段)…… |
PPP协议里包括各种控制协议如LCP,PAP,CHAP,IPCP等,这些控制协议都有很多共同的地方,因此PPPD将每个控制协议都用结构protent表示,并放在控制协议数组protocols[]中,一般常用的是LCP,PAP,CHAP,IPCP这四个协议。
/* * PPP Data Link Layer "protocol" table. * One entry per supported protocol. * The last entry must be NULL. */ struct protent*protocols[]={ &lcp_protent, //LCP协议 &pap_protent, //PAP协议 &chap_protent, //CHAP协议 #ifdef CBCP_SUPPORT &cbcp_protent, #endif &ipcp_protent, //IPCP协议,IPv4 #ifdef INET6 &ipv6cp_protent, //IPCP协议,IPv6 #endif &ccp_protent, &ecp_protent, #ifdef IPX_CHANGE &ipxcp_protent, #endif #ifdef AT_CHANGE &atcp_protent, #endif &eap_protent, NULL }; |
每个控制协议由protent结构来表示,此结构包含每个协议处理用到的函数指针:
/* * The following struct gives the addresses of procedures to call * for a particular protocol. */ struct protent{ u_short protocol; /* PPP protocol number */ /* Initialization procedure */ void(*init)__P((int /* Process a received packet */ void(*input)__P((int /* Process a received protocol-reject */ void(*protrej)__P((int /* Lower layer has come up */ void(*lowerup)__P((int /* Lower layer has gone down */ void(*lowerdown)__P((int /* Open the protocol */ void(*open)__P((int /* Close the protocol */ void(*close)__P((int /* Print a packet in readable form */ int (*printpkt)__P((u_char*pkt,int /* Process a received data packet */ void(*datainput)__P((int boolenabled_flag; /* 0 iff protocol is char*name; /* char*data_name; /* option_t*options; /* /* Check requested options, assign defaults */ void(*check_options)__P((void)); //检测和此协议有关的选项参数 /* Configure interface for demand-dial */ int (*demand_conf)__P((int /* Say whether to bring up link for this pkt */ int (*active_pkt)__P((u_char*pkt,int }; |
在main()函数中会调用所有支持的控制协议的初始化函数init(),之后初始化TTY channel,解析配置文件或命令行参数,接着检测内核是否支持PPP驱动:
pppd/sys_linux.c main() -> ppp_avaiable(): |
intppp_available(void) { …… no_ppp_msg= "This system lacks kernel support for PPP. This could be because\n" "the PPP kernel module could not be loaded, or because PPP was not\n" "included in the kernel configuration. If PPP was included as a\n" "module, try `/sbin/modprobe -v ppp'. If that fails, check that\n" "ppp.o exists in /lib/modules/`uname -r`/net.\n" "See README.linux file in the ppp distribution for more details.\n";
/* get the kernel version now, since we are called before sys_init */ uname(&utsname); osmaj=osmin=ospatch=0; sscanf(utsname.release,"%d.%d.%d",&osmaj,&osmin,&ospatch); kernel_version=KVERSION(osmaj,osmin,ospatch);
fd=open("/dev/ppp", if(fd>=0){ new_style_driver=1; //支持PPPK
/* XXX should get from driver */ driver_version=2; driver_modification=4; driver_patch=0; close(fd); return1; } …… } |
函数ppp_available会尝试打开/dev/ppp设备文件来判断PPP驱动是否已加载在内核中,如果此设备文件不能打开则通过uname判断内核版本号来区分当前内核版本是否支持PPP驱动,要是内核版本很老(2.3.x以下),则打开PTY设备文件并设置PPP线路规程。目前常用的内核版本基本上都是2.6以上,绝大多数情况下使用的内核都支持PPP驱动,因此本文不分析使用PTY的old driver部分。
接下来会检查选项的合法性,这些选项可以来自于配置文件/etc/ppp/options,也可以是命令行参数,PPPD里面对选项的处理比较多,这里不一一分析了。
后面是把PPPD以daemon方式执行或保持在前台运行并设置一些环境变量和信号处理函数,最后进入到第一个关键部分,当demand这个变量为1时,表示PPPD以按需拨号方式运行。
什么是按需拨号呢?如果大家用过无线路由器就知道,一般PPPoE拨号配置页面都会有一个“按需拨号”的选项,若没有到外部网络的数据流,PPP链路就不会建立,当检测到有流量访问外部网络时,PPP就开始拨号和ISP的拨号服务器建立连接,拨号成功后才产生计费。反之,如果在一定时间内没有访问外网的流量,PPP就会断开连接,为用户节省流量费用。在宽带网络普及的今天,宽带费用基本上都是包月收费了,对家庭宽带用户此功能意义不大。不过对于3G/4G网络这种按流量收费的数据访问方式,按需拨号功能还是有其用武之地。
PPP的按需拨号功能如何实现的呢?首先调用open_ppp_loopback:
pppd/sys-linux.c main() -> open_ppp_loopback(): |
int open_ppp_loopback(void) { intflags;
looped=1; //设置全局变量looped为1,后面会用到 if(new_style_driver){ /* allocate ourselves a ppp unit */ if(make_ppp_unit()<0) //创建PPP网络接口 die(1); modify_flags(ppp_dev_fd,0, set_kdebugflag(kdebugflag); ppp_fd=-1; returnppp_dev_fd; }
……(下面是old driver,忽略) } |
全局变量new_style_driver,这个变量已经在ppp_avaliable函数里被设置为1了。接下来调用make_ppp_unit打开/dev/ppp设备文件并请求建立一个新的unit。
pppd/sys-linux.c main() -> open_ppp_loopback() -> make_ppp_unit(): |
staticintmake_ppp_unit() { intx,flags;
if(ppp_dev_fd>=0){ //如果已经打开过,先关闭 dbglog("in make_ppp_unit, already had /dev/ppp open?"); close(ppp_dev_fd); } ppp_dev_fd=open("/dev/ppp", if(ppp_dev_fd<0) fatal("Couldn't open /dev/ppp: %m"); flags=fcntl(ppp_dev_fd, if(flags==-1 ||fcntl(ppp_dev_fd, warn("Couldn't set /dev/ppp to nonblock: %m");
ifunit=req_unit; //传入请求的unit x=ioctl(ppp_dev_fd, if(x<0&&req_unit>=0&& warn("Couldn't allocate PPP unit %d as it is already in use",req_unit); ifunit=-1; x=ioctl(ppp_dev_fd, } if(x<0) error("Couldn't create new ppp unit: %m"); returnx; } |
这里的unit可以理解为一个PPP接口,在Linux中通过ifconfig看到的ppp0就是通过ioctl(ppp_dev_fd, PPPIOCNEWUNIT, &ifunit)建立起来的,unit number是可以配置的,不过一般都不用配置,传入-1会自动分配一个未使用的unit number,默认从0开始。这个ioctl调用的是PPPK中注册的ppp_ioctl:
linux-2.6.18/drivers/net/ppp_generic.c main() -> open_ppp_loopback() -> make_ppp_unit() -> ioctl(ppp_dev_fd,PPPIOCNEWUNIT,&ifunit) -> ppp_ioctl(): |
staticintppp_ioctl(struct { struct ppp_file*pf=file->private_data; …… if(pf==0) returnppp_unattached_ioctl(pf,file,cmd,arg); |
TIPS:这里还要解释一下PPPK中channel和unit的关系,一个channel相当于一个物理链路,而unit相当于一个接口。在Multilink PPP中,一个unit可以由多个channel组合而成,也就是说一个PPP接口下面可以有多个物理链路,这里的物理链路不一定是物理接口,也可以是一个物理接口上的多个频段(channel)比如HDLC channel。
PPPK中channel用结构channel表示,unit用结构ppp表示。
linux-2.6.18/drivers/net/ppp_generic.c |
/* * Data structure describing one ppp unit. * A ppp unit corresponds to a ppp network interface device * and represents a multilink bundle. * It can have 0 or more ppp channels connected to it. */ struct ppp{ struct ppp_file file; /* struct file *owner; /* struct list_headchannels; /* int n_channels; /* spinlock_t rlock; /* spinlock_t wlock; /* int mru; /* unsignedint flags; /* unsignedint xstate; /* unsignedint rstate; /* int debug; /* struct slcompress*vj; /* enumNPmode npmode[NUM_NP];/* struct sk_buff *xmit_pending; /* struct compressor*xcomp;/* void *xc_state; /* struct compressor*rcomp; /* void *rc_state; /*
|