zfl9 / ipt2socks

将 iptables/nftables 传入的透明代理流量转为 socks5 流量的实用工具
GNU Affero General Public License v3.0
447 stars 103 forks source link

#fix udp Segment fault when timeout (and default timeout of udp) #18

Closed yuchting closed 4 years ago

yuchting commented 4 years ago

I debugged this segment fault bug and found if you change the timeout time as 500 millisecond, it would be reproduced easily in a short period: image

And finally found why and fixed it.

I don't know why the original default idle time need to be set as 300 sec, so I changed it into 5 sec. Could you explain detail for me? Thanks!

zfl9 commented 4 years ago

实际上问题根源不在这里,出现段错误的原因是我对 libuv 的 uv_close() 接口理解错了。libuv 的 uv_close() 调用后,对应 handle 上的 write_req 的 write_cb 不会收到 UV_ECANCELED(我写的时候是假设会这样的,但是实际上也没多测试,这是导致段错误的根本原因)导致对应的 handle 被释放后,write_cb 才被调用到,于是对无效指针进行解引用。然后gg了。其实在 dev 分支,我已经开始用 libev 进行重写了。不想用 libuv 了,libuv 包装的太重。不过由于时间关系,还没写完。。。

有个很类似的 issue 可以看下,uv_close 和 write_cb 的。https://github.com/libuv/libuv/issues/522

zfl9 commented 4 years ago

另外你改的这个 udp 空闲超时是不对的,这个超时与 Full cone NAT 有关系,具体我也不好解释。反正不能改为过低。不然达不到 Full cone NAT 的效果。

yuchting commented 4 years ago

实际上问题根源不在这里,出现段错误的原因是我对 libuv 的 uv_close() 接口理解错了。libuv 的 uv_close() 调用后,对应 handle 上的 write_req 的 write_cb 不会收到 UV_ECANCELED(我写的时候是假设会这样的,但是实际上也没多测试,这是导致段错误的根本原因)导致对应的 handle 被释放后,write_cb 才被调用到,于是对无效指针进行解引用。然后gg了。其实在 dev 分支,我已经开始用 libev 进行重写了。不想用 libuv 了,libuv 包装的太重。不过由于时间关系,还没写完。。。

有个很类似的 issue 可以看下,uv_close 和 write_cb 的。libuv/libuv#522

谢谢你的回复。

事实上,一点也不懂libuv,在测试的时候也没有往libuv库可能问题去联想。

之所以我会怀疑到是超时所作的操作有的问题,是因为我在debug的时候发现每次Segment fault都是在300秒timeout的是否发生的,需要等待5分钟多。我首先就需要重现它,于是我就把timeout 设置成 0.5 秒,一下就出现了。

于是我仔细看了一下代码,发现了每次出错都是在 HASH_ADD,我对GNU C的一些库不是很熟悉,貌似是处理一个全局列表的时候的问题,一开始以为是多线程的问题,后来发现,udp的代理是单线程的,所以有估计错误了。

在不断熟悉代码的过程中,发现貌似是在 timeout之后,把 svrentry_t cltentry_t两个指针资源释放了,但是HASH table g_udp_cltcache和 g_udp_svrcache里面并没有删除,所以在遍历的时候是否会访问到那些已经free过的 svrentry_t cltentry_t呢

于是我在timeout的处理函数里面加了 Hash 表剔除:

image

然后测试了一下果然不出异常了。

然后自己使用了48小时左右,没有崩溃,没有内存泄漏,至于是否还有其他的问题,我也不是很清楚了。

这个PR的主要代码就是上面两行代码,至于timeout那个变量的小更改,我只是强迫症犯了,所有的地方都乘以1000,不如一开始就乘上了。

yuchting commented 4 years ago

另外你改的这个 udp 空闲超时是不对的,这个超时与 Full cone NAT 有关系,具体我也不好解释。反正不能改为过低。不然达不到 Full cone NAT 的效果。

我刚才才google了一下“Full Cone”,发现是和内网穿透有关的设置,就是从外网可以直接访问内网的端口(即便不用路由器的端口映射),貌似是利用UDP协议发出去的时候,网关的端口延迟关闭做的,貌似以前的P2P现在就是这样的?不是很清楚。

不过一般来说,ipt2socks的使用都是在内网,socks5代理服务器也是在内网,是不是就不需要这个功能了呢?所以早点释放udp资源会更好?

只是建议。

yuchting commented 4 years ago

另外还有一个小建议,我看到ipt2socks使用了很多全局变量,但是又可以支持多线程。我倒是觉得可以想ss-libev那样,使用多进程方式,默认reuseport,用户自己开多个进程,让内核做负载均衡,在编码的复杂度上面就会小很多,不需要管thread的创建、销毁、同步锁之类的问题。

也是一个小建议。

zfl9 commented 4 years ago

我刚才才google了一下“Full Cone”,发现是和内网穿透有关的设置,就是从外网可以直接访问内网的端口(即便不用路由器的端口映射),貌似是利用UDP协议发出去的时候,网关的端口延迟关闭做的,貌似以前的P2P现在就是这样的?不是很清楚。

不过一般来说,ipt2socks的使用都是在内网,socks5代理服务器也是在内网,是不是就不需要这个功能了呢?所以早点释放udp资源会更好?

只能说你理解错了。和是否使用在内网没关联。这就是用于支持 full cone nat 的一个东西。你不用纠结这个超时时间。具体的一些东西我目前没有太多时间详细说。要是有我也早就把libev版本写完了。。

zfl9 commented 4 years ago

另外还有一个小建议,我看到ipt2socks使用了很多全局变量,但是又可以支持多线程。我倒是觉得可以想ss-libev那样,使用多进程方式,默认reuseport,用户自己开多个进程,让内核做负载均衡,在编码的复杂度上面就会小很多,不需要管thread的创建、销毁、同步锁之类的问题。

也是一个小建议。

现在就是 reuseport,内核做负载均衡。多进程和多线程都行。没有区别。ipt2socks 没有使用任何形式的线程锁,都是每个线程一个独立的 eventloop。不存在任何锁的竞争。至于多进程还是多线程,看个人喜好。我只是认为多线程方便,也省一点内存,毕竟符合我的强迫症。

zfl9 commented 4 years ago

另外我觉得我们讨论的方向有点问题,实际上 ipt2socks libuv 版本出问题的原因就在于理解错了 uv_close() 的一些细节。虽然可以花点时间纠正这个错误的使用方式,不过由于强迫症作祟,还是决定重写,不用 Libuv 了,用最轻量的 libev。

zfl9 commented 4 years ago

实际上问题根源不在这里,出现段错误的原因是我对 libuv 的 uv_close() 接口理解错了。libuv 的 uv_close() 调用后,对应 handle 上的 write_req 的 write_cb 不会收到 UV_ECANCELED(我写的时候是假设会这样的,但是实际上也没多测试,这是导致段错误的根本原因)导致对应的 handle 被释放后,write_cb 才被调用到,于是对无效指针进行解引用。然后gg了。其实在 dev 分支,我已经开始用 libev 进行重写了。不想用 libuv 了,libuv 包装的太重。不过由于时间关系,还没写完。。。 有个很类似的 issue 可以看下,uv_close 和 write_cb 的。libuv/libuv#522

谢谢你的回复。

事实上,一点也不懂libuv,在测试的时候也没有往libuv库可能问题去联想。

之所以我会怀疑到是超时所作的操作有的问题,是因为我在debug的时候发现每次Segment fault都是在300秒timeout的是否发生的,需要等待5分钟多。我首先就需要重现它,于是我就把timeout 设置成 0.5 秒,一下就出现了。

于是我仔细看了一下代码,发现了每次出错都是在 HASH_ADD,我对GNU C的一些库不是很熟悉,貌似是处理一个全局列表的时候的问题,一开始以为是多线程的问题,后来发现,udp的代理是单线程的,所以有估计错误了。

在不断熟悉代码的过程中,发现貌似是在 timeout之后,把 svrentry_t cltentry_t两个指针资源释放了,但是HASH table g_udp_cltcache和 g_udp_svrcache里面并没有删除,所以在遍历的时候是否会访问到那些已经free过的 svrentry_t cltentry_t呢

于是我在timeout的处理函数里面加了 Hash 表剔除:

image

然后测试了一下果然不出异常了。

然后自己使用了48小时左右,没有崩溃,没有内存泄漏,至于是否还有其他的问题,我也不是很清楚了。

这个PR的主要代码就是上面两行代码,至于timeout那个变量的小更改,我只是强迫症犯了,所有的地方都乘以1000,不如一开始就乘上了。

不过你这么热心的调试、测试也是辛苦了。我也有点强迫症:grin:

yuchting commented 4 years ago

关于 reuseport,目前的master分支里面可能有些小bug把: image

单线程无法启动: image

还有,ipt2socks目前udp是单线程的:

image

如果也需要使用多线程,那么在 g_udp_cltcache g_udp_svrcache两个hash table的处理就不能用全局变量,或者使用锁。所以我说为了简便,建议直接引导用户多进程,放弃多线程,多进程也没有占多少内存。只是建议,酌情考虑。

最后,我这个PR加了两行HASH table的删除代码,对ipt2socks的libuv版本打了一个小补丁,让segment fault暂时好了,你也可以加上,提交,让其他用户可以正常使用先,最后需要正常,还是等你的libev版本。

zfl9 commented 4 years ago

udp不能多进程、多线程。不然会破坏 full cone nat。

zfl9 commented 4 years ago

关于 reuseport,目前的master分支里面可能有些小bug把: 这个我感觉是 ipv6_only 的问题。你的系统是什么。 之所以只在启用了多线程时启用端口重用是因为只有 linux 3.9+ 内核才支持 reuseport。

zfl9 commented 4 years ago

关于 reuseport,目前的master分支里面可能有些小bug把: 这个我感觉是 ipv6_only 的问题。你的系统是什么。 之所以只在启用了多线程时启用端口重用是因为只有 linux 3.9+ 内核才支持 reuseport。

可以试试 ipv4only 或 ipv6only。看报错有点像是这个问题。

zfl9 commented 4 years ago

另外,干脆再麻烦你帮我去掉那个 reuseport 的判断吧,默认启用。先不考虑 3.9 以下的。等 libev 版本出来再看。辛苦辛苦,先谢了。

zfl9 commented 4 years ago

https://github.com/zfl9/ipt2socks/commit/2ca99f4392cdbbbb5d303ff117dacc89ee572c38

是最近才改的。之前其实是默认启用 reuseport 的。

yuchting commented 4 years ago

系统版本如下: Linux localhost.localdomain 4.19.6-1.el7.elrepo.x86_64 #1 SMP Sat Dec 1 11:58:18 EST 2018 x86_64 x86_64 x86_64 GNU/Linux CentOS Linux release 7.6.1810 (Core)

ip4/6_only 是可以用的,但是偶尔也不能用,我也不知道为啥。 image