点对点协议PPP 是数据链路层里最容易被翻过去的一节,讲起来不像以太网那样有存在感,帧格式看着也朴素。但我在实际工作里碰到它的次数远比想象中多:路由器带外管理口的串口登录、工业现场两台设备拉一根线通信、嵌入式板子通过蜂窝模块拨号上网、宽带接入跑的那层 PPPoE,底层全是这套东西。换句话说,只要涉及“两台设备之间只连一根线、没有共享介质、没有交换机”,PPP 就是绕不开的那层。这篇笔记我把公开课上讲的内容重排了一遍,按我自己理解的顺序拆开:先讲它为什么长这样,再把帧格式逐字节过一遍,然后是 LCP 建链、PAP/CHAP 认证、IPCP 协商,最后补一段抓包实录和我自己踩过的坑。刚学链路层的同学能顺着往下读,做嵌入式或网络运维的同行可以直接跳到第 2 节和第 6 节抄配置。
1. 把PPP放回它该在的位置:它到底解决哪一段的问题
1.1 一根线连两台机器,问题比想象的多
很多人对数据链路层的直觉停留在以太网:有 MAC 地址、有交换机、有广播。可 PPP 面对的场景完全不同,它是两个端点之间的一条独占链路,中间没有第二个人。这种拓扑看着简单,麻烦却在细节里。物理层只管把电信号或光信号推过去,它不保证你收到的比特流从哪一帧开始算起,也不保证业务数据和链路控制指令能分得开,更不保证对方是不是冒充的。
所以 PPP 要干的活其实是三件叠在一起的事:给数据划出帧边界、在链路层协商链路本身的参数、以及在允许传数据之前先确认对方身份。这三件事在以太网里被拆散到了不同机制上(前导码、MAC 帧头、802.1X),在 PPP 里被压缩进了一套协议。我第一次意识到这个设计的紧凑程度,是在一个只有 7 字节的 PPP 空帧上——两个标志、一个地址、一个控制、一个两字节协议、一个 FCS,链路层的骨架就齐了。
理解这一点很重要,因为后面所有看起来“多余”的字段和报文,全部是为这三件事服务的。你觉得地址字段没用?它是历史包袱。你觉得 LCP 反复来回太啰嗦?那是为了双向确认。想通这层逻辑,PPP 就不需要背了。
1.2 PPP的三个组成部分,缺一不可
PPP 不是一个单一协议,而是一个打包体,教材上通常拆成三块。第一块是把 IP 数据报封装成帧的方法,负责帧定界和透明传输,这一块决定了上层数据怎么塞进去、怎么取出来。第二块是链路控制协议 LCP,负责建链、配参数、测链路、拆链,它是整个 PPP 的“总开关”。第三块是网络控制协议 NCP,每一种网络层协议都有一套自己的 NCP,用来协商网络层相关参数,比如 IP 地址、DNS 服务器地址。
这个拆法的妙处在于解耦。链路层能干什么,由 LCP 说了算;上面跑 IP 还是跑 IPv6 还是跑别的,由各自的 NCP 说了算。所以同一套帧格式,可以同时支撑 IPv4、IPv6,未来加新协议也不用动底层。我在实际配置中感受最深的是:当 IPCP 协商失败时,LCP 往往已经显示 Opened 了,链路是通的,只是网络层参数没谈拢。分清楚这两层,排错效率立刻不一样。
还有一个容易被忽略的点:认证(PAP/CHAP)在标准里并不属于 LCP 和 NCP 中的任何一个,但它的报文走的是 PPP 帧,阶段上卡在 LCP 之后、NCP 之前。很多教材把它单独列一层,我习惯把它看成 LCP 的延伸——因为“要不要认证、用哪种认证”本身就是 LCP 协商出来的结果。
1.3 今天翻出PPP还有没有必要
有必要,而且比想象中有必要。第一个理由是它足够轻,一个几百字节的协议栈就能跑在资源受限的单片机上,做串口透传、做远程抄表、做工业设备点对点通信,上 TCP/IP 太重,裸串口又没校验没重传,PPP 恰好卡在中间。第二个理由是它是很多现成方案的底座,宽带接入用的是 PPPoE,也就是把 PPP 帧塞进以太网帧里跑,拨号用的还是 LCP + CHAP + IPCP 那一套。
第三个理由更实际:PPP 是理解“链路层到底该管什么”的最佳样本。它刻意不做可靠传输,没有序号、没有确认、没有重传。这个取舍当年被不少人质疑,但放到误码率极低的光纤和专线上,重传机制的收益远小于它的复杂度成本,况且上层 TCP 已经有一套完整的重传逻辑,链路层再叠一层属于功能重复。这种“宁可少做,也不做重复的事”的设计思路,比记住帧格式有价值得多。
2. PPP帧格式拆解:从标志位到FCS的逐字节账
2.1 帧格式总览与字段取值
PPP 的帧格式脱胎于 HDLC,但砍掉了序号和确认机制,变成了无编号的不可靠传输。完整结构是:标志、地址、控制、协议、信息、FCS、标志。最小帧是 7 字节(不含信息字段时前面 5 字节加尾部 FCS 加首尾各一个标志)。
| 字段 | 长度 | 典型值 | 说明 |
|---|---|---|---|
| 标志 | 1 字节 | 0x7E | 帧的起始和结束界定符 |
| 地址 | 1 字节 | 0xFF | HDLC 遗留,点对点下无实际寻址意义,可被 ACFC 压缩 |
| 控制 | 1 字节 | 0x03 | 无编号信息帧 UI,同样可被压缩 |
| 协议 | 2 字节 | 0x0021 / 0xC021 / 0x8021 | 标识信息字段承载的是什么 |
| 信息 | 0 到 MRU | 可变 | 默认 MRU 为 1500 字节 |
| FCS | 2 或 4 字节 | 计算得出 | 默认 2 字节,可选 4 字节 |
| 标志 | 1 字节 | 0x7E | 结束界定符 |
关于首尾两个标志,有一个实操细节:标准允许一帧的结束标志直接作为下一帧的起始标志,也就是连续发帧时两个标志共用一个。抓包时看到“两个帧之间只有一个 0x7E”,不要以为是帧粘连了,这是合法行为。相反,如果看到一长串 0x7E 连在一起,那是链路上的空闲填充,接收端要跳过连续标志直到出现有效内容。
协议字段的取值范围有明确分段,这个知识点在很多教材里被一笔带过,但排错时特别好用:0x0000 到 0x3FFF 段是网络层协议,0x4000 到 0x7FFF 段是低流量协议(如链路质量报告),0x8000 到 0xBFFF 段是网络控制协议,0xC000 到 0xFFFF 段是链路控制协议。所以看到 0xC0 开头就知道是 LCP 家族的,看到 0x80 开头就知道是 IPCP 一类的 NCP。
| 协议字段值 | 协议名称 | 所属类别 |
|---|---|---|
| 0x0021 | IP | 网络层 |
| 0x0057 | IPv6 | 网络层 |
| 0xC021 | LCP | 链路控制 |
| 0xC023 | PAP | 链路控制 |
| 0xC223 | CHAP | 链路控制 |
| 0x8021 | IPCP | 网络控制 |
| 0x8057 | IPv6CP | 网络控制 |
2.2 字节填充:0x7E和0x7D这对搭档
异步链路(比如串口)上,PPP 用字节填充来解决透明传输。原理很朴素:0x7E 被选为帧界定符,那么信息字段里万一也出现了 0x7E,接收端就会误判成帧结束。解决办法是把这些“撞车”的字节替换掉。
具体规则是三条:信息字段中每出现一个 0x7E,替换成 0x7D 0x5E;每出现一个 0x7D,替换成 0x7D 0x5D;对于小于 0x20 的控制字符,按异步控制字符映射表 ACCM 的配置决定是否转义,需要转义时在该字节前插入 0x7D 并将其与 0x20 异或。观察一下就很清楚:0x7E 异或 0x20 恰好是 0x5E,0x7D 异或 0x20 恰好是 0x5D,所以第二条和第三条其实是同一个操作,第二条只是第三条的特例。
为什么偏偏要转义控制字符?因为 PPP 早期大量跑在调制解调器拨号链路上,0x11(XON)和 0x13(XOFF)这类字节会被调制解调器当成流控指令直接吞掉,导致数据丢失。ACCM 是一个 32 位位图,对应 0x00 到 0x1F 这 32 个控制字符,哪一位为 1 就表示该字符需要转义。标准默认全 1,即全部转义,最保险但开销最大。
这里可以算一笔账。如果只转义 0x7E 和 0x7D 两个字节,转义概率约为 2/256,1500 字节的数据平均多出约 12 字节。如果 ACCM 全开,转义概率为 32/256 即 12.5%,1500 字节平均多出近 188 字节,开销超过 12%。所以在误码率低、链路两端都支持的情况下,把 ACCM 协商成只转义必要的字符,能实打实省带宽。我在一条 9600 波特率的低速链路上做过对比,把 ACCM 从全 1 改成只保留 0x7D 和 0x7E,批量传输小包的耗时肉眼可见地缩短。
2.3 同步链路上的零比特填充
如果链路是同步的(比如数字专线、某些工业总线),PPP 就不用字节填充,改用零比特填充。规则和 HDLC 一样:发送端在信息字段中发现连续 5 个 1,就强行插入一个 0;接收端看到连续 5 个 1 后面跟一个 0,就把这个 0 删掉。
判断是同步还是异步,看的是链路层介质,不是配置项。串口这类按字节流传输的是异步,时钟同步的专线是同步。很多同学在这里犯迷糊,其实记住一句话就行:字节填充处理的是“字节碰巧等于界定符”,零比特填充处理的是“比特序列碰巧等于界定符”。两条路殊途同归,都是为了保证数据里出现任何内容,都不会被误认成帧边界。
有一个坑值得单独说:如果一条链路两端一端配成同步、一端配成异步,现象是收不到任何有效帧,抓包工具里全是乱码或者干脆没有帧。这种故障极难从日志上看出来,因为两端都认为自己配置正常。我的经验是,跨厂商设备对接时先确认链路类型这一项,别急着怀疑 FCS 和认证。
2.4 FCS-16的计算口径与手算思路
FCS 是 PPP 的差错检测字段,默认 2 字节。计算时用的是反射形式的生成多项式 0x8408(对应 x^16 + x^12 + x^5 + 1 的位反转),初值 0xFFFF,算完后按位取反,传输时低字节在前。这个描述和很多教材直接给的多项式 0x1021 看起来不一样,其实是一回事,只是一个按高位优先算、一个按低位优先算,网上的实现经常在这两种口径之间打架。
接收端的验证方法更简洁:把包括 FCS 字段在内的整个帧(不含转义、不含首尾标志)按同样算法算一遍,如果结果是 0xF0B8,说明校验通过。这个 0xF0B8 在标准里被称为“好帧残差”,记住它能省不少事,比拿计算出来的 FCS 去和帧里带的 FCS 做比较要方便。
POLY = 0x8408 # x^16 + x^12 + x^5 + 1 的反射形式 def ppp_fcs16(data: bytes) -> int: crc = 0xFFFF for b in data: crc ^= b for _ in range(8): crc = (crc >> 1) ^ POLY if crc & 1 else crc >> 1 return crc ^ 0xFFFF def check_frame(frame_with_fcs: bytes) -> bool: crc = 0xFFFF for b in frame_with_fcs: crc ^= b for _ in range(8): crc = (crc >> 1) ^ POLY if crc & 1 else crc >> 1 return crc == 0xF0B8 # 好帧残差注意:FCS 计算的输入是去转义之后、去掉首尾标志的帧内容,包括地址、控制、协议、信息和 FCS 本身。如果在转义前的原始字节上算,结果必然不对。
这一段代码我是用来做离线分析抓包结果的,配合后面第 5 节的解析脚本,能把一串十六进制还原成可读的报文。手算的话不必强求,但至少要知道 FCS 覆盖哪些字段,否则排查“为什么抓包工具标红”时会完全没方向。
3. LCP链路控制:一条链路是怎么“谈”起来的
3.1 LCP的11种报文与三类用途
LCP 是 PPP 的控制核心,它的报文都封装在协议字段为 0xC021 的 PPP 帧里,报文本身由 Code、Identifier、Length、Data 四部分组成。Code 决定报文类型,Identifier 用来匹配请求和响应,Length 是整个 LCP 报文的长度。
| Code | 报文名称 | 用途分类 | 说明 |
|---|---|---|---|
| 1 | Configure-Request | 链路配置 | 发起参数协商,携带所有想配置的选项 |
| 2 | Configure-Ack | 链路配置 | 全部选项接受 |
| 3 | Configure-Nak | 链路配置 | 选项能识别但值不接受,回一个建议值 |
| 4 | Configure-Reject | 链路配置 | 有识别不了的选项,要求删除后重发 |
| 5 | Terminate-Request | 链路终止 | 请求关闭链路 |
| 6 | Terminate-Ack | 链路终止 | 同意关闭 |
| 7 | Code-Reject | 链路维护 | 收到了不认识的 Code |
| 8 | Protocol-Reject | 链路维护 | 收到了不支持的协议号 |
| 9 | Echo-Request | 链路维护 | 保活探测 |
| 10 | Echo-Reply | 链路维护 | 保活应答 |
| 11 | Discard-Request | 链路维护 | 收到就丢弃,用于测试 |
最能区分新手和老手的是 Nak 和 Reject 这两个。Configure-Nak 的意思是“选项我认识,但这个值我不能接受,你换成我给的试试”,典型场景是 MRU 太小、认证方式不支持。Configure-Reject 的意思是“这个选项我根本不认识,你别发了”,典型场景是对端设备不支持魔数(Magic-Number)或者某种压缩选项。搞混这两个,你会对着日志完全猜不出问题出在哪。
3.2 链路六大阶段与状态迁移
PPP 的链路生命周期被划成六个阶段,顺序是固定的:链路静止(物理层没信号)、链路建立(LCP 协商)、认证(可选)、网络层协议阶段(NCP 协商)、链路打开(正常传数据)、链路终止。
静止阶段只有物理层在活动,LCP 连影子都没有。一旦物理层检测到载波,进入链路建立阶段,双方开始互发 Configure-Request。这里有个关键点:两个方向是各自独立协商的,A 发给 B 的 Configure-Request 管的是 B 发给 A 的数据怎么发,反之亦然。所以抓包时会看到两侧各有一组 Request/Ack,这是正常的,不是重复。
认证阶段是可选的,LCP 协商时如果双方约定了认证协议,认证就会在 LCP 完成之后、NCP 开始之前进行。如果认证失败,链路的走向是直接进终止阶段,NCP 根本不会启动。这就解释了一个常见现象:拨号失败时,日志里看不到任何 IPCP 相关内容,因为压根没走到那一步。
3.3 配置选项协商:MRU、认证、Magic-Number
LCP 协商的选项类型里,最常用的三个是 MRU、认证协议和 Magic-Number。
MRU 是最大接收单元,默认 1500,和以太网的 MTU 数值一致纯属巧合,因为它俩面对的需求相近。MRU 协商的特点是“只能往小改,不能往大改”,如果对端发来 1500 而你只能收 1200,你不能用 Nak 要求对方改大,只能自己把接收值改成 1200。这个不对称设计源于“接收方有最终决定权”的原则。
认证协议选项携带的是“我要你用哪种方式认证我”或者“我要求你用哪种方式认证我”,取值常见的是 0xC023(PAP)和 0xC223(CHAP)。这里是排错的高频点:如果一端要求 CHAP,另一端只会 PAP,结果就是 Configure-Nak 或者反复重传直到超时。
Magic-Number 是个很有意思的设计,它是一个随机数,用途是环回检测。如果收到的 Configure-Request 里带的 Magic-Number 和自己发出去的完全一样,说明链路被环回了,自己发的报文又回到了自己手上。这个机制在专线误接、光纤自环的场景下能救命,我碰到过一次跳线接错自环的情况,就是靠日志里那句“magic number 相同,检测到环回”定位的。
重传参数也值得记一下:Configure-Request 的默认重启定时器是 3 秒,最大发送次数 10 次,最大失败次数 5 次。所以一条链路协商不了,大概 30 秒后就会放弃。看到“等了半分钟没动静然后断了”,基本可以确定是 LCP 阶段就卡住了。
4. 认证与NCP:PAP、CHAP和IPCP的分工
4.1 PAP与CHAP的取舍
PAP 是两次握手,被认证方直接把用户名和密码明文发给认证方,认证方比对后回 Accept 或 Reject。它的优点是简单、兼容性好,缺点是密码在链路上裸奔。虽然现在链路本身可能是加密隧道,但在串口这种物理可达的链路上,明文传输依然有风险。
CHAP 是三次握手,认证方先发挑战(Challenge),里面带一个随机数和自己的名字;被认证方用这个随机数、自己的密码和报文标识算一个哈希值回过去(Response);认证方自己也算一遍,两边一致就回 Success。整个过程密码从不上链路,而且每次挑战的随机数都不同,重放攻击无效。
| 对比项 | PAP | CHAP |
|---|---|---|
| 握手次数 | 2 次 | 3 次 |
| 密码传输 | 明文上链路 | 不上链路,只传哈希 |
| 抗重放 | 不抗 | 抗,每次挑战随机 |
| 认证时机 | 建链后一次 | 建链后,可周期性重认证 |
| 配置复杂度 | 低 | 中,需要两侧密码一致 |
| 典型问题 | 密码错或链路被监听 | 密码不一致、时间/实现差异 |
选择上没有悬念,能上 CHAP 就别用 PAP。但现实里经常是被老设备逼着用 PAP,因为某些老型号的模块只支持 PAP。这时候能做的是把密码设得足够复杂,并且接受这个风险。
4.2 CHAP挑战响应的细节
CHAP 的响应计算里,参与哈希的输入是:报文标识符、密码、挑战随机数,具体拼接顺序取决于实现,但标准规定了字段构成。这里有个细节常被忽略:CHAP 支持周期性重认证,也就是说链路建立好之后,认证方可以随时再发一次 Challenge 要求重新验证。这个机制在长时间挂着的链路上很有价值,能防止半开连接被冒用。
配 CHAP 最容易踩的坑是名字对不上。CHAP 报文里带的是“名字”而不是完整的用户名,这个名字必须在两侧的本地配置里存在且密码匹配。我见过一次故障,两侧密码完全一样,但一侧配置里的名字带了域名后缀,另一侧没有,结果 Response 一直验证失败。日志里只显示“CHAP authentication failed”,完全看不出是名字问题。所以配 CHAP 时,务必把两侧的名字逐字符核对一遍。
4.3 NCP为什么独立于LCP
NCP 是一族协议的总称,每个网络层协议有自己的 NCP。IPv4 对应 IPCP,IPv6 对应 IPv6CP。它们和 LCP 的关系是并列的,都走 PPP 帧,都用自己的 Code/Identifier 结构,但职责完全不同:LCP 管链路能不能用,NCP 管这条链路上跑的网络层参数怎么配。
以 IPCP 为例,它协商的核心选项是 IP 地址。典型流程是:一端发 Configure-Request,IP 地址填 0.0.0.0 表示“我不知道,请分配一个”;对端回 Configure-Nak,把要分配的地址填进去;发起方用这个地址再发一次 Configure-Request;对端回 Configure-Ack,协商完成。除了 IP 地址,IPCP 还能协商主 DNS、备用 DNS,以及头部压缩协议。
很多教材把 IPCP 讲得很轻,但它在实际排错中权重很高。一个典型现象是:LCP 显示 Opened,认证也过了,但就是上不了网。这时候看 IPCP 的状态,如果停在 Req-Sent,说明 IP 地址没谈下来,可能是地址池配错、可能是地址冲突、也可能是对端压根没配 IPCP。把 LCP 和 IPCP 的状态分开看,能砍掉一大半无效排查。
5. 抓包实录:一次完整的PPP建链过程
5.1 环境准备与抓包方法
我用的是两台设备通过串口直连的简易环境,一端跑 Linux 下的 PPP 守护进程,另一端是一块带串口联网模块的开发板,波特率 115200,8 位数据位、无校验、1 位停止位,无硬件流控。抓包不能抓网卡,只能抓串口,所以用的是串口监听的方式,把线路上的原始字节流录下来,再用脚本还原成 PPP 帧。
串口参数这块有个实操提醒:8N1 是绝大多数 PPP 场景的默认值,但某些工业设备会用 7 位数据位或者带奇偶校验。参数不一致的表现不是“完全没数据”,而是“收到一堆看似有效但 FCS 全错的帧”,非常迷惑。我在第一次调试时就被这个坑耽误了半天,后来养成习惯,先确认串口参数,再看协议。
解析脚本的核心是两个函数:去转义和拆字段。
def unstuff(payload: bytes) -> bytes: out = bytearray() i = 0 while i < len(payload): b = payload[i] if b == 0x7D and i + 1 < len(payload): out.append(payload[i + 1] ^ 0x20) i += 2 else: out.append(b) i += 1 return bytes(out) def parse_frame(raw: bytes): body = unstuff(raw) if len(body) < 5: raise ValueError("帧太短") addr, ctrl = body[0], body[1] proto = int.from_bytes(body[2:4], "big") info = body[4:-2] fcs = int.from_bytes(body[-2:], "little") return {"addr": addr, "ctrl": ctrl, "proto": hex(proto), "info": info, "fcs": hex(fcs), "ok": check_frame(body)}这个脚本跑起来之后,抓包这件事就从“看十六进制猜意思”变成了“看结构化字段”,效率完全不是一个量级。
5.2 报文时序逐条解读
一次正常的建链时序,我整理成了下面这样。这里的 PADI、PADO 那些是 PPPoE 的东西,串口直连场景下没有,纯 PPP 的时序要干净得多。
| 序号 | 方向 | 报文 | 关键内容 |
|---|---|---|---|
| 1 | A → B | LCP Configure-Request | MRU=1500,Auth=CHAP,Magic=随机值 |
| 2 | B → A | LCP Configure-Ack | 全部接受 |
| 3 | B → A | LCP Configure-Request | MRU=1500,Auth=CHAP,自己的 Magic |
| 4 | A → B | LCP Configure-Ack | 全部接受 |
| 5 | A → B | CHAP Challenge | 随机数 + 认证方名字 |
| 6 | B → A | CHAP Response | 标识符 + 哈希值 + 自己的名字 |
| 7 | A → B | CHAP Success | 认证通过 |
| 8 | A → B | IPCP Configure-Request | IP=0.0.0.0,请求分配 |
| 9 | B → A | IPCP Configure-Nak | 建议 IP=192.168.10.2 |
| 10 | A → B | IPCP Configure-Request | IP=192.168.10.2 |
| 11 | B → A | IPCP Configure-Ack | 接受 |
| 12 | B → A | IPCP Configure-Request | 自己的 IP 与 DNS |
| 13 | A → B | IPCP Configure-Ack | 接受 |
| 14 | 双向 | 协议号 0x0021 的数据帧 | 开始传 IP 报文 |
这里有几个细节值得放大看。第 1 到第 4 步是双向独立的,A 的配置管的是 B 收,B 的配置管的是 A 收。第 8 到第 11 步的 0.0.0.0 是标准中的“我不知道地址”表示法,不是配置错误。第 12、13 步也常被忽略,NCP 同样是双向协商,很多人只看到一端拿到地址就以为完了,实际上对端也要把自己的 IP 和 DNS 推过来。
另外注意第 5 步的 Challenge 里有名字字段。名字这个东西在日志里常常是空的或者没被打印出来,导致排查时一度以为认证没有携带身份信息。抓包一看就清楚了。
5.3 三个典型失败现场复盘
第一个现场:Configure-Request 一直重发,收不到任何响应。日志里就是每隔 3 秒发一次,10 次之后链路关闭。这种基本是物理层或者链路类型的问题,比如串口线接反、波特率不一致、一端同步一端异步。我的排查顺序是先用最简单的串口工具确认字节能收到,再确认参数一致,最后才怀疑协议配置。
第二个现场:收到 Configure-Reject,反复重发同一个选项。这说明对端不认识某个选项。最常见的元凶是压缩选项和魔数。解决办法是关掉对端的 PFC 和 ACFC 支持,让协商选项表收缩到最基础的三项。这个操作代价很小,但能解决大量兼容性问题。
第三个现场:CHAP 一直失败,但密码确实一样。这就是前面提到的名字不匹配。我用抓包把两边的 Response 和 Challenge 都导出来,逐字节比对,才发现一侧的名字带了后缀。这种问题光看日志永远看不出来,必须抓包。
6. 排错与避坑:PPP链路起不来怎么查
6.1 分层排查路径
我自己的排查顺序是从下往上,一层一层排除,绝对不跳步。先看物理层:线接好没有、接口指示灯亮不亮、串口参数是否一致。这一层用示波器或者最粗糙的串口回环测试就能验证。再看链路层类型:同步还是异步,如果这条不对,后面全是白费。
接下来看 LCP。判断标准很直接:看有没有 Configure-Request 发出去,看有没有 Ack 回来。有 Request 无 Ack,问题在物理层或对端未启动;有 Request 有 Nak 或 Reject,问题在选项协商,看 Code 就能定位。LCP 到 Opened 之后,如果配了认证,就盯认证阶段。认证失败和认证超时是两回事,失败说明报文交互成功但验证没过,超时说明报文根本没到。
最后看 NCP。这一步出问题的表现通常是链路显示已连接但业务不通,这时候看 IPCP 状态和分配的地址,八成就水落石出了。我见过最隐蔽的一次是 IPCP 协商成功但分到的地址和本地网段冲突,链路一切正常,就是路由不对,这种情况只能靠核对地址规划解决。
6.2 故障速查表
| 现象 | 最可能原因 | 排查动作 |
|---|---|---|
| LCP 请求无任何响应 | 物理层不通、串口参数错、链路类型不匹配 | 检查线序、波特率、8N1、同步/异步配置 |
| 反复 Configure-Reject | 对端不支持某选项(PFC、ACFC、魔数) | 关闭可选压缩项,只留基础三项 |
| 反复 Configure-Nak | MRU 或认证方式不被接受 | 看 Nak 里的建议值,改本端配置 |
| 提示检测到环回 | Magic-Number 相同,链路自环 | 检查跳线与对端连接关系 |
| CHAP 认证失败 | 名字或密码不一致 | 逐字符核对两侧配置的名字 |
| 认证长时间无响应 | 认证报文未到达,链路丢包 | 抓包确认 Challenge 是否发出 |
| LCP 通但 IPCP 卡住 | 地址池配置、地址冲突、NCP 未启用 | 检查 IPCP 状态与地址规划 |
| 能通但大包失败 | MRU 与上层 MTU 不匹配 | 统一两侧 MRU,检查分片行为 |
| 抓包全是 FCS 错误 | 串口参数错、转义处理错、链路误码 | 校验串口参数与去转义逻辑 |
6.3 几条踩出来的经验
第一条,别迷信日志里的“链路已建立”。这句话只代表 LCP 到了 Opened,认证和 NCP 都有可能还在后面卡着。养成看分阶段状态的习惯,比看一句汇总结论有用得多。
第二条,抓包永远比猜快。PPP 的报文结构极其规整,只要把去转义和字段解析写好,任何问题都能在几分钟内定位。我现在的习惯是,只要链路起不来,第一件事就是抓包,而不是翻配置文件。
第三条,跨厂商对接时把可选特性全关掉。PFC、ACFC、魔数这些东西在标准里都是“可以协商”的,但不同实现的默认行为不一致,很容易在协商阶段打起来。先用最小配置把链路跑通,再逐项打开可选特性,一次只改一项,出问题立刻能定位到是哪一项导致的。
第四条,MRU 和上层 MTU 一定要对齐。PPP 的 MRU 管的是链路层能收多大的帧,上层 IP 的 MTU 管的是网络层能发多大的包,两者不一致时会出现“小包通、大包丢”的诡异现象。排查这类问题时,先用固定大小的小包 ping 通,再逐步增大包长找到临界值,比翻配置快得多。
第五条,低速链路上要算清楚转义开销。9600 波特率下,12% 的转义开销意味着实际吞吐掉了一大截,这时候优化 ACCM 的收益比想象中大。但要注意,ACCM 是双向协商的,你改了自己这端,对端不认也是白搭。
最后分享一个我常用的笨办法:把一次成功建链的完整字节流存成模板,出问题时拿现场抓的流和模板逐帧比对,第一个出现差异的地方基本就是问题所在。这个方法不需要你完全理解每个字段,靠着差异定位就能解决大部分对接问题。