前言
起因是广东联通这条宽带无法通过 UPnP 让 BRAS 打开端口映射,IPv6 一侧又出现了手机访问软路由一直转圈的问题。两件事看着不相干,但都指向运营商的边界设备,于是打算把它探一遍,看看对面挂的到底是什么设备、跑了什么协议。这次探测的目的不是找漏洞,只是想搞清楚自己这条线路上到底发生了什么。全程只针对自己名下的这一条宽带,没有扫别人。
这类症状可以指向很多原因:路由器配置、防火墙策略、运营商 NAT,甚至设备本身。凭症状猜是没意义的,唯一可靠的办法是把运营商那侧的设备当成一个黑盒,逐层测它的行为。这次用的工具是 PyCharm 和 Python,手上能用的资源有:
- 一条联通广州家庭宽带,PPPoE 接入
- 一台 4 口 x86 软路由(iStoreOS 25.12.5),WAN 口直连运营商
- 一台腾讯云 VPS(公网 IPv6 出口)
- 一张联通流量卡(手机)
但第一件事不是发探测包,是先把地图画出来。
结论
从物理位置到协议行为,一共测了五层,结论如下:
| 层次 | 结论 | 证据强度 |
|---|---|---|
| PPPoE 发现层 | 双 BRAS 热备,华为设备,AC-Name 已提取 | 实测 |
| 认证层 | 账号并发上限 = 1,不支持多拨 | 实测(AAA 原文) |
| 入向过滤层 | 不存在 TCP 标志级过滤,56/56 全到达 | 实测(含 IPv4 对照) |
| NAT 类型 | 对称型 NAT4,4 组独立复现 | 实测 |
| 端口控制协议 | 运营商侧 PCP / NAT-PMP 均无响应 | 实测(含对照组) |
| IPv6 互通 | 固网↔移动单向不可达 | 实测(抓包定位) |
表里每一个「实测」,后面都有对应的报文原文、抓包或对照实验,下面逐层展开。
第零层:先把地图画出来
本机相对位置
Get-NetIPConfiguration | Select-Object InterfaceAlias, IPv4Address, IPv4DefaultGateway
Get-NetRoute -AddressFamily IPv4 -DestinationPrefix '0.0.0.0/0'

结果:192.168.100.237/24,网关 192.168.100.1。标准家庭网段,没有意外。
软路由内部
登录软路由(SSH 走 22822 端口,不是默认的 22),第一轮只读侦察:
ip -4 addr show; ip -6 addr show scope global
ip route; ip -6 route
ip -br link

关键发现:
pppoe-wan: inet 10.1.151.74 peer 10.1.128.1/32
inet6 2408:8256:a6c:f718:…:1dae/64
这是第一条硬事实:WAN 侧是 PPPoE 点对点,对端 10.1.128.1。两端都在运营商 10.0.0.0/8 城域网内,这就是 CGNAT 的内侧。同时 IPv6 拿到了全球可路由的 /64,说明运营商在这条线路上没做 NAT66。这一点后来变成整场调查的关键。
出口在哪
curl -s http://ip.3322.net

返回 58.249.143.142(联通广东)。与 PPPoE 会话地址 10.1.151.74 明显不同,确认 CGNAT 边界就在 BRAS 上。
traceroute 印证了这一点:

从 BRAS 直接跳到公网,中间没有第二层 NAT。
第一层:从 PADI 里挖出 BRAS 身份
为什么选 PADI
计算机网络当中,识别对端设备,常规的手段是端口扫描加服务指纹。但 BRAS 几乎不会开放端口,扫描反而会触发安全告警。PPPoE 恰好提供了一个更好的入口:发现阶段是明文、且无需认证。
PADI (广播) → PADO (BRAS 应答) → PADR (单播) → PADS (分配 Session-ID)
只要发一个 PADI,BRAS 就会返回一个 PADO,而 PADO 里带着 AC-Name(Access Concentrator Name),那是设备的自报家门。
实现
用 AF_PACKET 裸套接字手工构造以太网帧:

核心就几行:Service-Name 留空、Host-Uniq 随便填,以太类型 0x8863,目的 MAC 全 ff。
完整的探测脚本把发现阶段的报文类型和 tag 都定义好了,另外还得用 SIOCGIFFLAGS 打开混杂模式,否则收不到 BRAS 回的 PADO:

解析回来的 PADO,重点看 0x0102(AC-Name)和 0x0101(Service-Name)两个 tag。
结果
一次探测,收到了两个 PADO:
| AC-Name | 源 MAC |
|---|---|
b1-s-gdgz-gq | 08:19:a6:f4:11:5c |
b2-s-gdgz-xt | 8c:fd:18:f5:72:bb |
两条结论同时拿到:
- 存在两台 BRAS,同时响应同一个客户端,可以理解为做了双机热备,会话可热备切换。
- 命名规则可读:
gdgz= 广东广州,gq/xt为局站代号,b1/b2疑似机框或板卡编号(此项为推断)。
后来通过 PPPoE 会话表确认,当前会话挂在 08:19:a6:f4:11:5c(即 b1)上,另一台空闲。
这个方法的边界
PADI 只能拿到 AC-Name 和 MAC,拿不到型号。想要型号得靠 TCP 栈指纹,而 BRAS 的 22 端口 DROP、其余端口 RST,连接建立不起来,抓不到 SYN-ACK 的 TCP 选项。所以型号这一层,到此为止。
第二层:认证面与多拨能力
一次险些误判
第一个想法:同账号能否建多个 PPPoE 会话(多拨叠加带宽)。直觉做法是再发一次完整的 PADI→PADR,看能否拿到第二个 Session-ID。于是照做,三次全部超时(PADS 无响应)。差点就此写下「BRAS 拒绝多会话」的结论。
但那是错的。复查代码发现:我把 PADR 发成了广播(目的 MAC = ff:ff:ff:ff:ff:ff)。PPPoE 规范里只有 PADI 是广播,PADR 必须单播给选定的那个 AC。BRAS 收到广播 PADR 直接忽略,不会回 error,静默丢弃。修正为单播后,结果完全反转:
Session-ID=0x7E2A (b1-s-gdgz-gq)
Session-ID=0xB719 (b2-s-gdgz-xt)
Session-ID=0x4811 (b1-s-gdgz-gq)
这是本次调查最有价值的一次教训:一个字节的差异(广播 vs 单播),把「拒绝」变成了「允许」,方向完全相反。而错误答案的表现形式是「超时」,和真实的过滤行为一模一样。
分层设计:把风险切开
发现层允许 ≠ 账号允许。真正的并发限制在 CHAP 认证层。而贸然做第二次认证,有可能触发「后拨顶前拨」把主会话踢掉。
所以设计成三个阶段,风险递进:
| 阶段 | 动作 | 风险 |
|---|---|---|
| stage1 | 只做发现层(PADI/PADR/PADS) | 零账号风险 |
| stage2 | 完整 PPP 认证,但用错误口令 | 不建立账号会话 |
| stage3 | 完整 PPP 认证,用真实口令 | 唯一可能触发顶号的阶段 |
隔离手段:
- 测试流量走独立 macvlan(伪造 MAC
02:e2:59:00:31:ae),与生产 MAC00:e2:59:00:31:ae完全分离 nodefaultroute+nopersist,不抢默认路由、不循环重试- 全程有看门狗监控生产会话,掉线立即记录并触发重拨
- 退出即
PADT拆除 + 删除 macvlan
AAA 错误码把答案写在了脸上
stage2(错误口令):
rcvd [CHAP Challenge id=0x1 <40d9...>, name = "Huawei"]
rcvd [CHAP Failure "29;User(...)'s Authen Attrib(ai-Service-Password) Check Error,
Request Deny by gz.cncnet.net"]
stage3(真实口令):
rcvd [CHAP Failure "21;User(...) Checking LM,
(online count=1)>=(Max num=1)., Request Deny by gz.cncnet.net"]
两个错误码不同,这一点本身就是结论:
29;= 密码校验失败21;+(online count=1)>=(Max num=1)= 并发会话数超限
stage3 返回 21 而不是 29,说明凭据是对的、通过了密码校验,然后才被并发策略挡下。
结论:该账号最大并发会话数 = 1,不支持多拨。
附带结论:运营商策略是「拒绝新会话」,不是「后拨顶前拨」,主会话在整轮测试中 Session-ID 始终为 0000C03B,未受影响。
顺带拿到设备厂商:CHAP Challenge 的 name = "Huawei"。华为。
第三层:入向过滤探测
问题定义
想验证一个假设:BRAS 是否对入向的 TCP SYN+ACK 和 UDP 报文做差异化拦截。
这个问题的难点不在发包,在判据。「没收到响应」和「被拦截」是两件不同的事。
拦截成立必须同时满足三个条件:
- 观测点在 netdev 层(早于目标主机的 netfilter),否则测到的是主机防火墙
- 发送侧可确认已发出、且源侧无过滤
- 有同源对照,只改一个变量(标志位 / 协议)
第 1 条决定了工具选择:必须用 tcpdump,而不是 nft 的 counter。因为 tcpdump 挂在 netdev 上,报文能在这里出现,就证明路径放行了;后续被本地防火墙丢弃不影响这个判断。
发送端设计
12 种 TCP 标志组合加 10 个 UDP 端口,每个探测独占一个源端口(从 41000 递增),接收端据此归因,不需要解析载荷。
标志矩阵特意包含畸形组合:
SYN / SYN+ACK / ACK / FIN / FIN+ACK / RST / PSH+ACK
FIN+SYN / FIN+PSH+URG(XMAS) / NULL(无标志) / SYN+FIN+RST / ACK+URG+PSH
TCP 头必须手工构造,其中有个坑:data-offset 与 flags 是一个 16 位字段,不是两个字节。写成两个 B 会让 off_fl 被截断,报文直接构造错误。
IPv6 的校验和是强制项,伪首部算法:
def csum6(src, dst, nxt, payload):
pseudo = src + dst + struct.pack("!I", len(payload)) + b"\x00\x00\x00" + bytes([nxt])
return csum(pseudo + payload)
校验和算错 → 目标静默丢弃 → 和「被过滤」表现完全一致。
结果:56/56 全部到达
接收端在软路由 WAN 口抓包,源地址过滤只抓 VPS 发来的报文。2 轮 × 28 个探测:
proto flag sent recv rate verdict
tcp SYN 6 6 100% delivered
tcp SYN+ACK 6 6 100% delivered <- 核心怀疑项
tcp NULL 2 2 100% delivered <- 无任何标志
tcp FIN+PSH+URG 2 2 100% delivered <- XMAS 树
tcp SYN+FIN+RST 2 2 100% delivered
udp dport=53 2 2 100% delivered
udp dport=50000 2 2 100% delivered
22 种探测类型,全部 100% 到达,无一例外。
连 NULL(零标志)和 SYN+FIN+RST(自相矛盾)这种畸形组合都完整到达了网卡。
结论:BRAS 对 IPv6 入向报文没有任何标志级过滤。 原假设不成立。
IPv4 对照:把因果锁死
如果只测 IPv6,会有人说「那是因为你 IPv6 正常,IPv4 说不定被拦了」。
所以加一组对照:同一台 VPS、同一套矩阵、同一时间窗,目标是 CGNAT 出口 58.249.143.142。
探测报文(sport 41500-41511) → 0/12 到达
同路径有状态回包(443、22222) → 正常畅通
同一条物理路径上,有状态流量过、无状态主动发起不过。
这个对比把因果锁死了:丢包点在 NAT 映射查询,与报文标志无关。如果 BRAS 真有标志过滤,IPv6 侧(无 NAT、无法用映射缺失解释)必然会暴露它,而那里是 100%。
第四层:NAT 类型与端口控制协议
STUN 判定原理
从同一个已绑定的本地 UDP 端口,依次向多个不同地址的 STUN 服务器发 Binding Request,比对各自返回的 XOR-MAPPED-ADDRESS:
- 不同目的地 → 映射不变 = 锥形 NAT
- 不同目的地 → 映射变化 = 对称型 NAT(NAT4)
探测脚本里准备了四个 STUN 服务器和两个 PCP 目标:

四组复现
| 序号 | 本地端口 | → miwifi | → bilibili | |
|---|---|---|---|---|
| 1 | 34788 | :6227 | :6228 | :6237 |
| 2 | 57181 | :6287 | :6288 | :6292 |
| 3 | 50051 | :6325 | :6326 | :6343 |
| 4 | 51305 | :6361 | :6363 | :6379 |
同一个本地端口,对着三个不同目的地,被分配了三个不同的公网映射端口。
四组独立复现,行为完全一致。确认对称型 NAT4。
这种 NAT 下,映射条目绑定四元组,第三方无法预测映射端口,因此 NAT 穿透(hole punching)在最常见实现下都会失败。这就是 P2P、游戏联机、视频通话受影响的根因。四种 NAT 类型的分法和判据,之前单独写过一篇:NAT 类型有哪些?对称型 NAT 是啥?
PCP 与 NAT-PMP
两者都用 UDP/5351。向运营商边界设备发标准请求:
| 协议 | 目标 | 结果 |
|---|---|---|
| PCP (RFC 6887) | 10.1.128.1:5351 | 无响应 |
| NAT-PMP (RFC 6886) | 10.1.128.1:5351 | 无响应 |
| PCP(对照组) | 192.168.100.1:5351 | 有响应(rc=12) |
| NAT-PMP(对照组) | 192.168.100.1:5351 | 有响应 |
对照组是必须的。 同一探测端、同一方法,本地网关响应正常、运营商设备超时,这就把「你工具不行」这条抗辩堵死了。
结论:运营商部署了 CGNAT,但没提供 PCP。用户没有任何标准化途径申请端口映射。
第五层:IPv6 互通故障
一个意外的发现
测试过程中,用户报告:手机(联通流量卡)访问软路由的 IPv6 地址时浏览器一直转圈。
这个现象之前单独排过一轮,过程记在联通 IPv6 打不通,客服只会说「你家没有公网 IP」里。这次手上有抓包和对照节点,能把它钉到具体方向上。
于是做了四组两两测试:
| 源 | 目的 | 结果 |
|---|---|---|
| 腾讯云 VPS | 软路由 WAN v6 | 通 |
| 腾讯云 VPS | 手机 v6 | 通 |
| 软路由 | 手机 | 不通(0/3) |
| 手机 | 软路由 | 不通(转圈) |
三方节点,只有联通内部这两段互不可达。第三方反而是唯一能同时触达双方的。
traceroute 定位断点
VPS → 手机(通)
5 2408:8001:3161:f72c::4
9 2408:8142:6000:f101:2::1 <- 移动侧
12 2408:8142:6000:f501::f <- 到达
软路由 → 手机(不通)
1 2408:8001:3010:db:: <- 有响应
2..20 * <- 再无节点响应
同一目标,两条路径的入口不同。固网侧的流量卡在 2408:8001:3010:: 之后。
抓包把方向钉死
让手机再访问一次,同时在软路由抓 src net 2408:8459::/32:
17:48:50.966 ...3d0c.36368 > ...:1dae.80: Flags [S], seq 4210686661
17:48:51.983 ...3d0c.36368 > ...:1dae.80: Flags [S], seq 4210686661
17:48:53.006 ...3d0c.36368 > ...:1dae.80: Flags [S], seq 4210686661
... 共 8 次,间隔 0.3s → 0.75s → 1.5s → 3s
手机的 SYN 到达了软路由(去程通),而同一个 seq 重传 8 次、指数退避,这是 TCP 在完全没收到任何回应时的标准行为。
再用 VPS 对照验证软路由会不会回 RST:
VPS: connect to ...:80 failed: Connection refused, after 4 ms
Connection refused 就是收到 RST。4 毫秒往返,证明软路由确实回了 RST,且跨网到腾讯云正常送达。
所以是单向的:
| 方向 | 状态 |
|---|---|
| 手机 → 软路由(去程) | 通,SYN 到达网卡 |
| 软路由 → 手机(回程) | 断,RST 送不回去 |
排除其他可能
| 假设 | 排除依据 |
|---|---|
| 源地址问题 | 强制 WAN / 强制 LAN / 默认三种源,全部 0/3 |
| 协议问题 | TCP 和 ICMPv6 都受影响 |
| 整体路径故障 | 同一出口到腾讯云、阿里都正常 |
| 手机无 IPv6 | 有全局地址,且 VPS 能 ping 通它 |
| BRAS 标志过滤 | 56/56 全到达,见第三层 |
定位:联通固网 → 移动方向的单向路由/转发缺失。
更早一次的投诉经过写在联通下发的 IPv6 地址无法被直接访问:投诉历程里。
至于具体是「路由表缺条目」还是「ACL 丢弃」——从外部无法判定。因为运营商会过滤 ICMPv6 unreachable,恰好把唯一的判别信号遮掉了。我试过抓 ICMPv6 错误消息,探测手机期间一个都没有回来。
结果总表
| 项目 | 值 | 等级 |
|---|---|---|
| BRAS 厂商 | 华为 | 实测(CHAP name) |
| BRAS 集群 | b1-s-gdgz-gq / b2-s-gdgz-xt | 实测(PADO AC-Name) |
| BRAS 接入地址 | 10.1.128.1/32 | 实测 |
| BRAS 端口策略 | 22 恒定 DROP,其余 RST | 实测(5 样本 × 低速) |
| 控制平面 | 有 CoPP,连续扫描触发 DROP 漂移 | 实测(3 次运行不一致) |
| 认证协议 | CHAP-MD5 | 实测 |
| AAA 后端 | gz.cncnet.net | 实测 |
| 并发会话上限 | 1 | 实测(online count=1 >= Max num=1) |
| 顶号策略 | 拒绝新会话,非顶号 | 实测(主会话未变) |
| IPv6 入向标志过滤 | 不存在,56/56 到达 | 实测 |
| NAT 类型 | 对称型 NAT4 | 实测(4 组复现) |
| PCP (RFC 6887) | 无响应 | 实测(含对照组) |
| NAT-PMP (RFC 6886) | 无响应 | 实测(含对照组) |
| IPv4 出口 | 58.249.141.202 / 58.249.143.142(轮换) | 实测 |
| IPv6 互通 | 固网→移动单向断 | 实测(抓包定位) |
| BRAS 型号 | 未知 | 未验证 |
这对用联通宽带的人意味着什么
三条能直接用的结论:
- 别指望多拨叠带宽。限制不在路由器,在 AAA 的
Max num=1。换光猫、换软路由、换固件都没用,这个值在运营商侧。 - 想要公网端口,只剩 IPv6 或投诉两条路。IPv4 侧是对称型 NAT4,映射端口随目的地变化,IP 和端口都猜不到;PCP / NAT-PMP 又不响应,没有标准化申请通道。这也是为什么 UPnP 在这条线上是哑的。NAT 类型怎么影响联机,游戏老是掉线、联机卡顿?先查 NAT 类型和 UPnP这篇讲得更细。
- IPv6 有公网地址 ≠ IPv6 能用。
2408:8256开头的/64是真的全球可路由,但固网到移动方向的回程是断的,手机直连家里设备会一直转圈。真要远程访问,还是走中转或者 VPS,人在外面怎么进家里的网这篇里有现成的搭法。
至于这台 BRAS 的具体型号,从外部探不到,只能到此为止。
如果你觉得这篇文章不错,有用的话可以点击下面的“赞赏”支持作者,你的支持是我更新的动力(饿饿吃饭)。


Comments NOTHING