Landscape 一个从零实现的软路由系统 - Rust + eBPF

发布于

![alt](https://i.imgur.com/7vP18O4.png)
项目 GITHUB: [https://github.com/ThisSeanZhang/landscape](https://github.com/ThisSeanZhang/landscape)

# 为什么做 Landscape
想法源于一次 OpenWrt 崩溃。我意识到崩溃并不是 OP 自身的问题。

应该是我选择的插件之间产生了兼容性问题,而每次修改 OP 的配置总让我战战兢兢,有時觉得配置似乎没有生效,又或者是输入框和按钮太多。学习 OpenWrt 的成本让我感到很大,所以我开始想是否能在通用发行版 Linux 的基础上实现自定义的路由?当时我正在学习 Rust(这是入门第 N 次)和 eBPF,手中握着一把锤子,想找个东西来敲一敲,于是我的计划就此展开。

最初,我自己也有几个需求:

1. 怎么彻底抑制 PCDN 偷上行
2. 怎么方便将内网设备更新 DDNS
3. 不同网站针对不同设备使用不同的出口进行访问(当时我还不知道如此丰富的插件)
4. 不要每次都刷整个系统,程序可以直接升级,最好只需导出一份文件即可。

简单规划之后,我开始实施。

## NAT 的设计
NAT 能力演示: [https://www.bilibili.com/video/BV1b8Tn6ZEaz](https://www.bilibili.com/video/BV1b8Tn6ZEaz)
在使用组网软件时,我看到 tailscale 的一篇文章,是关于 tailscale 如何进行 P2P 打洞的,这给了我灵感。

简洁的流程是:当两个设备要进行穿透时,首先要向一台 STUN 服务器进行请求,告知自己的地址。服务器向双方告知对方的地址,双方开始尝试互联,因此可以利用这个机制,如果要进行强力阻断的,默认端口不可复用即可。

例如在连接存活期间:
- 客户端 A → STUN 服务器 B ✅ 允许
- STUN 服务器 B → 路由 A' ✅ 允许,然后路由转为 B → A
- 客户端 A → 另一个客户端 C ❌ 直接丢弃(不会像 NAT4 那样创建新映射)
- 另一个客户端 C → 路由 A' ❌ 丢弃

因此将会被迫回退到中继模式。
而允许进行 P2P 的,只要放行即可,行为与全锥一致,这已在一年多的使用中得到验证,且对日常毫无影响。

## 另一个大头是 IPv6 的支持
在 op 上的 IPv6 我总觉得用起来很奇怪,自己实现后才明白具体的原因。首先是 IPv6 首选时长与上游给的时间一致的问题,而 DHCPv6 虽然设计了 reconfigure 机制,但大部分客户端现在都不支持。

所以当你重启路由后的一段时间,虽然重新获得了新的 IP,但旧的 IPv6 仍然有效,导致无法访问 v6 网站。

不过 landscape 考虑到另一个场景,即 IPv6 NPT,并不是将 IPv6 使用 nat66 的方式进行 IP 映射,而是在多网口情况下,即使内网设备获得的是 A 网口的 IP,流量实际也能通过 B 网卡发出,并自动替换为 B 网卡获得的前缀信息。

另外,op 上不总是能固定后缀,而 landscape 中不仅能固定后缀,还能配合 DDNS 将 LAN 获得的 IP 直接写入运营商的 DNS 记录。

## 不同设备应用不同的规则
分流能力演示: [https://www.bilibili.com/video/BV1Wy26BiEJW/](https://www.bilibili.com/video/BV1Wy26BiEJW/)
现有的科学上网分流都是对本地局域网内的所有设备进行的,但我不希望所有设备都走同一套规则。例如,开发主机是全局的,IoT 设备全都直连,某些设备仅使用部分网站的科学。

虽然只配置了一个 DNS 上游,但依据不同的 DNS 请求与出口一致,天然能获得最终落地的 CDN 地址。有看到部分人称其为 DNS 跟踪,我认为称为 DNS 出口亲和或许更合适?

假设你想要使用某个新协议,你需要等待某些作者将其合入现有的分流程序,或者等待该协议的作者实现一套完整的分流。

landscape 将容器出口视为与 WAN 网卡同级的一等公民,采用 tproxy 进行解耦,只要容器内运行支持 tproxy 的程序即可以进行流量处理。多个容器间切换也是无缝的,因为 landscape 已做好分流,容器内仅需设置全局配置即可。

并且不同规则组的 DNS 缓存是隔离的,不会因融合不同策略而导致 DNS 缓存互相覆盖。例如:
- A 网站在 X 策略组中是直连的 -> 得到 Ax IP -> X 策略组缓存
- A 网站在 Y 策略组中是由容器处理 -> DNS 请求通过容器得到 Ay IP -> Y 策略组缓存

但是对于 Anycast IP,又该如何处理?目前你可以通过在容器中部署支持 Fake IP 的组件,将部分网站的 DNS 上游指向这个容器。

例如:
- A 网站在 X 策略组中是直连的 -> 得到 Ax real IP。
- B 网站在 X 策略组中是指向容器 I 的 -> 得到 Bx fake IP。
- C 网站在 X 策略组中是指向容器 J 的 -> 得到 Cx real IP。

可以做到只有部分容易混淆的访问使用 FakeIP。

仅将必须的流量转到容器的优点是,科学工具无需再处理直连流量,更不容易炸,若炸了也只影响海淘,而不影响直连。

直连体验基本与正常上网一致,因此延迟相对较低。

## 其余一些特点
安装: [https://www.bilibili.com/video/BV1aZ8w6TEY9/](https://www.bilibili.com/video/BV1aZ8w6TEY9/)

1. 前后端完全分离,您可以通过 API 控制所有 UI 上可以控制的操作,意味着可以实现自己的 UI。
2. 可将所有的配置导出为一份文件,并通过这一份文件恢复整个路由的配置。
3. 有 tui 工具能够直接热备份和恢复,不需要重启,且有救援工具,不小心配置错误也可连接上。

---

原文链接:[点击查看](https://www.v2ex.com/t/1236438)

评论(3)

看到 eBPF 这几个字就知道可以跟了。要是能再加上 DPDK,那不就无敌了?

· 0 个赞

回复

回复 @xiaoun001:之后可能会考虑把部分链路换成 AF_XDP,至于 DPDK,对家用场景来说功耗还是太大了。

· 0 个赞

关于 IPv6 重启后旧地址残留的问题:现在应该很少有设备专门用 DHCPv6 来分配地址了,如果走 SLAAC,其实本质是 preferred lifetime 参数没设好,客户端才不丢弃旧前缀。只要在 RA 报文里把旧前缀的 preferred lifetime 设成 0 就行,现代客户端收到 pref. time 为 0 会自动丢掉旧前缀,其实 nat66 也不需要。

· 0 个赞

0.046819s