最近Go言語でのシステム開発を行うことがあったのですが、ANYアドレスを使ったTCP待受でハマった挙動があったのでメモしておきます。
今回を確認している環境はLinuxです。WindowsやMac OSは挙動が異なるかもしれません。
環境
OS | Ubuntu 16.04.2 LTS |
Kernel | 4.4.0-62-generic |
Golang | go1.8 linux/amd64 |
サンプル
今回はHTTPサーバのサンプルです。
package main
import (
"flag"
"fmt"
"net"
"net/http"
)
var proto = flag.String("proto", "tcp", "proto(tcp|tcp4|tcp6)")
var addr = flag.String("addr", "0.0.0.0", "listen addres")
var port = flag.Int("port", 9999, "listen port")
func main() {
flag.Parse()
fmt.Printf("proto[%s]listen[%s:%d]\n", *proto, *addr, *port)
tcpAddr, err := net.ResolveTCPAddr(*proto, fmt.Sprintf("%s:%d", *addr, *port))
if err != nil {
panic(err)
}
listener, err := net.ListenTCP(*proto, tcpAddr)
if err != nil {
panic(err)
}
defer listener.Close()
http.HandleFunc("/", handler)
s := http.Server{}
s.Serve(listener)
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World")
}
このソースをビルドして起動します。
$ ./httptest
proto[tcp]listen[0.0.0.0:9999]
$ curl http://localhost:9999/
Hello, World
ということでHTTP通信自体はなんら問題はないのですが、netstatでlistenソケットを表示すると
$ netstat -lnt
稼働中のインターネット接続 (サーバのみ)
Proto 受信-Q 送信-Q 内部アドレス 外部アドレス 状態
tcp6 0 0 :::9999 :::* LISTEN
IPv4マップドアドレス(IPv4射影アドレス)が使われています。
試しにIPv4とIPv6のループバックアドレスを指定すると以下になります。
IPv4
$ ./httptest -addr 127.0.0.1
proto[tcp]listen[127.0.0.1:9999]
$ netstat -lnt
稼働中のインターネット接続 (サーバのみ)
Proto 受信-Q 送信-Q 内部アドレス 外部アドレス 状態
tcp 0 0 127.0.0.1:9999 0.0.0.0:* LISTEN
IPv6
$ ./httptest -addr [::1]
proto[tcp]listen[[::1]:9999]
$ netstat -lnt
稼働中のインターネット接続 (サーバのみ)
Proto 受信-Q 送信-Q 内部アドレス 外部アドレス 状態
tcp6 0 0 ::1:9999 :::* LISTEN
つまり「net.ListenTCP」に”tcp”を指定した場合、IPアドレスでIPv4かIPv6かを判断しているが、ANYアドレスの場合はIPv6でマップする仕様となっているようです。
通常これで困るケースはないのですが、今回はTCP接続中のコネクション情報をnetstatではなくprocから直接検索する時にIPv4とIPv6では参照先が異なるために情報が見つからずマッチしないというケースに遭遇しました。
IPv4 | /proc/net/tcp |
IPv6 | /proc/net/tcp6 |
なお、procを直接参照せずともnetstatコマンドの引数でIPv4限定にした場合でも同じくマッチはしません。
これを回避するには「net.ListenTCP」に”tcp4″を指定します。
$ ./httptest -proto tcp4
proto[tcp4]listen[0.0.0.0:9999]
$ netstat -lnt
稼働中のインターネット接続 (サーバのみ)
Proto 受信-Q 送信-Q 内部アドレス 外部アドレス 状態
tcp 0 0 0.0.0.0:9999 0.0.0.0:* LISTEN
HTTPを使うケースで今回と同様の事で困るケースは殆どないと思いますが、その場合は「http.ListenAndServe」だと”tcp”固定なので上記コードのように「net.TCPListener」を自前で作成するようにすれば解決です。