Skip to content

Commit 51fc10a

Browse files
authored
Add configurable idle-timeout for DoH connections (#484)
1 parent b67ee52 commit 51fc10a

File tree

5 files changed

+81
-6
lines changed

5 files changed

+81
-6
lines changed

cmd/routedns/config.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ type resolver struct {
7373

7474
// DoH-specific resolver options
7575
type doh struct {
76-
Method string
76+
Method string
77+
IdleTimeout int `toml:"idle-timeout"` // Idle connection timeout in seconds. TCP default: 30s. QUIC: uses library default if not set.
7778
}
7879

7980
// Cache backend options

cmd/routedns/resolver.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ func instantiateResolver(id string, r resolver, resolvers map[string]rdns.Resolv
8181
Transport: r.Transport,
8282
LocalAddr: net.ParseIP(r.LocalAddr),
8383
QueryTimeout: time.Duration(r.QueryTimeout) * time.Second,
84+
IdleTimeout: time.Duration(r.DoH.IdleTimeout) * time.Second,
8485
Dialer: socks5DialerFromConfig(r),
8586
Use0RTT: r.Use0RTT,
8687
}
@@ -100,6 +101,7 @@ func instantiateResolver(id string, r resolver, resolvers map[string]rdns.Resolv
100101
Transport: r.Transport,
101102
LocalAddr: net.ParseIP(r.LocalAddr),
102103
QueryTimeout: time.Duration(r.QueryTimeout) * time.Second,
104+
IdleTimeout: time.Duration(r.DoH.IdleTimeout) * time.Second,
103105
}
104106
resolvers[id], err = rdns.NewODoHClient(id, r.Address, r.Target, r.TargetConfig, opt)
105107
if err != nil {

doc/configuration.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1740,6 +1740,7 @@ Example config files: [well-known.toml](../cmd/routedns/example-config/well-know
17401740

17411741
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" }`.
17421742
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. This means the configured address nees to contain a URL template (with the `{?dns}` part).
1743+
The idle connection timeout can be configured with `doh = { idle-timeout = 60 }` (in seconds). This controls how long idle HTTP connections are kept open before being closed. For TCP transport, the default is 30 seconds. For QUIC transport, the default is determined by the quic-go library. Note that for QUIC, the actual idle timeout is the minimum of the client and server values.
17431744

17441745
Examples:
17451746

@@ -1770,6 +1771,15 @@ transport = "quic"
17701771
enable-0rtt = true
17711772
```
17721773

1774+
DoH resolver with extended idle timeout.
1775+
1776+
```toml
1777+
[resolvers.cloudflare-doh-long-idle]
1778+
address = "https://1.1.1.1/dns-query"
1779+
protocol = "doh"
1780+
doh = { idle-timeout = 60 }
1781+
```
1782+
17731783
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)
17741784

17751785
### Oblivious DNS (ODoH)

dohclient.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import (
2323
"golang.org/x/net/http2"
2424
)
2525

26+
// defaultDoHIdleTimeout is the default idle connection timeout for DoH TCP transport.
27+
const defaultDoHIdleTimeout = 30 * time.Second
28+
2629
// DoHClientOptions contains options used by the DNS-over-HTTP resolver.
2730
type DoHClientOptions struct {
2831
// Query method, either GET or POST. If empty, POST is used.
@@ -42,6 +45,12 @@ type DoHClientOptions struct {
4245

4346
QueryTimeout time.Duration
4447

48+
// IdleTimeout is the maximum amount of time an idle connection will remain
49+
// open before being closed. For TCP transport, defaults to 30 seconds if not set.
50+
// For QUIC transport, defaults to the quic-go library default if not set. Note
51+
// that for QUIC, the actual timeout is the minimum of client and server values.
52+
IdleTimeout time.Duration
53+
4554
// Optional dialer, e.g. proxy
4655
Dialer Dialer
4756

@@ -61,6 +70,11 @@ type DoHClient struct {
6170
var _ Resolver = &DoHClient{}
6271

6372
func NewDoHClient(id, endpoint string, opt DoHClientOptions) (*DoHClient, error) {
73+
// Validate options
74+
if opt.IdleTimeout < 0 {
75+
return nil, fmt.Errorf("idle-timeout must not be negative")
76+
}
77+
6478
// Parse the URL template
6579
template, err := uritemplates.Parse(endpoint)
6680
if err != nil {
@@ -237,12 +251,16 @@ func (d *DoHClient) responseFromHTTP(resp *http.Response) (*dns.Msg, error) {
237251
}
238252

239253
func dohTcpTransport(opt DoHClientOptions) (http.RoundTripper, error) {
254+
idleTimeout := opt.IdleTimeout
255+
if idleTimeout == 0 {
256+
idleTimeout = defaultDoHIdleTimeout
257+
}
240258
tr := &http.Transport{
241259
Proxy: http.ProxyFromEnvironment,
242260
TLSClientConfig: opt.TLSConfig,
243261
DisableCompression: true,
244262
ResponseHeaderTimeout: 10 * time.Second,
245-
IdleConnTimeout: 30 * time.Second,
263+
IdleConnTimeout: idleTimeout,
246264
}
247265
// If we're using a custom tls.Config, HTTP2 isn't enabled by default in
248266
// the HTTP library. Turn it on for this transport.
@@ -320,12 +338,17 @@ func dohQuicTransport(endpoint string, opt DoHClientOptions) (http.RoundTripper,
320338
return dialFunc(ctx, rAddr, tlsConfig, config)
321339
}
322340

341+
quicConfig := &quic.Config{
342+
TokenStore: quic.NewLRUTokenStore(10, 10),
343+
}
344+
if opt.IdleTimeout > 0 {
345+
quicConfig.MaxIdleTimeout = opt.IdleTimeout
346+
}
347+
323348
tr := &http3.Transport{
324349
TLSClientConfig: tlsConfig,
325-
QUICConfig: &quic.Config{
326-
TokenStore: quic.NewLRUTokenStore(10, 10),
327-
},
328-
Dial: dialer,
350+
QUICConfig: quicConfig,
351+
Dial: dialer,
329352
}
330353
return tr, nil
331354
}

dohclient_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package rdns
22

33
import (
4+
"net/http"
45
"testing"
6+
"time"
57

68
"github.com/miekg/dns"
9+
"github.com/quic-go/quic-go/http3"
710
"github.com/stretchr/testify/require"
811
)
912

@@ -26,3 +29,39 @@ func TestDoHClientSimpleGET(t *testing.T) {
2629
require.NoError(t, err)
2730
require.NotEmpty(t, r.Answer)
2831
}
32+
33+
func TestDoHTcpTransportIdleTimeoutDefault(t *testing.T) {
34+
tr, err := dohTcpTransport(DoHClientOptions{})
35+
require.NoError(t, err)
36+
httpTransport := tr.(*http.Transport)
37+
require.Equal(t, defaultDoHIdleTimeout, httpTransport.IdleConnTimeout)
38+
}
39+
40+
func TestDoHTcpTransportIdleTimeoutConfigured(t *testing.T) {
41+
tr, err := dohTcpTransport(DoHClientOptions{IdleTimeout: 2 * time.Minute})
42+
require.NoError(t, err)
43+
httpTransport := tr.(*http.Transport)
44+
require.Equal(t, 2*time.Minute, httpTransport.IdleConnTimeout)
45+
}
46+
47+
func TestDoHQuicTransportIdleTimeoutDefault(t *testing.T) {
48+
tr, err := dohQuicTransport("https://example.com/dns-query", DoHClientOptions{})
49+
require.NoError(t, err)
50+
http3Transport := tr.(*http3.Transport)
51+
// When not configured, MaxIdleTimeout should not be set (zero value)
52+
// The quic-go library will use its own default
53+
require.Equal(t, time.Duration(0), http3Transport.QUICConfig.MaxIdleTimeout)
54+
}
55+
56+
func TestDoHQuicTransportIdleTimeoutConfigured(t *testing.T) {
57+
tr, err := dohQuicTransport("https://example.com/dns-query", DoHClientOptions{IdleTimeout: 2 * time.Minute})
58+
require.NoError(t, err)
59+
http3Transport := tr.(*http3.Transport)
60+
require.Equal(t, 2*time.Minute, http3Transport.QUICConfig.MaxIdleTimeout)
61+
}
62+
63+
func TestDoHClientIdleTimeoutNegative(t *testing.T) {
64+
_, err := NewDoHClient("test-doh", "https://example.com/dns-query", DoHClientOptions{IdleTimeout: -1 * time.Second})
65+
require.Error(t, err)
66+
require.Contains(t, err.Error(), "idle-timeout must not be negative")
67+
}

0 commit comments

Comments
 (0)