简介
正式介绍前,分析两个名词:ppp、pppd,在调试中,这两个名字经常出现,ppp(Point to PointProtocol)点对点协议,pppd(Point to Point Protocol daemon)点对点协议守护进程。ppp 在内核中, 是ppp协议处理模块,pppd 是一个在应用层中的守护进程,其功能为实现ppp策略性的内容,包括所有鉴权、压缩/解压和加密/解密等扩展功能的控制协议。
这个文档先介绍ppp的原理及组成,后续文档再详细分析ppp协议处理模块和pppd部分。
点对点协议(PPP)为在点对点连接上传输多协议数据包提供了一个标准方法。ppp 位于数据链路层,是一种为同等单元之间传输数据包这样的简单链路设计的链路层协议。这种链路提供全双工操作,并按照顺序传递数据包。
PPP 最初设计是为两个对等节点之间的 IP 流量传输提供一种封装协议。在 TCP-IP 协议集中它是一种用来同步调制连接的数据链路层协议(OSI 模式中的第二层),替代了原来非标准的第二层协议,即 SLIP。除了 IP 以外 PPP 还可以携带其它协议,包括 DECnet 和 Novell 的Internet 网包交换(IPX)。。设计目的主要是用来通 过拨号或专线方式建立点对点连接发送数据,使其成为各种主机、网桥和路由器之间简单连接的一种共通的解决方案。
功能及组成
PPP(Point-to-Point)提供了一种标准的方法在点对点的连接上传输多种协议数据包,pppd、chat、tty、socket、ccp、chap、pap、eap、ecp、ipcp和很多其它概念在一起使用, PPP协议提供两个实体之间的数据链路连接的建立、维持和释放,负责流量和差错控制等等功能。
PPP协议之下是以太网和串口等物理层,之上是IP协议等网络层。发送时,TCP/IP数据包经过PPP打包之后经过串口发送。接收时,从串口上来的数据经PPP解包之后上报给TCP/IP协议层。网络协议是分层实现的,上层一般只需要知道其直接下层,只有在极少数据情况才使用间接下层的接口。比如,彩信、浏览器和邮件等应用程序使用socket接口编程,它们只需要知道TCP/IP协议,而无需要知道PPP协议的存在。这种分层设计简化了协议的实现和应用程序的开发。PPP协议不只是提供了简单的数据链路层功能,它还提供了诸如鉴权(如PAP/CHAP),数据压缩/解压(如CCP)和数据加密/解密(如ECP)等扩展功能。应用程序要求使用透明化,不关心这些扩展功能的存在,而反过来,PPP协议处理模块本身又无法处理这些策略性的东西,因为它不知道用户名/密码,不知道是否要进行压缩,不知道是否要进行加密。 怎么办?如何在对应用程序透明的情况下使用扩展功能呢?于是pppd就出现了。
pppd是一个后台服务进程(daemon),是一个用户空间的进程,所以把策略性的内容从内核的PPP协议处理模块移到pppd中是很自然的事了。pppd实现了所有鉴权、压缩/解压和加密/解密等扩展功能的控制协议。pppd只是一个普通的用户进程,它如何扩展PPP协议呢?这就是pppd与内核中的PPP协议处理模块之间约定了,它们之间采用了最传统的内核空间与用户空间之间通信方式:设备文件。设备文件名是/dev/ppp。通过read系统调用,pppd可以读取PPP协议处理模块的数据包,当然,PPP协议处理模块只会把应该由pppd处理的数据包发给pppd。通过write系统调用,pppd可以把要发送的数据包传递给PPP协议处理模块。通过ioctrl系统调用,pppd可以设置PPP协议的参数,可以建立/关闭连接。 在pppd里,每种协议实现都在独立的C文件中,它们通常要实现protent接口,该接口主要用于处理数据包,和fsm_callbacks接口,该接口主要用于状态机的状态切换。数据包的接收是由main.c: get_input统一处理的,然后根据协议类型分发到具体的协议实现上。而数据包的发送则是协议实现者根据需要调用output函数完成的。chat是pppd所带一个辅助工具。它和xchat不是一个类型的,xchat用来与人聊天,而chat用来与GSM模组建立会话。它的实现比较简单,它向串口发送AT命令,建立与GSM模组的会话,以便让PPP协议可以在串口上传输数据包。
pppd代码分析
pppd的代码从main.c开始,入口函数为main(),下面从main函数的几个关键点讲一下pppd的代码。
for (i = 0; (protp = protocols[i]) != NULL; ++i)
(*protp->init)(0);
这个循环从结构数组protocols中读出初化函数并执行,数组定义如下,目前ifdef中的部分没有支持。
struct protent *protocols[] = {
&lcp_protent,
&pap_protent,
&chap_protent,
#ifdef CBCP_SUPPORT
&cbcp_protent,
#endif
&ipcp_protent,
#ifdef INET6
&ipv6cp_protent,
#endif
&ccp_protent,
&ecp_protent,
#ifdef IPX_CHANGE
&ipxcp_protent,
#endif
#ifdef AT_CHANGE
&atcp_protent,
#endif
&eap_protent,
NULL
};
数组protocols中的结构体定义如下,其定义了pppd支持的几种协议的基本操作、协议数据、操作选项列表原型
struct protent {
u_short protocol; /*PPP protocol number */
/* Initialization procedure */
void (*init) __P((int unit));
/* Process a received packet */
void (*input) __P((int unit, u_char *pkt, int len));
/* Process a received protocol-reject */
void (*protrej) __P((int unit));
/* Lower layer has come up */
void (*lowerup) __P((int unit));
/* Lower layer has gone down */
void (*lowerdown) __P((int unit));
/* Open the protocol */
void (*open) __P((int unit));
/* Close the protocol */
void (*close) __P((int unit, char *reason));
/* Print a packet in readable form */
int (*printpkt) __P((u_char *pkt,int len,
void (*printer) __P((void *, char *, ...)),
void *arg));
/* Process a received data packet */
void (*datainput) __P((int unit, u_char *pkt, int len));
bool enabled_flag; /* 0iff protocol is disabled */
char *name; /*Text name of protocol */
char *data_name; /* Textname of corresponding data protocol */
option_t *options; /*List of command-line options */
/* Check requested options, assign defaults */
void (*check_options) __P((void));
/* Configure interface for demand-dial */
int (*demand_conf) __P((intunit));
/* Say whether to bring up link for this pkt */
int (*active_pkt) __P((u_char*pkt, int len));
};
以PAP鉴权协议为例,下面是PAP的protent结构体。结构体中定义了几个操作函数,协议名,以及pap_option_list(PAP协议操作选项)
struct protent pap_protent = {
PPP_PAP,
upap_init,
upap_input,
upap_protrej,
upap_lowerup,
upap_lowerdown,
NULL,
NULL,
upap_printpkt,
NULL,
1,
"PAP",
NULL,
pap_option_list,
NULL,
NULL,
NULL
};
option_t 记录了pppd的参数,启动pppd时传入相应该的参数既可完成对应的功能,option_t的原型如下:
typedef struct {
char *name; /*name of the option */
enumopt_type type;
void *addr;
char *description;
unsignedint flags;
void *addr2;
int upper_limit;
int lower_limit;
constchar *source;
shortint priority;
shortint winner;
} option_t;
下面便是pap的协议操作选项,有5个选项,通过设置启动参数来使用。
static option_t pap_option_list[] = {
{"hide-password", o_bool, &hide_password,
"Don't output passwords to log", OPT_PRIO | 1 },
{"show-password", o_bool, &hide_password,
"Show password string in debug log messages", OPT_PRIOSUB | 0},
{"pap-restart", o_int, &upap[0].us_timeouttime,
"Set retransmit timeout for PAP", OPT_PRIO },
{"pap-max-authreq", o_int, &upap[0].us_maxtransmits,
"Set max number of transmissions for auth-reqs", OPT_PRIO },
{"pap-timeout", o_int, &upap[0].us_reqtimeout,
"Set time limit for peer PAP authentication", OPT_PRIO },
{NULL }
};
Ppp 是通过串口来通信的,所以设置通信通道是ppp很重要的一部分,这部分的代码在tty.c中实现,主要通过下面这个结构中的几个函数来实现。
/*
*This struct contains pointers to a set of procedures for
*doing operations on a "channel". A channel provides a way
* tosend and receive PPP packets - the canonical example is
* aserial port device in PPP line discipline (or equivalently
*with PPP STREAMS modules pushed onto it).
*/
struct channel {
/*set of options for this channel */
option_t*options;
/*find and process a per-channel options file */
void(*process_extra_options) __P((void));
/*check all the options that have been given */
void(*check_options) __P((void));
/*get the channel ready to do PPP, return a file descriptor */
int (*connect) __P((void));
/*we're finished with the channel */
void(*disconnect) __P((void));
/*put the channel into PPP `mode' */
int (*establish_ppp) __P((int));
/*take the channel out of PPP `mode', restore loopback if demand */
void(*disestablish_ppp) __P((int));
/*set the transmit-side PPP parameters of the channel */
void(*send_config) __P((int, u_int32_t, int, int));
/*set the receive-side PPP parameters of the channel */
void(*recv_config) __P((int, u_int32_t, int, int));
/*cleanup on error or normal exit */
void(*cleanup) __P((void));
/*close the device, called in children after fork */
void(*close) __P((void));
};
状态机的回调函数,具体会在每个协议处理模块内实现,状态机在处理ppp连接建立过程时通过状态机调用具体的协议处理模块,完成协议处理过程。
typedef struct fsm_callbacks {
void (*resetci) /* Resetour Configuration Information */
__P((fsm*));
int (*cilen) /* Length of our ConfigurationInformation */
__P((fsm*));
void (*addci) /* Add ourConfiguration Information */
__P((fsm*, u_char *, int *));
int (*ackci) /* ACK our ConfigurationInformation */
__P((fsm*, u_char *, int));
int (*nakci) /* NAK our ConfigurationInformation */
__P((fsm*, u_char *, int, int));
int (*rejci) /* Reject our ConfigurationInformation */
__P((fsm*, u_char *, int));
int (*reqci) /* Request peer's ConfigurationInformation */
__P((fsm*, u_char *, int *, int));
void (*up) /*Called when fsm reaches OPENED state */
__P((fsm*));
void (*down) /* Calledwhen fsm leaves OPENED state */
__P((fsm*));
void (*starting) /*Called when we want the lower layer */
__P((fsm*));
void (*finished) /*Called when we don't want the lower layer */
__P((fsm*));
void (*protreject) /*Called when Protocol-Reject received */
__P((int));
void (*retransmit) /*Retransmission is necessary */
__P((fsm*));
int (*extcode) /*Called when unknown code received */
__P((fsm*, int, int, u_char *, int));
char *proto_name; /*String name for protocol (for messages) */
} fsm_callbacks;
整个程序的主体实现是从主函数的LCP_OPEN()开始的,在这个函数里,调用了有限状态机FSM_OPEN(),而在FSM_OPEN()中,callback指针指向了starting,于是就到了LCP_STARTING()函数来实现一个OPEN事件从而使得PPP状态准备从DEAD到ESTABLISHED的转变。接下来,回到主函数,下面一步是调用START_LINK(),在此函数中会把一个串口设备作为PPP的接口,并把状态转变为ESTABLISHED,然后调用lcp_lowerup()来告诉上层底层已经UP,lcp_lowerup()中调用FSM_LOWERUP()来发送一个configure-request请求,再把当前状态设置为REQSENT状态,至此,第一个LCP协商的报文已经发送出去。
接下来的流程实现主要就是在这个while循环中实现了。之前说过了我们已经发送了第一个配置协商报文,所以handle_events()主要就是做等待接收数据包的时间处理了,在handle_events()里主要调用了两个函数一个是wait_input(),他的任务是等待并判断是否超时。还有一个是calltimeout()他主要是做超时的处理。当等待并未超时而且有数据包过来,则调用整个PPPD中最重要的函数get_input()函数。他主要接收过来的数据包并做出相应的动作。接下来就get_input()函数进行详细的说明,首先对包进行判断,丢弃所有不在LCP阶段和没有OPENED状态的包,然后protop指针指向当前协议的input函数。于是就进入了LCP_INPUT(),同理LCP_INPUT()调用了FSM_INPUT()对收到的包进行代码域的判断,判断收到的是什么包。假设比较顺利,我们收到的是CONFACK的包,于是调用fsm_rconack()函数,在此函数中根据当前自身的状态来决定下一步的状态如何改变,这里我们假设也很顺利,已经发送完了configure-ack,因此我们把FSM当前状态变成了OPENED状态,并把callback指针指向UP.所以我们马上就调用LCP_UP()在那里我们又调用了link_established()函数来进入认证的协商,或者如果没有认证则直接进入网络层协议。当然这里我们还是要认证的所有在LINK_ESTABLISHED()里我们选择是利用何种认证方式是PAP,还是EAP,还是CHAP.假设我们这里采用CHAP而且是选择CHAP WITH PEER,意思是等待对端先发送CHALLENGE挑战报文。于是我们又调用了chap_auth_peer()函数,并等待接收挑战报文。于是从新又来到handle_events()等待接收。再利用get_input()来接收包,在get_input()里这次调用chap_input(),再调用FSM_INPUT(),在那里我们再对包的代码域进行判断,这次判断出是CHAP_CHALLENGE包,则我们要调用chap_respond()函数来回应对端,继续等待对方的报文,再次利用CHAP_INPUT(),FSM_INPUT()来判断,如果是SUCCESS,则调用chap_handle_status(),在这个函数里调用auth_withpeer_success函数,从而进入网络层阶段,调用network_phase()函数。网络层的互动是从start_networks()开始的,如果在网络层阶段同时有CCP协议(压缩控制协议)则进行压缩控制协议的协商,然后再进入正式的IPCP的协商,而IPCP的协商主要也是通过protop指针指向IPCP_OPEN()开始的。而IPCP_OPEN()则是调用了FSM_OPEN(),在这里,首先发送一个configure-request包,然后和之前一样等待接收。经过几个交互后最后调用NP_UP()完成网络层的协商,至此PPP链路可以承载网络层的数据包了。
pppd程序接受数据过程
Example: get_input()
read_packet (unsigned char *buf) //get a PPP packet from the serial device
read(ppp_fd, buf, len);
(*protp->input)(0, p, len);-------》
lcp_input(unit, p, len)
fsm_input(f, inpacket, l)
pppd程序发送数据过程
Example: start_link(unit)
lcp_lowerup(0);
fsm_lowerup(f)
fsm_sdata(f, code, id, data, datalen)
output (int unit, unsigned char *p, int len)
write(fd, p, len)
get_input()
lcp_input(unit, p, len)
fsm_input(f, inpacket, l)
fsm_rconfreq(f, id, inp, len);
fsm_sdata(f, code, id, data, datalen)
output (int unit, unsigned char *p, int len)
write(fd, p, len)