Go言語でTCP待受にANYアドレスを指定した場合の挙動

最近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」を自前で作成するようにすれば解決です。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

*