From a37bcc4f89fbcd4db980da2cd12fa473dd16af93 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sat, 7 May 2022 15:04:21 -0700 Subject: [PATCH] net/dns: add MagicDNS DNS-over-TLS support For Android Private DNS in "Automatic" (opportunistic) mdoe. Tested with: $ sudo apt-get install knot-dnsutils $ kdig @100.100.100.100 +tls google.com Updates #915 Change-Id: I2d59e2d6698f93384b8b3b833b2a3375145ef5ce Signed-off-by: Brad Fitzpatrick --- net/dns/manager.go | 99 ++++++++++++++++++++++++++++++++--- wgengine/netstack/netstack.go | 19 +++++-- 2 files changed, 107 insertions(+), 11 deletions(-) diff --git a/net/dns/manager.go b/net/dns/manager.go index 59bcdcf17..f7559cc50 100644 --- a/net/dns/manager.go +++ b/net/dns/manager.go @@ -6,12 +6,22 @@ package dns import ( "bufio" + "bytes" "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" "encoding/binary" + "encoding/pem" "errors" "io" + "math/big" "net" "runtime" + "sync" "sync/atomic" "time" @@ -83,6 +93,11 @@ type Manager struct { responses chan response activeQueriesAtomic int32 + // DNS-over-TLS cached value. + dotCertMu sync.Mutex + dotCertLast time.Time + dotCertVal tls.Certificate + ctx context.Context // good until Down ctxCancel context.CancelFunc // closes ctx @@ -367,8 +382,8 @@ type dnsTCPSession struct { conn net.Conn srcAddr netaddr.IPPort - readClosing chan struct{} - responses chan []byte // DNS replies pending writing + readClosing chan struct{} + responses chan []byte // DNS replies pending writing ctx context.Context closeCtx context.CancelFunc @@ -454,17 +469,87 @@ func (s *dnsTCPSession) handleReads() { // servicing DNS requests sent down it. func (m *Manager) HandleTCPConn(conn net.Conn, srcAddr netaddr.IPPort) { s := dnsTCPSession{ - m: m, - conn: conn, - srcAddr: srcAddr, - responses: make(chan []byte), - readClosing: make(chan struct{}), + m: m, + conn: conn, + srcAddr: srcAddr, + responses: make(chan []byte), + readClosing: make(chan struct{}), } s.ctx, s.closeCtx = context.WithCancel(context.Background()) go s.handleReads() s.handleWrites() } +const dotCertValidity = time.Hour * 24 * 30 // arbitrary; LetsEncrypt-ish + +func (m *Manager) dotCert() (tls.Certificate, error) { + m.dotCertMu.Lock() + defer m.dotCertMu.Unlock() + + if !m.dotCertLast.IsZero() && time.Since(m.dotCertLast) < dotCertValidity { + return m.dotCertVal, nil + } + + cert, err := genSelfSignedDoTCert() + if err == nil { + m.dotCertVal = cert + m.dotCertLast = time.Now() + } + return cert, err +} + +// genSelfSignedDoTCert generates a self-signed certificate for DNS-over-TLS +// (DoT) queries on 100.100.100.100. +// +// This exists for Android Private DNS, which in "Automatic" (aka opportunistic) +// mode doesn't verify certs. +// +// See https://github.com/tailscale/tailscale/issues/915. +func genSelfSignedDoTCert() (tls.Certificate, error) { + privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return tls.Certificate{}, err + } + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"Tailscale MagicDNS"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(dotCertValidity), + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) + if err != nil { + return tls.Certificate{}, err + } + privKeyBytes, _ := x509.MarshalECPrivateKey(privKey) + pemCert := new(bytes.Buffer) + pemKey := new(bytes.Buffer) + pem.Encode(pemCert, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + pem.Encode(pemKey, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privKeyBytes}) + return tls.X509KeyPair(pemCert.Bytes(), pemKey.Bytes()) +} + +// HandleDNSoverTLSConn implements magicDNS over DNS-over-TLS, taking a +// connection and servicing DNS requests sent down it. +// +// It uses a self-signed cert; see genSelfSignedDoTCert for backbground. +func (m *Manager) HandleDNSoverTLSConn(conn net.Conn, srcAddr netaddr.IPPort) { + tlsCert, err := m.dotCert() + if err != nil { + m.logf("[unexpected] HandleDNSoverTLSConn.dotCert: %v", err) + conn.Close() + } + tlsConn := tls.Server(conn, &tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + }) + m.HandleTCPConn(tlsConn, srcAddr) +} + func (m *Manager) Down() error { m.ctxCancel() if err := m.os.Close(); err != nil { diff --git a/wgengine/netstack/netstack.go b/wgengine/netstack/netstack.go index 7e6067b99..d8ebd579d 100644 --- a/wgengine/netstack/netstack.go +++ b/wgengine/netstack/netstack.go @@ -377,7 +377,10 @@ func (ns *Impl) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper) filter.Re // on port 80 & 53. switch p.IPProto { case ipproto.TCP: - if port := p.Dst.Port(); port != 53 && port != 80 { + switch p.Dst.Port() { + case 80, 53, 853: + // Handle below. + default: return filter.Accept } case ipproto.UDP: @@ -386,7 +389,6 @@ func (ns *Impl) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper) filter.Re } } - var pn tcpip.NetworkProtocolNumber switch p.IPVersion { case 4: @@ -771,8 +773,17 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) { // block until the TCP handshake is complete. c := gonet.NewTCPConn(&wq, ep) - if reqDetails.LocalPort == 53 && (dialIP == magicDNSIP || dialIP == magicDNSIPv6) { - go ns.dns.HandleTCPConn(c, netaddr.IPPortFrom(clientRemoteIP, reqDetails.RemotePort)) + if dialIP == magicDNSIP || dialIP == magicDNSIPv6 { + src := netaddr.IPPortFrom(clientRemoteIP, reqDetails.RemotePort) + switch reqDetails.LocalPort { + case 53: + go ns.dns.HandleTCPConn(c, src) + case 853: + go ns.dns.HandleDNSoverTLSConn(c, src) + default: + ns.logf("[unexpected] TCP connection to service IP on port %d", reqDetails.LocalPort) + c.Close() // should be unreachable + } return }