plugin/forward: support DNS-over-QUIC upstreams (#8474)

Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com>
This commit is contained in:
houyuwushang
2026-09-08 12:51:18 +08:00
committed by GitHub
parent 71a60e140b
commit e1d3fe6bc6
13 changed files with 1679 additions and 40 deletions

View File

@@ -30,7 +30,7 @@ forward FROM TO...
* **FROM** is the base domain to match for the request to be forwarded. Domains using CIDR notation
that expand to multiple reverse zones are not fully supported; only the first expanded zone is used.
* **TO...** are the destination endpoints to forward to. The **TO** syntax allows you to specify
a protocol, `tls://9.9.9.9`, `https://9.9.9.9` (DoH defaults to `/dns-query` path) or `dns://` (or no protocol)
a protocol, `tls://9.9.9.9`, `quic://94.140.14.14`, `https://9.9.9.9` (DoH defaults to `/dns-query` path) or `dns://` (or no protocol)
for plain DNS. The number of upstreams is limited to 15. In addition to IP addresses and files (like `/etc/resolv.conf`), **TO** can also be
a hostname (e.g., `my-dns.svc.cluster.local`). Hostnames are resolved to IP addresses at startup.
See the `resolver` option below.
@@ -46,6 +46,7 @@ forward FROM TO... {
force_tcp
prefer_udp
expire DURATION
max_age DURATION
max_idle_conns INTEGER
read_timeout DURATION
max_fails INTEGER
@@ -70,7 +71,8 @@ forward FROM TO... {
* `force_tcp`, use TCP even when the request comes in over UDP.
* `prefer_udp`, try first using UDP even when the request comes in over TCP. If response is truncated
(TC flag set in response) then do another attempt over TCP. In case if both `force_tcp` and
`prefer_udp` options specified the `force_tcp` takes precedence.
`prefer_udp` options specified the `force_tcp` takes precedence. These options do not change an
explicitly configured DoT, DoQ, or DoH upstream transport.
* `max_fails` is the number of subsequent failed health checks that are needed before considering
an upstream to be down. If 0, the upstream will never be marked as down (nor health checked).
Default is 2.
@@ -79,9 +81,11 @@ forward FROM TO... {
configured upstreams, allowing two complete passes when all upstreams are healthy.
Set this to 0 to disable the per-request cap.
* `expire` **DURATION**, expire (cached) connections after this time, the default is 10s.
* `max_age` **DURATION**, stop reusing and replace connections after this total lifetime.
The default is 0, which disables maximum connection age. A non-zero value must not be less than `expire`.
* `doh_method` **GET|POST**, whether to use GET or POST http method for DoH requests (defaults to POST).
* `max_idle_conns` **INTEGER**, maximum number of idle connections to cache per upstream for reuse.
Default is 0, which means unlimited.
Default is 0, which means unlimited. DoQ multiplexes streams over one cached connection per upstream.
* `read_timeout` **DURATION**, the per-query read timeout applied to each upstream when waiting for a
response. The default is 2s. Increase this if upstreams legitimately take longer than 2s to answer
(for example slow recursive resolutions that would otherwise surface as `SERVFAIL`/timeouts). Note
@@ -97,17 +101,17 @@ forward FROM TO... {
* `tls` **CERT** **KEY** **CA** - client authentication is used with the specified cert/key pair.
The server certificate is verified using the specified CA file
CoreDNS sets the minimum TLS version to TLS 1.2. The maximum TLS version, TLS 1.2 cipher suites, and
key exchange mechanisms use the Go `crypto/tls` defaults.
CoreDNS sets the minimum TLS version to TLS 1.2 for DoT and DoH. DoQ uses TLS 1.3 as required by QUIC.
The maximum TLS version, TLS 1.2 cipher suites, and key exchange mechanisms use the Go `crypto/tls` defaults.
* `tls_servername` **NAME** allows you to set a server name in the TLS configuration; for instance 9.9.9.9
needs this to be set to `dns.quad9.net`. Using TLS forwarding but not setting `tls_servername` results in anyone
being able to man-in-the-middle your connection to the DNS server you are forwarding to. Because of this,
it is strongly recommended to set this value when using TLS forwarding.
needs this to be set to `dns.quad9.net`. It is strongly recommended when using DoT, DoQ, or DoH with
an IP address whose certificate identifies a DNS name instead of that IP address.
Per destination endpoint TLS server name indication is possible in the form of `tls://9.9.9.9%dns.quad9.net`.
Per destination endpoint TLS server name indication is possible in the form of `tls://9.9.9.9%dns.quad9.net`
or `quic://9.9.9.9%dns.quad9.net`.
`tls_servername` must not be specified when using per destination endpoint TLS server name indication
as it would introduce clash between the server name indication spectifications. If destination endpoint
as it would introduce a clash between server name indication specifications. If destination endpoint
is to be reached via a port other than 853 then the port must be appended to the end of the destination
endpoint specifier. In case of port 10853, the above string would be: `tls://9.9.9.9%dns.quad9.net:10853`.
@@ -133,12 +137,13 @@ key exchange mechanisms use the Go `crypto/tls` defaults.
* `source_address` **IP** - set the address to use for all outgoing requests as source address (also health check query). This works reliably when upstream servers are reachable from that address. However, if upstream servers belong to different networks, care must be taken. The selected source address may not be valid for all upstreams, and responses may fail if return routing is not properly configured. In such cases, make sure that upstream servers have a route back to the configured source address.
* `resolver` **IP[:PORT] [IP[:PORT]...]** specifies one or more DNS resolver addresses used to resolve hostname-based **TO** endpoints at startup. If not specified, the system resolver (`/etc/resolv.conf`) is used. Each address is either a bare IP (IPv4 or IPv6, port 53 assumed) or `IP:port`. Multiple addresses can be specified for redundancy.
Also note the TLS config is "global" for the whole forwarding proxy if you need a different
`tls_servername` for different upstreams you're out of luck.
The client certificate, key, and CA configuration is global for one `forward` stanza. For DoT and DoQ,
use the `%servername` endpoint form when upstreams in the same stanza require different TLS server names.
On each endpoint, the timeouts for communication are set as follows:
* The dial timeout by default is 30s, and can decrease automatically down to 1s based on early results.
* The DNS and DoT dial timeout defaults to 30s and can decrease automatically down to 1s based on early results.
The DoQ handshake timeout is 5s.
* The read timeout is static at 2s.
## Metadata
@@ -162,7 +167,7 @@ If monitoring is enabled (via the *prometheus* plugin) then the following metric
* `coredns_proxy_conn_cache_misses_total{proxy_name="forward", to, proto}` - count of connection cache misses per upstream and protocol.
Where `to` is one of the upstream servers (**TO** from the config), `rcode` is the returned RCODE
from the upstream, `proto` is the transport protocol like `udp`, `tcp`, `tcp-tls`, `https`.
from the upstream, `proto` is the transport protocol like `udp`, `tcp`, `tcp-tls`, `quic`, `https`.
The following metrics have recently been deprecated:
* `coredns_forward_healthcheck_failures_total{to, rcode}`
@@ -261,6 +266,20 @@ service with health checks.
}
~~~
The following example uses DNS-over-QUIC (DoQ). DoQ uses UDP port 853 by default and multiplexes
concurrent queries over separate streams on one QUIC connection. The `forward` plugin's single-message
exchange path does not support AXFR or IXFR over DoQ; those requests return `NOTIMP`.
~~~ corefile
. {
forward . quic://94.140.14.14 {
tls_servername dns.adguard-dns.com
health_check 5s
}
cache 30
}
~~~
The same configuration but using DNS-over-HTTPS (DoH) protocol. Note that the implementation uses the default `/dns-query`
path (custom paths are not supported).
@@ -359,3 +378,5 @@ Forward to an upstream identified by hostname, using a specific resolver to look
[RFC 7858](https://tools.ietf.org/html/rfc7858) for DNS over TLS.
[RFC 8484](https://tools.ietf.org/html/rfc8484) for DNS over HTTPS.
[RFC 9250](https://www.rfc-editor.org/rfc/rfc9250.html) for DNS over QUIC.

196
plugin/forward/doq_test.go Normal file
View File

@@ -0,0 +1,196 @@
package forward
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/binary"
"errors"
"fmt"
"io"
"math/big"
"testing"
"time"
"github.com/coredns/caddy"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/test"
"github.com/miekg/dns"
"github.com/quic-go/quic-go"
)
func TestForwardDoQIntegration(t *testing.T) {
serverTLS, roots := makeForwardDoQTestTLS(t)
listener, err := quic.ListenAddr("127.0.0.1:0", serverTLS, &quic.Config{MaxIncomingStreams: 16})
if err != nil {
t.Fatalf("quic.ListenAddr() failed: %v", err)
}
defer listener.Close()
serverResult := make(chan error, 1)
go func() {
conn, err := listener.Accept(context.Background())
if err != nil {
serverResult <- err
return
}
if got := conn.ConnectionState().TLS.NegotiatedProtocol; got != "doq" {
serverResult <- fmt.Errorf("negotiated ALPN = %q, want doq", got)
return
}
stream, err := conn.AcceptStream(context.Background())
if err != nil {
serverResult <- err
return
}
_ = stream.SetDeadline(time.Now().Add(2 * time.Second))
query, err := readForwardDoQMessage(stream)
if err != nil {
serverResult <- err
return
}
var extra [1]byte
if n, err := stream.Read(extra[:]); n != 0 || !errors.Is(err, io.EOF) {
serverResult <- fmt.Errorf("query stream did not end with FIN: n=%d err=%v", n, err)
return
}
if query.Id != 0 {
serverResult <- fmt.Errorf("query ID = %d, want 0", query.Id)
return
}
response := new(dns.Msg)
response.SetReply(query)
record, err := dns.NewRR("example.org. 60 IN A 192.0.2.53")
if err != nil {
serverResult <- err
return
}
response.Answer = []dns.RR{record}
wire, err := response.Pack()
if err != nil {
serverResult <- err
return
}
frame := make([]byte, 2+len(wire))
binary.BigEndian.PutUint16(frame, uint16(len(wire))) // #nosec G115 -- DNS wire size is bounded by Pack
copy(frame[2:], wire)
for len(frame) > 0 {
n, err := stream.Write(frame)
if err != nil {
serverResult <- err
return
}
if n == 0 {
serverResult <- io.ErrShortWrite
return
}
frame = frame[n:]
}
if err := stream.Close(); err != nil {
serverResult <- err
return
}
serverResult <- nil
}()
c := caddy.NewTestController("dns", fmt.Sprintf(`forward . quic://%s {
tls_servername doq.test
}`, listener.Addr()))
fs, err := parseForward(c)
if err != nil {
t.Fatalf("parseForward() failed: %v", err)
}
f := fs[0]
clientTLS := f.proxies[0].GetTransport().GetTLSConfig().Clone()
clientTLS.RootCAs = roots
f.proxies[0].SetTLSConfig(clientTLS)
if err := f.OnStartup(); err != nil {
t.Fatalf("OnStartup() failed: %v", err)
}
defer f.OnShutdown()
query := new(dns.Msg)
query.SetQuestion("example.org.", dns.TypeA)
query.Id = 0x4321
recorder := dnstest.NewRecorder(&test.ResponseWriter{})
if _, err := f.ServeDNS(context.Background(), recorder, query); err != nil {
t.Fatalf("ServeDNS() failed: %v", err)
}
if recorder.Msg == nil || recorder.Msg.Id != 0x4321 {
t.Fatalf("response ID = %v, want %d", recorder.Msg, 0x4321)
}
if len(recorder.Msg.Answer) != 1 || recorder.Msg.Answer[0].String() != "example.org.\t60\tIN\tA\t192.0.2.53" {
t.Fatalf("unexpected response answers: %v", recorder.Msg.Answer)
}
select {
case err := <-serverResult:
if err != nil {
t.Fatalf("DoQ upstream failed: %v", err)
}
case <-time.After(3 * time.Second):
t.Fatal("DoQ upstream did not finish")
}
transfer := new(dns.Msg)
transfer.SetQuestion("example.org.", dns.TypeAXFR)
rcode, err := f.ServeDNS(context.Background(), &test.ResponseWriter{}, transfer)
if rcode != dns.RcodeNotImplemented || err == nil {
t.Fatalf("AXFR over DoQ returned rcode=%d err=%v, want NOTIMP with an error", rcode, err)
}
}
func readForwardDoQMessage(r io.Reader) (*dns.Msg, error) {
var size [2]byte
if _, err := io.ReadFull(r, size[:]); err != nil {
return nil, err
}
wire := make([]byte, int(binary.BigEndian.Uint16(size[:])))
if len(wire) == 0 {
return nil, errors.New("zero-length DoQ message")
}
if _, err := io.ReadFull(r, wire); err != nil {
return nil, err
}
msg := new(dns.Msg)
if err := msg.Unpack(wire); err != nil {
return nil, err
}
return msg, nil
}
func makeForwardDoQTestTLS(t *testing.T) (*tls.Config, *x509.CertPool) {
t.Helper()
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("ecdsa.GenerateKey() failed: %v", err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "doq.test"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: []string{"doq.test"},
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
if err != nil {
t.Fatalf("x509.CreateCertificate() failed: %v", err)
}
parsed, err := x509.ParseCertificate(der)
if err != nil {
t.Fatalf("x509.ParseCertificate() failed: %v", err)
}
roots := x509.NewCertPool()
roots.AddCert(parsed)
return &tls.Config{
Certificates: []tls.Certificate{{Certificate: [][]byte{der}, PrivateKey: privateKey}},
NextProtos: []string{"doq"},
}, roots
}

View File

@@ -1,7 +1,6 @@
// Package forward implements a forwarding proxy. It caches an upstream net.Conn for some time, so if the same
// client returns the upstream's Conn will be precached. Depending on how you benchmark this looks to be
// 50% faster than just opening a new connection for every client. It works with UDP and TCP and uses
// inband healthchecking.
// Package forward implements a DNS forwarding proxy. It reuses upstream
// connections across DNS, DoT, DoH, and DoQ transports and uses in-band
// health checking.
package forward
import (
@@ -190,7 +189,7 @@ func (f *Forward) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg
for {
ret, localAddr, upstreamProto, err = proxy.Connect(ctx, state, opts)
if err == proxyPkg.ErrCachedClosed { // Remote side closed conn, can only happen with TCP.
if err == proxyPkg.ErrCachedClosed { // The peer closed a cached TCP or QUIC connection before the query was sent.
continue
}
// Retry with TCP if truncated and prefer_udp configured.
@@ -215,6 +214,9 @@ func (f *Forward) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg
if errors.Is(err, proxyPkg.ErrInvalidRequest) {
return dns.RcodeFormatError, err
}
if errors.Is(err, proxyPkg.ErrUnsupportedRequest) {
return dns.RcodeNotImplemented, err
}
// Kick off health check to see if *our* upstream is broken.
if f.maxfails != 0 {

View File

@@ -17,7 +17,7 @@ import (
type hostEntry struct {
hostname string // the hostname to resolve (e.g., "rbldnsd.rbldnsd.svc.cluster.local")
port string // port (e.g., "53", "443", "853")
transport string // "dns", "tls", or "https"
transport string // "dns", "tls", "quic", or "https"
zone string // TLS server name zone (from %zone syntax)
}
@@ -67,8 +67,8 @@ func parseAsHostEntry(h string) (hostEntry, bool) {
cleanH, zone := splitZone(h)
trans, host := parse.Transport(cleanH)
// Only dns, tls, and https transports are supported for hostname resolution
if trans != transport.DNS && trans != transport.TLS && trans != transport.HTTPS {
// Only forward-supported transports are accepted for hostname resolution.
if trans != transport.DNS && trans != transport.TLS && trans != transport.QUIC && trans != transport.HTTPS {
return hostEntry{}, false
}
@@ -77,6 +77,8 @@ func parseAsHostEntry(h string) (hostEntry, bool) {
switch trans {
case transport.TLS:
port = transport.TLSPort
case transport.QUIC:
port = transport.QUICPort
case transport.HTTPS:
port = transport.HTTPSPort
}
@@ -126,8 +128,7 @@ func expandAndDedup(entries []toEntry, resolvers []string) ([]string, error) {
}
for _, addr := range addrs {
// Normalize the address for dedup comparison
key := normalizeAddr(addr)
key := dedupKey(addr)
if !seen[key] {
seen[key] = true
result = append(result, addr)
@@ -137,8 +138,16 @@ func expandAndDedup(entries []toEntry, resolvers []string) ([]string, error) {
return result, nil
}
// normalizeAddr extracts the canonical IP:port from an address string
// (stripping transport prefix and zone) for deduplication.
// dedupKey identifies an upstream endpoint without collapsing distinct
// transports or TLS server names that happen to use the same IP and port.
func dedupKey(addr string) string {
host, zone := splitZone(addr)
trans, endpoint := parse.Transport(host)
return trans + "\x00" + endpoint + "\x00" + strings.ToLower(zone)
}
// normalizeAddr extracts the IP:port from an address string, stripping its
// transport prefix and TLS server name.
func normalizeAddr(addr string) string {
host, _ := splitZone(addr)
_, h := parse.Transport(host)
@@ -164,7 +173,7 @@ func formatResolvedAddr(ip, port, trans, zone string) string {
isIPv6 := strings.Contains(ip, ":")
switch trans {
case transport.TLS, transport.HTTPS:
case transport.TLS, transport.QUIC, transport.HTTPS:
if zone != "" {
if isIPv6 {
return trans + "://[" + ip + "%" + zone + "]:" + port

View File

@@ -3,6 +3,7 @@ package forward
import (
"fmt"
"os"
"reflect"
"strings"
"testing"
@@ -171,6 +172,9 @@ func TestParseAsHostEntry(t *testing.T) {
{"tls://dns.example.com", true, "dns.example.com", "853", transport.TLS, ""},
{"tls://dns.example.com:8853", true, "dns.example.com", "8853", transport.TLS, ""},
{"tls://dns.example.com%servername.example.com", true, "dns.example.com", "853", transport.TLS, "servername.example.com"},
{"quic://dns.example.com", true, "dns.example.com", "853", transport.QUIC, ""},
{"quic://dns.example.com:8853", true, "dns.example.com", "8853", transport.QUIC, ""},
{"quic://dns.example.com%servername.example.com", true, "dns.example.com", "853", transport.QUIC, "servername.example.com"},
{"https://dns.example.com", true, "dns.example.com", "443", transport.HTTPS, ""},
{"https://dns.example.com:8443", true, "dns.example.com", "8443", transport.HTTPS, ""},
{"https://dns.example.com%servername.example.com", true, "dns.example.com", "443", transport.HTTPS, "servername.example.com"},
@@ -217,11 +221,15 @@ func TestFormatResolvedAddr(t *testing.T) {
{"10.0.0.1", "53", transport.DNS, "", "10.0.0.1:53"},
{"10.0.0.1", "853", transport.TLS, "", "tls://10.0.0.1:853"},
{"10.0.0.1", "853", transport.TLS, "example.com", "tls://10.0.0.1%example.com:853"},
{"10.0.0.1", "853", transport.QUIC, "", "quic://10.0.0.1:853"},
{"10.0.0.1", "853", transport.QUIC, "example.com", "quic://10.0.0.1%example.com:853"},
{"10.0.0.1", "443", transport.HTTPS, "", "https://10.0.0.1:443"},
{"10.0.0.1", "443", transport.HTTPS, "example.com", "https://10.0.0.1%example.com:443"},
{"::1", "53", transport.DNS, "", "[::1]:53"},
{"::1", "853", transport.TLS, "", "tls://[::1]:853"},
{"::1", "853", transport.TLS, "example.com", "tls://[::1%example.com]:853"},
{"::1", "853", transport.QUIC, "", "quic://[::1]:853"},
{"::1", "853", transport.QUIC, "example.com", "quic://[::1%example.com]:853"},
{"::1", "443", transport.HTTPS, "", "https://[::1]:443"},
{"::1", "443", transport.HTTPS, "example.com", "https://[::1%example.com]:443"},
}
@@ -610,6 +618,30 @@ func TestExpandAndDedupTLS(t *testing.T) {
}
}
func TestExpandAndDedupKeepsDistinctDoTAndDoQEndpoints(t *testing.T) {
entries := []toEntry{
{static: true, addrs: []string{"tls://192.0.2.1:853"}},
{static: true, addrs: []string{"quic://192.0.2.1:853"}},
{static: true, addrs: []string{"quic://192.0.2.1%doq.example:853"}},
{static: true, addrs: []string{"quic://192.0.2.1%DOQ.EXAMPLE:853"}},
{static: true, addrs: []string{"dns://192.0.2.2:53", "192.0.2.2:53"}},
}
result, err := expandAndDedup(entries, nil)
if err != nil {
t.Fatalf("expandAndDedup() failed: %v", err)
}
want := []string{
"tls://192.0.2.1:853",
"quic://192.0.2.1:853",
"quic://192.0.2.1%doq.example:853",
"dns://192.0.2.2:53",
}
if !reflect.DeepEqual(result, want) {
t.Fatalf("expandAndDedup() = %v, want %v", result, want)
}
}
func TestResolverWithHCOptions(t *testing.T) {
input := "forward . 127.0.0.1 {\nresolver 10.96.0.10\n}\n"

View File

@@ -186,7 +186,12 @@ func parseStanza(c *caddy.Controller) (*Forward, error) {
tlsServerNames := make([]string, len(toHosts))
perServerNameProxyCount := make(map[string]int)
transports := make([]string, len(toHosts))
allowedTrans := map[string]bool{"dns": true, "tls": true, "https": true}
allowedTrans := map[string]bool{
transport.DNS: true,
transport.TLS: true,
transport.QUIC: true,
transport.HTTPS: true,
}
for i, hostWithZone := range toHosts {
host, serverName := splitZone(hostWithZone)
trans, h := parse.Transport(host)
@@ -194,7 +199,7 @@ func parseStanza(c *caddy.Controller) (*Forward, error) {
if !allowedTrans[trans] {
return f, fmt.Errorf("'%s' is not supported as a destination protocol in forward: %s", trans, host)
}
if trans == transport.TLS && serverName != "" {
if (trans == transport.TLS || trans == transport.QUIC) && serverName != "" {
if f.tlsServerName != "" {
return f, fmt.Errorf("both forward ('%s') and proxy level ('%s') TLS servernames are set for upstream proxy '%s'", f.tlsServerName, serverName, host)
}
@@ -237,7 +242,7 @@ func parseStanza(c *caddy.Controller) (*Forward, error) {
}
// Only set this for proxies that need it.
if transports[i] == transport.TLS {
if transports[i] == transport.TLS || transports[i] == transport.QUIC {
if tlsConfig, ok := perServerNameTlsConfig[tlsServerNames[i]]; ok {
f.proxies[i].SetTLSConfig(tlsConfig)
} else {
@@ -250,8 +255,8 @@ func parseStanza(c *caddy.Controller) (*Forward, error) {
f.proxies[i].SetMaxIdleConns(f.maxIdleConns)
f.proxies[i].SetReadTimeout(f.readTimeout)
f.proxies[i].GetHealthchecker().SetRecursionDesired(f.opts.HCRecursionDesired)
// when TLS is used, checks are set to tcp-tls
if f.opts.ForceTCP && transports[i] != transport.TLS {
// DoT and DoQ health checkers already use their configured transport.
if f.opts.ForceTCP && transports[i] != transport.TLS && transports[i] != transport.QUIC {
f.proxies[i].GetHealthchecker().SetTCPTransport()
}
f.proxies[i].GetHealthchecker().SetDomain(f.opts.HCDomain)

View File

@@ -48,6 +48,7 @@ func TestSetup(t *testing.T) {
forward com ::2`, false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, "plugin"},
{"forward . tls://[2400:3200::1%dns.alidns.com]:853 {\ntls\n}\n", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""},
{"forward . https://127.0.0.1 \n", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""},
{"forward . quic://127.0.0.1 \n", false, ".", nil, 2, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, ""},
// negative
{"forward . https://1.1.1.1/ \n", true, "", nil, 0, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, "paths are not allowed in HTTPS upstream addresses"},
{"forward . a27.0.0.1", true, "", nil, 0, proxy.Options{HCRecursionDesired: true, HCDomain: "."}, "failed to resolve"},
@@ -94,6 +95,17 @@ func TestSetup(t *testing.T) {
}
}
func TestSetupKeepsDoTAndDoQAtSameAddress(t *testing.T) {
c := caddy.NewTestController("dns", `forward . tls://127.0.0.1 quic://127.0.0.1`)
fs, err := parseForward(c)
if err != nil {
t.Fatalf("parseForward() failed: %v", err)
}
if got := len(fs[0].proxies); got != 2 {
t.Fatalf("proxy count = %d, want 2", got)
}
}
func TestSourceAddress(t *testing.T) {
tests := []struct {
input string
@@ -147,6 +159,8 @@ func TestSplitZone(t *testing.T) {
"https://127.0.0.1%example.net:443", "https://127.0.0.1:443", "example.net",
}, {
"https://127.0.0.1%example.net", "https://127.0.0.1", "example.net",
}, {
"quic://127.0.0.1%example.net:853", "quic://127.0.0.1:853", "example.net",
}, {
"tls://127.0.0.1:854", "tls://127.0.0.1:854", "",
}, {
@@ -193,10 +207,16 @@ func TestSetupTLS(t *testing.T) {
{`forward . tls://127.0.0.1%example.net:854 {
tls
}`, false, "example.net", ""},
{`forward . quic://127.0.0.1%doq.example:853 {
tls
}`, false, "doq.example", ""},
// SNI specifications clash test
{`forward . tls://127.0.0.1%example.net:854 {
tls_servername foo
}`, true, "", "both forward ('foo') and proxy level ('example.net') TLS servernames are set for upstream proxy 'tls://127.0.0.1:854'"},
{`forward . quic://127.0.0.1%doq.example:853 {
tls_servername foo
}`, true, "", "both forward ('foo') and proxy level ('doq.example') TLS servernames are set for upstream proxy 'quic://127.0.0.1:853'"},
{`forward . 127.0.0.1 {
tls_servername dns
}`, false, "", ""},