Poindexter/dns_tools.go
Claude 298791ef95
feat: Add extended DNS record types (ClouDNS compatible)
- Add support for 13 additional record types: ALIAS, RP, SSHFP, TLSA,
  DS, DNSKEY, NAPTR, LOC, HINFO, CERT, SMIMEA, WR (Web Redirect), SPF
- Add GetDNSRecordTypeInfo() for metadata with RFC references
- Add GetCommonDNSRecordTypes() for commonly used types
- Add structured types for CAA, SSHFP, TLSA, DS, DNSKEY, NAPTR, RP,
  LOC, ALIAS, and WebRedirect records
- Export new functions in WASM bindings
- Update TypeScript definitions and loader.js
- Add comprehensive tests for new record types
2025-12-25 12:38:32 +00:00

1006 lines
34 KiB
Go

package poindexter
import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"sort"
"strings"
"time"
)
// ============================================================================
// DNS Record Types
// ============================================================================
// DNSRecordType represents DNS record types
type DNSRecordType string
const (
// Standard record types
DNSRecordA DNSRecordType = "A"
DNSRecordAAAA DNSRecordType = "AAAA"
DNSRecordMX DNSRecordType = "MX"
DNSRecordTXT DNSRecordType = "TXT"
DNSRecordNS DNSRecordType = "NS"
DNSRecordCNAME DNSRecordType = "CNAME"
DNSRecordSOA DNSRecordType = "SOA"
DNSRecordPTR DNSRecordType = "PTR"
DNSRecordSRV DNSRecordType = "SRV"
DNSRecordCAA DNSRecordType = "CAA"
// Additional record types (ClouDNS and others)
DNSRecordALIAS DNSRecordType = "ALIAS" // Virtual ANAME record (ClouDNS, Route53, etc.)
DNSRecordRP DNSRecordType = "RP" // Responsible Person
DNSRecordSSHFP DNSRecordType = "SSHFP" // SSH Fingerprint
DNSRecordTLSA DNSRecordType = "TLSA" // DANE TLS Authentication
DNSRecordDS DNSRecordType = "DS" // DNSSEC Delegation Signer
DNSRecordDNSKEY DNSRecordType = "DNSKEY" // DNSSEC Key
DNSRecordNAPTR DNSRecordType = "NAPTR" // Naming Authority Pointer
DNSRecordLOC DNSRecordType = "LOC" // Geographic Location
DNSRecordHINFO DNSRecordType = "HINFO" // Host Information
DNSRecordCERT DNSRecordType = "CERT" // Certificate record
DNSRecordSMIMEA DNSRecordType = "SMIMEA" // S/MIME Certificate Association
DNSRecordWR DNSRecordType = "WR" // Web Redirect (ClouDNS specific)
DNSRecordSPF DNSRecordType = "SPF" // Sender Policy Framework (legacy, use TXT)
)
// DNSRecord represents a generic DNS record
type DNSRecord struct {
Type DNSRecordType `json:"type"`
Name string `json:"name"`
Value string `json:"value"`
TTL int `json:"ttl,omitempty"`
}
// MXRecord represents an MX record with priority
type MXRecord struct {
Host string `json:"host"`
Priority uint16 `json:"priority"`
}
// SRVRecord represents an SRV record
type SRVRecord struct {
Target string `json:"target"`
Port uint16 `json:"port"`
Priority uint16 `json:"priority"`
Weight uint16 `json:"weight"`
}
// SOARecord represents an SOA record
type SOARecord struct {
PrimaryNS string `json:"primaryNs"`
AdminEmail string `json:"adminEmail"`
Serial uint32 `json:"serial"`
Refresh uint32 `json:"refresh"`
Retry uint32 `json:"retry"`
Expire uint32 `json:"expire"`
MinTTL uint32 `json:"minTtl"`
}
// CAARecord represents a CAA record
type CAARecord struct {
Flag uint8 `json:"flag"`
Tag string `json:"tag"` // "issue", "issuewild", "iodef"
Value string `json:"value"`
}
// SSHFPRecord represents an SSHFP record
type SSHFPRecord struct {
Algorithm uint8 `json:"algorithm"` // 1=RSA, 2=DSA, 3=ECDSA, 4=Ed25519
FPType uint8 `json:"fpType"` // 1=SHA-1, 2=SHA-256
Fingerprint string `json:"fingerprint"`
}
// TLSARecord represents a TLSA (DANE) record
type TLSARecord struct {
Usage uint8 `json:"usage"` // 0-3: CA constraint, Service cert, Trust anchor, Domain-issued
Selector uint8 `json:"selector"` // 0=Full cert, 1=SubjectPublicKeyInfo
MatchingType uint8 `json:"matchingType"` // 0=Exact, 1=SHA-256, 2=SHA-512
CertData string `json:"certData"`
}
// DSRecord represents a DS (DNSSEC Delegation Signer) record
type DSRecord struct {
KeyTag uint16 `json:"keyTag"`
Algorithm uint8 `json:"algorithm"`
DigestType uint8 `json:"digestType"`
Digest string `json:"digest"`
}
// DNSKEYRecord represents a DNSKEY record
type DNSKEYRecord struct {
Flags uint16 `json:"flags"`
Protocol uint8 `json:"protocol"`
Algorithm uint8 `json:"algorithm"`
PublicKey string `json:"publicKey"`
}
// NAPTRRecord represents a NAPTR record
type NAPTRRecord struct {
Order uint16 `json:"order"`
Preference uint16 `json:"preference"`
Flags string `json:"flags"`
Service string `json:"service"`
Regexp string `json:"regexp"`
Replacement string `json:"replacement"`
}
// RPRecord represents an RP (Responsible Person) record
type RPRecord struct {
Mailbox string `json:"mailbox"` // Email as DNS name (user.domain.com)
TxtDom string `json:"txtDom"` // Domain with TXT record containing more info
}
// LOCRecord represents a LOC (Location) record
type LOCRecord struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Altitude float64 `json:"altitude"`
Size float64 `json:"size"`
HPrecis float64 `json:"hPrecision"`
VPrecis float64 `json:"vPrecision"`
}
// ALIASRecord represents an ALIAS/ANAME record (provider-specific)
type ALIASRecord struct {
Target string `json:"target"`
}
// WebRedirectRecord represents a Web Redirect record (ClouDNS specific)
type WebRedirectRecord struct {
URL string `json:"url"`
RedirectType int `json:"redirectType"` // 301, 302, etc.
Frame bool `json:"frame"` // Frame redirect vs HTTP redirect
}
// DNSRecordTypeInfo provides metadata about a DNS record type
type DNSRecordTypeInfo struct {
Type DNSRecordType `json:"type"`
Name string `json:"name"`
Description string `json:"description"`
RFC string `json:"rfc,omitempty"`
Common bool `json:"common"` // Commonly used record type
}
// GetDNSRecordTypeInfo returns metadata for all supported DNS record types
func GetDNSRecordTypeInfo() []DNSRecordTypeInfo {
return []DNSRecordTypeInfo{
// Common record types
{DNSRecordA, "A", "IPv4 address record - maps hostname to IPv4", "RFC 1035", true},
{DNSRecordAAAA, "AAAA", "IPv6 address record - maps hostname to IPv6", "RFC 3596", true},
{DNSRecordCNAME, "CNAME", "Canonical name - alias to another domain", "RFC 1035", true},
{DNSRecordMX, "MX", "Mail exchanger - specifies mail servers", "RFC 1035", true},
{DNSRecordTXT, "TXT", "Text record - stores arbitrary text (SPF, DKIM, etc.)", "RFC 1035", true},
{DNSRecordNS, "NS", "Nameserver - delegates DNS zone to nameservers", "RFC 1035", true},
{DNSRecordSOA, "SOA", "Start of Authority - zone administration data", "RFC 1035", true},
{DNSRecordPTR, "PTR", "Pointer - reverse DNS lookup (IP to hostname)", "RFC 1035", true},
{DNSRecordSRV, "SRV", "Service - locates services (port, priority, weight)", "RFC 2782", true},
{DNSRecordCAA, "CAA", "Certification Authority Authorization", "RFC 6844", true},
// Additional/specialized record types
{DNSRecordALIAS, "ALIAS", "Virtual A record - CNAME-like for apex domain", "", true},
{DNSRecordRP, "RP", "Responsible Person - contact info for domain", "RFC 1183", false},
{DNSRecordSSHFP, "SSHFP", "SSH Fingerprint - SSH host key verification", "RFC 4255", false},
{DNSRecordTLSA, "TLSA", "DANE TLS Authentication - certificate pinning", "RFC 6698", false},
{DNSRecordDS, "DS", "Delegation Signer - DNSSEC chain of trust", "RFC 4034", false},
{DNSRecordDNSKEY, "DNSKEY", "DNSSEC public key", "RFC 4034", false},
{DNSRecordNAPTR, "NAPTR", "Naming Authority Pointer - ENUM, SIP routing", "RFC 2915", false},
{DNSRecordLOC, "LOC", "Location - geographic coordinates", "RFC 1876", false},
{DNSRecordHINFO, "HINFO", "Host Information - CPU and OS type", "RFC 1035", false},
{DNSRecordCERT, "CERT", "Certificate - stores certificates", "RFC 4398", false},
{DNSRecordSMIMEA, "SMIMEA", "S/MIME Certificate Association", "RFC 8162", false},
{DNSRecordSPF, "SPF", "Sender Policy Framework (legacy, use TXT)", "RFC 4408", false},
{DNSRecordWR, "WR", "Web Redirect - HTTP redirect (ClouDNS specific)", "", false},
}
}
// GetCommonDNSRecordTypes returns only commonly used record types
func GetCommonDNSRecordTypes() []DNSRecordType {
info := GetDNSRecordTypeInfo()
result := make([]DNSRecordType, 0)
for _, r := range info {
if r.Common {
result = append(result, r.Type)
}
}
return result
}
// GetAllDNSRecordTypes returns all supported record types
func GetAllDNSRecordTypes() []DNSRecordType {
return []DNSRecordType{
DNSRecordA, DNSRecordAAAA, DNSRecordCNAME, DNSRecordMX, DNSRecordTXT,
DNSRecordNS, DNSRecordSOA, DNSRecordPTR, DNSRecordSRV, DNSRecordCAA,
DNSRecordALIAS, DNSRecordRP, DNSRecordSSHFP, DNSRecordTLSA, DNSRecordDS,
DNSRecordDNSKEY, DNSRecordNAPTR, DNSRecordLOC, DNSRecordHINFO, DNSRecordCERT,
DNSRecordSMIMEA, DNSRecordSPF, DNSRecordWR,
}
}
// DNSLookupResult contains the results of a DNS lookup
type DNSLookupResult struct {
Domain string `json:"domain"`
QueryType string `json:"queryType"`
Records []DNSRecord `json:"records"`
MXRecords []MXRecord `json:"mxRecords,omitempty"`
SRVRecords []SRVRecord `json:"srvRecords,omitempty"`
SOARecord *SOARecord `json:"soaRecord,omitempty"`
LookupTimeMs int64 `json:"lookupTimeMs"`
Error string `json:"error,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
// CompleteDNSLookup contains all DNS records for a domain
type CompleteDNSLookup struct {
Domain string `json:"domain"`
A []string `json:"a,omitempty"`
AAAA []string `json:"aaaa,omitempty"`
MX []MXRecord `json:"mx,omitempty"`
NS []string `json:"ns,omitempty"`
TXT []string `json:"txt,omitempty"`
CNAME string `json:"cname,omitempty"`
SOA *SOARecord `json:"soa,omitempty"`
LookupTimeMs int64 `json:"lookupTimeMs"`
Errors []string `json:"errors,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
// ============================================================================
// DNS Lookup Functions
// ============================================================================
// DNSLookup performs a DNS lookup for the specified record type
func DNSLookup(domain string, recordType DNSRecordType) DNSLookupResult {
return DNSLookupWithTimeout(domain, recordType, 10*time.Second)
}
// DNSLookupWithTimeout performs a DNS lookup with a custom timeout
func DNSLookupWithTimeout(domain string, recordType DNSRecordType, timeout time.Duration) DNSLookupResult {
start := time.Now()
result := DNSLookupResult{
Domain: domain,
QueryType: string(recordType),
Timestamp: start,
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
resolver := net.Resolver{}
switch recordType {
case DNSRecordA:
ips, err := resolver.LookupIP(ctx, "ip4", domain)
if err != nil {
result.Error = err.Error()
} else {
for _, ip := range ips {
result.Records = append(result.Records, DNSRecord{
Type: DNSRecordA,
Name: domain,
Value: ip.String(),
})
}
}
case DNSRecordAAAA:
ips, err := resolver.LookupIP(ctx, "ip6", domain)
if err != nil {
result.Error = err.Error()
} else {
for _, ip := range ips {
result.Records = append(result.Records, DNSRecord{
Type: DNSRecordAAAA,
Name: domain,
Value: ip.String(),
})
}
}
case DNSRecordMX:
mxs, err := resolver.LookupMX(ctx, domain)
if err != nil {
result.Error = err.Error()
} else {
for _, mx := range mxs {
result.MXRecords = append(result.MXRecords, MXRecord{
Host: strings.TrimSuffix(mx.Host, "."),
Priority: mx.Pref,
})
result.Records = append(result.Records, DNSRecord{
Type: DNSRecordMX,
Name: domain,
Value: fmt.Sprintf("%d %s", mx.Pref, mx.Host),
})
}
// Sort by priority
sort.Slice(result.MXRecords, func(i, j int) bool {
return result.MXRecords[i].Priority < result.MXRecords[j].Priority
})
}
case DNSRecordTXT:
txts, err := resolver.LookupTXT(ctx, domain)
if err != nil {
result.Error = err.Error()
} else {
for _, txt := range txts {
result.Records = append(result.Records, DNSRecord{
Type: DNSRecordTXT,
Name: domain,
Value: txt,
})
}
}
case DNSRecordNS:
nss, err := resolver.LookupNS(ctx, domain)
if err != nil {
result.Error = err.Error()
} else {
for _, ns := range nss {
result.Records = append(result.Records, DNSRecord{
Type: DNSRecordNS,
Name: domain,
Value: strings.TrimSuffix(ns.Host, "."),
})
}
}
case DNSRecordCNAME:
cname, err := resolver.LookupCNAME(ctx, domain)
if err != nil {
result.Error = err.Error()
} else {
result.Records = append(result.Records, DNSRecord{
Type: DNSRecordCNAME,
Name: domain,
Value: strings.TrimSuffix(cname, "."),
})
}
case DNSRecordSRV:
// SRV records require a service and protocol prefix, e.g., _http._tcp.example.com
_, srvs, err := resolver.LookupSRV(ctx, "", "", domain)
if err != nil {
result.Error = err.Error()
} else {
for _, srv := range srvs {
result.SRVRecords = append(result.SRVRecords, SRVRecord{
Target: strings.TrimSuffix(srv.Target, "."),
Port: srv.Port,
Priority: srv.Priority,
Weight: srv.Weight,
})
result.Records = append(result.Records, DNSRecord{
Type: DNSRecordSRV,
Name: domain,
Value: fmt.Sprintf("%d %d %d %s", srv.Priority, srv.Weight, srv.Port, srv.Target),
})
}
}
case DNSRecordPTR:
names, err := resolver.LookupAddr(ctx, domain)
if err != nil {
result.Error = err.Error()
} else {
for _, name := range names {
result.Records = append(result.Records, DNSRecord{
Type: DNSRecordPTR,
Name: domain,
Value: strings.TrimSuffix(name, "."),
})
}
}
default:
result.Error = fmt.Sprintf("unsupported record type: %s", recordType)
}
result.LookupTimeMs = time.Since(start).Milliseconds()
return result
}
// DNSLookupAll performs lookups for all common record types
func DNSLookupAll(domain string) CompleteDNSLookup {
return DNSLookupAllWithTimeout(domain, 10*time.Second)
}
// DNSLookupAllWithTimeout performs lookups for all common record types with timeout
func DNSLookupAllWithTimeout(domain string, timeout time.Duration) CompleteDNSLookup {
start := time.Now()
result := CompleteDNSLookup{
Domain: domain,
Timestamp: start,
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
resolver := net.Resolver{}
// A records
if ips, err := resolver.LookupIP(ctx, "ip4", domain); err == nil {
for _, ip := range ips {
result.A = append(result.A, ip.String())
}
} else if !isNoSuchHostError(err) {
result.Errors = append(result.Errors, fmt.Sprintf("A: %s", err.Error()))
}
// AAAA records
if ips, err := resolver.LookupIP(ctx, "ip6", domain); err == nil {
for _, ip := range ips {
result.AAAA = append(result.AAAA, ip.String())
}
} else if !isNoSuchHostError(err) {
result.Errors = append(result.Errors, fmt.Sprintf("AAAA: %s", err.Error()))
}
// MX records
if mxs, err := resolver.LookupMX(ctx, domain); err == nil {
for _, mx := range mxs {
result.MX = append(result.MX, MXRecord{
Host: strings.TrimSuffix(mx.Host, "."),
Priority: mx.Pref,
})
}
sort.Slice(result.MX, func(i, j int) bool {
return result.MX[i].Priority < result.MX[j].Priority
})
} else if !isNoSuchHostError(err) {
result.Errors = append(result.Errors, fmt.Sprintf("MX: %s", err.Error()))
}
// NS records
if nss, err := resolver.LookupNS(ctx, domain); err == nil {
for _, ns := range nss {
result.NS = append(result.NS, strings.TrimSuffix(ns.Host, "."))
}
} else if !isNoSuchHostError(err) {
result.Errors = append(result.Errors, fmt.Sprintf("NS: %s", err.Error()))
}
// TXT records
if txts, err := resolver.LookupTXT(ctx, domain); err == nil {
result.TXT = txts
} else if !isNoSuchHostError(err) {
result.Errors = append(result.Errors, fmt.Sprintf("TXT: %s", err.Error()))
}
// CNAME record
if cname, err := resolver.LookupCNAME(ctx, domain); err == nil {
result.CNAME = strings.TrimSuffix(cname, ".")
// If CNAME equals domain, it's not really a CNAME
if result.CNAME == domain {
result.CNAME = ""
}
}
result.LookupTimeMs = time.Since(start).Milliseconds()
return result
}
// ReverseDNSLookup performs a reverse DNS lookup for an IP address
func ReverseDNSLookup(ip string) DNSLookupResult {
return DNSLookupWithTimeout(ip, DNSRecordPTR, 10*time.Second)
}
func isNoSuchHostError(err error) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), "no such host") ||
strings.Contains(err.Error(), "NXDOMAIN") ||
strings.Contains(err.Error(), "not found")
}
// ============================================================================
// RDAP (Registration Data Access Protocol) - New Style WHOIS
// ============================================================================
// RDAPResponse represents an RDAP response
type RDAPResponse struct {
// Common fields
Handle string `json:"handle,omitempty"`
LDHName string `json:"ldhName,omitempty"` // Domain name
UnicodeName string `json:"unicodeName,omitempty"`
Status []string `json:"status,omitempty"`
Events []RDAPEvent `json:"events,omitempty"`
Entities []RDAPEntity `json:"entities,omitempty"`
Nameservers []RDAPNs `json:"nameservers,omitempty"`
Links []RDAPLink `json:"links,omitempty"`
Remarks []RDAPRemark `json:"remarks,omitempty"`
Notices []RDAPNotice `json:"notices,omitempty"`
// Network-specific (for IP lookups)
StartAddress string `json:"startAddress,omitempty"`
EndAddress string `json:"endAddress,omitempty"`
IPVersion string `json:"ipVersion,omitempty"`
Name string `json:"name,omitempty"`
Type string `json:"type,omitempty"`
Country string `json:"country,omitempty"`
ParentHandle string `json:"parentHandle,omitempty"`
// Error fields
ErrorCode int `json:"errorCode,omitempty"`
Title string `json:"title,omitempty"`
Description []string `json:"description,omitempty"`
// Metadata
RawJSON string `json:"rawJson,omitempty"`
LookupTimeMs int64 `json:"lookupTimeMs"`
Timestamp time.Time `json:"timestamp"`
Error string `json:"error,omitempty"`
}
// RDAPEvent represents an RDAP event (registration, expiration, etc.)
type RDAPEvent struct {
EventAction string `json:"eventAction"`
EventDate string `json:"eventDate"`
EventActor string `json:"eventActor,omitempty"`
}
// RDAPEntity represents an entity (registrar, registrant, etc.)
type RDAPEntity struct {
Handle string `json:"handle,omitempty"`
Roles []string `json:"roles,omitempty"`
VCardArray []any `json:"vcardArray,omitempty"`
Entities []RDAPEntity `json:"entities,omitempty"`
Events []RDAPEvent `json:"events,omitempty"`
Links []RDAPLink `json:"links,omitempty"`
Remarks []RDAPRemark `json:"remarks,omitempty"`
}
// RDAPNs represents a nameserver in RDAP
type RDAPNs struct {
LDHName string `json:"ldhName"`
IPAddresses *RDAPIPs `json:"ipAddresses,omitempty"`
}
// RDAPIPs represents IP addresses for a nameserver
type RDAPIPs struct {
V4 []string `json:"v4,omitempty"`
V6 []string `json:"v6,omitempty"`
}
// RDAPLink represents a link in RDAP
type RDAPLink struct {
Value string `json:"value,omitempty"`
Rel string `json:"rel,omitempty"`
Href string `json:"href,omitempty"`
Type string `json:"type,omitempty"`
}
// RDAPRemark represents a remark/notice
type RDAPRemark struct {
Title string `json:"title,omitempty"`
Description []string `json:"description,omitempty"`
Links []RDAPLink `json:"links,omitempty"`
}
// RDAPNotice is an alias for RDAPRemark
type RDAPNotice = RDAPRemark
// RDAPBootstrapRegistry holds the RDAP bootstrap data
type RDAPBootstrapRegistry struct {
Services [][]interface{} `json:"services"`
Version string `json:"version"`
}
// RDAP server URLs for different TLDs and RIRs
var rdapServers = map[string]string{
// Generic TLDs (ICANN)
"com": "https://rdap.verisign.com/com/v1/",
"net": "https://rdap.verisign.com/net/v1/",
"org": "https://rdap.publicinterestregistry.org/rdap/",
"info": "https://rdap.afilias.net/rdap/info/",
"biz": "https://rdap.afilias.net/rdap/biz/",
"io": "https://rdap.nic.io/",
"co": "https://rdap.nic.co/",
"me": "https://rdap.nic.me/",
"app": "https://rdap.nic.google/",
"dev": "https://rdap.nic.google/",
// Country code TLDs
"uk": "https://rdap.nominet.uk/uk/",
"de": "https://rdap.denic.de/",
"nl": "https://rdap.sidn.nl/",
"au": "https://rdap.auda.org.au/",
"nz": "https://rdap.dns.net.nz/",
"br": "https://rdap.registro.br/",
"jp": "https://rdap.jprs.jp/",
// RIRs for IP lookups
"arin": "https://rdap.arin.net/registry/",
"ripe": "https://rdap.db.ripe.net/",
"apnic": "https://rdap.apnic.net/",
"afrinic": "https://rdap.afrinic.net/rdap/",
"lacnic": "https://rdap.lacnic.net/rdap/",
}
// RDAPLookupDomain performs an RDAP lookup for a domain
func RDAPLookupDomain(domain string) RDAPResponse {
return RDAPLookupDomainWithTimeout(domain, 15*time.Second)
}
// RDAPLookupDomainWithTimeout performs an RDAP lookup with custom timeout
func RDAPLookupDomainWithTimeout(domain string, timeout time.Duration) RDAPResponse {
start := time.Now()
result := RDAPResponse{
LDHName: domain,
Timestamp: start,
}
// Extract TLD
parts := strings.Split(strings.ToLower(domain), ".")
if len(parts) < 2 {
result.Error = "invalid domain format"
result.LookupTimeMs = time.Since(start).Milliseconds()
return result
}
tld := parts[len(parts)-1]
// Find RDAP server
serverURL, ok := rdapServers[tld]
if !ok {
// Try to use IANA bootstrap
serverURL = fmt.Sprintf("https://rdap.org/domain/%s", domain)
} else {
serverURL = serverURL + "domain/" + domain
}
client := &http.Client{Timeout: timeout}
resp, err := client.Get(serverURL)
if err != nil {
result.Error = fmt.Sprintf("RDAP request failed: %s", err.Error())
result.LookupTimeMs = time.Since(start).Milliseconds()
return result
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
result.Error = fmt.Sprintf("failed to read response: %s", err.Error())
result.LookupTimeMs = time.Since(start).Milliseconds()
return result
}
result.RawJSON = string(body)
if resp.StatusCode != http.StatusOK {
result.Error = fmt.Sprintf("RDAP server returned status %d", resp.StatusCode)
result.LookupTimeMs = time.Since(start).Milliseconds()
return result
}
if err := json.Unmarshal(body, &result); err != nil {
result.Error = fmt.Sprintf("failed to parse RDAP response: %s", err.Error())
}
result.LookupTimeMs = time.Since(start).Milliseconds()
return result
}
// RDAPLookupIP performs an RDAP lookup for an IP address
func RDAPLookupIP(ip string) RDAPResponse {
return RDAPLookupIPWithTimeout(ip, 15*time.Second)
}
// RDAPLookupIPWithTimeout performs an RDAP lookup for an IP with custom timeout
func RDAPLookupIPWithTimeout(ip string, timeout time.Duration) RDAPResponse {
start := time.Now()
result := RDAPResponse{
StartAddress: ip,
Timestamp: start,
}
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
result.Error = "invalid IP address"
result.LookupTimeMs = time.Since(start).Milliseconds()
return result
}
// Use rdap.org as a universal redirector
serverURL := fmt.Sprintf("https://rdap.org/ip/%s", ip)
client := &http.Client{Timeout: timeout}
resp, err := client.Get(serverURL)
if err != nil {
result.Error = fmt.Sprintf("RDAP request failed: %s", err.Error())
result.LookupTimeMs = time.Since(start).Milliseconds()
return result
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
result.Error = fmt.Sprintf("failed to read response: %s", err.Error())
result.LookupTimeMs = time.Since(start).Milliseconds()
return result
}
result.RawJSON = string(body)
if resp.StatusCode != http.StatusOK {
result.Error = fmt.Sprintf("RDAP server returned status %d", resp.StatusCode)
result.LookupTimeMs = time.Since(start).Milliseconds()
return result
}
if err := json.Unmarshal(body, &result); err != nil {
result.Error = fmt.Sprintf("failed to parse RDAP response: %s", err.Error())
}
result.LookupTimeMs = time.Since(start).Milliseconds()
return result
}
// RDAPLookupASN performs an RDAP lookup for an ASN
func RDAPLookupASN(asn string) RDAPResponse {
return RDAPLookupASNWithTimeout(asn, 15*time.Second)
}
// RDAPLookupASNWithTimeout performs an RDAP lookup for an ASN with timeout
func RDAPLookupASNWithTimeout(asn string, timeout time.Duration) RDAPResponse {
start := time.Now()
result := RDAPResponse{
Handle: asn,
Timestamp: start,
}
// Normalize ASN (remove "AS" prefix if present)
asnNum := strings.TrimPrefix(strings.ToUpper(asn), "AS")
// Use rdap.org as a universal redirector
serverURL := fmt.Sprintf("https://rdap.org/autnum/%s", asnNum)
client := &http.Client{Timeout: timeout}
resp, err := client.Get(serverURL)
if err != nil {
result.Error = fmt.Sprintf("RDAP request failed: %s", err.Error())
result.LookupTimeMs = time.Since(start).Milliseconds()
return result
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
result.Error = fmt.Sprintf("failed to read response: %s", err.Error())
result.LookupTimeMs = time.Since(start).Milliseconds()
return result
}
result.RawJSON = string(body)
if resp.StatusCode != http.StatusOK {
result.Error = fmt.Sprintf("RDAP server returned status %d", resp.StatusCode)
result.LookupTimeMs = time.Since(start).Milliseconds()
return result
}
if err := json.Unmarshal(body, &result); err != nil {
result.Error = fmt.Sprintf("failed to parse RDAP response: %s", err.Error())
}
result.LookupTimeMs = time.Since(start).Milliseconds()
return result
}
// ============================================================================
// External Tool Links
// ============================================================================
// ExternalToolLinks contains links to external DNS/network analysis tools
type ExternalToolLinks struct {
// Target being analyzed
Target string `json:"target"`
Type string `json:"type"` // "domain", "ip", "email"
// MXToolbox links
MXToolboxDNS string `json:"mxtoolboxDns,omitempty"`
MXToolboxMX string `json:"mxtoolboxMx,omitempty"`
MXToolboxBlacklist string `json:"mxtoolboxBlacklist,omitempty"`
MXToolboxSMTP string `json:"mxtoolboxSmtp,omitempty"`
MXToolboxSPF string `json:"mxtoolboxSpf,omitempty"`
MXToolboxDMARC string `json:"mxtoolboxDmarc,omitempty"`
MXToolboxDKIM string `json:"mxtoolboxDkim,omitempty"`
MXToolboxHTTP string `json:"mxtoolboxHttp,omitempty"`
MXToolboxHTTPS string `json:"mxtoolboxHttps,omitempty"`
MXToolboxPing string `json:"mxtoolboxPing,omitempty"`
MXToolboxTrace string `json:"mxtoolboxTrace,omitempty"`
MXToolboxWhois string `json:"mxtoolboxWhois,omitempty"`
MXToolboxASN string `json:"mxtoolboxAsn,omitempty"`
// DNSChecker links
DNSCheckerDNS string `json:"dnscheckerDns,omitempty"`
DNSCheckerPropagation string `json:"dnscheckerPropagation,omitempty"`
// Other tools
WhoIs string `json:"whois,omitempty"`
ViewDNS string `json:"viewdns,omitempty"`
IntoDNS string `json:"intodns,omitempty"`
DNSViz string `json:"dnsviz,omitempty"`
SecurityTrails string `json:"securitytrails,omitempty"`
Shodan string `json:"shodan,omitempty"`
Censys string `json:"censys,omitempty"`
BuiltWith string `json:"builtwith,omitempty"`
SSLLabs string `json:"ssllabs,omitempty"`
HSTSPreload string `json:"hstsPreload,omitempty"`
Hardenize string `json:"hardenize,omitempty"`
// IP-specific tools
IPInfo string `json:"ipinfo,omitempty"`
AbuseIPDB string `json:"abuseipdb,omitempty"`
VirusTotal string `json:"virustotal,omitempty"`
ThreatCrowd string `json:"threatcrowd,omitempty"`
// Email-specific tools
MailTester string `json:"mailtester,omitempty"`
LearnDMARC string `json:"learndmarc,omitempty"`
}
// GetExternalToolLinks generates links to external analysis tools for a domain
func GetExternalToolLinks(domain string) ExternalToolLinks {
encoded := url.QueryEscape(domain)
return ExternalToolLinks{
Target: domain,
Type: "domain",
// MXToolbox
MXToolboxDNS: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=dns%%3a%s&run=toolpage", encoded),
MXToolboxMX: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=mx%%3a%s&run=toolpage", encoded),
MXToolboxBlacklist: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=blacklist%%3a%s&run=toolpage", encoded),
MXToolboxSMTP: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=smtp%%3a%s&run=toolpage", encoded),
MXToolboxSPF: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=spf%%3a%s&run=toolpage", encoded),
MXToolboxDMARC: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=dmarc%%3a%s&run=toolpage", encoded),
MXToolboxDKIM: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=dkim%%3a%s&run=toolpage", encoded),
MXToolboxHTTP: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=http%%3a%s&run=toolpage", encoded),
MXToolboxHTTPS: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=https%%3a%s&run=toolpage", encoded),
MXToolboxPing: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=ping%%3a%s&run=toolpage", encoded),
MXToolboxTrace: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=trace%%3a%s&run=toolpage", encoded),
MXToolboxWhois: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=whois%%3a%s&run=toolpage", encoded),
// DNSChecker
DNSCheckerDNS: fmt.Sprintf("https://dnschecker.org/#A/%s", encoded),
DNSCheckerPropagation: fmt.Sprintf("https://dnschecker.org/dns-propagation.php?domain=%s", encoded),
// Other tools
WhoIs: fmt.Sprintf("https://who.is/whois/%s", encoded),
ViewDNS: fmt.Sprintf("https://viewdns.info/dnsrecord/?domain=%s", encoded),
IntoDNS: fmt.Sprintf("https://intodns.com/%s", encoded),
DNSViz: fmt.Sprintf("https://dnsviz.net/d/%s/analyze/", encoded),
SecurityTrails: fmt.Sprintf("https://securitytrails.com/domain/%s", encoded),
BuiltWith: fmt.Sprintf("https://builtwith.com/%s", encoded),
SSLLabs: fmt.Sprintf("https://www.ssllabs.com/ssltest/analyze.html?d=%s", encoded),
HSTSPreload: fmt.Sprintf("https://hstspreload.org/?domain=%s", encoded),
Hardenize: fmt.Sprintf("https://www.hardenize.com/report/%s", encoded),
VirusTotal: fmt.Sprintf("https://www.virustotal.com/gui/domain/%s", encoded),
}
}
// GetExternalToolLinksIP generates links to external analysis tools for an IP
func GetExternalToolLinksIP(ip string) ExternalToolLinks {
encoded := url.QueryEscape(ip)
return ExternalToolLinks{
Target: ip,
Type: "ip",
// MXToolbox
MXToolboxBlacklist: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=blacklist%%3a%s&run=toolpage", encoded),
MXToolboxPing: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=ping%%3a%s&run=toolpage", encoded),
MXToolboxTrace: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=trace%%3a%s&run=toolpage", encoded),
MXToolboxWhois: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=whois%%3a%s&run=toolpage", encoded),
MXToolboxASN: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=asn%%3a%s&run=toolpage", encoded),
// IP-specific tools
IPInfo: fmt.Sprintf("https://ipinfo.io/%s", encoded),
AbuseIPDB: fmt.Sprintf("https://www.abuseipdb.com/check/%s", encoded),
VirusTotal: fmt.Sprintf("https://www.virustotal.com/gui/ip-address/%s", encoded),
Shodan: fmt.Sprintf("https://www.shodan.io/host/%s", encoded),
Censys: fmt.Sprintf("https://search.censys.io/hosts/%s", encoded),
ThreatCrowd: fmt.Sprintf("https://www.threatcrowd.org/ip.php?ip=%s", encoded),
}
}
// GetExternalToolLinksEmail generates links for email-related checks
func GetExternalToolLinksEmail(emailOrDomain string) ExternalToolLinks {
// Extract domain from email if needed
domain := emailOrDomain
if strings.Contains(emailOrDomain, "@") {
parts := strings.Split(emailOrDomain, "@")
if len(parts) == 2 {
domain = parts[1]
}
}
encoded := url.QueryEscape(domain)
emailEncoded := url.QueryEscape(emailOrDomain)
return ExternalToolLinks{
Target: emailOrDomain,
Type: "email",
// MXToolbox email checks
MXToolboxMX: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=mx%%3a%s&run=toolpage", encoded),
MXToolboxSMTP: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=smtp%%3a%s&run=toolpage", encoded),
MXToolboxSPF: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=spf%%3a%s&run=toolpage", encoded),
MXToolboxDMARC: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=dmarc%%3a%s&run=toolpage", encoded),
MXToolboxDKIM: fmt.Sprintf("https://mxtoolbox.com/SuperTool.aspx?action=dkim%%3a%s&run=toolpage", encoded),
// Email-specific tools
MailTester: fmt.Sprintf("https://www.mail-tester.com/test-%s", emailEncoded),
LearnDMARC: fmt.Sprintf("https://www.learndmarc.com/?domain=%s", encoded),
}
}
// ============================================================================
// Convenience Types for Parsed Results
// ============================================================================
// ParsedDomainInfo provides a simplified view of domain information
type ParsedDomainInfo struct {
Domain string `json:"domain"`
Registrar string `json:"registrar,omitempty"`
RegistrationDate string `json:"registrationDate,omitempty"`
ExpirationDate string `json:"expirationDate,omitempty"`
UpdatedDate string `json:"updatedDate,omitempty"`
Status []string `json:"status,omitempty"`
Nameservers []string `json:"nameservers,omitempty"`
DNSSEC bool `json:"dnssec"`
}
// ParseRDAPResponse extracts key information from an RDAP response
func ParseRDAPResponse(resp RDAPResponse) ParsedDomainInfo {
info := ParsedDomainInfo{
Domain: resp.LDHName,
Status: resp.Status,
}
// Extract dates from events
for _, event := range resp.Events {
switch event.EventAction {
case "registration":
info.RegistrationDate = event.EventDate
case "expiration":
info.ExpirationDate = event.EventDate
case "last changed", "last update":
info.UpdatedDate = event.EventDate
}
}
// Extract registrar from entities
for _, entity := range resp.Entities {
for _, role := range entity.Roles {
if role == "registrar" {
info.Registrar = entity.Handle
break
}
}
}
// Extract nameservers
for _, ns := range resp.Nameservers {
info.Nameservers = append(info.Nameservers, ns.LDHName)
}
// Check for DNSSEC
for _, status := range resp.Status {
if strings.Contains(strings.ToLower(status), "dnssec") ||
strings.Contains(strings.ToLower(status), "signed") {
info.DNSSEC = true
break
}
}
return info
}