简介:面向计算机科学、信息安全、数据科学与大数据技术、人工智能、通信、物联网等专业的学生与教师,是一套围绕SDN环境下的DDoS攻击检测与防御的完整课程大作业/毕设源码。项目基于Maven构建,共88个文件,其中Java源码71个,另有XML配置、YAML配置、Shell脚本、Markdown说明等,压缩包仅62KB,整体结构清晰。XML与YAML主要用于依赖管理和运行参数配置,Shell脚本可辅助环境初始化,Markdown文档则提供项目介绍,便于快速上手。已有750人学习浏览,可作为课程设计、期末大作业、毕业设计或初期项目立项演示使用。从源码中可以学习DDoS流量特征识别、攻击检测逻辑、SDN控制器下发流表进行防御等关键思路,结合配置文件和启动脚本可快速搭建实验环境,进一步扩展检测算法或对接实际SDN网络,适合作为安全方向入门的实践参考。
1. 课程大作业为什么绕不开SDN架构:控制器就是最便宜的DDoS观测点
拿到一份「课程大作业基于SDN的ddos攻击检测与防御系统源码.zip」,先别急着解压跑代码,得想明白一个问题:为什么这门课的大作业偏偏选 SDN 来做 DDoS 攻防,而不是直接在路由器上写 ACL 或者上防火墙?答案其实很反直觉——SDN 控制器本身就是一个天然的流量观测点,你不用在链路上串接任何探针,就能拿到全网每个交换机的流表统计、端口计数和包级样本。这意味着检测 DDoS 不再依赖旁路抓包设备,只要控制器轮询交换机的 flow stats,就能算出流量特征的突变。这个特性,正是课程大作业设计成 SDN 方案的根本原因。
这套系统的典型架构非常固定:数据平面用 Open vSwitch 或 Mininet 模拟的网络交换机,控制平面用 RYU、Floodlight 或 ONOS 做中央控制器,检测模块读流表统计,防御模块下发 OpenFlow 流表完成丢包或限速。整份源码 zip 拆开之后,核心也就这三块——拓扑脚本、检测算法、防御动作。本文要解决的就是:环境怎么搭、检测模块怎么写、防御模块怎么下流表、哪些参数坑会让你的演示现场翻车,以及最后怎么用一套脚本向老师证明系统真的生效了。
2. 先把实验环境跑通:Mininet、RYU 和 OVS 的最小组合
课程作业最忌讳的事,是代码写完但环境跑不起来。所以环境搭建必须用最小组合,能少装一个包就少装一个。我一般建议 Ubuntu 22.04 或 20.04 上用 Mininet 模拟网络,RYU 做控制器,OVS 做交换机,攻击流量用 hping3 或 Scapy 构造 SYN Flood。这套组合的好处是全部走 pip 和 apt,不需要编译源码,而且 RYU 的代码量比 ONOS 的 Java 工程小一个量级,课设周期内能改得动。
2.1 环境安装:四条命令装完,不用碰源码编译
先确认系统里有没有 Mininet,没有就 apt 装。然后装 RYU,注意要用 pip 指定版本,因为最新版的依赖偶尔会和系统 Python 冲突。Open vSwitch 在 Mininet 里通常自带,但如果你是想物理机做实验而不是纯模拟,再单独装 ovs-switchd。
sudo apt update sudo apt install -y mininet openvswitch-switch pip install ryu==4.34 sudo ovs-vsctl show这段逻辑不复杂:前两条装的是数据平面的基础,ryu==4.34是目前课程作业里用得最多的版本,支持 OpenFlow 1.3,API 稳定;最后一条ovs-vsctl show是验证 OVS 服务有没有起来,如果提示 socket 不存在,先sudo systemctl start openvswitch-switch,没启动的话 Mininet 里--switch ovs会直接失败。很多同学在这里卡住,其实是 ovsdb-server 没拉起,而不是 Mininet 的问题。
装完验证一下 RYU 能不能正常监听端口。RYU 默认监听 6633,但新版 OVS 的默认控制器端口是 6653,所以启动 RYU 时最好显式指定:
ryu-manager --ofp-tcp-listen-port 6653 ryu.app.simple_switch_13看到loading app ryu.app.simple_switch_13和installing 0 of 1之类的日志就说明监听成功。这里有第一个容易忽略的点:Mininet 里的 remote controller 也建议写成 6653,而不是默认 6633,否则会出现交换机一直在WAITING_CONNECTION状态,看起来像代码 bug,实际是端口没对上。
2.2 拓扑脚本:一台交换机三条主机,攻击者单独挂一个端口
课程演示场景不用复杂拓扑,一个交换机、三台正常主机、一台攻击主机就够。攻击者单独挂一个端口的意义在于:防御时你可以精确匹配in_port,不做全网丢包,这样演示更有说服力——只断攻击者的流量,不影响正常通信。
from mininet.topo import Topo class MyTopo(Topo): def build(self): s1 = self.addSwitch('s1', protocols='OpenFlow13') h1 = self.addHost('h1', ip='10.0.0.1/24') h2 = self.addHost('h2', ip='10.0.0.2/24') h3 = self.addHost('h3', ip='10.0.0.3/24') attacker = self.addHost('h4', ip='10.0.0.100/24') self.addLink(h1, s1) self.addLink(h2, s1) self.addLink(h3, s1) self.addLink(attacker, s1) topos = {'mytopo': MyTopo}脚本里的关键设计是攻击者 IP 故意用10.0.0.100,和正常主机网段一致但地址偏离主段,方便你在检测日志里一眼认出来。protocols='OpenFlow13'是必须写的,否则 Mininet 默认协商 OpenFlow10,RYU 的简单交换机虽然兼容,但高级流表特性在 OF1.0 里表现不一样。启动时用--controller remote指到本地 RYU:
sudo mn --custom mytopo.py --topo mytopo \ --controller remote,ip=127.0.0.1,port=6653 \ --switch ovs,protocols=OpenFlow13启动后别急着测,先sudo ovs-ofctl -O OpenFlow13 dump-flows s1看一眼有没有流表。如果有table=0, priority=0, actions=CONTROLLER这类默认表项,说明交换机已经连上控制器了。这个检查步骤花十秒,能帮你把“网络没通”和“控制器没连上”两类问题分开。
3. 检测模块怎么写:用流表统计做熵值突变,不靠抓包
检测是整个系统的灵魂。课程作业最常见的错误是检测模块去抓交换机端口镜像或者监听网卡,这等于抛弃了 SDN 的核心优势。正确做法是让 RYU 定期向交换机发OFPFlowStatsRequest,拿回每个流表项的packet_count和byte_count,然后聚合成目的 IP 维度上的流量分布,再算信息熵。DDoS 的特征是大量攻击流量涌向一个或少数几个目的 IP,目的 IP 分布的熵值会骤降,这个突变就是检测信号。
3.1 熵值计算:香农熵为什么对 SYN Flood 敏感
先用一句话说原理:正常情况下流量分散到多台主机,目的 IP 概率分布比较均匀,熵值高;SYN Flood 把绝大部分包打向同一个 IP,概率分布集中,熵值断崖式下跌。这个特性比单纯看带宽阈值靠谱,因为它不依赖“多大流量算攻击”的绝对值,而是看分布形态的变化,对低速慢速 DDoS 也能有反应。
计算熵值的代码只有十几行:
import math from collections import Counter def calc_entropy(ip_counter): """ip_counter: {dst_ip: packet_count}""" total = sum(ip_counter.values()) if total <= 0: return 0 entropy = 0.0 for cnt in ip_counter.values(): p = cnt / total if p > 0: entropy -= p * math.log2(p) return entropy这段的逻辑是:先统计每个目的 IP 的包数占总包数的比例,然后按香农公式累加-p * log2(p)。注意分母用的是总包数而不是 IP 数,所以只发一个 IP 的极端情况熵值是 0,流量均匀分布到 4 个 IP 时熵值接近 2。参数上你需要关心的只有一件事:统计窗口取多大。窗口太小,正常流量波动就会引起误报;窗口太大,攻击开始后要几十秒才能反映到熵值上。我一般取 5000 到 10000 个包为一个窗口,配合 2 秒轮询间隔,检测延迟控制在 3 秒内。
3.2 RYU 定时轮询:请求流表统计的完整代码
RYU 里做这件事要用到ryu.controller.handler的set_ev_cls装饰器监听EventOFPFlowStatsReply,同时自己定时发请求。注意 RYU 的packet_count是累计值,不会自己清零,所以代码里必须缓存上一次的数值做差,否则同一批流表项会反复触发告警。
from ryu.base import app_manager from ryu.controller import ofp_event from ryu.controller.handler import MAIN_DISPATCHER, set_ev_cls from ryu.ofproto import ofproto_v1_3 from ryu.lib import hub from collections import Counter class DDoSDetector(app_manager.RyuApp): OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.datapaths = {} self.prev_counts = {} # 缓存上次的 packet_count self.monitor_thread = hub.spawn(self._monitor) self.BASELINE_ENTROPY = 1.8 # 正常时的熵值基线 def _monitor(self): while True: for dp in self.datapaths.values(): self._request_flow_stats(dp) hub.sleep(2) def _request_flow_stats(self, dp): ofp = dp.ofproto parser = dp.ofproto_parser req = parser.OFPFlowStatsRequest(dp) dp.send_msg(req) @set_ev_cls(ofp_event.EventOFPFlowStatsReply, MAIN_DISPATCHER) def _flow_stats_handler(self, ev): dst_counter = Counter() for stat in ev.msg.body: key = (stat.match.get('ipv4_dst'), stat.priority) cur_cnt = stat.packet_count prev = self.prev_counts.get(key, 0) delta = cur_cnt - prev self.prev_counts[key] = cur_cnt if delta > 0 and stat.match.get('ipv4_dst'): dst_counter[stat.match['ipv4_dst']] += delta entropy = calc_entropy(dst_counter) self.logger.info("entropy=%.3f total_pkts=%d", entropy, sum(dst_counter.values())) if entropy < self.BASELINE_ENTROPY * 0.6: self.logger.warning("possible DDoS detected, entropy dropped to %.3f", entropy)代码里的关键点有三个。第一,self.prev_counts用(dst_ip, priority)做键,因为同一目的 IP 可能有不同优先级的表项,混在一起会让计数错乱。第二,delta = cur_cnt - prev解决累计值问题,R 不,是 RYU 的 packet_count 不重置,不差分会把历史总量全算进当前窗口。第三,阈值BASELINE_ENTROPY * 0.6是经验值,意思是熵值跌到基线的 60% 就认定攻击。你可以先跑 10 分钟正常流量记录基线,再把这个值写死,这是课设答辩时能讲清楚调参过程的素材。
4. 防御模块怎么写:高优先级 Drop 流表,先保住交换机端口
检测到攻击之后,防御动作最常用的是两条路:下发精确匹配的 drop 流表,或者用 OpenFlow meter 做限速。课程作业里我建议先用 drop 流表,因为 meter 表在 OVS 里的行为受内核模块版本影响,现场演示容易翻车。Drop 流表的核心逻辑是:在交换机里插一条优先级高于正常转发规则的表项,匹配攻击流量,action 为空即丢弃,然后给控制器留一条逃生通道,确保管理流量不被一起丢。
4.1 匹配什么字段:源 IP、目的 IP 还是 in_port
防御的匹配精度直接决定演示效果。只匹配目的 IP 会把所有去往受害主机的流量全丢了,包括正常用户的请求,这不叫防御是叫自损。只匹配 in_port 的问题是攻击者换端口就绕过。课程作业不用搞那么复杂,但至少要做到“源 IP + 目的 IP + 协议类型”三元组匹配,这样正常用户不受影响。
常见做法是先检测攻击目的 IP,然后查流表找到从哪个端口进来,再下发带in_port和ipv4_src的 drop 规则。展示效果上,只丢攻击者,h2 上的正常 ping 还能通,这比一刀切更让答辩老师信服。
4.2 下发防御流表的 RYU 实现与参数说明
from ryu.ofproto import ofproto_v1_3 from ryu.ofproto import ether from ryu.lib.packet import ethernet, ipv4, tcp def block_attacker(self, datapath, src_ip, dst_ip, in_port): ofp = datapath.ofproto parser = datapath.ofproto_parser match = parser.OFPMatch( in_port=in_port, eth_type=ether.ETH_TYPE_IP, ipv4_src=src_ip, ipv4_dst=dst_ip, ip_proto=6 ) instructions = [parser.OFPInstructionActions(ofp.OFPIT_CLEAR_ACTIONS, [])] mod = parser.OFPFlowMod( datapath=datapath, priority=100, match=match, instructions=instructions, hard_timeout=60, buffer_id=ofp.OFP_NO_BUFFER ) datapath.send_msg(mod)这段代码里最有讲究的是priority=100和OFPIT_CLEAR_ACTIONS。OVS 默认转发规则优先级是 0,你下发的防御规则必须在数值上远大于 0,否则会被默认规则覆盖。OFPIT_CLEAR_ACTIONS表示清空所有动作再执行,相当于强制丢包。hard_timeout=60是自动过期时间,这是课设演示的一步妙棋:攻击结束后 60 秒规则自动消失,避免把交换机流表永久写死,也方便做二次攻击测试。ip_proto=6是 TCP,SYN Flood 用这个;如果是 UDP Flood 改成 17 即可。
我习惯在检测到阈值下降后的第一次告警里延迟 2 秒再下发防御流表,并把这两秒内的日志打出来。这个“检测到动作”之间的间隔就是系统的响应时间,答辩时直接甩日志给老师看:entropy dropped at 12:01:03, block rule installed at 12:01:05,响应时间 2 秒,这就是可量化的成果。
5. 避坑:OVS 优先级、累计计数器和控制器断连的三类翻车现场
这个系统的坑比想象的深,而且很多坑不跑到演示前一刻根本发现不了。我把课程作业里最常翻车的三类问题拆开讲,每条按“现象 → 原因 → 解决”写,你可以对照自己的源码逐一排查。
坑一:流表下发了但攻击流量还在跑。
现象是 hping3 一直刷包,ovs-ofctl dump-flows也能看到 drop 表项,但 h2 还是被打挂。原因大概率是优先级不够或匹配字段写错。OVS 里如果同时存在两条匹配同一流量但优先级不同的规则,低优先级的会被隐藏而不是被删除。很多课程源码里防御规则的 priority 填 0,和默认转发规则同级,OVS 倾向选择先安装的,结果就是 drop 规则永远不生效。解决方法是防御规则 priority 显式设成 100 以上,并且在代码里确认OFPIT_CLEAR_ACTIONS而不是OFPIT_GOTO_TABLE。
坑二:检测模块每隔几秒误报一次 DDoS。
现象是没攻击的时候日志里也反复出现 entropy 下降告警。原因是packet_count是累计值,而 RYU 的 EventOFPFlowStatsReply 拿到的 body 里每次都是全部表项。如果你用stat.packet_count直接参与熵值计算,等于把历史流量全部加进了当前窗口,正常流量波动就会被放大成熵值突变。解决方法是代码里维护self.prev_counts缓存,每次只取增量delta = cur_cnt - prev_cnt。这个坑在 3.2 节已经埋了伏笔,但值得单独拎出来重复一次,因为它是所有课设里出现频率最高的 bug。
坑三:Mininet 启动后交换机一直显示WAITING_CONNECTION,控制器什么日志都没有。
现象是 RYU 控制台空白,ovs-vsctl show里is_connected: false。原因不是代码问题,是端口或协议版本没对齐。Mininet 里--controller remote,port=6653和 RYU 的--ofp-tcp-listen-port必须一致,一个用 6633 一个默认 6653,就会互相等。另一个隐藏原因是 Mininet 创建 OVS 时默认用 OpenFlow10,而 RYU 的 app 写了OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION],协商失败直接不握手。解决方法是拓扑脚本里交换机指定protocols='OpenFlow13',RYU 启动参数也显式写--ofp-tcp-listen-port 6653。
第四个常踩的坑是基线阈值不科学。很多源码直接把BASELINE_ENTROPY写死成 1.8,但不同拓扑、不同主机数量下正常熵值不一样,四台主机打流量和八台主机的熵基线差了快一倍。我的做法是检测模块启动前先跑 30 秒学习模式,统计正常熵值均值,再乘以 0.6 作为告警阈值。这个细节答辩时提出来很加分,说明你理解熵值的相对性而不是背公式。
最后一个坑必须讲:手动执行ryu-manager调试没问题,但写进课程报告里的启动命令别忘加--observe-links。不加这个参数,RYU 拿不到链路发现信息,部分拓扑相关的 API 会静默失败。如果你发现检测模块只能处理单交换机拓扑,多交换机的时候流表统计对不上,先检查这个参数。
6. 用一套脚本验证攻防生效:从熵值曲线到流表下发日志
验证不是“点一下按钮看有没有告警”,而是要做成一条可复现的演示链路:记录正常基线 → 发起攻击 → 观察熵值骤降 → 观察防御流表下发 → 验证正常流量没有中断。我会把整个过程压缩成一个脚本,每步留日志,这样老师看日志就能确认每个环节不是吹的。
# 步骤1:启动 RYU 检测应用,后台记录日志 ryu-manager --ofp-tcp-listen-port 6653 ddos_detector.py > ryu.log 2>&1 & # 步骤2:发起持续 30 秒的 SYN Flood,源地址随机 sudo hping3 -S -p 80 --flood --rand-source 10.0.0.2 # 步骤3:观察防御流表 sudo ovs-ofctl -O OpenFlow13 dump-flows s1 | grep priority=100脚本的思路是让攻击持续 30 秒,然后手动中断,再从ryu.log里提取熵值序列。如果你嫌 grep 日志不够直观,可以写个 20 行 Python 脚本把熵值按时间戳画出来,攻击开始前一条直线、攻击开始后断崖下跌、防御下发后回弹,这三段曲线就是整套系统最好的说明书。画图代码不复杂,用 matplotlib 读日志文件里的entropy=字段就行,这里不展开。
关于响应时间,我习惯给_flow_stats_handler里加一行time.time()记录,攻击脚本启动时也打一个时间戳,两个时间戳之差就是检测延迟。课程作业报告里写“平均检测延迟 1.5 秒、防御规则生效延迟 0.8 秒”这种量化数据,比任何文字描述都有说服力。想压更低延迟就把轮询间隔从 2 秒改成 0.5 秒,但要注意 RYU 处理 FlowStatsReply 的 CPU 占用会上升,Mininet 模拟环境下太频繁反而可能丢事件,这个参数要实测。
最后一件事:演示完把 Mininet 清干净再走。不清理的话 OVS 里的旧流表会带着上一次实验的防御规则,下次启动拓扑时 h4 可能直接从 switch 里被 “静默丢包”,你误以为代码有 bug,其实是自己上次没擦干净。用sudo mn -c清一次再重新跑拓扑,这个习惯我保持了很久,也建议你写进实验笔记里。希望帮到你——课件里那句 “基于 SDN 的 DDoS 检测与防御” 听起来像个大词,拆开之后其实就是“控制器里读计数器、算熵值、下流表”三件事,把这三件事做扎实,离一个能演示、能答辩、能讲清楚原理的系统就不远了。
本文还有配套的精品资源,点击获取