Skip to content

Commit 04eab65

Browse files
committed
feat(registry): allow rdap/whois resolving of Internet Numbers(ASN+IPs) and Domains)
1 parent 6ee0137 commit 04eab65

8 files changed

Lines changed: 719 additions & 0 deletions

File tree

cli/cli.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ type Flags struct {
3838
// Special query modes
3939
RecAXFR bool `long:"recaxfr" description:"Perform recursive AXFR"`
4040

41+
// Registry lookup (RDAP / port-43 WHOIS) — distinct from -w (bgptools ASN enrichment)
42+
Registry bool `short:"g" long:"registry" description:"Resolve target via RDAP with WHOIS fallback"`
43+
RegistryRDAP bool `long:"registry-rdap" description:"Resolve target via RDAP only"`
44+
RDAPServer string `long:"rdap-server" description:"RDAP base URL override (skips bootstrap)"`
45+
RIR string `long:"rir" description:"Force RIR for registry lookups: iana, arin, ripe, apnic, lacnic, afrinic" default:"iana"`
46+
RegistryWhois bool `long:"registry-whois" description:"Resolve target via port-43 WHOIS only"`
47+
WhoisServer string `long:"whois-server" description:"Port-43 WHOIS server override (host or host:port)"`
48+
4149
// Output
4250
Format string `short:"f" long:"format" description:"Output format (pretty, column, json, yaml, raw)" default:"pretty"`
4351
PrettyTTLs bool `long:"pretty-ttls" description:"Format TTLs in human readable format (default: true)"`

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/json-iterator/go v1.1.12
1111
github.com/miekg/dns v1.1.72
1212
github.com/natesales/bgptools-go v0.0.0-20230212051756-2b519d61269c
13+
github.com/openrdap/rdap v0.9.1
1314
github.com/quic-go/quic-go v0.59.0
1415
github.com/sthorne/odoh-go v1.0.4
1516
github.com/stretchr/testify v1.11.1
@@ -19,6 +20,8 @@ require (
1920

2021
require (
2122
github.com/AdguardTeam/golibs v0.35.9 // indirect
23+
github.com/alecthomas/kingpin/v2 v2.3.2 // indirect
24+
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
2225
github.com/ameshkov/dnsstamps v1.0.3 // indirect
2326
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
2427
github.com/charmbracelet/colorprofile v0.4.2 // indirect
@@ -37,12 +40,14 @@ require (
3740
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
3841
github.com/mattn/go-isatty v0.0.20 // indirect
3942
github.com/mattn/go-runewidth v0.0.20 // indirect
43+
github.com/mitchellh/go-homedir v1.1.0 // indirect
4044
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
4145
github.com/modern-go/reflect2 v1.0.2 // indirect
4246
github.com/muesli/termenv v0.16.0 // indirect
4347
github.com/pmezard/go-difflib v1.0.0 // indirect
4448
github.com/quic-go/qpack v0.6.0 // indirect
4549
github.com/rivo/uniseg v0.4.7 // indirect
50+
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
4651
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
4752
go.uber.org/mock v0.6.0 // indirect
4853
golang.org/x/crypto v0.48.0 // indirect

go.sum

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
git.schwanenlied.me/yawning/x448.git v0.0.0-20170617130356-01b048fb03d6/go.mod h1:wQaGCqEu44ykB17jZHCevrgSVl3KJnwQBObUtrKU4uU=
22
github.com/AdguardTeam/golibs v0.35.9 h1:KmmPBgE+PnXmxd+sp5fACRjU6oYT8r7okWEU3L9B6Qo=
33
github.com/AdguardTeam/golibs v0.35.9/go.mod h1:kuLQ0yNRTl0Em2FmmXtSri7ZdVT7p62oojyc51RvP38=
4+
github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU=
5+
github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
6+
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
7+
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
48
github.com/ameshkov/dnscrypt/v2 v2.4.0 h1:if6ZG2cuQmcP2TwSY+D0+8+xbPfoatufGlOQTMNkI9o=
59
github.com/ameshkov/dnscrypt/v2 v2.4.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI=
610
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
@@ -40,6 +44,8 @@ github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/
4044
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
4145
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
4246
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
47+
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
48+
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
4349
github.com/jedisct1/go-dnsstamps v0.0.0-20251112173516-191fc465df31 h1:O2qUxpVUeJxoBU5FJ0vGD03g5bY9uRrouaLlJeVOHr8=
4450
github.com/jedisct1/go-dnsstamps v0.0.0-20251112173516-191fc465df31/go.mod h1:mEGEFZsGe4sG5Mb3Xi89pmsy+TZ0946ArbYMGKAM5uA=
4551
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
@@ -58,6 +64,8 @@ github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjc
5864
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
5965
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
6066
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
67+
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
68+
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
6169
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
6270
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
6371
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -67,6 +75,8 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc
6775
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
6876
github.com/natesales/bgptools-go v0.0.0-20230212051756-2b519d61269c h1:bblm7D7Ld1/zkWvMU1j60lMk5h/F4AiKW23j1yffM5Y=
6977
github.com/natesales/bgptools-go v0.0.0-20230212051756-2b519d61269c/go.mod h1:jl8YnQACciyOXRgNIRhURrCF9FmRHjnfT8UCj3LBkyY=
78+
github.com/openrdap/rdap v0.9.1 h1:Rv6YbanbiVPsKRvOLdUmlU1AL5+2OFuEFLjFN+mQsCM=
79+
github.com/openrdap/rdap v0.9.1/go.mod h1:vKSiotbsENrjM/vaHXLddXbW8iQkBfa+ldEuYEjyLTQ=
7080
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
7181
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7282
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
@@ -81,9 +91,12 @@ github.com/sthorne/odoh-go v1.0.4 h1:RPJceVs/dIpNE3gyzk6wdSay+nR+tI1YXBUwykvCEXI
8191
github.com/sthorne/odoh-go v1.0.4/go.mod h1:KdB/NGiepr9bLVs3k26uWl4HHPHqa2DaoPUgUfKNmJU=
8292
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
8393
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
94+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
8495
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
8596
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
8697
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
98+
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
99+
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
87100
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
88101
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
89102
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
@@ -119,6 +132,7 @@ golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0
119132
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
120133
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
121134
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
135+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
122136
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
123137
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
124138
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

main.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
"github.com/natesales/q/cli"
2525
"github.com/natesales/q/output"
26+
"github.com/natesales/q/registry"
2627
"github.com/natesales/q/transport"
2728
"github.com/natesales/q/util"
2829
tlsutil "github.com/natesales/q/util/tls"
@@ -347,6 +348,33 @@ All long form (--) flags can be toggled with the dig-standard +[no]flag notation
347348
}
348349
}
349350

351+
// Registry lookup (RDAP / port-43 WHOIS)
352+
// rdap first, whois fallback, autoresolve LIR via -SUFFIX
353+
if opts.Registry || opts.RegistryRDAP || opts.RegistryWhois {
354+
if opts.Name == "" {
355+
return fmt.Errorf("registry lookup: target required")
356+
}
357+
useRDAP := opts.RegistryRDAP || opts.Registry
358+
useWHOIS := opts.RegistryWhois || opts.Registry
359+
ctx, cancel := context.WithTimeout(context.Background(), opts.Timeout)
360+
defer cancel()
361+
text, err := registry.Query(ctx, registry.Options{
362+
Target: opts.Name,
363+
UseRDAP: useRDAP,
364+
UseWHOIS: useWHOIS,
365+
RDAPServer: opts.RDAPServer,
366+
WhoisServer: opts.WhoisServer,
367+
RIR: strings.ToLower(opts.RIR),
368+
Format: opts.Format,
369+
Timeout: opts.Timeout,
370+
})
371+
if err != nil {
372+
return err
373+
}
374+
util.MustWritef(out, "%s\n", text)
375+
return nil
376+
}
377+
350378
// If no RR types are defined, set a list of default ones
351379
if len(rrTypes) < 1 {
352380
if opts.Name == "" {

registry/rdap.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package registry
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/url"
8+
"strings"
9+
10+
"github.com/openrdap/rdap"
11+
)
12+
13+
// queryRDAP performs an RDAP lookup using github.com/openrdap/rdap.
14+
// When opts.RDAPServer or a non-IANA RIR is set, the server URL is supplied
15+
// directly on the request, bypassing bootstrap.
16+
func queryRDAP(ctx context.Context, opts Options) (string, error) {
17+
kind := Classify(opts.Target)
18+
19+
req, err := buildRDAPRequest(kind, opts.Target)
20+
if err != nil {
21+
return "", err
22+
}
23+
24+
base := opts.RDAPServer
25+
if base == "" {
26+
base = rirRDAPBase(opts.RIR)
27+
}
28+
if base != "" {
29+
serverURL, err := url.Parse(base)
30+
if err != nil {
31+
return "", fmt.Errorf("registry: parse rdap server %q: %w", base, err)
32+
}
33+
req = req.WithServer(serverURL)
34+
}
35+
req = req.WithContext(ctx)
36+
req.Timeout = opts.Timeout
37+
38+
client := &rdap.Client{}
39+
resp, err := client.Do(req)
40+
if err != nil {
41+
return "", fmt.Errorf("registry: rdap: %w", err)
42+
}
43+
if resp == nil || resp.Object == nil {
44+
return "", fmt.Errorf("registry: rdap: empty response")
45+
}
46+
47+
return formatRDAP(resp, opts.Format)
48+
}
49+
50+
func buildRDAPRequest(kind Kind, target string) (*rdap.Request, error) {
51+
switch kind {
52+
case KindDomain:
53+
return &rdap.Request{Type: rdap.DomainRequest, Query: target}, nil
54+
case KindIPv4, KindIPv6:
55+
return &rdap.Request{Type: rdap.IPRequest, Query: target}, nil
56+
case KindASN:
57+
num := strings.TrimPrefix(strings.ToUpper(target), "AS")
58+
return &rdap.Request{Type: rdap.AutnumRequest, Query: num}, nil
59+
case KindNICHandle:
60+
return &rdap.Request{Type: rdap.EntityRequest, Query: target}, nil
61+
}
62+
return nil, fmt.Errorf("registry: cannot classify rdap target %q", target)
63+
}
64+
65+
func formatRDAP(resp *rdap.Response, format string) (string, error) {
66+
switch strings.ToLower(format) {
67+
case "json", "raw":
68+
b, err := json.MarshalIndent(resp.Object, "", " ")
69+
if err != nil {
70+
return "", fmt.Errorf("registry: marshal rdap: %w", err)
71+
}
72+
return string(b), nil
73+
}
74+
75+
if pretty := formatRDAPPretty(resp); pretty != "" {
76+
return pretty, nil
77+
}
78+
79+
b, err := json.MarshalIndent(resp.Object, "", " ")
80+
if err != nil {
81+
return "", fmt.Errorf("registry: marshal rdap: %w", err)
82+
}
83+
return string(b), nil
84+
}

0 commit comments

Comments
 (0)