【golang】net/httpのタイムアウト設定

# 概要
先日golangci-lintのバージョンアップを行ったところ、lintの際に下記のような警告が発生しました。
```bash
cmd/main.go:134:11: G114: Use of net/http serve function that has no support for setting timeouts (gosec)
        if err = http.ListenAndServe(":8080", mux); err != nil {
```
調べてみてなるほどと思ったので備忘録として残します。

# 前提
- go version go1.19.3 linux/amd64
- golangci-lint version 1.50.1 

# net/httpのタイムアウト

golangの標準ライブラリのnet/httpでは、デフォルトではタイムアウト設定が存在しません。
つまりデフォルトではタイムアウトが発生せず、リクエストの処理終わりを常に待機し続けることになります。

下記はnet/httpライブラリのServer構造体ですが、`ReadTimeout`、`ReadHeaderTimeout`、`WriteTimeout`、`IdleTimeout`の説明を読むとどれも`値が存在しない場合はタイムアウトしない`と記載されています。

```golang
// A Server defines parameters for running an HTTP server.
// The zero value for Server is a valid configuration.
type Server struct {
	// Addr optionally specifies the TCP address for the server to listen on,
	// in the form "host:port". If empty, ":http" (port 80) is used.
	// The service names are defined in RFC 6335 and assigned by IANA.
	// See net.Dial for details of the address format.
	Addr string

	Handler Handler // handler to invoke, http.DefaultServeMux if nil

	// TLSConfig optionally provides a TLS configuration for use
	// by ServeTLS and ListenAndServeTLS. Note that this value is
	// cloned by ServeTLS and ListenAndServeTLS, so it's not
	// possible to modify the configuration with methods like
	// tls.Config.SetSessionTicketKeys. To use
	// SetSessionTicketKeys, use Server.Serve with a TLS Listener
	// instead.
	TLSConfig *tls.Config

	// ReadTimeout is the maximum duration for reading the entire
	// request, including the body. A zero or negative value means
	// there will be no timeout.
	//
	// Because ReadTimeout does not let Handlers make per-request
	// decisions on each request body's acceptable deadline or
	// upload rate, most users will prefer to use
	// ReadHeaderTimeout. It is valid to use them both.
	ReadTimeout time.Duration

	// ReadHeaderTimeout is the amount of time allowed to read
	// request headers. The connection's read deadline is reset
	// after reading the headers and the Handler can decide what
	// is considered too slow for the body. If ReadHeaderTimeout
	// is zero, the value of ReadTimeout is used. If both are
	// zero, there is no timeout.
	ReadHeaderTimeout time.Duration

	// WriteTimeout is the maximum duration before timing out
	// writes of the response. It is reset whenever a new
	// request's header is read. Like ReadTimeout, it does not
	// let Handlers make decisions on a per-request basis.
	// A zero or negative value means there will be no timeout.
	WriteTimeout time.Duration

	// IdleTimeout is the maximum amount of time to wait for the
	// next request when keep-alives are enabled. If IdleTimeout
	// is zero, the value of ReadTimeout is used. If both are
	// zero, there is no timeout.
	IdleTimeout time.Duration

	// MaxHeaderBytes controls the maximum number of bytes the
	// server will read parsing the request header's keys and
	// values, including the request line. It does not limit the
	// size of the request body.
	// If zero, DefaultMaxHeaderBytes is used.
	MaxHeaderBytes int

	// TLSNextProto optionally specifies a function to take over
	// ownership of the provided TLS connection when an ALPN
	// protocol upgrade has occurred. The map key is the protocol
	// name negotiated. The Handler argument should be used to
	// handle HTTP requests and will initialize the Request's TLS
	// and RemoteAddr if not already set. The connection is
	// automatically closed when the function returns.
	// If TLSNextProto is not nil, HTTP/2 support is not enabled
	// automatically.
	TLSNextProto map[string]func(*Server, *tls.Conn, Handler)

	// ConnState specifies an optional callback function that is
	// called when a client connection changes state. See the
	// ConnState type and associated constants for details.
	ConnState func(net.Conn, ConnState)

	// ErrorLog specifies an optional logger for errors accepting
	// connections, unexpected behavior from handlers, and
	// underlying FileSystem errors.
	// If nil, logging is done via the log package's standard logger.
	ErrorLog *log.Logger

	// BaseContext optionally specifies a function that returns
	// the base context for incoming requests on this server.
	// The provided Listener is the specific Listener that's
	// about to start accepting requests.
	// If BaseContext is nil, the default is context.Background().
	// If non-nil, it must return a non-nil context.
	BaseContext func(net.Listener) context.Context

	// ConnContext optionally specifies a function that modifies
	// the context used for a new connection c. The provided ctx
	// is derived from the base context and has a ServerContextKey
	// value.
	ConnContext func(ctx context.Context, c net.Conn) context.Context

	inShutdown atomicBool // true when server is in shutdown

	disableKeepAlives int32     // accessed atomically.
	nextProtoOnce     sync.Once // guards setupHTTP2_* init
	nextProtoErr      error     // result of http2.ConfigureServer if used

	mu         sync.Mutex
	listeners  map[*net.Listener]struct{}
	activeConn map[*conn]struct{}
	doneChan   chan struct{}
	onShutdown []func()

	listenerGroup sync.WaitGroup
}
```

# 各タイムアウト設定の意味
[こちら](https://christina04.hatenablog.com/entry/go-timeouts)のブログが非常にわかりやすくまとまっていましたが、自分の言葉でもまとめておきます。

### ReadHeaderTimeout
リクエストヘッダーを読み切るまでのタイムアウト設定です。

### ReadTimeout
リクエストヘッダーとリクエストボディを読み切るまでのタイムアウト設定です。

### WriteTimeout
リクエストボディの読み込み ~ レスポンスの書き込みまでのタイムアウト設定です。

### IdleTimeout
keep-alivesが有効な場合に次のリクエストが来るまで待つまでの時間です。


# 警告について

ここで警告の内容に戻ると、`http.ListenAndServe()`を使ったサーバー起動方法では、タイムアウト設定ができないために警告してくれているということでした。

そこでサーバー起動の記述を下記のように書き換えました。
`http.Server{}`でserverインスタンスを作成すれば、タイムアウト設定ができると考えたからです。

```golang
	srv := &http.Server{
		Addr:    ":",
		Handler: mux,
	}
	if err = srv.ListenAndServe(); err != nil {
		zap.S().Panicf("filed to serve: %v", err)
	}
```

この記述に変えた後にlintを走らせると、次は以下のような警告が表示されました。
```bash
cmd/main.go:120:10: G112: Potential Slowloris Attack because ReadHeaderTimeout is not configured in the http.Server (gosec)
        srv := &http.Server{
                Addr:    ":",
                Handler: mux,
        }

```

# Slowloris Attackとは
https://products.nvc.co.jp/imperva/blog/glossary-ddos.html

> slowloris攻撃は、できるだけ長く、標的のWebサーバへの接続をオープンに保持することにより攻撃を実施します。 標的のサーバへの接続を生成しますが、要求を部分的にしか送信しません。 つまり、継続的に多くのHTTPヘッダを送信しますが、要求を完了することがなく、標的のサーバはこの偽の接続をオープンにしたままになります。 結果として、最大同時接続プールをオーバーフローし、正当なクライアントからの追加の接続を拒否さぜるを得なくなります。

つまりこのような流れが想定できます。

- 攻撃者は当サーバーに何かしらのリクエストを送る
- このリクエストではサーバーとのコネクションを貼るものの、何もせずコネクションを維持し続ける
- 攻撃者はこの何もしないリクエストを大量にサーバーに送信する
- サーバー側ではタイムアウト設定を行なっていないため、コネクションを維持し続けてしまう
- いずれ並列処理実行数が限界を迎え、正規のリクエストが通らなくなってしまう。

# Slowloris Attackの対策
警告が教えてくれている通り`ReadHeaderTimeout`を設定するだけです。
あまり短くしすぎると正規のリクエストを弾いてしまう可能性があるため、20秒で設定しました。

```golang
	srv := &http.Server{
		Addr:    ":" + conf.Port,
		Handler: mux,
		ReadHeaderTimeout: 20 * time.Second,
	}
	if err = srv.ListenAndServe(); err != nil {
		zap.S().Panicf("filed to serve: %v", err)
	}
```

これでlintをすると警告が消えました

# 補足
実際の環境に移す際はサーバーまでにロードバランサを組み合わせていたり、nginxなどのproxyが入っていることが多いのではないかなと思います。
AWSのALBであればデフォルトのタイムアウト設定でこういった攻撃は弾くことができますし、nginxでもデフォルトでタイムアウト設定がなされているため問題は生じないはずです。
とはいえ、言語の仕様などを理解してサーバー構築を行う必要がある内容だなと感じました。
(タイムアウトくらいデフォルトで設定してくれていてもいいのになぁという気持ち)

# 参考
- https://christina04.hatenablog.com/entry/go-timeouts
- https://christina04.hatenablog.com/entry/server-request-timeout
- https://blog.cloudflare.com/ja-jp/the-complete-guide-to-golang-net-http-timeouts-ja-jp/

# さいごに

トレタでは一緒に開発する仲間を募集しています。<br>
興味がある方は是非カジュアル面談へお越しください！

https://www.wantedly.com/companies/toreta/projects