| // Copyright 2019 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package nettest provides utilities for network testing. |
| package nettest |
| |
| import ( |
| "errors" |
| "fmt" |
| "io/ioutil" |
| "net" |
| "os" |
| "os/exec" |
| "runtime" |
| "strconv" |
| "strings" |
| "sync" |
| "time" |
| ) |
| |
| var ( |
| stackOnce sync.Once |
| ipv4Enabled bool |
| ipv6Enabled bool |
| unStrmDgramEnabled bool |
| rawSocketSess bool |
| |
| aLongTimeAgo = time.Unix(233431200, 0) |
| neverTimeout = time.Time{} |
| |
| errNoAvailableInterface = errors.New("no available interface") |
| errNoAvailableAddress = errors.New("no available address") |
| ) |
| |
| func probeStack() { |
| if ln, err := net.Listen("tcp4", "127.0.0.1:0"); err == nil { |
| ln.Close() |
| ipv4Enabled = true |
| } |
| if ln, err := net.Listen("tcp6", "[::1]:0"); err == nil { |
| ln.Close() |
| ipv6Enabled = true |
| } |
| rawSocketSess = supportsRawSocket() |
| switch runtime.GOOS { |
| case "aix": |
| // Unix network isn't properly working on AIX 7.2 with |
| // Technical Level < 2. |
| out, _ := exec.Command("oslevel", "-s").Output() |
| if len(out) >= len("7200-XX-ZZ-YYMM") { // AIX 7.2, Tech Level XX, Service Pack ZZ, date YYMM |
| ver := string(out[:4]) |
| tl, _ := strconv.Atoi(string(out[5:7])) |
| unStrmDgramEnabled = ver > "7200" || (ver == "7200" && tl >= 2) |
| } |
| default: |
| unStrmDgramEnabled = true |
| } |
| } |
| |
| func unixStrmDgramEnabled() bool { |
| stackOnce.Do(probeStack) |
| return unStrmDgramEnabled |
| } |
| |
| // SupportsIPv4 reports whether the platform supports IPv4 networking |
| // functionality. |
| func SupportsIPv4() bool { |
| stackOnce.Do(probeStack) |
| return ipv4Enabled |
| } |
| |
| // SupportsIPv6 reports whether the platform supports IPv6 networking |
| // functionality. |
| func SupportsIPv6() bool { |
| stackOnce.Do(probeStack) |
| return ipv6Enabled |
| } |
| |
| // SupportsRawSocket reports whether the current session is available |
| // to use raw sockets. |
| func SupportsRawSocket() bool { |
| stackOnce.Do(probeStack) |
| return rawSocketSess |
| } |
| |
| // TestableNetwork reports whether network is testable on the current |
| // platform configuration. |
| // |
| // See func Dial of the standard library for the supported networks. |
| func TestableNetwork(network string) bool { |
| ss := strings.Split(network, ":") |
| switch ss[0] { |
| case "ip+nopriv": |
| // This is an internal network name for testing on the |
| // package net of the standard library. |
| switch runtime.GOOS { |
| case "android", "fuchsia", "hurd", "js", "nacl", "plan9", "windows": |
| return false |
| case "darwin": |
| // iOS doesn't support it. |
| if runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" { |
| return false |
| } |
| } |
| case "ip", "ip4", "ip6": |
| switch runtime.GOOS { |
| case "fuchsia", "hurd", "js", "nacl", "plan9": |
| return false |
| default: |
| if os.Getuid() != 0 { |
| return false |
| } |
| } |
| case "unix", "unixgram": |
| switch runtime.GOOS { |
| case "android", "fuchsia", "hurd", "js", "nacl", "plan9", "windows": |
| return false |
| case "aix": |
| return unixStrmDgramEnabled() |
| case "darwin": |
| // iOS does not support unix, unixgram. |
| if runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" { |
| return false |
| } |
| } |
| case "unixpacket": |
| switch runtime.GOOS { |
| case "aix", "android", "fuchsia", "hurd", "darwin", "js", "nacl", "plan9", "windows": |
| return false |
| case "netbsd": |
| // It passes on amd64 at least. 386 fails |
| // (Issue 22927). arm is unknown. |
| if runtime.GOARCH == "386" { |
| return false |
| } |
| } |
| } |
| switch ss[0] { |
| case "tcp4", "udp4", "ip4": |
| return SupportsIPv4() |
| case "tcp6", "udp6", "ip6": |
| return SupportsIPv6() |
| } |
| return true |
| } |
| |
| // TestableAddress reports whether address of network is testable on |
| // the current platform configuration. |
| func TestableAddress(network, address string) bool { |
| switch ss := strings.Split(network, ":"); ss[0] { |
| case "unix", "unixgram", "unixpacket": |
| // Abstract unix domain sockets, a Linux-ism. |
| if address[0] == '@' && runtime.GOOS != "linux" { |
| return false |
| } |
| } |
| return true |
| } |
| |
| // NewLocalListener returns a listener which listens to a loopback IP |
| // address or local file system path. |
| // |
| // The provided network must be "tcp", "tcp4", "tcp6", "unix" or |
| // "unixpacket". |
| func NewLocalListener(network string) (net.Listener, error) { |
| switch network { |
| case "tcp": |
| if SupportsIPv4() { |
| if ln, err := net.Listen("tcp4", "127.0.0.1:0"); err == nil { |
| return ln, nil |
| } |
| } |
| if SupportsIPv6() { |
| return net.Listen("tcp6", "[::1]:0") |
| } |
| case "tcp4": |
| if SupportsIPv4() { |
| return net.Listen("tcp4", "127.0.0.1:0") |
| } |
| case "tcp6": |
| if SupportsIPv6() { |
| return net.Listen("tcp6", "[::1]:0") |
| } |
| case "unix", "unixpacket": |
| path, err := LocalPath() |
| if err != nil { |
| return nil, err |
| } |
| return net.Listen(network, path) |
| } |
| return nil, fmt.Errorf("%s is not supported on %s/%s", network, runtime.GOOS, runtime.GOARCH) |
| } |
| |
| // NewLocalPacketListener returns a packet listener which listens to a |
| // loopback IP address or local file system path. |
| // |
| // The provided network must be "udp", "udp4", "udp6" or "unixgram". |
| func NewLocalPacketListener(network string) (net.PacketConn, error) { |
| switch network { |
| case "udp": |
| if SupportsIPv4() { |
| if c, err := net.ListenPacket("udp4", "127.0.0.1:0"); err == nil { |
| return c, nil |
| } |
| } |
| if SupportsIPv6() { |
| return net.ListenPacket("udp6", "[::1]:0") |
| } |
| case "udp4": |
| if SupportsIPv4() { |
| return net.ListenPacket("udp4", "127.0.0.1:0") |
| } |
| case "udp6": |
| if SupportsIPv6() { |
| return net.ListenPacket("udp6", "[::1]:0") |
| } |
| case "unixgram": |
| path, err := LocalPath() |
| if err != nil { |
| return nil, err |
| } |
| return net.ListenPacket(network, path) |
| } |
| return nil, fmt.Errorf("%s is not supported on %s/%s", network, runtime.GOOS, runtime.GOARCH) |
| } |
| |
| // LocalPath returns a local path that can be used for Unix-domain |
| // protocol testing. |
| func LocalPath() (string, error) { |
| f, err := ioutil.TempFile("", "go-nettest") |
| if err != nil { |
| return "", err |
| } |
| path := f.Name() |
| f.Close() |
| os.Remove(path) |
| return path, nil |
| } |
| |
| // MulticastSource returns a unicast IP address on ifi when ifi is an |
| // IP multicast-capable network interface. |
| // |
| // The provided network must be "ip", "ip4" or "ip6". |
| func MulticastSource(network string, ifi *net.Interface) (net.IP, error) { |
| switch network { |
| case "ip", "ip4", "ip6": |
| default: |
| return nil, errNoAvailableAddress |
| } |
| if ifi == nil || ifi.Flags&net.FlagUp == 0 || ifi.Flags&net.FlagMulticast == 0 { |
| return nil, errNoAvailableAddress |
| } |
| ip, ok := hasRoutableIP(network, ifi) |
| if !ok { |
| return nil, errNoAvailableAddress |
| } |
| return ip, nil |
| } |
| |
| // LoopbackInterface returns an available logical network interface |
| // for loopback test. |
| func LoopbackInterface() (*net.Interface, error) { |
| ift, err := net.Interfaces() |
| if err != nil { |
| return nil, errNoAvailableInterface |
| } |
| for _, ifi := range ift { |
| if ifi.Flags&net.FlagLoopback != 0 && ifi.Flags&net.FlagUp != 0 { |
| return &ifi, nil |
| } |
| } |
| return nil, errNoAvailableInterface |
| } |
| |
| // RoutedInterface returns a network interface that can route IP |
| // traffic and satisfies flags. |
| // |
| // The provided network must be "ip", "ip4" or "ip6". |
| func RoutedInterface(network string, flags net.Flags) (*net.Interface, error) { |
| switch network { |
| case "ip", "ip4", "ip6": |
| default: |
| return nil, errNoAvailableInterface |
| } |
| ift, err := net.Interfaces() |
| if err != nil { |
| return nil, errNoAvailableInterface |
| } |
| for _, ifi := range ift { |
| if ifi.Flags&flags != flags { |
| continue |
| } |
| if _, ok := hasRoutableIP(network, &ifi); !ok { |
| continue |
| } |
| return &ifi, nil |
| } |
| return nil, errNoAvailableInterface |
| } |
| |
| func hasRoutableIP(network string, ifi *net.Interface) (net.IP, bool) { |
| ifat, err := ifi.Addrs() |
| if err != nil { |
| return nil, false |
| } |
| for _, ifa := range ifat { |
| switch ifa := ifa.(type) { |
| case *net.IPAddr: |
| if ip, ok := routableIP(network, ifa.IP); ok { |
| return ip, true |
| } |
| case *net.IPNet: |
| if ip, ok := routableIP(network, ifa.IP); ok { |
| return ip, true |
| } |
| } |
| } |
| return nil, false |
| } |
| |
| func routableIP(network string, ip net.IP) (net.IP, bool) { |
| if !ip.IsLoopback() && !ip.IsLinkLocalUnicast() && !ip.IsGlobalUnicast() { |
| return nil, false |
| } |
| switch network { |
| case "ip4": |
| if ip := ip.To4(); ip != nil { |
| return ip, true |
| } |
| case "ip6": |
| if ip.IsLoopback() { // addressing scope of the loopback address depends on each implementation |
| return nil, false |
| } |
| if ip := ip.To16(); ip != nil && ip.To4() == nil { |
| return ip, true |
| } |
| default: |
| if ip := ip.To4(); ip != nil { |
| return ip, true |
| } |
| if ip := ip.To16(); ip != nil { |
| return ip, true |
| } |
| } |
| return nil, false |
| } |