mirror of
https://github.com/SamTherapy/dnscrypt.git
synced 2024-11-20 04:43:47 +00:00
5ddb58f703
Graceful shutdown of the DNSCrypt server This PR implements Server.Shutdown(ctx context.Context) method that allows to shut down the DNSCrypt server gracefully. Some additional changes that were inadvertently made while doing that: 1. Added benchmark tests 2. Started using dns.ReadFromSessionUDP / dns.WriteToSessionUDP instead of implementing it by ourselves 3. Generally improved tests 4. Added depguard 5. Improved comments overall in the code
222 lines
5.8 KiB
Go
222 lines
5.8 KiB
Go
package dnscrypt
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"net"
|
|
"runtime"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/AdguardTeam/golibs/log"
|
|
"github.com/miekg/dns"
|
|
"golang.org/x/net/ipv4"
|
|
"golang.org/x/net/ipv6"
|
|
)
|
|
|
|
type encryptionFunc func(m *dns.Msg, q EncryptedQuery) ([]byte, error)
|
|
|
|
// UDPResponseWriter is the ResponseWriter implementation for UDP
|
|
type UDPResponseWriter struct {
|
|
udpConn *net.UDPConn // UDP connection
|
|
sess *dns.SessionUDP // SessionUDP (necessary to use dns.WriteToSessionUDP)
|
|
encrypt encryptionFunc // DNSCRypt encryption function
|
|
req *dns.Msg // DNS query that was processed
|
|
query EncryptedQuery // DNSCrypt query properties
|
|
}
|
|
|
|
// type check
|
|
var _ ResponseWriter = &UDPResponseWriter{}
|
|
|
|
// LocalAddr is the server socket local address
|
|
func (w *UDPResponseWriter) LocalAddr() net.Addr {
|
|
return w.udpConn.LocalAddr()
|
|
}
|
|
|
|
// RemoteAddr is the client's address
|
|
func (w *UDPResponseWriter) RemoteAddr() net.Addr {
|
|
return w.udpConn.RemoteAddr()
|
|
}
|
|
|
|
// WriteMsg writes DNS message to the client
|
|
func (w *UDPResponseWriter) WriteMsg(m *dns.Msg) error {
|
|
m.Truncate(dnsSize("udp", w.req))
|
|
|
|
res, err := w.encrypt(m, w.query)
|
|
if err != nil {
|
|
log.Tracef("Failed to encrypt the DNS query: %v", err)
|
|
return err
|
|
}
|
|
_, err = dns.WriteToSessionUDP(w.udpConn, res, w.sess)
|
|
return err
|
|
}
|
|
|
|
// ServeUDP listens to UDP connections, queries are then processed by Server.Handler.
|
|
// It blocks the calling goroutine and to stop it you need to close the listener
|
|
// or call Server.Shutdown.
|
|
func (s *Server) ServeUDP(l *net.UDPConn) error {
|
|
err := s.prepareServeUDP(l)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Tracks UDP handling goroutines
|
|
udpWg := &sync.WaitGroup{}
|
|
defer s.cleanUpUDP(udpWg, l)
|
|
|
|
// Track active goroutine
|
|
s.wg.Add(1)
|
|
|
|
log.Info("Entering DNSCrypt UDP listening loop on udp://%s", l.LocalAddr())
|
|
|
|
// Serialize the cert right away and prepare it to be sent to the client
|
|
certTxt, err := s.getCertTXT()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for s.isStarted() {
|
|
b, sess, err := s.readUDPMsg(l)
|
|
|
|
// Check the error code and exit loop if necessary
|
|
if err != nil {
|
|
if !s.isStarted() {
|
|
// Stopped gracefully
|
|
return nil
|
|
}
|
|
var netErr net.Error
|
|
if errors.As(err, &netErr) && netErr.Temporary() {
|
|
// Note that timeout errors will be here (i.e. hitting ReadDeadline)
|
|
continue
|
|
}
|
|
if isConnClosed(err) {
|
|
log.Info("udpListen.ReadFrom() returned because we're reading from a closed connection, exiting loop")
|
|
} else {
|
|
log.Info("got error when reading from UDP listen: %s", err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
if len(b) < minDNSPacketSize {
|
|
// Ignore the packets that are too short
|
|
continue
|
|
}
|
|
|
|
udpWg.Add(1)
|
|
go func() {
|
|
s.serveUDPMsg(b, certTxt, sess, l)
|
|
udpWg.Done()
|
|
}()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// prepareServeUDP prepares the server and listener to serving DNSCrypt
|
|
func (s *Server) prepareServeUDP(l *net.UDPConn) error {
|
|
// Check that server is properly configured
|
|
if !s.validate() {
|
|
return ErrServerConfig
|
|
}
|
|
|
|
// set UDP options to allow receiving OOB data
|
|
err := setUDPSocketOptions(l)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Protect shutdown-related fields
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
s.initOnce.Do(s.init)
|
|
|
|
// Mark the server as started.
|
|
// Note that we don't check if it was started before as
|
|
// Serve* methods can be called multiple times.
|
|
s.started = true
|
|
|
|
// Track an active UDP listener
|
|
s.udpListeners[l] = struct{}{}
|
|
return err
|
|
}
|
|
|
|
// cleanUpUDP waits until all UDP messages before cleaning up
|
|
func (s *Server) cleanUpUDP(udpWg *sync.WaitGroup, l *net.UDPConn) {
|
|
// Wait until UDP messages are processed
|
|
udpWg.Wait()
|
|
|
|
// Not using it anymore so can be removed from the active listeners
|
|
s.lock.Lock()
|
|
delete(s.udpListeners, l)
|
|
s.lock.Unlock()
|
|
|
|
// The work is finished
|
|
s.wg.Done()
|
|
}
|
|
|
|
// readUDPMsg reads incoming UDP message
|
|
func (s *Server) readUDPMsg(l *net.UDPConn) ([]byte, *dns.SessionUDP, error) {
|
|
_ = l.SetReadDeadline(time.Now().Add(defaultReadTimeout))
|
|
b := make([]byte, dns.MinMsgSize)
|
|
n, sess, err := dns.ReadFromSessionUDP(l, b)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return b[:n], sess, err
|
|
}
|
|
|
|
// serveUDPMsg handles incoming DNS message
|
|
func (s *Server) serveUDPMsg(b []byte, certTxt string, sess *dns.SessionUDP, l *net.UDPConn) {
|
|
// First of all, check for "ClientMagic" in the incoming query
|
|
if !bytes.Equal(b[:clientMagicSize], s.ResolverCert.ClientMagic[:]) {
|
|
// If there's no ClientMagic in the packet, we assume this
|
|
// is a plain DNS query requesting the certificate data
|
|
reply, err := s.handleHandshake(b, certTxt)
|
|
if err != nil {
|
|
log.Tracef("failed to process a plain DNS query: %v", err)
|
|
}
|
|
if err == nil {
|
|
// Ignore errors, we don't care and can't handle them anyway
|
|
_, _ = dns.WriteToSessionUDP(l, reply, sess)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// If we got here, this is an encrypted DNSCrypt message
|
|
// We should decrypt it first to get the plain DNS query
|
|
m, q, err := s.decrypt(b)
|
|
if err == nil {
|
|
rw := &UDPResponseWriter{
|
|
udpConn: l,
|
|
sess: sess,
|
|
encrypt: s.encrypt,
|
|
req: m,
|
|
query: q,
|
|
}
|
|
err = s.serveDNS(rw, m)
|
|
if err != nil {
|
|
log.Tracef("failed to process a DNS query: %v", err)
|
|
}
|
|
} else {
|
|
log.Tracef("failed to decrypt incoming message len=%d: %v", len(b), err)
|
|
}
|
|
}
|
|
|
|
// setUDPSocketOptions method is necessary to be able to use dns.ReadFromSessionUDP / dns.WriteToSessionUDP
|
|
func setUDPSocketOptions(conn *net.UDPConn) error {
|
|
if runtime.GOOS == "windows" {
|
|
return nil
|
|
}
|
|
|
|
// We don't know if this a IPv4-only, IPv6-only or a IPv4-and-IPv6 connection.
|
|
// Try enabling receiving of ECN and packet info for both IP versions.
|
|
// We expect at least one of those syscalls to succeed.
|
|
err6 := ipv6.NewPacketConn(conn).SetControlMessage(ipv6.FlagDst|ipv6.FlagInterface, true)
|
|
err4 := ipv4.NewPacketConn(conn).SetControlMessage(ipv4.FlagDst|ipv4.FlagInterface, true)
|
|
if err6 != nil && err4 != nil {
|
|
return err4
|
|
}
|
|
return nil
|
|
}
|