后浪笔记一零二四

在华为服务器上运行go的http服务时,看到监听的端口是ipv6

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

先说结论

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 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: 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

但是,开启了这个参数后,go服务依然可以使用 IPv4 的地址来访问。

Go语言

内核参数没有生效,问题多半是发生在 syscall 的调用上,祭出 strace大杀器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
205 epoll_ctl(4, EPOLL_CTL_DEL, 3, 0xc42005bc04) = 0
206 close(3)
207 mmap(0xc420100000, 1048576, PORT_READ|PORT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xc420100000
208 mmap(0xc41fff0000, 32768, PORT_READ|PORT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xc41fff0000
209 socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPORTO_TCP) = 3
210 close(3)
211 socket(AF_INET6, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_TCP) = 3
212 setsockopt(3, SOL_IPV6, IPV6_V6ONLY, [1], 4) = 0
213 bind(3, {sa_family=AF_INET6, sin6_port=htons(0), inet_pton(AF_INET6, "::1", &sin6_ddr), sin6_f...})
214 socket(AF_INET6, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_TCP) = 5
215 setsockopt(5, SOL_IPV6, IPV6_V6ONLY, [0], 4) = 0
216 bind(5, {sa_family=AF_INET6, sin6_port=htons(0), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_a...)})
217 close(5)
218 close(3)
219 socket(AF_INET6, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 3
220 setsockopt(3, SOL_IPV6, IPV6_V6ONLY, [0], 4) = 0
221 setsockopt(3, SOL_SOCKET, SO_BROADCAST, [1], 4) = 0
222 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0

可以看到在 220 行 Golang 把准备 listen 的 socket 选项置为了 0。查看代码src/net/ipsock_posix.go :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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来选择,如:

1
2
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

示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 服务:

1
2
3
4
5
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

插曲

如果你用 Chrome 访问 http://localhost:6666 这样的地址,可能会看到 ERR_UNSAFE_PORT 这样的错误页面。这个其实是因为 Chrome 的非安全端口限制。

像这样的端口,一共有 64 个:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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]

本文发表于 0001-01-01,最后修改于 0001-01-01。

本站永久域名「 jiavvc.top 」,也可搜索「 后浪笔记一零二四 」找到我。


上一篇 « 下一篇 »

赞赏支持

请我吃鸡腿 =^_^=

i ysf

云闪付

i wechat

微信

推荐阅读

Big Image