hana_shinのLinux技術ブログ

Linuxの技術情報を掲載しています。特にネットワークをメインに掲載していきます。

ソケットオプションの使い方(SO_KEEPALIVE編)



1 TCP Keep-Aliveとは?

1.1 概要

TCPコネクション確立状態において、相手の生存確認をするためのTCP Keep-Aliveという機能があります。相手からTCPパケットを受信したあと、一定時間相手からTCPパケットを受信しないと、相手が生存しているかどうかを確認するため、TCP Keep-Aliveパケットを相手に送信します。TCP Keep-Aliveパケットを規定回数送信しても、相手から応答がないとRSTパケットを送信してTCPコネクションを終了します。

なお、TCP Keep-Alive機能の設定方法は、以下のものがあります。

方法 備考
カーネルパラメータを変更する方法 カーネルパラメータの変更がシステム全体のアプリケーションに影響します
ソケットオプションを使う方法 ソケットオプションを指定したTCPコネクションについてTCP Keep-Alive機能が有効になります。ソケットオプションを使うと、カーネルパラメータの設定内容をオーバライトします

1.2 ソケットオプション

ソケットオプション 意味
TCP_KEEPIDLE 相手からTCPパケットを受信してから、TCP Keep-Aliveパケットを送信するまでの時間(秒)をあらわします
TCP_KEEPINTVL TCP Keep-Aliveパケットに対して相手からACKパケットを受信しないと、TCP_KEEPINTVL間隔(秒)でTCP Keep-Aliveパケットを送信します
TCP_KEEPCNT TCP Keep-Aliveパケットの送信回数をあらわします。TCP Keep-Aliveパケットは、生存確認のため相手に送信するパケットです。TCP Keep-Aliveパケットは、TCPペイロード長が0バイトのパケットです。つまりTCPヘッダだけのTCPパケットです

1.3 シーケンス

TCP Keep-Aliveパケットに対して相手からACKパケットが返ってくる場合のシーケンスを以下に示します。相手からTCPパケット(データやACK等)を受信して、一定時間(TCP_KEEPIDLE)経過すると、TCP Keep-Aliveパケットを相手に送信します。そのあと、TCP_KEEPINTVL間隔でTCP Keep-Aliveパケットを送信します。TCP Keep-Aliveパケットに対してACKパケットが返ってくると、TCPコネクションは維持されます。

client                              server(11111)
   |                                   |
   |                                   |
   |------------ TCP packet ---------->| -*-
   |                                   |  |
   |                                   | TCP_KEEPIDLE
   |                                   |  |
   |<-------- TCP Keep-Alive ----------| -*-
   |--------------- ACK -------------->| -*-
   |                                   |  |
   |                                   | TCP_KEEPINTVL
   |                                   |  |
   |<-------- TCP Keep-Alive ----------| -*-
   |--------------- ACK -------------->|
   |                                   |

TCP Keep-Aliveパケットに対して相手からACKパケットが返ってこない場合のシーケンスを以下に示します。相手からACKパケットが返ってこない場合、TCP Keep-Aliveパケットを再送します。規定した回数TCP Keep-Aliveパケットを送信しても相手からACKパケットが返ってこないと、RSTパケットを送信してTCPコネクションを終了します。

      client                              server(11111)
         |                                   |
         |------------ TCP packet ---------->| -*-
         |                                   |  |
         |                                   | TCP_KEEPIDLE
         |                                   |  |
         |<-------- TCP Keep-Alive ----------| -*-
         |                                   |  |
         |                                   | TCP_KEEPINTVL
         |                                   |  |
         |<---- TCP Keep-Alive(Retry 1) -----| -*-
         |                                   |  |
 ====================================================
         |                                   |  |
         |                                   | TCP_KEEPINTVL
         |                                   |  |
         |<---- TCP Keep-Alive(Retry n) -----| -*-
         |                                   |
         |<-------------- RST ---------------|
         |                                   |

2 検証環境

2.1 ネットワーク構成

サーバとクライアントの2台構成です。図中のeth0はNICの名前です。

                           192.168.122.0/24
client(eth0) ------------------------------------------(eth0) server
            .177                                       .68

2.2 版数

サーバとクライアントのAlmaLinuxの版数は以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 8.6 (Sky Tiger)

カーネル版数は以下のとおりです。

[root@server ~]# uname -r
4.18.0-372.9.1.el8.x86_64

クライアントからサーバの11111番ポートにアクセスできるようにするため、TCPの11111番ポートを解放します。なお、firewall-cmdの使い方は、firewall-cmdの使い方は、firewall-cmdの使い方 - hana_shinのLinux技術ブログを参照してください。

[root@server ~]# firewall-cmd --add-port=11111/tcp
success

ルールを確認します。11111番ポートへのアクセスを許可するルールが追加されたことがわかります。

[root@server ~]# firewall-cmd --list-ports
11111/tcp

3 テストプログラム

サーバとクライアントのテストプログラム(以降TP)を作成します。

3.1 サーバ側TP

サーバのTPは、以下のようになります。11111番ポートでTCPパケットを受信します。TPの引数に1を指定すると、TCP Keep-Aliveが有効になります。0を指定すると、TCP Keep-Aliveが無効になります。なお、処理を分かりやすくするため、意図的に全ての異常処理は記載していません。

[root@server ~]# cat sv.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>
#include <netinet/tcp.h>

int main(int argc, char *argv[])
{
    int lfd, cfd, val;
    socklen_t len;
    struct sockaddr_in sv, cl;
    char buf[32];
    ssize_t n;

    lfd = socket(AF_INET, SOCK_STREAM, 0);

    sv.sin_family = AF_INET;
    sv.sin_port = htons(11111);
    sv.sin_addr.s_addr = INADDR_ANY;

    bind(lfd, (struct sockaddr *)&sv, sizeof(sv));
    listen(lfd, 5);
    memset(buf, 0, sizeof(buf));
    len = sizeof(cl);
    cfd = accept(lfd, (struct sockaddr *)&cl, &len);

    val = atoi(argv[1]);
    if(val == 1){
      setsockopt(cfd, SOL_SOCKET, SO_KEEPALIVE , (void *)&val, sizeof(val));

      val = 30;
      setsockopt(cfd, SOL_TCP, TCP_KEEPIDLE, (void *)&val, sizeof(val));
      val = 5;
      setsockopt(cfd, SOL_TCP, TCP_KEEPINTVL, (void *)&val, sizeof(val));
      val = 2;
      setsockopt(cfd, SOL_TCP, TCP_KEEPCNT, (void *)&val, sizeof(val));

    }

    while (1) {
        n = read(cfd, buf, sizeof(buf));
        if(n > 0) {
            continue;
        }
        else if(n == 0) {
            fprintf(stderr,"We received EOF.\n");
            close(cfd);
            return 0;
        }
        else {
            perror("read");
            return 1;
        }
    }
    close(lfd);
    return 0;
}

3.2 クライアント側TP

クライアントのTPは、以下のようになります。connectシステムコールを実行してTCPコネクションを確立したあと、600秒スリープします。なお、処理を分かりやすくするため、意図的に全ての異常処理は記載していません。

[root@client ~]# cat cl.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
    struct sockaddr_in server;
    int sock;

    sock = socket(AF_INET, SOCK_STREAM, 0);
    server.sin_family = AF_INET;
    server.sin_port = htons(11111);
    server.sin_addr.s_addr = inet_addr("192.168.122.68");

    connect(sock, (struct sockaddr *)&server, sizeof(server));
    fprintf(stderr,"I will now sleep for 600 seconds.\n");
    sleep(600);
    close(sock);
    return 0;
}

3.3 コンパイル

サーバのTPをコンパイルします。

[root@server ~]# gcc -Wall -o sv sv.c

クライアントのTPをコンパイルします。

[root@client ~]# gcc -Wall -o cl cl.c

4 動作確認

4.1 相手から応答がない場合

サーバでTPを実行します。引数に1を指定してTCP Keep-Alive機能を有効にします。

[root@server ~]# ./sv 1

TCP Keep-Aliveの動作確認をするため、クライアントでtsharkコマンドを実行します。なお、tsharkコマンドのインストール方法、使い方は、tsharkコマンドの使い方 - hana_shinのLinux技術ブログを参照してください。

[root@server ~]# tshark -i eth0 port 11111

クライアントでTPを実行します。

[root@client ~]# ./cl

iptablesコマンドを使って、TCP Keep-Aliveパケットに対するACKパケットを廃棄する設定をします。iptablesコマンドは、サーバがクライアントにTCP Keep-Aliveパケット送信する30秒以内に実行してください。なお、iptablesコマンドの使い方は、iptablesコマンドの使い方 - Qiitaを参照してください(hatenaに移行予定)。

[root@client ~]# iptables -I OUTPUT -p tcp --dport 11111 --tcp-flags ACK ACK -j DROP

tsharkコマンドの実行結果を確認します。TCPコネクション確立のあと(No1,2,3)、30秒後にサーバからクライアントにTCP Keep-Aliveを送信しています(No.4)。そのあと、もう一度TCP Keep-Aliveパケットを送信していますが(No.5)、クライアントからACKパケットの応答がないので、No.6でクライアントにRSTパケットを送信していることがわかります。

[root@server ~]# tshark -i eth0 port 11111
    1 0.000000000 192.168.122.177 → 192.168.122.68 TCP 74 51692 → 11111 [SYN] Seq=0 Win=29200 Len=0 MSS=1460 SACK_PERM=1 TSval=522082979 TSecr=0 WS=128
    2 0.000172750 192.168.122.68 → 192.168.122.177 TCP 74 11111 → 51692 [SYN, ACK] Seq=0 Ack=1 Win=28960 Len=0 MSS=1460 SACK_PERM=1 TSval=383746932 TSecr=522082979 WS=128
    3 0.001971330 192.168.122.177 → 192.168.122.68 TCP 66 51692 → 11111 [ACK] Seq=1 Ack=1 Win=29312 Len=0 TSval=522082981 TSecr=383746932
    4 30.271443982 192.168.122.68 → 192.168.122.177 TCP 66 [TCP Keep-Alive] 11111 → 51692 [ACK] Seq=0 Ack=1 Win=29056 Len=0 TSval=383777203 TSecr=522082981
    5 35.391063189 192.168.122.68 → 192.168.122.177 TCP 66 [TCP Keep-Alive] 11111 → 51692 [ACK] Seq=0 Ack=1 Win=29056 Len=0 TSval=383782323 TSecr=522082981
    6 40.511581482 192.168.122.68 → 192.168.122.177 TCP 66 11111 → 51692 [RST, ACK] Seq=1 Ack=1 Win=29056 Len=0 TSval=383787443 TSecr=522082981

クライアントでiptablesのOUTPUTチェインの統計情報を確認します。宛先ポート番号が11111番のACKパケットが2つ廃棄されていることがわかります。

[root@client ~]# iptables -nvL OUTPUT
Chain OUTPUT (policy ACCEPT 3031 packets, 211K bytes)
 pkts bytes target     prot opt in     out     source               destination
    2   104 DROP       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:11111 flags:0x10/0x10

iptablesの設定を削除します。

[root@client ~]# iptables -D OUTPUT 1

iptablesの設定を確認します。設定が削除されたことがわかります。

[root@client ~]# iptables -nvL OUTPUT
Chain OUTPUT (policy ACCEPT 3053 packets, 214K bytes)
 pkts bytes target     prot opt in     out     source               destination

4.2 相手から応答がある場合

サーバでTPを実行します。引数に1を指定してTCP Keep-Alive機能を有効にします。

[root@server ~]# ./sv 1

クライアントでtsharkコマンドを実行します。

[root@client ~]# tshark -i eth0 port 11111

クライアントでTPを実行します。

[root@client ~]# ./cl

tsharkコマンドの実行結果を確認します。サーバからクライアントへのTCP Keep-Aliveパケットに対してACKパケットの応答があるので、サーバからクライアントにRSTパケットは送信されず、TCPコネクションが維持されたままであることがわかります。

[root@server ~]# tshark -i eth0 port 11111
Running as user "root" and group "root". This could be dangerous.
Capturing on 'eth0'
    1 0.000000000 192.168.122.177 → 192.168.122.68 TCP 74 51704 → 11111 [SYN] Seq=0 Win=29200 Len=0 MSS=1460 SACK_PERM=1 TSval=526571670 TSecr=0 WS=128
    2 0.000109383 192.168.122.68 → 192.168.122.177 TCP 74 11111 → 51704 [SYN, ACK] Seq=0 Ack=1 Win=28960 Len=0 MSS=1460 SACK_PERM=1 TSval=388235623 TSecr=526571670 WS=128
    3 0.001454536 192.168.122.177 → 192.168.122.68 TCP 66 51704 → 11111 [ACK] Seq=1 Ack=1 Win=29312 Len=0 TSval=526571672 TSecr=388235623
    4 30.284162023 192.168.122.68 → 192.168.122.177 TCP 66 [TCP Keep-Alive] 11111 → 51704 [ACK] Seq=0 Ack=1 Win=29056 Len=0 TSval=388265907 TSecr=526571672
    5 30.285131057 192.168.122.177 → 192.168.122.68 TCP 66 [TCP Keep-Alive ACK] 51704 → 11111 [ACK] Seq=1 Ack=1 Win=29312 Len=0 TSval=526601956 TSecr=388235623
    6 60.492579664 192.168.122.68 → 192.168.122.177 TCP 66 [TCP Keep-Alive] 11111 → 51704 [ACK] Seq=0 Ack=1 Win=29056 Len=0 TSval=388296115 TSecr=526601956
    7 60.493456777 192.168.122.177 → 192.168.122.68 TCP 66 [TCP Keep-Alive ACK] 51704 → 11111 [ACK] Seq=1 Ack=1 Win=29312 Len=0 TSval=526632164 TSecr=388235623
    8 90.700236661 192.168.122.68 → 192.168.122.177 TCP 66 [TCP Keep-Alive] 11111 → 51704 [ACK] Seq=0 Ack=1 Win=29056 Len=0 TSval=388326323 TSecr=526632164
    9 90.701144264 192.168.122.177 → 192.168.122.68 TCP 66 [TCP Keep-Alive ACK] 51704 → 11111 [ACK] Seq=1 Ack=1 Win=29312 Len=0 TSval=526662372 TSecr=388235623

5 メモ

setsockoptシステムコールを実行すると、sock_setsockopt関数でSO_KEEPALIVEオプションの設定を有効にします。

net/core/sock.c
int sock_setsockopt(struct socket *sock, int level, int optname,
                    char __user *optval, unsigned int optlen)
{
-snip-
        case SO_KEEPALIVE:
                if (sk->sk_prot->keepalive)
                        sk->sk_prot->keepalive(sk, valbool);
                sock_valbool_flag(sk, SOCK_KEEPOPEN, valbool);
                break;

TCP_KEEPIDLE、TCP_KEEPINTVL、TCP_KEEPCNTは以下の関数で設定されます。

net/ipv4/tcp.c
static int do_tcp_setsockopt(struct sock *sk, int level,
                int optname, char __user *optval, unsigned int optlen)
{
-snip-
        case TCP_KEEPIDLE:
                err = tcp_sock_set_keepidle_locked(sk, val);
                break;
        case TCP_KEEPINTVL:
                if (val < 1 || val > MAX_TCP_KEEPINTVL)
                        err = -EINVAL;
                else
                        tp->keepalive_intvl = val * HZ;
                break;
        case TCP_KEEPCNT:
                if (val < 1 || val > MAX_TCP_KEEPCNT)
                        err = -EINVAL;
                else
                        tp->keepalive_probes = val;
                break;

Z 参考情報

私が業務や記事執筆で参考にした書籍を以下のページに記載します。
Linux技術のスキルアップをしよう! - hana_shinのLinux技術ブログ
https://tldp.org/HOWTO/html_single/TCP-Keepalive-HOWTO