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

70 天前
 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 模式。

2759 次点击
所在节点    宽带症候群
31 条回复
eh5
68 天前
@Jirajine

> 对于 masquarade 而言根本不需要做这种事情,没有哪个 p2p 的应用需要从内网访问映射的端口而不是直接连接

RFC 4787 ,REQ-9: A NAT MUST support "Hairpinning"

需要,而且很需要,如果没有 Hairpin ,你自己的外部地址+端口外部可以访问但内部不能访问不是很奇怪么,而且这个内部包括整个 NAT 后面的网络。没有 Hairpin ,内部网络间的 P2P 就无法联通。

> 这样做肯定会和很多东西冲突的,比如本地监听的程序和 netfilter dnat

你有点想当然了,hairpin 的流量最终是会从网卡返回来的,对于需要 NAT 的流量会进行 NAT ,不需要的流量会最终 loopback 回来,这个路由规则相当于在正常的 (lan -> local) 包流中加入 einat (lan -> einat > local),并不会有什么所谓的冲突。

> 网络就不要部署以 ipv4 地址作为 host 的 http 服务啊

这不是你可以控制的,没有域名使用 IPv4 作为地址的公共网站还是有的,用户也有需求访问这些网站。而且可以嵌入 L3 IPv4 地址的协议也不止 HTTP ,比如 BT 。对于家庭/办公室网络这些都是刚需。

> 现代系统会自己自动进行本地 464XLAT

我不知道有哪个 Linux 发行版默认配置了这个,而且家庭用户的设备也不一定是高集成度的”现代系统“。

关于 NAT64 你单方面的阐述它对你限制性应用场景下方便网络配置的必要性就到此为止吧,这差不多跑题了,建议另开一主题先让大家达成共识。

而且你我都不在一个频道上,我的论点是 NAT64 下的 IPv6-only 对于家庭用户来说为了配置它所付出的并不能得到相比传统双栈网络在网络连通性、速度方面的提升,作为家庭用户我个人并没有动机去使用它并花费精力实现它,所以对于在 einat 中实现 NAT64 的优先级为低。

我对 einat 预想的使用场景充其量是家庭和办公室网络,而且非图灵完备的 eBPF 程序能实现的功能是有限制的,对于其他场景和更高级的功能建议使用企业级 NAT 设备。。
eh5
68 天前
@Jirajine

> 如果我理解的不错的话,把这个 ebpf 挂载到网卡上之后,对网络栈的其他部分而言完全透明,就好像根本没有使用 nat ,来自的内网地址的包会被公网的路由器发回来一样。

是的,我应该提一嘴 einat 是从头开始写的不依赖 conntrack 的独立 NAT 实现,einat 的 README 里也有稍微提到,https://github.com/EHfive/einat-ebpf?tab=readme-ov-file#alternatives

最开始的版本的确是类似一些 eBFP 负载均衡应用通过维护 Netfilter conntrack 实现的,但是 eBFP 无法控制初始 conntrack 的源端口分配所以并不能把 “Address and Port-Dependent Mapping” 的 conntrack 系统限制为 “Endpoint-Independent Mapping”,所以初始版本是一个失败的实现,见 https://github.com/EHfive/einat-ebpf/tree/legacy 。于是就决定从头写一个独立的 NAT 实现。
maybeonly
68 天前
@eh5
> “和很多东西冲突”

也曾考虑过这方面的问题,在真实部署中会很依赖端口选择(配置)
确实,代码本身不会利用自己已经用过的 snat 后的源端口,但是如果这些端口和其他 dnat 规则冲突,抑或被其他程序占用呢?运营商的 cgnat 没有这个问题,毕竟人家的 ip 就是专供 nat 用的。家用的话,可能不得不特别小心 dnat 的选择,以及用 ip_local_port_range 隔离了。然后发现 dnat 搭配的 hairpin 怎么办? emmmm 。。。

当时自己考虑的结果是,不得不和 conntrack 做某种程度的“交易”才能解决这个问题。
交给 conntrack 选 snat 后的源端口就没这个问题,只要识别到这个内部 ip:port 和外部 ip:port 的组合,后续不走 conntrack 按照 fullcone 的实现就好了。不仅能指定源端口,也可以实现比如针对某个 ip 实现 fullcone ,etc 。

同时被解决的另一个问题是,什么都没配置的话可以继续走系统的 conntrack ,只有配置了命中了正确的端口范围才能 fullcone 。在实践上也会是相对比较安全的。

然而咱终究是懒得 1b ,对勤奋的楼主表示深深的敬意。
eh5
68 天前
> 但是如果这些端口和其他 dnat 规则冲突,抑或被其他程序占用呢
> 可能不得不特别小心 dnat 的选择,以及用 ip_local_port_range 隔离了

嗯,对于本来预想接受初始入站连接的端口(不管是本机的监听服务还是对外部地址端口的 DNAT )是应该被排除在 einat 的 NAT 端口范围外。我当初也是预想到了这种情况所以在 einat 中实现了端口范围限定,并且对 TCP 和 UDP 以 20000-29999 为默认端口范围从而隔离可能常用的低端口和 ip_local_port_range (默认 32768-61000 )。

einat 对于范围内的端口则是严格执行 EIM + EIF ,其中重要的一点是只允许在有从内部发出的初始出站连接后才能打开相应的源端口接受后续其他入站连接。而且即使对于以网卡外部地址+范围内端口为 source 的初始出站连接也是如此(即范围内的端口有可能被重映射,比如 egress: 20000 -> 20001, ingress: 20001 -> 20000 ),但因为 einat 如你所说是透明的,这其实和其他以私有地址为来源的出战连接没多少不同,见 https://github.com/EHfive/einat-ebpf/blob/9e6f8e6720c7244f40693e2bd119d268b855afd5/src/bpf/einat.bpf.c#L1879-L1892

PS:话说我应该在博客里写这些的。。
eh5
68 天前
@maybeonly
> 但因为 einat 如你所说是透明的
*但因为 einat 如 @Jirajine 所说是透明的

抱歉,刚起床迷糊把你看成他了。。
Jirajine
68 天前
@eh5 你说的对,内部网络之间(非同一子网)的 p2p 应用确实需要最外层的 nat 网关支持 hairpin ,这是我之前忽略了的情况。但 nat 就是这样的 hack ,开启了 hairpin 会 break 某些应用,关闭了 hairpin 会 break 另一些应用。是否需要 hairpin 取决于具体需求,比如现在大部分家庭用户都是无公网 ipv4 (意味着 hairpin 发生在最外层的运营商 cgnat 上)+公网 ipv6 (对任何 sane 的 p2p 应用都是首选),实现 hairpin 的意义就不是那么大。
hairpin 最大的问题是 snat ,snat 会丢失源地址信息,当你收到一个入站包的时候,你必须 remotely 知道你要转发的目的主机到源主机之间的路由是否还经过你,才能决定是否进行 snat ,这很难正确的实现,你只能假定一些简单的网络拓扑下的情况。
具体到通过策略路由把目的为本机的包强行发到外部接口上,这已经 break 了其他所有人预期的行为,而你无法提前知道和测试所有的使用场景。比如本机端口冲突、多实例多地址个外部接口和类型( eth/pppoe/wireguard/其他基于 tun 的代理程序)的动态路由场景,动态 ipv4/ipv6 地址。并且这会让所有目的的为该地址的入站包全部发送到外部网卡再转发回来,那么本来从内网入站的包源网卡成了外部网卡(并且经过两遍 netfilter ),正常系统都会默认的允许内网入站、禁止外网入站的防火墙规则也就 break 了。更不用说如果内网接口都是 virtio 的虚拟网卡,这种转发会非常严重的降低性能(这还没有考虑 ebpf 本身的开销,在嵌入式设备上应该也是不可忽略的)。
我觉得既然因为网卡上的 ebpf 无法捕获发往本机的包,那干脆直接让用户态的服务 reserve 所有活跃记录中的端口,然后对 hairpin 的请求直接在用户态转发,这样也能解决端口冲突问题(其他程序没有办法得知 nat 使用了哪些端口)。

NAT64 并不是限制性应用场景,646xlat 不同于 nat64 ,它是无状态的静态重写。对于支持 464xlat 的终端,并不会 break 任何除了 nat44 已经 break 的应用,唯一的 regression 就是网关有没有实现 fullcone 。当我说所有现代设备,我指的是所有面向消费者的设备,所有可以插 sim 卡的系统都必须实现 464xlat (因为很多运营商的移动数据已经是纯 ipv6 了),包括 windows/android/ios ,linux 没有把它做到开箱即用确实 behind 了。对于不支持的设备,它们不太可能依赖内嵌 ipv4 的应用,这些设备要么压根根本不需要公网联通,要么可以在纯 ipv6 环境工作。
如果你考虑连通性方面的好处,那么毫无疑问增加 ipv6 的 adoption 是对每一个 p2p 用户都有好处的。ipv6 现在的状态已经达到了只要你是接入 isp 的就已经普及了,没有的都是本地网络不支持(云厂商/企业/家庭网络用户自己),去搜一下会发现用户自己禁用 ipv6 的原因,像什么卡/打不开网页禁用 ipv6 就好了,都是因为复杂度或某些系统实现的问题(没错,又是 android ),而这些问题在纯 ipv6 环境下却可以解决和避免。如果以后因为缺少一个 fullcone 的 nat64 实现(上游那些生活在公网 ip 非常充裕的国家的人不太可能去支持)而 block ipv6 的 adoption 的话,对 p2p 应用绝对是不好的,用户希望能够同时拥有 ipv4 fullcone 和 ipv6 gua 的连通性。从端到端连通性的角度来看,fullcone 的 nat64 比 nat44 更 canonical ,毕竟打洞总是 hack ,ipv6 才是“正确”。

不过实现 nat64 确实会引入额外的复杂度,虽然在 nat44 的基础上没有额外的状态只需要一些静态重写,不熟悉 ebpf ,但是现在只有重写地址就要两千行了(可能大部分是维护状态?),再加上构造包可能会不容易,我记得几年前还看到有新闻把 rust 编译到 ebpf ,现在也没发展了。
上面回复有提到需要构造 icmp 报文返回 mtu 问题,这应该搞错了吧,分片或者返回 icmp 应该是网络栈或者网卡的事,nat 只是重写包的地址,甚至是否 EIF 都是防火墙配置的,和 nat 有啥关系。
Jirajine
68 天前
@eh5 说道性能,还有一个值得考虑的问题。现在常用的家用嵌入式路由器的 cpu 根本无法支持高带宽场景下的 nat ,而是 offload 到专用硬件处理。考虑到这种硬件应该是负责重写包而不是负责连接追踪,实现支持 hw offload 的 fullcone nat 应该是可能的,只不过能否通过 ebpf 就不知道了。不然的话,很可能受限于嵌入式设备的性能而让用户的带宽严重降低。
s82kd92l
67 天前
其实 linux 内核里 openvswitch 社区热度还蛮高的,xdp dpdk bpf 甚至是 p4 后端都有人做实现。fullcone nat 如果用 ovs 或者 openflow 做用户态实现可能适用面更广呢,性能应该不输直接 bpf
eh5
67 天前
@Jirajine

> 具体到通过策略路由把目的为本机的包强行发到外部接口上,这已经 break 了其他所有人预期的行为

所以你不是已经知道了么?默认的规则已经覆盖了最常见的场景了,你对于自己的网络有特殊配置完全可以关闭 hairpin 或者定制你自己的路由规则不是么,请注意这个主题的内容是关于 einat ,如果你对于任何 einat 的实现有建议意见提升方法的话请发 issue 。

> 这种转发会非常严重的降低性能(这还没有考虑 ebpf 本身的开销,在嵌入式设备上应该也是不可忽略的)

请你先 profile 一下给出比较数据,或者给出 source 。

> NAT64

关于你对 NAT64 的话题已经 off-topic 了,我也不会再重复我之前的观点(当然你也没有看到我的侧重点而是单方面的输出),请单发主题或者在 https://github.com/EHfive/einat-ebpf/issues/3 下给出评论。

> 说道性能,在常用的家用嵌入式路由器的 cpu 根本无法支持高带宽场景下的 nat ,而是 offload 到专用硬件处理

off-topic ,偷换概念,比较的对象应该是 Netfilter conntrack SNAT/DNAT 和 einat 而不是 NAT offloading 和 einat 。

鉴于你一直无视主题单方面输出观点,我在此主题不会再回复你的评论。
eh5
67 天前
@s82kd92l
不是很了解 Open vSwitch 和 OpenFlow ,但粗略的看了下似乎是基于匹配规则进行包的转发,而且它的 NAT 似乎也是基于 Netfilter conntrack 的,见 https://ovs-istokes.readthedocs.io/en/latest/tutorials/ovs-conntrack.html

但要实现 Endpoint-Independent Mapping 还是需要实时地在包解析阶段就查询更新 mapping 表。如果不使用 conntrack 通过用户态的监控并(滞后地?)添加映射规则那实时性肯定不够的。而如果使用 conntrack 那和我上面所说在 eBPF 中动态维护 conntrack 表没有区别,你只能去控制 filtering 行为而无法控制 mapping 行为,最终结果还是会回落到 Netfilter conntrack 系统的 “Address and Port-Dependent Mapping” 行为。

如果 会有某一天 kernel 实现了 EIM + EIF ,那也只会是独立于 conntrack 的另外一套系统,因为 conntrack 本来就是 Address and Port-Dependent 的。
Jirajine
66 天前
@eh5 你认为 nat64 是限制性应用场景,我解释 xlat 能兼容所有 nat44 的场景;你认为 nat64 对家庭用户比双栈网络没有连通性和速度方面的提升,我告诉你事实上是有,很多用户双栈网络的连通性和速度还不如 ipv4 only ,他们关于禁用 ipv6 的帖子搜索引擎一搜一大把。对了,作为一个 side effect ,nat64 还能让你不用为了实现 hairpin 而把所有内网入站连接都从外部网卡绕一圈,你认为这种做法没问题那就没问题吧,记得告诉你的用户在防火墙中允许来自公网接口的入站连接以及相应的 security implication 。

当然你认为以上都是 off topic ,“没有看到你的侧重点单方面输出”,没有问题,是否实现或以什么优先级实现 nat64 是你的自由,我并没有要求你做什么,其他问题跟用户说去吧,本帖不用再回复。

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

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

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

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

© 2021 V2EX