mirror of
https://github.com/AdguardTeam/dnsproxy.git
synced 2026-10-12 02:01:26 +00:00
253 lines
6.2 KiB
Go
253 lines
6.2 KiB
Go
package proxy
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net"
|
|
|
|
"github.com/AdguardTeam/golibs/container"
|
|
"github.com/AdguardTeam/golibs/errors"
|
|
"github.com/AdguardTeam/golibs/logutil/optslog"
|
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
|
"github.com/AdguardTeam/golibs/netutil"
|
|
"github.com/miekg/dns"
|
|
"github.com/quic-go/quic-go"
|
|
)
|
|
|
|
// startListeners configures listeners and starts listening each configured
|
|
// address. If it returns an error, all listeners should be closed manually.
|
|
func (p *Proxy) startListeners(ctx context.Context) (err error) {
|
|
err = p.initUDPListeners(ctx)
|
|
if err != nil {
|
|
// Don't wrap the error since it's informative enough as is.
|
|
return err
|
|
}
|
|
|
|
err = p.initTCPListeners(ctx)
|
|
if err != nil {
|
|
// Don't wrap the error since it's informative enough as is.
|
|
return err
|
|
}
|
|
|
|
err = p.initTLSListeners(ctx)
|
|
if err != nil {
|
|
// Don't wrap the error since it's informative enough as is.
|
|
return err
|
|
}
|
|
|
|
err = p.initHTTPSListeners(ctx)
|
|
if err != nil {
|
|
// Don't wrap the error since it's informative enough as is.
|
|
return err
|
|
}
|
|
|
|
err = p.initQUICListeners(ctx)
|
|
if err != nil {
|
|
// Don't wrap the error since it's informative enough as is.
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// serveListeners starts serving the configured listeners.
|
|
func (p *Proxy) serveListeners(ctx context.Context) {
|
|
for _, l := range p.udpListen {
|
|
go p.udpPacketLoop(ctx, l, p.requestsSema)
|
|
}
|
|
|
|
for _, l := range p.tcpListen {
|
|
go p.tcpPacketLoop(ctx, l, ProtoTCP, p.requestsSema)
|
|
}
|
|
|
|
for _, l := range p.tlsListen {
|
|
go p.tcpPacketLoop(ctx, l, ProtoTLS, p.requestsSema)
|
|
}
|
|
|
|
for _, l := range p.httpsListen {
|
|
go func(l net.Listener) { _ = p.httpsServer.Serve(l) }(l)
|
|
}
|
|
|
|
for _, l := range p.h3Listen {
|
|
go func(l *quic.EarlyListener) { _ = p.h3Server.ServeListener(l) }(l)
|
|
}
|
|
|
|
for _, l := range p.quicListen {
|
|
go p.quicPacketLoop(ctx, l, p.requestsSema)
|
|
}
|
|
}
|
|
|
|
// handleDNSRequest processes the context. The only error it returns is the one
|
|
// from the [Handler]. d must not be nil.
|
|
func (p *Proxy) handleDNSRequest(ctx context.Context, d *DNSContext) (err error) {
|
|
p.logDNSMessage(ctx, d.Req)
|
|
|
|
if d.Req.Response {
|
|
p.logger.DebugContext(ctx, "dropping incoming response packet", "addr", d.Addr)
|
|
|
|
return nil
|
|
}
|
|
|
|
ip := d.Addr.Addr()
|
|
d.IsPrivateClient = p.privateNets.Contains(ip)
|
|
|
|
// TODO(d.kolyshev): Consider moving validation to a new middleware.
|
|
d.Res = p.validateRequest(d)
|
|
if d.Res == nil {
|
|
err = p.requestHandler.ServeDNS(ctx, p, d)
|
|
if errors.Is(err, ErrDrop) {
|
|
// Don't reply to dropped clients.
|
|
return nil
|
|
}
|
|
}
|
|
|
|
p.logDNSMessage(ctx, d.Res)
|
|
p.respond(ctx, d)
|
|
|
|
// Don't wrap the error since it's informative enough as is.
|
|
return err
|
|
}
|
|
|
|
// isForbiddenARPA returns true if dctx contains a PTR, SOA, or NS request for
|
|
// some private address and client's address is not within the private network.
|
|
// Otherwise, it sets [DNSContext.RequestedPrivateRDNS] for future use.
|
|
func (dctx *DNSContext) isForbiddenARPA(privateNets netutil.SubnetSet, l *slog.Logger) (ok bool) {
|
|
q := dctx.Req.Question[0]
|
|
switch q.Qtype {
|
|
case dns.TypePTR, dns.TypeSOA, dns.TypeNS:
|
|
// Go on.
|
|
//
|
|
// TODO(e.burkov): Reconsider the list of types involved to private
|
|
// address space. Perhaps, use the logic for any type. See
|
|
// https://www.rfc-editor.org/rfc/rfc6761.html#section-6.1.
|
|
default:
|
|
return false
|
|
}
|
|
|
|
requestedPref, err := netutil.ExtractReversedAddr(q.Name)
|
|
if err != nil {
|
|
l.Debug("parsing reversed subnet", slogutil.KeyError, err)
|
|
|
|
return false
|
|
}
|
|
|
|
if privateNets.Contains(requestedPref.Addr()) {
|
|
dctx.RequestedPrivateRDNS = requestedPref
|
|
|
|
return !dctx.IsPrivateClient
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// respond writes the specified response to the client, if any. d must not be
|
|
// nil.
|
|
func (p *Proxy) respond(ctx context.Context, d *DNSContext) {
|
|
// d.Conn can be nil in the case of a DoH request.
|
|
if d.Conn != nil {
|
|
_ = d.Conn.SetWriteDeadline(p.time.Now().Add(defaultTimeout))
|
|
}
|
|
|
|
var err error
|
|
|
|
switch d.Proto {
|
|
case ProtoUDP:
|
|
err = p.respondUDP(d.Res, d.Conn.(*net.UDPConn), net.UDPAddrFromAddrPort(d.Addr), d.localIP)
|
|
case ProtoTCP:
|
|
err = p.respondTCP(d)
|
|
case ProtoTLS:
|
|
err = p.respondTCP(d)
|
|
case ProtoHTTPS:
|
|
err = p.respondHTTPS(d)
|
|
case ProtoQUIC:
|
|
err = p.respondQUIC(d)
|
|
case ProtoDNSCrypt:
|
|
err = p.respondDNSCrypt(ctx, d)
|
|
default:
|
|
err = fmt.Errorf("SHOULD NOT HAPPEN - unknown protocol: %s", d.Proto)
|
|
}
|
|
|
|
if err != nil {
|
|
logWithNonCrit(ctx, err, "responding request", d.Proto, p.logger)
|
|
}
|
|
}
|
|
|
|
// setMinMaxTTL sets the TTL values of all records according to the proxy
|
|
// settings. r must not be nil.
|
|
func (p *Proxy) setMinMaxTTL(ctx context.Context, r *dns.Msg) {
|
|
rrSets := container.KeyValues[string, []dns.RR]{{
|
|
Key: "answer",
|
|
Value: r.Answer,
|
|
}, {
|
|
Key: "extra",
|
|
Value: r.Extra,
|
|
}, {
|
|
Key: "ns",
|
|
Value: r.Ns,
|
|
}}
|
|
|
|
for _, rrSet := range rrSets {
|
|
for _, rr := range rrSet.Value {
|
|
original := rr.Header().Ttl
|
|
overridden := respectTTLOverrides(original, p.cacheMinTTL, p.cacheMaxTTL)
|
|
|
|
if original == overridden {
|
|
continue
|
|
}
|
|
|
|
optslog.Trace3(
|
|
ctx,
|
|
p.logger,
|
|
"ttl overwritten",
|
|
"section", rrSet.Key,
|
|
"old", original,
|
|
"new", overridden,
|
|
)
|
|
rr.Header().Ttl = overridden
|
|
}
|
|
}
|
|
}
|
|
|
|
// logDNSMessage logs the given DNS message.
|
|
func (p *Proxy) logDNSMessage(ctx context.Context, m *dns.Msg) {
|
|
if m == nil {
|
|
return
|
|
}
|
|
|
|
var msg string
|
|
if m.Response {
|
|
msg = "out"
|
|
} else {
|
|
msg = "in"
|
|
}
|
|
|
|
slogutil.PrintLines(ctx, p.logger, slog.LevelDebug, msg, m.String())
|
|
}
|
|
|
|
// logWithNonCrit logs the error on the appropriate level, depending on whether
|
|
// err is a critical error or not. Connection closure or timeout errors are
|
|
// considered non-critical. l must not be nil.
|
|
func logWithNonCrit(ctx context.Context, err error, msg string, proto Proto, l *slog.Logger) {
|
|
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) || isEPIPE(err) {
|
|
l.DebugContext(
|
|
ctx,
|
|
"connection is closed",
|
|
"proto", proto,
|
|
"details", msg,
|
|
slogutil.KeyError, err,
|
|
)
|
|
} else if netErr, ok := errors.AsType[net.Error](err); ok && netErr.Timeout() {
|
|
l.DebugContext(
|
|
ctx,
|
|
"connection timed out",
|
|
"proto", proto,
|
|
"details", msg,
|
|
slogutil.KeyError, err,
|
|
)
|
|
} else {
|
|
l.ErrorContext(ctx, msg, "proto", proto, slogutil.KeyError, err)
|
|
}
|
|
}
|