前阵子有个朋友找到我,说他搞了个公益服务放在网上,免费给大家用的。本来挺好的事儿,结果没过几天服务器就扛不住了——有人疯狂刷接口、暴力破解、搞DDoS。他问我:有没有什么简单的办法能挡一挡?我说,这不是有防火墙吗?他说云服务商的防火墙太粗糙,只能按IP段封,而且按流量计费,攻击流量进来之
前阵子有个朋友找到我,说他搞了个公益服务放在网上,免费给大家用的。本来挺好的事儿,结果没过几天服务器就扛不住了 —— 有人疯狂刷接口、暴力破解、搞DDoS。
他问我:有没有什么简单的办法能挡一挡?
我说,这不是有防火墙吗?他说云服务商的防火墙太粗糙,只能按IP段封,而且按流量计费,攻击流量进来之前已经产生了费用。他想要一个更灵活的东西,能在更底层把恶意流量截住。
于是就有了这个项目 —— net-sentry。
需求其实很简单:
Linux有个东西叫TUN设备,是个虚拟网卡。它能拦截到的数据是IP层及以上,正好符合需求。用户空间程序读取TUN设备拿到原始IP包,自己决定是放行还是丢弃。
为什么用Rust?说实话,一开始也想用Go,毕竟写网络程序Go生态成熟。但后来想了想:
而且现在有AI帮忙写代码,Rust那套所有权、生命周期的门槛被大大降低了。我问Claude:"帮我写一个解析IP头的结构体",它直接给我生成好,我只需要检查一下对不对。
这就是Vibe Coding时代 —— 你不用纠结语法细节,专注于解决问题本身就行。
整体架构比较直接:
TUN设备 → 读取原始包 → 解析协议头 → 匹配规则 → 放行或丢弃
↓
定期统计日志
代码分了几个模块:
io.rs:TUN设备的读写封装parser.rs:IP/TCP/UDP/ICMP协议解析rules.rs:规则定义和匹配engine.rs:规则引擎,决定放行还是阻断stats.rs:统计上报Rust的trait很适合这种场景。我把每个组件都抽象成trait:
// 数据包来源
pub trait PacketSource {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>;
}
// 数据包出口
pub trait PacketSink {
fn write(&mut self, buf: &[u8]) -> io::Result<usize>;
}
// 协议解析器
pub trait PacketParser {
fn parse(&self, buf: &[u8]) -> Option<PacketMeta>;
}
// 规则引擎
pub trait RuleEngine {
fn should_drop(&self, meta: &PacketMeta) -> bool;
}
这样做的好处是,核心循环完全不依赖具体实现:
fn run_loop<S, P, E, R>(device: &mut S, parser: &P, engine: &E, mut stats: R, buf_size: usize)
where
S: PacketSource + PacketSink,
P: PacketParser,
E: RuleEngine,
R: StatsReporter,
{
let mut buf = vec![0u8; buf_size];
loop {
let n = device.read(&mut buf).unwrap();
if let Some(meta) = parser.parse(&buf[..n]) {
if !engine.should_drop(&meta) {
device.write(&buf[..n]);
}
}
}
}
想换成不同的解析库?实现一个PacketParser就行。想换规则引擎?实现RuleEngine。测试的时候,可以注入mock实现,不需要真的创建TUN设备。
解析网络包这种事,千万别自己手写。各种边界条件、大小端、可选字段,写一个错一个。
直接用现成的库etherparse,它把IP、TCP、UDP、ICMP的解析都做好了:
fn parse_ipv4(buf: &[u8]) -> Option<PacketMeta> {
let ip = Ipv4HeaderSlice::from_slice(buf).ok()?;
let src_ip = IpAddr::V4(ip.source_addr());
let dst_ip = IpAddr::V4(ip.destination_addr());
let proto = ip.protocol();
// 继续解析传输层...
}
解析出来的元信息放在一个简单的结构体里:
pub struct PacketMeta {
pub proto: Proto, // TCP/UDP/ICMP/其他
pub src_ip: IpAddr, // 源IP
pub dst_ip: IpAddr, // 目标IP
pub src_port: Option<u16>, // 源端口
pub dst_port: Option<u16>, // 目标端口
}
规则设计成简单的DSL,命令行直接传:
# 封掉某个IP的所有流量
--block "dst=192.168.1.100:*"
# 只允许访问80和443端口
--allow "tcp,dst=*:80" --allow "tcp,dst=*:443" --default-policy deny
# 封掉某个IP段的TCP 443端口
--block "tcp,dst=10.0.0.0/8:443"
规则解析就是把字符串切分,识别关键词:
pub fn parse_rule(input: &str) -> Result<Rule, String> {
let mut proto = None;
let mut src = None;
let mut dst = None;
for part in input.split(',') {
if part == "tcp" { proto = Some(ProtoMatch::Tcp); }
else if part.starts_with("src=") {
src = Some(parse_netport(&part[4..])?);
}
// ...
}
Ok(Rule { proto, src, dst })
}
规则匹配就是逐条检查:
impl RuleEngine for SimpleRuleEngine {
fn should_drop(&self, meta: &PacketMeta) -> bool {
// 先检查允许规则
if self.allow.iter().any(|r| r.matches(meta)) {
return false;
}
// 再检查阻断规则
if self.block.iter().any(|r| r.matches(meta)) {
return true;
}
// 都没匹配,按默认策略
self.default_policy == DefaultPolicy::Deny
}
}
说实话,这个项目大部分代码都是我描述需求,Claude生成的。我的角色更像是在做代码审查和架构设计。
比如我说:"写一个TUN设备的封装,支持read和write",它给我生成好。我说"解析IP头",它生成好。我说"实现一个规则引擎,支持allow和block列表",它又生成好。
我需要做的是:
这就是Vibe Coding——你不再是一个字一个字敲代码的打字员,而是一个用自然语言指挥AI的建筑师。AI是你的pair programmer,而且是那种永不疲倦、随时响应的搭档。
当然,AI也会犯错。有次生成的规则解析代码,端口范围解析的逻辑有问题,我检查的时候发现了。还有一次,trait的泛型约束写漏了,编译器报错我才意识到。
但总的来说,开发效率至少提升了3-5倍。
编译运行需要root权限(创建TUN设备需要):
cargo build --release
sudo ./target/release/net-sentry \
--tun-name tun0 \
--block "tcp,dst=恶意IP:443" \
--block "dst=另一个恶意IP:*"
运行后会定期打印统计:
INFO stats total=10000 dropped=328 forwarded=9672
目前这个版本已经能用,但还有不少可以完善的地方:
不过这些都是"锦上添花"。对于朋友的公益服务来说,现在这个版本已经能挡住大部分恶意流量了。
这个项目大概花了一个晚上的时间。如果是以前,用C写这类东西,光是调库、处理边界条件就得折腾好久。现在有了Rust的生态和AI的加持,从想法到可用的原型,速度比以前快太多了。
技术的进步,本质上是让开发者能把更多精力放在解决问题上,而不是在工具本身的折腾上。Rust解决了内存安全的问题,AI解决了语法和样板代码的问题。
这就是Vibe Coding时代——你只需要说"我要做什么",剩下的"怎么做",让AI和工具来帮你。
如果你也遇到类似的流量攻击问题,可以参考这个思路实现自己的版本。核心逻辑其实不复杂,关键是理解TUN设备的原理和网络包的结构。
代码量不多,几百行Rust,但解决问题刚刚好。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!