
做运维的兄弟如果被“广东IP视频分片刷量”折磨过应该能体会那种感觉封IP第二天换一批封UA人家改个UA继续打上WAF规则攻击方直接换SDK版本。我前后和PCDN刷量流量对抗了大半年中间试过很多方案最后真正把问题压下去的是从TLS指纹层引入JA3/JA4把“按IP封、按UA封”这种被动思路彻底换成了“按身份识别、按行为积分”的主动思路。这篇文章就把整个链路拆开讲清楚为什么广东IP段的PCDN刷量这么难根除JA3/JA4到底解决了什么问题以及一套可以从零落地的精准防护方案。这套内容适合正在被刷量流量困扰的运维、安全工程师也适合带宽成本被异常抬高、想搞明白流量构成的技术负责人。文章里所有规则和配置都来自真实对抗场景可以直接抄。1. 先复盘一次典型的PCDN刷量攻击1.1 刷量事件的现象与初步处置先说一次让我印象特别深的处置过程。当时凌晨日志里突然出现大量视频分片请求流量曲线一夜之间抬上去好几个档次机房出口带宽直接爆掉。打开日志一看特征非常明显源IP高度集中在广东地区的几个运营商地址段尤其是广电出口的NAT池User-Agent大量出现gdhg-cw5100、internet_r_vid这类看起来像设备固件或SDK生成的标识请求路径全是视频分片文件单个请求体积不大但并发数和QPS高得离谱几乎不带RefererCookie为空完全不像正常用户的播放行为。第一反应肯定是封IP。我当时的操作是统计TOP源IP按IP段拉黑iptables一次性ban掉几百个地址。效果持续了大概十分钟紧接着同一批地址段里又冒出新IP继续打。这是因为广电宽带是典型的DHCPNAT架构设备重拨一下地址就换你根本封不完。后来我又把UA关键字加进WAF规则结果对方SDK里UA字符串做了参数化前脚封gdhg-cw5100后脚就出来一个gdhg-cw5101规则追着人家跑。这种“封了又来、换汤不换药”的循环本质上说明一件事你用的判定维度攻击者可以低成本伪造。IP会漂移UA是明文只有往下挖到TLS握手层才有机会拿到一个改动成本足够高的身份标识。1.2 PCDN流量劫持是怎么发生的PCDN说白了就是P2P方式的CDN分发。正规玩法是用户安装客户端贡献上行带宽平台给奖励大家双赢。但黑产的路子完全不一样。他们会把PCDN的SDK塞进运营商的机顶盒、光猫、路由器固件里或者通过固件升级流程静默植入。用户根本感知不到家里的设备就变成了一个“肉鸡节点”。这些节点平时表现得很正常但只要后台下发指令它就会向内容源站发起大量的视频分片请求。请求来的内容一部分被本地缓存复用一部分被用来构造虚假的“上行贡献量”从而骗过PCDN调度平台套取流量结算收益。源站视角看到的就是某个区域运营商出口IP持续刷量请求特征和正常用户交错在一起带宽成本蹭蹭往上涨。关键点在“劫持”两个字。这些流量不是用户主动产生的是设备在后台偷偷跑的。所以你在日志里看不到用户的登录态、没有业务Token、没有Cookie因为SDK根本不需要这些。它要的就是在最底层把内容拉起来、把流量指标做上去。明白了这个机制再看封IP为什么失效就清楚了攻击源不是一个固定服务器而是几万个分布在各家各户的联网设备出口IP是运营商NAT池动态分配的今天是这个、明天是那个UA和请求路径都是SDK可控的字符串改起来毫无成本。1.3 广东广电IP段为何成为“重灾区”很多人问我为什么偏偏是广东IP、偏偏是广电段。我不是广东广电的内部人士但从实际流量特征和行业公开信息来看原因是结构性的。第一设备基数大。广东广电体系下机顶盒、光猫的存量非常大而且很多设备是运营商统一定制、统一派发的。厂家在固件里做了什么、SDK里集成了哪些模块普通用户完全没有感知也不会去检查。基数一大被植入PCDN节点的绝对数量自然就上来了。第二网络结构特殊。广电宽带大量使用运营商级NAT出口地址池相对集中但IP和用户不是一一绑定关系。日志里看到同一个IP背后可能挂了几百个真实家庭用户。你要封这个IP极大概率把正常用户也误伤进去。第三请求特征固定。很多被植入的终端上报UA时会带上设备型号和SDK标识比如gdhg-cw5100这种来自特定光猫/网关型号的标识以及internet_r_vid这类视频SDK相关的UA前缀。虽然可以伪造但对于已经大规模铺开的老旧设备来说固件不会频繁升级UA和TLS指纹在很长时间内都保持稳定——这在后面反而成了我们识别它的突破口。第四单点流量小但总量惊人。每个家庭设备的带宽占用其实不大可能就是几百Kbps到几Mbps但几千台设备同时跑汇聚到源站就是持续的洪峰。单IP看到的值不高很容易被监控系统的阈值漏过去但整体带宽曲线会慢慢爬升直到你某天收到账单才意识到问题。2. 为什么传统封禁策略总是治标不治本2.1 IP封禁与UA封禁的脆弱性先说IP封禁。IP在这个场景里等于“门牌号”而攻击设备的门牌号是动态的。动态IPNAT出口带来的结果是同一时刻同一个源IP可以承载真实用户和攻击流量下一次攻击流量出现时源IP可能已经换了。基于IP做黑名单注定是打地鼠游戏。再说UA封禁。UA是HTTP明文头攻击SDK生成UA的成本低到可以忽略。你封gdhg-cw5100SDK下次带上GDHG-CW5100或者后面加个空格、加个版本号规则就失效了。UA只能作为辅助信号不能作为独立判定依据。还有一类人喜欢用限速或封禁特定Range分段请求。这个有一定效果但同样绕得开。SDK可以调整请求策略把分段改成整段拉取把并发降下来伪装成慢速观看。你每加一条规则对方就调整一次参数运维永远在追赶。真正稳固的判定维度必须满足两个条件一是藏在协议底层、普通请求里难以篡改二是改动成本高到攻击方不愿意为你单独修改。TLS握手层的指纹恰好满足这两点。2.2 从TLS握手层寻找“可信身份”TLS握手是整个HTTPS请求的第一环。客户端在发起ClientHello时会带上自己支持的TLS版本、密码套件列表、扩展列表、椭圆曲线、格式等参数。这些参数不是随便填的而是由客户端的TLS实现库在编译期和运行期共同决定的。浏览器有浏览器的组合Go语言写的SDK有Go的那一套OpenSSL和BoringSSL又自成体系。JA3就是把这些握手参数按照固定顺序拼接成字符串再做一次MD5哈希得到一个32位的十六进制指纹。这个指纹相当于TLS客户端的“DNA”。正常情况下同一个SDK的各个终端算出来的JA3几乎一样而正规浏览器的JA3和PCDN SDK的JA3差异非常明显。我在实际抓包里见过一个典型的PCDN节点它基于某款Go语言SDK封装ClientHello里的密码套件顺序、扩展类型和浏览器完全不一样。同一个IP上用Chrome打开的网页和后台PCDN流量各自握手的JA3完全不同。这给了我们一个很干净的切分维度同一个来源IP上用户可以放行但特定指纹必须拦截。打个比方IP是门牌号UA是着装JA3是声音。门牌号会换衣服可以改但声音特征短时间内很难伪装。2.3 JA3与JA4的差异和演进JA3虽然好用但有明显短板它把所有字段拼在一起做MD5字段间没有结构化区分一旦某个扩展顺序变化整个哈希就全变容易误伤而且它只覆盖ClientHello对服务端响应、HTTP层行为没有刻画。JA4是后来提出的新一代TLS指纹标准用四个字段分别描述TLS版本、SNI、密码套件数量、扩展数量、ALPN、指纹分片等信息。典型格式类似t13d1516h2_8daaf6152771_02713d6d1a1f_0拆开看第一段表示TLS 1.3、DH模式、1516字节ClientHello等概要信息第二段是密码套件的有序指纹分片第三段是扩展指纹最后一段表示没有ALPN等额外信息。JA4还衍生出了JA4S服务端指纹、JA4HHTTP请求头部指纹、JA4LTCP流量时序指纹覆盖了整个会话链路。放到PCDN防护场景里JA4的价值在于即使SDK做了一点参数调整导致JA3变化JA4的结构化信息依然能帮你快速判断“这个流量是什么类型的客户端发出来的”再配合JA4H看HTTP层的Range、UA、Content-Type组合识别率会更高。我在规则里同时保留JA3和JA4两条路JA3做粗过滤JA4做细判定实测误报率比单纯用JA3低不少。3. 精准防护方案从采集到联动的完整落地3.1 采集层把JA3/JA4指纹变成日志字段第一步是要能让边缘网关输出TLS指纹。方案有三个大家按自己环境选。方案ANginx nginx-ssl-ja3模块。这个模块需要把Nginx和BoringSSL配合编译编译参数大致如下./configure \ --with-compat \ --add-dynamic-module../nginx-ssl-ja3 \ --with-http_ssl_module make make install装好之后在server块里开启变量并打到access_log里server { listen 443 ssl; ssl_ja3 on; log_format ja3log $remote_addr|$ssl_ja3|$http_user_agent|$request_uri; access_log /var/log/nginx/ja3.log ja3log; }这样每条请求日志里都会带上$ssl_ja3后续用ELK或ClickHouse做聚合非常方便。方案BAPISIX/Kong等API网关。Kong有ja3插件APISIX也可以通过自定义插件在ssl阶段做指纹提取。如果你的业务已经走在网关上这是改动最小的路径不需要动Nginx源码。方案C流量镜像分析。把核心边缘节点的TLS握手流量镜像一份到抓包服务器用离线分析工具解析ClientHello并算指纹。这个方案适合第一阶段排查因为实时性差但胜在不用改生产环境能先摸清楚攻击指纹长什么样。我个人实际用的是“Nginx补丁日志采集”的组合。第一周先离线抓包把攻击流量里的JA3/JA4样本洗出来确认特征之后再给生产Nginx打上模块让指纹字段进入实时日志链路。3.2 规则层多维联合判定模型有指纹之后不要急着封禁。直接按指纹封死误伤面太大尤其广电NAT出口后面还挂着真实用户。我采用的是“安全积分”模型把多维信号折算成分数分数触发不同动作。维度命中条件分值TLS指纹JA3命中已知PCDN指纹库30UA特征命中gdhg-cw5100、internet_r_vid10IP地域源IP归属于广东广电NAT特征段20请求行为单IP QPS100且无Referer15请求路径连续Range分片视频请求10规则引擎逻辑总分小于50正常放行只记录日志50到70进入“观察区”响应头加自定义标记限速70到90进入“挑战区”302跳转一次性token校验页大于90直接返回403并在缓存里对该“指纹区域”组合执行数小时封禁。这套模型的关键是单看任何一条都不致命。IP来自广东不是罪UA长得怪也不是罪但“IP特征UA特征TLS指纹高频行为”同时命中基本可以确定是PCDN刷量。挑战机制要单独说一下。PCDN的SDK是嵌入式终端它不具备执行JavaScript的能力。你给它一个302跳转到校验页正常浏览器会跟着跳、执行JS、拿Token、回源继续播放而SDK拿到302之后要么原地丢弃、要么直接重试原地址永远不会完成校验。这一个动作就能把真实用户和攻击流量区分开比单纯封禁优雅得多。3.3 执行层观察、挑战、封禁三级处置执行层要解决“怎么拦”的问题。很多人一上来就封IP段这是最粗暴也最容易误伤的做法。我的建议是按“日志→挑战→封禁”三级递进。第一级观察限速。对命中50到70分的流量直接给源站带宽加一层限速例如Nginx配limit_req_zone $binary_remote_addr zoneflood:10m rate5r/s; server { location /video/ { limit_req zoneflood burst20 nodelay; proxy_pass http://upstream_video; } }5r/s对真实用户来说完全够用但对PCDN节点的每秒几十个分片请求来说等于断粮。限速的好处是不会产生“用户刷不出来”的投诉只是变慢。第二级挑战校验。70到90分的流量强制302到校验页。校验页种下短期Token回源请求带上Token才放行。这一步能过滤掉绝大多数不具备浏览器能力的SDK流量。第三级指纹封禁。90分以上的流量按“JA3/JA4IP地域”二元组封禁而不是只封IP。比如某个广东广电IP段在换IP但JA3始终不变那就把该IP段的这个JA3封掉后面的真实用户不受影响如果同一IP上出现了两个指纹一个浏览器、一个PCDN SDK那浏览器继续放行SDK指纹照封不误。这套三级处置的节奏很重要。不要第一天就把所有规则调到最严先观察记录一周把误伤数据拉出来看看再逐步收紧。攻击方会试探你的底线你的规则也要有灰度发布的过程。4. 实战问题排查与长期治理4.1 误伤真实用户这是所有封禁策略绕不开的坑我踩过的最大一个坑是用IP段直接封禁导致广电宽带下一整片真实用户播放异常。当时客服反馈量立刻上来了社区里开始有人骂平台“看视频卡成PPT”。后来排查发现问题出在NAT上。同一个出口IP背后有大量真实家庭用户他们有的人在用PC浏览器看视频指纹是Chrome系有的人在用手机App指纹是Android WebView系还有人家里被植入了PCDN节点指纹是SDK系。三种流量共用同一个IP。调整策略之后我做了三件事封禁对象从“IP”改成“IP指纹”二元组对带业务Cookie或登录态的请求直接跳过挑战通过验证的用户种一个12小时有效的放行Cookie。这三招下去客服投诉基本清零攻击流量依然被挡在外面。核心思路就一句话真实用户有业务特征攻击流量没有把有业务特征的放行剩下的才进入严格判定。4.2 指纹随机化对抗规则库要“养”很多人会问攻击方把SDK升级一下TLS指纹不就变了吗理论上确实可以但实际对抗中PCDN黑产有一个天然弱点SDK已经烧录在几十万台终端里终端固件的升级周期非常慢短期内让所有终端同步更换TLS栈根本不现实。不过这不代表可以躺平。我发现过攻击方尝试用curl-impersonate这类工具模拟浏览器指纹把JA3改成Chrome的样子。对这种变异流量单靠静态指纹库会失效。我的应对是加一个“新指纹聚类”流程每周把上一周的日志跑一遍聚类按“相似UA相似IP地域相似分片行为”归并出新指纹人工确认后入库。攻击方每变异一次我们最多滞后几天就能补上规则。还有一个经验不要在UA层面和对方纠缠。UA是他们最方便改的字段改了毫无成本TLS指纹是他们最不方便改的字段改了需要重新拆包。精力永远要投在对方改动成本高的环节上。4.3 排查命令与工具速查诊断一个流量是不是PCDN刷量最快的路径是抓包看TLS握手。下面这条命令把443端口的握手流量抓下来tcpdump -i eth0 -f tcp dst port 443 -w pcdn.pcap抓到包后用Python脚本解析ClientHello并计算JA3。完整实现大概200行核心思路是解析TLS Record层提取ClientHello里的版本号、密码套件、扩展类型、椭圆曲线和点格式拼成固定格式字符串再做MD5。示意代码如下import hashlib from scapy.all import rdpcap, TCP, Raw def calc_ja3(pcap_file): packets rdpcap(pcap_file) for pkt in packets: if TCP in pkt and Raw in pkt: data pkt[Raw].load # 这里需要按TLS ClientHello结构逐字段解析 # ssl_version, ciphers, extensions, curves, point_formats # 拼接规则 # ja3_string {},{},{},{},{}.format(...) # ja3 hashlib.md5(ja3_string.encode()).hexdigest() pass不想自己写的话有两个现成工具很好用一个是ja3的Python库能直接读取pcap输出JA3指纹另一个是tshark一条命令就能看tshark -r pcdn.pcap -Y ssl.handshake.type1 -T fields \ -e tcp.stream -e ja3.hash -e ja3.string生产环境的实时排查则直接聚合Nginx日志。我习惯用awk快速看Top指纹awk -F| {print $2} /var/log/nginx/ja3.log | sort | uniq -c | sort -rn | head -20把Top指纹和UA字段放一起看基本一眼就能判断出哪些是SDK流量。4.4 从应急封禁走向常态化运营把火扑灭只是第一步长期治理需要把规则变成一套持续运营的体系。我现在维护的仪表盘包括四个核心指标命中指纹库的请求数趋势、UA特征分布、地域命中分布、带宽占比。任何一项出现异常抬升先看是不是新指纹变异。告警阈值设在“某个指纹的请求量环比上涨100%”触发后自动把新增指纹加入观察区人工确认再转封禁。每周做一次新指纹聚类每月复盘一次误伤率。误伤率大于0.1%就要回退规则因为真实用户体验永远排在第一位。跟设备侧的协同也很重要。通过UA里的设备型号比如gdhg-cw5100可以反查这批设备的固件版本和出厂批次再推动设备方排查固件里是否有异常模块。这个环节需要耐心效果也不是立竿见影但一旦下掉一批问题固件刷量基数会真正减少比单纯堵流量管用得多。我在实际对抗里最大的体会是PCDN刷量不是一场能“打赢”的仗而是一场需要持续投入的“管理”工作。IP封禁是灭火JA3/JA4是建防火墙而规则的持续迭代才是让防火墙长期有效的关键。最后分享一个小技巧所有封禁规则上线前先用历史流量回放一遍算清楚误伤率再上生产这一步每次都能帮我避开大坑。后续我打算单独写一篇关于新指纹自动聚类的实现细节如果大家也在被这类问题困扰欢迎交流各自的对抗经验。