原文:https://ms2008.github.io/2018/12/10/ipv6-bindv6only/
起因
對於 Golang 的 net.Listen()
函數,如果你不強行指定 IPv4 或 IPv6 的話,在雙棧系統上默認只會監聽 IPv6 地址。比如,用 Golang 實現一個 HTTP 服務非常簡單:
package main
import (
"net/http"
)
type helloHandler struct{}
func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, world!"))
}
func main() {
http.Handle("/", &helloHandler{})
http.ListenAndServe(":6666", nil)
}
啓動後,通過 netstat
查看可以確定服務只有在 IPv6 上監聽:
這時候,如果 curl
的是服務的 IPv4 地址,其實也是可以看到預期輸出的:
$ curl -i -s http://192.168.3.10:6666
HTTP/1.1 200 OK
Date: Sat, 08 Dec 2018 06:33:54 GMT
Content-Length: 13
Content-Type: text/plain; charset=utf-8
Hello, world!
之所以會有這樣的行爲,是因爲在 linux 上有個內核參數 net.ipv6.bindv6only
默認爲關閉狀態,這樣 IPv6 的 socket 也就可以解析映射到同一個網卡的 IPv4 請求了。這樣的話,如果我們的服務需要同時提供 IPv4 和 IPv6 的訪問能力,只需要監聽一個 IPv6 的 socket 即可。
我這裏並不希望 IPv4 可以訪問 IPv6 的服務,所以我把 net.ipv6.bindv6only
置爲了 1:
$ cat /proc/sys/net/ipv6/bindv6only
1
遺憾的是,開啓了這個參數後,似乎並沒有效果,curl
依然可以使用 IPv4 的地址來訪問。
溯源
怎麼解釋這個問題?既然是看起來內核參數沒有生效,問題多半是發生在 syscall
的調用上,祭出 strace
大殺器:
可以看到在 220 行 Golang 把準備 `listen` 的 socket 選項置爲了 0。顯然這個是 Golang 自身的行爲。
通過追蹤 Golang 的源碼,問題最終定位在了這裏 src/net/ipsock_posix.go
:
func favoriteAddrFamily(network string, laddr, raddr sockaddr, mode string) (family int, ipv6only bool) {
switch network[len(network)-1] {
case '4':
return syscall.AF_INET, false
case '6':
return syscall.AF_INET6, true
}
if mode == "listen" && (laddr == nil || laddr.isWildcard()) {
if supportsIPv4map() || !supportsIPv4() {
return syscall.AF_INET6, false
}
if laddr == nil {
return syscall.AF_INET, false
}
return laddr.family(), false
}
if (laddr == nil || laddr.family() == syscall.AF_INET) &&
(raddr == nil || raddr.family() == syscall.AF_INET) {
return syscall.AF_INET, false
}
return syscall.AF_INET6, false
}
原來 Golang 自己定義了 IPV6_V6ONLY 這個行爲,至於這麼做的原因在官方 Github 也有一些討論:net: Listen is unfriendly to multiple address families, endpoints and subflows
既然是這樣,那如何解決這個問題?
- 自己定製需要的
net.Listen()
listen
完整的 IPv4 和 IPv6 地址
插曲
如果你用 Chrome 訪問 http://localhost:6666 這樣的地址,可能會看到 ERR_UNSAFE_PORT
這樣的錯誤頁面。這個其實是因爲 Chrome 的非安全端口限制。
像這樣的端口,一共有 64 個:
1, // tcpmux
7, // echo
9, // discard
11, // systat
13, // daytime
15, // netstat
17, // qotd
19, // chargen
20, // ftp data
21, // ftp access
22, // ssh
23, // telnet
25, // smtp
37, // time
42, // name
43, // nicname
53, // domain
77, // priv-rjs
79, // finger
87, // ttylink
95, // supdup
101, // hostriame
102, // iso-tsap
103, // gppitnp
104, // acr-nema
109, // pop2
110, // pop3
111, // sunrpc
113, // auth
115, // sftp
117, // uucp-path
119, // nntp
123, // NTP
135, // loc-srv /epmap
139, // netbios
143, // imap2
179, // BGP
389, // ldap
465, // smtp+ssl
512, // print / exec
513, // login
514, // shell
515, // printer
526, // tempo
530, // courier
531, // chat
532, // netnews
540, // uucp
556, // remotefs
563, // nntp+ssl
587, // stmp?
601, // ??
636, // ldap+ssl
993, // ldap+ssl
995, // pop3+ssl
2049, // nfs
3659, // apple-sasl / PasswordServer
4045, // lockd
6000, // X11
6665, // Alternate IRC [Apple addition]
6666, // Alternate IRC [Apple addition]
6667, // Standard IRC [Apple addition]
6668, // Alternate IRC [Apple addition]
6669, // Alternate IRC [Apple addition]