ソケットオプションの使い方(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 ---------------|
| |
その他ソケットオプションについても記事を書きました。
ソケットオプションの使い方(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/0x10iptablesの設定を削除します。
[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

