弄了个 wireguard 补丁,求熟悉 Linux kernel 协议栈的大神帮忙完善。。。。

2022-05-31 10:13:32 +08:00
 wacke

如题,补丁主要解决多 wan 下 wireguard 始终只使用最小跃点数的 wan 作为源 ip 的问题,本人不太熟悉 kernel 的网络协议栈,so 写出来的补丁比较 low ,求大神帮忙完善。

具体的始末可参考我在 github 的 issue:https://github.com/openwrt/packages/issues/9538

补丁如下:

diff -uNr linux-5.15.12_orig/drivers/net/wireguard/socket.c linux-5.15.12_wg/drivers/net/wireguard/socket.c
--- linux-5.15.12_orig/drivers/net/wireguard/socket.c	2021-12-29 19:29:03.000000000 +0800
+++ linux-5.15.12_wg/drivers/net/wireguard/socket.c	2022-05-27 15:27:40.000000000 +0800
@@ -17,6 +17,12 @@
 #include <net/udp_tunnel.h>
 #include <net/ipv6.h>
 
+static u32 dst_addr;
+static u32 src_addr;
+
+int receive = 0;
+int send = 0;
+
 static int send4(struct wg_device *wg, struct sk_buff *skb,
 		 struct endpoint *endpoint, u8 ds, struct dst_cache *cache)
 {
@@ -37,6 +43,13 @@
 
 	rcu_read_lock_bh();
 	sock = rcu_dereference_bh(wg->sock4);
+    
+	if ((receive) && (!send) || (send) && (!receive)) {
+		src_addr = dst_addr;
+	}
+	else {
+		src_addr = 0;
+	}
 
 	if (unlikely(!sock)) {
 		ret = -ENONET;
@@ -52,9 +65,11 @@
 		security_sk_classify_flow(sock, flowi4_to_flowi_common(&fl));
 		if (unlikely(!inet_confirm_addr(sock_net(sock), NULL, 0,
 						fl.saddr, RT_SCOPE_HOST))) {
-			endpoint->src4.s_addr = 0;
-			endpoint->src_if4 = 0;
-			fl.saddr = 0;
+			endpoint->src4.s_addr = src_addr;
+			endpoint->src_if4 = src_addr;
+			fl.saddr = src_addr;
+			send = 1;
+			receive = 0;
 			if (cache)
 				dst_cache_reset(cache);
 		}
@@ -62,9 +77,11 @@
 		if (unlikely(endpoint->src_if4 && ((IS_ERR(rt) &&
 			     PTR_ERR(rt) == -EINVAL) || (!IS_ERR(rt) &&
 			     rt->dst.dev->ifindex != endpoint->src_if4)))) {
-			endpoint->src4.s_addr = 0;
-			endpoint->src_if4 = 0;
-			fl.saddr = 0;
+			endpoint->src4.s_addr = src_addr;
+			endpoint->src_if4 = src_addr;
+			fl.saddr = src_addr;
+			send = 1;
+			receive = 0;
 			if (cache)
 				dst_cache_reset(cache);
 			if (!IS_ERR(rt))
@@ -77,8 +94,12 @@
 					    wg->dev->name, &endpoint->addr, ret);
 			goto err;
 		}
-		if (cache)
+		if (cache) {
+			if (receive) {
+				fl.saddr = src_addr;
+			}
 			dst_cache_set_ip4(cache, &rt->dst, fl.saddr);
+		}
 	}
 
 	skb->ignore_df = 1;
@@ -315,6 +336,11 @@
 static int wg_receive(struct sock *sk, struct sk_buff *skb)
 {
 	struct wg_device *wg;
+	struct iphdr *ip_header = (struct iphdr *)skb_network_header(skb);
+
+	dst_addr = ip_header->daddr;
+	receive = 1;
+	send = 0;
 
 	if (unlikely(!sk))
 		goto err;

补丁主要的核心就是通过读取 kernel skb_buff 中的目的地址,并把该地址应用到 wireguard 的源地址上,补丁我自己测试貌似没啥大问题,只是考虑到我本人的编程水平,求大神帮忙完善。

ps:wireguard 官方貌似不认为这是 bug 。。。

4415 次点击
所在节点    Linux
44 条回复
yanqiyu
2022-05-31 13:20:33 +08:00
这种情况是典型的非对称路由?
这肯定不是好事情,建议能解决非对称路由的问题就先解决它,比如导入一个更合理的路由表解决出入路径不一致的问题
确实在 wireguard 内部维护一个 saddr 状态也是一个办法,但是上游肯定不会喜欢,因为 wireguard 没必要为了非对称路由擦屁股
wacke
2022-05-31 13:27:33 +08:00
@yanqiyu 我是个小白。。。没有丰富的路由相关的理论知识,更没有内核协议栈及内核编程的知识背景。。。。只能从实际应用出发,尝试解决相关的问题。。。想要让上游接受,我差太远了。。。。另外就是想等待上游解决,估计还是自己实现来得靠谱。。。这个问题已经等了 5 年以上了。。。
wacke
2022-05-31 13:29:46 +08:00
@yanqiyu 还有一个情况就是,从我理解的 wireguard 源码来看,是 wireguard 自己重置了 ip mark 以及传入连接的目标 ip 的。。。
lcdtyph
2022-05-31 13:56:22 +08:00
@wacke 你再分析分析这代码,多 peer 快速发包的时候真的能正常工作吗? wg_receive 和 send 又不是一定成对出现的
wacke
2022-05-31 14:05:05 +08:00
@lcdtyph 所以才求大神帮忙完善啊,我自己测试,至少 2 个 peer 同时连接我路由的 pppoe-wan2 ,是正常的。。。
wacke
2022-05-31 14:12:52 +08:00
@lcdtyph 这个 patch ,我自己也改了好多个版本了,既要保证传入连接的 ip 被正确应用,还要保证当已保存的 ip 失效时,wireguard 能重置这个失效的 ip ,另外还得保证配合 mwan3 的对于 wireguard 对端的策略路由正常工作,头疼。。。
此前我尝试使用 `inet_confirm_addr(sock_net(sock), NULL, 0, &src_addr, RT_SCOPE_LINL)` 这个函数,貌似没起作用。。。最后发出来的补丁,是我 debug 了 n 次之后貌似没问题的版本。。。
geekvcn
2022-05-31 14:13:20 +08:00
@wacke 你需要自己配置对称路由
wacke
2022-05-31 14:21:31 +08:00
@geekvcn 那么求解,该如何配置,才能实现 wireguard 作为 server 时,client 从任一 wan 口连入时,wireguard 的握手包从连入的 wan 口出去?针对 wireguard 的监听端口做策略路由吗?
geekvcn
2022-05-31 16:04:57 +08:00
@wacke 搜关键词对称路由
wacke
2022-05-31 16:12:08 +08:00
@geekvcn 好吧。你这回了等于没回。。。
qakito
2022-05-31 22:46:05 +08:00
1. IPv4/IPv6 源地址选择是有 RFC 的,在未指定源地址的情况下,出接口地址优先,你看有没有办法能通过配置 bind 指定接口地址
2. metric 都不同,根本不是多出口问题,路由始终选择 metric 最低的路由
3. wireguard 我不熟悉,但是作为一个 tunnel responder ,你的 tunnel source 不应该是客户端在建立 tunnel 时决定的么?
4.有没有办法在 tunnel 建立的时候动态注入路由
qakito
2022-05-31 22:55:25 +08:00
5. 粗略翻了一下 wireguard 的介绍,wireguard 是没有 tunnel connect 这个过程的,主要问题是 client 在 NAT/防火墙内侧么?回程的源地址改变了无法穿过 NAT/防火墙是么?
wacke
2022-05-31 23:10:26 +08:00
@qakito 相当一部分的 NAT/防火墙,确实会因为回程地址变了,会丢弃数据包,导致无法握手。。。但其实根源还是 wireguard 本身, 收到数据包后,主动去重置源地址及传入的 ip mark 。。。
qakito
2022-05-31 23:17:59 +08:00
顺便说一句,用全局变量绝对不可行,根本无法重入
比如你这种多 wan 的情况,clientA -> eth0 和 clientB -> eth1 交替访问
qakito
2022-05-31 23:47:05 +08:00
还有 endpoint->src_if4 是 netdevice 的 ifindex ,不是 src_addr
wacke
2022-06-01 07:52:18 +08:00
@qakito
>顺便说一句,用全局变量绝对不可行,根本无法重入
OK ,我尝试把 static 去掉试试。。。
>还有 endpoint->src_if4 是 netdevice 的 ifindex ,不是 src_addr
这个改回原来的 “endpoint->src_if4 = 0;” 吗?

目前来说,我自己从公司的电脑及手机同时分别连接路由的两个 wan ,没发现有啥异常。。。
wacke
2022-06-01 09:54:19 +08:00
@qakito

更新了下补丁,粗略测试,貌似没啥问题。。。

```
diff -uNr linux-5.15.12_orig/drivers/net/wireguard/socket.c linux-5.15.12_wg/drivers/net/wireguard/socket.c
--- linux-5.15.12_orig/drivers/net/wireguard/socket.c 2021-12-29 19:29:03.000000000 +0800
+++ linux-5.15.12_wg/drivers/net/wireguard/socket.c 2022-06-01 08:18:00.990080098 +0800
@@ -17,6 +17,12 @@
#include <net/udp_tunnel.h>
#include <net/ipv6.h>

+u32 dst_addr;
+u32 src_addr;
+
+int receive = 0;
+int send = 0;
+
static int send4(struct wg_device *wg, struct sk_buff *skb,
struct endpoint *endpoint, u8 ds, struct dst_cache *cache)
{
@@ -37,6 +43,13 @@

rcu_read_lock_bh();
sock = rcu_dereference_bh(wg->sock4);
+
+ if ((receive) && (!send) || (send) && (!receive)) {
+ src_addr = dst_addr;
+ }
+ else {
+ src_addr = 0;
+ }

if (unlikely(!sock)) {
ret = -ENONET;
@@ -52,9 +65,11 @@
security_sk_classify_flow(sock, flowi4_to_flowi_common(&fl));
if (unlikely(!inet_confirm_addr(sock_net(sock), NULL, 0,
fl.saddr, RT_SCOPE_HOST))) {
- endpoint->src4.s_addr = 0;
+ endpoint->src4.s_addr = src_addr;
endpoint->src_if4 = 0;
- fl.saddr = 0;
+ fl.saddr = src_addr;
+ send = 1;
+ receive = 0;
if (cache)
dst_cache_reset(cache);
}
@@ -62,9 +77,11 @@
if (unlikely(endpoint->src_if4 && ((IS_ERR(rt) &&
PTR_ERR(rt) == -EINVAL) || (!IS_ERR(rt) &&
rt->dst.dev->ifindex != endpoint->src_if4)))) {
- endpoint->src4.s_addr = 0;
+ endpoint->src4.s_addr = src_addr;
endpoint->src_if4 = 0;
- fl.saddr = 0;
+ fl.saddr = src_addr;
+ send = 1;
+ receive = 0;
if (cache)
dst_cache_reset(cache);
if (!IS_ERR(rt))
@@ -77,8 +94,12 @@
wg->dev->name, &endpoint->addr, ret);
goto err;
}
- if (cache)
+ if (cache) {
+ if (receive) {
+ fl.saddr = src_addr;
+ }
dst_cache_set_ip4(cache, &rt->dst, fl.saddr);
+ }
}

skb->ignore_df = 1;
@@ -315,6 +336,11 @@
static int wg_receive(struct sock *sk, struct sk_buff *skb)
{
struct wg_device *wg;
+ struct iphdr *ip_header = (struct iphdr *)skb_network_header(skb);
+
+ dst_addr = ip_header->daddr;
+ receive = 1;
+ send = 0;

if (unlikely(!sk))
goto err;

```
qakito
2022-06-02 16:50:53 +08:00
@wacke
不好意思,我现在没有方便的环境调试
我想等验证后再分享我的想法
wacke
2022-06-02 19:22:52 +08:00
@qakito
没有关系,这个补丁,至少在我自己的环境下,实现了我的需求。。。
qakito
2022-06-06 23:39:03 +08:00
看了下 wireguard 的实现
1. 对方的目的地址在本端是有记录的,就记录在 wg_peer 的 endpoint->src4 里
这个包从哪个 netdevice 进来的也有记录,同样记录在 wg_peer 的 endpoint->src_if4 里
只不过在查路由的时候,如果查到的出口与入口不相同(rt->dst.dev->ifindex != endpoint->src_if4),就会让路由模块重新选择源地址
2. 你的修改只是不让路由模块重新选择源地址,但是出口还是错的,在某些情况下回程包仍有可能被丢弃
3. 个人认为上游不接受你的修改是因为已经提供了 fwmark 来解决你的问题
举例来说
ip route add default dev ens3 table 1000 #创建路由表 1000 ,默认路由出口是 ens3 ,如果是 p2p 接口这样就 ok ,否则要写成下一跳
wg set wg0 fwmark 1234 # 由 wg0 接口出的包一律打上 fwmark 1234
ip rule add fwmark 1234 table 1000 # 打上 fwmark 1234 的包查路由表 1000

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

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

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

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

© 2021 V2EX