😺

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

2022/06/19に公開

概要

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

KeepAliveとは?

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

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) と書かれているのでOFFになっていると考えていましたが
  • 上記GoのTCPServerでは特に何かを設定したわけではないですがKeepAliveが効いています
  • ドキュメントのListenConfig見るとosやプロトコルでサポートされていれば有効になるようです(特に何かを設定しない限り0になるはず)。
  • 何かしら経緯があるんだろうなとgit blameして見た感じよくわからなかった
  • 追記: このdefaultはOS側の話かもしれない

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