简介:这份源码资源面向安全工程师、后端开发者及网络安全爱好者,聚焦在线教育平台遭遇DDoS攻击的真实案例,提供从攻击原理到防御落地的完整参考。资源包共3个文件,以html页面、inscode配置与gitignore文件为主,压缩包约7KB,体量轻便,便于快速查阅与本地复现。内容围绕带宽耗尽、协议资源耗尽与应用层耗尽三类攻击展开,并给出基础防护、高防IP与清洗服务、动态带宽调度、CDN流量分发、限流熔断降级及高可用架构等全链路防御思路,帮助读者理解攻击全貌并掌握可落地的应对策略。目前已有298人学习,适合需要构建抗DDoS能力、补齐安全防护短板的开发者参考,也可作为团队安全演练与架构优化的辅助材料。
1. DDoS攻击防御实战:从被流量打崩到守住业务入口
凌晨两点,监控群突然炸了——某公司对外API的响应时间从80ms飙到12s,带宽曲线几乎垂直拉满,运维第一反应是“业务量暴涨”,第二反应是“完了,被打了”。这不是电影桥段,而是很多中小团队第一次遭遇DDoS时的真实写照。DDoS攻击防御实战要解决的核心问题很朴素:当海量垃圾流量涌向你的入口时,怎么让正常用户还能进来、让业务不崩、让账单不失控。它适合后端工程师、运维、SRE,以及任何需要把服务暴露到公网的人。这篇笔记不讲空泛概念,而是按“先看懂攻击面、再搭最小可复现防御、最后调参和排错”的路径,把源码级实现拆开讲清楚。你不需要一开始就上高防机房,但必须知道每一层能挡什么、挡不住什么。
2. 先分清对手:DDoS的四种常见打法与你的防御边界
2.1 流量型、协议型、应用型、反射型,打法完全不同
很多人把DDoS当成一个东西,结果用错药。常见做法是先按OSI层次分四类:流量型(UDP Flood、ICMP Flood)靠体积压垮带宽;协议型(SYN Flood、ACK Flood)消耗连接表;应用型(HTTP Flood、慢速攻击)伪装成正常请求打后端;反射放大(DNS/NTP反射)借第三方把流量放大几十倍。这四类的防御点完全不同:流量型要在边缘清洗,协议型要在内核和连接队列做文章,应用型必须看业务语义,反射型则要限制暴露面。
我一般会先画一张表,把“攻击类型 → 主要消耗资源 → 第一道防线 → 兜底手段”列清楚,再决定代码写在哪一层。比如SYN Flood,如果你只在Nginx层做限流,内核的SYN队列早就满了,根本轮不到Nginx处理。所以源码级防御的第一步不是写业务代码,而是确认内核参数和边缘策略。
2.2 单机防御的极限:为什么只靠一台机器扛不住
单机防御的上限由三个硬指标决定:网卡带宽、内核包处理能力、连接跟踪表大小。一块千兆网卡理论峰值约125MB/s,遇到10Gbps的UDP Flood,包还没进应用层,网卡和中断就已经饱和。更隐蔽的是conntrack表:默认nf_conntrack_max在不少发行版上是65536,SYN Flood几秒钟就能塞满,之后所有新连接被丢弃,表现为“服务器没死但谁也连不上”。
所以单机方案适合做“第一层过滤”和“应用层精细限流”,但不要指望它扛住大流量。常见做法是:单机负责识别和丢弃明显异常包,把干净流量放给后端;真正的体积型攻击交给上游清洗或云厂商的高防IP。源码防御的价值在于“低成本挡住小规模攻击”和“为大流量争取响应时间”。
2.3 最小可复现防御栈:iptables + 内核参数 + 应用层限流
下面给出一套可以在测试机复现的最小防御栈。它不追求扛住T级攻击,但能让你在本地把“识别异常→丢弃→限流→记录”的链路跑通。
第一步,调整内核参数,缓解SYN Flood和连接表压力:
# /etc/sysctl.d/99-ddos.conf # 开启SYN Cookies,SYN队列满时用cookie应答,防止队列耗尽 net.ipv4.tcp_syncookies = 1 # 增大SYN队列和accept队列,给突发连接留缓冲 net.ipv4.tcp_max_syn_backlog = 8192 net.core.somaxconn = 8192 # 扩大连接跟踪表,并缩短空闲连接超时,加快回收 net.netfilter.nf_conntrack_max = 262144 net.netfilter.nf_conntrack_tcp_timeout_established = 600 # 降低TIME_WAIT占用,允许复用 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 15执行sysctl -p生效。参数说明:tcp_syncookies是SYN Flood的后悔药,但注意它只在队列满时启用,正常连接不受影响;nf_conntrack_max要结合内存算,每条跟踪记录约300字节,262144条约占75MB,内存小的机器别照抄。
第二步,用iptables做基础速率限制和异常包丢弃:
# 限制单个IP对新连接的速率,超过则丢弃(注意:这是粗粒度,生产要配合白名单) iptables -N SYN_FLOOD iptables -A SYN_FLOOD -p tcp --syn -m limit --limit 100/s --limit-burst 200 -j RETURN iptables -A SYN_FLOOD -p tcp --syn -j DROP iptables -A INPUT -p tcp --syn -j SYN_FLOOD # 丢弃非法TCP标志组合(如NULL、XMAS扫描) iptables -A INPUT -p tcp --tcp-flags ALL NONE -j DROP iptables -A INPUT -p tcp --tcp-flags ALL ALL -j DROP # 限制ICMP速率,防止ICMP Flood iptables -A INPUT -p icmp -m limit --limit 10/s -j ACCEPT iptables -A INPUT -p icmp -j DROP逻辑说明:SYN_FLOOD链先放行低于阈值的SYN,超过的直接DROP。--limit 100/s表示平均每秒100个新SYN,--limit-burst 200是突发上限。这个阈值对单机小服务够用,但对NAT后的办公网会误伤,所以生产环境要先把公司出口IP加白名单。
第三步,应用层用令牌桶做细粒度限流。以Python为例,写一个可复用的限流中间件:
import time from collections import defaultdict class TokenBucket: def __init__(self, rate, capacity): self.rate = rate # 每秒补充的令牌数 self.capacity = capacity # 桶容量,即突发上限 self.tokens = capacity self.last = time.time() def allow(self): now = time.time() # 按时间差补充令牌,最多补到capacity self.tokens = min(self.capacity, self.tokens + (now - self.last) * self.rate) self.last = now if self.tokens >= 1: self.tokens -= 1 return True return False # 按IP维护桶,实际生产要加过期清理,防止内存膨胀 buckets = defaultdict(lambda: TokenBucket(rate=20, capacity=40)) def check_request(ip): if not buckets[ip].allow(): return False, "429 Too Many Requests" return True, "OK"参数说明:rate=20表示每个IP平均每秒20个请求,capacity=40允许短时突发到40。这个值要按业务调整:登录接口可以更低(如5/s),静态资源可以更高。注意defaultdict会无限增长,生产环境必须加LRU或定时清理,否则限流器本身变成内存泄漏点。
2.4 把防御接进请求链路:从入口到业务的无缝衔接
限流器写好了,关键是接在哪。常见做法是在反向代理层(如Nginx的limit_req)做第一道,应用层做第二道。Nginx配置示例:
# 定义限流区,按IP,10MB共享内存约存16万个IP状态 limit_req_zone $binary_remote_addr zone=perip:10m rate=20r/s; server { location /api/ { limit_req zone=perip burst=40 nodelay; limit_req_status 429; proxy_pass http://backend; } }burst=40允许突发40个请求,nodelay表示突发请求立即处理而不是排队。这样应用层拿到的已经是相对干净的流量,令牌桶只作为兜底。两层配合的好处是:Nginx用C实现,性能损耗小;应用层能结合用户身份做更细的策略,比如VIP用户单独放行。
3. 源码级实现:把防御逻辑写成可测试的模块
3.1 用滑动窗口替代固定窗口,解决临界突刺
固定窗口限流有个经典问题:窗口边界处可能通过两倍流量。比如限制每秒100次,第1秒最后10ms来100次,第2秒前10ms又来100次,实际200次都放过了。滑动窗口能缓解这个问题。下面是一个基于Redis的滑动窗口实现:
import time import redis r = redis.Redis(host='localhost', port=6379, decode_responses=True) def sliding_window_allow(key, limit, window=1): """ key: 限流键,如 f"rate:{ip}" limit: 窗口内最大请求数 window: 窗口大小(秒) """ now = time.time() pipe = r.pipeline() # 移除窗口外的旧记录 pipe.zremrangebyscore(key, 0, now - window) # 当前窗口内请求数 pipe.zcard(key) # 加入本次请求,score用时间戳保证有序 pipe.zadd(key, {str(now): now}) # 设置过期,避免冷key常驻内存 pipe.expire(key, window + 1) _, count, _, _ = pipe.execute() return count < limit逻辑说明:用Redis有序集合存请求时间戳,每次先删旧记录再计数。zremrangebyscore和zcard在同一个pipeline里执行,减少往返。参数上,limit和window要按接口定:短信验证码接口可以limit=1, window=60,查询接口可以limit=100, window=1。注意Redis本身可能成为瓶颈,高并发下建议用本地滑动窗口+Redis定期同步,或者直接用Redis的CL.THROTTLE模块。
3.2 连接数限制:比请求数更早发现异常
有些攻击不靠高QPS,而是靠大量慢连接占满连接表。这时候请求数限流没用,要看并发连接数。Linux可以用ss统计,但实时防御要在代码里做。下面是一个基于iptables connlimit和ss监控的组合方案:
# 限制单IP对80端口的并发连接数不超过50 iptables -A INPUT -p tcp --dport 80 -m connlimit --connlimit-above 50 --connlimit-mask 32 -j REJECT # 监控脚本:每10秒统计一次,超过阈值告警并记录TOP IP while true; do ss -nt state established '( sport = :80 )' | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -5 sleep 10 doneconnlimit-above 50表示超过50个并发连接就拒绝,--connlimit-mask 32按单个IP算。这个值对普通用户足够,但如果有用户走代理池,可能误伤。监控脚本的输出要接入告警,因为connlimit是硬拒绝,调太低会影响正常用户。
3.3 日志与取证:被打了要知道谁打的、打了多久
防御不只是挡,还要留证据。下面是一个结构化日志模块,记录每个被限流请求的IP、时间、路径和触发规则:
import json import logging from datetime import datetime logger = logging.getLogger("ddos") handler = logging.FileHandler("/var/log/ddos_block.log") logger.addHandler(handler) logger.setLevel(logging.INFO) def log_block(ip, path, rule, count): record = { "ts": datetime.utcnow().isoformat(), "ip": ip, "path": path, "rule": rule, # 触发的规则名,如 "sliding_window" 或 "connlimit" "count": count, # 当前窗口计数或连接数 } logger.info(json.dumps(record))日志按行JSON存储,方便后续用jq或ELK分析。关键字段是rule和count,能帮你判断是误伤还是真攻击。比如某个IP在sliding_window规则下count稳定在limit附近,可能是正常高频用户;如果count瞬间冲到几千,基本可以确定是攻击源。取证数据还可以用来向上游申请清洗或报警。
4. 避坑与排查:那些让防御失效的细节
4.1 坑一:只调了应用层,内核先崩了
现象:Nginx限流配置正确,但攻击一来服务器直接失联,SSH都连不上。原因:SYN Flood打满内核SYN队列,或者conntrack表溢出,包根本到不了应用层。解决:先按2.3节调内核参数,再用dmesg | grep -i "syn flood"和conntrack -S确认。如果nf_conntrack: table full出现,说明nf_conntrack_max太小,要调大并缩短超时。
4.2 坑二:限流阈值照抄博客,把正常用户挡了
现象:上线限流后,客服反馈部分用户频繁429。原因:阈值没结合业务。比如某接口正常用户每秒3-5次,但博客示例写20/s,看似宽松,实际NAT后的公司出口几百人共享一个IP,20/s根本不够。解决:先跑一周观察日志,用P99值定阈值,再留2-3倍余量。对已知出口IP加白名单,或者按用户ID而非IP限流。
4.3 坑三:Redis限流器自己成了单点
现象:攻击时Redis响应变慢,限流判断超时,请求全部放行或全部拒绝。原因:限流逻辑强依赖Redis,而Redis没做高可用或连接池太小。解决:本地加一层令牌桶做快速判断,Redis只做跨机同步;或者用Redis Cluster并设置合理的socket_timeout,超时后走降级策略(如放行但记录日志)。降级策略要提前想清楚:是宁可放过还是宁可错杀。
4.4 坑四:日志写太多,磁盘先满了
现象:被攻击时防御日志暴增,几小时写满磁盘,服务因无法写日志而崩溃。原因:每个被限流请求都写文件,且没做轮转。解决:用logrotate按小时轮转,限制保留份数;高频规则只记录聚合统计(如每分钟每IP计数),不记录单条。或者把日志直接推到远端,本地只留最近1小时。
4.5 坑五:忘了防御自己的管理端口
现象:攻击者打不动业务端口,转而打SSH或数据库端口,导致运维通道被占满。原因:防御策略只覆盖了80/443。解决:管理端口只对跳板机开放,用iptables -A INPUT -p tcp --dport 22 -s 跳板机IP -j ACCEPT加默认拒绝。数据库端口同理,不要暴露到公网。
5. 进阶技巧:把防御做成可观测、可回滚的闭环
5.1 用压测验证防御阈值,而不是等真攻击
防御配置最怕“看起来对,一打就穿”。我一般会写一个轻量压测脚本,在测试环境模拟SYN Flood和HTTP Flood,观察系统在阈值附近的行为。比如用hping3发SYN包,用ab或wrk发HTTP请求,同时监控ss -s、conntrack -S和限流日志。重点看三个指标:正常请求的成功率、被限流请求的比例、系统资源(CPU、内存、连接表)的拐点。如果压测时正常请求成功率低于99%,说明阈值太紧;如果资源先于限流触发而耗尽,说明内核参数或硬件是瓶颈。
压测脚本示例(仅测试环境使用):
# 模拟SYN Flood,持续30秒,每秒1000包 hping3 -S -p 80 --flood --rand-source -d 120 目标IP & # 同时用wrk压正常请求,观察成功率 wrk -t4 -c100 -d30s --latency http://目标IP/api/health注意--rand-source会伪造源IP,只在隔离测试网用。压测后要清理iptables规则,避免残留影响。
5.2 防御策略的灰度与回滚
限流规则上线不能一把梭。常见做法是先用log模式跑一周,只记录不拦截,观察会命中哪些IP、频率如何。确认误伤可控后,再切到block模式。回滚要快:把规则写成配置文件,用nginx -s reload或发送信号热加载,避免重启服务。应用层限流器可以加一个开关,通过配置中心动态调整rate和capacity,攻击过去后逐步放宽。
5.3 一个具体技巧:用布隆过滤器挡掉已知恶意IP
如果攻击源IP相对固定,可以用布隆过滤器在内存里做O(1)判断,比查Redis快得多。Python示例:
from pybloom_live import BloomFilter # 容量100万,误判率0.1% bf = BloomFilter(capacity=1000000, error_rate=0.001) def add_malicious(ip): bf.add(ip) def is_malicious(ip): return ip in bf布隆过滤器的特点是“宁可错杀不可放过”——它说不在的IP一定不在,说在的IP可能误判。所以适合做第一层快速过滤,命中后再查精确黑名单确认。误判率0.1%意味着1000个正常IP里可能有1个被误拦,对大多数业务可接受。更新策略是定期从日志里提取高频攻击IP,批量加入过滤器。
这套方案我用了两年多,最大的教训是:防御不是配一次就完事,而是跟着业务和攻击手法一起演进。每次被攻击后,我都会把新的攻击特征加进日志分析,调整阈值,再压测一遍。希望帮到你。
本文还有配套的精品资源,点击获取