简体   繁体   English

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

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

TLDR: I have a HTTP server application written in C++ which launches some scripts using popen() . TLDR:我有一个用 C++ 编写的 HTTP 服务器应用程序,它使用popen()启动一些脚本。 The scripts start a few daemons: wpa_supplicant and udhcpd .这些脚本启动了几个守护进程: wpa_supplicantudhcpd Those daemons seem to hold onto my HTTP server port after my server stops.在我的服务器停止后,那些守护进程似乎保留在我的 HTTP 服务器端口上。 Why?为什么?

During initialization, my HTTP server application uses popen() to launch a script to start wpa_supplicant and udhcpd to make sure my interfaces are ready to go. After the scripts execute, my application opens port 80 as you would expect.在初始化期间,我的 HTTP 服务器应用程序使用 popen() 启动脚本以启动wpa_supplicantudhcpd以确保我的接口已准备好连接到 go。脚本执行后,我的应用程序将如您所料打开端口 80。

The problem: When my application closes and goes through all the destructors, it correctly closes the socket with close(int_socket_val) , yet trying to start my application a second time will fail because port 80 is not available.问题:当我的应用程序关闭并通过所有析构函数时,它使用close(int_socket_val)正确关闭了套接字,但第二次尝试启动我的应用程序将失败,因为端口 80 不可用。

Doing a netstat -tulpn shows that either wpa_supplicant or udhcpd is hanging onto my port 80. Interestingly, while my HTTP server is still running, netstat shows this same result - so my HTTP server is never listed as owning the port.执行netstat -tulpn显示wpa_supplicantudhcpd挂在我的端口 80 上。有趣的是,虽然我的 HTTP 服务器仍在运行,但netstat显示相同的结果 - 因此我的 HTTP 服务器从未被列为拥有该端口。 Killing those applications with killall -9 wpa_supplicant udhcpd will free port 80 and allow me to start my HTTP server again.使用killall -9 wpa_supplicant udhcpd杀死这些应用程序将释放端口 80 并允许我再次启动我的 HTTP 服务器。 But why does this happen?但是为什么会这样呢? This has proven a difficult problem to research.这已被证明是一个很难研究的问题。

For reference, here is the method I use to launch scripts and be able to read what was returned during those calls:作为参考,这是我用来启动脚本并能够读取这些调用期间返回的内容的方法:

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;
}

This is not a special case where port 80 is somehow magical.这不是 80 端口有某种魔力的特殊情况。 It works on any port I use for development - starting my HTTP server on port 50000 poses the same effect.它适用于我用于开发的任何端口——在端口 50000 上启动我的 HTTP 服务器会产生相同的效果。 Here is the netstat output for reference:这里是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#

During a subsequent run, I might get wpa_supplicant hanging onto the port - that part seems random:在随后的运行中,我可能会让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#

For reference, here is a section of the script that calls these two daemons:作为参考,下面是调用这两个守护进程的脚本部分:

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

@G. @G。 Sleipen - provided an accurate explanation of the problem. Sleipen - 提供了对问题的准确解释。 What did the trick for me was to add the flag that was suggested in addition to explicitly setting the FD_CLOEXEC flag in a subsequent system call.对我来说,除了在后续系统调用中显式设置 FD_CLOEXEC 标志之外,还添加了建议的标志。 This may not be ideal for everyone because that second call is not atomic in the way that the SOCK_CLOEXEC flag should have been, but it provides a fallback in cases where your kernel might not support the SOCK_CLOEXEC flag.这可能对每个人来说都不是理想的,因为第二次调用不是原子的,而 SOCK_CLOEXEC 标志应该是这样,但它在您的 kernel 可能不支持 SOCK_CLOEXEC 标志的情况下提供了一个回退。 I'd be interested in an explanation why it did NOT work, but here's my solution:我会对它为什么不起作用的解释感兴趣,但这是我的解决方案:

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() forks the process and executes a shell in the child. popen()分叉进程并在子进程中执行 shell。 The child inherits the filedescriptors of the parent.孩子继承了父母的文件描述符。 When the shell executes udhcpd, that in turn will cause a fork and exec.当 shell 执行 udhcpd 时,这反过来会导致 fork 和 exec。 Then udhcpd will daemonize itself, but looking at the source code it looks like it won't close all open filedescriptors first.然后 udhcpd 将自行守护进程,但查看源代码看起来它不会首先关闭所有打开的文件描述符。 This means udhcpd continues to hold on to a filedescriptor for a socket your program opened earlier, and thus keeps it alive.这意味着 udhcpd 会继续保留您的程序之前打开的套接字的文件描述符,从而使其保持活动状态。

There are several workarounds.有几种解决方法。 The easiest would be to ensure any filedescriptors you open in your program have the CLOEXEC flag set.最简单的方法是确保您在程序中打开的任何文件描述符都设置了CLOEXEC标志。 For example, create your listening socket with:例如,创建你的监听套接字:

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

This ensures that if you call popen() , the child process won't inherit the filedescriptors with that flag set.这确保如果您调用popen() ,子进程将不会继承设置了该标志的文件描述符。


If your program causes udhcpd to start, it would also be prudent to make it stop udhcpd before it terminates.如果您的程序导致 udhcpd 启动,那么在它终止之前让它停止 udhcpd 也是明智的。 This would also have avoided the issue.这也可以避免这个问题。 Probably a combination of both would be best though.可能两者的结合是最好的。

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

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