Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .github/workflows/gateway-sharness.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,20 @@ name: Gateway Sharness
on:
workflow_dispatch:
pull_request:
paths: ['gateway/**']
paths:
- 'gateway/**'
- 'namesys/**'
- 'ipns/**'
- 'path/**'
- '.github/workflows/gateway-sharness.yml'
push:
branches: ['main']
paths: ['gateway/**']
paths:
- 'gateway/**'
- 'namesys/**'
- 'ipns/**'
- 'path/**'
- '.github/workflows/gateway-sharness.yml'

jobs:
sharness:
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ The following emojis are used to highlight certain changes:

### Added

- `namesys`: DNSLink lookups now return the DNS TXT record's TTL, and the gateway uses it as `Cache-Control: max-age` for `/ipns/<dnslink-host>` responses. Clients cache a DNSLink website for exactly as long as its DNS record allows and fetch updates once it expires, instead of relying on a static default. [#329](https://github.com/ipfs/boxo/issues/329)
- Requires a resolver that reports TTLs, such as DNS-over-HTTPS via [`go-doh-resolver` v0.6.0](https://github.com/libp2p/go-doh-resolver/releases/tag/v0.6.0). The OS resolver cannot report TTLs: Go's standard library `net.Resolver` returns only record values, and libp2p's default DNS wiring (`madns.DefaultResolver`) wraps it, so domains it serves keep the old behavior. In Kubo, a `DNS.Resolvers` entry for `.` covers all domains.
- `WithDNSResolver` detects TTL support automatically. `NewDNSResolverWithTTL`, `WithDNSResolverWithTTL`, and `LookupTXTWithTTLFunc` take a TTL-aware lookup directly. `NewDNSResolver` and `LookupTXTFunc` are unchanged and report an unknown TTL (0).
- `gateway`: `GET`/`HEAD /ipfs/bafkqaaa?format=raw` now always returns `200` with an empty body, so probing clients keep marking the gateway as functional even when its backend cannot serve identity CIDs. `bitswap/network/httpnet` sends this [trustless gateway probe](https://specs.ipfs.tech/http-gateways/trustless-gateway/#dedicated-probe-paths) to check providers, and a failed probe drops the provider. Exported as `gateway.EmptyIdentityCID`. [#1179](https://github.com/ipfs/boxo/pull/1179)
- `path`: added `NewPathFromURI`, which accepts native IPFS URIs (`ipfs://cid`, `ipns://name`, `ipld://cid`, and the schemeless `ipfs:`/`ipns:`/`ipld:` forms) and rewrites them to canonical content paths, so values copied from browsers and other tools parse as-is. `NewPath` stays strict and still rejects URI-shaped input, leaving untrusted parsing such as DNSLink records unchanged. [#1182](https://github.com/ipfs/boxo/pull/1182)
- `blockstore`: `CachedBlockstore` now returns a value implementing the
Expand All @@ -32,6 +35,9 @@ enumeration. [#1184](https://github.com/ipfs/boxo/pull/1184)

### Changed

- `namesys`: a name that resolves through several hops (a DNSLink pointing at an IPNS name, or an IPNS chain) now uses the shortest TTL among them, so an update to any link in the chain reaches clients on time. Hops with an unknown TTL (0) are ignored; single-hop results are unchanged. [#329](https://github.com/ipfs/boxo/issues/329)
- `namesys`: TTLs in results now respect the operator's cap. `WithMaxCacheTTL` (in Kubo: `Ipns.MaxCacheTTL`) bounds the reported TTL, so `Cache-Control: max-age` never promises freshness past it. Cached results report their remaining lifetime instead of the original TTL, so a late cache hit no longer restarts the full caching period on clients. A cap of 0 still means "no local cache" and leaves the reported TTL alone. [#329](https://github.com/ipfs/boxo/issues/329)
- upgrade to `go-multiaddr-dns` [v0.6.0](https://github.com/multiformats/go-multiaddr-dns/releases/tag/v0.6.0) and `go-doh-resolver` [v0.6.0](https://github.com/libp2p/go-doh-resolver/releases/tag/v0.6.0), which add DNS TXT TTL reporting and turn on the DNSLink `Cache-Control` behavior above for DoH-backed setups.
- 🛠 `mfs`: `File.Open` now takes a `context.Context`. Writing to a file whose data has to be fetched from the network (for example, a lazy reference created with `ipfs files cp`) now uses that context, so the write stops when the context is cancelled, such as on a client timeout or when MFS shuts down, instead of waiting forever for a block that never arrives. Callers must add a context argument; pass the request's context to let a timeout cancel the write. [#1185](https://github.com/ipfs/boxo/pull/1185)
- 🧪 `testing`: tests now use `go-test` v0.4.0 and `math/rand/v2`. `go-test/random` no longer has a global seed or a global sequence, so one test can no longer change the values another test expects to generate deterministically, a class of failure that showed up as an intermittent break in an unrelated test. A generator can also be reused now, instead of being rebuilt for every value. [#1187](https://github.com/ipfs/boxo/pull/1187)
- upgrade to `go-libp2p-kad-dht` [v0.41.0](https://github.com/libp2p/go-libp2p-kad-dht/releases/tag/v0.41.0)
Expand Down
4 changes: 2 additions & 2 deletions examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ require (
github.com/koron/go-ssdp v0.0.6 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/libp2p/go-cidranger v1.1.0 // indirect
github.com/libp2p/go-doh-resolver v0.5.0 // indirect
github.com/libp2p/go-doh-resolver v0.6.0 // indirect
github.com/libp2p/go-flow-metrics v0.3.0 // indirect
github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect
github.com/libp2p/go-libp2p-kad-dht v0.41.0 // indirect
Expand All @@ -91,7 +91,7 @@ require (
github.com/mr-tron/base58 v1.3.0 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect
github.com/multiformats/go-multiaddr-dns v0.5.0 // indirect
github.com/multiformats/go-multiaddr-dns v0.6.0 // indirect
github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
github.com/multiformats/go-multibase v0.3.0 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect
Expand Down
8 changes: 4 additions & 4 deletions examples/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c=
github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic=
github.com/libp2p/go-doh-resolver v0.5.0 h1:4h7plVVW+XTS+oUBw2+8KfoM1jF6w8XmO7+skhePFdE=
github.com/libp2p/go-doh-resolver v0.5.0/go.mod h1:aPDxfiD2hNURgd13+hfo29z9IC22fv30ee5iM31RzxU=
github.com/libp2p/go-doh-resolver v0.6.0 h1:/UjWsDZ7CtF5WxojsgcWXNKa0uY1H9qnGO0cwbNF3N4=
github.com/libp2p/go-doh-resolver v0.6.0/go.mod h1:C/HWNVB7j7tC6VsiGKe5Mi+m1/nfRK+dKAivZQDqeww=
github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784=
github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo=
github.com/libp2p/go-libp2p v0.48.0 h1:h2BrLAgrj7X8bEN05K7qmrjpNHYA+6tnsGRdprjTnvo=
Expand Down Expand Up @@ -232,8 +232,8 @@ github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a
github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo=
github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw=
github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0=
github.com/multiformats/go-multiaddr-dns v0.5.0 h1:p/FTyHKX0nl59f+S+dEUe8HRK+i5Ow/QHMw8Nh3gPCo=
github.com/multiformats/go-multiaddr-dns v0.5.0/go.mod h1:yJ349b8TPIAANUyuOzn1oz9o22tV9f+06L+cCeMxC14=
github.com/multiformats/go-multiaddr-dns v0.6.0 h1:yKIW08WJHSPJ8bDAT2O/5fypCaUu9Bjl8r/1eJ4XAW8=
github.com/multiformats/go-multiaddr-dns v0.6.0/go.mod h1:dwIQwdORZfnNQCeS7xLXyn+7626oRmMsVP30Uronhf0=
github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E=
github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo=
github.com/multiformats/go-multibase v0.3.0 h1:8helZD2+4Db7NNWFiktk2NePbF0boolBe6bDQvM4r68=
Expand Down
12 changes: 12 additions & 0 deletions gateway/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ var defaultResolvers = map[string]string{
"eth.": "https://dns.eth.limo/dns-query",
}

// The DoH resolvers built here must keep reporting TXT TTLs: namesys detects
// the capability only at runtime, so without this assertion a signature drift
// in either dependency would surface as DNSLink Cache-Control silently losing
// its max-age instead of a compile error.
var _ madns.TXTWithTTLResolver = (*doh.Resolver)(nil)

func newResolver(url string, opts ...doh.Option) (madns.BasicResolver, error) {
if !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "http://") {
return nil, fmt.Errorf("invalid DoH resolver URL: %s", url)
Expand All @@ -32,6 +38,12 @@ func newResolver(url string, opts ...doh.Option) (madns.BasicResolver, error) {
// - Custom resolver for ENS: "eth." → "https://eth.link/dns-query"
// - Override the default OS resolver: "." → "https://doh.applied-privacy.net/query"
//
// Domains matched by a DoH entry report the DNS TTL of DNSLink TXT records,
// which the gateway turns into Cache-Control max-age. Domains that fall
// through to the OS resolver cannot report TTLs (Go's [net.Resolver] returns
// only record values); add a "." entry to route every lookup through DoH and
// cover all DNSLink domains.
//
// [FQDNs]: https://en.wikipedia.org/wiki/Fully_qualified_domain_name
// [DoH]: https://en.wikipedia.org/wiki/DNS_over_HTTPS
func NewDNSResolver(resolvers map[string]string, dohOpts ...doh.Option) (*madns.Resolver, error) {
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ require (
github.com/ipld/go-codec-dagpb v1.7.0
github.com/ipld/go-ipld-prime v0.24.0
github.com/libp2p/go-buffer-pool v0.1.0
github.com/libp2p/go-doh-resolver v0.5.0
github.com/libp2p/go-doh-resolver v0.6.0
github.com/libp2p/go-libp2p v0.48.0
github.com/libp2p/go-libp2p-kad-dht v0.41.0
github.com/libp2p/go-libp2p-record v0.3.1
Expand All @@ -46,7 +46,7 @@ require (
github.com/mr-tron/base58 v1.3.0
github.com/multiformats/go-base32 v0.1.0
github.com/multiformats/go-multiaddr v0.16.1
github.com/multiformats/go-multiaddr-dns v0.5.0
github.com/multiformats/go-multiaddr-dns v0.6.0
github.com/multiformats/go-multibase v0.3.0
github.com/multiformats/go-multicodec v0.10.0
github.com/multiformats/go-multihash v0.2.3
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c=
github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic=
github.com/libp2p/go-doh-resolver v0.5.0 h1:4h7plVVW+XTS+oUBw2+8KfoM1jF6w8XmO7+skhePFdE=
github.com/libp2p/go-doh-resolver v0.5.0/go.mod h1:aPDxfiD2hNURgd13+hfo29z9IC22fv30ee5iM31RzxU=
github.com/libp2p/go-doh-resolver v0.6.0 h1:/UjWsDZ7CtF5WxojsgcWXNKa0uY1H9qnGO0cwbNF3N4=
github.com/libp2p/go-doh-resolver v0.6.0/go.mod h1:C/HWNVB7j7tC6VsiGKe5Mi+m1/nfRK+dKAivZQDqeww=
github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784=
github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo=
github.com/libp2p/go-libp2p v0.48.0 h1:h2BrLAgrj7X8bEN05K7qmrjpNHYA+6tnsGRdprjTnvo=
Expand Down Expand Up @@ -229,8 +229,8 @@ github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a
github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo=
github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw=
github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0=
github.com/multiformats/go-multiaddr-dns v0.5.0 h1:p/FTyHKX0nl59f+S+dEUe8HRK+i5Ow/QHMw8Nh3gPCo=
github.com/multiformats/go-multiaddr-dns v0.5.0/go.mod h1:yJ349b8TPIAANUyuOzn1oz9o22tV9f+06L+cCeMxC14=
github.com/multiformats/go-multiaddr-dns v0.6.0 h1:yKIW08WJHSPJ8bDAT2O/5fypCaUu9Bjl8r/1eJ4XAW8=
github.com/multiformats/go-multiaddr-dns v0.6.0/go.mod h1:dwIQwdORZfnNQCeS7xLXyn+7626oRmMsVP30Uronhf0=
github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E=
github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo=
github.com/multiformats/go-multibase v0.3.0 h1:8helZD2+4Db7NNWFiktk2NePbF0boolBe6bDQvM4r68=
Expand Down
28 changes: 22 additions & 6 deletions namesys/dns_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,31 @@ import (
// LookupTXTFunc is a function that lookups TXT record values.
type LookupTXTFunc func(ctx context.Context, name string) (txt []string, err error)

// LookupTXTWithTTLFunc is like [LookupTXTFunc] but also returns how long the TXT
// records may be cached. A TTL of 0 means the TTL is unknown.
type LookupTXTWithTTLFunc func(ctx context.Context, name string) (txt []string, ttl time.Duration, err error)

// DNSResolver implements [Resolver] on DNS domains.
type DNSResolver struct {
lookupTXT LookupTXTFunc
lookupTXT LookupTXTWithTTLFunc
}

var _ Resolver = &DNSResolver{}

// NewDNSResolver constructs a name resolver using DNS TXT records.
// NewDNSResolver constructs a name resolver from DNS TXT records. It reports an
// unknown TTL (0) for every result; use [NewDNSResolverWithTTL] when the lookup
// can report real TTLs.
func NewDNSResolver(lookup LookupTXTFunc) *DNSResolver {
return &DNSResolver{lookupTXT: func(ctx context.Context, name string) ([]string, time.Duration, error) {
txt, err := lookup(ctx, name)
return txt, 0, err
}}
}

// NewDNSResolverWithTTL is like [NewDNSResolver] but takes a lookup that reports
// each record's TTL. The TTL flows into the resolved result, so a gateway can
// set Cache-Control max-age from a DNSLink's TTL.
func NewDNSResolverWithTTL(lookup LookupTXTWithTTLFunc) *DNSResolver {
return &DNSResolver{lookupTXT: lookup}
}

Expand Down Expand Up @@ -85,7 +101,7 @@ func (r *DNSResolver) resolveOnceAsync(ctx context.Context, p path.Path, options
}
if subRes.Err == nil {
p, err := joinPaths(subRes.Path, p)
emitOnceResult(ctx, out, AsyncResult{Path: p, LastMod: time.Now(), Err: err})
emitOnceResult(ctx, out, AsyncResult{Path: p, TTL: subRes.TTL, LastMod: time.Now(), Err: err})
// Return without waiting for rootRes, since this result
// (for "_dnslink."+fqdn) takes precedence
} else {
Expand All @@ -107,7 +123,7 @@ func workDomain(ctx context.Context, r *DNSResolver, name string, res chan Async

defer close(res)

txt, err := r.lookupTXT(ctx, name)
txt, ttl, err := r.lookupTXT(ctx, name)
if err != nil {
var dnsErr *net.DNSError
if errors.As(err, &dnsErr) {
Expand Down Expand Up @@ -142,8 +158,8 @@ func workDomain(ctx context.Context, r *DNSResolver, name string, res chan Async
// There were no TXT records with a dnslink
res <- AsyncResult{Err: ErrMissingDNSLinkRecord}
case 1:
// Found 1 valid! Return it.
res <- AsyncResult{Path: paths[0]}
// Found 1 valid! Return it with the record TTL.
res <- AsyncResult{Path: paths[0], TTL: ttl}
default:
// Found more than 1 IPFS/IPNS path.
res <- AsyncResult{Err: ErrMultipleDNSLinkRecords}
Expand Down
47 changes: 46 additions & 1 deletion namesys/dns_resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"net"
"testing"
"time"

"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -152,7 +153,7 @@ func newMockDNS() *mockDNS {

func TestDNSResolution(t *testing.T) {
t.Parallel()
r := &DNSResolver{lookupTXT: newMockDNS().lookupTXT}
r := NewDNSResolver(newMockDNS().lookupTXT)

for _, testCase := range []struct {
name string
Expand Down Expand Up @@ -206,3 +207,47 @@ func TestDNSResolution(t *testing.T) {
})
}
}

func TestDNSResolutionWithTTL(t *testing.T) {
t.Parallel()

const ttl = 42 * time.Second
lookup := func(ctx context.Context, name string) ([]string, time.Duration, error) {
if name == "_dnslink.ttl.example.com." {
return []string{"dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD"}, ttl, nil
}
return nil, 0, &net.DNSError{IsNotFound: true}
}

// the TTL-aware resolver propagates the record TTL to the resolved result
r := NewDNSResolverWithTTL(lookup)
testResolution(t, r, "/ipns/ttl.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", ttl, nil)

// the legacy constructor reports an unknown TTL (0) for the same lookup
rNoTTL := NewDNSResolver(func(ctx context.Context, name string) ([]string, error) {
txt, _, err := lookup(ctx, name)
return txt, err
})
testResolution(t, rNoTTL, "/ipns/ttl.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 0, nil)
}

func TestMinNonZeroTTL(t *testing.T) {
t.Parallel()

for _, tc := range []struct {
a, b, want time.Duration
}{
{0, 0, 0}, // both unknown -> unknown
{0, 5 * time.Second, 5 * time.Second}, // one unknown -> the other
{5 * time.Second, 0, 5 * time.Second}, // one unknown -> the other
{3 * time.Second, 5 * time.Second, 3 * time.Second}, // both known -> min
{5 * time.Second, 3 * time.Second, 3 * time.Second}, // both known -> min
{-1 * time.Second, 5 * time.Second, 5 * time.Second}, // negative ignored -> the other
{-3 * time.Second, -5 * time.Second, 0}, // both negative -> never negative
{-1 * time.Second, 0, 0}, // negative and unknown -> never negative
} {
if got := minNonZeroTTL(tc.a, tc.b); got != tc.want {
t.Fatalf("minNonZeroTTL(%s, %s) = %s; want %s", tc.a, tc.b, got, tc.want)
}
}
}
36 changes: 31 additions & 5 deletions namesys/namesys.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ func WithCache(size int) Option {

// WithMaxCacheTTL configures the maximum cache TTL. By default, if the cache is
// enabled, the entry TTL will be used for caching. By setting this option, you
// can limit how long that TTL is.
// can limit how long that TTL is. A positive cap also applies to the TTL
// reported in resolution results, so a gateway deriving Cache-Control max-age
// from it stays within the configured bound. A cap of 0 or less only disables
// the cache; the reported TTL still comes from the record.
//
// For example, if you configure a maximum cache TTL of 1 minute:
// - Entry TTL is 5 minutes -> Cache TTL is 1 minute
Expand All @@ -91,11 +94,34 @@ func WithMaxCacheTTL(dur time.Duration) Option {
}
}

// WithDNSResolver is an option that supplies a custom DNS resolver to use instead
// of the system default.
// WithDNSResolver sets a custom DNS resolver in place of the system default. If
// that resolver also implements [madns.TXTWithTTLResolver], its TXT TTLs flow
// into resolved results, and from there into the gateway's Cache-Control header.
//
// The OS resolver cannot report TTLs: Go's [net.Resolver] returns only record
// values, and [madns.DefaultResolver] wraps it. TTLs flow only for domains
// routed through a resolver that can report them, such as a DNS-over-HTTPS
// resolver (go-doh-resolver). A [madns.Resolver] mixing DoH with the OS
// default reports real TTLs only for the DoH-covered domains; make DoH the
// default resolver to cover every domain.
func WithDNSResolver(rslv madns.BasicResolver) Option {
return func(ns *namesys) error {
ns.dnsResolver = NewDNSResolver(rslv.LookupTXT)
// A resolver that reports TXT TTLs (such as a DoH resolver via
// multiformats/go-multiaddr-dns#75) carries the DNSLink TTL through.
if ttlRslv, ok := rslv.(madns.TXTWithTTLResolver); ok {
ns.dnsResolver = NewDNSResolverWithTTL(ttlRslv.LookupTXTWithTTL)
} else {
ns.dnsResolver = NewDNSResolver(rslv.LookupTXT)
}
return nil
}
}

// WithDNSResolverWithTTL is like [WithDNSResolver] but takes a lookup that
// reports TXT TTLs directly.
func WithDNSResolverWithTTL(lookup LookupTXTWithTTLFunc) Option {
return func(ns *namesys) error {
ns.dnsResolver = NewDNSResolverWithTTL(lookup)
return nil
}
}
Expand Down Expand Up @@ -250,7 +276,7 @@ func (ns *namesys) resolveOnceAsync(ctx context.Context, p path.Path, options Re
res.Err = errors.Join(err, res.Err)
}

emitOnceResult(ctx, out, AsyncResult{Path: p, TTL: res.TTL, LastMod: res.LastMod, Err: res.Err})
emitOnceResult(ctx, out, AsyncResult{Path: p, TTL: ns.capTTL(res.TTL), LastMod: res.LastMod, Err: res.Err})
case <-ctx.Done():
return
}
Expand Down
Loading
Loading