Skip to content
Draft
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
7 changes: 7 additions & 0 deletions config/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,11 @@ type DNS struct {
Resolvers map[string]string
// MaxCacheTTL is the maximum duration DNS entries are valid in the cache.
MaxCacheTTL *OptionalDuration `json:",omitempty"`
// OverrideSystem controls whether DNS.Resolvers config is applied globally
// to all DNS lookups performed by the daemon, including third-party libraries.
// When enabled (default), net.DefaultResolver is replaced with one that uses
// the configured resolvers, ensuring consistent DNS behavior across the daemon.
// Set to false to use the OS resolver for code that doesn't explicitly use
// the Kubo DNS resolver (useful for testing or debugging).
OverrideSystem Flag `json:",omitempty"`
}
20 changes: 20 additions & 0 deletions core/node/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package node

import (
"math"
"net"
"time"

"github.com/ipfs/boxo/gateway"
config "github.com/ipfs/kubo/config"
"github.com/ipfs/kubo/core/node/libp2p"
doh "github.com/libp2p/go-doh-resolver"
madns "github.com/multiformats/go-multiaddr-dns"
"go.uber.org/fx"
)

func DNSResolver(cfg *config.Config) (*madns.Resolver, error) {
Expand All @@ -21,3 +24,20 @@ func DNSResolver(cfg *config.Config) (*madns.Resolver, error) {

return gateway.NewDNSResolver(resolvers, dohOpts...)
}

// OverrideDefaultResolver replaces net.DefaultResolver with one that uses
// the provided madns.Resolver. This ensures all Go code in the daemon
// (including third-party libraries like p2p-forge/client) respects the
// DNS.Resolvers configuration.
func OverrideDefaultResolver(resolver *madns.Resolver) {
net.DefaultResolver = libp2p.NewNetResolverFromMadns(resolver)
}

// maybeOverrideDefaultResolver returns an fx.Option that conditionally
// invokes OverrideDefaultResolver based on the DNS.OverrideSystem config flag.
func maybeOverrideDefaultResolver(enabled bool) fx.Option {
if enabled {
return fx.Invoke(OverrideDefaultResolver)
}
return fx.Options()
}
57 changes: 57 additions & 0 deletions core/node/dns_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package node

import (
"context"
"net"
"testing"

madns "github.com/multiformats/go-multiaddr-dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// mockResolver implements madns.BasicResolver for testing
type mockResolver struct {
txtRecords map[string][]string
ipRecords map[string][]net.IPAddr
}

func (m *mockResolver) LookupIPAddr(ctx context.Context, name string) ([]net.IPAddr, error) {
if m.ipRecords != nil {
return m.ipRecords[name], nil
}
return nil, nil
}

func (m *mockResolver) LookupTXT(ctx context.Context, name string) ([]string, error) {
if m.txtRecords != nil {
return m.txtRecords[name], nil
}
return nil, nil
}

func TestOverrideDefaultResolver(t *testing.T) {
// Save original resolver to restore after test
originalResolver := net.DefaultResolver
t.Cleanup(func() {
net.DefaultResolver = originalResolver
})

// Create mock with known records
mock := &mockResolver{
txtRecords: map[string][]string{
"test.override.example": {"override-test-value"},
},
}

madnsResolver, err := madns.NewResolver(madns.WithDefaultResolver(mock))
require.NoError(t, err)

// Override the default resolver
OverrideDefaultResolver(madnsResolver)

// Verify net.DefaultResolver now uses our mock
records, err := net.DefaultResolver.LookupTXT(t.Context(), "test.override.example")
require.NoError(t, err)
assert.Equal(t, []string{"override-test-value"}, records)
}
2 changes: 2 additions & 0 deletions core/node/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ func Online(bcfg *BuildCfg, cfg *config.Config, userResourceOverrides rcmgr.Part
fx.Provide(Bitswap(isBitswapServerEnabled, isBitswapLibp2pEnabled, isHTTPRetrievalEnabled)),
fx.Provide(OnlineExchange(isBitswapLibp2pEnabled)),
fx.Provide(DNSResolver),
maybeOverrideDefaultResolver(cfg.DNS.OverrideSystem.WithDefault(true)),
fx.Provide(Namesys(ipnsCacheSize, cfg.Ipns.MaxCacheTTL.WithDefault(config.DefaultIpnsMaxCacheTTL))),
fx.Provide(Peering),
PeerWith(cfg.Peering.Peers...),
Expand All @@ -373,6 +374,7 @@ func Offline(cfg *config.Config) fx.Option {
return fx.Options(
fx.Provide(offline.Exchange),
fx.Provide(DNSResolver),
maybeOverrideDefaultResolver(cfg.DNS.OverrideSystem.WithDefault(true)),
fx.Provide(Namesys(0, 0)),
fx.Provide(libp2p.Routing),
fx.Provide(libp2p.ContentRouting),
Expand Down
139 changes: 139 additions & 0 deletions core/node/libp2p/madns_net_resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package libp2p

import (
"bytes"
"context"
"encoding/binary"
"net"
"strings"
"time"

"github.com/miekg/dns"
madns "github.com/multiformats/go-multiaddr-dns"
)

// NewNetResolverFromMadns creates a *net.Resolver that uses madns.Resolver internally.
// This allows p2p-forge to use DNS.Resolvers config for ACME DNS-01 self-checks.
func NewNetResolverFromMadns(resolver *madns.Resolver) *net.Resolver {
return &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
return &madnsProxyConn{
resolver: resolver,
ctx: ctx,
}, nil
},
}
}

// madnsProxyConn implements net.Conn by proxying DNS queries to madns.Resolver.
// It intercepts DNS wire protocol, parses queries, calls the madns resolver,
// and returns properly formatted DNS responses.
type madnsProxyConn struct {
resolver *madns.Resolver
ctx context.Context
resp bytes.Buffer
}

func (c *madnsProxyConn) Write(p []byte) (int, error) {
c.resp.Reset()

// Go's net.Resolver with PreferGo=true uses TCP-style messages
// with 2-byte length prefix even for "udp" network
var queryData []byte
if len(p) >= 2 {
length := int(binary.BigEndian.Uint16(p[:2]))
if len(p) >= 2+length {
queryData = p[2 : 2+length]
} else {
queryData = p[2:] // partial data
}
} else {
queryData = p
}

if len(queryData) == 0 {
return len(p), nil
}

// Parse DNS message
var msg dns.Msg
if err := msg.Unpack(queryData); err != nil {
// Return len(p) to indicate we consumed the data, but don't fail
// The response buffer will be empty, causing Read to return EOF
return len(p), nil
}

// Build response
resp := &dns.Msg{}
resp.SetReply(&msg)
resp.Authoritative = true // Prevents "lame referral" errors

for _, q := range msg.Question {
name := strings.TrimSuffix(q.Name, ".")
switch q.Qtype {
case dns.TypeTXT:
records, err := c.resolver.LookupTXT(c.ctx, name)
if err == nil {
for _, txt := range records {
resp.Answer = append(resp.Answer, &dns.TXT{
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 300},
Txt: []string{txt},
})
}
}
case dns.TypeA:
addrs, err := c.resolver.LookupIPAddr(c.ctx, name)
if err == nil {
for _, addr := range addrs {
if ipv4 := addr.IP.To4(); ipv4 != nil {
resp.Answer = append(resp.Answer, &dns.A{
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
A: ipv4,
})
}
}
}
case dns.TypeAAAA:
addrs, err := c.resolver.LookupIPAddr(c.ctx, name)
if err == nil {
for _, addr := range addrs {
if addr.IP.To4() == nil && addr.IP.To16() != nil {
resp.Answer = append(resp.Answer, &dns.AAAA{
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 300},
AAAA: addr.IP,
})
}
}
}
default:
// Unsupported query type - return empty response (NODATA)
}
}

// Pack response
respData, err := resp.Pack()
if err != nil {
return len(p), err
}

// Go's pure-Go resolver (PreferGo=true) always uses TCP-style length prefix
// Write 2-byte big-endian length, then the response data
lengthBuf := make([]byte, 2)
binary.BigEndian.PutUint16(lengthBuf, uint16(len(respData)))
c.resp.Write(lengthBuf)
c.resp.Write(respData)

return len(p), nil
}

func (c *madnsProxyConn) Read(p []byte) (int, error) {
return c.resp.Read(p)
}

func (c *madnsProxyConn) Close() error { return nil }
func (c *madnsProxyConn) LocalAddr() net.Addr { return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1)} }
func (c *madnsProxyConn) RemoteAddr() net.Addr { return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1)} }
func (c *madnsProxyConn) SetDeadline(t time.Time) error { return nil }
func (c *madnsProxyConn) SetReadDeadline(t time.Time) error { return nil }
func (c *madnsProxyConn) SetWriteDeadline(t time.Time) error { return nil }
Loading
Loading