ppp 完全理解(一)
ppp 协议简介、功能及组成分析
作者:李圳均
日期:2013/11/27
简介:
正式介绍前,分析两个名词: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提供了丰富的参数来设置相应的操作,下面用一个pppd启动参数的例子说简单的介绍一下pppd的参数.
下面这一行是在我们的3G平板上使用的pppd启动参数,其内容出自/system/etc/ppp/call-ppd 脚本。
/system/bin/pppd $1 debug defaultroutenoauth nodetach nocrtscts $2 noipdefault usepeerdns user "$3"password "$4" connect "$5" disconnect "$6"
下面是对上面参数在我们3G平板上的具体解释,上面一行就可以看到有6个传入参数,下面这行给出每个参数的具体值,这些值是可变的。这6个参数取出来分别为:/dev/ttyUSB244(串口),-pap(鉴权模式),"test"(用户名),"test"(密码),"/system/bin/chat -v -f/data/connect"(connect 脚本),"/system/bin/chat -v -f /data/disconnect"(disconnect 脚本,目前为空)。
/system/bin/pppd /dev/ttyUSB244 debugdefaultroute noauth nodetach nocrtscts -pap noipdefault usepeerdns user "test"password "test" connect "/system/bin/chat -v -f /data/connect" disconnect " "
脚本/data/connect的内容如下:
TIMEOUT 5
ABORT 'BUSY'
ABORT 'NO CARRIER'
ABORT 'NO ANSWER'
ABORT 'ERROR'
ABORT '+CME ERROR: 100'
"" AT
OK AT+CGDCONT=1,"IP","3gnet"
OK ATD*99#
CONNECT
下面这组pppd启动参数来自华为MU509的RIL代码:
/system/bin/pppd %s 115200 mru 1280 nodetach debug dump %s defaultroute usepeerdns novj user \"%s\" password\"%s\" novjccomp noipdefaultipcp-accept-local ipcp-accept-remote connect-delay 5000 ipcp-max-failure 60ipcp-max-configure 60 -am
从这一段参数可以看出,有4个参数要传入,分别为串口,鉴权模式,用户,密码
-------------------------------------分割线------------------------------------
对于我们的平板在巴西遇到的专有APN连接数据业务有问题的情况,经过分析,问题来自鉴权模式,在使用有公网连接数据业务时,一般是不需要鉴权的,移动数据业务的鉴权是在SIM卡鉴权后就完成了,简单的说,就是卡上有钱,就可以上网,不需要在连接数据业务时再进行鉴权。但专有网络在这部分有别于公网,其在连接数据业务的过程中,会根据具体专有网络有的拥有者的需求设置为需要鉴权或不用,通常都是需要鉴权的,鉴权协议多数据为PAP鉴权。
因早期没有鉴权需求,RK的RIL代码中是没有发送鉴权设置的,当网络端有鉴权请求时,pppd会默认发送chap-MD5的鉴权响应,而另一方面,运营商对条件符合,但鉴权协议不对的鉴权,不会执行具体的鉴权,直接响应鉴权成功,以便一些设置了用户名和密码的公网用户可以正常访问网络。所以,默认的chap-MD5鉴权可以鉴权成功(这部分干扰了我们的早期的判断),但AUC(核心网中的鉴权中心)实际上认定该用户没有进行鉴权,不能连接数据业务。
后期的调试集中在pppd上面,因RK的赵工和我在这之前都没有深入调试过pppd,工作习惯让我们直接进入pppd的代码加调试LOG来找问题,事实证明,这个方法的效果很小,出现了一些很奇怪的问题,打印传入参数时,会出现一部分的传入参数打印不出来,打印结果随机性很高,无法确认问题点。
在仔细看过pppd的参数 后,我发现dump参数可以打印出pppd的传入参数,在启动参数行中加入参数dump后,所有pppd的启动参数都可以正常打印出来,发现传入参数没有问题,查看LCP过程,鉴权还是用的chap-MD5,在启动参数中加入-chap +pap后会出现需要一个pap-sercet脚本的需求,但后华为袭工进行多轮沟通并多次测试后,发现要使用PAP鉴权,只能一个一个的减去在其之前优先支持的鉴权,参数如下:-chap -mschap-v2 -mschap refuse-eap
综合起来,我们最终使用的pppd参数为:
/system/bin/pppd /dev/ttyUSB244 debugdefaultroute dump nodetach nocrtscts-chap -mschap-v2 -mschap refuse-eapnoipdefault usepeerdns user "test" password "test" connect"/system/bin/chat -v -f/data/connect" disconnect " "
在RIL代码中我们加入段处理鉴权模式的代码,处理APN设置的鉴权模式,把APN的鉴权需求转换成相应的pppd启动参数传入pppd,通过pppd完成相应的鉴权过程。
从调试过程来看,这个问题的解决过程中,最主要的问题是沟通,花了大量的时间进行无效沟通,有效的沟通是从11月12晚上开始的,但当晚因为是在家里和巴西的工程师联系上的,没有有效的调试环境,调试上没有实际的进展,但沟通渠道OK,为后续有效调试奠定了基础,后面几天来看,13号和14晚上的沟通和调试查明了问题,从15号到18号,巴西那边休息3天,调试没有进行,之后两天巴西那边不能进入警局测试,20号晚上,我们已确认问题原因并基本解决,19号在巴西圣保罗的警局测试通过,可以正常连接数据业务。整个专有APN不能连接数据业务的问题解决。
具体的ppp协议,LCP协议,CHAP和PAP鉴权协议,IPCP协议,pppd代码分析和ppp协议处理模块代码分析在文档二和文档三中介绍。
ppp 完全理解(二)
pppd 协议及代码分析
作者:李圳均
日期:2013/11/27
通过前文所述,我们可以知道,pppd是一个后台服务进程(daemon),是一个用户空间的进程,其实现了ppp策略性的内容,包括所有鉴权、压缩/解压和加密/解密等扩展功能的控制协议。
PPP提供了一种通过串行点对点连接传输数据包的方法。它主要包括四个部分:
· 在串行链路上封装IP数据报的方法
· 链路控制协议 LCP(Link Control Protocol);
· 网络控制协议 NCP(Network Control Protocol);
· 认证协议:口 令验证协议PAP(Password Authentication Protocol)和挑战握手验证协议CHAP(Challenge-Handshake Authentication Protocol)。
注:在一般的PPP描述中会把PPP会为三个部分,有些是上面分类的前三个,其依据是认证协议在一定程度上可分到LCP中;而另一些则是后面三个,其依据是PPP在串行链路上封装IP数据报的方法是ppp协议处理模块的内容,其分类以ppp控制协议为标准。本文档将主要介绍LCP,NCP及认证协议相关内容,有关在串行链路上封装IP数据报的方法,将在下一个文档中详细介绍。
一:ppp协议
1:ppp数据帧格式
PPP 协议是在高级数据链路控制协议(HDLC)的基础上设计的,因此,其基本格式与 HDLC 相同,只是地址域和控制域已经被固定,PPP 数据帧格式如下表:
• 标志域
标志域为一个字节,表示数据帧的开始或结束。标志序列为 16 进制 7E。它是数据帧间隔,两个数据帧之间只要一个标志即可,两个连续的标志表示一个空数据帧。
• 地址域
地址域为一个内容为 16 进制 FF 的字节,该地址为广播地址,所有的终端都必须接受并使用该地址,使用其它地址的数据帧应该被忽略。控制域控制域为一个字节,内容为 16 进制 0x03,对于包含其它内容的数据帧应该忽略。
• 协议域
协议域为两个字节,它表示 PPP 封装的信息域内的信息使用的协议。下面给出常用的几种协议编号,详细信息可以参考文献[26]:(协议编号为 16 进制形式)
0021: IP 8021:IP 控制协议
C021: 连接控制协议 C023:密码鉴权协议 PAP
C223: 握手挑战鉴权协议
完全内容:
0001 Padding Protocol
0003 to 001f reserved (transparency inefficient)
0021 Internet Protocol
0023 OSI Network Layer
0025 Xerox NS IDP
0027 DECnet Phase IV
0029 Appletalk
002b Novell IPX
002d Van Jacobson Compressed TCP/IP 002f Van Jacobson Uncompressed TCP/IP
0031 Bridging PDU
0033 Stream Protocol (ST-II)
0035 Banyan Vines
0037 unused
0039 AppleTalk EDDP
003b AppleTalk SmartBuffered
003d Multi-Link
005d reserved (compression inefficient) 00cf reserved (PPP NLPID)
00fd 1st choice compression
00ff reserved (compression inefficient) 0201 802.1d Hello Packets
0203 IBM Source Routing BPDU
0231 Luxcom
0233 Sigma Network Systems
8021 Internet Protocol Control Protocol 8023 OSI Network Layer Control Protocol
8025 Xerox NS IDP Control Protocol 8027 DECnet Phase IV Control Protocol
8029 Appletalk Control Protocol
802b Novell IPX Control Protocol
802d Reserved
802f Reserved
8031 Bridging NCP
8033 Stream Protocol Control Protocol 8035 Banyan Vines Control Protocol
8037 unused
8039 Reserved
803b Reserved
803d Multi-Link Control Protocol
80fd Compression Control Protocol
80ff Reserved
c021 Link Control Protocol
c023 Password Authentication Protocol
c025 Link Quality Report
c223 Challenge Handshake Authentication Protocol
• 信息域
信息域的长度是可变的,其中的内容是按协议域指定的协议封装的用户数据,信息域的最大长度默认是 1500 字节,在经过双方协商后,可以更改为其它值。
• 校验域
校验域(FrameCheck Sequence, FCS)为 2 个字节,它计算的是在没有插入任何转义符号前的地址域、控制域、协议域、信息域内的数据,不包括标志域和校验域。在发送数据时,依次计算上述内容,然后将计算后的结果放入校验域;在接收时,首先去除转义字符,然后再计算校验。在接收中计算校验时可以将校验域也计算在内,计算的结果应该是固定值F0B8(16 进制),有关该算法的具体实现,见文献[26]。
• 空闲
在链路空闲时间中,异步链路可以发送全 1 表示链路空闲,同步链路应该发送标志序列表示空闲。由于 PPP 采用起始结束标志来表示数据帧的起始和结束,而在 PPP 的信息字段和 FCS 字段都可能包含 7E,这样可能会被误认为是开始/结束标志,所以 PPP 中引入了一个转义序列。转义序列包括一个转义字符 7D,后面是原来的值与 0x20 异或的结果,即7E 转义为7D 5E。而发送 7D 时则转义为 7D 5D。同样,这种转义方式还保护控制字符,比如 XOFF 是底层驱动用来中断串行传输的,为了避免引起混淆,将 0x14 用转义序列7D 34发送。默认的,0x00 到 0x1F 之间的所有值都要转义,不过,经过协商后可以去除部分需要转义的值。
2:拨号建立连接的过程
拨号建立连接的过程就是 PPP 操作的过程。为了在一个点对点链路上建立通信,通信双方必须发送链路控制协议数据包来配置和测试链路。当链路建立后,通信的一方可能需要进行鉴权,然后使用 NCP 数据包来选择和配置网络层使用的协议。当这些过程完成后,通信链路建立完毕,通信双方就可以开始发送数据了。这个链路将一直存在直到通信的一方发送 LCP、NCP 数据包关闭链路或发生其它意外事故。在本节我们将介绍 PPP 操作过程中的各个阶段,其中涉及到的协议将在下文继续说明。在建立、维持和终止PPP 连接的过程中,经历了若干个阶段,如下面两图所示
1. 链路死亡阶段(物理层不存在)
连接通常开始和结束于这个阶段。在链路死亡阶段,通信链路不存在。当有外部操作要启动连接时,PPP 开始进入建立阶段并向 LCP 协议的状态机发送 UP 事件。
2.链路建立阶段
链路建立阶段使用 LCP协议协商通信过程中所需要的配置信息。这些信息包括:通信过程中的数据包的最大长度、异步通信中的控制字符映射、协议和地址控制域压缩、鉴权协议以及链路检测协议等。这些选项是链路建立阶段后必须使用的。如果某些选项没有进行协商则认为使用默认值。值得注意的是,此时协商的内容都是与网络层协议无关的选项,与网络有关的选项要在网络层协议阶段使用特定的协议进行协商。当通信的双方都收到对方的确认信息后,通信链路建立完毕。
3. 鉴权阶段
鉴权阶段不是必须的,但是在很多系统都需要进行鉴权,以验证客户端的身份。需要鉴权的主机在链路建立阶段发送配置鉴权协议的选项。目前,PPP 中支持的鉴权协议包括密码鉴权协议(PAP)和挑战握手协议(CHAP)。如果鉴权通过则进入网络层协商阶段,否则直接进入终止阶段。
4. 网络层协议阶段
当进入网络协议阶段后,首先必须使用特定的网络配置协议进行网络层协议配置,比如如果网络层使用 IP 协议,则使用 IP 控制协议(IPControl Protocol, IPCP)进行网络配置,以获得相关的网络协议信息。当网络协议配置通过后,就可以使用网络协议进行数据传输了,此后 PPP 中携带的数据将是网络协议数据。
5. 终止阶段
PPP可以在任何时候终止连接。终止连接采用 LCP 协议,当链路关闭时,PPP 通知相应的网络层协议采取相应的行动。当通信双方交换终止数据包后,整个终止过程就完成了, PPP 进入链路死亡阶段
3:LCP(链路控制协议)
如上所述,链路控制协议(LCP)用于 PPP 链路的建立、维护和拆除。LCP 数据包是在 PPP 数据包的信息域发送的。LCP 数据帧分为如下三个部分:
• 连接配置包:用于建立和配置一个 PPP 链路连接
• 连接终止包:用于终止链路连接。
• 连接维持包:用于管理和测试链路连接。
整个LCP数据包的内容都在ppp数据包的信息域中,ppp协议字段通过C021这个值标注当前ppp数据报为LCP协议数据。
1:LCP数据包格式
其中代码域用来决定数据包的类型;标志域用来确定发送和请求数据包是否匹配;长度域表示数据包的长度,包括代码域、标志域、长度域和数据域。
2.LCP 数据包类型
根据代码域的不同,LCP 数据包可以分为以下数据帧:
• 配置请求数据帧(Configure-Request):代码域:1
为了打开一个 LCP 连接,必须发送一个配置请求数据包,欲设置的数据在 LCP 的数据域中设置,接收到该数据包后必须应答。配置选项的内容在下文论述。
• 配置确认数据帧(Configure-ACK):代码域:2
如果接收到的配置数据包中的所有配置选项都可以接受,则用配置确认数据帧应答。应答时将配置请求数据包的代码域、标志域和数据域复制到配置确认数据帧中。
• 配置否认数据帧(Configure-NAK):代码域:3
如果在接收到的配置数据帧中有参数无法接受,则用该数据帧应答。将无法接受的选项的内容修改为可以接受的值后按顺序添加到数据域中,如果还有其它选项需要协商,也可以增加到数据域中。
• 配置拒绝数据帧(Configure-Reject):代码域:4
如果接收到的配置请求数据帧中有部分选项无法识别或不允许使用,则用配置拒绝数据帧应答。此时,复制标志域并将请求数据帧中的要拒绝的选项按原来的顺序复制到数据域中。
• 终止请求数据帧/终止确认数据帧(Terminate-Request/Terminate-ACK)
终止请求数据帧:代码域为 5;终止确认数据帧:代码域为6如果通信一方要终止链路连接,则应该发送终止请求数据帧,代码域设为 5,数据域为任何附加信息;接收到终止请求的一方发送终止确认数据帧,此时代码域为 6,标志域和
数据域从接收到的请求数据帧中拷贝。
• 代码拒绝数据帧(Code-Reject):代码域:7
如果接收到的数据帧的代码域为无效代码,则用代码拒绝帧应答,表示该错误无法恢复。接收到代码拒绝数据帧的主机应该报告错误。
• 协议拒绝数据帧(Protocol-Reject):代码域:8
如果在 PPP 封装中接收到一个未知的通信协议,表示对方想要使用一个本机不支持的协议。此时,如果 LCP 已经处于打开状态,则必须发送协议拒绝数据帧来通知对方,信息域中包括拒绝的协议和信息;但如果在其它状态,则直接丢弃数据帧。
• 回应请求数据帧/回应应答数据帧(Echo-Request/Echo-Reply)
Echo-Request:代码域:9 Echo-Reply:代码域:10 LCP 包含 Echo-Request 和 Echo-Reply 代码用于训练双方通信的数据链路层上的循环通信机制。通信一方发送一个 Echo-Request 包,其中代码域为 9,在信息域中插入本地魔数(Magic-Number,关于魔数,见下文)和任何用于测试的数据。接收到Echo-Request 的一方则用 Echo-Reply 来回应,其中代码域为 10,标志域从请求数据帧中复制,然后在信息域中插入本地魔数,并将请求数据包的内容拷贝到应答数据包中。数据帧格式如下表:
• 丢弃请求数据帧(Discard-Request):代码域:11
该数据帧提供了一种在数据链路层上的测试机制,一方发送该数据帧,另一方接收后直接丢弃。
3.LCP 配置选项
LCP 配置选项允许在一个点对点链路上通过协商修订标准特性值,这些选项包括:最大接收单元,异步控制字符映射、链路鉴权协议等。如果一个配置选项没有在配置请求数据包(Configure-Request)中出现,那么该配置选项将使用默认值。配置选项列表的结束由LCP 数据包的长度标识。在协商过程中,除非特别声明,这些配置选项应用在半双工方式,经过协商后的值仅在接收配置请求数据包的方向上有效。配置选项是 LCP 配置请求等数据帧的数据域内的值。配置选项格式如下:
选项类型:1 字节,指示配置选项类别。
选项长度:1 字节,表示该选项的长度,包括类型、长度和数据。
数据:指示该选项的配置内容,它的格式和长度由选项类型决定。
选项类型分别如下:
• 最大接收单元(Maximum-Receive-Unit, MRU)
该选项用来通知对方该实现可以接收的最大数据包长度,如果要将数据包长度设置为较小值,必须保证在链路同步丢失后仍然能够接收 1500 个字节的数据包。
• 异步控制字符映射(Asynchronous-Control-Character-Map, ACCM)
这个配置选项提供了一个在异步链路上协商控制字符映射表的方法。默认的,PPP 将所有的控制字符映射到相应的两字符序列。然而,很少有必要将所有控制字符都进行转义映射。因此,应用程序可以通过该选项去通知对方哪些控制字符需要进行转义。控制字符映射表通过 4 个字节来表示,其中的每一位表示相应的值是否映射,0 表示不进行映射,1 表示进行映射。在传输过程中最先传输的是第 31 位,最后传输的是 0 位。其中,第 0 位对应的是ASCII 码 NUL。
• 鉴权协议(Authentication-Protocol)
一般在网络层交换数据前要求进行鉴权,这个配置选项提供了一种协商鉴权协议的方法。默认不进行鉴权。在请求鉴权的过时,每次只能使用一个鉴权协议选项,只有当该协议被拒绝以后,才能再请求使用别的协议进行。
• 质量协议(Quality-Protocol)
在一些连接中,可能需要决定什么时候、多久进行数据发送,这一过程称为质量监控。这个配置选项提供了一种协商使用的质量监控协议的方法。默认不使用质量监控协议。
• 魔数(Magic-Number)
该选项提供了一种探测短路连接和其它数据链路层异常的方法,它可能在其它配置选项中用到。使用魔数检测链路的基本思想是:当一方接收到带有魔数选项的配置请求数据帧后,将接收到的魔数与上次发送的魔数进行比较,如果不相同就认为没有发生短路。如果两个魔数相同,则需要发送一个携带不同魔数的配置否认帧,然后将接收到的魔数与发送的魔数进行比较。
• 协议域压缩(Protocol-Field-Compression)
该选项提供了一种压缩数据链路层协议域的方法。在标准的PPP 中,协议编号为两个字节,经过协商后,可以把编号小于 256 的协议压缩为一个字节传输,比如传输 IP 信息时,协议编号可以由 0021 压缩为 21,但是编号大于 256 的协议无法压缩。默认不使用协议压缩。
选项类型:7 选项长度:2
• 地址和控制域压缩(Address-and-Control-Field-Compression)
该选项提供了一种压缩数据链路层地址和控制域的方法。标准 PPP 协议中必须发送地址和控制域,但由于这些是固定值,因此很容易压缩。在接收过程中,如果没有接收到 FF则认为进行了地址和控制域压缩。
选项类型:8 选项长度:2
下面为一组LCP协商过程的LOG:
我对第一条LCP报文做一个分析,后面可以由读自己练习一下。
Ff 03是ppp协议的地址域和控制域,C0 21表示这个报文是LCP数据包,01是LCP代码,表示配置请求数据帧(Configure-Request),01是LCP标志位,标志两条发送和接收命令的对就关系,00 14是LCP命令的长度,02 06表示异步控制字符映射(Asynchronous-Control-Character-Map, ACCM),0000 00 00 是ACCM的值,表示全部不映射,05 06,表示魔术字,a5 d0 6c aa是魔术字的值,07 02表示协议域压缩(Protocol-Field-Compression),08 02表示地址和控制域压缩(Address-and-Control-Field-Compression)
sent [ LCP ConfReqid=0x1
ff 03 c0 21 01 01 0014 02 06 00 00 00 00 05 06a5 d0 6c aa 07 02 0802
rcvd [LCP ConfReq id=0xa7
rcvd ff 03 c0 21 01 a7 00 19 02 06 00 00 00 00 03 05 c223 05 05 06 02 d0 c5 77 07 02 08 02
sent [ LCP ConfNakid=0xa7
sent ff 03 c0 21 03 a7 0008 03 04 c0 23
rcvd [LCP ConfAck id=0x1
rcvd ff 03 c0 21 02 01 00 14 02 06 00 00 00 00 05 06 a5d0 6c aa 07 02 08 02
rcvd [LCP ConfReq id=0xa8
rcvd ff 03 c0 21 01 a8 00 18 02 06 00 00 00 00 03 04 c023 05 06 02 d0 c5 77 07 02 08 02
sent [LCP ConfAck id=0xa8
sent ff 03 c0 21 02 a8 00 18 02 06 00 00 00 00 03 04 c023 05 06 02 d0 c5 77 07 02 08 02
rcvd [LCP DiscReq id=0xa9 magic=0x2d0c577]
rcvd ff 03 c0 21 0b a9 00 08 02 d0 c5 77
4. LCP 协商过程
下图所示为一个典型的 LCP 协商过程。图中终端 1 和终端 2 分别代表通信的双方,有向线段表示数据帧的流向。图中给出了终端 1 方向的协商完成过程。表格中给出的是每次发送的数据帧的具体内容。终端 1 第一次发送的请求数据帧中请求使用的鉴权协议为PAP,终端 2 不使用 PAP 协议鉴权,所以终端 2 用 NAK 数据帧应答,终端 1 检测到对方不使用 PAP 后,重新以 CHAP 协议发送请求数据帧,终端 2 此时可以接受所有的配置请求,所以终端 2 发送协商 ACK 数据帧。这样就完成了一个方向的协商,同理,终端 2 发送的协商请求过程与此类似。图中标注的值都是没有经过转义的字符,实际发送过程中要转义这些字符。
4:鉴权协议
在 PPP 连接过程中, LCP 协议定义了一种使用鉴权协议进行鉴权的方法。这种机制可以使用不同的协议进行鉴权,目前支持的鉴权协议包括 PAP(Password Authentication Protocol)和CHAP(Challenge Handshake Authentication Protocol)。
1.密码鉴权协议(PAP)PAP 提供了一种通过双向握手进行身份确认的简单方法。在LCP 链路建立后,被鉴权者将身份和密码发送给鉴权者,然后等待对方的确认信息。因为用户的身份和密码是通过链路以明码的方式发送的,所以 PAP 不是一种绝对安全的鉴权方法。
(1)PAP 数据帧格式
(2)数据帧类型
• 鉴权请求(Authenticate-Request)
鉴权请求用于启动密码认证协议,将本地的身份标识和密码发送给对方,并等待对方应答。该过程可以多次重复直到接收到对方的应答信息。
• 鉴权确认/鉴权否认(Authenticate-ACK/Authenticate-NAK)
如果接收到的鉴权请求数据帧中的用户标识和密码都合法,那么通信终端将使用鉴权确认数据帧进行确认,以通知对方已经通过了身份验证;如果接收到的信息不合法,则使用鉴权否认数据帧通知对方。在鉴权确认数据帧的数据域中可以包含一些用于显示的信息。
2.挑战握手鉴权协议(CHAP)
CHAP 协议使用三方握手来周期性的确定对方的身份,它可以在链路建立后的任何时候进行。当链路建立后,鉴权者向对方发送一个“挑战”信息,对方使用单向链表(one-way hash)函数计算后发送结果,鉴权者将接收到的信息与自己计算出来的结果进行比较,如果两者相同,则鉴权成功;否则,鉴权失败,连接被终止。同 PAP 相比,CHAP 更具有安全性。首先,鉴权过程中使用不断变化的挑战信息和身份标识,这使得攻击者很难有机会进行破解;其次,鉴权由鉴权者控制,它可以随时对对方进行身份确认。使用 CHAP 时,必须配合一种链表算法,目前与 CHAP 配合使用的算法是 MD5 算法。在 PPP 中使用CHAP必须在 LCP 协商时配置相应的鉴权算法为 CHAP,配置选项格式如下:
(2)CHAP 数据帧类型
• 挑战和应答数据帧
挑战数据帧用来启动 CHAP。鉴权者在 LCP 协商后主动发送挑战信息来验证用户身份。对方在接收到挑战信息后用单向链表算法进行计算,然后将计算后的结果用应答数据帧进行应答。数据帧格式如下:
代码:挑战数据帧为 1;应答数据帧为 2
标识:一个字节。每次发送挑战数据帧时必须使用不同的标识码;应答数据帧必须将挑战数据帧的标识码复制后发送。
挑战值长度:一个字节,指示挑战值的长度。
挑战值:一个以上的字节,首先发送高位字节。挑战值是一个可变的字节流,每次挑战要使用不同的数值;应答数据帧中该域存放经过计算后的信息流,信息流的长度取决于使用的链表算法,比如 MD5 算法计算的结果是 16 字节。
名称:标识传输数据包的系统的名称,但是该域的值并没有限制,可以采用不同数值进行发送。
• 成功和失败数据帧如果接收到的应答信息是正确的,那么主机使用成功数据
包进行应答;反之,主机发送失败数据包并终止连接。
代码:成功数据帧为 3;失败数据帧为 4
标识:必须从应答数据帧中复制该值。
信息:信息域是可选的而且其内容是由具体的应用来决定的,一般来说,信息域存放的是可以显示的 ASCII 码。
下面是一个PAP鉴权过程的命令
c0 23:PAP;AuthReq :01;id :01;长度:00 14;用户名长度:0d;用户名:77 65 72 74 33 34 35 36 24 25 35 3637;密码长度:01;密码71
用户名为:wert3456$%567;密码为:q;
sent [ PAP AuthReq id=0x1 user="wert3456$%567" password=
ff 03 c0 23 01 01 00 14 0d77 65 72 74 33 34 35 36 24 25 35 36 37 01 71
rcvd [ PAP AuthAck id=0x1 "" ]
ff 03 c0 23 02 01 0005 00
下面是CHAP鉴权的过程,没有二进制的命令,读者可试着自行解析为二进制。
rcvd [CHAP Challenge id=0x1
sent [CHAP Response id=0x1
rcvd [CHAP Success id=0x1""]
CHAP authentication succeeded
注:以上两组LOG都是在不需要鉴权的状态下抓取的,所以在AuthAck 时是个空值,在有鉴权的情况下,AuthAck 时的值与LOG可能不同,请读者注意区分
5:IPCP协议
在 LCP 协商和鉴权阶段后, PPP 操作进入网络协议配置阶段。在这个阶段通信双方发送 NCP(Network Control Protocol)数据包来选择和配置一个或多个网络层协议。比如,如果网络层要使用 IP 协议,那么此时必须使用一个网络配置协议来配置双方的 IP 地址、域名服务器 IP 地址等,只有双方获得了这些必要的信息,才能进行网络协议层的数据传输。配置 IP 协议的 PPP 配置协议是 IPCP。IPCP 用来配置、使能、禁用通信双方的 IP 协议模块。IPCP使用与 LCP 相同的协商机制,与 LCP 不同的地方有:
• PPP 中的数据链路层协议域
因为 IPCP 是在 PPP 的数据域中发送的,因此,在 PPP 的协议域中必须设置为 IPCP的协议编号。IPCP 的编号是 8021。
• 使用的代码域 IPCP 使用的数据帧格式与 LCP 相同,只是它的代码只有 1到7 的这 7 种取值,即它只有 7 种数据帧类型,主要用来发送配置请求、接收应答以及拒绝应答等,见上文有关 LCP 数据帧的部分。
• 配置选项
IPCP 使用与 LCP 完全不同的配置选项,见下文详细论述。
1.IPCP 数据帧格式
2.IPCP 配置选项
• IP-Addresses:代码域:1
由于该选项在具体应用中存在问题,所以目前不再使用该选项,它由 IP-Address 选项取代。只有在对方发送该协商选项时才使用,否则不要主动使用该选项[29]。
• IP-Compression-Protocol 用来提供协商使用指定的压缩协议,默认不使用压缩选项。选项的格式如下:
IP 压缩协议域指明要使用的压缩协议,协议编号与 PPP 协议域中的协议编号相同。目前支持的协议有 Van JacobsonCompressed TCP/IP[29],编号为 002D(16 进制)。
• IP-Address 用来协商本地使用的IP 地址。该选项允许请求发送者提供自己的IP 地址或请求对方给自己分配 IP 地址,在后一种情况下,请求者发送一个全为 0 的IP 地址,对方在一个 NAK 数据帧中给出请求者的 IP 地址。选项的格式如下:
• Primary DNS Server Address/Secondary DNSServer Address
该选项用来协商远端的主、次 DNS(Domain Name System,域名服务器)服务器地址。将选项的数据域设置为 0 表示要求对方提供 DNS 地址,对方使用 NAK 数据帧来应答 DNS地址。选项格式如下:
• Primary NBNS Server Address/Secondary NBNSServer Address
该选项用来协商远端的主、次 NBNS(NetBIOS Name Server, NetBIOS 名称服务器)节点地址。该选项的数据为 0 表示要求对方提供 NBNS 地址,对方使用 NAK 数据帧来应答NBNS 地址。类型代码分别为 130 和 132。
3.IPCP 协商过程
客户端首先发送 IP 地址和 DNS 地址都为零的请求数据帧,服务器接收到后用 NAK 数据帧回复动态 IP 地址和DNS 地址,然后客户端再次用接收到地址发送请求,接收到 ACK数据帧即完成了 IPCP 配置。服务器直接将自己的 IP 地址发送给客户端,接收到 ACK 数据帧后 IPCP 配置成功。这个过程如图 3-8 所示。
sent [ IPCP ConfReq id=0x1
ff 03 80 21 01 01 00 16 0306 00 00 00 00 81 06 00 00 00 00 83 06 00 00 00 00
rcvd [LCP ProtRej id=0xaa 80fd 01 01 00 0f 1a 04 78 00 18 04 78 00 15 03 2f]
rcvd ff 03 c0 21 08 aa 00 15 80 fd 01 01 00 0f 1a04 78 00 18 04 78 00 15 03 2f
rcvd [IPCP ConfNak id=0x1
rcvd ff 03 80 21 03 01 00 1c 81 06 0a 0b 0c 0d 8306 0a 0b 0c 0e 82 06 0a 0b 0c 0d 84 06 0a 0b 0c 0e
sent [IPCP ConfReq id=0x2
sent ff 03 80 21 01 02 00 16 03 06 00 00 00 00 8106 0a 0b 0c 0d 83 06 0a 0b 0c 0e
rcvd [IPCP ConfReq id=0x2a]
rcvd ff 03 80 21 01 2a 00 04
sent [IPCP ConfNak id=0x2a
sent ff 03 80 21 03 2a 00 0a 03 06 00 00 00 00
rcvd [IPCP ConfNak id=0x2
rcvd ff 03 80 21 03 02 00 16 03 06 0a 1c 0f ae 8106 3a 16 60 42 83 06 da 68 80 6a
sent [IPCP ConfReq id=0x3
sent ff 03 80 21 01 03 00 16 03 06 0a 1c 0f ae 8106 3a 16 60 42 83 06 da 68 80 6a
rcvd [IPCP ConfReq id=0x2b]
rcvd ff 03 80 21 01 2b 00 04
sent [IPCP ConfAck id=0x2b]
sent ff 03 80 21 02 2b 00 04
rcvd [IPCP ConfAck id=0x3
rcvd ff 03 80 21 02 03 00 16 03 06 0a 1c 0f ae 8106 3a 16 60 42 83 06 da 68 80 6a
二: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)
PPP完全理解(三)
内核ppp协议处理模块代码分析
作者:李圳均
日期:2013/11/27
点对点协议(PPP)为在点对点连接上传输多协议数据包提供了一个标准方法。ppp 位于数据链路层,是一种为同等单元之间传输数据包这样的简单链路设计的链路层协议。这种链路提供全双工操作,并按照顺序传递数据包。
PPP 最初设计是为两个对等节点之间的 IP 流量传输提供一种封装协议。在 TCP-IP 协议集中它是一种用来同步调制连接的数据链路层协议(OSI 模式中的第二层),替代了原来非标准的第二层协议,即 SLIP。除了 IP 以外 PPP 还可以携带其它协议,包括 DECnet 和 Novell 的Internet 网包交换(IPX)。。设计目的主要是用来通 过拨号或专线方式建立点对点连接发送数据,使其成为各种主机、网桥和路由器之间简单连接的一种共通的解决方案。
在内核中的ppp协议处理模块,其基本功能是发送和接收IP数据报、IP数据报的封装,解封、数据报的分割与重组,其数据帧格式在文档二中有详细说明,这里再次贴出ppp的数据帧格式,说明一下ppp协议处理模块对这部分的处理。从文档二可知,pppd已实现了ppp策略性的内容,包括所有鉴权、压缩/解压和加密/解密等扩展功能的控制协议,在ppp协议处理模块,会完成从IP协议层下发的IP数据的发送并把从串口收到IP数据报发送给IP协议层。Pppd的协议数据也会通过ppp封装后与网络进行协商。
(一)PPP驱动程序的基本原理
PPP 协议之下是以太网和串口等物理层,之上是IP协议等网络层。这里,对于下层,我们只讨论串口的情况,对于上层,我们只讨论TCP/IP的情况。发送时, TCP/IP数据包经过PPP打包之后经过串口发送。接收时,从串口上来的数据经PPP解包之后上报给TCP/IP协议层。
pppd是一个后台服务进程(daemon),是一个用户空间的进程,所以把策略性的内容从内核的PPP协议处理模块移到pppd中是很自然的事了。pppd实现了所有鉴权、压缩/解压和加密/解密等扩展功能的控制协议。
在移动终端向监控中心发送定位信息的过程中,移动终端上的 GPRS 通信程序通过 socket 接口发送 TCP/IP 数据包,内核根据 IP 地址和路由表,找到 PPP 网络接口,然后调用函数 ppp_start_xmit( ),此时控制权就转移到了 PPP 协议模块。函数 ppp_start_xmit( ) 调用函数 ppp_xmit_process( ) 去发送队列中的所有数据包,而函数ppp_xmit_process( ) 会进一步调用函数 ppp_send_frame( ) 去发送单个数据包。函数 ppp_send_frame( ) 根据前面 pppd 对 PPP 协议模块的设置调用压缩等扩展功能之后,又经函数 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->driver->write( )(定义在低层驱动程序中)把数据发送到串口 2(GPRS 通信模块接在串口 2 上)。
ppp_async.c 在初始化时(ppp_async_init),调用函数 tty_register_ldisc( ) 向 tty 注册了行规程 N_PPP 的处理接口,也就是一组回调函数。在移动终端接收监控中心指令的过程中,当 GPRS 通信模块收到数据时,就会回调 N_PPP 行规程中的函数 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( ) 提交给上层的 TCP/IP 协议模块进行处理,最后经 socket 接口传递给应用层的 GPRS 通信程序。
=====================
1) ppp设备是指在点对点的物理链路之间使用PPP帧进行分组交换的内核网络接口设备,
由于Linux内核将串行设备作为终端设备来驱动,
于是引入PPP终端规程来实现终端设备与PPP设备的接口. 根据终端设备的物理传输特性的不同,
PPP规程分为异步规程(N_PPP)和同步规程(N_SYNC_PPP)两种, 对于普通串口设备使用异步PPP规程.
2) 在PPP驱动程序中, 每一tty终端设备对应于一条PPP传输通道(chanell),
每一ppp网络设备对应于一个PPP接口单元(unit).
从终端设备上接收到的数据流通过PPP传输通道解码后转换成PPP帧传递到PPP网络接口单元,
PPP接口单元再将PPP帧转换为PPP设备的接收帧.
反之, 当PPP设备发射数据帧时,发射帧通过PPP接口单元转换成PPP帧传递给PPP通道, PPP通道负责将PPP帧编码后写入终端设备.
在配置了多链路PPP时(CONFIG_PPP_MULTILINK),多个PPP传输通道可连接到同一PPP接口单元.
PPP接口单元将PPP帧分割成若干个片段传递给不同的PPP传输通道, 反之,
PPP传输通道接收到的PPP帧片段被PPP接口单元重组成完整的PPP帧.
3) 在Linux-2.4中, 应用程序可通过字符设备/dev/ppp监控内核PPP驱动程序.
用户可以用ioctl(PPPIOCATTACH)将文件绑定到PPP接口单元上, 来读写PPP接口单元的输出帧,
也可以用ioctl(PPPIOCATTCHAN)将文件绑定到PPP传输通道上, 来读写PPP传输通道的输入帧.
4) PPP传输通道用channel结构描述, 系统中所有打开的传输通道在all_channels链表中.
PPP接口单元用ppp结构描述, 系统中所有建立的接口单元在all_ppp_units链表中.
当终端设备的物理链路连接成功后, 用户使用ioctl(TIOCSETD)将终端切换到PPP规程.
PPP规程初始化时, 将建立终端设备的传输通道和通道驱动结构. 对于异步PPP规程来说,
通道驱动结构为asyncppp, 它包含通道操作表async_ops.
传输通道和接口单元各自包含自已的设备文件(/dev/ppp)参数结构(ppp_file).
/dev/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函数完成的。
staticconst struct file_operations ppp_device_fops= {
.owner =THIS_MODULE,
.read =ppp_read,
.write =ppp_write,
.poll =ppp_poll,
.unlocked_ioctl = ppp_ioctl,
.open =ppp_open,
.release =ppp_release
};
()ppp_init(void)
err= register_chrdev(PPP_MAJOR,"ppp", &ppp_device_fops);
()ppp_async_init(void)
tty_register_ldisc(N_PPP,&ppp_ldisc);
(二)ppp相关数据结构
struct ppp{
structppp_file file; /* stuff for read/write/poll 0 */
structfile *owner; /* file that owns this unit 48 */
struct list_head channels; /* list of attached channels 4c */
int n_channels; /* how many channels are attached 54 */
spinlock_t rlock; /*lock for receive side 58 */
spinlock_t wlock; /*lock for transmit side 5c*/
int mru; /* max receive unit 60 */
unsignedint flags; /* control bits 64 */
unsignedint xstate; /* transmit state bits 68 */
unsignedint rstate; /* receive state bits 6c */
int debug; /* debug flags 70 */
structslcompress *vj; /* state forVJ header compression */
enumNPmode npmode[NUM_NP]; /* what to do with each net proto 78*/
structsk_buff *xmit_pending; /* a packet ready to Go out 88 */
structcompressor *xcomp; /* transmit packetcompressor 8c */
void *xc_state; /* its internal state 90 */
structcompressor *rcomp; /* receive decompressor94 */
void *rc_state; /* its internal state 98 */
unsignedlong last_xmit; /* jiffies when last pkt sent 9c */
unsignedlong last_recv; /* jiffies when last pkt rcvd a0 */
struct net_device *dev; /* network interface device a4 */
int closing; /* is device closing down? a8 */
#ifdef CONFIG_PPP_MULTILINK
int nxchan; /* next channel to send something on */
u32 nxseq; /* next sequence number to send */
int mrru; /* MP: max reconst. receive unit */
u32 nextseq; /* MP: seq no of next packet */
u32 minseq; /* MP: min of most recent seqnos */
structsk_buff_head mrq; /* MP: receivereconstruction queue */
#endif /* CONFIG_PPP_MULTILINK */
#ifdef CONFIG_PPP_FILTER
structsock_filter *pass_filter; /*filter for packets to pass */
structsock_filter *active_filter;/* filter for pkts to reset idle */
unsignedpass_len, active_len;
#endif /* CONFIG_PPP_FILTER */
struct net *ppp_net; /* the net we belong to */
};
struct channel{
struct ppp_file file; /*stuff for read/write/poll */
structlist_head list; /* linkin all/new_channels list */
structppp_channel *chan; /* public channeldata structure */
structrw_semaphore chan_sem; /* protects`chan' during chan ioctl */
spinlock_t downl; /*protects `chan', file.xq dequeue */
struct ppp *ppp; /* ppp unit we're connected to */
structnet *chan_net; /* the net channel belongs to */
structlist_head clist; /* link inlist of channels per unit */
rwlock_t upl; /*protects `ppp' */
#ifdef CONFIG_PPP_MULTILINK
u8 avail; /* flag used in multilink stuff */
u8 had_frag; /* >= 1 fragments have been sent */
u32 lastseq; /* MP: last sequence # received */
int speed; /*speed of the corresponding ppp channel*/
#endif /* CONFIG_PPP_MULTILINK */
};
struct ppp_file {
enum{
INTERFACE=1,CHANNEL
} kind;
struct sk_buff_head xq; /*pppd transmit queue */ /*传输队列*/
struct sk_buff_head rq; /*receive queue for pppd */ /*发送队列*/
wait_queue_head_trwait; /* for poll on reading/dev/ppp */
atomic_t refcnt; /*# refs (incl /dev/ppp attached) */
int hdrlen; /* space to leave for headers */
int index; /* interface unit / channel number */
int dead; /* unit/channel has been shut down */
};
struct ppp_channel {
void *private; /* channel private data */
struct ppp_channel_ops *ops; /* operations for this channel */
int mtu; /* max transmit packet size */
int hdrlen; /* amount of headroom channel needs */
void *ppp; /*opaque to channel */
int speed; /* transfer rate (bytes/second) */
/*the following is not used at present */
int latency; /* overhead time in milliseconds */
};
struct ppp_channel_ops {
/*Send a packet (or multilink fragment) on this channel.
Returns 1 if it was accepted, 0 if not. */
int (*start_xmit)(struct ppp_channel *, struct sk_buff *);
/*Handle an ioctl call that has come in via /dev/ppp. */
int (*ioctl)(struct ppp_channel *, unsigned int, unsigned long);
};
static struct ppp_channel_ops async_ops = {
ppp_async_send,
ppp_async_ioctl
};
(三)ppp内核发送数据过程
应用程序通过socket 接口发送TCP/IP数据包,这些TCP/IP数据包如何流经PPP协议处理模块,然后通过串口发送出去呢?pppd在make_ppp_unit函数调用ioctrl(PPPIOCNEWUNIT)创建一个网络接口(如ppp0),内核中的PPP协议模块在处理PPPIOCNEWUNIT时,调用register_netdev向内核注册ppp的网络接口,该网络接口的传输函数指向ppp_start_xmit。当应用程序发送数据时,内核根据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->driver->write把数据发送串口。
ppp_start_xmit(struct sk_buff *skb, structnet_device *dev)
ppp_xmit_process(struct ppp *ppp)
ppp_push(struct ppp *ppp)
pch->chan->ops->start_xmit(pch->chan,skb)
ppp_async_send(struct ppp_channel *chan,struct sk_buff *skb)
ppp_async_push(struct asyncppp *ap)
tty->ops->write(tty, ap->optr,avail)
(四)ppp内核接受数据过程
接收数据的情形又是如何的?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传递到应用程序。
ppp_asynctty_receive(struct tty_struct*tty, const unsigned char *buf,
char *cflags, int count)
ppp_async_input(ap, buf, cflags, count);
ppp_async_process(unsigned long arg)
ppp_input(struct ppp_channel *chan, structsk_buff *skb)
{
if (!pch->ppp || proto >= 0xc000 ||proto == PPP_CCPFRAG) {
/*put it on the channel queue */
skb_queue_tail(&pch->file.rq, skb); //是控制协议数据包,放入pch->file.rqb队列,交给pppd处理。
/*drop old frames if queue too long */
while(pch->file.rq.qlen > PPP_MAX_RQLEN
&& (skb =skb_dequeue(&pch->file.rq)))
kfree_skb(skb);
wake_up_interruptible(&pch->file.rwait);
}
else {
ppp_do_recv(pch->ppp, skb, pch); //进行PPP处理之后,再由netif_rx提交给上层协议处理
}
}