Files
dnsproxy/proxy/serverhttps_internal_test.go
T
Eugene Burkov 95e7ffa668 Pull request #406: Update all
Merge in GO/dnsproxy from upd-all to master

Squashed commit of the following:

commit 5479b35631e98e7f9acf1e9ded3840e37567d3a7
Author: Eugene Burkov <[email protected]>
Date:   Tue Aug 19 15:12:17 2025 +0300

    all: export tests

commit 8b1e94183b69e94cbdd4ee470a6299494c495ea3
Author: Eugene Burkov <[email protected]>
Date:   Tue Aug 19 14:24:07 2025 +0300

    specs: upd build image

commit fa7fd88e88c9f538a92f49a43c1801b294ce3810
Author: Eugene Burkov <[email protected]>
Date:   Tue Aug 19 14:12:18 2025 +0300

    all: gitignore

commit f931ab06ba868c79757c02cbbbd8043fcff28b35
Author: Eugene Burkov <[email protected]>
Date:   Tue Aug 19 14:05:20 2025 +0300

    all: upd scripts

commit bfba1bf9d7f7089bc701757171bc0a1204040830
Author: Eugene Burkov <[email protected]>
Date:   Tue Aug 19 13:40:14 2025 +0300

    all: upd golibs

commit 66f1891fd51b465ec60c4f21ee1f4601ceb0da65
Author: Eugene Burkov <[email protected]>
Date:   Tue Aug 19 12:55:09 2025 +0300

    all: upd go & tools
2025-08-19 15:28:28 +03:00

465 lines
12 KiB
Go

package proxy
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
"testing"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"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/assert"
"github.com/stretchr/testify/require"
)
func TestHttpsProxy(t *testing.T) {
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) {
tlsConf, caPem := newTLSConfig(t)
dnsProxy := mustNew(t, &Config{
Logger: slogutil.NewDiscardLogger(),
TLSListenAddr: []*net.TCPAddr{net.TCPAddrFromAddrPort(localhostAnyPort)},
HTTPSListenAddr: []*net.TCPAddr{net.TCPAddrFromAddrPort(localhostAnyPort)},
QUICListenAddr: []*net.UDPAddr{net.UDPAddrFromAddrPort(localhostAnyPort)},
TLSConfig: tlsConf,
UpstreamConfig: newTestUpstreamConfig(t, defaultTimeout, testDefaultUpstreamAddr),
TrustedProxies: defaultTrustedProxies,
RatelimitSubnetLenIPv4: 24,
RatelimitSubnetLenIPv6: 64,
HTTP3: tc.http3,
})
servicetest.RequireRun(t, dnsProxy, testTimeout)
// 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 := newTestMessage()
// Send the test message and check if the response is what we
// expected.
resp := sendTestDoHMessage(t, client, msg, nil)
requireResponse(t, msg, resp)
})
}
}
func TestProxy_trustedProxies(t *testing.T) {
var (
clientAddr = netip.MustParseAddr("1.2.3.4")
proxyAddr = netip.MustParseAddr("127.0.0.1")
)
doRequest := func(t *testing.T, addr, expectedClientIP netip.Addr) {
// Prepare the proxy server.
tlsConf, caPem := newTLSConfig(t)
dnsProxy := mustNew(t, &Config{
Logger: slogutil.NewDiscardLogger(),
TLSListenAddr: []*net.TCPAddr{net.TCPAddrFromAddrPort(localhostAnyPort)},
HTTPSListenAddr: []*net.TCPAddr{net.TCPAddrFromAddrPort(localhostAnyPort)},
QUICListenAddr: []*net.UDPAddr{net.UDPAddrFromAddrPort(localhostAnyPort)},
TLSConfig: tlsConf,
UpstreamConfig: newTestUpstreamConfig(t, defaultTimeout, testDefaultUpstreamAddr),
TrustedProxies: defaultTrustedProxies,
RatelimitSubnetLenIPv4: 24,
RatelimitSubnetLenIPv6: 64,
})
var gotAddr netip.Addr
dnsProxy.RequestHandler = func(_ *Proxy, d *DNSContext) (err error) {
gotAddr = d.Addr.Addr()
return dnsProxy.Resolve(d)
}
client := createTestHTTPClient(dnsProxy, caPem, false)
msg := newTestMessage()
dnsProxy.TrustedProxies = netip.PrefixFrom(addr, addr.BitLen())
servicetest.RequireRun(t, dnsProxy, testTimeout)
hdrs := map[string]string{
"X-Forwarded-For": strings.Join([]string{clientAddr.String(), proxyAddr.String()}, ","),
}
resp := sendTestDoHMessage(t, client, msg, hdrs)
requireResponse(t, msg, resp)
require.Equal(t, expectedClientIP, gotAddr)
}
t.Run("success", func(t *testing.T) {
doRequest(t, proxyAddr, clientAddr)
})
t.Run("not_in_trusted", func(t *testing.T) {
doRequest(t, netip.MustParseAddr("127.0.0.2"), proxyAddr)
})
}
func TestAddrsFromRequest(t *testing.T) {
var (
theIP = netip.AddrFrom4([4]byte{1, 2, 3, 4})
anotherIP = netip.AddrFrom4([4]byte{1, 2, 3, 5})
theIPStr = theIP.String()
anotherIPStr = anotherIP.String()
)
testCases := []struct {
name string
hdrs map[string]string
wantIP netip.Addr
wantErr string
}{{
name: "cf-connecting-ip",
hdrs: map[string]string{
"CF-Connecting-IP": theIPStr,
},
wantIP: theIP,
wantErr: "",
}, {
name: "true-client-ip",
hdrs: map[string]string{
"True-Client-IP": theIPStr,
},
wantIP: theIP,
wantErr: "",
}, {
name: "x-real-ip",
hdrs: map[string]string{
"X-Real-IP": theIPStr,
},
wantIP: theIP,
wantErr: "",
}, {
name: "no_any",
hdrs: map[string]string{
"CF-Connecting-IP": "invalid",
"True-Client-IP": "invalid",
"X-Real-IP": "invalid",
},
wantIP: netip.Addr{},
wantErr: `ParseAddr(""): unable to parse IP`,
}, {
name: "priority",
hdrs: map[string]string{
"X-Forwarded-For": strings.Join([]string{anotherIPStr, theIPStr}, ","),
"True-Client-IP": anotherIPStr,
"X-Real-IP": anotherIPStr,
"CF-Connecting-IP": theIPStr,
},
wantIP: theIP,
wantErr: "",
}, {
name: "x-forwarded-for_simple",
hdrs: map[string]string{
"X-Forwarded-For": strings.Join([]string{anotherIPStr, theIPStr}, ","),
},
wantIP: anotherIP,
wantErr: "",
}, {
name: "x-forwarded-for_single",
hdrs: map[string]string{
"X-Forwarded-For": theIPStr,
},
wantIP: theIP,
wantErr: "",
}, {
name: "x-forwarded-for_invalid_proxy",
hdrs: map[string]string{
"X-Forwarded-For": strings.Join([]string{theIPStr, "invalid"}, ","),
},
wantIP: theIP,
wantErr: "",
}, {
name: "x-forwarded-for_empty",
hdrs: map[string]string{
"X-Forwarded-For": "",
},
wantIP: netip.Addr{},
wantErr: `ParseAddr(""): unable to parse IP`,
}, {
name: "x-forwarded-for_redundant_spaces",
hdrs: map[string]string{
"X-Forwarded-For": " " + theIPStr + " ,\t" + anotherIPStr,
},
wantIP: theIP,
wantErr: "",
}, {
name: "cf-connecting-ip_redundant_spaces",
hdrs: map[string]string{
"CF-Connecting-IP": " " + theIPStr + "\t",
},
wantIP: theIP,
wantErr: "",
}}
for _, tc := range testCases {
r, err := http.NewRequest(http.MethodGet, "localhost", nil)
require.NoError(t, err)
for h, v := range tc.hdrs {
r.Header.Set(h, v)
}
t.Run(tc.name, func(t *testing.T) {
var ip netip.Addr
ip, err = realIPFromHdrs(r)
testutil.AssertErrorMsg(t, tc.wantErr, err)
assert.Equal(t, tc.wantIP, ip)
})
}
}
func TestRemoteAddr(t *testing.T) {
const thePort = 4321
var (
theIP = netip.AddrFrom4([4]byte{1, 2, 3, 4})
anotherIP = netip.AddrFrom4([4]byte{1, 2, 3, 5})
thirdIP = netip.AddrFrom4([4]byte{1, 2, 3, 6})
theIPStr = theIP.String()
anotherIPStr = anotherIP.String()
thirdIPStr = thirdIP.String()
)
rAddr := netip.AddrPortFrom(theIP, thePort)
testCases := []struct {
name string
remoteAddr string
hdrs map[string]string
wantErr string
wantIP netip.AddrPort
wantProxy netip.AddrPort
}{{
name: "no_proxy",
remoteAddr: rAddr.String(),
hdrs: nil,
wantErr: "",
wantIP: netip.AddrPortFrom(theIP, thePort),
wantProxy: netip.AddrPort{},
}, {
name: "proxied_with_cloudflare",
remoteAddr: rAddr.String(),
hdrs: map[string]string{
"CF-Connecting-IP": anotherIPStr,
},
wantErr: "",
wantIP: netip.AddrPortFrom(anotherIP, 0),
wantProxy: netip.AddrPortFrom(theIP, thePort),
}, {
name: "proxied_once",
remoteAddr: rAddr.String(),
hdrs: map[string]string{
"X-Forwarded-For": anotherIPStr,
},
wantErr: "",
wantIP: netip.AddrPortFrom(anotherIP, 0),
wantProxy: netip.AddrPortFrom(theIP, thePort),
}, {
name: "proxied_multiple",
remoteAddr: rAddr.String(),
hdrs: map[string]string{
"X-Forwarded-For": strings.Join([]string{anotherIPStr, thirdIPStr}, ","),
},
wantErr: "",
wantIP: netip.AddrPortFrom(anotherIP, 0),
wantProxy: netip.AddrPortFrom(theIP, thePort),
}, {
name: "no_port",
remoteAddr: theIPStr,
hdrs: nil,
wantErr: "not an ip:port",
wantIP: netip.AddrPort{},
wantProxy: netip.AddrPort{},
}, {
name: "bad_port",
remoteAddr: theIPStr + ":notport",
hdrs: nil,
wantErr: `invalid port "notport" parsing "1.2.3.4:notport"`,
wantIP: netip.AddrPort{},
wantProxy: netip.AddrPort{},
}, {
name: "bad_host",
remoteAddr: "host:1",
hdrs: nil,
wantErr: `ParseAddr("host"): unable to parse IP`,
wantIP: netip.AddrPort{},
wantProxy: netip.AddrPort{},
}, {
name: "bad_proxied_host",
remoteAddr: "host:1",
hdrs: map[string]string{
"CF-Connecting-IP": theIPStr,
},
wantErr: `ParseAddr("host"): unable to parse IP`,
wantIP: netip.AddrPort{},
wantProxy: netip.AddrPort{},
}}
l := slogutil.NewDiscardLogger()
for _, tc := range testCases {
r, err := http.NewRequest(http.MethodGet, "localhost", nil)
require.NoError(t, err)
r.RemoteAddr = tc.remoteAddr
for h, v := range tc.hdrs {
r.Header.Set(h, v)
}
t.Run(tc.name, func(t *testing.T) {
var addr, prx netip.AddrPort
addr, prx, err = remoteAddr(r, l)
if tc.wantErr != "" {
testutil.AssertErrorMsg(t, tc.wantErr, err)
return
}
require.NoError(t, err)
assert.Equal(t, tc.wantIP, addr)
assert.Equal(t, tc.wantProxy, prx)
})
}
}
// sendTestDoHMessage sends the specified DNS message using client and returns
// the DNS response.
func sendTestDoHMessage(
t *testing.T,
client *http.Client,
m *dns.Msg,
hdrs map[string]string,
) (resp *dns.Msg) {
packed, err := m.Pack()
require.NoError(t, err)
u := url.URL{
Scheme: "https",
Host: 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(t, err)
req.Header.Set("Content-Type", "application/dns-message")
req.Header.Set("Accept", "application/dns-message")
for k, v := range hdrs {
req.Header.Set(k, v)
}
httpResp, err := client.Do(req) // nolint:bodyclose
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, httpResp.Body.Close)
require.True(
t,
httpResp.ProtoAtLeast(2, 0),
"the proto is too old: %s",
httpResp.Proto,
)
body, err := io.ReadAll(httpResp.Body)
require.NoError(t, err)
resp = &dns.Msg{}
err = resp.Unpack(body)
require.NoError(t, err)
return resp
}
// createTestHTTPClient creates an *http.Client that will be used to send
// requests to the specified dnsProxy.
func createTestHTTPClient(dnsProxy *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: 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(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(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,
}
}