diff --git a/client/internal/dns/service_listener.go b/client/internal/dns/service_listener.go index ebaabdf95..89cf4daf6 100644 --- a/client/internal/dns/service_listener.go +++ b/client/internal/dns/service_listener.go @@ -28,7 +28,7 @@ type serviceViaListener struct { customAddr *netip.AddrPort server *dns.Server listenIP string - listenPort int + listenPort uint16 listenerIsRunning bool listenerFlagLock sync.Mutex ebpfService ebpfMgr.Manager @@ -66,15 +66,6 @@ func (s *serviceViaListener) Listen() error { return fmt.Errorf("eval listen address: %w", err) } s.server.Addr = fmt.Sprintf("%s:%d", s.listenIP, s.listenPort) - - if s.shouldApplyPortFwd() { - s.ebpfService = ebpf.GetEbpfManagerInstance() - err = s.ebpfService.LoadDNSFwd(s.listenIP, s.listenPort) - if err != nil { - log.Warnf("failed to load DNS port forwarder, custom port may not work well on some Linux operating systems: %s", err) - s.ebpfService = nil - } - } log.Debugf("starting dns on %s", s.server.Addr) go func() { s.setListenerStatus(true) @@ -128,7 +119,7 @@ func (s *serviceViaListener) RuntimePort() int { if s.ebpfService != nil { return defaultPort } else { - return s.listenPort + return int(s.listenPort) } } @@ -140,55 +131,112 @@ func (s *serviceViaListener) setListenerStatus(running bool) { s.listenerIsRunning = running } -func (s *serviceViaListener) getFirstDNSListenerAvailable() (string, int, error) { - ips := []string{defaultIP, customIP} +// evalListenAddress figure out the listen address for the DNS server +// first check the 53 port availability on WG interface or lo, if not success +// pick a random port on WG interface for eBPF, if not success +// check the 5053 port availability on WG interface or lo without eBPF usage, +func (s *serviceViaListener) evalListenAddress() (string, uint16, error) { + if s.customAddr != nil { + return s.customAddr.Addr().String(), s.customAddr.Port(), nil + } + + ip, ok := s.testFreePort(defaultPort) + if ok { + return ip, defaultPort, nil + } + + ebpfSrv, port, ok := s.tryToUseeBPF() + if ok { + s.ebpfService = ebpfSrv + return s.wgInterface.Address().IP.String(), port, nil + } + + ip, ok = s.testFreePort(customPort) + if ok { + return ip, customPort, nil + } + + return "", 0, fmt.Errorf("failed to find a free port for DNS server") +} + +func (s *serviceViaListener) testFreePort(port int) (string, bool) { + var ips []string if runtime.GOOS != "darwin" { - ips = append([]string{s.wgInterface.Address().IP.String()}, ips...) + ips = []string{s.wgInterface.Address().IP.String(), defaultIP, customIP} + } else { + ips = []string{defaultIP, customIP} } - ports := []int{defaultPort, customPort} - for _, port := range ports { - for _, ip := range ips { - addrString := fmt.Sprintf("%s:%d", ip, port) - udpAddr := net.UDPAddrFromAddrPort(netip.MustParseAddrPort(addrString)) - probeListener, err := net.ListenUDP("udp", udpAddr) - if err == nil { - err = probeListener.Close() - if err != nil { - log.Errorf("got an error closing the probe listener, error: %s", err) - } - log.Infof("successfully found dns binding candidate at %s", addrString) - return ip, port, nil - } - log.Debugf("binding dns on %s is not available, error: %s", addrString, err) + + for _, ip := range ips { + if !s.tryToBind(ip, port) { + continue } + + return ip, true } - return "", 0, fmt.Errorf("unable to find an unused ip and port combination. IPs tested: %v and ports %v", ips, ports) + return "", false } -func (s *serviceViaListener) evalListenAddress() (string, int, error) { - if s.customAddr != nil { - return s.customAddr.Addr().String(), int(s.customAddr.Port()), nil - } - - return s.getFirstDNSListenerAvailable() -} - -// shouldApplyPortFwd decides whether to apply eBPF program to capture DNS traffic on port 53. -// This is needed because on some operating systems if we start a DNS server not on a default port 53, the domain name -// resolution won't work. -// So, in case we are running on Linux and picked a non-default port (53) we should fall back to the eBPF solution that will capture -// traffic on port 53 and forward it to a local DNS server running on 5053. -func (s *serviceViaListener) shouldApplyPortFwd() bool { - if runtime.GOOS != "linux" { +func (s *serviceViaListener) tryToBind(ip string, port int) bool { + addrString := fmt.Sprintf("%s:%d", ip, port) + udpAddr := net.UDPAddrFromAddrPort(netip.MustParseAddrPort(addrString)) + probeListener, err := net.ListenUDP("udp", udpAddr) + if err != nil { + log.Warnf("binding dns on %s is not available, error: %s", addrString, err) return false } - if s.customAddr != nil { - return false - } - - if s.listenPort == defaultPort { - return false + err = probeListener.Close() + if err != nil { + log.Errorf("got an error closing the probe listener, error: %s", err) } return true } + +// tryToUseeBPF decides whether to apply eBPF program to capture DNS traffic on port 53. +// This is needed because on some operating systems if we start a DNS server not on a default port 53, +// the domain name resolution won't work. So, in case we are running on Linux and picked a free +// port we should fall back to the eBPF solution that will capture traffic on port 53 and forward +// it to a local DNS server running on the chosen port. +func (s *serviceViaListener) tryToUseeBPF() (ebpfMgr.Manager, uint16, bool) { + if runtime.GOOS != "linux" { + return nil, 0, false + } + + port, err := s.generateFreePort() //nolint:staticcheck,unused + if err != nil { + log.Warnf("failed to generate a free port for eBPF DNS forwarder server: %s", err) + return nil, 0, false + } + + ebpfSrv := ebpf.GetEbpfManagerInstance() + err = ebpfSrv.LoadDNSFwd(s.wgInterface.Address().IP.String(), int(port)) + if err != nil { + log.Warnf("failed to load DNS forwarder eBPF program, error: %s", err) + return nil, 0, false + } + + return ebpfSrv, port, true +} + +func (s *serviceViaListener) generateFreePort() (uint16, error) { + ok := s.tryToBind(s.wgInterface.Address().IP.String(), customPort) + if ok { + return customPort, nil + } + + udpAddr := net.UDPAddrFromAddrPort(netip.MustParseAddrPort("0.0.0.0:0")) + probeListener, err := net.ListenUDP("udp", udpAddr) + if err != nil { + log.Debugf("failed to bind random port for DNS: %s", err) + return 0, err + } + + addrPort := netip.MustParseAddrPort(probeListener.LocalAddr().String()) // might panic if address is incorrect + err = probeListener.Close() + if err != nil { + log.Debugf("failed to free up DNS port: %s", err) + return 0, err + } + return addrPort.Port(), nil +} diff --git a/client/internal/ebpf/ebpf/dns_fwd_linux.go b/client/internal/ebpf/ebpf/dns_fwd_linux.go index 1b6493692..93797da76 100644 --- a/client/internal/ebpf/ebpf/dns_fwd_linux.go +++ b/client/internal/ebpf/ebpf/dns_fwd_linux.go @@ -13,7 +13,7 @@ const ( ) func (tf *GeneralManager) LoadDNSFwd(ip string, dnsPort int) error { - log.Debugf("load ebpf DNS forwarder: address: %s:%d", ip, dnsPort) + log.Debugf("load eBPF DNS forwarder, watching addr: %s:53, redirect to port: %d", ip, dnsPort) tf.lock.Lock() defer tf.lock.Unlock() diff --git a/client/internal/ebpf/ebpf/src/readme.md b/client/internal/ebpf/ebpf/src/readme.md index f0582ea32..0ab393dd4 100644 --- a/client/internal/ebpf/ebpf/src/readme.md +++ b/client/internal/ebpf/ebpf/src/readme.md @@ -1,5 +1,10 @@ -# Debug +# DNS forwarder +The agent attach the XDP program to the lo device. We can not use fake address in eBPF because the +traffic does not appear in the eBPF program. The program capture the traffic on wg_ip:53 and +overwrite in it the destination port to 5053. + +# Debug The CONFIG_BPF_EVENTS kernel module is required for bpf_printk. Apply this code to use bpf_printk