2

我在 Mac OS XI 中遇到了一个奇怪的套接字泄漏问题,无法解释。我有一个开源守护程序 (olad),它在 9010 (tcp)、9090 (tcp) 和 6454 (udp) 以及其他端口上进行侦听。当守护进程退出时,netstat 显示端口 6454 仍处于打开状态并正在侦听:

$ netstat -f inet -n | grep 6454
<nothing>
$ olad/olad 
<exit server>
$ netstat -f inet -n | grep 6454
udp4       0      0  *.6454                 *.* 

但是, lsof 不显示套接字:

$ lsof -i 4 -P | grep 6454
<nothing>

一旦系统处于这种状态,通过向端口发送数据包,我可以看到队列计数增加:

$ netstat -f inet -n | grep 6454
udp4     612      0  *.6454                 *.*  

只有在 Preferences -> Security & Privacy 中启用了应用程序防火墙并且这是第一次运行二进制文件时,才会发生这种情况。也就是说,如果我禁用防火墙,则不会发生泄漏。或者,如果启用了防火墙,在第一次运行后弹出对话框并单击接受后,问题不再出现。

一旦套接字泄漏,禁用防火墙将无法释放它。

我已经确认在程序退出之前我在所有套接字上调用 close() 并且没有调用 fork() 或新线程。

我一直在尝试缩小错误案例的范围,但它似乎是 pipe()、socket()、bind()、listen()、ioctl()、fnctl() 和 select() 之间相当复杂的交互。更改调用顺序并删除端口 9010 和 9090 上的侦听会导致问题消失。

有没有人有关于如何继续调试的建议,或者关于 Mac 应用程序防火墙如何在内部工作的指南?

4

2 回答 2

2

我们在 Mac OS 10.14 上使用支持网络的应用程序时也遇到了这个问题。一些调查的结果是一个 Python 脚本,它很好地重现了这个问题。

import socket

udpsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udpsock.bind(("0.0.0.0", 7744))

tcpsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcpsock.bind(("0.0.0.0", 17744))
tcpsock.listen()

print("Listening ...")

while True:
    data, addr = udpsock.recvfrom(16)
    print("Received data: ", data)

启用应用程序防火墙并且发送方将数据发送到 UDP 端口 7744 后,操作系统会在第一次启动时提示用户接受传入的网络流量,一旦允许,脚本将接收数据包。然而,在停止脚本之后,套接字保持打开状态,没有匹配的用户空间任务使用它,并且随后bind()再次尝试将导致Address already in use,无论是否设置SO_REUSE_PORT和/或SO_REUSE_ADDR设置。在此之后,必须重新启动机器才能让侦听器再次访问该 UDP 端口。

$ netstat -an -pudp | grep 7744
udp4     0      0  *.7744                 *.*

请注意,仅当还注册了 TCP 侦听器并且脚本运行时正在接收 UDP 数据包时才会发生这种情况。这也可能是由其他情况引起的,但上面是我们可以将问题归结为最简化的示例。

所以这是 Mac OS 中的一个内核错误,它已经存在至少 4 年了,它显然会影响所有同时具有 UDP 和 TCP 侦听器的任务。在解决此问题之前,用户需要禁用防火墙选项。

于 2019-06-24T12:54:44.673 回答
1

事实证明,即使在使用它的进程关闭后,防火墙也会“记住”先前绑定的 UDP 端口的套接字选项。这导致“netstat -n -f inet”列出的 UDP 端口没有任何进程从它接收。从那时起,任何将套接字绑定到该端口的尝试都将如预期的那样,当防火墙关闭时一切都很好。

正如您已经发现的那样,OS-X 需要 SO_REUSEPORT 而不是 SO_REUSEADDR 以避免防火墙出现这种奇怪的状态问题。此外,刷新防火墙状态的唯一方法是重新启动。有趣的是,在重新启动后启动服务器之前,您需要等待几分钟。出于某种我不想进一步研究的原因,如果您在防火墙完成初始化之前启动服务器,您不会收到防火墙弹出窗口要求您允许访问并且您的服务器保持“永远”(即直到下次重新启动或直到您重建服务器二进制文件)被防火墙阻止。

于 2017-03-17T01:13:13.480 回答