繁体   English   中英

在服务器应用程序中使用 popen 劫持端口

[英]Using popen within a server application holds port hostage

TLDR:我有一个用 C++ 编写的 HTTP 服务器应用程序,它使用popen()启动一些脚本。 这些脚本启动了几个守护进程: wpa_supplicantudhcpd 在我的服务器停止后,那些守护进程似乎保留在我的 HTTP 服务器端口上。 为什么?

在初始化期间,我的 HTTP 服务器应用程序使用 popen() 启动脚本以启动wpa_supplicantudhcpd以确保我的接口已准备好连接到 go。脚本执行后,我的应用程序将如您所料打开端口 80。

问题:当我的应用程序关闭并通过所有析构函数时,它使用close(int_socket_val)正确关闭了套接字,但第二次尝试启动我的应用程序将失败,因为端口 80 不可用。

执行netstat -tulpn显示wpa_supplicantudhcpd挂在我的端口 80 上。有趣的是,虽然我的 HTTP 服务器仍在运行,但netstat显示相同的结果 - 因此我的 HTTP 服务器从未被列为拥有该端口。 使用killall -9 wpa_supplicant udhcpd杀死这些应用程序将释放端口 80 并允许我再次启动我的 HTTP 服务器。 但是为什么会这样呢? 这已被证明是一个很难研究的问题。

作为参考,这是我用来启动脚本并能够读取这些调用期间返回的内容的方法:

std::string ConnectionManager::exec(const std::string& command, bool strip)
{
   char buffer[EXEC_BUFFER_LEN];
   std::string result = "";

   // Open pipe to file
   FILE* pipe = popen(command.c_str(), "r");
   if (!pipe)
   {
       std::cout << "ERROR: ConnectionManager::exec() - failed to open command: " << command << std::endl;
      return result;
   }

   // read till end of process:
   while (!feof(pipe))
   {
      // use buffer to read and add to result
      if (fgets(buffer, EXEC_BUFFER_LEN, pipe) != NULL)
      {
        result += buffer;
      }
   }

   pclose(pipe);

   if ( strip )
   {
       removeLineEndings(result);
   }

   return result;
}

这不是 80 端口有某种魔力的特殊情况。 它适用于我用于开发的任何端口——在端口 50000 上启动我的 HTTP 服务器会产生相同的效果。 这里是netstat output 供参考:

root@device:/usr/bin# netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
.........
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      4267/udhcpd
.........
root@device:/usr/bin#

在随后的运行中,我可能会让wpa_supplicant挂在端口上——这部分看起来是随机的:

root@device:/usr/bin# netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
.........
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      4393/wpa_supplicant
.........
root@device:/usr/bin#

作为参考,下面是调用这两个守护进程的脚本部分:

wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant.conf
udhcpc -i wlan0

@G。 Sleipen - 提供了对问题的准确解释。 对我来说,除了在后续系统调用中显式设置 FD_CLOEXEC 标志之外,还添加了建议的标志。 这可能对每个人来说都不是理想的,因为第二次调用不是原子的,而 SOCK_CLOEXEC 标志应该是这样,但它在您的 kernel 可能不支持 SOCK_CLOEXEC 标志的情况下提供了一个回退。 我会对它为什么不起作用的解释感兴趣,但这是我的解决方案:

int Socket::openServerSocket(uint16_t port)
{
    int hSocket;
    int flag;
    /* Create the TCP socket */
    if ((hSocket = socket(PF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_TCP)) < 0)
    {
        return -1;
    }
    fcntl(hSocket, F_SETFD, fcntl(hSocket, F_GETFD) | FD_CLOEXEC);

    /* Disable the Nagle (TCP No Delay) algorithm */
    flag = 1;
    if (-1 == setsockopt(hSocket, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(flag)))
    {
        return -1;
    }
    /* Set the Keep Alive property */
    flag = 1;
    if (-1 == setsockopt(hSocket, SOL_SOCKET, SO_KEEPALIVE, (char *)&flag, sizeof(flag)))
    {
        return -1;
    }
    /* Allow the re-use of port numbers to avoid error */
    flag = 1;
    if (-1 == setsockopt(hSocket, SOL_SOCKET, SO_REUSEADDR, (char *)&flag, sizeof(flag)))
    {
        return -1;
    }
    /* Set an explicit socket timeout value */
    struct timeval tv;
    tv.tv_sec = TIMEOUT_SEC;
    tv.tv_usec = 0;
    if (-1 == setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv))
    {
        printf("ERROR: Socket::openServerSocket->setsockopt(port timeout)\n");
        return -1;
    }

    /* Construct the server sockaddr_in structure */
    memset(&m_sockaddr, 0, sizeof(m_sockaddr));       /* Clear struct */
    m_sockaddr.sin_family = AF_INET;                /* Internet/IP */
    m_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); /* Incoming addr */
    m_sockaddr.sin_port = htons(port);              /* server port */

    /* Bind the server socket */
    if (bind(hSocket, (struct sockaddr *)&m_sockaddr,
             sizeof(m_sockaddr)) < 0)
    {
        return -1;
    }
    /* Listen on the server socket */
    if (listen(hSocket, MAXPENDING) < 0)
    {
        return -1;
    }

    return hSocket;
}

int Socket::acceptClient(int hSocket)
{
    unsigned int sockaddr_len = sizeof(m_sockaddr);
    int ret = accept4(hSocket, (struct sockaddr *)&m_sockaddr, &sockaddr_len, SOCK_CLOEXEC);
    fcntl(ret, F_SETFD, fcntl(ret, F_GETFD) | FD_CLOEXEC);
    return ret;
}

popen()分叉进程并在子进程中执行 shell。 孩子继承了父母的文件描述符。 当 shell 执行 udhcpd 时,这反过来会导致 fork 和 exec。 然后 udhcpd 将自行守护进程,但查看源代码看起来它不会首先关闭所有打开的文件描述符。 这意味着 udhcpd 会继续保留您的程序之前打开的套接字的文件描述符,从而使其保持活动状态。

有几种解决方法。 最简单的方法是确保您在程序中打开的任何文件描述符都设置了CLOEXEC标志。 例如,创建你的监听套接字:

int listen_fd = socket(AF_SOMETHING, SOCK_STREAM | SOCK_CLOEXEC, 0);

这确保如果您调用popen() ,子进程将不会继承设置了该标志的文件描述符。


如果您的程序导致 udhcpd 启动,那么在它终止之前让它停止 udhcpd 也是明智的。 这也可以避免这个问题。 可能两者的结合是最好的。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM