mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-05 12:16:44 +02:00
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>
This commit is contained in:
parent
741ae9956e
commit
a37bcc4f89
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user