😺

goのtcp serverでkeepaliveを調べてみた

2022/06/19に公開

概要

TCP/IPの通信をしているとkeepaliveという機能がでてきます。これが有効になってるのかそもそも論どっちがデフォルトなんだとかよくわからないので追ってみました。

KeepAliveとは?

TCPの機能でRFC793 に記述されている(ただこのRFC改訂版が出るらしくて新しい番号がつくらしい)。こちらに書かれているkeepaliveの実装を概ねこんな感じで、長い間送受信がない状態というアイドル状態の定義このアイドル接続がアクティブかどうかをprobe segmentを送信して確認するSEG.SEQ = SND.NXT-1が含まれる)。この記述はlinuxだと以下に現れる。

アプリケーションはTCPコネクションに対してオンまたはオフにできなければいけない。これはsocketに対してオプションを渡すことによって達成される。他にも方法はありこちらが詳しい。
KeepAliveはSO_KEEPALIVEを設定することによりオンにできる。 デフォルトはオフでなければならない(OS自身の設定)。

SEG.SEQ = SND.NXT-1 について

linuxのコードを見ると以下のように存在しているが、他のOSでもどうなっているか調べたが少なくともgrepなどでは引っかからなかった。

 ag 'SEG.SEQ'     
net/ipv4/tcp_output.c
3990: * one is with SEG.SEQ=SND.UNA to deliver urgent pointer, another is

net/netfilter/nf_synproxy_core.c
696:			/* Keep-Alives are sent with SEG.SEQ = SND.NXT-1,
1119:			/* Keep-Alives are sent with SEG.SEQ = SND.NXT-1,

golangでtcp serverの場合

  • goでkeepaliveでのTCP通信をして検証してみる
package main

import (
	"io"
	"log"
	"net"
)

func main() {
	l, err := net.Listen("tcp", ":2000")
	if err != nil {
		panic(err)
	}

	defer l.Close()

	for {
		conn, err := l.Accept()
		if err != nil {
			log.Fatal(err)
		}
		go func(c net.Conn) {
			io.Copy(c, c)
			c.Close()
		}(conn)
	}
}
nc -v 127.0.0.1 2000
  • localで通信をキャプチャするとKeepAliveの通信しているのがわかります
No.     Time           Source                Destination           Protocol Length Info
     23 76.389967682   127.0.0.1             127.0.0.1             TCP      66     [TCP Keep-Alive] 2000 → 50212 [ACK] Seq=0 Ack=1 Win=65536 Len=0 TSval=409283988 TSecr=409268883

RFCの方を見るとMUST default to off (MUST-25) と書かれていますがこちらはOSの方の設定をさしています。GoのTCPServerでは特に何かを設定したわけではないですがKeepAliveが効いています。ドキュメントのListenConfig見るとosやプロトコルでサポートされていれば有効になるようです(特に何かを設定しない限り0になるはず)。

socketへのオプション

  • 実際にどこでkeepaliveがオンになっているかを見ていく
conn, err := l.Accept()
func (l *TCPListener) Accept() (Conn, error) {
	if !l.ok() {
		return nil, syscall.EINVAL
	}
	c, err := l.accept()
	if err != nil {
		return nil, &OpError{Op: "accept", Net: l.fd.net, Source: nil, Addr: l.fd.laddr, Err: err}
	}
	return c, nil
}
  • Acceptからaccept()を呼び出しているln.lc.KeepAliveはデフォルトで0なんで
  • setKeepAlive() を呼び出している
func (ln *TCPListener) accept() (*TCPConn, error) {
	fd, err := ln.fd.accept()
	if err != nil {
		return nil, err
	}
	tc := newTCPConn(fd)
	if ln.lc.KeepAlive >= 0 {
		setKeepAlive(fd, true)
		ka := ln.lc.KeepAlive
		if ln.lc.KeepAlive == 0 {
			ka = defaultTCPKeepAlive
		}
		setKeepAlivePeriod(fd, ka)
	}
	return tc, nil
}
  • setKeepAliveメソッドでsyscall.SOL_SOCKET syscall.SO_KEEPALIVE を渡している
    • SOL_SOCKET はソケットレベルでオプションを指定するときに必要
    • SO_KEEPALIVE はKeepAliveを有効にする
  • これによりソケットでKeepAliveが有効になる
func setKeepAlive(fd *netFD, keepalive bool) error {
	err := fd.pfd.SetsockoptInt(syscall.SOL_SOCKET, syscall.SO_KEEPALIVE, boolint(keepalive))
	runtime.KeepAlive(fd)
	return wrapSyscallError("setsockopt", err)
}
  • この状態でアクセスを受け付けるのでKeepAliveが有効な状態で通信が行われる
  • KeepAliveが何をしているかは最初に書いたが詳細はこちらを見てもらったほうがいい

まとめ

  • KeepAliveとはTCPの機能でRFC793 に記述されている
  • 長い間送受信がない状態の通信がアクティブかどうかセグメント送信して確認する
  • keepaliveの機能はOn/OffができてデフォルトはOff(あーこれOSレベルの方の話か)
  • GoのTCP Serverの場合デフォルトでkeepaliveの機能は有効化されている
  • 関係ないがOSの中身追うのとQUICが気になったので追ってみます

参考

Discussion