Pull request: AG-32410-proxy

Squashed commit of the following:

commit 0d4b13d220d8a2e1ddf807c36b4677e445821f55
Author: Dimitry Kolyshev <[email protected]>
Date:   Tue May 28 12:11:16 2024 +0400

    proxy: imp code

commit 10b564b201deb0bd6d8b9f8a591d1ed1b0934a22
Author: Dimitry Kolyshev <[email protected]>
Date:   Mon May 27 11:48:11 2024 +0400

    proxy: imp docs

commit ee010801be05f6ee35538d400d3e2687687a8cd9
Author: Dimitry Kolyshev <[email protected]>
Date:   Fri May 24 11:41:38 2024 +0300

    proxy: imp code

commit f3033c104ffb8c7610fba27ca23cfdc07779ea54
Author: Dimitry Kolyshev <[email protected]>
Date:   Fri May 24 11:39:17 2024 +0300

    proxy: imp code

commit 222c2e1cc673c5beab3a4f529f46677fa4ff62e4
Author: Dimitry Kolyshev <[email protected]>
Date:   Fri May 24 11:31:31 2024 +0300

    proxy: imp code

commit 195526347f581f9199f2d142596c49aaa6cf64a6
Author: Dimitry Kolyshev <[email protected]>
Date:   Thu May 23 12:46:12 2024 +0300

    proxy: imp docs

commit f9348cd20b8de8c56c0f9b15dbb2bcfc35e983b4
Author: Dimitry Kolyshev <[email protected]>
Date:   Thu May 23 12:44:35 2024 +0300

    proxy: imp code

commit d7b2073786b048b6881dd178052b64af50bbe8a4
Author: Dimitry Kolyshev <[email protected]>
Date:   Thu May 23 11:59:32 2024 +0300

    proxy: imp code

commit c976bf6506f19780c761b1f3d619a888676f8d5f
Author: Dimitry Kolyshev <[email protected]>
Date:   Thu May 23 11:35:38 2024 +0300

    proxy: imp code

commit 15645eb6738800d07f3d110352edc149cc76f196
Author: Dimitry Kolyshev <[email protected]>
Date:   Thu May 23 11:14:16 2024 +0300

    proxy: imp code

commit d67444568fa6372cedca9bd87d565b09e3209e55
Author: Dimitry Kolyshev <[email protected]>
Date:   Thu May 23 10:52:00 2024 +0300

    proxy: imp code

commit 9eef793f38c6633311b17cdbe3538449c35feb38
Author: Dimitry Kolyshev <[email protected]>
Date:   Wed May 22 13:24:38 2024 +0300

    proxy: imp code

commit 9d07061d80c343cd7fc81ef568c684ad4a1827f1
Author: Dimitry Kolyshev <[email protected]>
Date:   Wed May 22 13:06:58 2024 +0300

    proxy: imp code

commit ccd43943a7fc14627ac558bd14cf3956d7570dd5
Author: Dimitry Kolyshev <[email protected]>
Date:   Wed May 22 12:55:46 2024 +0300

    proxy: imp code

commit bf9bcd3634ddf9377c94494537eb0aa94d44131e
Author: Dimitry Kolyshev <[email protected]>
Date:   Wed May 22 12:42:32 2024 +0300

    proxy: imp code

commit 7ff481719e1a8947fc868fcf2224162d6db225ba
Author: Dimitry Kolyshev <[email protected]>
Date:   Wed May 22 12:38:03 2024 +0300

    proxy: imp code

commit de4b8a7e0cdd3a5c2157c53956480d6dfabc7632
Author: Dimitry Kolyshev <[email protected]>
Date:   Tue May 21 13:52:52 2024 +0300

    proxy: imp code

commit b28f71b4a18f5cd5d2097719d58eee703a7d5384
Author: Dimitry Kolyshev <[email protected]>
Date:   Tue May 21 13:46:52 2024 +0300

    proxy: imp code

commit 3947f376687dfe11374b2969be5f5f6caa8c63ed
Author: Dimitry Kolyshev <[email protected]>
Date:   Tue May 21 13:12:10 2024 +0300

    proxy: imp code

commit 580c135382652b61dff546bb282c6b042e585562
Author: Dimitry Kolyshev <[email protected]>
Date:   Tue May 21 12:52:53 2024 +0300

    proxy: imp code

... and 6 more commits
This commit is contained in:
Dimitry Kolyshev
2024-05-29 14:03:43 +03:00
parent b65c1a7be1
commit 26a31c79d6
12 changed files with 324 additions and 251 deletions
+31 -13
View File
@@ -339,28 +339,46 @@ func (p *Proxy) logConfigInfo() {
// validateListenAddrs returns an error if the addresses are not configured
// properly.
func (p *Proxy) validateListenAddrs() error {
func (p *Proxy) validateListenAddrs() (err error) {
if !p.hasListenAddrs() {
return errors.Error("no listen address specified")
}
if p.TLSConfig == nil {
if p.TLSListenAddr != nil {
return errors.Error("cannot create tls listener without tls config")
err = p.validateTLSConfig()
if err != nil {
return fmt.Errorf("invalid tls configuration: %w", err)
}
if p.DNSCryptResolverCert == nil || p.DNSCryptProviderName == "" {
if p.DNSCryptTCPListenAddr != nil {
return errors.Error("cannot create dnscrypt tcp listener without dnscrypt config")
}
if p.HTTPSListenAddr != nil {
return errors.Error("cannot create https listener without tls config")
}
if p.QUICListenAddr != nil {
return errors.Error("cannot create quic listener without tls config")
if p.DNSCryptUDPListenAddr != nil {
return errors.Error("cannot create dnscrypt udp listener without dnscrypt config")
}
}
if (p.DNSCryptTCPListenAddr != nil || p.DNSCryptUDPListenAddr != nil) &&
(p.DNSCryptResolverCert == nil || p.DNSCryptProviderName == "") {
return errors.Error("cannot create dnscrypt listener without dnscrypt config")
return nil
}
// validateTLSConfig returns an error if proxy TLS configuration parameters are
// needed but aren't provided.
func (p *Proxy) validateTLSConfig() (err error) {
if p.TLSConfig != nil {
return nil
}
if p.TLSListenAddr != nil {
return errors.Error("tls listener configuration not found")
}
if p.HTTPSListenAddr != nil {
return errors.Error("https listener configuration not found")
}
if p.QUICListenAddr != nil {
return errors.Error("quic listener configuration not found")
}
return nil
+2 -3
View File
@@ -105,12 +105,11 @@ type DNSContext struct {
//
// TODO(e.burkov): Consider creating DNSContext with this everywhere, to
// actually respect the contract of DNSContext.RequestID field.
//
// TODO(e.burkov): Add remote address into arguments.
func (p *Proxy) newDNSContext(proto Proto, req *dns.Msg) (d *DNSContext) {
func (p *Proxy) newDNSContext(proto Proto, req *dns.Msg, addr netip.AddrPort) (d *DNSContext) {
return &DNSContext{
Proto: proto,
Req: req,
Addr: addr,
RequestID: p.counter.Add(1),
}
+21 -9
View File
@@ -8,6 +8,7 @@ import (
"github.com/AdguardTeam/dnsproxy/proxyutil"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/miekg/dns"
)
@@ -18,11 +19,15 @@ type lookupResult struct {
err error
}
// lookupIPAddr resolves the specified host IP addresses.
// lookupIPAddr resolves the specified host IP addresses. It is intended to be
// used as a goroutine.
func (p *Proxy) lookupIPAddr(host string, qtype uint16, ch chan *lookupResult) {
defer log.OnPanic("Proxy.lookupIPAddr")
req := (&dns.Msg{}).SetQuestion(host, qtype)
d := p.newDNSContext(ProtoUDP, req)
// TODO(d.kolyshev): Investigate why the client address is not defined.
d := p.newDNSContext(ProtoUDP, req, netip.AddrPort{})
err := p.Resolve(d)
ch <- &lookupResult{d.Res, err}
}
@@ -36,7 +41,7 @@ var _ upstream.Resolver = (*Proxy)(nil)
// LookupNetIP implements the [upstream.Resolver] interface for *Proxy. It
// resolves the specified host IP addresses by sending two DNS queries (A and
// AAAA) in parallel. It returns both results for those two queries.
// AAAA) in parallel. It returns both results for those two queries.
func (p *Proxy) LookupNetIP(
_ context.Context,
_ string,
@@ -61,12 +66,7 @@ func (p *Proxy) LookupNetIP(
continue
}
for _, ans := range result.resp.Answer {
a := proxyutil.IPFromRR(ans)
if a != (netip.Addr{}) {
addrs = append(addrs, a)
}
}
addrs = appendAnswerAddrs(addrs, result.resp.Answer)
}
if len(addrs) == 0 && len(errs) != 0 {
@@ -81,3 +81,15 @@ func (p *Proxy) LookupNetIP(
return addrs, nil
}
// appendAnswerAddrs returns addrs with addresses appended from the given ans.
func appendAnswerAddrs(addrs []netip.Addr, ans []dns.RR) (res []netip.Addr) {
for _, ansRR := range ans {
a := proxyutil.IPFromRR(ansRR)
if a != (netip.Addr{}) {
addrs = append(addrs, a)
}
}
return addrs
}
+70 -78
View File
@@ -340,6 +340,14 @@ func (p *Proxy) validateBasicAuth() (err error) {
return nil
}
// Returns true if proxy is started. It is safe for concurrent use.
func (p *Proxy) isStarted() (ok bool) {
p.RLock()
defer p.RUnlock()
return p.started
}
// type check
var _ service.Interface = (*Proxy)(nil)
@@ -360,11 +368,12 @@ func (p *Proxy) Start(ctx context.Context) (err error) {
return err
}
err = p.startListeners(ctx)
err = p.configureListeners(ctx)
if err != nil {
return fmt.Errorf("starting listeners: %w", err)
return fmt.Errorf("configuring listeners: %w", err)
}
p.startListeners()
p.started = true
return nil
@@ -458,97 +467,80 @@ func (p *Proxy) Shutdown(_ context.Context) (err error) {
return nil
}
// Addrs returns all listen addresses for the specified proto or nil if the proxy does not listen to it.
// proto must be "tcp", "tls", "https", "quic", or "udp"
func (p *Proxy) Addrs(proto Proto) []net.Addr {
p.RLock()
defer p.RUnlock()
// addrFunc provides the address from the given A.
type addrFunc[A any] func(l A) (addr net.Addr)
var addrs []net.Addr
switch proto {
case ProtoTCP:
for _, l := range p.tcpListen {
addrs = append(addrs, l.Addr())
}
case ProtoTLS:
for _, l := range p.tlsListen {
addrs = append(addrs, l.Addr())
}
case ProtoHTTPS:
for _, l := range p.httpsListen {
addrs = append(addrs, l.Addr())
}
case ProtoUDP:
for _, l := range p.udpListen {
addrs = append(addrs, l.LocalAddr())
}
case ProtoQUIC:
for _, l := range p.quicListen {
addrs = append(addrs, l.Addr())
}
case ProtoDNSCrypt:
// Using only UDP addrs here
// TODO: to do it better we should either do ProtoDNSCryptTCP/ProtoDNSCryptUDP
// or we should change the configuration so that it was not possible to
// set different ports for TCP/UDP listeners.
for _, l := range p.dnsCryptUDPListen {
addrs = append(addrs, l.LocalAddr())
}
default:
panic("proto must be 'tcp', 'tls', 'https', 'quic', 'dnscrypt' or 'udp'")
// collectAddrs returns the slice of network addresses of the given listeners
// using the given addrFunc.
func collectAddrs[A any](listeners []A, af addrFunc[A]) (addrs []net.Addr) {
for _, l := range listeners {
addrs = append(addrs, af(l))
}
return addrs
}
// Addr returns the first listen address for the specified proto or null if the proxy does not listen to it
// proto must be "tcp", "tls", "https", "quic", or "udp"
func (p *Proxy) Addr(proto Proto) net.Addr {
// Addrs returns all listen addresses for the specified proto or nil if the
// proxy does not listen to it. proto must be one of [Proto]: [ProtoTCP],
// [ProtoUDP], [ProtoTLS], [ProtoHTTPS], [ProtoQUIC], or [ProtoDNSCrypt].
func (p *Proxy) Addrs(proto Proto) (addrs []net.Addr) {
p.RLock()
defer p.RUnlock()
switch proto {
case ProtoTCP:
if len(p.tcpListen) == 0 {
return nil
}
return p.tcpListen[0].Addr()
return collectAddrs(p.tcpListen, net.Listener.Addr)
case ProtoTLS:
if len(p.tlsListen) == 0 {
return nil
}
return p.tlsListen[0].Addr()
return collectAddrs(p.tlsListen, net.Listener.Addr)
case ProtoHTTPS:
if len(p.httpsListen) == 0 {
return nil
}
return p.httpsListen[0].Addr()
return collectAddrs(p.httpsListen, net.Listener.Addr)
case ProtoUDP:
if len(p.udpListen) == 0 {
return nil
}
return p.udpListen[0].LocalAddr()
return collectAddrs(p.udpListen, (*net.UDPConn).LocalAddr)
case ProtoQUIC:
if len(p.quicListen) == 0 {
return nil
}
return p.quicListen[0].Addr()
return collectAddrs(p.quicListen, (*quic.EarlyListener).Addr)
case ProtoDNSCrypt:
if len(p.dnsCryptUDPListen) == 0 {
return nil
}
return p.dnsCryptUDPListen[0].LocalAddr()
// Using only UDP addrs here
//
// TODO(ameshkov): To do it better we should either do
// ProtoDNSCryptTCP/ProtoDNSCryptUDP or we should change the
// configuration so that it was not possible to set different ports for
// TCP/UDP listeners.
return collectAddrs(p.dnsCryptUDPListen, (*net.UDPConn).LocalAddr)
default:
panic("proto must be 'tcp', 'tls', 'https', 'quic', 'dnscrypt' or 'udp'")
}
}
// firstAddr returns the network address of the first listener in the given
// listeners or nil using the given addrFunc.
func firstAddr[A any](listeners []A, af addrFunc[A]) (addr net.Addr) {
if len(listeners) == 0 {
return nil
}
return af(listeners[0])
}
// Addr returns the first listen address for the specified proto or nil if the
// proxy does not listen to it. proto must be one of [Proto]: [ProtoTCP],
// [ProtoUDP], [ProtoTLS], [ProtoHTTPS], [ProtoQUIC], or [ProtoDNSCrypt].
func (p *Proxy) Addr(proto Proto) (addr net.Addr) {
p.RLock()
defer p.RUnlock()
switch proto {
case ProtoTCP:
return firstAddr(p.tcpListen, net.Listener.Addr)
case ProtoTLS:
return firstAddr(p.tlsListen, net.Listener.Addr)
case ProtoHTTPS:
return firstAddr(p.httpsListen, net.Listener.Addr)
case ProtoUDP:
return firstAddr(p.udpListen, (*net.UDPConn).LocalAddr)
case ProtoQUIC:
return firstAddr(p.quicListen, (*quic.EarlyListener).Addr)
case ProtoDNSCrypt:
return firstAddr(p.dnsCryptUDPListen, (*net.UDPConn).LocalAddr)
default:
panic("proto must be 'tcp', 'tls', 'https', 'quic', 'dnscrypt' or 'udp'")
}
+64 -35
View File
@@ -335,30 +335,28 @@ func TestProxyRace(t *testing.T) {
g.Wait()
}
func TestProxy_Resolve_dnssecCache(t *testing.T) {
const host = "example.com"
// newTxts returns new test TXT RR strings.
func newTxts(t *testing.T, txtDataLen int) (txts []string) {
t.Helper()
const (
// Larger than UDP buffer size to invoke truncation.
txtDataLen = 1024
txtDataChunkLen = 255
)
const txtDataChunkLen = 255
txtDataChunkNum := txtDataLen / txtDataChunkLen
if txtDataLen%txtDataChunkLen > 0 {
txtDataChunkNum++
}
txts := make([]string, txtDataChunkNum)
txts = make([]string, txtDataChunkNum)
randData := make([]byte, txtDataLen)
n, err := rand.Read(randData)
require.NoError(t, err)
require.Equal(t, txtDataLen, n)
for i, c := range randData {
randData[i] = c%26 + 'a'
}
// *dns.TXT requires splitting the actual data into
// 256-byte chunks.
// *dns.TXT requires splitting the actual data into 256-byte chunks.
for i := range txtDataChunkNum {
r := txtDataChunkLen * (i + 1)
if r > txtDataLen {
@@ -366,13 +364,60 @@ func TestProxy_Resolve_dnssecCache(t *testing.T) {
}
txts[i] = string(randData[txtDataChunkLen*i : r])
}
return txts
}
// newDNSContext returns new DNS request message context with Proto set to
// [ProtoUDP]. Constructs request message from the given parameters.
func newDNSContext(
domain string,
qtype uint16,
qclass uint16,
edns bool,
udpsize uint16,
) (dctx *DNSContext) {
req := newReq(domain, qtype, qclass)
if edns {
req.SetEdns0(udpsize, true)
}
return &DNSContext{
Req: req,
Proto: ProtoUDP,
}
}
// newReq returns new request message for provided parameters.
func newReq(domain string, qtype, qclass uint16) (req *dns.Msg) {
return &dns.Msg{
MsgHdr: dns.MsgHdr{
Id: dns.Id(),
},
Compress: true,
Question: []dns.Question{{
Name: dns.Fqdn(domain),
Qtype: qtype,
Qclass: qclass,
}},
}
}
func TestProxy_Resolve_dnssecCache(t *testing.T) {
const (
host = "example.com"
// Larger than UDP buffer size to invoke truncation.
txtDataLen = 1024
)
txt := &dns.TXT{
Hdr: dns.RR_Header{
Name: dns.Fqdn(host),
Rrtype: dns.TypeTXT,
Class: dns.ClassINET,
},
Txt: txts,
Txt: newTxts(t, txtDataLen),
}
a := &dns.A{
@@ -406,10 +451,10 @@ func TestProxy_Resolve_dnssecCache(t *testing.T) {
SignerName: dns.Fqdn(host),
Signature: "c29tZSBycnNpZyByZWxhdGVkIHN0dWZm",
}
u := &fakeUpstream{
onExchange: func(m *dns.Msg) (resp *dns.Msg, err error) {
resp = &dns.Msg{}
resp.SetReply(m)
resp = (&dns.Msg{}).SetReply(m)
q := m.Question[0]
switch q.Qtype {
@@ -490,29 +535,13 @@ func TestProxy_Resolve_dnssecCache(t *testing.T) {
}}
for _, tc := range testCases {
req := &dns.Msg{
MsgHdr: dns.MsgHdr{
Id: dns.Id(),
},
Compress: true,
Question: []dns.Question{{
Name: dns.Fqdn(tc.wantAns.Header().Name),
Qtype: tc.wantAns.Header().Rrtype,
Qclass: tc.wantAns.Header().Class,
}},
}
if tc.edns {
req.SetEdns0(txtDataLen/2, true)
}
dctx := &DNSContext{
Req: req,
Proto: ProtoUDP,
}
ansHdr := tc.wantAns.Header()
dctx := newDNSContext(ansHdr.Name, ansHdr.Rrtype, ansHdr.Class, tc.edns, txtDataLen/2)
t.Run(tc.name, func(t *testing.T) {
t.Cleanup(p.cache.items.Clear)
err = p.Resolve(dctx)
err := p.Resolve(dctx)
require.NoError(t, err)
res := dctx.Res
@@ -536,6 +565,7 @@ func TestProxy_Resolve_dnssecCache(t *testing.T) {
cached, expired, key := p.cache.get(dctx.Req)
require.NotNil(t, cached)
require.Len(t, cached.m.Answer, 2)
assert.False(t, expired)
assert.Equal(t, key, msgToKey(dctx.Req))
@@ -1575,8 +1605,7 @@ func TestProxy_HandleDNSRequest_private(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
dctx := p.newDNSContext(ProtoUDP, tc.req)
dctx.Addr = tc.cliAddr
dctx := p.newDNSContext(ProtoUDP, tc.req, tc.cliAddr)
require.NoError(t, p.handleDNSRequest(dctx))
assert.Equal(t, tc.want, dctx.Res)
+8 -5
View File
@@ -13,9 +13,9 @@ import (
"github.com/quic-go/quic-go"
)
// startListeners configures and starts listener loops
func (p *Proxy) startListeners(ctx context.Context) error {
err := p.createUDPListeners(ctx)
// configureListeners configures listeners.
func (p *Proxy) configureListeners(ctx context.Context) (err error) {
err = p.createUDPListeners(ctx)
if err != nil {
return err
}
@@ -45,6 +45,11 @@ func (p *Proxy) startListeners(ctx context.Context) error {
return err
}
return nil
}
// startListeners starts listener loops.
func (p *Proxy) startListeners() {
for _, l := range p.udpListen {
go p.udpPacketLoop(l, p.requestsSema)
}
@@ -76,8 +81,6 @@ func (p *Proxy) startListeners(ctx context.Context) error {
for _, l := range p.dnsCryptTCPListen {
go func(l net.Listener) { _ = p.dnsCryptServer.ServeTCP(l) }(l)
}
return nil
}
// handleDNSRequest processes the context. The only error it returns is the one
+1 -2
View File
@@ -71,8 +71,7 @@ var _ dnscrypt.Handler = &dnsCryptHandler{}
// ServeDNS - processes the DNS query
func (h *dnsCryptHandler) ServeDNS(rw dnscrypt.ResponseWriter, req *dns.Msg) (err error) {
d := h.proxy.newDNSContext(ProtoDNSCrypt, req)
d.Addr = netutil.NetAddrToAddrPort(rw.RemoteAddr())
d := h.proxy.newDNSContext(ProtoDNSCrypt, req, netutil.NetAddrToAddrPort(rw.RemoteAddr()))
d.DNSCryptResponseWriter = rw
// TODO(d.kolyshev): Pass and use context from above.
+65 -49
View File
@@ -90,12 +90,62 @@ func (p *Proxy) createHTTPSListeners() (err error) {
return nil
}
// newDoHReq returns new DNS request parsed from the given HTTP request. In
// case of invalid request returns nil and the suitable status code for an HTTP
// error response.
func newDoHReq(r *http.Request) (req *dns.Msg, statusCode int) {
var buf []byte
var err error
switch r.Method {
case http.MethodGet:
dnsParam := r.URL.Query().Get("dns")
buf, err = base64.RawURLEncoding.DecodeString(dnsParam)
if len(buf) == 0 || err != nil {
log.Debug("dnsproxy: parsing dns request from get param %q: %v", dnsParam, err)
return nil, http.StatusBadRequest
}
case http.MethodPost:
contentType := r.Header.Get(httphdr.ContentType)
if contentType != "application/dns-message" {
log.Debug("dnsproxy: unsupported media type %q", contentType)
return nil, http.StatusUnsupportedMediaType
}
// TODO(d.kolyshev): Limit reader.
buf, err = io.ReadAll(r.Body)
if err != nil {
log.Debug("dnsproxy: reading http request body: %s", err)
return nil, http.StatusBadRequest
}
defer log.OnCloserError(r.Body, log.DEBUG)
default:
log.Debug("dnsproxy: bad http method %q", r.Method)
return nil, http.StatusMethodNotAllowed
}
req = &dns.Msg{}
if err = req.Unpack(buf); err != nil {
log.Debug("dnsproxy: unpacking http msg: %s", err)
return nil, http.StatusBadRequest
}
return req, http.StatusOK
}
// ServeHTTP is the http.Handler implementation that handles DoH queries.
//
// Here is what it returns:
//
// - http.StatusBadRequest if there is no DNS request data;
// - http.StatusBadRequest if there is no DNS request data,
// - http.StatusUnsupportedMediaType if request content type is not
// "application/dns-message";
// "application/dns-message",
// - http.StatusMethodNotAllowed if request method is not GET or POST.
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Debug("dnsproxy: incoming https request on %s", r.URL)
@@ -109,65 +159,31 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
var buf []byte
switch r.Method {
case http.MethodGet:
dnsParam := r.URL.Query().Get("dns")
buf, err = base64.RawURLEncoding.DecodeString(dnsParam)
if len(buf) == 0 || err != nil {
log.Debug("dnsproxy: parsing dns request from get param %q: %v", dnsParam, err)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
case http.MethodPost:
contentType := r.Header.Get("Content-Type")
if contentType != "application/dns-message" {
log.Debug("dnsproxy: unsupported media type %q", contentType)
http.Error(w, http.StatusText(http.StatusUnsupportedMediaType), http.StatusUnsupportedMediaType)
return
}
buf, err = io.ReadAll(r.Body)
if err != nil {
log.Debug("dnsproxy: reading http request body: %s", err)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
defer log.OnCloserError(r.Body, log.DEBUG)
default:
log.Debug("dnsproxy: bad http method %q", r.Method)
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
req, statusCode := newDoHReq(r)
if req == nil {
http.Error(w, http.StatusText(statusCode), statusCode)
return
}
req := &dns.Msg{}
if err = req.Unpack(buf); err != nil {
log.Debug("dnsproxy: unpacking http msg: %s", err)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
d := p.newDNSContext(ProtoHTTPS, req)
d.Addr = raddr
d.HTTPRequest = r
d.HTTPResponseWriter = w
if prx.IsValid() {
log.Debug("dnsproxy: request came from proxy server %s", prx)
if !p.TrustedProxies.Contains(prx.Addr()) {
log.Debug("dnsproxy: proxy %s is not trusted, using original remote addr", prx)
d.Addr = prx
// So the address of the proxy itself is used, as the remote address
// parsed from headers cannot be trusted.
//
// TODO(e.burkov): Do not parse headers in this case.
raddr = prx
}
}
d := p.newDNSContext(ProtoHTTPS, req, raddr)
d.HTTPRequest = r
d.HTTPResponseWriter = w
err = p.handleDNSRequest(d)
if err != nil {
log.Debug("dnsproxy: handling dns (%s) request: %s", d.Proto, err)
+1 -2
View File
@@ -237,8 +237,7 @@ func (p *Proxy) handleQUICStream(stream quic.Stream, conn quic.Connection) {
return
}
d := p.newDNSContext(ProtoQUIC, req)
d.Addr = netutil.NetAddrToAddrPort(conn.RemoteAddr())
d := p.newDNSContext(ProtoQUIC, req, netutil.NetAddrToAddrPort(conn.RemoteAddr()))
d.QUICStream = stream
d.QUICConnection = conn
d.DoQVersion = doqVersion
+42 -36
View File
@@ -58,8 +58,8 @@ func (p *Proxy) createTLSListeners() (err error) {
return nil
}
// tcpPacketLoop listens for incoming TCP packets. proto must be either "tcp"
// or "tls".
// tcpPacketLoop listens for incoming TCP packets. proto must be either
// [ProtoTCP] or [ProtoTLS].
//
// See also the comment on Proxy.requestsSema.
func (p *Proxy) tcpPacketLoop(l net.Listener, proto Proto, reqSema syncutil.Semaphore) {
@@ -68,11 +68,7 @@ func (p *Proxy) tcpPacketLoop(l net.Listener, proto Proto, reqSema syncutil.Sema
for {
clientConn, err := l.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
log.Debug("dnsproxy: tcp connection %s closed", l.Addr())
} else {
log.Error("dnsproxy: reading from tcp: %s", err)
}
logTCPConnError(err, l)
break
}
@@ -84,21 +80,25 @@ func (p *Proxy) tcpPacketLoop(l net.Listener, proto Proto, reqSema syncutil.Sema
break
}
go func() {
defer reqSema.Release()
p.handleTCPConnection(clientConn, proto)
}()
go p.handleTCPConnection(clientConn, proto, reqSema)
}
}
// logTCPConnError writes suitable log message for given err.
func logTCPConnError(err error, l net.Listener) {
if errors.Is(err, net.ErrClosed) {
log.Debug("dnsproxy: tcp connection %s closed", l.Addr())
} else {
log.Error("dnsproxy: reading from tcp: %s", err)
}
}
// handleTCPConnection starts a loop that handles an incoming TCP connection.
// proto must be either ProtoTCP or ProtoTLS.
func (p *Proxy) handleTCPConnection(conn net.Conn, proto Proto) {
// proto must be either [ProtoTCP] or [ProtoTLS].
func (p *Proxy) handleTCPConnection(conn net.Conn, proto Proto, reqSema syncutil.Semaphore) {
defer log.OnPanic("proxy.handleTCPConnection")
log.Debug("dnsproxy: handling new %s request from %s", proto, conn.RemoteAddr())
defer reqSema.Release()
defer func() {
err := conn.Close()
if err != nil {
@@ -106,36 +106,21 @@ func (p *Proxy) handleTCPConnection(conn net.Conn, proto Proto) {
}
}()
for {
p.RLock()
if !p.started {
return
}
p.RUnlock()
log.Debug("dnsproxy: handling new %s request from %s", proto, conn.RemoteAddr())
for p.isStarted() {
err := conn.SetDeadline(time.Now().Add(defaultTimeout))
if err != nil {
// Consider deadline errors non-critical.
logWithNonCrit(err, "handling tcp: setting deadline")
}
packet, err := readPrefixed(conn)
if err != nil {
logWithNonCrit(err, "handling tcp: reading msg")
break
}
req := &dns.Msg{}
err = req.Unpack(packet)
if err != nil {
log.Error("dnsproxy: handling tcp: unpacking msg: %s", err)
req := readDNSReq(conn)
if req == nil {
return
}
d := p.newDNSContext(proto, req)
d.Addr = netutil.NetAddrToAddrPort(conn.RemoteAddr())
d := p.newDNSContext(proto, req, netutil.NetAddrToAddrPort(conn.RemoteAddr()))
d.Conn = conn
err = p.handleDNSRequest(d)
@@ -145,6 +130,27 @@ func (p *Proxy) handleTCPConnection(conn net.Conn, proto Proto) {
}
}
// readDNSReq returns DNS request message from the given connection or nil if
// it failed to read it. Properly logs the error if it happened.
func readDNSReq(conn net.Conn) (req *dns.Msg) {
packet, err := readPrefixed(conn)
if err != nil {
logWithNonCrit(err, "handling tcp: reading msg")
return nil
}
req = &dns.Msg{}
err = req.Unpack(packet)
if err != nil {
log.Error("dnsproxy: handling tcp: unpacking msg: %s", err)
return nil
}
return req
}
// errTooLarge means that a DNS message is larger than 64KiB.
const errTooLarge errors.Error = "dns message is too large"
+17 -17
View File
@@ -66,18 +66,13 @@ func (p *Proxy) udpPacketLoop(conn *net.UDPConn, reqSema syncutil.Semaphore) {
log.Info("dnsproxy: entering udp listener loop on %s", conn.LocalAddr())
b := make([]byte, dns.MaxMsgSize)
for {
p.RLock()
if !p.started {
return
}
p.RUnlock()
for p.isStarted() {
n, localIP, remoteAddr, err := proxynetutil.UDPRead(conn, b, p.udpOOBSize)
// documentation says to handle the packet even if err occurs, so do that first
// The documentation says to handle the packet even if err occurs.
if n > 0 {
// make a copy of all bytes because ReadFrom() will overwrite contents of b on next call
// we need the contents to survive the call because we're handling them in goroutine
// Make a copy of all bytes because ReadFrom() will overwrite the
// contents of b on the next call. We need that contents to sustain
// the call because we're handling them in goroutines.
packet := make([]byte, n)
copy(packet, b)
@@ -94,18 +89,24 @@ func (p *Proxy) udpPacketLoop(conn *net.UDPConn, reqSema syncutil.Semaphore) {
p.udpHandlePacket(packet, localIP, remoteAddr, conn)
}()
}
if err != nil {
if errors.Is(err, net.ErrClosed) {
log.Debug("dnsproxy: udp connection %s closed", conn.LocalAddr())
} else {
log.Error("dnsproxy: reading from udp: %s", err)
}
logUDPConnError(err, conn)
break
}
}
}
// logUDPConnError writes suitable log message for given err.
func logUDPConnError(err error, conn *net.UDPConn) {
if errors.Is(err, net.ErrClosed) {
log.Debug("dnsproxy: udp connection %s closed", conn.LocalAddr())
} else {
log.Error("dnsproxy: reading from udp: %s", err)
}
}
// udpHandlePacket processes the incoming UDP packet and sends a DNS response
func (p *Proxy) udpHandlePacket(
packet []byte,
@@ -123,8 +124,7 @@ func (p *Proxy) udpHandlePacket(
return
}
d := p.newDNSContext(ProtoUDP, req)
d.Addr = netutil.NetAddrToAddrPort(remoteAddr)
d := p.newDNSContext(ProtoUDP, req, netutil.NetAddrToAddrPort(remoteAddr))
d.Conn = conn
d.localIP = localIP
+2 -2
View File
@@ -156,12 +156,12 @@ run_linter gocyclo --over 10\
./internal/netutil/\
./internal/version/\
./fastip/\
./proxy/\
./proxyutil/\
./upstream/\
;
run_linter gocyclo --over 20 ./main.go
run_linter gocyclo --over 15 ./proxy/
# TODO(a.garipov): Enable for all.
run_linter gocognit --over 10\
@@ -169,12 +169,12 @@ run_linter gocognit --over 10\
./internal/netutil/\
./internal/version/\
./fastip/\
./proxy/\
./proxyutil/\
./upstream/\
;
run_linter gocognit --over 35 ./main.go
run_linter gocognit --over 17 ./proxy/
run_linter ineffassign ./...