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 }