diff --git a/cmd/routedns/config.go b/cmd/routedns/config.go index 6e68dd8a..577d621c 100644 --- a/cmd/routedns/config.go +++ b/cmd/routedns/config.go @@ -57,6 +57,9 @@ type resolver struct { Socks5Username string `toml:"socks5-username"` Socks5Password string `toml:"socks5-password"` Socks5ResolveLocal bool `toml:"socks5-resolve-local"` // Resolve DNS server address locally (i.e. bootstrap-resolver), not on the SOCK5 proxy + + //QUIC and DoH/3 configuration + Use0RTT bool `toml:"enable-0rtt"` } // DoH-specific resolver options diff --git a/cmd/routedns/example-config/doh-quic-client.toml b/cmd/routedns/example-config/doh-quic-client.toml index d0367666..e7d55773 100644 --- a/cmd/routedns/example-config/doh-quic-client.toml +++ b/cmd/routedns/example-config/doh-quic-client.toml @@ -1,9 +1,12 @@ # DNS-over-HTTPS using the QUIC protocol. +# New connections get initiated with 0-RTT if possible. +# Use0RTT will overwrite the method to GET. [resolvers.cloudflare-doh-quic] address = "https://cloudflare-dns.com/dns-query" protocol = "doh" transport = "quic" +enable-0rtt = true [listeners.local-udp] address = "127.0.0.1:53" diff --git a/cmd/routedns/example-config/doq-client.toml b/cmd/routedns/example-config/doq-client.toml index 1b2ca09d..be92eda4 100644 --- a/cmd/routedns/example-config/doq-client.toml +++ b/cmd/routedns/example-config/doq-client.toml @@ -1,11 +1,13 @@ # This config starts a UDP and a TCP resolver on the loopback interface for plain DNS. # All queries are forwarded to a local DNS-over-QUIC server. +# New connections get initiated with 0-RTT if possible. [resolvers.local-doq] address = "server.acme.test:8853" protocol = "doq" ca = "example-config/server.crt" bootstrap-address = "127.0.0.1" +enable-0rtt = true [listeners.local-udp] address = "127.0.0.1:53" diff --git a/cmd/routedns/resolver.go b/cmd/routedns/resolver.go index b7588968..80d56b58 100644 --- a/cmd/routedns/resolver.go +++ b/cmd/routedns/resolver.go @@ -25,6 +25,7 @@ func instantiateResolver(id string, r resolver, resolvers map[string]rdns.Resolv LocalAddr: net.ParseIP(r.LocalAddr), TLSConfig: tlsConfig, QueryTimeout: time.Duration(r.QueryTimeout) * time.Second, + Use0RTT: r.Use0RTT, } resolvers[id], err = rdns.NewDoQClient(id, r.Address, opt) if err != nil { @@ -81,6 +82,7 @@ func instantiateResolver(id string, r resolver, resolvers map[string]rdns.Resolv LocalAddr: net.ParseIP(r.LocalAddr), QueryTimeout: time.Duration(r.QueryTimeout) * time.Second, Dialer: socks5DialerFromConfig(r), + Use0RTT: r.Use0RTT, } resolvers[id], err = rdns.NewDoHClient(id, r.Address, opt) if err != nil { diff --git a/doc/configuration.md b/doc/configuration.md index e58596d4..f4194f2f 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -1508,6 +1508,7 @@ Example config files: [well-known.toml](../cmd/routedns/example-config/well-know ### DNS-over-HTTPS Resolver DNS resolvers using the HTTPS protocol are configured with `protocol = "doh"`. By default, DoH uses TCP as transport, but it can also be run over QUIC (UDP) by providing the option `transport = "quic"`. DoH supports two HTTP methods, GET and POST. By default RouteDNS uses the POST method, but can be configured to use GET as well using the option `doh = { method = "GET" }`. +DoH with QUIC supports 0-RTT. The DoH resolver will try to use 0-RTT connection establishment if `transport = "quic"` and `enable-0rtt = true` are configured. When 0-RTT is enabled, the resolver will disregard the configured method and always use GET instead. Examples: @@ -1535,6 +1536,7 @@ DoH resolver using QUIC transport. address = "https://cloudflare-dns.com/dns-query" protocol = "doh" transport = "quic" +enable-0rtt = true ``` Example config files: [well-known.toml](../cmd/routedns/example-config/well-known.toml), [simple-doh.toml](../cmd/routedns/example-config/simple-doh.toml), [mutual-tls-doh-client.toml](../cmd/routedns/example-config/mutual-tls-doh-client.toml) @@ -1560,6 +1562,7 @@ Example config files: [dtls-client.toml](../cmd/routedns/example-config/dtls-cli ### DNS-over-QUIC Resolver Similar to DoT, but uses a QUIC connection as transport as per [RFC9250](https://datatracker.ietf.org/doc/rfc9250/). Configured with `protocol = "doq"`. Note that this is different from DoH over QUIC. See [DNS-over-HTTPS](#DNS-over-HTTPS-Resolver) for how to configure this. +The DoQ resolver will try to use 0-RTT connection establishment if `enable-0rtt = true` is configured. Examples: @@ -1569,6 +1572,7 @@ address = "server.acme.test:8853" protocol = "doq" ca = "example-config/server.crt" bootstrap-address = "127.0.0.1" +enable-0rtt = true ``` Example config files: [doq-client.toml](../cmd/routedns/example-config/doq-client.toml) diff --git a/dohclient.go b/dohclient.go index 091e16ff..9d30d3d2 100644 --- a/dohclient.go +++ b/dohclient.go @@ -44,6 +44,8 @@ type DoHClientOptions struct { // Optional dialer, e.g. proxy Dialer Dialer + + Use0RTT bool } // DoHClient is a DNS-over-HTTP resolver with support fot HTTP/2. @@ -85,6 +87,9 @@ func NewDoHClient(id, endpoint string, opt DoHClientOptions) (*DoHClient, error) if opt.Method == "" { opt.Method = "POST" } + if opt.Use0RTT && opt.Transport == "quic" { + opt.Method = "GET" + } if opt.Method != "POST" && opt.Method != "GET" { return nil, fmt.Errorf("unsupported method '%s'", opt.Method) } @@ -181,7 +186,12 @@ func (d *DoHClient) ResolveGET(q *dns.Msg) (*dns.Msg, error) { ctx, cancel := context.WithTimeout(context.Background(), d.opt.QueryTimeout) defer cancel() - req, err := http.NewRequestWithContext(ctx, "GET", u, nil) + method := http.MethodGet + if d.opt.Use0RTT && d.opt.Transport == "quic" { + method = http3.MethodGet0RTT + } + + req, err := http.NewRequestWithContext(ctx, method, u, nil) if err != nil { d.metrics.err.Add("http", 1) return nil, err @@ -268,6 +278,9 @@ func dohQuicTransport(endpoint string, opt DoHClientOptions) (http.RoundTripper, if err != nil { return nil, err } + + // enable TLS session caching for session resumption and 0-RTT + tlsConfig.ClientSessionCache = tls.NewLRUClientSessionCache(100) tlsConfig.ServerName = u.Hostname() lAddr := net.IPv4zero if opt.LocalAddr != nil { diff --git a/doqclient.go b/doqclient.go index 8c0a4102..b90bdbee 100644 --- a/doqclient.go +++ b/doqclient.go @@ -42,6 +42,8 @@ type DoQClientOptions struct { TLSConfig *tls.Config QueryTimeout time.Duration + + Use0RTT bool } var _ Resolver = &DoQClient{} @@ -77,6 +79,11 @@ func NewDoQClient(id, endpoint string, opt DoQClientOptions) (*DoQClient, error) // quic-go requires the ServerName be set explicitly tlsConfig.ServerName = host + // enable TLS session caching for session resumption and 0-RTT + if opt.Use0RTT { + tlsConfig.ClientSessionCache = tls.NewLRUClientSessionCache(100) + } + if opt.QueryTimeout == 0 { opt.QueryTimeout = defaultQueryTimeout }