TCP保持活动参数不受尊重

Wad*_*Wad 4 sockets linux networking tcp

我正在试验我的Linux机器上的TCP保持活动,并编写了以下小型服务器:

#include <iostream>
#include <cstring>

#include <netinet/in.h>
#include <arpa/inet.h>  // inet_ntop
#include <netinet/tcp.h>
#include <netdb.h>          // addrinfo stuff

using namespace std;

typedef int SOCKET;

int main(int argc, char *argv []) 
{
    struct sockaddr_in sockaddr_IPv4;
    memset(&sockaddr_IPv4, 0, sizeof(struct sockaddr_in));
    sockaddr_IPv4.sin_family = AF_INET;
    sockaddr_IPv4.sin_port = htons(58080);

    if (inet_pton(AF_INET, "10.6.186.24", &sockaddr_IPv4.sin_addr) != 1)
        return -1;

    SOCKET serverSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    if (bind(serverSock, (sockaddr*)&sockaddr_IPv4, sizeof(sockaddr_IPv4)) != 0 || listen(serverSock, SOMAXCONN) != 0) 
    { 
        cout << "Failed to setup listening socket!\n";
    }

    SOCKET clientSock = accept(serverSock, 0, 0);
    if (clientSock == -1) 
        return -1;

    // Enable keep-alive on the client socket
    const int nVal = 1;
    if (setsockopt(clientSock, SOL_SOCKET, SO_KEEPALIVE, &nVal, sizeof(nVal)) < 0)
    {
        cout << "Failed to set keep-alive!\n";
        return -1;
    }

    // Get the keep-alive options that will be used on the client socket

    int nProbes, nTime, nInterval;
    socklen_t nOptLen = sizeof(int);
    bool bError = false;

    if (getsockopt(clientSock, IPPROTO_TCP, TCP_KEEPIDLE, &nTime, &nOptLen) < 0) { bError = true; }
    nOptLen = sizeof(int);

    if (getsockopt(clientSock, IPPROTO_TCP, TCP_KEEPCNT, &nProbes, &nOptLen) < 0) {bError = true; }
    nOptLen = sizeof(int);

    if (getsockopt(clientSock, IPPROTO_TCP, TCP_KEEPINTVL, &nInterval, &nOptLen) < 0) { bError = true; }

    cout << "Keep alive settings are: time: " << nTime << ", interval: " << nInterval << ", number of probes: " << nProbes << "\n";

    if (bError) 
    {
        // Failed to retrieve values
        cout << "Failed to get keep-alive options!\n";
        return -1;
    }

    int nRead = 0;
    char buf[128];
    do 
    {
        nRead = recv(clientSock, buf, 128, 0);
    } while (nRead != 0);


    return 0;
}
Run Code Online (Sandbox Code Playgroud)

然后我调整了系统范围的TCP keep alive设置如下:

# cat /proc/sys/net/ipv4/tcp_keepalive_time
20
# cat /proc/sys/net/ipv4/tcp_keepalive_intvl
30
Run Code Online (Sandbox Code Playgroud)

然后我从Windows连接到我的服务器,并运行Wireshark跟踪以查看保持活动的数据包.下图显示了结果.

数据包1

这使我感到困惑,因为我现在理解保持活动间隔只有在没有收到ACK以响应原始保持活动数据包时才会发挥作用(请参阅我的其他问题).所以我希望后续的数据包能够以20秒的间隔(不是30,这就是我们看到的)一致地发送,而不仅仅是第一个.

然后,我调整了系统范围设置如下:

# cat /proc/sys/net/ipv4/tcp_keepalive_time
30
# cat /proc/sys/net/ipv4/tcp_keepalive_intvl
20
Run Code Online (Sandbox Code Playgroud)

这次当我连接时,我在Wireshark跟踪中看到以下内容:

Packets2

现在我们看到第一个保活包在30秒后发送,但此后每个包也在30秒发送,而不是前一次运行建议的20!

有人可以解释这种不一致的行为吗?

Jim*_* D. 8

粗略地说,它应该如何工作是每秒钟发送一个keepalive消息tcp_keepalive_time.如果ACK没有收到,则会每秒探测一次tcp_keepalive_intvl.如果ACK之后没有收到tcp_keepalive_probes,则连接将中止.因此,连接最多会中止

    tcp_keepalive_time + tcp_keepalive_probes * tcp_keepalive_intvl
Run Code Online (Sandbox Code Playgroud)

没有回应的秒.请参阅内核文档.

我们可以使用netcat keepalive轻松地观察这项工作,netcat是一种允许我们设置tcp keepalive参数的netcat版本(sysctl keepalive参数是默认参数,但它们可以在tcp_sockstruct中基于每个套接字覆盖).

首先启动一个侦听端口的服务器8888,keepalive_timer设置为5秒,keepalive_intval设置为1秒,并keepalive_probes设置为4.

    $ ./nckl-linux -K -O 5 -I 1 -P 4 -l 8888 >/dev/null &
Run Code Online (Sandbox Code Playgroud)

接下来,让我们使用iptables引入ACK发送到服务器的数据包的丢失:

    $ sudo iptables -A OUTPUT -p tcp --dport 8888 \
    >   --tcp-flags SYN,ACK,RST,FIN ACK \
    >   -m statistic --mode random --probability 0.5 \
    >   -j DROP
Run Code Online (Sandbox Code Playgroud)

这将导致发送到TCP端口8888的数据包仅将ACK标志设置为以0.5的概率丢弃.

现在让我们连接并观察vanilla netcat(它将使用sysctl keepalive值):

    $ nc localhost 8888
Run Code Online (Sandbox Code Playgroud)

这是捕获:

TCP keepalive捕获

如您所见,它在收到ACK另一个keepalive消息之前等待5秒钟.如果它ACK在1秒内没有收到,它会发送另一个探测器,如果它没有接收到ACK4个探测器,它将中止连接.这正是keepalive应该如何工作的.

所以让我们尝试重现你所看到的.让我们删除iptables规则(没有丢失),启动一个tcp_keepalive_time设置为1秒的新服务器,并tcp_keepalive_intvl设置为5秒,然后连接到客户端.结果如下:

使用keepalive_time捕获<keepalive_intvl,没有损失

有趣的是,我们看到了你所做的相同行为:在第一次之后ACK,它等待1秒钟发送一个keepalive消息,然后每5秒钟.

让我们重新添加iptables规则来引入丢失以查看它实际等待发送另一个探测的时间,如果它没有得到ACK(-K -O 1 -I 5 -P 4在服务器上使用):

使用keepalive_time <keepalive_intvl捕获,丢失

再次,它从第一个ACK发送keepalive消息等待1秒,但此后它等待5秒,无论它是否看到ACK,好像keepalive_time并且keepalive_intvl都被设置为5.

为了理解这种行为,我们需要看一下linux内核的TCP实现.我们先来看看tcp_finish_connect:

 if (sock_flag(sk, SOCK_KEEPOPEN))
        inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tp));
Run Code Online (Sandbox Code Playgroud)

建立TCP连接后,keepalive定时器有效设置tcp_keepalive_time为1,在我们的例子中为1秒.

接下来,让我们看看如何处理计时器tcp_keepalive_timer:

  elapsed = keepalive_time_elapsed(tp);

  if (elapsed >= keepalive_time_when(tp)) {
          /* If the TCP_USER_TIMEOUT option is enabled, use that
           * to determine when to timeout instead.
           */
          if ((icsk->icsk_user_timeout != 0 &&
              elapsed >= icsk->icsk_user_timeout &&
              icsk->icsk_probes_out > 0) ||
              (icsk->icsk_user_timeout == 0 &&
              icsk->icsk_probes_out >= keepalive_probes(tp))) {
                  tcp_send_active_reset(sk, GFP_ATOMIC);
                  tcp_write_err(sk);
                  goto out;
          }
          if (tcp_write_wakeup(sk, LINUX_MIB_TCPKEEPALIVE) <= 0) {
                  icsk->icsk_probes_out++;
                  elapsed = keepalive_intvl_when(tp);
          } else {
                  /* If keepalive was lost due to local congestion,
                   * try harder.
                   */
                  elapsed = TCP_RESOURCE_PROBE_INTERVAL;
          }
  } else {
          /* It is tp->rcv_tstamp + keepalive_time_when(tp) */
          elapsed = keepalive_time_when(tp) - elapsed;
  }

  sk_mem_reclaim(sk);

resched:
  inet_csk_reset_keepalive_timer (sk, elapsed);
  goto out;
Run Code Online (Sandbox Code Playgroud)

何时keepalive_time_whenkeepalive_itvl_when于此代码按预期工作.但是,如果不是,您会看到您观察到的行为.

当初始定时器(建立TCP连接时设置)在1秒后到期时,我们将延长定时器直到elapsed大于keepalive_time_when.此时我们将发送一个探测器,并将计时器设置keepalive_intvl_when为5秒.当此计时器到期时,如果最后1秒(keepalive_time_when)没有收到任何内容,我们将发送一个探测器,然后再次设置定时器keepalive_intvl_when,并在另外5秒内唤醒,依此类推.

但是,如果我们keepalive_time_when在计时器到期时收到了某些内容,它将keepalive_time_when用于自上次收到任何内容后重新安排计时器1秒钟.

所以,为了回答你的问题,TCP keepalive的linux实现假定它keepalive_intvl小于keepalive_time,但仍然"明智地"工作.

  • 非常感谢 :) 对于未来的读者,https://www.netfilter.org/documentation/HOWTO/packet-filtering-HOWTO-7.html 提供了对 `iptables` 命令的有用见解。 (2认同)