いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-548 (GoでTCP通信切断時にRSTを送信)(SO_LINGER, TCPConn.SetLinger)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ

概要

以下、自分用のメモです。忘れないようにここにメモメモ。。。

通信処理を書いていると、たまにプロトコル仕様にて特定のタイミングでRSTを送信する必要があったりします。

C言語の場合だと SO_LINGER0 で設定して close システムコール を呼ぶとRSTが送信されます。

Goの場合、ソケットは抽象化された net.Conn (*net.TCPConn) という形になっています。

んで、そのものスバリのメソッドが *net.TCPConn.SetLinger という名前のメソッドで存在します。

$ go doc net.tcpconn.setlinger
package net // import "net"

func (c *TCPConn) SetLinger(sec int) error
    SetLinger sets the behavior of Close on a connection which still has data
    waiting to be sent or to be acknowledged.

    If sec < 0 (the default), the operating system finishes sending the data in
    the background.

    If sec == 0, the operating system discards any unsent or unacknowledged
    data.

    If sec > 0, the data is sent in the background as with sec < 0. On some
    operating systems including Linux, this may cause Close to block until all
    data has been sent or discarded. On some operating systems after sec seconds
    have elapsed any remaining unsent data may be discarded.

サンプル

いちいちファイル分けて実装するのが面倒だったので、サーバーとクライアント兼用です。

main.go

package main

import (
    "flag"
    "net"
)

type (
    Args struct {
        IsServer bool
        UseRst   bool
    }
)

var (
    args Args
)

func init() {
    flag.BoolVar(&args.IsServer, "server", false, "サーバモードで起動")
    flag.BoolVar(&args.UseRst, "rst", false, "RST送信による強制切断を使用")
}

func main() {
    flag.Parse()

    if err := run(); err != nil {
        panic(err)
    }
}

func run() error {
    var (
        err error
    )
    if args.IsServer {
        err = runServer()
    } else {
        err = runClient()
    }

    if err != nil {
        return err
    }

    return nil
}

func runServer() error {
    var (
        l   net.Listener
        err error
    )
    l, err = net.Listen("tcp", ":8888")
    if err != nil {
        return err
    }
    defer l.Close()

    // サンプルなので1回のみ接続を受付
    var (
        conn net.Conn
    )
    conn, err = l.Accept()
    if err != nil {
        return err
    }
    defer conn.Close()

    // 適当にデータを送信しクライアントが切ってきたら終わり
    _, err = conn.Write([]byte("hello"))
    if err != nil {
        return err
    }

    var (
        buf = make([]byte, 10)
        n   int
    )
    for {
        clear(buf)

        n, err = conn.Read(buf)
        if n == 0 || err != nil {
            break
        }
    }

    if args.UseRst {
        // RST送信するために SO_LINGER を設定
        // Goの場合 *net.TCPConn に SetLinger メソッドが用意されている。
        var (
            tcpConn   *net.TCPConn
            ok        bool
            lingerSec = 0
        )
        tcpConn, ok = conn.(*net.TCPConn)
        if ok {
            // $ go doc net.tcpconn.setlinger
            //
            // > SetLinger sets the behavior of Close on a connection which still has data waiting to be sent or to be acknowledged.
            // > If sec < 0 (the default), the operating system finishes sending the data in the background.
            // > If sec == 0, the operating system discards any unsent or unacknowledged data.
            // > If sec > 0, the data is sent in the background as with sec < 0. On some operating systems including Linux,
            // > this may cause Close to block until all data has been sent or discarded.
            // > On some operating systems after sec seconds have elapsed any remaining unsent data may be discarded.
            tcpConn.SetLinger(lingerSec)
        }
    }

    return nil
}

func runClient() error {
    var (
        conn net.Conn
        err  error
    )
    conn, err = net.Dial("tcp", "localhost:8888")
    if err != nil {
        return err
    }
    defer func() {
        conn.Close()
    }()

    // データを受信したら切断
    var (
        buf = make([]byte, 10)
    )
    _, err = conn.Read(buf)
    if err != nil {
        return err
    }

    if args.UseRst {
        tcpConn, ok := conn.(*net.TCPConn)
        if ok {
            tcpConn.SetLinger(0)
        }
    }

    return nil
}

Taskfile.yml

tcpdumpコマンドでパケットを確認するようにして実行します。

# https://taskfile.dev

version: '3'

tasks:
  default:
    cmds:
      - task: build
      - task: run
  build:
    cmds:
      - go build -o app main.go
  run:
    cmds:
      - task: run-fin
      - sleep 2
      - task: run-rst
  run-fin:
    cmds:
      - sudo tcpdump -i lo -n 'tcp port 8888' -S &
      - sleep 1
      - ./app -server &
      - ./app
      - sleep 2
      - sudo pkill tcpdump
  run-rst:
    cmds:
      - sudo tcpdump -i lo -n 'tcp port 8888' -S &
      - sleep 1
      - ./app -server -rst &
      - ./app -rst
      - sleep 2
      - sudo pkill tcpdump

shell

実行すると以下のように出力されます。

$ task
task: [build] go build -o app main.go
task: [run-fin] sudo tcpdump -i lo -n 'tcp port 8888' -S &
task: [run-fin] sleep 1
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on lo, link-type EN10MB (Ethernet), snapshot length 262144 bytes
task: [run-fin] ./app -server &
task: [run-fin] ./app
task: [run-fin] sleep 2
09:22:02.504826 IP 127.0.0.1.32782 > 127.0.0.1.8888: Flags [S], seq 1561972627, win 43690, options [mss 65495,sackOK,TS val 3209706398 ecr 0,nop,wscale 7], length 0
09:22:02.504835 IP 127.0.0.1.8888 > 127.0.0.1.32782: Flags [S.], seq 2366941560, ack 1561972628, win 43690, options [mss 65495,sackOK,TS val 3209706398 ecr 3209706398,nop,wscale 7], length 0
09:22:02.504844 IP 127.0.0.1.32782 > 127.0.0.1.8888: Flags [.], ack 2366941561, win 342, options [nop,nop,TS val 3209706398 ecr 3209706398], length 0
09:22:02.504914 IP 127.0.0.1.8888 > 127.0.0.1.32782: Flags [P.], seq 2366941561:2366941566, ack 1561972628, win 342, options [nop,nop,TS val 3209706398 ecr 3209706398], length 5
09:22:02.504924 IP 127.0.0.1.32782 > 127.0.0.1.8888: Flags [.], ack 2366941566, win 342, options [nop,nop,TS val 3209706398 ecr 3209706398], length 0
09:22:02.504944 IP 127.0.0.1.32782 > 127.0.0.1.8888: Flags [F.], seq 1561972628, ack 2366941566, win 342, options [nop,nop,TS val 3209706398 ecr 3209706398], length 0
09:22:02.504982 IP 127.0.0.1.8888 > 127.0.0.1.32782: Flags [F.], seq 2366941566, ack 1561972629, win 342, options [nop,nop,TS val 3209706398 ecr 3209706398], length 0
09:22:02.504995 IP 127.0.0.1.32782 > 127.0.0.1.8888: Flags [.], ack 2366941567, win 342, options [nop,nop,TS val 3209706398 ecr 3209706398], length 0
task: [run-fin] sudo pkill tcpdump

8 packets captured
16 packets received by filter
0 packets dropped by kernel
task: [run] sleep 2
task: [run-rst] sudo tcpdump -i lo -n 'tcp port 8888' -S &
task: [run-rst] sleep 1
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on lo, link-type EN10MB (Ethernet), snapshot length 262144 bytes
task: [run-rst] ./app -server -rst &
task: [run-rst] ./app -rst
task: [run-rst] sleep 2
09:22:07.566462 IP 127.0.0.1.50180 > 127.0.0.1.8888: Flags [S], seq 3347564028, win 43690, options [mss 65495,sackOK,TS val 3209711459 ecr 0,nop,wscale 7], length 0
09:22:07.566476 IP 127.0.0.1.8888 > 127.0.0.1.50180: Flags [S.], seq 671550310, ack 3347564029, win 43690, options [mss 65495,sackOK,TS val 3209711459 ecr 3209711459,nop,wscale 7], length 0
09:22:07.566485 IP 127.0.0.1.50180 > 127.0.0.1.8888: Flags [.], ack 671550311, win 342, options [nop,nop,TS val 3209711459 ecr 3209711459], length 0
09:22:07.566556 IP 127.0.0.1.8888 > 127.0.0.1.50180: Flags [P.], seq 671550311:671550316, ack 3347564029, win 342, options [nop,nop,TS val 3209711459 ecr 3209711459], length 5
09:22:07.566566 IP 127.0.0.1.50180 > 127.0.0.1.8888: Flags [.], ack 671550316, win 342, options [nop,nop,TS val 3209711459 ecr 3209711459], length 0
09:22:07.566589 IP 127.0.0.1.50180 > 127.0.0.1.8888: Flags [R.], seq 3347564029, ack 671550316, win 342, options [nop,nop,TS val 3209711459 ecr 3209711459], length 0
task: [run-rst] sudo pkill tcpdump

6 packets captured
12 packets received by filter
0 packets dropped by kernel

最初の実行は通常通りのFIN送信で実行しています。パケットの Flags を見ると

127.0.0.1.32782 > 127.0.0.1.8888: Flags [F.]
127.0.0.1.8888 > 127.0.0.1.32782: Flags [F.]
127.0.0.1.32782 > 127.0.0.1.8888: Flags [.]

となっており、ちゃんとFIN (FがFINの意味) が飛んでますね。

2回目の実行はRST送信するように SO_LINGER を設定してから close しています。パケットの Flags を見ると

127.0.0.1.50180 > 127.0.0.1.8888: Flags [R.]

でRST (RがRSTの意味) が送信されて、それでプチっと終了していますね。

参考情報

net package - net - Go Packages

ソケットオプションの使い方(SO_LINGER編) - hana_shinのLinux技術ブログ

sockets - When is TCP option SO_LINGER (0) required? - Stack Overflow

C言語 close()でRST送信する #ソケットプログラミング - Qiita

Goのおすすめ書籍


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。