Files
2026-09-22 18:32:25 +03:00

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