Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Porting NATMap to Windows #89

Open
MikeWang000000 opened this issue Feb 9, 2025 · 19 comments
Open

Porting NATMap to Windows #89

MikeWang000000 opened this issue Feb 9, 2025 · 19 comments
Labels
enhancement New feature or request

Comments

@MikeWang000000
Copy link
Collaborator

将 NATMap 移植到 Windows 平台

先开一个坑:https://github.com/MikeWang000000/natmap_win32
(什么时候能填上就不知道了)


使用 MSYS2 编译,目前仅需修改少部分即可通过。
参见:https://github.com/MikeWang000000/natmap_win32/tree/master/patches

一些问题需要处理的:

  1. kqueue 相关 (<sys/event.h>) ,Windows 上使用 libkqueue 替代。而 libkqueue 没有适配 MSYS,先用 clang 环境编的 dll 凑合(尚不清楚有无兼容性问题)

  2. Windows 下无 SO_REUSEPORT,设置 SO_REUSEADDR 就可以达到同等效果了

  3. 链接器没找到 hev_task_execute(),估计汇编那可能还有点问题要调一下,目前做了个空的函数让链接通过

  4. MSYS 下的 mmap() 没有实现 MAP_STACKhev_task_stack_new() 可能受影响

  5. Windows 下 socket 行为的差异,可能需要微调


因为上述问题,NATMap 在 Windows 上目前还没有实质功能可用,只能显示帮助页面。
但是可以算是迈出了从无到有的第一步吧,后面再慢慢填。不过我的时间还是比较有限(该死的 996)

Image
GitHub Actions: https://github.com/MikeWang000000/natmap_win32/actions

@MikeWang000000
Copy link
Collaborator Author

arch/x86/asm.h 问题解决,在于 MSYS 上的 gcc 不认 .type.size。修改后链接上了 hev_task_execute()

跑了一下,不出意料的 crash 掉了。

Image

汇编不熟,猜测:

  1. 直接拿 libkqueue 的 DLL 来用有问题?毕竟没有兼容 MSYS2/Cygwin。或许可以改用 libevent 实现下,对 MSYS 和 Windows 平台自身都有支持。
  2. 还是说 Unix 和 Windows 的 Calling convention 不一样,汇编不能直接拿来用?不熟悉,只是猜测。
  3. ...?

@heiher
Copy link
Owner

heiher commented Feb 11, 2025

libkquque很可能与msys不兼容;Windows x64的调用约定与Unix的不同,前两个参数改为 rcx和rdx传递,调用者栈帧上放4个参数的保留槽(紧邻返回地址槽向高地址方向),且call指令前保持栈rsp是16字节对齐

@heiher
Copy link
Owner

heiher commented Feb 11, 2025

我基于你的补丁创建了hev-task-system的msys分支,目前协程任务切换、调度和定时器可用,发现libkqueue在Windows上只有EVFILT_READ是可用的,其余事件都未实现,因此I/O类测试只有test-io-read*可通过。

libkqueue用的是mingw-w64-clang-x86_64-libkqueue,其它版本实测不可用。

@MikeWang000000
Copy link
Collaborator Author

🐮,协程跑起来了。看起来 libkqueue 确实不行,实测 natmap 在 kevent() 卡住了。同样地,执行使用 libkqueue 的 hev-task-system 用例也有卡住。

Image

或许就用 poll() 来实现一个?虽然效率不如 epoll() 或者 kevent(),但它被 MSYS 直接支持。

Windows 下的一个原生替代是 IOCP,据说有着和 epoll() 类似的高性能。 (刚查的)

@heiher
Copy link
Owner

heiher commented Feb 12, 2025

是的,部分测试会卡住。libkqueue在Windows上底层就是使用IOCP实现的,我想后面实现EVFILT_WRITE试试看。

@heiher
Copy link
Owner

heiher commented Feb 13, 2025

TCP现在可以通过STUN获取到映射信息并建立HTTP保活连接,应用层端口转发功能未测试。UDP还不工作,看上去Windows上UDP入站派发行为与Unix不同。比如有两个或以上个UDP Socket绑定在相同的地址端口上,Socket S1未与任何远程主机建立”连接“,Socket S2与主机地址端口A建立”连接“(connect调用),在接收从主机地址端口A发送过来的报文行为上,Unix系统报文会送至S2,Windows并不一定送至S2,除非S2以外的全部关闭。

Image

源代码

编译方法

msys2-runtime

wget https://repo.msys2.org/msys/sources/msys2-runtime-3.5.7-2.src.tar.zst
tar xf msys2-runtime-3.5.7-2.src.tar.zst
cd msys2-runtime
# 补丁增加至PKGBUILD
makepkg

libkqueue

wget https://repo.msys2.org/mingw/sources/mingw-w64-libkqueue-2.6.2-1.src.tar.zst
tar xf mingw-w64-libkqueue-2.6.2-1.src.tar.zst
cd mingw-w64-libkqueue
# 补丁增加至PKGBUILD
MINGW_ARCH=clang64 makepkg-mingw

natmap

make CFLAGS="-I/clang64/include/kqueue" LFLAGS="-L/clang64/lib -lkqueue"

@MikeWang000000
Copy link
Collaborator Author

MikeWang000000 commented Feb 13, 2025

我这里构建成功了🎉

关于 Windows UDP 的处理,有两种方法:

  1. 放弃使用 connect(),全部使用 sendto(), recvfrom() 自己维护对应连接
  2. UNSK 发送一次 UDP 请求之后就立即把连接断掉(不要保持长时间的连接),这样 STUN 请求就能正常收到。

比如下面这种情况,STUN 的响应就被 UNSK 的 socket 吃掉了:

[UNSK] connect() ---> [STUN] connect() ---> [STUN] TIMEOUT ---> [STUN] close() ---> [UNSK] close()

这样可以解决:

[UNSK] connect() ---> [UNSK] close() ---> [STUN] connect() ---> [STUN] close()

下一轮 UNSK 请求的时候,可以复用异常断开的逻辑,重新再 connect()

Natter 这里有过这样的 work-around,相关逻辑可供参考。
https://github.com/MikeWang000000/Natter/blob/f8257e8cff8f013202d9af9d0f9af4d986c1c718/natter.py#L378-L381

@MikeWang000000
Copy link
Collaborator Author

把这个注释掉,就会发现 STUN 有输出了(当然,这是因为直接关闭了原来的 fd),要确保有方法让 UNSK 的 socket 重开,应该就行了。

fd = hev_task_io_dup (fd);

@heiher
Copy link
Owner

heiher commented Feb 14, 2025

hev-task-system的msys支持已合入主线 ( heiher/hev-task-system@aa3e261 ),感谢 @MikeWang000000

@heiher
Copy link
Owner

heiher commented Feb 14, 2025

我这里构建成功了🎉

关于 Windows UDP 的处理,有两种方法:

  1. 放弃使用 connect(),全部使用 sendto(), recvfrom() 自己维护对应连接
  2. UNSK 发送一次 UDP 请求之后就立即把连接断掉(不要保持长时间的连接),这样 STUN 请求就能正常收到。

采用方法2,应用层转发功能还是不可用的吧

@MikeWang000000
Copy link
Collaborator Author

MikeWang000000 commented Feb 14, 2025

采用方法2,应用层转发功能还是不可用的吧

其实是有办法的。假如你的网络接口 IP 地址是 192.168.1.100,Windows 下 bind()192.168.1.100:12340.0.0.0:1234 的两个 socket 就互不冲突了。

举个例子哈,STUN 和 UNSK 在 bind() 之前,先用下面函数获取真正的 IP 地址,再 bind() 到上面。
然后转发的 socket,就 bind()0.0.0.0 上。这样就不会冲突了。

int get_real_bind_addr (struct sockaddr *stun_addr,
                        struct sockaddr *bind_addr,
                        struct sockaddr *real_addr,
                        socklen_t slen)
{
    int fd, ret = -1;
    
    if ((fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
        return -1;

    if (bind(fd, bind_addr, slen))
        goto end;

    if (connect(fd, stun_addr, slen))
        goto end;

    if (getsockname(fd, real_addr, &slen))
        goto end;

    ret = 0;

end:
    close(fd);
    return ret;
}

@heiher
Copy link
Owner

heiher commented Feb 14, 2025

采用方法2,应用层转发功能还是不可用的吧

其实是有办法的。假如你的网络接口 IP 地址是 192.168.1.100,Windows 下 bind()192.168.1.100:12340.0.0.0:1234 的两个 socket 就互不冲突了。

举个例子哈,STUN 和 UNSK 在 bind() 之前,先用下面函数获取真正的 IP 地址,再 bind() 到上面。 然后转发的 socket,就 bind()0.0.0.0 上。这样就不会冲突了。

666,我找时间改改看

@heiher
Copy link
Owner

heiher commented Feb 14, 2025

Windows x64的基本支持已经合并到主线,CI构建也好了 https://github.com/heiher/natmap/actions/runs/13331876605

接下来修复UDP

@heiher heiher added the enhancement New feature or request label Feb 14, 2025
@MikeWang000000
Copy link
Collaborator Author

小修了一下,现在不开转发的情况下应该都正常了。(run 函数似乎只能传两个参数,手动捂脸)

@MikeWang000000
Copy link
Collaborator Author

#90 的改法有bug,导致转发传入的 fd 失效了。我得重修一下。


UDP 在 Windows 上能用的秘诀就是,每个 connect() 之后都要保持尽可能的短,因为在此期间,其他连接都短暂不可用。

然而我细看了一下,模块间都是以 fd 传递的(或许只传递绑定地址就够用了),这导致了 UDP 连接迟迟不能关闭,因为关闭了下一个模块就用不了了。

理想情况下,NATMap 以 UDP 方式运行时(不开转发),netstat -ano 应该看不到一条(长)连接。


我估计,或许得重构一下 fd 传递了...

@heiher
Copy link
Owner

heiher commented Feb 16, 2025

#90 的改法有bug,导致转发传入的 fd 失效了。我得重修一下。

UDP 在 Windows 上能用的秘诀就是,每个 connect() 之后都要保持尽可能的短,因为在此期间,其他连接都短暂不可用。

然而我细看了一下,模块间都是以 fd 传递的(或许只传递绑定地址就够用了),这导致了 UDP 连接迟迟不能关闭,因为关闭了下一个模块就用不了了。

理想情况下,NATMap 以 UDP 方式运行时(不开转发),netstat -ano 应该看不到一条(长)连接。

我估计,或许得重构一下 fd 传递了...

按照这个思路改了一版,再测测看

@heiher
Copy link
Owner

heiher commented Feb 16, 2025

其实是有办法的。假如你的网络接口 IP 地址是 192.168.1.100,Windows 下 bind() 在 192.168.1.100:1234 和 0.0.0.0:1234 的两个 socket 就互不冲突了。

这个也在 #92 中实现了 (今天要外出,还没来得及做更多的测试

@heiher
Copy link
Owner

heiher commented Feb 17, 2025

修复执行通知脚本: #95

@MikeWang000000
Copy link
Collaborator Author

写了一个关于 Winsock 的测试:https://github.com/MikeWang000000/natmap_win32/blob/master/socktest.c
可以先运行 echo server 再运行 dns client。它们俩 bind 在同一个端口,经测试都是可以正常使用的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants