Skip to content

Commit

Permalink
Update port, ip choice logic in DNS service (#1514)
Browse files Browse the repository at this point in the history
Ensure we use WG address instead of loopback addresses for eBPF.
- First try to use 53 port
- Try to use 5053 port on WG interface for eBPF
- Try to use 5053 on WG interface or loopback interface
  • Loading branch information
pappz authored Feb 2, 2024
1 parent 8604add commit f89332f
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 49 deletions.
142 changes: 95 additions & 47 deletions client/internal/dns/service_listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -128,7 +119,7 @@ func (s *serviceViaListener) RuntimePort() int {
if s.ebpfService != nil {
return defaultPort
} else {
return s.listenPort
return int(s.listenPort)
}
}

Expand All @@ -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...)
}
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)
ips = []string{s.wgInterface.Address().IP.String(), defaultIP, customIP}
} else {
ips = []string{defaultIP, customIP}
}

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
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
}

return s.getFirstDNSListenerAvailable()
err = probeListener.Close()
if err != nil {
log.Errorf("got an error closing the probe listener, error: %s", err)
}
return true
}

// 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 {
// 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 false
return nil, 0, false
}

if s.customAddr != nil {
return 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
}

if s.listenPort == defaultPort {
return 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 true

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
}
2 changes: 1 addition & 1 deletion client/internal/ebpf/ebpf/dns_fwd_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
7 changes: 6 additions & 1 deletion client/internal/ebpf/ebpf/src/readme.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down

0 comments on commit f89332f

Please sign in to comment.