Update port, ip choice logic in DNS service (#1514)

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
This commit is contained in:
Zoltan Papp 2024-02-02 17:53:55 +01:00 committed by GitHub
parent 8604add997
commit f89332fcd2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 106 additions and 53 deletions

View File

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

View File

@ -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()

View File

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