Skip to content

Commit 5e4803a

Browse files
authored
feat: add LookupTXTWithTTL to Resolver (#75)
* feat: add LookupTXTWithTTL to Resolver Resolver.LookupTXT returns only TXT values and drops the TTL, so a resolver that knows it (such as DNS-over-HTTPS) cannot report it and callers cannot tell how long a record set is valid or when to re-resolve. Correct caching and expiration needs that TTL for both kinds of TXT records this library resolves: - DNSADDR (dnsaddr=): the TTL bounds how long a resolved multiaddr is valid. Multiaddrs increasingly carry rotating /certhash/ segments (AutoTLS WebTransport and WebRTC-direct nodes, including libp2p bootstrappers); without the TTL these cannot be cached or expired correctly, so a peer cannot reliably publish addresses whose certhash changes over time. - DNSLink (dnslink=): the TTL lets an IPFS gateway set Cache-Control max-age for /ipns/<dnslink-host> responses (ipfs/boxo#329). Add LookupTXTWithTTL, which routes to the matched per-domain resolver and returns its TTL when it implements the new TXTWithTTLResolver interface; resolvers without TTL support (such as the OS resolver) report 0 (unknown). LookupTXT and BasicResolver are unchanged, so this is additive. * docs: clarify which resolver reports the TTL LookupTXTWithTTL routes TTL reporting through whichever resolver the lookup matches, including the default one, not only per-domain resolvers. New test pins the default-resolver path. * chore: release v0.6.0
1 parent 7c22394 commit 5e4803a

3 files changed

Lines changed: 115 additions & 1 deletion

File tree

resolve.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"net"
66
"strings"
7+
"time"
78

89
ma "github.com/multiformats/go-multiaddr"
910
)
@@ -312,3 +313,29 @@ func (r *Resolver) LookupIPAddr(ctx context.Context, domain string) ([]net.IPAdd
312313
func (r *Resolver) LookupTXT(ctx context.Context, txt string) ([]string, error) {
313314
return r.getResolver(txt).LookupTXT(ctx, txt)
314315
}
316+
317+
// TXTWithTTLResolver is an optional interface a [BasicResolver] may implement to
318+
// report the TTL of a TXT record set alongside its values. Resolvers backed by
319+
// a protocol that carries TTLs (such as DNS-over-HTTPS) can implement it; the
320+
// default OS resolver cannot.
321+
type TXTWithTTLResolver interface {
322+
LookupTXTWithTTL(ctx context.Context, name string) (txt []string, ttl time.Duration, err error)
323+
}
324+
325+
// a Resolver routes TXT-with-TTL lookups, so it satisfies the interface too
326+
var _ TXTWithTTLResolver = (*Resolver)(nil)
327+
328+
// LookupTXTWithTTL resolves the TXT records for a domain and, when the resolver
329+
// the lookup routes to (a matched per-domain resolver, or the default one
330+
// otherwise) implements [TXTWithTTLResolver], also returns their TTL. Resolvers
331+
// that cannot report a TTL (such as the default OS resolver) yield a TTL of 0,
332+
// meaning unknown.
333+
func (r *Resolver) LookupTXTWithTTL(ctx context.Context, domain string) ([]string, time.Duration, error) {
334+
rslv := r.getResolver(domain)
335+
if ttlRslv, ok := rslv.(TXTWithTTLResolver); ok {
336+
return ttlRslv.LookupTXTWithTTL(ctx, domain)
337+
}
338+
339+
txt, err := rslv.LookupTXT(ctx, domain)
340+
return txt, 0, err
341+
}

resolve_ttl_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package madns
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
)
8+
9+
// mockTTLResolver is a MockResolver that also reports a fixed TTL for TXT lookups.
10+
type mockTTLResolver struct {
11+
*MockResolver
12+
ttl time.Duration
13+
}
14+
15+
func (r *mockTTLResolver) LookupTXTWithTTL(ctx context.Context, name string) ([]string, time.Duration, error) {
16+
txt, err := r.MockResolver.LookupTXT(ctx, name)
17+
return txt, r.ttl, err
18+
}
19+
20+
func TestLookupTXTWithTTL(t *testing.T) {
21+
ctx := context.Background()
22+
23+
def := &MockResolver{TXT: map[string][]string{
24+
"example.com": {"dnslink=/ipfs/bafkqaaa"},
25+
}}
26+
withTTL := &mockTTLResolver{
27+
MockResolver: &MockResolver{TXT: map[string][]string{
28+
"custom.test": {"dnslink=/ipfs/bafkqaaa"},
29+
}},
30+
ttl: 42 * time.Second,
31+
}
32+
33+
rslv, err := NewResolver(
34+
WithDefaultResolver(def),
35+
WithDomainResolver("custom.test", withTTL),
36+
)
37+
if err != nil {
38+
t.Fatal(err)
39+
}
40+
41+
// the matched per-domain resolver supports TTL, so it is reported
42+
txt, ttl, err := rslv.LookupTXTWithTTL(ctx, "custom.test")
43+
if err != nil {
44+
t.Fatal(err)
45+
}
46+
if len(txt) != 1 {
47+
t.Fatalf("expected 1 TXT record, got %d", len(txt))
48+
}
49+
if ttl != 42*time.Second {
50+
t.Fatalf("expected ttl 42s, got %s", ttl)
51+
}
52+
53+
// the default resolver does not support TTL, so it is reported as unknown (0)
54+
_, ttl, err = rslv.LookupTXTWithTTL(ctx, "example.com")
55+
if err != nil {
56+
t.Fatal(err)
57+
}
58+
if ttl != 0 {
59+
t.Fatalf("expected unknown ttl 0 for resolver without TTL support, got %s", ttl)
60+
}
61+
}
62+
63+
func TestLookupTXTWithTTLDefaultResolver(t *testing.T) {
64+
ctx := context.Background()
65+
66+
// TTL flows through when the default resolver (no per-domain match)
67+
// supports it
68+
withTTL := &mockTTLResolver{
69+
MockResolver: &MockResolver{TXT: map[string][]string{
70+
"example.com": {"dnslink=/ipfs/bafkqaaa"},
71+
}},
72+
ttl: 7 * time.Second,
73+
}
74+
75+
rslv, err := NewResolver(WithDefaultResolver(withTTL))
76+
if err != nil {
77+
t.Fatal(err)
78+
}
79+
80+
_, ttl, err := rslv.LookupTXTWithTTL(ctx, "example.com")
81+
if err != nil {
82+
t.Fatal(err)
83+
}
84+
if ttl != 7*time.Second {
85+
t.Fatalf("expected ttl 7s from the default resolver, got %s", ttl)
86+
}
87+
}

version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"version": "v0.5.0"
2+
"version": "v0.6.0"
33
}

0 commit comments

Comments
 (0)