mirror of
https://github.com/AdguardTeam/dnsproxy.git
synced 2026-10-12 02:01:26 +00:00
397 lines
10 KiB
Go
397 lines
10 KiB
Go
package proxy_test
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"net/url"
|
|
"testing"
|
|
|
|
proxytest "github.com/AdguardTeam/dnsproxy/dnsproxytest"
|
|
"github.com/AdguardTeam/dnsproxy/internal/dnsproxytest"
|
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
|
"github.com/AdguardTeam/golibs/httphdr"
|
|
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
|
"github.com/AdguardTeam/golibs/testutil"
|
|
"github.com/AdguardTeam/golibs/testutil/servicetest"
|
|
"github.com/miekg/dns"
|
|
"github.com/quic-go/quic-go"
|
|
"github.com/quic-go/quic-go/http3"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// Common address strings for tests.
|
|
const (
|
|
testClientAddrStr1 = "192.0.2.1"
|
|
testClientAddrStr2 = "192.0.2.2"
|
|
testProxyAddrStr = "127.0.0.1"
|
|
testUntrustedProxyAddrStr = "127.0.0.2"
|
|
)
|
|
|
|
// Common addresses for tests.
|
|
var (
|
|
testClientAddr1 = netip.MustParseAddr(testClientAddrStr1)
|
|
testClientAddr2 = netip.MustParseAddr(testClientAddrStr2)
|
|
testProxyAddr = netip.MustParseAddr(testProxyAddrStr)
|
|
testUntrustedProxyAddr = netip.MustParseAddr(testUntrustedProxyAddrStr)
|
|
)
|
|
|
|
func TestProxy_HandleDNSRequest_https(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
http3 bool
|
|
}{{
|
|
name: "https_proxy",
|
|
http3: false,
|
|
}, {
|
|
name: "h3_proxy",
|
|
http3: true,
|
|
}}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tlsConf, caPem := dnsproxytest.NewTLSConfig(t)
|
|
|
|
httpConf := &proxy.HTTPConfig{
|
|
ListenAddresses: []netip.AddrPort{dnsproxytest.LocalhostAnyPort},
|
|
HTTP3Enabled: tc.http3,
|
|
}
|
|
|
|
tlsListenAddr := dnsproxytest.LocalhostAnyPortTCP
|
|
quicListenAddr := dnsproxytest.LocalhostAnyPortUDP
|
|
dnsProxy, err := proxy.New(&proxy.Config{
|
|
Logger: testLogger,
|
|
TLSListenAddr: []*net.TCPAddr{tlsListenAddr},
|
|
QUICListenAddr: []*net.UDPAddr{quicListenAddr},
|
|
TLSConfig: tlsConf,
|
|
UpstreamConfig: &proxy.UpstreamConfig{
|
|
Upstreams: []upstream.Upstream{newTestUpstream(t)},
|
|
},
|
|
TrustedProxies: dnsproxytest.DefaultTrustedProxies,
|
|
HTTPConfig: httpConf,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
servicetest.RequireRun(t, dnsProxy, dnsproxytest.Timeout)
|
|
|
|
// Create the HTTP client that we'll be using for this test.
|
|
client := createTestHTTPClient(dnsProxy, caPem, tc.http3)
|
|
|
|
// Prepare a test message to be sent to the server.
|
|
msg := dnsproxytest.NewTestRequest()
|
|
|
|
// Send the test message and check if the response is what we
|
|
// expected.
|
|
resp := sendTestDoHMessage(t, client, msg, nil)
|
|
dnsproxytest.RequireResponse(t, msg, resp)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProxy_HandleDNSRequest_trustedProxies(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
trustedProxy netip.Addr
|
|
wantClientIP netip.Addr
|
|
name string
|
|
}{{
|
|
name: "success",
|
|
trustedProxy: testProxyAddr,
|
|
wantClientIP: testClientAddr1,
|
|
}, {
|
|
name: "not_in_trusted",
|
|
trustedProxy: testUntrustedProxyAddr,
|
|
wantClientIP: testProxyAddr,
|
|
}}
|
|
|
|
hdr := http.Header{
|
|
httphdr.XForwardedFor: []string{testClientAddrStr1 + "," + testProxyAddrStr},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testProxyRealIPDetection(t, tc.trustedProxy, hdr, tc.wantClientIP)
|
|
})
|
|
}
|
|
}
|
|
|
|
// testProxyRealIPDetection starts a proxy and checks the client IP detected
|
|
// from hdr. trustedProxy and wantClientIP must be valid.
|
|
func testProxyRealIPDetection(
|
|
tb testing.TB,
|
|
trustedProxy netip.Addr,
|
|
hdr http.Header,
|
|
wantClientIP netip.Addr,
|
|
) {
|
|
var gotAddr netip.Addr
|
|
reqHandler := &proxytest.Handler{
|
|
OnHandle: func(ctx context.Context, p *proxy.Proxy, d *proxy.DNSContext) (err error) {
|
|
gotAddr = d.Addr.Addr()
|
|
|
|
return p.Resolve(ctx, d)
|
|
},
|
|
}
|
|
|
|
tlsConf, caPem := dnsproxytest.NewTLSConfig(tb)
|
|
httpConf := &proxy.HTTPConfig{
|
|
ListenAddresses: []netip.AddrPort{dnsproxytest.LocalhostAnyPort},
|
|
}
|
|
trustedProxies := netip.PrefixFrom(trustedProxy, trustedProxy.BitLen())
|
|
dnsProxy, err := proxy.New(&proxy.Config{
|
|
Logger: testLogger,
|
|
UpstreamConfig: &proxy.UpstreamConfig{
|
|
Upstreams: []upstream.Upstream{newTestUpstream(tb)},
|
|
},
|
|
TrustedProxies: trustedProxies,
|
|
RequestHandler: reqHandler,
|
|
TLSConfig: tlsConf,
|
|
TLSListenAddr: []*net.TCPAddr{dnsproxytest.LocalhostAnyPortTCP},
|
|
QUICListenAddr: []*net.UDPAddr{dnsproxytest.LocalhostAnyPortUDP},
|
|
HTTPConfig: httpConf,
|
|
})
|
|
require.NoError(tb, err)
|
|
|
|
client := createTestHTTPClient(dnsProxy, caPem, false)
|
|
msg := dnsproxytest.NewTestRequest()
|
|
|
|
servicetest.RequireRun(tb, dnsProxy, dnsproxytest.Timeout)
|
|
|
|
resp := sendTestDoHMessage(tb, client, msg, hdr)
|
|
dnsproxytest.RequireResponse(tb, msg, resp)
|
|
|
|
require.Equal(tb, wantClientIP, gotAddr)
|
|
}
|
|
|
|
func TestProxy_HandleDNSRequest_realIPFromHeader(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
wantClientIP netip.Addr
|
|
hdr http.Header
|
|
name string
|
|
}{{
|
|
name: "cf-connecting-ip",
|
|
hdr: http.Header{
|
|
httphdr.CFConnectingIP: []string{testClientAddrStr1},
|
|
},
|
|
wantClientIP: testClientAddr1,
|
|
}, {
|
|
name: "true-client-ip",
|
|
hdr: http.Header{
|
|
httphdr.TrueClientIP: []string{testClientAddrStr1},
|
|
},
|
|
wantClientIP: testClientAddr1,
|
|
}, {
|
|
name: "x-real-ip",
|
|
hdr: http.Header{
|
|
httphdr.XRealIP: []string{testClientAddrStr1},
|
|
},
|
|
wantClientIP: testClientAddr1,
|
|
}, {
|
|
name: "cf-connecting-ip_redundant_spaces",
|
|
hdr: http.Header{
|
|
httphdr.CFConnectingIP: []string{" " + testClientAddrStr1 + "\t"},
|
|
},
|
|
wantClientIP: testClientAddr1,
|
|
}, {
|
|
name: "no_any",
|
|
hdr: http.Header{
|
|
httphdr.CFConnectingIP: []string{"invalid"},
|
|
httphdr.TrueClientIP: []string{"invalid"},
|
|
httphdr.XRealIP: []string{"invalid"},
|
|
},
|
|
wantClientIP: testProxyAddr,
|
|
}, {
|
|
name: "priority",
|
|
hdr: http.Header{
|
|
httphdr.XForwardedFor: []string{testClientAddrStr2 + "," + testClientAddrStr1},
|
|
httphdr.TrueClientIP: []string{testClientAddrStr2},
|
|
httphdr.XRealIP: []string{testClientAddrStr2},
|
|
httphdr.CFConnectingIP: []string{testClientAddrStr1},
|
|
},
|
|
wantClientIP: testClientAddr1,
|
|
}}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testProxyRealIPDetection(t, testProxyAddr, tc.hdr, tc.wantClientIP)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProxy_HandleDNSRequest_realIPFromHeaderXFF(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
wantClientIP netip.Addr
|
|
hdr http.Header
|
|
name string
|
|
}{{
|
|
name: "x-forwarded-for_simple",
|
|
hdr: http.Header{
|
|
httphdr.XForwardedFor: []string{testClientAddrStr2 + "," + testClientAddrStr1},
|
|
},
|
|
wantClientIP: testClientAddr2,
|
|
}, {
|
|
name: "x-forwarded-for_single",
|
|
hdr: http.Header{
|
|
httphdr.XForwardedFor: []string{testClientAddrStr1},
|
|
},
|
|
wantClientIP: testClientAddr1,
|
|
}, {
|
|
name: "x-forwarded-for_invalid_proxy",
|
|
hdr: http.Header{
|
|
httphdr.XForwardedFor: []string{testClientAddrStr1 + ",invalid"},
|
|
},
|
|
wantClientIP: testClientAddr1,
|
|
}, {
|
|
name: "x-forwarded-for_empty",
|
|
hdr: http.Header{
|
|
httphdr.XForwardedFor: []string{""},
|
|
},
|
|
wantClientIP: testProxyAddr,
|
|
}, {
|
|
name: "x-forwarded-for_redundant_spaces",
|
|
hdr: http.Header{
|
|
httphdr.XForwardedFor: []string{
|
|
" " + testClientAddrStr1 + " ,\t" + testClientAddrStr2,
|
|
},
|
|
},
|
|
wantClientIP: testClientAddr1,
|
|
}}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testProxyRealIPDetection(t, testProxyAddr, tc.hdr, tc.wantClientIP)
|
|
})
|
|
}
|
|
}
|
|
|
|
// createTestHTTPClient creates an *http.Client that will be used to send
|
|
// requests to the specified dnsProxy.
|
|
func createTestHTTPClient(
|
|
dnsProxy *proxy.Proxy,
|
|
caPem []byte,
|
|
http3Enabled bool,
|
|
) (client *http.Client) {
|
|
// prepare roots list so that the server cert was successfully validated.
|
|
roots := x509.NewCertPool()
|
|
roots.AppendCertsFromPEM(caPem)
|
|
tlsClientConfig := &tls.Config{
|
|
ServerName: dnsproxytest.TLSServerName,
|
|
RootCAs: roots,
|
|
}
|
|
|
|
var transport http.RoundTripper
|
|
|
|
if http3Enabled {
|
|
tlsClientConfig.NextProtos = []string{"h3"}
|
|
|
|
transport = &http3.Transport{
|
|
Dial: func(
|
|
ctx context.Context,
|
|
_ string,
|
|
tlsCfg *tls.Config,
|
|
cfg *quic.Config,
|
|
) (*quic.Conn, error) {
|
|
addr := dnsProxy.Addr(proxy.ProtoHTTPS).String()
|
|
|
|
return quic.DialAddrEarly(ctx, addr, tlsCfg, cfg)
|
|
},
|
|
TLSClientConfig: tlsClientConfig,
|
|
QUICConfig: &quic.Config{},
|
|
DisableCompression: true,
|
|
}
|
|
} else {
|
|
dialer := &net.Dialer{
|
|
Timeout: defaultTimeout,
|
|
}
|
|
dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
// Route request to the DNS-over-HTTPS server address.
|
|
return dialer.DialContext(ctx, network, dnsProxy.Addr(proxy.ProtoHTTPS).String())
|
|
}
|
|
|
|
tlsClientConfig.NextProtos = []string{"h2", "http/1.1"}
|
|
transport = &http.Transport{
|
|
TLSClientConfig: tlsClientConfig,
|
|
DisableCompression: true,
|
|
DialContext: dialContext,
|
|
ForceAttemptHTTP2: true,
|
|
}
|
|
}
|
|
|
|
return &http.Client{
|
|
Transport: transport,
|
|
Timeout: defaultTimeout,
|
|
}
|
|
}
|
|
|
|
// sendTestDoHMessage sends the specified DNS message using client and returns
|
|
// the DNS response.
|
|
func sendTestDoHMessage(
|
|
tb testing.TB,
|
|
client *http.Client,
|
|
m *dns.Msg,
|
|
hdr http.Header,
|
|
) (resp *dns.Msg) {
|
|
tb.Helper()
|
|
|
|
packed, err := m.Pack()
|
|
require.NoError(tb, err)
|
|
|
|
u := url.URL{
|
|
Scheme: urlutil.SchemeHTTPS,
|
|
Host: dnsproxytest.TLSServerName,
|
|
Path: "/dns-query",
|
|
RawQuery: fmt.Sprintf("dns=%s", base64.RawURLEncoding.EncodeToString(packed)),
|
|
}
|
|
|
|
method := http.MethodGet
|
|
if _, ok := client.Transport.(*http3.Transport); ok {
|
|
// If we're using HTTP/3, use http3.MethodGet0RTT to force using 0-RTT.
|
|
method = http3.MethodGet0RTT
|
|
}
|
|
|
|
req, err := http.NewRequest(method, u.String(), nil)
|
|
require.NoError(tb, err)
|
|
|
|
maps.Copy(req.Header, hdr)
|
|
|
|
req.Header.Set("Content-Type", "application/dns-message")
|
|
req.Header.Set("Accept", "application/dns-message")
|
|
|
|
httpResp, err := client.Do(req)
|
|
require.NoError(tb, err)
|
|
testutil.CleanupAndRequireSuccess(tb, httpResp.Body.Close)
|
|
|
|
require.True(tb, httpResp.ProtoAtLeast(2, 0))
|
|
|
|
body, err := io.ReadAll(httpResp.Body)
|
|
require.NoError(tb, err)
|
|
|
|
resp = &dns.Msg{}
|
|
err = resp.Unpack(body)
|
|
require.NoError(tb, err)
|
|
|
|
return resp
|
|
}
|