2021-12-03 16:33:05 +00:00
|
|
|
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
package tsdial
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"time"
|
2021-12-06 22:41:30 +00:00
|
|
|
|
|
|
|
"tailscale.com/net/dnscache"
|
2021-12-03 16:33:05 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// dohConn is a net.PacketConn suitable for returning from
|
|
|
|
// net.Dialer.Dial to send DNS queries over PeerAPI to exit nodes'
|
|
|
|
// ExitDNS DoH proxy service.
|
|
|
|
type dohConn struct {
|
2021-12-06 22:41:30 +00:00
|
|
|
ctx context.Context
|
|
|
|
baseURL string
|
|
|
|
hc *http.Client // if nil, default is used
|
|
|
|
dnsCache *dnscache.MessageCache
|
2021-12-03 16:33:05 +00:00
|
|
|
|
|
|
|
rbuf bytes.Buffer
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
_ net.Conn = (*dohConn)(nil)
|
|
|
|
_ net.PacketConn = (*dohConn)(nil) // be a PacketConn to change net.Resolver semantics
|
|
|
|
)
|
|
|
|
|
|
|
|
func (*dohConn) Close() error { return nil }
|
|
|
|
func (*dohConn) LocalAddr() net.Addr { return todoAddr{} }
|
|
|
|
func (*dohConn) RemoteAddr() net.Addr { return todoAddr{} }
|
|
|
|
func (*dohConn) SetDeadline(t time.Time) error { return nil }
|
|
|
|
func (*dohConn) SetReadDeadline(t time.Time) error { return nil }
|
|
|
|
func (*dohConn) SetWriteDeadline(t time.Time) error { return nil }
|
|
|
|
|
|
|
|
func (c *dohConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
|
|
|
return c.Write(p)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *dohConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
|
|
|
n, err = c.Read(p)
|
|
|
|
return n, todoAddr{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *dohConn) Read(p []byte) (n int, err error) {
|
|
|
|
return c.rbuf.Read(p)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *dohConn) Write(packet []byte) (n int, err error) {
|
2021-12-06 22:41:30 +00:00
|
|
|
if c.dnsCache != nil {
|
|
|
|
err := c.dnsCache.ReplyFromCache(&c.rbuf, packet)
|
|
|
|
if err == nil {
|
|
|
|
// Cache hit.
|
|
|
|
// TODO(bradfitz): add clientmetric
|
|
|
|
return len(packet), nil
|
|
|
|
}
|
|
|
|
c.rbuf.Reset()
|
|
|
|
}
|
2021-12-03 16:33:05 +00:00
|
|
|
req, err := http.NewRequestWithContext(c.ctx, "POST", c.baseURL, bytes.NewReader(packet))
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
const dohType = "application/dns-message"
|
|
|
|
req.Header.Set("Content-Type", dohType)
|
|
|
|
hc := c.hc
|
|
|
|
if hc == nil {
|
|
|
|
hc = http.DefaultClient
|
|
|
|
}
|
|
|
|
hres, err := hc.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
defer hres.Body.Close()
|
|
|
|
if hres.StatusCode != 200 {
|
|
|
|
return 0, errors.New(hres.Status)
|
|
|
|
}
|
|
|
|
if ct := hres.Header.Get("Content-Type"); ct != dohType {
|
|
|
|
return 0, fmt.Errorf("unexpected response Content-Type %q", ct)
|
|
|
|
}
|
|
|
|
_, err = io.Copy(&c.rbuf, hres.Body)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
2021-12-06 22:41:30 +00:00
|
|
|
if c.dnsCache != nil {
|
|
|
|
c.dnsCache.AddCacheEntry(packet, c.rbuf.Bytes())
|
|
|
|
}
|
2021-12-03 16:33:05 +00:00
|
|
|
return len(packet), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type todoAddr struct{}
|
|
|
|
|
|
|
|
func (todoAddr) Network() string { return "unused" }
|
|
|
|
func (todoAddr) String() string { return "unused-todoAddr" }
|