<commit_before>package daemon

import (
	"context"
	"errors"
	"fmt"
	"math"
	"net"
	"os"
	"strconv"
	"strings"

	dns2 "github.com/miekg/dns"

	"github.com/datawire/dlib/dcontext"
	"github.com/datawire/dlib/dexec"
	"github.com/datawire/dlib/dgroup"
	"github.com/datawire/dlib/dlog"
	"github.com/telepresenceio/telepresence/v2/pkg/client/daemon/dns"
)

var errResolveDNotConfigured = errors.New("resolved not configured")

func (o *outbound) dnsServerWorker(c context.Context) error {
	if runningInDocker() {
		// Don't bother with systemd-resolved when running in a docker container
		return o.runOverridingServer(dgroup.WithGoroutineName(c, "/docker"))
	}

	err := o.tryResolveD(dgroup.WithGoroutineName(c, "/resolved"), o.router.dev)
	if err == errResolveDNotConfigured {
		dlog.Info(c, "Unable to use systemd-resolved, falling back to local server")
		err = o.runOverridingServer(dgroup.WithGoroutineName(c, "/legacy"))
	}
	return err
}

// shouldApplySearch returns true if search path should be applied
func (o *outbound) shouldApplySearch(query string) bool {
	if len(o.search) == 0 {
		return false
	}

	if query == "localhost." {
		return false
	}

	// Don't apply search paths to the kubernetes zone
	if strings.HasSuffix(query, "."+o.router.clusterDomain) {
		return false
	}

	// Don't apply search paths if one is already there
	for _, s := range o.search {
		if strings.HasSuffix(query, s) {
			return false
		}
	}

	// Don't apply search path to namespaces or "svc".
	query = query[:len(query)-1]
	if lastDot := strings.LastIndexByte(query, '.'); lastDot >= 0 {
		tld := query[lastDot+1:]
		if _, ok := o.namespaces[tld]; ok || tld == "svc" {
			return false
		}
	}
	return true
}

// resolveInSearch is only used by the overriding resolver. It is needed because unlike other resolvers, this
// resolver does not hook into a DNS system that handles search paths prior to the arrival of the request.
//
// TODO: With the DNS lookups now being done in the cluster, there's only one reason left to have a search path,
// and that's the local-only intercepts which means that using search-paths really should be limited to that
// use-case.
func (o *outbound) resolveInSearch(c context.Context, qType uint16, query string) []net.IP {
	query = strings.ToLower(query)
	query = strings.TrimSuffix(query, tel2SubDomainDot)

	if !o.shouldDoClusterLookup(query) {
		return nil
	}

	if o.shouldApplySearch(query) {
		for _, s := range o.search {
			if ips := o.resolveInCluster(c, qType, query+s); len(ips) > 0 {
				return ips
			}
		}
	}
	return o.resolveInCluster(c, qType, query)
}

func (o *outbound) runOverridingServer(c context.Context) error {
	if o.dnsConfig.LocalIp == nil {
		dat, err := os.ReadFile("/etc/resolv.conf")
		if err != nil {
			return err
		}
		for _, line := range strings.Split(string(dat), "\n") {
			if strings.HasPrefix(strings.TrimSpace(line), "nameserver") {
				fields := strings.Fields(line)
				o.dnsConfig.LocalIp = net.ParseIP(fields[1])
				dlog.Infof(c, "Automatically set -dns=%s", net.IP(o.dnsConfig.LocalIp))
				break
			}
		}
	}
	if o.dnsConfig.LocalIp == nil {
		return errors.New("couldn't determine dns ip from /etc/resolv.conf")
	}

	listeners, err := o.dnsListeners(c)
	if err != nil {
		return err
	}
	dnsResolverAddr, err := splitToUDPAddr(listeners[0].LocalAddr())
	if err != nil {
		return err
	}
	ncc := dcontext.WithoutCancel(c)
	dlog.Debugf(c, "Bootstrapping local DNS server on port %d", dnsResolverAddr.Port)

	// Create the connection later used for fallback. We need to create this before the firewall
	// rule because the rule must exclude the local address of this connection in order to
	// let it reach the original destination and not cause an endless loop.
	conn, err := dns2.Dial("udp", net.JoinHostPort(net.IP(o.dnsConfig.LocalIp).String(), "53"))
	if err != nil {
		return err
	}
	if err = routeDNS(ncc, o.dnsConfig.LocalIp, dnsResolverAddr.Port, conn.LocalAddr().(*net.UDPAddr)); err != nil {
		return err
	}
	defer func() {
		_ = conn.Close()
		unrouteDNS(ncc)
	}()
	dns.Flush(c)

	g := dgroup.NewGroup(c, dgroup.GroupConfig{})
	g.Go("Server", func(c context.Context) error {
		select {
		case <-c.Done():
			return nil
		case <-o.router.configured():
			// Server will close the listener, so no need to close it here.
			o.processSearchPaths(g, func(c context.Context, paths []string) error {
				namespaces := make(map[string]struct{})
				search := make([]string, 0)
				for _, path := range paths {
					if strings.ContainsRune(path, '.') {
						search = append(search, path)
					} else if path != "" {
						namespaces[path] = struct{}{}
					}
				}
				o.domainsLock.Lock()
				o.namespaces = namespaces
				o.search = search
				o.domainsLock.Unlock()
				return nil
			})
			v := dns.NewServer(c, listeners, conn, o.resolveInSearch)
			return v.Run(c)
		}
	})
	return g.Wait()<commit_msg>Fix synchronization around iptables nat mod for overriding DNS resolver<commit_after>package daemon

import (
	"context"
	"errors"
	"fmt"
	"math"
	"net"
	"os"
	"strconv"
	"strings"
	"time"

	dns2 "github.com/miekg/dns"

	"github.com/datawire/dlib/dexec"
	"github.com/datawire/dlib/dgroup"
	"github.com/datawire/dlib/dlog"
	"github.com/datawire/dlib/dtime"
	"github.com/telepresenceio/telepresence/v2/pkg/client/daemon/dns"
)

var errResolveDNotConfigured = errors.New("resolved not configured")

func (o *outbound) dnsServerWorker(c context.Context) error {
	if runningInDocker() {
		// Don't bother with systemd-resolved when running in a docker container
		return o.runOverridingServer(dgroup.WithGoroutineName(c, "/docker"))
	}

	err := o.tryResolveD(dgroup.WithGoroutineName(c, "/resolved"), o.router.dev)
	if err == errResolveDNotConfigured {
		dlog.Info(c, "Unable to use systemd-resolved, falling back to local server")
		err = o.runOverridingServer(dgroup.WithGoroutineName(c, "/legacy"))
	}
	return err
}

// shouldApplySearch returns true if search path should be applied
func (o *outbound) shouldApplySearch(query string) bool {
	if len(o.search) == 0 {
		return false
	}

	if query == "localhost." {
		return false
	}

	// Don't apply search paths to the kubernetes zone
	if strings.HasSuffix(query, "."+o.router.clusterDomain) {
		return false
	}

	// Don't apply search paths if one is already there
	for _, s := range o.search {
		if strings.HasSuffix(query, s) {
			return false
		}
	}

	// Don't apply search path to namespaces or "svc".
	query = query[:len(query)-1]
	if lastDot := strings.LastIndexByte(query, '.'); lastDot >= 0 {
		tld := query[lastDot+1:]
		if _, ok := o.namespaces[tld]; ok || tld == "svc" {
			return false
		}
	}
	return true
}

// resolveInSearch is only used by the overriding resolver. It is needed because unlike other resolvers, this
// resolver does not hook into a DNS system that handles search paths prior to the arrival of the request.
//
// TODO: With the DNS lookups now being done in the cluster, there's only one reason left to have a search path,
// and that's the local-only intercepts which means that using search-paths really should be limited to that
// use-case.
func (o *outbound) resolveInSearch(c context.Context, qType uint16, query string) []net.IP {
	query = strings.ToLower(query)
	query = strings.TrimSuffix(query, tel2SubDomainDot)

	if !o.shouldDoClusterLookup(query) {
		return nil
	}

	if o.shouldApplySearch(query) {
		for _, s := range o.search {
			if ips := o.resolveInCluster(c, qType, query+s); len(ips) > 0 {
				return ips
			}
		}
	}
	return o.resolveInCluster(c, qType, query)
}

func (o *outbound) runOverridingServer(c context.Context) error {
	if o.dnsConfig.LocalIp == nil {
		dat, err := os.ReadFile("/etc/resolv.conf")
		if err != nil {
			return err
		}
		for _, line := range strings.Split(string(dat), "\n") {
			if strings.HasPrefix(strings.TrimSpace(line), "nameserver") {
				fields := strings.Fields(line)
				o.dnsConfig.LocalIp = net.ParseIP(fields[1])
				dlog.Infof(c, "Automatically set -dns=%s", net.IP(o.dnsConfig.LocalIp))
				break
			}
		}
	}
	if o.dnsConfig.LocalIp == nil {
		return errors.New("couldn't determine dns ip from /etc/resolv.conf")
	}

	listeners, err := o.dnsListeners(c)
	if err != nil {
		return err
	}
	dnsResolverAddr, err := splitToUDPAddr(listeners[0].LocalAddr())
	if err != nil {
		return err
	}
	dlog.Debugf(c, "Bootstrapping local DNS server on port %d", dnsResolverAddr.Port)

	// Create the connection later used for fallback. We need to create this before the firewall
	// rule because the rule must exclude the local address of this connection in order to
	// let it reach the original destination and not cause an endless loop.
	conn, err := dns2.Dial("udp", net.JoinHostPort(net.IP(o.dnsConfig.LocalIp).String(), "53"))
	if err != nil {
		return err
	}
	defer func() {
		_ = conn.Close()
	}()

	serverStarted := make(chan struct{})
	serverDone := make(chan struct{})
	g := dgroup.NewGroup(c, dgroup.GroupConfig{})
	g.Go("Server", func(c context.Context) error {
		defer close(serverDone)
		select {
		case <-c.Done():
			return nil
		case <-o.router.configured():
			// Server will close the listener, so no need to close it here.
			o.processSearchPaths(g, func(c context.Context, paths []string) error {
				namespaces := make(map[string]struct{})
				search := make([]string, 0)
				for _, path := range paths {
					if strings.ContainsRune(path, '.') {
						search = append(search, path)
					} else if path != "" {
						namespaces[path] = struct{}{}
					}
				}
				o.domainsLock.Lock()
				o.namespaces = namespaces
				o.search = search
				o.domainsLock.Unlock()
				dns.Flush(c)
				return nil
			})
			v := dns.NewServer(c, listeners, conn, o.resolveInSearch)
			close(serverStarted)
			return v.Run(c)
		}
	})

	g.Go("NAT-redirect", func(c context.Context) error {
		select {
		case <-c.Done():
		case <-serverStarted:
			// Give DNS server time to start before rerouting NAT
			dtime.SleepWithContext(c, time.Millisecond)

			err := routeDNS(c, o.dnsConfig.LocalIp, dnsResolverAddr.Port, conn.LocalAddr().(*net.UDPAddr))
			if err != nil {
				return err
			}
			defer func() {
				c := context.Background()
				unrouteDNS(c)
				dns.Flush(c)
			}()
			dns.Flush(c)
			<-serverDone // Stay alive until DNS server is done
		}
		return nil
	})
	return g.Wait()
{'fraction_non_alphanumeric': 0.13127187864644108, 'fraction_numerical': 0.0031116297160637884, 'mean_word_length': 3.6079749103942653, 'pattern_counts': {'":': 0, '<': 10, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '110572', 'n_tokens_mistral': 3926, 'n_tokens_neox': 3559, 'n_words': 986}