| // Copyright 2014 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 net | |
| import ( | |
| "bufio" | |
| "bytes" | |
| "fmt" | |
| "internal/testenv" | |
| "io" | |
| "os" | |
| "os/exec" | |
| "regexp" | |
| "slices" | |
| "strings" | |
| "syscall" | |
| "testing" | |
| "time" | |
| ) | |
| func toErrno(err error) (syscall.Errno, bool) { | |
| operr, ok := err.(*OpError) | |
| if !ok { | |
| return 0, false | |
| } | |
| syserr, ok := operr.Err.(*os.SyscallError) | |
| if !ok { | |
| return 0, false | |
| } | |
| errno, ok := syserr.Err.(syscall.Errno) | |
| if !ok { | |
| return 0, false | |
| } | |
| return errno, true | |
| } | |
| // TestAcceptIgnoreSomeErrors tests that windows TCPListener.AcceptTCP | |
| // handles broken connections. It verifies that broken connections do | |
| // not affect future connections. | |
| func TestAcceptIgnoreSomeErrors(t *testing.T) { | |
| recv := func(ln Listener, ignoreSomeReadErrors bool) (string, error) { | |
| c, err := ln.Accept() | |
| if err != nil { | |
| // Display windows errno in error message. | |
| errno, ok := toErrno(err) | |
| if !ok { | |
| return "", err | |
| } | |
| return "", fmt.Errorf("%v (windows errno=%d)", err, errno) | |
| } | |
| defer c.Close() | |
| b := make([]byte, 100) | |
| n, err := c.Read(b) | |
| if err == nil || err == io.EOF { | |
| return string(b[:n]), nil | |
| } | |
| errno, ok := toErrno(err) | |
| if ok && ignoreSomeReadErrors && (errno == syscall.ERROR_NETNAME_DELETED || errno == syscall.WSAECONNRESET) { | |
| return "", nil | |
| } | |
| return "", err | |
| } | |
| send := func(addr string, data string) error { | |
| c, err := Dial("tcp", addr) | |
| if err != nil { | |
| return err | |
| } | |
| defer c.Close() | |
| b := []byte(data) | |
| n, err := c.Write(b) | |
| if err != nil { | |
| return err | |
| } | |
| if n != len(b) { | |
| return fmt.Errorf(`Only %d chars of string "%s" sent`, n, data) | |
| } | |
| return nil | |
| } | |
| if envaddr := os.Getenv("GOTEST_DIAL_ADDR"); envaddr != "" { | |
| // In child process. | |
| c, err := Dial("tcp", envaddr) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| fmt.Printf("sleeping\n") | |
| time.Sleep(time.Minute) // process will be killed here | |
| c.Close() | |
| } | |
| ln, err := Listen("tcp", "127.0.0.1:0") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer ln.Close() | |
| // Start child process that connects to our listener. | |
| cmd := exec.Command(testenv.Executable(t), "-test.run=^TestAcceptIgnoreSomeErrors$") | |
| cmd.Env = append(os.Environ(), "GOTEST_DIAL_ADDR="+ln.Addr().String()) | |
| stdout, err := cmd.StdoutPipe() | |
| if err != nil { | |
| t.Fatalf("cmd.StdoutPipe failed: %v", err) | |
| } | |
| err = cmd.Start() | |
| if err != nil { | |
| t.Fatalf("cmd.Start failed: %v\n", err) | |
| } | |
| outReader := bufio.NewReader(stdout) | |
| for { | |
| s, err := outReader.ReadString('\n') | |
| if err != nil { | |
| t.Fatalf("reading stdout failed: %v", err) | |
| } | |
| if s == "sleeping\n" { | |
| break | |
| } | |
| } | |
| defer cmd.Wait() // ignore error - we know it is getting killed | |
| const alittle = 100 * time.Millisecond | |
| time.Sleep(alittle) | |
| cmd.Process.Kill() // the only way to trigger the errors | |
| time.Sleep(alittle) | |
| // Send second connection data (with delay in a separate goroutine). | |
| result := make(chan error) | |
| go func() { | |
| time.Sleep(alittle) | |
| err := send(ln.Addr().String(), "abc") | |
| if err != nil { | |
| result <- err | |
| } | |
| result <- nil | |
| }() | |
| defer func() { | |
| err := <-result | |
| if err != nil { | |
| t.Fatalf("send failed: %v", err) | |
| } | |
| }() | |
| // Receive first or second connection. | |
| s, err := recv(ln, true) | |
| if err != nil { | |
| t.Fatalf("recv failed: %v", err) | |
| } | |
| switch s { | |
| case "": | |
| // First connection data is received, let's get second connection data. | |
| case "abc": | |
| // First connection is lost forever, but that is ok. | |
| return | |
| default: | |
| t.Fatalf(`"%s" received from recv, but "" or "abc" expected`, s) | |
| } | |
| // Get second connection data. | |
| s, err = recv(ln, false) | |
| if err != nil { | |
| t.Fatalf("recv failed: %v", err) | |
| } | |
| if s != "abc" { | |
| t.Fatalf(`"%s" received from recv, but "abc" expected`, s) | |
| } | |
| } | |
| func runCmd(args ...string) ([]byte, error) { | |
| removeUTF8BOM := func(b []byte) []byte { | |
| if len(b) >= 3 && b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF { | |
| return b[3:] | |
| } | |
| return b | |
| } | |
| f, err := os.CreateTemp("", "netcmd") | |
| if err != nil { | |
| return nil, err | |
| } | |
| f.Close() | |
| defer os.Remove(f.Name()) | |
| cmd := fmt.Sprintf(`%s | Out-File "%s" -encoding UTF8`, strings.Join(args, " "), f.Name()) | |
| out, err := exec.Command("powershell", "-Command", cmd).CombinedOutput() | |
| if err != nil { | |
| if len(out) != 0 { | |
| return nil, fmt.Errorf("%s failed: %v: %q", args[0], err, string(removeUTF8BOM(out))) | |
| } | |
| var err2 error | |
| out, err2 = os.ReadFile(f.Name()) | |
| if err2 != nil { | |
| return nil, err2 | |
| } | |
| if len(out) != 0 { | |
| return nil, fmt.Errorf("%s failed: %v: %q", args[0], err, string(removeUTF8BOM(out))) | |
| } | |
| return nil, fmt.Errorf("%s failed: %v", args[0], err) | |
| } | |
| out, err = os.ReadFile(f.Name()) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return removeUTF8BOM(out), nil | |
| } | |
| func checkNetsh(t *testing.T) { | |
| if testenv.Builder() == "windows-arm64-10" { | |
| // netsh was observed to sometimes hang on this builder. | |
| // We have not observed failures on windows-arm64-11, so for the | |
| // moment we are leaving the test enabled elsewhere on the theory | |
| // that it may have been a platform bug fixed in Windows 11. | |
| testenv.SkipFlaky(t, 52082) | |
| } | |
| out, err := runCmd("netsh", "help") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if bytes.Contains(out, []byte("The following helper DLL cannot be loaded")) { | |
| t.Skipf("powershell failure:\n%s", err) | |
| } | |
| if !bytes.Contains(out, []byte("The following commands are available:")) { | |
| t.Skipf("powershell does not speak English:\n%s", out) | |
| } | |
| } | |
| func netshInterfaceIPShowInterface(ipver string, ifaces map[string]bool) error { | |
| out, err := runCmd("netsh", "interface", ipver, "show", "interface", "level=verbose") | |
| if err != nil { | |
| return err | |
| } | |
| // interface information is listed like: | |
| // | |
| //Interface Local Area Connection Parameters | |
| //---------------------------------------------- | |
| //IfLuid : ethernet_6 | |
| //IfIndex : 11 | |
| //State : connected | |
| //Metric : 10 | |
| //... | |
| var name string | |
| for line := range bytes.SplitSeq(out, []byte{'\r', '\n'}) { | |
| if bytes.HasPrefix(line, []byte("Interface ")) && bytes.HasSuffix(line, []byte(" Parameters")) { | |
| f := line[len("Interface "):] | |
| f = f[:len(f)-len(" Parameters")] | |
| name = string(f) | |
| continue | |
| } | |
| var isup bool | |
| switch string(line) { | |
| case "State : connected": | |
| isup = true | |
| case "State : disconnected": | |
| isup = false | |
| default: | |
| continue | |
| } | |
| if name != "" { | |
| if v, ok := ifaces[name]; ok && v != isup { | |
| return fmt.Errorf("%s:%s isup=%v: ipv4 and ipv6 report different interface state", ipver, name, isup) | |
| } | |
| ifaces[name] = isup | |
| name = "" | |
| } | |
| } | |
| return nil | |
| } | |
| func TestInterfacesWithNetsh(t *testing.T) { | |
| checkNetsh(t) | |
| toString := func(name string, isup bool) string { | |
| if isup { | |
| return name + ":up" | |
| } | |
| return name + ":down" | |
| } | |
| ift, err := Interfaces() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| have := make([]string, 0) | |
| for _, ifi := range ift { | |
| have = append(have, toString(ifi.Name, ifi.Flags&FlagUp != 0)) | |
| } | |
| slices.Sort(have) | |
| ifaces := make(map[string]bool) | |
| err = netshInterfaceIPShowInterface("ipv6", ifaces) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| err = netshInterfaceIPShowInterface("ipv4", ifaces) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| want := make([]string, 0) | |
| for name, isup := range ifaces { | |
| want = append(want, toString(name, isup)) | |
| } | |
| slices.Sort(want) | |
| if !slices.Equal(want, have) { | |
| t.Fatalf("unexpected interface list %q, want %q", have, want) | |
| } | |
| } | |
| func netshInterfaceIPv4ShowAddress(name string, netshOutput []byte) []string { | |
| // Address information is listed like: | |
| // | |
| //Configuration for interface "Local Area Connection" | |
| // DHCP enabled: Yes | |
| // IP Address: 10.0.0.2 | |
| // Subnet Prefix: 10.0.0.0/24 (mask 255.255.255.0) | |
| // IP Address: 10.0.0.3 | |
| // Subnet Prefix: 10.0.0.0/24 (mask 255.255.255.0) | |
| // Default Gateway: 10.0.0.254 | |
| // Gateway Metric: 0 | |
| // InterfaceMetric: 10 | |
| // | |
| //Configuration for interface "Loopback Pseudo-Interface 1" | |
| // DHCP enabled: No | |
| // IP Address: 127.0.0.1 | |
| // Subnet Prefix: 127.0.0.0/8 (mask 255.0.0.0) | |
| // InterfaceMetric: 50 | |
| // | |
| addrs := make([]string, 0) | |
| var addr, subnetprefix string | |
| var processingOurInterface bool | |
| for line := range bytes.SplitSeq(netshOutput, []byte{'\r', '\n'}) { | |
| if !processingOurInterface { | |
| if !bytes.HasPrefix(line, []byte("Configuration for interface")) { | |
| continue | |
| } | |
| if !bytes.Contains(line, []byte(`"`+name+`"`)) { | |
| continue | |
| } | |
| processingOurInterface = true | |
| continue | |
| } | |
| if len(line) == 0 { | |
| break | |
| } | |
| if bytes.Contains(line, []byte("Subnet Prefix:")) { | |
| f := bytes.Split(line, []byte{':'}) | |
| if len(f) == 2 { | |
| f = bytes.Split(f[1], []byte{'('}) | |
| if len(f) == 2 { | |
| f = bytes.Split(f[0], []byte{'/'}) | |
| if len(f) == 2 { | |
| subnetprefix = string(bytes.TrimSpace(f[1])) | |
| if addr != "" && subnetprefix != "" { | |
| addrs = append(addrs, addr+"/"+subnetprefix) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| addr = "" | |
| if bytes.Contains(line, []byte("IP Address:")) { | |
| f := bytes.Split(line, []byte{':'}) | |
| if len(f) == 2 { | |
| addr = string(bytes.TrimSpace(f[1])) | |
| } | |
| } | |
| } | |
| return addrs | |
| } | |
| func netshInterfaceIPv6ShowAddress(name string, netshOutput []byte) []string { | |
| // Address information is listed like: | |
| // | |
| //Address ::1 Parameters | |
| //--------------------------------------------------------- | |
| //Interface Luid : Loopback Pseudo-Interface 1 | |
| //Scope Id : 0.0 | |
| //Valid Lifetime : infinite | |
| //Preferred Lifetime : infinite | |
| //DAD State : Preferred | |
| //Address Type : Other | |
| //Skip as Source : false | |
| // | |
| //Address XXXX::XXXX:XXXX:XXXX:XXXX%11 Parameters | |
| //--------------------------------------------------------- | |
| //Interface Luid : Local Area Connection | |
| //Scope Id : 0.11 | |
| //Valid Lifetime : infinite | |
| //Preferred Lifetime : infinite | |
| //DAD State : Preferred | |
| //Address Type : Other | |
| //Skip as Source : false | |
| // | |
| // TODO: need to test ipv6 netmask too, but netsh does not outputs it | |
| var addr string | |
| addrs := make([]string, 0) | |
| for line := range bytes.SplitSeq(netshOutput, []byte{'\r', '\n'}) { | |
| if addr != "" { | |
| if len(line) == 0 { | |
| addr = "" | |
| continue | |
| } | |
| if string(line) != "Interface Luid : "+name { | |
| continue | |
| } | |
| addrs = append(addrs, addr) | |
| addr = "" | |
| continue | |
| } | |
| if !bytes.HasPrefix(line, []byte("Address")) { | |
| continue | |
| } | |
| if !bytes.HasSuffix(line, []byte("Parameters")) { | |
| continue | |
| } | |
| f := bytes.Split(line, []byte{' '}) | |
| if len(f) != 3 { | |
| continue | |
| } | |
| // remove scope ID if present | |
| f = bytes.Split(f[1], []byte{'%'}) | |
| // netsh can create IPv4-embedded IPv6 addresses, like fe80::5efe:192.168.140.1. | |
| // Convert these to all hexadecimal fe80::5efe:c0a8:8c01 for later string comparisons. | |
| ipv4Tail := regexp.MustCompile(`:\d+\.\d+\.\d+\.\d+$`) | |
| if ipv4Tail.Match(f[0]) { | |
| f[0] = []byte(ParseIP(string(f[0])).String()) | |
| } | |
| addr = string(bytes.ToLower(bytes.TrimSpace(f[0]))) | |
| } | |
| return addrs | |
| } | |
| func TestInterfaceAddrsWithNetsh(t *testing.T) { | |
| checkNetsh(t) | |
| outIPV4, err := runCmd("netsh", "interface", "ipv4", "show", "address") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| outIPV6, err := runCmd("netsh", "interface", "ipv6", "show", "address", "level=verbose") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| ift, err := Interfaces() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| for _, ifi := range ift { | |
| // Skip the interface if it's down. | |
| if (ifi.Flags & FlagUp) == 0 { | |
| continue | |
| } | |
| have := make([]string, 0) | |
| addrs, err := ifi.Addrs() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| for _, addr := range addrs { | |
| switch addr := addr.(type) { | |
| case *IPNet: | |
| if addr.IP.To4() != nil { | |
| have = append(have, addr.String()) | |
| } | |
| if addr.IP.To16() != nil && addr.IP.To4() == nil { | |
| // netsh does not output netmask for ipv6, so ignore ipv6 mask | |
| have = append(have, addr.IP.String()) | |
| } | |
| case *IPAddr: | |
| if addr.IP.To4() != nil { | |
| have = append(have, addr.String()) | |
| } | |
| if addr.IP.To16() != nil && addr.IP.To4() == nil { | |
| // netsh does not output netmask for ipv6, so ignore ipv6 mask | |
| have = append(have, addr.IP.String()) | |
| } | |
| } | |
| } | |
| slices.Sort(have) | |
| want := netshInterfaceIPv4ShowAddress(ifi.Name, outIPV4) | |
| wantIPv6 := netshInterfaceIPv6ShowAddress(ifi.Name, outIPV6) | |
| want = append(want, wantIPv6...) | |
| slices.Sort(want) | |
| if !slices.Equal(want, have) { | |
| t.Errorf("%s: unexpected addresses list %q, want %q", ifi.Name, have, want) | |
| } | |
| } | |
| } | |
| // check that getmac exists as a powershell command, and that it | |
| // speaks English. | |
| func checkGetmac(t *testing.T) { | |
| out, err := runCmd("getmac", "/?") | |
| if err != nil { | |
| if strings.Contains(err.Error(), "term 'getmac' is not recognized as the name of a cmdlet") { | |
| t.Skipf("getmac not available") | |
| } | |
| t.Fatal(err) | |
| } | |
| if !bytes.Contains(out, []byte("network adapters on a system")) { | |
| t.Skipf("skipping test on non-English system") | |
| } | |
| } | |
| func TestInterfaceHardwareAddrWithGetmac(t *testing.T) { | |
| checkGetmac(t) | |
| ift, err := Interfaces() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| have := make(map[string]string) | |
| for _, ifi := range ift { | |
| if ifi.Flags&FlagLoopback != 0 { | |
| // no MAC address for loopback interfaces | |
| continue | |
| } | |
| have[ifi.Name] = ifi.HardwareAddr.String() | |
| } | |
| out, err := runCmd("getmac", "/fo", "list", "/v") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // getmac output looks like: | |
| // | |
| //Connection Name: Local Area Connection | |
| //Network Adapter: Intel Gigabit Network Connection | |
| //Physical Address: XX-XX-XX-XX-XX-XX | |
| //Transport Name: \Device\Tcpip_{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX} | |
| // | |
| //Connection Name: Wireless Network Connection | |
| //Network Adapter: Wireles WLAN Card | |
| //Physical Address: XX-XX-XX-XX-XX-XX | |
| //Transport Name: Media disconnected | |
| // | |
| //Connection Name: Bluetooth Network Connection | |
| //Network Adapter: Bluetooth Device (Personal Area Network) | |
| //Physical Address: N/A | |
| //Transport Name: Hardware not present | |
| // | |
| //Connection Name: VMware Network Adapter VMnet8 | |
| //Network Adapter: VMware Virtual Ethernet Adapter for VMnet8 | |
| //Physical Address: Disabled | |
| //Transport Name: Disconnected | |
| // | |
| want := make(map[string]string) | |
| group := make(map[string]string) // name / values for single adapter | |
| getValue := func(name string) string { | |
| value, found := group[name] | |
| if !found { | |
| t.Fatalf("%q has no %q line in it", group, name) | |
| } | |
| if value == "" { | |
| t.Fatalf("%q has empty %q value", group, name) | |
| } | |
| return value | |
| } | |
| processGroup := func() { | |
| if len(group) == 0 { | |
| return | |
| } | |
| tname := strings.ToLower(getValue("Transport Name")) | |
| if tname == "n/a" { | |
| // skip these | |
| return | |
| } | |
| addr := strings.ToLower(getValue("Physical Address")) | |
| if addr == "disabled" || addr == "n/a" { | |
| // skip these | |
| return | |
| } | |
| addr = strings.ReplaceAll(addr, "-", ":") | |
| cname := getValue("Connection Name") | |
| want[cname] = addr | |
| group = make(map[string]string) | |
| } | |
| for line := range bytes.SplitSeq(out, []byte{'\r', '\n'}) { | |
| if len(line) == 0 { | |
| processGroup() | |
| continue | |
| } | |
| i := bytes.IndexByte(line, ':') | |
| if i == -1 { | |
| t.Fatalf("line %q has no : in it", line) | |
| } | |
| group[string(line[:i])] = string(bytes.TrimSpace(line[i+1:])) | |
| } | |
| processGroup() | |
| dups := make(map[string][]string) | |
| for name, addr := range want { | |
| if _, ok := dups[addr]; !ok { | |
| dups[addr] = make([]string, 0) | |
| } | |
| dups[addr] = append(dups[addr], name) | |
| } | |
| nextWant: | |
| for name, wantAddr := range want { | |
| if haveAddr, ok := have[name]; ok { | |
| if haveAddr != wantAddr { | |
| t.Errorf("unexpected MAC address for %q - %v, want %v", name, haveAddr, wantAddr) | |
| } | |
| continue | |
| } | |
| // We could not find the interface in getmac output by name. | |
| // But sometimes getmac lists many interface names | |
| // for the same MAC address. If that is the case here, | |
| // and we can match at least one of those names, | |
| // let's ignore the other names. | |
| if dupNames, ok := dups[wantAddr]; ok && len(dupNames) > 1 { | |
| for _, dupName := range dupNames { | |
| if haveAddr, ok := have[dupName]; ok && haveAddr == wantAddr { | |
| continue nextWant | |
| } | |
| } | |
| } | |
| t.Errorf("getmac lists %q, but it could not be found among Go interfaces %v", name, have) | |
| } | |
| } |