最近新做了个功能,在华为服务器上运行go的http服务时,看到监听的端口是ipv6

而在阿里云机器上,则是ipv4

恰好该端口telnet不通,误以为是这个原因(其实是华为服务器默认有安全组,只开放5901-5910段
),顺便就研究了一下go在Linux上对IPv4/IPv6服务的方式。
Go的 net.Listen()
函数,如果不强行指定 IPv4 或 IPv6 ,在双栈系统上(VPS 同时支持 IPv4 和 IPv6)默认只会监听 IPv6 地址。这不影响客户端使用 IPv4 地址来访问。

Linux相关
使用man 7 ipv6
来查看如下,"The port space of IPv6 is shared with IPv4",ipv4地址在ipv6上有map ::FFF:<ipv4 address>。
IPv4 connections can be handled with the v6 API by using the v4-mapped-on-v6 address type;thus a program needs to support only this API type to support both protocols.This is handled transparently by the address handling functions in the C library.IPv4 and IPv6 share the local port space.When you get an IPv4 connection or packet to a IPv6 socket,its source address will be mapped to v6 and it will be mapped to v6.The address notation for IPv6 is a group of 8 4-digit hexadecimal numbers, separated with a ':'."::" stands for a string of 0 bits.Special addresses are ::1 for loopback and ::FFFF:<IPv4 address> for IPv4-mapped-on-IPv6.
在 linux 上有个内核参数 net.ipv6.bindv6only
,默认为关闭状态,这样 IPv6 的 socket 也就可以解析映射到同一个网卡的 IPv4 请求了。如果服务需要同时提供 IPv4 和 IPv6 的访问能力,只需要监听一个 IPv6 的 socket 即可。如果不希望 IPv4 可以访问 IPv6 的服务,就把 net.ipv6.bindv6only
置为 1:
$ cat /proc/sys/net/ipv6/bindv6only
1
但是,开启了这个参数后,go服务依然可以使用 IPv4 的地址来访问。
Go语言
内核参数没有生效,问题多半是发生在 syscall
的调用上,祭出 strace
大杀器:

可以看到在 220 行 Golang 把准备 listen 的 socket 选项置为了 0。查看代码 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, falsecase '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}
Go自己定义了 IPV6_V6ONLY 这个行为,至于这么做的原因在官方 Github 也有一些讨论:net: Listen is unfriendly to multiple address families, endpoints and subflows。
如何更准确地控制
在上层可以使用 http.ListenAndServe
来选择,如:
http.ListenAndServe(":8083", nil) // tcp
http.ListenAndServe("[2604:180:3:dd3::276e]:8083", nil) // 具体指定 tcp6
如果觉得具体指定 IPv6地址太麻烦,可以用 net.Listen
重构 ListenAndServe
,在该函数里指定 network ,可选参数:
"tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only),
"udp", "udp4" (IPv4-only), "udp6" (IPv6-only),
"ip", "ip4" (IPv4-only), "ip6" (IPv6-only),
"unix", "unixgram" and "unixpacket"
常用:
tcp 自动适配,优先IPv6
tcp4 仅使用IPv4
tcp6 仅使用IPv6
示例如下:
package mainimport ("fmt""net""net/http")type helloHandler struct{}func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {w.Write([]byte("Hello, world!"))}func main() {var err errorhttp.Handle("/", &helloHandler{})//err = http.ListenAndServe(":8083", nil) // IPv4 或 IPv6//err = http.ListenAndServe("[2604:180:3:dd3::276e]:8083", nil) // 具体指定,仅 IPv6err = ListenAndServe(":8083", nil) // 重构 ListenAndServe 函数if err != nil {fmt.Println(err)}}type tcpKeepAliveListener struct {*net.TCPListener}func ListenAndServe(addr string, handler http.Handler) error {srv := &http.Server{Addr: addr, Handler: handler}addr = srv.Addrif addr == "" {addr = ":http"}ln, err := net.Listen("tcp6", addr) // 仅指定 IPv6if err != nil {return err}return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})}
客户端可以用下面的命令行检测 IPv6 服务:
curl "http://[2604:180:3:dd3::276e]:8083"
curl -g -6 'http://[2604:180:3:dd3::276e]:8083/'
telnet -6 2604:180:3:dd3::276e 8083
拖地先生,从事互联网技术工作,在这里每周两篇文章,一起聊聊日常的技术点滴和软素质模型。
往期推荐:

如果对你有帮助,让大家也看看呗~




