HDLC与PPP链路层抓包排错:从串口帧到LCP认证实战

发布时间:2026/10/26 11:13:34
HDLC与PPP链路层抓包排错:从串口帧到LCP认证实战 调一串老设备的串口抓包窗口里最先蹦出来的往往不是 IP 报文而是一串被0x7E两头夹住的十六进制。很多人看到这玩意儿第一反应是看不懂直接跳过去看应用层然后就在认证、分片、保活这些地方反复栽跟头。HDLC 和 PPP 就是架在这个位置上的两个东西——HDLC 给了链路层一套通用的分帧和差错检测骨架PPP 在它的壳子上补齐了协商、认证、多协议复用这些几乎每个现场都会用到的能力。只要你碰过企业专线的串口链路、工业网关的透传口、蜂窝数传模块的拨号链路或者仅仅是想搞明白为什么ifconfig里那个ppp0网卡突然没了这两个协议迟早会挡在你面前。我写这篇不是想复述帧格式表——手册上都有。我想讲的是:抓到一串码流之后怎么一眼判断它是 HDLC 还是 PPP、控制字段里的那个字节到底在表达什么、LCP 协商卡在哪一步、认证失败为什么日志里只有一句链路已断开。文中涉及的命令和配置都能在你自己的机器上跑起来用虚拟串口造两只猫对拨不需要真实的广域网设备。适合刚接手链路层调试的朋友也适合那些会用但说不清原理的老手对照着复盘。1. 从串口抓包反推0x7E 夹住的那段十六进制到底是什么1.1 一个 12 字节的帧把两个协议的边界划出来先把最常见的形态摆出来。假设你在一条 115200 的串口上抓到了这么一段:7E FF 03 C0 21 09 01 00 08 7E去掉首尾两个7E剩下 8 个字节。FF是地址字段03是控制字段C0 21是协议字段09是 LCP 报文里的 Code表示 Echo-Request01是 Identifier00 08是长度字段。这一串是 PPP 的 LCP 保活探测一秒钟发一次都不奇怪。再看另一种形态:7E 0F 00 08 00 45 00 ... 7E同样是7E开头结尾同样是地址加控制但控制字段后面直接跟了08 00——这是以太网类型里 IPv4 的编号。地址字段是0F而不是FF控制字段固定为00而不是03。这就是厂商私有扩展的 HDLC 封装它在标准 HDLC 的地址/控制字段之后插了一个 2 字节的类型字段把这条链路上跑的是什么这件事交给类型号去回答而不是靠上层自己识别。两个帧放在一起对比你就能看出分界线在哪:PPP 用FF 03这对固定值,后面跟一个 2 字节协议号,协议号本身就是一个完整的命名空间;私有 HDLC 用0F 00这对固定值,后面跟的 2 字节在语义上更接近以太网的 EtherType。前者是我告诉你这是 LCP 还是 IPCP 还是 IPv4后者是我告诉你这是 IPv4 还是 IPv6 还是保活。理解了这个差异后面所有的抓包判读都顺了。还有第三种形态也就是最干净的标准 HDLC:7E 01 03 00 00 00 ... CRC CRC 7E地址01、控制03没有协议号。这种帧在 X.25 的 LAPB 链路上很常见03是 U 帧里的 UI无编号信息不做序号确认纯承载数据。地址字段的01表示这是 DTE 侧03表示 DCE 侧具体哪边用哪个取决于谁是主站。1.2 HDLC 帧头三件套标志、地址、控制理解 HDLC 得先接受一个前提:它是面向比特的不是面向字节的。这句话听起来抽象落到实际就是——标准 HDLC 在同步链路上根本不保证你的数据是字节对齐的接收方靠标志序列01111110找帧边界靠比特填充保证数据里不会碰巧出现这个序列。发送端每遇到 5 个连续的 1就硬塞一个 0 进去;接收端看到 5 个 1 后面跟一个 0就把这个 0 拿掉。这套机制在异步链路上是不用的异步链路靠字节填充也就是我们后面在 PPP 里会详细说的7D转义。字段长度作用常见取值Flag1 字节帧定界0x7EAddress1~N 字节标识从站或方向0x01、0x03、0x0F、0xFFControl1~2 字节帧类型与序号确认0x00、0x03、0x63、0x83Information变长承载数据任意FCS2 或 4 字节差错检测CRC-16 或 CRC-32Flag1 字节帧定界0x7E地址字段有个容易忽略的设计:每个字节的最低位是扩展位为 1 表示这是地址的最后一个字节为 0 表示后面还有。所以一个字节里真正能用的只有 7 位最多 127 个地址。单字节场景下大家习惯直接用0x01、0x03、0x0F这些值其实它们的二进制最低位都是 1正好满足结束语义。控制字段是 HDLC 的灵魂下一节单独拆。这里先把 FCS 说清楚:标准 HDLC 默认用 CRC-16多项式是 x^16x^12x^51反转实现对应0x8408初值0xFFFF算完取反。PPP 的 FCS-16 跟这个完全一致就是 CRC-16/X-25 那个变体。如果你自己写解析器算出来的 FCS 是0x1234那在线路上传输的顺序是先0x34再0x12——低位字节在前这个细节坑过不少人。1.3 PPP 在 HDLC 壳子上动了哪三处手术PPP 的正式定义在 RFC 1661 里它明说了自己是HDLC-like framing也就是说定位上就是个 HDLC 的变种不是另起炉灶。但它在三个地方做了实质性改动每一处都对应着工程上的一个痛点。第一处是把地址和控制字段固定成FF 03。标准 HDLC 的地址用于多点链路上区分从站控制用于序号确认和流控而 PPP 的实际部署场景几乎清一色是点对点——一根线两头连两个设备没有第三方。既然只有对端地址就没有区分意义;既然上层协议IP自己有可靠性机制链路层的序号确认反而成了累赘。所以 PPP 干脆把这 2 个字节的值写死然后在 ACFC地址控制字段压缩协商成功后这 2 个字节连发都不用发。你抓包时如果发现帧头是7E 21 45这种别怀疑那就是 ACFC 生效了0021是 IPv4 的协议号。第二处是加了 2 字节的协议字段。这一刀下去PPP 从一条链路跑一种协议变成了一条链路跑多种协议。链路上同时可能有 LCP 的协商帧、CHAP 的认证帧、IPCP 的地址分配帧、IPv4 的数据帧、IPv6 的数据帧全靠协议号区分。这种设计带来的直接好处是:认证、地址分配、链路质量监测这些功能可以各走各的协议号互不干扰也不需要额外开一条控制通道。第三处是引入了完整的链路控制协议族。LCP 负责链路本身最大接收单元、认证方式、魔术字、压缩选项认证协议负责身份校验NCP 负责具体网络层的参数协商。这三层是串行执行的任何时候某一层失败了链路就停在那个阶段后面的都进不去。这就是为什么排错时必须先看日志停在哪个阶段——这个后面会展开。2. HDLC 的控制字段读懂它在说什么比记住帧格式有用2.1 I 帧、S 帧、U 帧的判别只看最低两位控制字段的判别规则简单到有点反直觉:不用查表看最低两位就行。最低位是0这是 I 帧信息帧承载用户数据同时捎带序号确认最低两位是01这是 S 帧监控帧只做确认和重传请求不带数据最低两位是11这是 U 帧无编号帧用来建链、拆链、传无编号信息I 帧的格式里N(S) 是发送序号N(R) 是接收序号也就是我期待你发下一个的编号P/F 位在中间。3 位序号构成模 8 的窗口最多 7 个未确认帧;7 位序号构成模 128 的窗口这个在 LAPB 的扩展模式下才用。S 帧有四种用中间两位区分:00是 RR接收就绪01是 REJ拒绝请求重传从 N(R) 开始的帧10是 RNR接收未就绪相当于给对端一个流控信号11是 SREJ选择拒绝只请求重传某一帧。现场能见到的绝大多数是 RRRNR 偶尔在设备缓冲区打满时出现REJ 和 SREJ 基本意味着链路质量出问题了。U 帧的编码空间最大也最杂。建链用0x83SNRM或者0x93SNRME带扩展序号对应的响应是0x63UA无编号确认。拆链用0x43DISC响应同样是0x63。传无编号信息的用0x03UI测试链路用0xE3TEST对端必须原样返回。这些都是通过那 5 位 M 编码bit 2~4 加上 bit 5~7组合出来的不是随便定的。2.2 用 0x03 和 0x63 判断一次建链的握手节奏我排查过一次串口链路起不来的问题现象是设备指示灯显示链路 UP但上层业务完全不通。抓了一段码流看到的是这样的循环:- 7E 01 83 ... 7E (SNRM请求建链) - 7E 01 63 ... 7E (UA确认) - 7E 01 03 00 ... 7E (UI发数据) - 7E 01 03 00 ... 7E (UI发数据)83和63这一对出现说明链路层的握手已经完成了对端给了 UA。后面的03是 UI 帧说明数据也在发了。那问题就不在链路层得往上看。最后查出来是上层配置的地址映射错了链路层本身没问题。这个案例的教训很直接:UA 出现 HDLC 层面 OK别在这个层面上继续耗。反过来如果看到的是一直重发83而没有63那就是对端根本没响应。原因可能是波特率不匹配、接线反了TX 和 RX 交叉、对端没上电、或者对端配置成了 DCE 而你这端也是 DCE。如果83发了之后收到的是43DISC那说明对端在主动拆链可能是它那边认为链路状态异常或者配置层的链路保持没开。还有一个容易误判的情况:收到的63是周期性的每秒一次但业务数据始终不通。这种心跳型 UA通常是设备在做链路层的心跳维持只证明物理链路活着不证明上层通。看到这种情况要立刻放弃在链路层找原因。2.3 厂商私有扩展把地址、控制字段降级成固定值标准 HDLC 的那套序号确认机制在实际部署中经常被架空。很多厂商在实现自己的串口封装时会把地址字段固定成0x0F、控制字段固定成0x00然后在后面插入一个 2 字节的类型字段直接照抄以太网的 EtherType 值:类型值含义0x0800IPv40x86DDIPv60x8035链路保活私有0x0806ARP这种做法的逻辑很实在:链路层既然不需要寻址和流控那这两个字段就是浪费;而上层协议的类型信息必须要传那就借以太网的类型号体系抓包软件也能直接识别省得自己再造一套。代价是保活帧的格式变成了私有约定。你会看到这样的帧:7E 0F 00 80 35 00 00 00 00 00 00 00 7E解码出来是一堆零长度很短没有 IP 头。它的实际内容是序列号和链路状态按秒递增对端连续若干次收不到就认为链路断了。由于这个格式没有公开标准抓包软件只会把它当成未知协议显示成原始字节。这里有个实操上很容易踩的坑:厂商私有 HDLC 的保活间隔通常默认 10 秒连续 3 次丢失判定 down也就是 30 秒。如果你的链路本身有间歇性误码30 秒的容错窗口其实很窄。把间隔调大、次数调多能显著减少链路闪断的假告警代价是真正的断链要更晚才发现。这个取舍在做现场调试时必须和业务侧确认清楚。3. PPP 建链的五个阶段每一次协商失败都停在某个固定的位置3.1 LCP 阶段魔术字、MRU、ACCM 三个选项决定后面能不能跑通PPP 的状态机是严格串行的:Link Dead——物理层还没起来Link Establishment——LCP 协商中Authentication——认证中可选Network-Layer Protocol——NCP 协商中比如 IPCPLink Termination——拆链第一步 LCP 是所有事情的前提。双方互相发 Configure-Request里面塞一堆选项对端要么回 Configure-Ack全接受要么回 Configure-Nak部分值不接受建议改要么回 Configure-Reject这个选项我不认别发了。三个响应里 Nak 和 Reject 是最容易让人迷惑的因为它们看起来都像失败其实 Nak 是能谈改个数Reject 是这条免谈。选项号名称典型值作用1MRU1500最大接收单元2ACCMFF FF FF FF异步控制字符映射决定哪些字节要转义3认证协议0xC023/0xC223PAP 或 CHAP5魔术字随机值检测链路环路7协议字段压缩无值省掉 1 字节协议号8地址控制字段压缩无值省掉FF 039FCS 备选32 位改用 CRC-32魔术字这个选项值得多说两句。它的作用只有一个:检测自己跟自己说话。如果一端发出的魔术字在收到的 Configure-Request 里又出现了同样的值说明链路成环了必须拒绝。这个机制在串口自环、交换机环路、配置错误的桥接场景下能救命。我在一次实验室调试中遇到 PPP 起不来、日志刷屏的情况最后发现是把两个端口用一根线短接了TX 接到 RX自己发的包自己收魔术字直接把环路抓出来了。ACCM 是另一个隐藏的坑。它的默认值是FF FF FF FF意思是所有小于0x20的控制字符都要转义。这导致传输效率明显下降因为你的数据里只要出现换行、制表符这些字节就会膨胀成两个字节。协商成00 00 00 00可以彻底关掉这层转义前提是链路上不存在会把控制字符吃掉的老式设备比如某些早期的调制解调器会把XON、XOFF当成流控信号。这个字段在抓包里表现为 LCP 选项里一个 4 字节的值看到FF FF FF FF就知道还在用最保守的配置。3.2 认证阶段PAP 与 CHAP 在抓包里长得完全不一样认证不是 PPP 的必选项但一旦 LCP 里协商了认证协议就必须走完。PAP 只有两个报文:Authenticate-RequestCode 1里明文携带用户名和密码Authenticate-AckCode 2表示通过Authenticate-NakCode 3表示拒绝。因为密码不加密抓包软件能直接把用户名密码打印出来。现场看到 PAP 就要意识到这一点:这条链路上的凭据是裸奔的。CHAP 是三次握手。服务端先发 ChallengeCode 1带一个随机串和 Identifier;客户端算出MD5(Identifier || 密码 || 随机串)放进 ResponseCode 2发回去;服务端用自己存的密码算一遍比对一致就回 SuccessCode 3否则回 FailureCode 4。整个过程密码不过线而且 Identifier 和随机串每次都变重放拿不到好处。对比项PAPCHAP握手次数23密码是否过线是明文否只传摘要抓包能否直接看到密码能不能能否重复认证建链时一次可周期性重新挑战常见使用场景老旧设备、调试期生产环境排错时判断卡在认证阶段有个简单办法:看日志里有没有出现PAP authentication failed或者CHAP authentication failed这样的字样。如果有问题一定在凭据文件或者服务端配置上跟链路质量、MTU、路由统统无关。如果日志只说Peer is not authorized或者干脆只有一句链路断开那通常是服务端配置了认证要求但客户端压根没配凭据LCP 谈完就被踢了。还有一个细节:CHAP 的密码不能超过一定的长度限制而且某些实现对用户名大小写敏感。我遇到过客户端用大写用户名、服务端配的是小写MD5 算出来的摘要永远对不上日志里只有CHAP authentication failed查了半小时才发现是大小写。这类问题在任何文档里都不会写只能靠经验。3.3 NCP 阶段IPCP 怎么把地址和 DNS 塞给对端认证过了之后链路还不能跑 IP得先协商网络层参数。IPv4 用的是 IPCP报文格式跟 LCP 一模一样Code、Identifier、Length、Options只是选项号不同。IPCP 选项号名称长度2IP 压缩协议变长3IP 地址6129主 DNS6131备 DNS6IP 地址选项的语义要分方向看:一端请求0.0.0.0意思是请你给我分配一个地址;一端请求具体的地址意思是我希望用这个地址。服务端收到0.0.0.0的请求回一个 Configure-Nak里面写上要分配的地址客户端再用这个地址重发 Configure-Request服务端回 Ack协商完成。DNS 选项129/131不在最初的 IPCP 标准里是后来补充的。很多数传模块在协商 IPCP 时会主动带上这两个选项把运营商分配的 DNS 推给对端。如果你的设备始终拿不到 DNS先看 IPCP 的 Configure-Request 里有没有带 129没带的话就得手动配别指望协商能给你。NCP 阶段最常见的失败是地址冲突或者服务端不分配地址。前者表现为 IPCP 反复 Nak后者表现为协商一直停在请求状态。还有一种更隐蔽的情况:IPCP 协商成功了对端地址也拿到了但网卡上没看到路由。这时候要检查的是有没有配defaultroute以及 pppd 有没有权限去改路由表。4. 搭一套能复现的实验环境用虚拟串口把 pppd 跑起来4.1 用 socat 造一对串口绕开物理设备想搞懂 PPP 的协商过程最快的方法是自己跑一遍。没有物理串口也没关系用 socat 造一对虚拟串口把两端桥起来。socat -d -d \ pty,raw,echo0,link/tmp/ttyV0 \ pty,raw,echo0,link/tmp/ttyV1这条命令会在/tmp下生成两个软链接/tmp/ttyV0和/tmp/ttyV1分别指向两个伪终端两端之间的数据是互通的。往ttyV0写的东西会从ttyV1读出来反之亦然。raw,echo0这两个参数很关键前者关掉终端的行缓冲和各种特殊字符处理后者关掉回显——如果不关回显你写进去的字节会被原样吐回来pppd 会以为链路成环了。权限上socat 创建的 pty 属主是当前用户pppd 如果以普通用户身份跑倒还好如果要改路由表就得上 root。建议的做法是先用普通用户把协商跑通确认配置没问题再考虑用 root 起服务。这里有个必须提前说的坑:pppd 打开一个串口设备时会尝试把它设成 PPP 线路规程N_PPP。真串口支持这个操作但伪终端不一定支持你会看到类似Couldnt set line discipline的报错然后直接退出。遇到这个情况有两条路:一是接受这个报错改用notty选项让 pppd 走标准输入输出再用 socat 的EXEC把两端接起来;二是干脆找两台真实设备或者两个 USB 转串口模块交叉对接这样最接近现场环境。notty模式的基本形态是这样:socat \ EXEC:pppd notty noauth 10.0.0.1:10.0.0.2 debug \ EXEC:pppd notty noipdefault 10.0.0.2:10.0.0.1 debug两端各自在一条管道上跑 pppdsocat 负责把两条管道的字节流对接起来。这种写法绕过了线路规程的问题纯靠字节流跑也能完整观察到 LCP、认证、IPCP 的过程。4.2 pppd 的两端配置与那次线路规程报错假设用虚拟串口方案服务端和客户端的启动参数大致是这样:# 服务端 pppd /tmp/ttyV0 115200 \ 10.10.0.1:10.10.0.2 \ local noauth \ mtu 1400 mru 1400 \ lcp-echo-interval 10 \ lcp-echo-failure 3 \ debug nodetach # 客户端 pppd /tmp/ttyV1 115200 \ noipdefault \ mtu 1400 mru 1400 \ lcp-echo-interval 10 \ lcp-echo-failure 3 \ debug nodetach10.10.0.1:10.10.0.2这个写法是 pppd 特有的语法冒号前是本端地址冒号后是给对端分配的地址。local的意思是不修改串口的线路设置在很多非标准串口上不加这个参数会直接失败。noauth表示不要求对端认证本地实验用得上生产环境千万别这么配。nodetach让 pppd 留在前台debug打开详细的协商日志。这两个组合起来你能看到每一个 LCP、IPCP 报文的收发比看系统日志直观得多。启动之后正常情况下会看到类似这样的输出:sent [LCP ConfReq id0x1 mru 1400 asyncmap 0x0 magic 0x8a3c1d2f] rcvd [LCP ConfReq id0x1 mru 1400 asyncmap 0x0 magic 0x5b7e9c11] sent [LCP ConfAck id0x1 mru 1400 asyncmap 0x0 magic 0x5b7e9c11] rcvd [LCP ConfAck id0x1 mru 1400 asyncmap 0x0 magic 0x8a3c1d2f] sent [IPCP ConfReq id0x1 addr 10.10.0.1] rcvd [IPCP ConfReq id0x1 addr 0.0.0.0] sent [IPCP ConfNak id0x1 addr 10.10.0.2] rcvd [IPCP ConfReq id0x2 addr 10.10.0.2]这段日志的信息量很大:两个 ConfReq 交叉发出、互回 Ack说明 LCP 完成;客户端发addr 0.0.0.0服务端回 Nak 并指定10.10.0.2客户端接受后重发这是 IPCP 的标准流程。如果卡在某一对 ConfReq 之后没有 Ack那就是对方不接受你的选项得看它回的 Nak 或 Reject 里写了什么。要加认证的话凭据文件是这样的:# /etc/ppp/chap-secrets服务端 user01 * 你的密码 10.10.0.2 # /etc/ppp/chap-secrets客户端 user01 * 你的密码服务端启动参数里加auth require-chap客户端加user user01。注意客户端不要加authauth是我要求对端认证客户端是被认证方加了反而会要求服务端也来一套。4.3 用 pppd record 把原始码流落盘再用抓包工具读光看日志只能知道协商了什么看不到线上跑了什么字节。pppd 有个record选项能把串口的原始字节流完整写进文件:pppd /tmp/ttyV0 115200 noauth local \ record /tmp/ppp-trace.bin \ debug nodetach这个文件里有完整的 PPP 帧但因为链路是异步的字节之间做了填充转义直接看是十六进制乱码。用抓包软件打开时要把封装格式选成支持方向的 PPP 形式——pppd 记录的文件在每段数据前会加一个方向字节用来区分收和发。如果封装选错了你会看到所有帧都被判定成格式错误FCS 校验全红。这个技巧的价值在于事后分析。现场环境不允许你架一台机器盯着看但record可以一直开着出问题之后把文件拿回来慢慢分析。我一般会在文件开头记下时间戳和配置参数过几天再回来看也能对上。另一个用途是验证字节填充的效果。把 record 下来的文件和 pppd 日志里显示的逻辑帧长度对比一下如果物理字节数明显大于逻辑长度说明 ACCM 的转义开销很大可以考虑协商压缩。这个数字在链路带宽紧张的场景下很实在比如 9600 波特的链路上10% 的转义开销就意味着实际吞吐掉了 10%。5. 现场调过的五个问题认证、填充、MTU、保活、串口抢占5.1 认证失败但日志只有一句链路已断开这个问题在数传模块上极其常见。现象是模块上电后拨号成功、LCP 完成然后链路断开日志里没有任何authentication failed字样。注意pppd 在认证失败时的日志级别依赖debug如果没开debug很多细节会被吞掉只剩一句笼统的Peer is not authorized。排查链路是这样的:步骤检查内容判据1日志里有没有 LCP ConfReq 的认证选项有auth选项说明对端要求认证2凭据文件是否被读取用debug看有没有using password from ...3用户名是否完全一致包括大小写和引号4认证协议是否匹配服务端require-chap客户端不能只配 PAP5密码里有没有特殊字符引号和反斜杠需要转义第 4 条是最容易出错的。如果服务端配了require-chap客户端只支持 PAPLCP 协商时服务端会拒绝客户端的认证协议选项或者接受之后客户端压根不会算 CHAP 摘要直接超时。反过来如果服务端只要 PAP而客户端默认发 CHAP也会卡住。p 参数用来强制指定客户端使用的认证方式不清楚对端要求时可以先两个都试一遍。5.2 字节填充让帧长对不上FCS 校验失败异步链路的 PPP 在发帧之前要做字节填充:数据里出现的0x7E要变成0x7D 0x5E0x7D变成0x7D 0x5D另外 ACCM 里置位的控制字符默认全部也要变成0x7D加原字节异或0x20。收端反过来做一遍。这套规则导致一个现象:发送的逻辑帧是 20 字节线上实际跑了 28 字节。如果你用抓包软件分析 record 文件时没有开启异步填充解码FCS 会全部校验失败因为它算的是填充后的字节。排查这类问题的顺序是:先确认抓包工具的封装格式选对了再确认 ACCM 的值。如果帧里频繁出现7D而且分布很有规律比如每个换行符之前都有一个那就是 ACCM 没协商成零。有些老设备不会主动发 ACCM 选项pppd 会用默认的FFFFFFFF这时候只能靠对端也支持协商才能改。还有一个更隐蔽的情况:FCS 算出来始终差一点点。原因可能是 CRC 的初始值或者最终取反的问题。自己写解析器时标准的做法是初值FFFF、按位右移、多项式8408、最后取反。少做任何一步都会得到错误的结果而且错误的结果看起来还挺像那么回事。def ppp_fcs16(data: bytes) - int: fcs 0xFFFF for byte in data: fcs ^ byte for _ in range(8): if fcs 1: fcs (fcs 1) ^ 0x8408 else: fcs 1 return fcs ^ 0xFFFF这个函数算出来的是 16 位整数往线上放的时候低字节在前。验证方法很简单:一个只包含FF 03 C0 21的帧算出 FCS 之后拼上去再整体过一遍这个函数结果应该是0xF0B8这是 CRC-16/X-25 的魔法残留值任何正确的帧加上正确的 FCS 再过一遍都会得到这个数。这个自检技巧我经常用来验证解析器的正确性。5.3 MTU 没对齐ping 能通传文件就卡死PPP 的默认 MRU 是 1500很多设备也会老实地协商 1500。但链路的实际承载能力未必有这么大。比如某些工业模块的串口缓冲区只有 1KB 左右超过这个长度的帧会被截断表现出来就是小包通、大包丢。诊断方法是逐步逼近:ping -M do -s 1400 10.10.0.2 ping -M do -s 1472 10.10.0.2-M do禁止分片-s指定数据长度加上 28 字节的 IP 和 ICMP 头1400 对应 1428 字节的帧。如果 1400 通、1472 不通说明 MTU 大概在 1472 以下具体值要一点点试。找到临界点之后把 MTU 设成临界值减 20 留余量比如临界 1450 就配 1430。在 pppd 里配mtu 1430 mru 1430两端要一致或者客户端不大于服务端。另外别忘了 TCP 的 MSS 也需要调整否则大文件传输时上层照样会发出超长报文被路由器丢弃后触发分片。还有一个相关的坑:IPCP 协商出来的地址是私网地址但设备上还配了一条默认路由走了别的出口。这种情况下 ping 对端能通走 PPP 接口但访问远端业务不通走了错误的默认路由。检查ip route输出确认目标网段确实指向了ppp0。5.4 保活超时与空闲断链LCP 的 Echo-Request/Echo-Reply 是 PPP 用来检测链路存活的机制。lcp-echo-interval控制发送间隔lcp-echo-failure控制连续多少次没响应就拆链。默认是 0 和 0也就是不发探测。很多现场配置成 10 秒和 3 次也就是 30 秒无响应就断。这个机制在链路质量差的时候会误伤。串口链路上偶尔的误码可能导致几个 Echo 包丢失累积到 3 次就断链然后重连业务跟着抖。我的做法是把 interval 调大30 秒、failure 调大5 次把容错窗口拉到 150 秒。代价是真断链时恢复慢但对大多数周期性上报的业务来说这个延迟可以接受。另一种空闲断链跟 LCP 没关系是运营商侧的策略。蜂窝数据链路如果一段时间没有流量网络侧会主动释放承载设备端的 ppp 接口就消失了。应对办法是配置persist让 pppd 自动重连同时加一个定时任务定期发一个小包维持链路活跃。这个包不需要多大几百字节足够频率看运营商策略一般几分钟一次。判断是哪种断链看日志里的拆除顺序:LCP 主动发了 Terminate-Request 的是本地拆的;收到 Terminate-Request 的是对端拆的;什么都没有接口突然消失的通常是底层承载没了pppd 感知到串口异常后退出的。5.5 串口设备被系统服务抢占这个问题跟协议没关系但杀伤力很大。现象是手动跑 pppd 一切正常配成开机自启之后就打不开设备日志报Couldnt open the serial port。原因是系统里有些服务会主动扫描/dev/ttyUSB*和/dev/ttyACM*试图识别接入的设备。它一旦打开这个设备你的 pppd 就没法独占访问了而且它还会往串口里写一些探测指令把对端设备搞得一头雾水。处理办法是三步:先找到占用设备的进程用fuser或者lsof看谁打开了它;然后把那个服务对当前设备的扫描禁掉通常是加一条 udev 规则或者在服务配置里排除;最后在 pppd 启动脚本里加一个等待逻辑等设备节点稳定之后再启动避免上电瞬间设备还没枚举完就去打开。# 查看谁占用了串口 lsof /dev/ttyUSB0 fuser -v /dev/ttyUSB0 # 等设备出现再启动 while [ ! -e /dev/ttyUSB0 ]; do sleep 1; done pppd /dev/ttyUSB0 115200 ...这个等待逻辑看起来多余但在冷启动场景下非常必要。USB 枚举的耗时不确定快的时候一两秒慢的时候十几秒写死的sleep 5总有翻车的时候。最后再提一点个人体会:调试链路层问题时最容易犯的错是跳级。看到业务不通直接去查应用日志、查防火墙、查路由唯独不去看链路层到底有没有起来。我的习惯是先确认三层握手到哪一步:LCP 完成了没有、认证过了没有、IPCP 拿到地址了没有。这三个问题答清楚排查范围立刻缩小一大半剩下的基本都是配置细节而不是玄学。