首页 > 数据库 > Go语言的IPv4/IPv6服务
2019
12-30

Go语言的IPv4/IPv6服务

最近新做了个功能,在华为服务器上运行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, 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}

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 main import ( "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 error http.Handle("/", &helloHandler{}) //err = http.ListenAndServe(":8083", nil) // IPv4 或 IPv6 //err = http.ListenAndServe("[2604:180:3:dd3::276e]:8083", nil) // 具体指定,仅 IPv6 err = 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.Addr if addr == "" { addr = ":http" } ln, err := net.Listen("tcp6", addr) // 仅指定 IPv6 if 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





扫码芷若 获取免费视频学习资料

编程学习

查 看2019高级编程视频教程免费获取