einat-ebpf v0.1.0 发布,基于 eBPF 的 Full Cone NAT

264 天前
 eh5

第一个版本,欢迎试用反馈!

Full Cone 即 Endpoint-Independent Mapping 加 Endpoint-Independent Filtering 的 NAT 行为

功能

是的,你没看错,不只是 UDP 的 Full Cone ,还支持对 TCP 和 ICMP 的 Full Cone ,基本上 match 现在三大运营商 IPv4 CGNAT 的行为了,所以你可以在运营商 Full Cone CGNAT 后面无限套娃 einat 。。

而且这意味着你可以用类似 https://github.com/heiher/natmap 的工具开 TCP 服务。

开发动机

之前一直在用 https://github.com/fullcone-nat-nftables (由 https://github.com/Chion82/netfilter-full-cone-nat 启发) 的 patch, 但是它不仅需要 patch kernel, 还要 patch nftables 的用户前端程序和依赖库,实在太难维护了。而且自我开发 einat 前就已经没有维护了,而且现在 repo 也归档了。

加上之前一直在各处看到 eBPF 的概念(我的第一印象是这东西怎么炒的这么厉害。。),再了解到确实可以在 TC hook 上实现 NAT ,索性从头写一个 NAT 顺便学习一下 eBPF ,前前后后完善了四个月终于完成了一个版本。

现在已经在我的 R2S 上运行了几个月替代 Netfilter 的 masquerade 。

在写 einat 的过程中我学到了很多,后续打算在我的博客发布几篇关于 einat 的文章和为什么 Netfilter masquerade 不是真的 Endpoint-Independent Mapping 。

einat 主要是便利了基于 STUN 的 P2P 应用,但对于 BT tracker 模式这种依赖 UPnP IGD/PCP 的没什么用。所以在这个项目完善之后,我计划写一个基于 STUN 在 Full Cone 网络下工作的 PCP/NAT-PMP/UPnP 端口映射服务,主要是为了支持 BT tracker 模式。

3556 次点击
所在节点    宽带症候群
31 条回复
Jirajine
264 天前
这个看起来不错,但是不知道在和 netfilter/contrack/策略路由一起使用的时候会不会有未预期的行为,导致流量泄漏等问题。
其实开一个用户态的实现了 fullcone 的 socks 代理,就可以满足很多应用的需求了。
有状态的 nat6 应该是没什么使用场景的,ipv6 一般不用 nat ,即使需要 NPTv6 几乎总是最好的选项,除非你只有单个地址并且上游也是 fullcone 的。
相比之下不如整一下 nat64 ,尝试把内网完全迁移到 ipv6 only ,可以极大的简化维护成本和 p2p 连通性问题。
所有层 nat 全部都是 fullcone 的话,确实可以无限套娃,stun 打洞也能正常工作。但 upnp 那一套主动打洞协议却没有办法自动转发请求给上游,通过 stun 自己实现其中一部分协议应该是可行的。
eh5
264 天前
@Jirajine

> 不知道在和 netfilter/contrack/策略路由一起使用的时候会不会有未预期的行为
这个相当于在网卡端做了 NAT ,会对除了配置排除的目标地址无条件做 NAT 到选定的或配置的某一个外部地址,而且可选支持查路由表选定外部地址(即 prefsrc ),只要配置正确大概没什么影响,当然要具体情况具体分析。

> 有状态的 nat6 应该是没什么使用场景的,ipv6 一般不用 nat ,即使需要 NPTv6 几乎总是最好的选项,除非你只有单个地址并且上游也是 fullcone 的。

简单加个支持不难的,大部分代码都是复用的,而且正如你说的 NAT66 没什么用所以默认构建是关掉的。

> 相比之下不如整一下 nat64

我其实是 have NAT64 in mind 去开发的,见 https://github.com/EHfive/einat-ebpf/issues/3 ,但是这样 IP 包更改的内容就更多了,进一步增加 eBPF 程序复杂度,考虑到 NAT64 家庭应用实在没什么大的好处我没什么动机去开发,还得配个 NAT64 DNS server 。。
yyzh
264 天前
所以有计划开发 openwrt 的 luci 面板以及加入 openwrt 的软件源吗?这玩意应该在 openwrt 上有比较高需求.
另外你的端口映射软件有计划开发 openwrt 版的吗?现在在用 Natter 打洞但是没 openwrt 版的就很烦...
eh5
264 天前
@yyzh

我个人没有计划,现在在路由器上用 NixOS 不怎么用 OpenWrt 了,但是如果这个软件获得了足够多的兴趣或许有人会写一个包和 luci 应用甚至最终合并到 OpenWrt 中(现阶段还太早了)。

而且命令行很简单的,不一定需要 luci 面板。

但是这个软件由于 Linux 内核对某些架构 eBPF JIT 实现缺失的原因,不支持在这些架构上运行,比如流行的 MIPS 。现在只测试过了在 x86_64 和 ARM64 上可以运行。而且对于 OpenWrt 你还得手动编译打开 eBPF 的内核编译选项才行。
BoomMan
264 天前
期待详细分享原理和使用场景及使用 case
1423
264 天前
支持 op 做一期视频讲代码和使用, 开发和用户角度都涉及
不管是 B 站还是油管这类视频都算优质内容

而且也是趁热打铁
maybeonly
264 天前

有个问题是,有没有机会和现有的 ipt/nft 创建的 dnat 规则甚至 snat 结合使用呢?
有考虑过自己搓一个,用 ipt/nft 写 snat ,对于匹配某些东西的 snat 到某些特定的源端口(范围),然后在出口侧抓住这些内核 nat 过的源端口,对与这些源端口相关的报文/连接/端口进行 full cone 的……不过不知道能实现到什么程度,以及代价到底怎么样
再赞一遍。
p.s. 用 netns 测试了 v4 ,好像没有理睬用 ip l s mtu 设置的 mtu ,设置为 1480 仍然是按照 1500 拆包的
eh5
264 天前
@maybeonly
einat 支持指定 SNAT 源端口的范围,对于不在范围内的外部地址的出入包不做处理直接通过,所以只要做好端口区分混合 Netfilter 和 einat 没什么问题。你甚至可以 TCP 用 iptables/nftables masquerade 但 UDP 用 einat ,我在没完成 TCP NAT 功能时就是这么做的。

> 在出口侧抓住这些内核 nat 过的源端口,对与这些源端口相关的报文/连接/端口进行 full cone

这其实就是 netfilter-full-cone-nat 的思路,我最开始也是依赖 conntrack 做的, 但是这太 hacky 了而且维护状态很麻烦,并且 Netfilter 不是真的 “Endpoint-Independent Mapping” 所以会出现源端口复用的情况(即实际上是 “Address and Port-Dependent Mapping”)所以不可能实现真正的 Full Cone , 所以我重头开始实现了 NAT 。

> ip l s mtu 设置的 mtu ,设置为 1480 仍然是按照 1500 拆包的

你是说 IP fragmentation 么,很遗憾 eBPF 上实现不了,内核进来的包是多大 einat 处理过出去的包也多大。拆包应该是内核进行的。
maybeonly
264 天前
@eh5
1. 既然想着共存了,改 conntrack 确实不是好事情,不过似乎可以(部分)绕过 conntrack ?
当时考虑的是在入口和出口分别捕包,然后在出口处发现符合条件的报文后反查刚刚从入口抓过来的数据包,可能用到的匹配条件有:protocol+dst ip & port/id, length, 应该还有 ip 报文的 id 。
抓到该端口相关的东西后续由 ebpf 完成 nat ,不再经过内核。
2. 对于碎片,考虑发 icmpv6 type2 或者 icmp type3 回去?不确定能起多大作用。

由于我太懒了,以上全部都停留在设想,具体能实现到什么程度,在真实网络环境中运行咋样,以及对性能的影响,也只能说停留在设想中了。。。
p.s. ctrl+c 掉程序没有清理 tc 钩子,下次重启进程得手工删 tc 。。。

再次感谢楼主。
ysc3839
264 天前
有一说一,netfilter-full-cone-nat 这个项目不需要 patch kernel ,可以在普通 kernel 的基础上编译出 kernel module 。只要 kernel 不大改,理论上是可以同一个编译脚本支持多个版本内核的,参见 https://github.com/ysc3839/openwrt-official-builds-fullcone
不过还是免不了修改其他用户模式工具。
mikewang
264 天前
赞,
我之前一直没找到 Linux 环境上能模拟 CGNAT 行为的工具,用于测试 Natter 功能。
这个对我来说十分有用 hhh
eh5
264 天前
@maybeonly
> 对于碎片,考虑发 icmpv6 type2 或者 icmp type3 回去?不确定能起多大作用。

这个只有改了包的大小超过 MTU 时需要发回去,比如 Cilium 的 NAT64 对超过 MTU 的包就是这样的,但 einat 没改理应不需要啊。。 为什么内核没有拆包我就不知道了

> ctrl+c 掉程序没有清理 tc 钩子,下次重启进程得手工删 tc

正常情况下 ctrl + c 是会清 bpf 程序再退出的,可以`bpftool prog` 看一下有没有 `egress_snat` 和 `ingress_rev_snat`, 但 qdisc clsact 确实没删但也没什么大问题(主要是懒得查 qdisc 占用情况了,也不能全部删掉。。)
Love4Taylor
264 天前
@ysc3839 真一路到底的话直接 hardcode 内核 patch 里的参数,本质上给用户端工具打 patch 就是让其可以传参来开启功能吧,缺点就是用户没法后悔关 fullcone

0001 patch 走 iptables 经测试 tailscale 以及 docker 可以 nat1 ,0002 没试。
https://gist.github.com/love4taylor/a56985ecde4f16bab2cf54ee1c6a0c32
maybeonly
264 天前
@eh5
> 这个只有改了包的大小超过 MTU 时需要发回去,比如 Cilium 的 NAT64 对超过 MTU 的包就是这样的,但 einat 没改理应不需要啊。。 为什么内核没有拆包我就不知道了
理论上吓一跳链路比包“窄”就需要。虽然对于 ipv4 ,我是没见过哪个路由器是不分片而回 icmp 的(除非设置了 df )

> 正常情况下 ctrl + c 是会清 bpf 程序再退出的,可以`bpftool prog` 看一下有没有 `egress_snat` 和 `ingress_rev_snat`, 但 qdisc clsact 确实没删但也没什么大问题(主要是懒得查 qdisc 占用情况了,也不能全部删掉。。)
没有了,只是再启动会报个 warning ,并不影响正常工作的。
WARN einat: libbpf: Kernel error message: Exclusivity flag on, cannot modify
s82kd92l
264 天前
赞!可以提供 mips 版的二进制包么?
eh5
264 天前
@s82kd92l
MIPS 不支持,看上面关于 OpenWrt 的回复 https://www.v2ex.com/t/1029886#r_14545955
eh5
264 天前
@maybeonly 哦,这个确实,那目前要改 MTU 只能把相关转发的网卡也改了,避免在 einat 处理拆包,不过 BPF 里或许可以通过 `BPF_FIB_LKUP_RET_FRAG_NEEDED` 知道 MTU 信息从而发回 PMTU 错误包,不过这又是一大工程了。。得在 BPF 里构建一个包。。
Jirajine
263 天前
@eh5 #2 常见的家庭使用场景:
通过策略路由把在 netfilter 中根据元数据打了不同标记的包路由到不同的外部接口。
通过 ct 把来自外部接口发起的入站连接打标把相应的回复包从它们的来源接口发回去。
通过 ct 把来自某个 mac 地址的包打标从而能够在路由选择之前匹配到将要发送到该地址的包。

通过 nat64 能部署纯 ipv6 内网最大的好处就是简化双栈网络的复杂度,防火墙/路由/地址规则不用每个主机都重复两遍,双栈选择可以直接在 dns64 服务器中配置,不用为每个应用每个设备都单独配置,用容器跑个 p2p 应用不用修改镜像的 gai.conf (非 glibc 的应用也不一定遵守),有些系统(如 android )直接硬编码 prefer ipv6 无法修改。
常见的 dnsmasq/adguardhome/pi-hole/dnscrypt-proxy 等转发器都内置了 dns64 的功能,不过国内的公共 dns 没一个支持 dns64 的。
eh5
263 天前
@Jirajine

https://wiki.nftables.org/wiki-nftables/index.php/Netfilter_hooks 中的图表为示意图,
einat 和 Netfilter DNAT/SNAT 的区别是前者 NAT 发生在出口网卡的 ingress 和 egress ,后者发生在 Prerouting 和 Postrouting ,所以只要你的规则本来就在这之间不会有区别。当然如果你配置了 hairpin 路由配置那对你的策略路由或许会有影响,具体路由见配置 hairpin 后的 `ip rule`,另见 https://github.com/EHfive/einat-ebpf/issues/4#issuecomment-2001996895

> 通过 nat64 能部署纯 ipv6 内网最大的好处就是简化双栈网络的复杂度,防火墙/路由/地址规则不用每个主机都重复两遍

NAT64 对于业务组网确实有好处,但应用必须依赖 DNS64 ,对于有限制的业务网络当然没什么问题。 然而如果应用内附了 L3 IPv4 信息,比如以 IPv4 地址作为 Host 的 HTTP 服务(对于家庭/办公室网络是很常见的场景),那为了访问这些 IPv4-only 的应用必然得在终端设备配置静态 NAT46 ,而且也不可能覆盖所有非通用设备,那最后还是需要冗余的双栈网络覆盖所有终端设备。你说的简化网络复杂度和方便配置规则的好处也就不成立了。
Jirajine
263 天前
@eh5 #19 如果我理解的不错的话,把这个 ebpf 挂载到网卡上之后,对网络栈的其他部分而言完全透明,就好像根本没有使用 nat ,来自的内网地址的包会被公网的路由器发回来一样。
这样的话 netfilter/策略路由行为应该和分配到一个公网 ipv4 网段的路由器一样,你自己维护自己的映射表,和内核的 conntrack 也互不影响,多个外部网卡运行多个实例进行策略路由的情况下应该也不会有问题。
不过这个 harpin 看起来有点太 hacky ,这样做肯定会和很多东西冲突的,比如本地监听的程序和 netfilter dnat 。对于 masquarade 而言根本不需要做这种事情,没有哪个 p2p 的应用需要从内网访问映射的端口而不是直接连接。这种事情就交给 netfilter dnat 做,只要端口不冲突应该就不会冲突,hairpin 需要进行 snat ,会让应用丢失掉源地址信息,只有特别需要的场景才会专门配置 snat ,或者在用户态使用 proxy protocol 的转发来保留源地址信息。

关于 nat64 ,最主要是能够控制 android 这种不允许用户控制、且 ipv6 实现不完整的设备在双栈环境下的行为。
纯 ipv6 网络就不要部署以 ipv4 地址作为 host 的 http 服务啊,可以用 hostname ( dns/dhcp 自动分配和解析),或者配置一个简单好输入的 ULA 地址,实在不行可以手动为需要的设备配置静态 ipv4 地址(甚至 dhcp 也行),但是不要下发路由和网关,这样依然是一个纯 ipv6 网络。访问外部的 ipv4 http 服务也不需要专门配置,现代系统会自己自动进行本地 464XLAT ,虽然多了层套娃,但这种转换对网络侧透明,不会增加复杂度。
还有一个 nat64 与打洞相关的场景,一个 p2p 应用可以监听一个 GUA ipv6 的 socket ,然后同时接收来自打洞映射的 ipv4 入站和 GUA ipv6 入站,也就是纯 ipv6 下的 p2p 应用可以无缝的、透明的和纯 ipv4 下的 p2p 应用互联。无论 ipv4 的打洞是否成功,该应用都可以接收到入站,并且在不同的网络环境不需要单独处理或配置双栈。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1029886

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX