Compare commits

...

1 Commits

Author SHA1 Message Date
Brad Fitzpatrick
a37bcc4f89 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 <bradfitz@tailscale.com>
2022-05-07 20:38:20 -07:00
2 changed files with 107 additions and 11 deletions

View File

@@ -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 {

View File

@@ -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
}