Pull request #411: AGDNS-3379-imp-optimistic-cache

Merge in GO/dnsproxy from AGDNS-3379-imp-optimistic-cache to master

Squashed commit of the following:

commit 94c31763b5ced9b5c2624a9b1f642747266f7387
Author: f.setrakov <[email protected]>
Date:   Wed Nov 19 11:55:17 2025 +0300

    cmd: use default const

commit 82f50969701de0ab74e122e03bdde3e0685ca8e7
Author: f.setrakov <[email protected]>
Date:   Tue Nov 18 19:45:05 2025 +0300

    all: imp docs

commit 0e1cb4935d9ab716a3e654e89e3758b6b07ccf27
Author: f.setrakov <[email protected]>
Date:   Tue Nov 18 17:42:11 2025 +0300

    all: imp style

commit d9b29b6fe4a6cda55599eb6174914e09f54439ed
Author: f.setrakov <[email protected]>
Date:   Tue Nov 18 15:59:02 2025 +0300

    all: imp style, imp tests

commit 8b03cab16175f49bc03b3314fc3cff504996beca
Author: f.setrakov <[email protected]>
Date:   Mon Nov 17 18:19:18 2025 +0300

    all: imp docs, style

commit bdd2a7e8e90539caf47747e73f7626e5b4420e2f
Author: f.setrakov <[email protected]>
Date:   Mon Nov 17 17:57:04 2025 +0300

    all: imp optimistic cache
This commit is contained in:
Fedor Setrakov
2025-11-19 18:59:49 +03:00
parent 8c5f8411d3
commit 314ac48191
9 changed files with 229 additions and 108 deletions
+4
View File
@@ -99,6 +99,10 @@ Usage of ./dnsproxy:
Listening addresses.
--max-go-routines=uint
Set the maximum number of go routines. A zero value will not not set a maximum.
--optimistic-answer-ttl
Default TTL value for expired DNS entries in optimistic cache. Default: 30s
--optimistic-max-age
Period of time after which entries are removed from optimistic cache in human-readable form. Default: 12h.
--output=path/-o path
Path to the log file.
--pending-requests-enabled
+65 -49
View File
@@ -42,6 +42,8 @@ const (
timeoutIdx
cacheMinTTLIdx
cacheMaxTTLIdx
cacheOptimisticAnswerTTLIdx
cacheOptimisticMaxAgeIdx
cacheSizeBytesIdx
ratelimitIdx
ratelimitSubnetLenIPv4Idx
@@ -247,6 +249,18 @@ var commandLineOptions = []*commandLineOption{
short: "",
valueType: "uint32",
},
cacheOptimisticAnswerTTLIdx: {
description: "Default TTL value for expired answers from optimistic cache",
long: "optimistic-answer-ttl",
short: "",
valueType: "duration",
},
cacheOptimisticMaxAgeIdx: {
description: "Period of time after which entries are removed from the optimistic cache",
long: "optimistic-max-age",
short: "",
valueType: "duration",
},
cacheSizeBytesIdx: {
description: "Cache size (in bytes). Default: 64k.",
long: "cache-size",
@@ -399,55 +413,57 @@ func parseCmdLineOptions(conf *configuration) (err error) {
flags := flag.NewFlagSet(cmdName, flag.ContinueOnError)
for i, fieldPtr := range []any{
configPathIdx: &conf.ConfigPath,
logOutputIdx: &conf.LogOutput,
tlsCertPathIdx: &conf.TLSCertPath,
tlsKeyPathIdx: &conf.TLSKeyPath,
httpsServerNameIdx: &conf.HTTPSServerName,
httpsUserinfoIdx: &conf.HTTPSUserinfo,
dnsCryptConfigPathIdx: &conf.DNSCryptConfigPath,
ednsAddrIdx: &conf.EDNSAddr,
upstreamModeIdx: &conf.UpstreamMode,
listenAddrsIdx: &conf.ListenAddrs,
listenPortsIdx: &conf.ListenPorts,
httpsListenPortsIdx: &conf.HTTPSListenPorts,
tlsListenPortsIdx: &conf.TLSListenPorts,
quicListenPortsIdx: &conf.QUICListenPorts,
dnsCryptListenPortsIdx: &conf.DNSCryptListenPorts,
upstreamsIdx: &conf.Upstreams,
bootstrapDNSIdx: &conf.BootstrapDNS,
fallbacksIdx: &conf.Fallbacks,
privateRDNSUpstreamsIdx: &conf.PrivateRDNSUpstreams,
dns64PrefixIdx: &conf.DNS64Prefix,
privateSubnetsIdx: &conf.PrivateSubnets,
bogusNXDomainIdx: &conf.BogusNXDomain,
hostsFilesIdx: &conf.HostsFiles,
timeoutIdx: &conf.Timeout,
cacheMinTTLIdx: &conf.CacheMinTTL,
cacheMaxTTLIdx: &conf.CacheMaxTTL,
cacheSizeBytesIdx: &conf.CacheSizeBytes,
ratelimitIdx: &conf.Ratelimit,
ratelimitSubnetLenIPv4Idx: &conf.RatelimitSubnetLenIPv4,
ratelimitSubnetLenIPv6Idx: &conf.RatelimitSubnetLenIPv6,
udpBufferSizeIdx: &conf.UDPBufferSize,
maxGoRoutinesIdx: &conf.MaxGoRoutines,
tlsMinVersionIdx: &conf.TLSMinVersion,
tlsMaxVersionIdx: &conf.TLSMaxVersion,
helpIdx: &conf.help,
hostsFileEnabledIdx: &conf.HostsFileEnabled,
pprofIdx: &conf.Pprof,
versionIdx: &conf.Version,
verboseIdx: &conf.Verbose,
insecureIdx: &conf.Insecure,
ipv6DisabledIdx: &conf.IPv6Disabled,
http3Idx: &conf.HTTP3,
cacheOptimisticIdx: &conf.CacheOptimistic,
cacheIdx: &conf.Cache,
refuseAnyIdx: &conf.RefuseAny,
enableEDNSSubnetIdx: &conf.EnableEDNSSubnet,
pendingRequestsEnabledIdx: &conf.PendingRequestsEnabled,
dns64Idx: &conf.DNS64,
usePrivateRDNSIdx: &conf.UsePrivateRDNS,
configPathIdx: &conf.ConfigPath,
logOutputIdx: &conf.LogOutput,
tlsCertPathIdx: &conf.TLSCertPath,
tlsKeyPathIdx: &conf.TLSKeyPath,
httpsServerNameIdx: &conf.HTTPSServerName,
httpsUserinfoIdx: &conf.HTTPSUserinfo,
dnsCryptConfigPathIdx: &conf.DNSCryptConfigPath,
ednsAddrIdx: &conf.EDNSAddr,
upstreamModeIdx: &conf.UpstreamMode,
listenAddrsIdx: &conf.ListenAddrs,
listenPortsIdx: &conf.ListenPorts,
httpsListenPortsIdx: &conf.HTTPSListenPorts,
tlsListenPortsIdx: &conf.TLSListenPorts,
quicListenPortsIdx: &conf.QUICListenPorts,
dnsCryptListenPortsIdx: &conf.DNSCryptListenPorts,
upstreamsIdx: &conf.Upstreams,
bootstrapDNSIdx: &conf.BootstrapDNS,
fallbacksIdx: &conf.Fallbacks,
privateRDNSUpstreamsIdx: &conf.PrivateRDNSUpstreams,
dns64PrefixIdx: &conf.DNS64Prefix,
privateSubnetsIdx: &conf.PrivateSubnets,
bogusNXDomainIdx: &conf.BogusNXDomain,
hostsFilesIdx: &conf.HostsFiles,
timeoutIdx: &conf.Timeout,
cacheMinTTLIdx: &conf.CacheMinTTL,
cacheMaxTTLIdx: &conf.CacheMaxTTL,
cacheOptimisticAnswerTTLIdx: &conf.OptimisticAnswerTTL,
cacheOptimisticMaxAgeIdx: &conf.OptimisticMaxAge,
cacheSizeBytesIdx: &conf.CacheSizeBytes,
ratelimitIdx: &conf.Ratelimit,
ratelimitSubnetLenIPv4Idx: &conf.RatelimitSubnetLenIPv4,
ratelimitSubnetLenIPv6Idx: &conf.RatelimitSubnetLenIPv6,
udpBufferSizeIdx: &conf.UDPBufferSize,
maxGoRoutinesIdx: &conf.MaxGoRoutines,
tlsMinVersionIdx: &conf.TLSMinVersion,
tlsMaxVersionIdx: &conf.TLSMaxVersion,
helpIdx: &conf.help,
hostsFileEnabledIdx: &conf.HostsFileEnabled,
pprofIdx: &conf.Pprof,
versionIdx: &conf.Version,
verboseIdx: &conf.Verbose,
insecureIdx: &conf.Insecure,
ipv6DisabledIdx: &conf.IPv6Disabled,
http3Idx: &conf.HTTP3,
cacheOptimisticIdx: &conf.CacheOptimistic,
cacheIdx: &conf.Cache,
refuseAnyIdx: &conf.RefuseAny,
enableEDNSSubnetIdx: &conf.EnableEDNSSubnet,
pendingRequestsEnabledIdx: &conf.PendingRequestsEnabled,
dns64Idx: &conf.DNS64,
usePrivateRDNSIdx: &conf.UsePrivateRDNS,
} {
addOption(flags, fieldPtr, commandLineOptions[i])
}
+10
View File
@@ -107,6 +107,14 @@ type configuration struct {
// greater.
CacheMaxTTL uint32 `yaml:"cache-max-ttl"`
// OptimisticAnswerTTL is the default TTL for expired cached responses
// in seconds.
OptimisticAnswerTTL timeutil.Duration `yaml:"optimistic-answer-ttl"`
// OptimisticMaxAge is the maximum time entries remain in the cache
// when cache is optimistic.
OptimisticMaxAge timeutil.Duration `yaml:"optimistic-max-age"`
// CacheSizeBytes is the cache size in bytes. Default is 64k.
CacheSizeBytes int `yaml:"cache-size"`
@@ -202,6 +210,8 @@ func parseConfig() (conf *configuration, exitCode int, err error) {
UpstreamMode: string(proxy.UpstreamModeLoadBalance),
CacheSizeBytes: 64 * 1024,
Timeout: timeutil.Duration(10 * time.Second),
OptimisticAnswerTTL: timeutil.Duration(proxy.DefaultOptimisticAnswerTTL),
OptimisticMaxAge: timeutil.Duration(proxy.DefaultOptimisticMaxAge),
RatelimitSubnetLenIPv4: 24,
RatelimitSubnetLenIPv6: 56,
HostsFileEnabled: true,
+10 -8
View File
@@ -58,14 +58,16 @@ func createProxyConfig(
RatelimitSubnetLenIPv4: conf.RatelimitSubnetLenIPv4,
RatelimitSubnetLenIPv6: conf.RatelimitSubnetLenIPv6,
Ratelimit: conf.Ratelimit,
CacheEnabled: conf.Cache,
CacheSizeBytes: conf.CacheSizeBytes,
CacheMinTTL: conf.CacheMinTTL,
CacheMaxTTL: conf.CacheMaxTTL,
CacheOptimistic: conf.CacheOptimistic,
RefuseAny: conf.RefuseAny,
HTTP3: conf.HTTP3,
Ratelimit: conf.Ratelimit,
CacheEnabled: conf.Cache,
CacheSizeBytes: conf.CacheSizeBytes,
CacheMinTTL: conf.CacheMinTTL,
CacheMaxTTL: conf.CacheMaxTTL,
CacheOptimisticAnswerTTL: time.Duration(conf.OptimisticAnswerTTL),
CacheOptimisticMaxAge: time.Duration(conf.OptimisticMaxAge),
CacheOptimistic: conf.CacheOptimistic,
RefuseAny: conf.RefuseAny,
HTTP3: conf.HTTP3,
// TODO(e.burkov): The following CIDRs are aimed to match any address.
// This is not quite proper approach to be used by default so think
// about configuring it.
+53 -25
View File
@@ -39,19 +39,20 @@ type cache struct {
// optimistic defines if the cache should return expired items and resolve
// those again.
optimistic bool
// optimisticTTL is the default TTL for expired cached responses.
optimisticTTL time.Duration
// optimisticMaxAge is the maximum time entries remain in the cache when
// cache is optimistic.
optimisticMaxAge time.Duration
}
// cacheItem is a single cache entry. It's a helper type to aggregate the
// item-specific logic.
type cacheItem struct {
// m contains the cached response.
m *dns.Msg
// u contains an address of the upstream which resolved m.
u string
// ttl is the time-to-live value for the item. Should be set before calling
// [cacheItem.pack].
m *dns.Msg
u string
ttl uint32
}
@@ -108,29 +109,28 @@ func (ci *cacheItem) pack() (packed []byte) {
return packed
}
// optimisticTTL is the default TTL for expired cached responses in seconds.
const optimisticTTL = 10
// unpackItem converts the data into cacheItem using req as a request message.
// expired is true if the item exists but expired. The expired cached items are
// only returned if c is optimistic. req must not be nil.
// only returned if c is optimistic and optimistic max age is not exceeded. req
// must not be nil.
func (c *cache) unpackItem(data []byte, req *dns.Msg) (ci *cacheItem, expired bool) {
if len(data) < minPackedLen {
return nil, false
}
b := bytes.NewBuffer(data)
expire := int64(binary.BigEndian.Uint32(b.Next(expTimeSz)))
now := time.Now().Unix()
expire := time.Unix(int64(binary.BigEndian.Uint32(b.Next(expTimeSz))), 0)
now := time.Now()
var ttl uint32
if expired = expire <= now; expired {
if !c.optimistic {
if expired = now.After(expire); expired {
optimisticExpire := expire.Add(c.optimisticMaxAge)
if !c.optimistic || now.After(optimisticExpire) {
return nil, expired
}
ttl = optimisticTTL
ttl = uint32(c.optimisticTTL.Seconds())
} else {
ttl = uint32(expire - now)
ttl = uint32(expire.Unix() - now.Unix())
}
l := int(binary.BigEndian.Uint16(b.Next(packedMsgLenSz)))
@@ -173,22 +173,50 @@ func (p *Proxy) initCache() {
size := p.CacheSizeBytes
p.logger.Info("cache enabled", "size", size)
p.cache = newCache(size, p.EnableEDNSClientSubnet, p.CacheOptimistic)
p.cache = newCache(&cacheConfig{
size: size,
optimisticTTL: p.CacheOptimisticAnswerTTL,
optimisticMaxAge: p.CacheOptimisticMaxAge,
withECS: p.EnableEDNSClientSubnet,
optimistic: p.CacheOptimistic,
})
p.shortFlighter = newOptimisticResolver(p)
}
// cacheConfig is the configuration structure for [cache].
type cacheConfig struct {
// size is the cache size in bytes.
size int
// optimisticTTL is the default TTL for expired cached responses when
// optimistic is enabled.
optimisticTTL time.Duration
// optimisticMaxAge is the maximum time entries remain in the cache when
// cache is optimistic.
optimisticMaxAge time.Duration
// withECS enables EDNS Client Subnet support for cache.
withECS bool
// optimistic defines if the cache should return expired items and resolve
// those again.
optimistic bool
}
// newCache returns a properly initialized cache. logger must not be nil.
func newCache(size int, withECS, optimistic bool) (c *cache) {
func newCache(conf *cacheConfig) (c *cache) {
c = &cache{
itemsLock: &sync.RWMutex{},
itemsWithSubnetLock: &sync.RWMutex{},
items: createCache(size),
optimistic: optimistic,
items: createCache(conf.size),
optimistic: conf.optimistic,
optimisticTTL: conf.optimisticTTL,
optimisticMaxAge: conf.optimisticMaxAge,
}
if withECS {
c.itemsWithSubnet = createCache(size)
if conf.withECS {
c.itemsWithSubnet = createCache(conf.size)
}
return c
+52 -21
View File
@@ -1,6 +1,7 @@
package proxy
import (
"cmp"
"net"
"net/netip"
"strings"
@@ -34,6 +35,22 @@ var upstreamWithAddr = &dnsproxytest.Upstream{
OnAddress: func() (addr string) { return testUpsAddr },
}
// newTestCache is a helper that returns new cache and fills its config with
// given values. If conf is nil, the default configuration will be used.
func newTestCache(tb testing.TB, conf *cacheConfig) (c *cache) {
tb.Helper()
conf = cmp.Or(conf, &cacheConfig{})
return newCache(&cacheConfig{
size: cmp.Or(conf.size, testCacheSize),
optimisticTTL: cmp.Or(conf.optimisticTTL, testOptimisticTTL),
optimisticMaxAge: cmp.Or(conf.optimisticMaxAge, testOptimisticMaxAge),
withECS: conf.withECS,
optimistic: conf.optimistic,
})
}
func TestServeCached(t *testing.T) {
dnsProxy := mustNew(t, &Config{
Logger: slogutil.NewDiscardLogger(),
@@ -96,12 +113,14 @@ func TestCache_expired(t *testing.T) {
}).SetQuestion(host, dns.TypeA)
testCases := []struct {
name string
ttl uint32
wantTTL uint32
optimistic bool
name string
ttl uint32
wantTTL uint32
optimistic bool
optimisticMaxAge time.Duration
}{{
name: "realistic_hit",
name: "realistic_hit",
ttl: defaultTestTTL,
wantTTL: defaultTestTTL,
optimistic: false,
@@ -111,18 +130,26 @@ func TestCache_expired(t *testing.T) {
wantTTL: 0,
optimistic: false,
}, {
name: "optimistic_hit",
ttl: defaultTestTTL,
wantTTL: defaultTestTTL,
optimistic: true,
name: "optimistic_hit",
ttl: defaultTestTTL,
wantTTL: defaultTestTTL,
optimistic: true,
optimisticMaxAge: testOptimisticMaxAge,
}, {
name: "optimistic_expired",
ttl: 0,
wantTTL: optimisticTTL,
optimistic: true,
name: "optimistic_expired",
ttl: 0,
wantTTL: uint32(testOptimisticTTL.Seconds()),
optimistic: true,
optimisticMaxAge: testOptimisticMaxAge,
}, {
name: "optimistic_max_age_exceeded",
ttl: 0,
wantTTL: 0,
optimistic: true,
optimisticMaxAge: time.Hour * -1,
}}
testCache := newCache(testCacheSize, false, false)
testCache := newTestCache(t, nil)
for _, tc := range testCases {
ans.Hdr.Ttl = tc.ttl
req := (&dns.Msg{}).SetQuestion(host, dns.TypeA)
@@ -130,6 +157,7 @@ func TestCache_expired(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
if tc.optimistic {
testCache.optimistic = true
testCache.optimisticMaxAge = tc.optimisticMaxAge
t.Cleanup(func() { testCache.optimistic = false })
}
@@ -159,7 +187,7 @@ func TestCache_expired(t *testing.T) {
}
func TestCacheDO(t *testing.T) {
testCache := newCache(testCacheSize, false, false)
testCache := newTestCache(t, nil)
// Fill the cache.
reply := (&dns.Msg{
@@ -204,7 +232,7 @@ func TestCacheDO(t *testing.T) {
func TestCacheCNAME(t *testing.T) {
l := slogutil.NewDiscardLogger()
testCache := newCache(testCacheSize, false, false)
testCache := newTestCache(t, nil)
// Fill the cache
reply := (&dns.Msg{
@@ -241,7 +269,7 @@ func TestCacheCNAME(t *testing.T) {
}
func TestCache_uncacheable(t *testing.T) {
testCache := newCache(testCacheSize, false, false)
testCache := newTestCache(t, nil)
// Create a DNS request.
request := (&dns.Msg{}).SetQuestion("google.com.", dns.TypeA)
@@ -257,7 +285,7 @@ func TestCache_uncacheable(t *testing.T) {
}
func TestCache_concurrent(t *testing.T) {
testCache := newCache(testCacheSize, false, false)
testCache := newTestCache(t, nil)
hosts := map[string]string{
dns.Fqdn("yandex.com"): "213.180.204.62",
@@ -530,7 +558,7 @@ func TestCache(t *testing.T) {
func (tests testCases) run(t *testing.T) {
l := slogutil.NewDiscardLogger()
testCache := newCache(testCacheSize, false, false)
testCache := newTestCache(t, nil)
for _, res := range tests.cache {
reply := (&dns.Msg{
@@ -637,7 +665,7 @@ func TestCache_getWithSubnet(t *testing.T) {
mask24 := net.CIDRMask(24, netutil.IPv4BitLen)
l := slogutil.NewDiscardLogger()
c := newCache(testCacheSize, true, false)
c := newTestCache(t, &cacheConfig{withECS: true})
t.Run("empty", func(t *testing.T) {
ci, expired, _ := c.getWithSubnet(req, &net.IPNet{IP: ip1234, Mask: mask24})
@@ -723,7 +751,10 @@ func TestCache_getWithSubnet_mask(t *testing.T) {
ansIP := net.IP{4, 4, 4, 4}
c := newCache(testCacheSize, true, true)
c := newTestCache(t, &cacheConfig{
withECS: true,
optimistic: true,
})
req := (&dns.Msg{}).SetQuestion(testFQDN, dns.TypeA)
resp := (&dns.Msg{
+18
View File
@@ -18,6 +18,16 @@ import (
// LogPrefix is a prefix for logging.
const LogPrefix = "dnsproxy"
const (
// DefaultOptimisticMaxAge is default value for
// [Config.CacheOptimisticMaxAge].
DefaultOptimisticMaxAge = 12 * time.Hour
// DefaultOptimisticAnswerTTL is default value for
// [Config.CacheOptimisticAnswerTTL].
DefaultOptimisticAnswerTTL = 30 * time.Second
)
// RequestHandler is an optional custom handler for DNS requests. It's used
// instead of [Proxy.Resolve] if set. The resulting error doesn't affect the
// request processing. The custom handler is responsible for calling
@@ -188,6 +198,14 @@ type Config struct {
// CacheMaxTTL is the maximum TTL for cached DNS responses in seconds.
CacheMaxTTL uint32
// CacheOptimisticAnswerTTL is the default TTL for expired cached responses.
// Default value is [DefaultOptimisticAnswerTTL].
CacheOptimisticAnswerTTL time.Duration
// CacheOptimisticMaxAge is the maximum time entries remain in the cache
// when cache is optimistic. Default value is [DefaultOptimisticMaxAge].
CacheOptimisticMaxAge time.Duration
// MaxGoroutines is the maximum number of goroutines processing DNS
// requests. Important for mobile users.
//
+5 -1
View File
@@ -231,7 +231,11 @@ func NewCustomUpstreamConfig(
var customCache *cache
if cacheEnabled {
// TODO(d.kolyshev): Support optimistic with newOptimisticResolver.
customCache = newCache(cacheSize, enableEDNSClientSubnet, false)
customCache = newCache(&cacheConfig{
size: cacheSize,
withECS: enableEDNSClientSubnet,
optimistic: false,
})
}
return &CustomUpstreamConfig{
+12 -4
View File
@@ -38,6 +38,12 @@ const (
// testTimeout is the default timeout for tests.
testTimeout = 500 * time.Millisecond
// testOptimisticTTL is a common optimistic cache ttl value for tests.
testOptimisticTTL = 10 * time.Second
// testOptimisticMaxAge is a common optimistic max age value for tests.
testOptimisticMaxAge = 12 * time.Hour
)
// localhostAnyPort is a [netip.AddrPort] having a value of 127.0.0.1:0.
@@ -1391,8 +1397,10 @@ func TestProxy_Resolve_withOptimisticResolver(t *testing.T) {
p := &Proxy{
Config: Config{
CacheEnabled: true,
CacheOptimistic: true,
CacheEnabled: true,
CacheOptimistic: true,
CacheOptimisticAnswerTTL: testOptimisticTTL,
CacheOptimisticMaxAge: testOptimisticMaxAge,
},
logger: slogutil.NewDiscardLogger(),
pendingRequests: newDefaultPendingRequests(),
@@ -1440,7 +1448,7 @@ func TestProxy_Resolve_withOptimisticResolver(t *testing.T) {
require.NoError(t, err)
require.Len(t, firstCtx.Res.Answer, 1)
assert.EqualValues(t, optimisticTTL, firstCtx.Res.Answer[0].Header().Ttl)
assert.Equal(t, uint32(testOptimisticTTL.Seconds()), firstCtx.Res.Answer[0].Header().Ttl)
// Wait for optimisticResolver to reach the tested function.
<-out
@@ -1449,7 +1457,7 @@ func TestProxy_Resolve_withOptimisticResolver(t *testing.T) {
require.NoError(t, err)
require.Len(t, secondCtx.Res.Answer, 1)
assert.EqualValues(t, optimisticTTL, secondCtx.Res.Answer[0].Header().Ttl)
assert.Equal(t, uint32(testOptimisticTTL.Seconds()), secondCtx.Res.Answer[0].Header().Ttl)
// Continue and wait for it to finish.
in <- unit{}