Hi! I'm having some confusion over the behaviour of net.Listen() and it's interactions with http.Server.
Can anyone take a look at this, and let me know what I'm doing wrong? Thanks! System description ------------------------- Go: go version go1.12.9 darwin/amd64 OS: macOS Mojave (10.14.6) Problem description -------------------------- Passing a net.Listener from net.Listen() into the Serve() method of an http.Server does not behave how I expect ... Test program ----------------- A simple server that responds to connections by echoing info regarding the URL by which it was contacted (see below): package main import ( "context" "flag" "fmt" "log" "net" "net/http" "os" "os/signal" "strconv" "syscall" "time" ) // Print information about the local machine's network interfaces func printNetworkInterfaces() { ifaces, err := net.Interfaces() if err != nil { panic("net.Interfaces()") } if len(ifaces)<1 { log.Println("No network interfaces found.") return } hostname, _ := os.Hostname() log.Println( "Network interfaces for " + hostname ) for _, iface := range ifaces { addrs, err := iface.Addrs() if err != nil { panic("iface.Addrs()") } if len(addrs) < 1 { continue } log.Println("-",iface.Name,iface.HardwareAddr) for _, addr := range addrs { switch v := addr.(type) { case *net.IPNet: str := fmt.Sprintf("IPNet: IP=%s, mask=%s, network=%s, string=%s", v.IP, v.Mask, v.Network(), v.String()) log.Println(" ", str) case *net.IPAddr: str := fmt.Sprintf("IPAddr: IP=%s, zone=%s, network=%s, string=%s", v.IP, v.Zone, v.Network(), v.String()) log.Println(" ", str) default: log.Println("<unknown>") } } } } // Just write the incoming url back to the sender func echoHandler(w http.ResponseWriter, r *http.Request) { txt := fmt.Sprintf("Echo: (%s)",r.URL.Path) w.Write( []byte(txt+"\n") ) log.Println(txt) } var ( listener_ = flag.Int("listener", 0, "Use an explicit net.Listener.") port_ = flag.Int("port", 0, "Set the port to listen on (0 = any free port?).") timeout_ = flag.Int("wait", 0, "Timout (in seconds) before server killed (0 = no timout).") ) func main() { onShutdown := func(what string, cleanup func()) { log.Println( fmt.Sprintf("- Shutting down %s ...",what) ) cleanup() log.Println( fmt.Sprintf(" %s shut down.",what) ) } flag.Parse() useListener := *listener_ port := *port_ timeout := *timeout_ // Let's see what interfaces are present on the local machine printNetworkInterfaces() // Simple server for incoming connections. http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {echoHandler(w,r)}); var listener net.Listener = nil addrString := fmt.Sprintf(":%d",port) // Using an explicit Listener provides more control over the specifics, // e.g. tcp4/6 and letting the system select a currently free port. if useListener>0 { log.Println("Using net.Listener") listener, err := net.Listen("tcp4", addrString) // :0 -> use any free port if err != nil { log.Fatalln(err) } defer onShutdown("listener", func() {listener.Close()} ) addrString = listener.Addr().String() host, portStr, err := net.SplitHostPort(addrString) // as port may have been assigned by system if err != nil { log.Fatalln(err) } log.Println( fmt.Sprintf("Listener Addr string: %s (host: %s, port: %s)",addrString,host,portStr) ) port, err = strconv.Atoi(portStr) if err != nil { log.Fatalln(err) } addrString = fmt.Sprintf(":%d",port) // as port may have been assigned by the system } server := http.Server { Addr: addrString } // Run web server in a separate goroutine so it doesn't block our progress go func(server *http.Server, listener net.Listener) { var err error if listener == nil { err = server.ListenAndServe() } else { err = server.Serve(listener) } switch err { case nil: case http.ErrServerClosed: log.Println("Caught ErrServerClosed") default: panic(err) } }(&server, listener) defer onShutdown("server", func() {server.Shutdown(context.Background())} ) log.Println("Port:", port) log.Println("Address:", addrString) // User interrupt channel sig := make(chan os.Signal, 1) signal.Notify(sig, os.Interrupt, syscall.SIGTERM) // Timeout channel, if needed tc := make(<-chan time.Time); if timeout > 0 { tc = time.After(time.Second * time.Duration(timeout)) } // Wait on user interrupt or timeout select { case <-sig: // user interrupt case <-tc: // timeout } // Cleanup log.Println("Shutting down.") } According to the Go docs <https://golang.org/pkg/net/#Listen>: For TCP networks, if the host in the address parameter is empty or a literal unspecified IP address, Listen listens on all available unicast and anycast IP addresses of the local system. To only use IPv4, use network "tcp4". It also says "See func Dial for a description of the network and address parameters", from which <https://golang.org/pkg/net/#Dial>: Known networks are "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". Therefore, I would expect that calling net.Listen("tcp4",":0") will listen on an (arbitrary) free port using all IPv4 interfaces. However, lsof indicates that it's listening for both IPv4 and IPv6 (I've masked some identifying information): me$ go run . --listener=1 2019/09/05 11:14:44 Network interfaces for XXXX 2019/09/05 11:14:44 - lo0 2019/09/05 11:14:44 IPNet: IP=127.0.0.1, mask=ff000000, network=ip+net, string=127.0.0.1/8 2019/09/05 11:14:44 IPNet: IP=::1, mask=ffffffffffffffffffffffffffffffff, network=ip+net, string=::1/128 2019/09/05 11:14:44 IPNet: IP=fe80::1, mask=ffffffffffffffff0000000000000000, network=ip+net, string=fe80::1/64 2019/09/05 11:14:44 - en0 x:x:x:x:x:x 2019/09/05 11:14:44 IPNet: IP=fe80::x:x:x:x, mask=ffffffffffffffff0000000000000000, network=ip+net, string=fe80::x:x:x:x/64 2019/09/05 11:14:44 IPNet: IP=10.195.66.129, mask=fffff800, network=ip+net, string=10.195.66.129/21 2019/09/05 11:14:44 - utun0 2019/09/05 11:14:44 IPNet: IP=fe80::x:x:x:x, mask=ffffffffffffffff0000000000000000, network=ip+net, string=fe80::x:x:x:x/64 2019/09/05 11:18:37 Using net.Listener 2019/09/05 11:18:37 Listener Addr string: 0.0.0.0:53133 (host: 0.0.0.0, port: 53133) 2019/09/05 11:14:44 Port: 53133 2019/09/05 11:14:44 Address: :53133 me, in another Terminal window$ lsof -i | grep LISTEN ARDAgent 360 me 9u IPv6 0xd5f6fecc3cdf4c41 0t0 TCP *:net-assistant (LISTEN) LogiVCCor 935 me 9u IPv4 0xd5f6fecc47403101 0t0 TCP *:iims (LISTEN) LogiVCCor 935 me 14u IPv6 0xd5f6fecc3cdf40c1 0t0 TCP *:iims (LISTEN) go_listen 14859 me 3u IPv4 0xd5f6fecc451a7781 0t0 TCP *:53133 (LISTEN) go_listen 14859 me 5u IPv6 0xd5f6fecc3cdf4681 0t0 TCP *:53133 (LISTEN) Furthermore, it's only responding on localhost and [::1]; using any of the other interface addresses listed by net.Interfaces() fails to get a response: me$ time curl localhost:53133/localhost Echo: (/localhost) real 0m0.015s user 0m0.004s sys 0m0.004s me$ time curl [::1]:53133/[::1] Echo: (/[::1]) real 0m0.015s user 0m0.004s sys 0m0.005s me$ time curl 127.0.0.1:53133/127.0.0.1 ^C real 0m4.521s user 0m0.004s sys 0m0.004s me$ time curl [fe80::1]:53133/[fe80::1] curl: (7) Couldn't connect to server real 0m0.014s user 0m0.004s sys 0m0.004s me$ time curl 10.195.66.129:53133/10.195.66.129 ^C real 0m6.465s user 0m0.004s sys 0m0.004s me$ time curl [fe80::x:x:x:x]:53133/[fe80::x:x:x:x] curl: (7) Couldn't connect to server real 0m0.013s user 0m0.004s sys 0m0.004s It looks like trying to connect via (non-localhost) IPv4 addresses hangs on both lo0 and en0 (127.0.0.1, 10.195.66.129), and (non-[::1]) IPv6 addresses flat out refuse to connect. However, if we skip the use of a net.Listener, it looks like the http.Server only listens for IPv6: me$ go run . 2019/09/05 11:26:38 Network interfaces for XXXX 2019/09/05 11:26:38 - lo0 2019/09/05 11:26:38 IPNet: IP=127.0.0.1, mask=ff000000, network=ip+net, string=127.0.0.1/8 2019/09/05 11:26:38 IPNet: IP=::1, mask=ffffffffffffffffffffffffffffffff, network=ip+net, string=::1/128 2019/09/05 11:26:38 IPNet: IP=fe80::1, mask=ffffffffffffffff0000000000000000, network=ip+net, string=fe80::1/64 2019/09/05 11:26:38 - en0 x:x:x:x:x:x 2019/09/05 11:26:38 IPNet: IP=fe80::x:x:x:x, mask=ffffffffffffffff0000000000000000, network=ip+net, string=fe80::x:x:x:x/64 2019/09/05 11:26:38 IPNet: IP=10.195.66.129, mask=fffff800, network=ip+net, string=10.195.66.129/21 2019/09/05 11:26:38 - utun0 2019/09/05 11:26:38 IPNet: IP=fe80::x:x:x:x, mask=ffffffffffffffff0000000000000000, network=ip+net, string=fe80::x:x:x:x/64 2019/09/05 11:26:38 Port: 0 2019/09/05 11:26:38 Address: :0 me, in another Terminal window$ lsof -i | grep LISTEN ARDAgent 360 me 9u IPv6 0xd5f6fecc3cdf4c41 0t0 TCP *:net-assistant (LISTEN) LogiVCCor 935 me 9u IPv4 0xd5f6fecc47403101 0t0 TCP *:iims (LISTEN) LogiVCCor 935 me 14u IPv6 0xd5f6fecc3cdf40c1 0t0 TCP *:iims (LISTEN) go_listen 15016 me 3u IPv6 0xd5f6fecc3cdf5201 0t0 TCP *:53170 (LISTEN) This approach (i.e., ignoring net.Listener to simply use server.ListenAndServe()) seems to do a better job of listening on multiple interfaces/addresses: me$ time curl localhost:53170/localhost Echo: (/localhost) real 0m0.015s user 0m0.004s sys 0m0.004s me$ time curl [::1]:53170/[::1] Echo: (/[::1]) real 0m0.015s user 0m0.004s sys 0m0.004s me$ time curl 127.0.0.1:53170/127.0.0.1 Echo: (/127.0.0.1) real 0m0.014s user 0m0.004s sys 0m0.004s me$ time curl [fe80::1]:53170/[fe80::1] curl: (7) Couldn't connect to server real 0m0.014s user 0m0.004s sys 0m0.004s me$ time curl 10.195.66.129:53170/10.195.66.129 Echo: (/10.195.66.129) real 0m0.015s user 0m0.004s sys 0m0.004s me$ time curl [fe80::x:x:x:x]:53170/[fe80::x:x:x:x] curl: (7) Couldn't connect to server real 0m0.015s user 0m0.004s sys 0m0.004s ... although it also has problems for IPv6 addresses other than ::1. If I use tcp instead of tcp4 in the call to net.Listen() (e.g. net.Listen("tcp", ":0")) I *always* get a bind error due to address already in use: me$ go run . --listener=1 2019/09/05 11:45:13 Network interfaces for XXXX 2019/09/05 11:45:13 - lo0 2019/09/05 11:45:13 IPNet: IP=127.0.0.1, mask=ff000000, network=ip+net, string=127.0.0.1/8 2019/09/05 11:45:13 IPNet: IP=::1, mask=ffffffffffffffffffffffffffffffff, network=ip+net, string=::1/128 2019/09/05 11:45:13 IPNet: IP=fe80::1, mask=ffffffffffffffff0000000000000000, network=ip+net, string=fe80::1/64 2019/09/05 11:45:13 - en0 x:x:x:x:x:x 2019/09/05 11:45:13 IPNet: IP=fe80::x:x:x:x, mask=ffffffffffffffff0000000000000000, network=ip+net, string=fe80::x:x:x:x/64 2019/09/05 11:45:13 IPNet: IP=10.195.66.129, mask=fffff800, network=ip+net, string=10.195.66.129/21 2019/09/05 11:45:13 - utun0 2019/09/05 11:45:13 IPNet: IP=fe80::x:x:x:x, mask=ffffffffffffffff0000000000000000, network=ip+net, string=fe80::x:x:x:x/64 2019/09/05 11:45:13 Using net.Listener 2019/09/05 11:45:13 Listener Addr string: [::]:53260 (host: ::, port: 53260) 2019/09/05 11:45:13 Port: 53260 2019/09/05 11:45:13 Address: :53260 panic: listen tcp :53260: bind: address already in use I get the same outcome if I use tcp6 in net.Listen(); I can only get the program to run using tcp4 which, on my machine at least, actually seems to open an additional IPv6 connection anyway - so I'm confused as to why net.Listen() doesn't seem to like tcp6. Despite the net.Listen() documentation directing you to net.Dial() docs for a discussion of the network parameter, some of these networks (e.g. ip, ip4, ip6) are unknown to net.Listen(). The documentation could be clearer in that respect! :) I can't figure out what's going on here. Any ideas what I might be doing wrong? -- You received this message because you are subscribed to the Google Groups "golang-nuts" group. To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts+unsubscr...@googlegroups.com. To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/cc253ab0-d32f-4ba2-bc15-527f8ce67e3b%40googlegroups.com.