github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/dnsproxy/lookup.go (about)

     1  package dnsproxy
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/miekg/dns"
    12  	"google.golang.org/grpc/codes"
    13  	"google.golang.org/grpc/status"
    14  
    15  	"github.com/datawire/dlib/dlog"
    16  	"github.com/telepresenceio/telepresence/v2/pkg/iputil"
    17  )
    18  
    19  const dnsTTL = 4
    20  
    21  const (
    22  	arpaV4 = ".in-addr.arpa."
    23  	arpaV6 = ".ip6.arpa."
    24  )
    25  
    26  type RRs []dns.RR
    27  
    28  func SupportedType(qType uint16) bool {
    29  	switch qType {
    30  	case dns.TypeA, dns.TypeAAAA, dns.TypePTR, dns.TypeCNAME, dns.TypeMX, dns.TypeNS, dns.TypeSRV, dns.TypeTXT:
    31  		return true
    32  	default:
    33  		return false
    34  	}
    35  }
    36  
    37  func writeRR(rr dns.RR, bf *strings.Builder) {
    38  	switch rr := rr.(type) {
    39  	case *dns.A:
    40  		bf.WriteString(rr.A.String())
    41  	case *dns.AAAA:
    42  		bf.WriteString(rr.AAAA.String())
    43  	case *dns.PTR:
    44  		bf.WriteString(rr.Ptr)
    45  	case *dns.CNAME:
    46  		bf.WriteString(rr.Target)
    47  	case *dns.MX:
    48  		fmt.Fprintf(bf, "%s(pref %d)", rr.Mx, rr.Preference)
    49  	case *dns.NS:
    50  		bf.WriteString(rr.Ns)
    51  	case *dns.SRV:
    52  		fmt.Fprintf(bf, "%s(port %d, prio %d, weight %d)", rr.Target, rr.Port, rr.Priority, rr.Weight)
    53  	case *dns.TXT:
    54  		bf.WriteString(strings.Join(rr.Txt, ","))
    55  	default:
    56  		bf.WriteString(rr.String())
    57  	}
    58  }
    59  
    60  func (a RRs) String() string {
    61  	if len(a) == 0 {
    62  		return "EMPTY"
    63  	}
    64  	bf := strings.Builder{}
    65  	bf.WriteByte('[')
    66  	for i, rr := range a {
    67  		if i > 0 {
    68  			bf.WriteByte(',')
    69  		}
    70  		writeRR(rr, &bf)
    71  	}
    72  	bf.WriteByte(']')
    73  	return bf.String()
    74  }
    75  
    76  func nibbleToInt(v string) (uint8, bool) {
    77  	if len(v) != 1 {
    78  		return 0, false
    79  	}
    80  	hd := v[0]
    81  	if hd >= '0' && hd <= '9' {
    82  		return hd - '0', true
    83  	}
    84  	if hd >= 'A' && hd <= 'F' {
    85  		return 10 + hd - 'A', true
    86  	}
    87  	if hd >= 'a' && hd <= 'f' {
    88  		return 10 + hd - 'a', true
    89  	}
    90  	return 0, false
    91  }
    92  
    93  func PtrAddress(addr string) (net.IP, error) {
    94  	ip := iputil.Parse(addr)
    95  	switch {
    96  	case ip != nil:
    97  		return ip, nil
    98  	case strings.HasSuffix(addr, arpaV4):
    99  		ix := addr[0 : len(addr)-len(arpaV4)]
   100  		if ip = iputil.Parse(ix); len(ip) == 4 {
   101  			return net.IP{ip[3], ip[2], ip[1], ip[0]}, nil
   102  		}
   103  		return nil, fmt.Errorf("%q is not a valid IP (v4) prefixing .in-addr.arpa", ix)
   104  	case strings.HasSuffix(addr, arpaV6):
   105  		hds := strings.Split(addr[0:len(addr)-len(arpaV6)], ".")
   106  		if len(hds) != 32 {
   107  			return nil, errors.New("expected 32 nibbles to prefix .ip6.arpa")
   108  		}
   109  		ip = make(net.IP, 16)
   110  		odd := false
   111  		for i, nb := range hds {
   112  			d, ok := nibbleToInt(nb)
   113  			if !ok {
   114  				return nil, errors.New("expected 32 nibbles to prefix .ip6.arpa")
   115  			}
   116  			b := 15 - i>>1
   117  			if odd {
   118  				ip[b] |= d << 4
   119  			} else {
   120  				ip[b] = d
   121  			}
   122  			odd = !odd
   123  		}
   124  		return ip, nil
   125  	default:
   126  		return nil, fmt.Errorf("%q is neither a valid IP-address or a valid reverse notation", addr)
   127  	}
   128  }
   129  
   130  func NewHeader(qName string, qType uint16) dns.RR_Header {
   131  	return dns.RR_Header{Name: qName, Rrtype: qType, Class: dns.ClassINET, Ttl: dnsTTL}
   132  }
   133  
   134  // useLookupName takes care of an undocumented "feature" in some lookup functions.
   135  // If the name ends with a dot, then no search path will be applied. If however,
   136  // the name doesn't end with a dot, the search path is always applied and the name
   137  // is never used verbatim.
   138  func useLookupName(qName string) (string, bool) {
   139  	dots := 0
   140  	name := qName[:len(qName)-1]
   141  	for _, c := range qName {
   142  		if c == '.' {
   143  			dots++
   144  		}
   145  	}
   146  	switch dots {
   147  	case 1:
   148  		// singleton name, it's safe to assume that a search path must be applied
   149  		return name, true
   150  	case 2, 3, 4:
   151  		// might need a search path, or might be a full name.
   152  		return name, false
   153  	default:
   154  		// With > 4 dots, we can safely assume that no search path should be applied
   155  		return qName, true
   156  	}
   157  }
   158  
   159  // TimedExternalLookup will shell out to an operating specific lookup command. The reason for this
   160  // is to make sure that no caching or a negative result is performed in this process, which would
   161  // invalidate subsequent attempts.
   162  func TimedExternalLookup(ctx context.Context, name string, timeout time.Duration) iputil.IPs {
   163  	return externalLookup(ctx, name, timeout)
   164  }
   165  
   166  func lookupIP(ctx context.Context, network, qName string, r *net.Resolver) ([]net.IP, error) {
   167  	name, final := useLookupName(qName)
   168  	ips, err := r.LookupIP(ctx, network, name)
   169  	if err != nil && !final {
   170  		dlog.Errorf(ctx, "LookupIP failed, trying LookupIP %q", qName)
   171  		ips, err = r.LookupIP(ctx, network, qName)
   172  	}
   173  	if err == nil && len(ips) == 0 {
   174  		err = &net.DNSError{
   175  			Err:        "no such host",
   176  			Name:       name,
   177  			IsNotFound: true,
   178  		}
   179  	}
   180  	return ips, err
   181  }
   182  
   183  func makeError(err error) (RRs, int, error) {
   184  	var dnsErr *net.DNSError
   185  	if errors.As(err, &dnsErr) {
   186  		switch {
   187  		case dnsErr.IsNotFound:
   188  			return nil, dns.RcodeNameError, nil
   189  		case dnsErr.IsTemporary:
   190  			return nil, dns.RcodeNameError, status.Error(codes.Unavailable, dnsErr.Error())
   191  		case dnsErr.IsTimeout:
   192  			return nil, dns.RcodeNameError, status.Error(codes.DeadlineExceeded, dnsErr.Error())
   193  		}
   194  	}
   195  	return nil, dns.RcodeServerFailure, status.Error(codes.Internal, err.Error())
   196  }
   197  
   198  func Lookup(ctx context.Context, qType uint16, qName string) (RRs, int, error) {
   199  	var answer RRs
   200  	r := &net.Resolver{StrictErrors: true}
   201  	switch qType {
   202  	case dns.TypeA, dns.TypeAAAA:
   203  		ips, err := lookupIP(ctx, "ip", qName, r)
   204  		if err != nil {
   205  			return makeError(err)
   206  		}
   207  		for _, ip := range ips {
   208  			if ip4 := ip.To4(); ip4 != nil {
   209  				if qType == dns.TypeA {
   210  					answer = append(answer, &dns.A{
   211  						Hdr: NewHeader(qName, qType),
   212  						A:   ip4,
   213  					})
   214  				}
   215  			} else if ip16 := ip.To16(); ip16 != nil && qType == dns.TypeAAAA {
   216  				answer = append(answer, &dns.AAAA{
   217  					Hdr:  NewHeader(qName, qType),
   218  					AAAA: ip16,
   219  				})
   220  			}
   221  		}
   222  	case dns.TypePTR:
   223  		var names []string
   224  		ip, err := PtrAddress(qName)
   225  		if err != nil {
   226  			return makeError(err)
   227  		}
   228  		if names, err = r.LookupAddr(ctx, ip.String()); err != nil {
   229  			return makeError(err)
   230  		}
   231  		answer = make(RRs, len(names))
   232  		for i, n := range names {
   233  			answer[i] = &dns.PTR{
   234  				Hdr: NewHeader(qName, qType),
   235  				Ptr: n,
   236  			}
   237  		}
   238  	case dns.TypeCNAME:
   239  		name, final := useLookupName(qName)
   240  		target, err := r.LookupCNAME(ctx, name)
   241  		if err != nil && !final {
   242  			target, err = r.LookupCNAME(ctx, qName)
   243  		}
   244  		if err != nil {
   245  			return makeError(err)
   246  		}
   247  		answer = RRs{&dns.CNAME{
   248  			Hdr:    NewHeader(qName, qType),
   249  			Target: target,
   250  		}}
   251  	case dns.TypeMX:
   252  		mx, err := r.LookupMX(ctx, qName[:len(qName)-1])
   253  		if err != nil {
   254  			return makeError(err)
   255  		}
   256  		answer = make(RRs, len(mx))
   257  		for i, r := range mx {
   258  			answer[i] = &dns.MX{
   259  				Hdr:        NewHeader(qName, qType),
   260  				Preference: r.Pref,
   261  				Mx:         r.Host,
   262  			}
   263  		}
   264  	case dns.TypeNS:
   265  		ns, err := r.LookupNS(ctx, qName[:len(qName)-1])
   266  		if err != nil {
   267  			return makeError(err)
   268  		}
   269  		answer = make(RRs, len(ns))
   270  		for i, n := range ns {
   271  			answer[i] = &dns.NS{
   272  				Hdr: NewHeader(qName, qType),
   273  				Ns:  n.Host,
   274  			}
   275  		}
   276  	case dns.TypeSRV:
   277  		_, srvs, err := r.LookupSRV(ctx, "", "", qName[:len(qName)-1])
   278  		if err != nil {
   279  			rrs, rCode, err := makeError(err)
   280  			if rCode != dns.RcodeNameError {
   281  				return rrs, rCode, err
   282  			}
   283  			// The LookupSRV doesn't use libc for the lookup even when told to do so, amd normal
   284  			// search-path expansion doesn't seem to apply. Let's see if the FQN is different, and
   285  			// if so, try that instead.
   286  			fqn := svcFQN(ctx, qName, r)
   287  			if fqn == "" || fqn == qName {
   288  				return rrs, rCode, err
   289  			}
   290  			var fqnErr error
   291  			if _, srvs, fqnErr = r.LookupSRV(ctx, "", "", fqn); fqnErr != nil {
   292  				// Return original error
   293  				return rrs, rCode, err
   294  			}
   295  		}
   296  		answer = make(RRs, len(srvs))
   297  		for i, s := range srvs {
   298  			answer[i] = &dns.SRV{
   299  				Hdr:      NewHeader(qName, qType),
   300  				Target:   s.Target,
   301  				Port:     s.Port,
   302  				Priority: s.Priority,
   303  				Weight:   s.Weight,
   304  			}
   305  		}
   306  	case dns.TypeTXT:
   307  		names, err := r.LookupTXT(ctx, qName)
   308  		if err != nil {
   309  			return makeError(err)
   310  		}
   311  		answer = RRs{&dns.TXT{
   312  			Hdr: NewHeader(qName, qType),
   313  			Txt: names,
   314  		}}
   315  	default:
   316  		return nil, dns.RcodeNotImplemented, status.Errorf(codes.Unimplemented, "unsupported DNS query type %s", dns.TypeToString[qType])
   317  	}
   318  	return answer, dns.RcodeSuccess, nil
   319  }
   320  
   321  func svcFQN(ctx context.Context, name string, r *net.Resolver) string {
   322  	parts := strings.Split(name, ".")
   323  	if !(len(parts) > 2 && strings.HasPrefix(parts[0], "_") && strings.HasPrefix(parts[1], "_")) {
   324  		return ""
   325  	}
   326  	svcName := strings.Join(parts[2:], ".")
   327  	ips, err := r.LookupIP(ctx, "ip", svcName[:len(svcName)-1])
   328  	if err != nil || len(ips) < 1 {
   329  		return ""
   330  	}
   331  	names, err := r.LookupAddr(ctx, ips[0].String())
   332  	if err != nil || len(names) < 1 {
   333  		return ""
   334  	}
   335  	fqn := names[0]
   336  	ix := strings.Index(fqn, svcName)
   337  	if ix < 0 {
   338  		return ""
   339  	}
   340  	fqn = parts[0] + "." + parts[1] + "." + fqn[ix:]
   341  	return fqn
   342  }