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 ---------------| | |
その他ソケットオプションについても記事を書きました。
ソケットオプションの使い方(SO_REUSEPORT) - hana_shinのLinux技術ブログ
ソケットオプションの使い方(TCP_NODELAY編) - hana_shinのLinux技術ブログ
ソケットオプションの使い方(TCP_CORK編) - hana_shinのLinux技術ブログ
ソケットオプションの使い方(SO_REUSEADDR編) - hana_shinのLinux技術ブログ
ソケットオプションの使い方(SO_SNDBUF編) - hana_shinのLinux技術ブログ
ソケットオプションの使い方(SO_SNDTIMEO編) - hana_shinのLinux技術ブログ
ソケットオプションの使い方(SO_RCVTIMEO編) - hana_shinのLinux技術ブログ
ソケットオプションの使い方(SO_KEEPALIVE編) - hana_shinのLinux技術ブログ
ソケットオプションの使い方(TCP_NODELAY編) - hana_shinのLinux技術ブログ
ソケットオプションの使い方(SO_LINGER編) - hana_shinのLinux技術ブログ
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; }
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コマンドの使い方は、https://qiita.com/hana_shin/items/956dfaca4539ba257c16を参照してください(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