github.com/Uhtred009/v2ray-core-1@v4.31.2+incompatible/app/dns/dohdns.go (about) 1 // +build !confonly 2 3 package dns 4 5 import ( 6 "bytes" 7 "context" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "net/http" 12 "net/url" 13 "sync" 14 "sync/atomic" 15 "time" 16 17 "golang.org/x/net/dns/dnsmessage" 18 "v2ray.com/core/common" 19 "v2ray.com/core/common/net" 20 "v2ray.com/core/common/protocol/dns" 21 "v2ray.com/core/common/session" 22 "v2ray.com/core/common/signal/pubsub" 23 "v2ray.com/core/common/task" 24 dns_feature "v2ray.com/core/features/dns" 25 "v2ray.com/core/features/routing" 26 "v2ray.com/core/transport/internet" 27 ) 28 29 // DoHNameServer implemented DNS over HTTPS (RFC8484) Wire Format, 30 // which is compatible with traditional dns over udp(RFC1035), 31 // thus most of the DOH implementation is copied from udpns.go 32 type DoHNameServer struct { 33 sync.RWMutex 34 ips map[string]record 35 pub *pubsub.Service 36 cleanup *task.Periodic 37 reqID uint32 38 clientIP net.IP 39 httpClient *http.Client 40 dohURL string 41 name string 42 } 43 44 // NewDoHNameServer creates DOH client object for remote resolving 45 func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, clientIP net.IP) (*DoHNameServer, error) { 46 47 newError("DNS: created Remote DOH client for ", url.String()).AtInfo().WriteToLog() 48 s := baseDOHNameServer(url, "DOH", clientIP) 49 50 // Dispatched connection will be closed (interrupted) after each request 51 // This makes DOH inefficient without a keep-alived connection 52 // See: core/app/proxyman/outbound/handler.go:113 53 // Using mux (https request wrapped in a stream layer) improves the situation. 54 // Recommend to use NewDoHLocalNameServer (DOHL:) if v2ray instance is running on 55 // a normal network eg. the server side of v2ray 56 tr := &http.Transport{ 57 MaxIdleConns: 30, 58 IdleConnTimeout: 90 * time.Second, 59 TLSHandshakeTimeout: 30 * time.Second, 60 ForceAttemptHTTP2: true, 61 DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 62 dest, err := net.ParseDestination(network + ":" + addr) 63 if err != nil { 64 return nil, err 65 } 66 67 link, err := dispatcher.Dispatch(ctx, dest) 68 if err != nil { 69 return nil, err 70 } 71 return net.NewConnection( 72 net.ConnectionInputMulti(link.Writer), 73 net.ConnectionOutputMulti(link.Reader), 74 ), nil 75 }, 76 } 77 78 dispatchedClient := &http.Client{ 79 Transport: tr, 80 Timeout: 60 * time.Second, 81 } 82 83 s.httpClient = dispatchedClient 84 return s, nil 85 } 86 87 // NewDoHLocalNameServer creates DOH client object for local resolving 88 func NewDoHLocalNameServer(url *url.URL, clientIP net.IP) *DoHNameServer { 89 url.Scheme = "https" 90 s := baseDOHNameServer(url, "DOHL", clientIP) 91 tr := &http.Transport{ 92 IdleConnTimeout: 90 * time.Second, 93 ForceAttemptHTTP2: true, 94 DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 95 dest, err := net.ParseDestination(network + ":" + addr) 96 if err != nil { 97 return nil, err 98 } 99 conn, err := internet.DialSystem(ctx, dest, nil) 100 if err != nil { 101 return nil, err 102 } 103 return conn, nil 104 }, 105 } 106 s.httpClient = &http.Client{ 107 Timeout: time.Second * 180, 108 Transport: tr, 109 } 110 newError("DNS: created Local DOH client for ", url.String()).AtInfo().WriteToLog() 111 return s 112 } 113 114 func baseDOHNameServer(url *url.URL, prefix string, clientIP net.IP) *DoHNameServer { 115 116 s := &DoHNameServer{ 117 ips: make(map[string]record), 118 clientIP: clientIP, 119 pub: pubsub.NewService(), 120 name: prefix + "//" + url.Host, 121 dohURL: url.String(), 122 } 123 s.cleanup = &task.Periodic{ 124 Interval: time.Minute, 125 Execute: s.Cleanup, 126 } 127 128 return s 129 } 130 131 // Name returns client name 132 func (s *DoHNameServer) Name() string { 133 return s.name 134 } 135 136 // Cleanup clears expired items from cache 137 func (s *DoHNameServer) Cleanup() error { 138 now := time.Now() 139 s.Lock() 140 defer s.Unlock() 141 142 if len(s.ips) == 0 { 143 return newError("nothing to do. stopping...") 144 } 145 146 for domain, record := range s.ips { 147 if record.A != nil && record.A.Expire.Before(now) { 148 record.A = nil 149 } 150 if record.AAAA != nil && record.AAAA.Expire.Before(now) { 151 record.AAAA = nil 152 } 153 154 if record.A == nil && record.AAAA == nil { 155 newError(s.name, " cleanup ", domain).AtDebug().WriteToLog() 156 delete(s.ips, domain) 157 } else { 158 s.ips[domain] = record 159 } 160 } 161 162 if len(s.ips) == 0 { 163 s.ips = make(map[string]record) 164 } 165 166 return nil 167 } 168 169 func (s *DoHNameServer) updateIP(req *dnsRequest, ipRec *IPRecord) { 170 elapsed := time.Since(req.start) 171 172 s.Lock() 173 rec := s.ips[req.domain] 174 updated := false 175 176 switch req.reqType { 177 case dnsmessage.TypeA: 178 if isNewer(rec.A, ipRec) { 179 rec.A = ipRec 180 updated = true 181 } 182 case dnsmessage.TypeAAAA: 183 addr := make([]net.Address, 0) 184 for _, ip := range ipRec.IP { 185 if len(ip.IP()) == net.IPv6len { 186 addr = append(addr, ip) 187 } 188 } 189 ipRec.IP = addr 190 if isNewer(rec.AAAA, ipRec) { 191 rec.AAAA = ipRec 192 updated = true 193 } 194 } 195 newError(s.name, " got answer: ", req.domain, " ", req.reqType, " -> ", ipRec.IP, " ", elapsed).AtInfo().WriteToLog() 196 197 if updated { 198 s.ips[req.domain] = rec 199 } 200 switch req.reqType { 201 case dnsmessage.TypeA: 202 s.pub.Publish(req.domain+"4", nil) 203 case dnsmessage.TypeAAAA: 204 s.pub.Publish(req.domain+"6", nil) 205 } 206 s.Unlock() 207 common.Must(s.cleanup.Start()) 208 } 209 210 func (s *DoHNameServer) newReqID() uint16 { 211 return uint16(atomic.AddUint32(&s.reqID, 1)) 212 } 213 214 func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, option IPOption) { 215 newError(s.name, " querying: ", domain).AtInfo().WriteToLog(session.ExportIDToError(ctx)) 216 217 reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(s.clientIP)) 218 219 var deadline time.Time 220 if d, ok := ctx.Deadline(); ok { 221 deadline = d 222 } else { 223 deadline = time.Now().Add(time.Second * 5) 224 } 225 226 for _, req := range reqs { 227 go func(r *dnsRequest) { 228 // generate new context for each req, using same context 229 // may cause reqs all aborted if any one encounter an error 230 dnsCtx := context.Background() 231 232 // reserve internal dns server requested Inbound 233 if inbound := session.InboundFromContext(ctx); inbound != nil { 234 dnsCtx = session.ContextWithInbound(dnsCtx, inbound) 235 } 236 237 dnsCtx = session.ContextWithContent(dnsCtx, &session.Content{ 238 Protocol: "https", 239 SkipRoutePick: true, 240 }) 241 242 // forced to use mux for DOH 243 dnsCtx = session.ContextWithMuxPrefered(dnsCtx, true) 244 245 var cancel context.CancelFunc 246 dnsCtx, cancel = context.WithDeadline(dnsCtx, deadline) 247 defer cancel() 248 249 b, err := dns.PackMessage(r.msg) 250 if err != nil { 251 newError("failed to pack dns query").Base(err).AtError().WriteToLog() 252 return 253 } 254 resp, err := s.dohHTTPSContext(dnsCtx, b.Bytes()) 255 if err != nil { 256 newError("failed to retrieve response").Base(err).AtError().WriteToLog() 257 return 258 } 259 rec, err := parseResponse(resp) 260 if err != nil { 261 newError("failed to handle DOH response").Base(err).AtError().WriteToLog() 262 return 263 } 264 s.updateIP(r, rec) 265 }(req) 266 } 267 } 268 269 func (s *DoHNameServer) dohHTTPSContext(ctx context.Context, b []byte) ([]byte, error) { 270 body := bytes.NewBuffer(b) 271 req, err := http.NewRequest("POST", s.dohURL, body) 272 if err != nil { 273 return nil, err 274 } 275 276 req.Header.Add("Accept", "application/dns-message") 277 req.Header.Add("Content-Type", "application/dns-message") 278 279 resp, err := s.httpClient.Do(req.WithContext(ctx)) 280 if err != nil { 281 return nil, err 282 } 283 284 defer resp.Body.Close() 285 if resp.StatusCode != http.StatusOK { 286 io.Copy(ioutil.Discard, resp.Body) // flush resp.Body so that the conn is reusable 287 return nil, fmt.Errorf("DOH server returned code %d", resp.StatusCode) 288 } 289 290 return ioutil.ReadAll(resp.Body) 291 } 292 293 func (s *DoHNameServer) findIPsForDomain(domain string, option IPOption) ([]net.IP, error) { 294 s.RLock() 295 record, found := s.ips[domain] 296 s.RUnlock() 297 298 if !found { 299 return nil, errRecordNotFound 300 } 301 302 var ips []net.Address 303 var lastErr error 304 if option.IPv6Enable && record.AAAA != nil && record.AAAA.RCode == dnsmessage.RCodeSuccess { 305 aaaa, err := record.AAAA.getIPs() 306 if err != nil { 307 lastErr = err 308 } 309 ips = append(ips, aaaa...) 310 } 311 312 if option.IPv4Enable && record.A != nil && record.A.RCode == dnsmessage.RCodeSuccess { 313 a, err := record.A.getIPs() 314 if err != nil { 315 lastErr = err 316 } 317 ips = append(ips, a...) 318 } 319 320 if len(ips) > 0 { 321 return toNetIP(ips), nil 322 } 323 324 if lastErr != nil { 325 return nil, lastErr 326 } 327 328 if (option.IPv4Enable && record.A != nil) || (option.IPv6Enable && record.AAAA != nil) { 329 return nil, dns_feature.ErrEmptyResponse 330 } 331 332 return nil, errRecordNotFound 333 } 334 335 // QueryIP is called from dns.Server->queryIPTimeout 336 func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error) { 337 fqdn := Fqdn(domain) 338 339 ips, err := s.findIPsForDomain(fqdn, option) 340 if err != errRecordNotFound { 341 newError(s.name, " cache HIT ", domain, " -> ", ips).Base(err).AtDebug().WriteToLog() 342 return ips, err 343 } 344 345 // ipv4 and ipv6 belong to different subscription groups 346 var sub4, sub6 *pubsub.Subscriber 347 if option.IPv4Enable { 348 sub4 = s.pub.Subscribe(fqdn + "4") 349 defer sub4.Close() 350 } 351 if option.IPv6Enable { 352 sub6 = s.pub.Subscribe(fqdn + "6") 353 defer sub6.Close() 354 } 355 done := make(chan interface{}) 356 go func() { 357 if sub4 != nil { 358 select { 359 case <-sub4.Wait(): 360 case <-ctx.Done(): 361 } 362 } 363 if sub6 != nil { 364 select { 365 case <-sub6.Wait(): 366 case <-ctx.Done(): 367 } 368 } 369 close(done) 370 }() 371 s.sendQuery(ctx, fqdn, option) 372 373 for { 374 ips, err := s.findIPsForDomain(fqdn, option) 375 if err != errRecordNotFound { 376 return ips, err 377 } 378 379 select { 380 case <-ctx.Done(): 381 return nil, ctx.Err() 382 case <-done: 383 } 384 } 385 }