//go:build windows // +build windows package aghnet import ( "bufio" "fmt" "io" "net" "os/exec" "strings" "sync" "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" ) // systemResolvers implementation differs for Windows since Go's resolver // doesn't work there. // // See https://github.com/golang/go/issues/33097. type systemResolvers struct { // addrs is the slice of cached local resolvers' addresses. addrs []string addrsLock sync.RWMutex } func newSystemResolvers(_ HostGenFunc) (sr SystemResolvers) { return &systemResolvers{} } func (sr *systemResolvers) Get() (rs []string) { sr.addrsLock.RLock() defer sr.addrsLock.RUnlock() addrs := sr.addrs rs = make([]string, len(addrs)) copy(rs, addrs) return rs } // writeExit writes "exit" to w and closes it. It is supposed to be run in // a goroutine. func writeExit(w io.WriteCloser) { defer log.OnPanic("systemResolvers: writeExit") defer func() { derr := w.Close() if derr != nil { log.Error("systemResolvers: writeExit: closing: %s", derr) } }() _, err := io.WriteString(w, "exit") if err != nil { log.Error("systemResolvers: writeExit: writing: %s", err) } } // scanAddrs scans the DNS addresses from nslookup's output. The expected // output of nslookup looks like this: // // Default Server: 192-168-1-1.qualified.domain.ru // Address: 192.168.1.1 func scanAddrs(s *bufio.Scanner) (addrs []string) { for s.Scan() { line := strings.TrimSpace(s.Text()) fields := strings.Fields(line) if len(fields) != 2 || fields[0] != "Address:" { continue } // If the address contains port then it is separated with '#'. ipPort := strings.Split(fields[1], "#") if len(ipPort) == 0 { continue } addr := ipPort[0] if net.ParseIP(addr) == nil { log.Debug("systemResolvers: %q is not a valid ip", addr) continue } addrs = append(addrs, addr) } return addrs } // getAddrs gets local resolvers' addresses from OS in a special Windows way. // // TODO(e.burkov): This whole function needs more detailed research on getting // local resolvers addresses on Windows. We execute the external command for // now that is not the most accurate way. func (sr *systemResolvers) getAddrs() (addrs []string, err error) { var cmdPath string cmdPath, err = exec.LookPath("nslookup.exe") if err != nil { return nil, fmt.Errorf("looking up cmd path: %w", err) } cmd := exec.Command(cmdPath) var stdin io.WriteCloser stdin, err = cmd.StdinPipe() if err != nil { return nil, fmt.Errorf("getting the command's stdin pipe: %w", err) } var stdout io.ReadCloser stdout, err = cmd.StdoutPipe() if err != nil { return nil, fmt.Errorf("getting the command's stdout pipe: %w", err) } go writeExit(stdin) err = cmd.Start() if err != nil { return nil, fmt.Errorf("start command executing: %w", err) } s := bufio.NewScanner(stdout) addrs = scanAddrs(s) err = cmd.Wait() if err != nil { return nil, fmt.Errorf("executing the command: %w", err) } err = s.Err() if err != nil { return nil, fmt.Errorf("scanning output: %w", err) } // Don't close StdoutPipe since Wait do it for us in ¿most? cases. // // See go doc os/exec.Cmd.StdoutPipe. return addrs, nil } func (sr *systemResolvers) refresh() (err error) { defer func() { err = errors.Annotate(err, "systemResolvers: %w") }() got, err := sr.getAddrs() if err != nil { return fmt.Errorf("can't get addresses: %w", err) } if len(got) == 0 { return nil } sr.addrsLock.Lock() defer sr.addrsLock.Unlock() sr.addrs = got return nil }