Skip to content

Commit 2cbddc7

Browse files
committed
feat: add p2p-forge domain support
- Implement synthetic offline resolution for p2p-forge protocol ref. https://github.com/ipshipyard/p2p-forge - Support any domain suffix with valid libp2p peer IDs in second position - If domain turns out to be false-positive, it fallbacks to normal DNS resolver - Saves client from sending A/AAAA DNS query for each dns multiaddr that follows p2p-forge convention - Avoid expensive parsing if DNS labels are too short
1 parent 4af978f commit 2cbddc7

File tree

5 files changed

+744
-1
lines changed

5 files changed

+744
-1
lines changed

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
module github.com/multiformats/go-multiaddr-dns
22

33
require (
4+
github.com/ipfs/go-cid v0.0.7
45
github.com/miekg/dns v1.1.41
56
github.com/multiformats/go-multiaddr v0.13.0
7+
github.com/multiformats/go-multicodec v0.9.2
68
)
79

810
require (
9-
github.com/ipfs/go-cid v0.0.7 // indirect
1011
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
1112
github.com/minio/sha256-simd v1.0.1 // indirect
1213
github.com/mr-tron/base58 v1.2.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ github.com/multiformats/go-multiaddr v0.13.0/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamG
2525
github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc=
2626
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
2727
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
28+
github.com/multiformats/go-multicodec v0.9.2 h1:YrlXCuqxjqm3bXl+vBq5LKz5pz4mvAsugdqy78k0pXQ=
29+
github.com/multiformats/go-multicodec v0.9.2/go.mod h1:LLWNMtyV5ithSBUo3vFIMaeDy+h3EbkMTek1m+Fybbo=
2830
github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc=
2931
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
3032
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=

resolve.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,12 @@ func (r *Resolver) Resolve(ctx context.Context, maddr ma.Multiaddr) ([]ma.Multia
278278
}
279279

280280
func (r *Resolver) LookupIPAddr(ctx context.Context, domain string) ([]net.IPAddr, error) {
281+
if parts := parseP2PForgeDomain(domain); parts != nil {
282+
if addrs, err := r.resolveP2PForge(ctx, domain, parts); err == nil {
283+
return addrs, nil
284+
}
285+
// Fallback to normal resolver if synthetic offline resolution fails
286+
}
281287
return r.getResolver(domain).LookupIPAddr(ctx, domain)
282288
}
283289

resolve_static.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package madns
2+
3+
// This file contains performance optimizations where well-known DNS label conventions
4+
// are resolved statically to avoid unnecessary network I/O.
5+
//
6+
// Currently supports:
7+
// - p2p-forge protocol domains (deterministic IP address resolution): https://github.com/ipshipyard/p2p-forge
8+
9+
import (
10+
"context"
11+
"net"
12+
"strings"
13+
14+
"github.com/ipfs/go-cid"
15+
"github.com/multiformats/go-multicodec"
16+
)
17+
18+
const minLibp2pPeerIDLength = 42 // Conservative minimum per https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md
19+
20+
// minP2PForgeDomain is the minimum possible length for a valid p2p-forge domain
21+
// Format: <ip>.<peerID>.<suffix>
22+
// Shortest IPv4: "0-0-0-0" (7 chars), shortest peerID: 42 chars, shortest suffix: "a" (1 char), dots: 2
23+
const minP2PForgeDomain = 7 + 1 + minLibp2pPeerIDLength + 1 + 1 // 52 characters
24+
25+
// parseP2PForgeDomain checks if a domain follows the p2p-forge pattern
26+
// Format: <encoded-ip>.<peerID>.<suffix>
27+
// Returns the DNS labels if valid, nil otherwise
28+
func parseP2PForgeDomain(domain string) []string {
29+
// Quick length check to avoid splitting obviously too-short domains
30+
if len(domain) < minP2PForgeDomain {
31+
return nil
32+
}
33+
34+
parts := strings.Split(domain, ".")
35+
if len(parts) < 3 { // need at least <ip>.<peerID>.<suffix>
36+
return nil
37+
}
38+
39+
// Check if the second part (index 1) looks like a libp2p peer ID
40+
peerID := parts[1]
41+
if !isLibp2pPeerID(peerID) {
42+
return nil
43+
}
44+
45+
return parts
46+
}
47+
48+
// isLibp2pPeerID checks if a string is a valid libp2p peer ID
49+
// by parsing it as a CID and verifying it uses the libp2p-key codec
50+
func isLibp2pPeerID(s string) bool {
51+
// Only attempt CID parsing if string is long enough to be a valid base36 libp2p peer ID
52+
if len(s) < minLibp2pPeerIDLength {
53+
return false
54+
}
55+
56+
c, err := cid.Decode(s)
57+
if err != nil {
58+
return false
59+
}
60+
61+
// Check if the CID uses the libp2p-key codec
62+
return c.Type() == uint64(multicodec.Libp2pKey)
63+
}
64+
65+
// resolveP2PForge handles p2p-forge domains that encode IP addresses
66+
// according to the p2p-forge protocol specification via synthetic offline resolution.
67+
//
68+
// Domain format: <encoded-ip>.<base36-peerID>.<suffix>
69+
//
70+
// See: https://github.com/ipshipyard/p2p-forge?tab=readme-ov-file#handled-dns-records
71+
func (r *Resolver) resolveP2PForge(ctx context.Context, domain string, parts []string) ([]net.IPAddr, error) {
72+
// The first part is the encoded IP address
73+
encodedIP := parts[0]
74+
75+
// Try IPv6 first (as per spec), then IPv4
76+
if ip := decodeIPv6(encodedIP); ip != nil {
77+
return []net.IPAddr{{IP: ip}}, nil
78+
}
79+
80+
if ip := decodeIPv4(encodedIP); ip != nil {
81+
return []net.IPAddr{{IP: ip}}, nil
82+
}
83+
84+
return nil, &net.DNSError{
85+
Err: "invalid IP encoding in p2p-forge domain",
86+
Name: domain,
87+
Server: "",
88+
}
89+
}
90+
91+
// decodeIPv4 converts hyphens back to dots for IPv4 addresses
92+
// Example: 1-2-3-4 → 1.2.3.4
93+
func decodeIPv4(encoded string) net.IP {
94+
// Convert hyphens back to dots
95+
ipStr := strings.ReplaceAll(encoded, "-", ".")
96+
ip := net.ParseIP(ipStr)
97+
if ip == nil {
98+
return nil
99+
}
100+
// Ensure it's actually an IPv4 address
101+
if ip.To4() == nil {
102+
return nil
103+
}
104+
return ip
105+
}
106+
107+
// decodeIPv6 converts encoded IPv6 addresses back to standard format
108+
// Handles multiple encoding rules:
109+
// 1. Standard: A-B-C-D-1-2-3-4 → A:B:C:D:1:2:3:4
110+
// 2. Condensed: A--C-D → A::C:D
111+
// 3. Leading zeros: 0--B-C-D → ::B:C:D
112+
// 4. Trailing zeros: 1--0 → 1::
113+
func decodeIPv6(encoded string) net.IP {
114+
// Handle RFC 1123 compliance: replace leading/trailing 0 with empty string
115+
if strings.HasPrefix(encoded, "0--") {
116+
encoded = strings.TrimPrefix(encoded, "0")
117+
}
118+
if strings.HasSuffix(encoded, "--0") {
119+
encoded = strings.TrimSuffix(encoded, "0")
120+
}
121+
122+
// Replace -- with ::
123+
ipStr := strings.ReplaceAll(encoded, "--", "::")
124+
// Replace remaining hyphens with colons
125+
ipStr = strings.ReplaceAll(ipStr, "-", ":")
126+
127+
ip := net.ParseIP(ipStr)
128+
if ip == nil {
129+
return nil
130+
}
131+
// Ensure it's actually an IPv6 address
132+
if ip.To4() != nil {
133+
return nil
134+
}
135+
return ip
136+
}

0 commit comments

Comments
 (0)