Mac OS X上奇怪的防火墙相关套接字泄漏



我在Mac OS X中遇到了一个无法解释的奇怪套接字泄漏问题。我有一个开源守护进程(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                 *.*  

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

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

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

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

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

我们在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操作系统中的一个内核错误,它已经存在了至少4年,显然会影响所有同时具有UDP和TCP侦听器的任务。在修复此问题之前,用户需要禁用防火墙选项。

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

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

最新更新