feat: Add DNS tools with lookup, RDAP, and external tool links
Add comprehensive DNS tools module for network analysis:
DNS Lookup functionality:
- Support for A, AAAA, MX, TXT, NS, CNAME, SOA, PTR, SRV, CAA records
- DNSLookup() and DNSLookupAll() for single/complete lookups
- Configurable timeouts
- Structured result types for all record types
RDAP (new-style WHOIS) support:
- RDAPLookupDomain() for domain registration data
- RDAPLookupIP() for IP address information
- RDAPLookupASN() for autonomous system info
- Built-in server registry for common TLDs and RIRs
- ParseRDAPResponse() for extracting key domain info
External tool link generators:
- GetExternalToolLinks() - 20+ links for domain analysis
- GetExternalToolLinksIP() - IP-specific analysis tools
- GetExternalToolLinksEmail() - Email/domain verification
Tools include: MXToolbox (DNS, MX, SPF, DMARC, DKIM, blacklist),
DNSChecker, ViewDNS, IntoDNS, DNSViz, SecurityTrails, SSL Labs,
Shodan, Censys, IPInfo, AbuseIPDB, VirusTotal, and more.
WASM bindings expose link generators and RDAP URL builders
for use in TypeScript/browser environments.
2025-12-25 12:26:06 +00:00
|
|
|
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 (
|
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
|
|
|
// Standard record types
|
feat: Add DNS tools with lookup, RDAP, and external tool links
Add comprehensive DNS tools module for network analysis:
DNS Lookup functionality:
- Support for A, AAAA, MX, TXT, NS, CNAME, SOA, PTR, SRV, CAA records
- DNSLookup() and DNSLookupAll() for single/complete lookups
- Configurable timeouts
- Structured result types for all record types
RDAP (new-style WHOIS) support:
- RDAPLookupDomain() for domain registration data
- RDAPLookupIP() for IP address information
- RDAPLookupASN() for autonomous system info
- Built-in server registry for common TLDs and RIRs
- ParseRDAPResponse() for extracting key domain info
External tool link generators:
- GetExternalToolLinks() - 20+ links for domain analysis
- GetExternalToolLinksIP() - IP-specific analysis tools
- GetExternalToolLinksEmail() - Email/domain verification
Tools include: MXToolbox (DNS, MX, SPF, DMARC, DKIM, blacklist),
DNSChecker, ViewDNS, IntoDNS, DNSViz, SecurityTrails, SSL Labs,
Shodan, Censys, IPInfo, AbuseIPDB, VirusTotal, and more.
WASM bindings expose link generators and RDAP URL builders
for use in TypeScript/browser environments.
2025-12-25 12:26:06 +00:00
|
|
|
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"
|
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
|
|
|
|
|
|
|
|
// 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)
|
feat: Add DNS tools with lookup, RDAP, and external tool links
Add comprehensive DNS tools module for network analysis:
DNS Lookup functionality:
- Support for A, AAAA, MX, TXT, NS, CNAME, SOA, PTR, SRV, CAA records
- DNSLookup() and DNSLookupAll() for single/complete lookups
- Configurable timeouts
- Structured result types for all record types
RDAP (new-style WHOIS) support:
- RDAPLookupDomain() for domain registration data
- RDAPLookupIP() for IP address information
- RDAPLookupASN() for autonomous system info
- Built-in server registry for common TLDs and RIRs
- ParseRDAPResponse() for extracting key domain info
External tool link generators:
- GetExternalToolLinks() - 20+ links for domain analysis
- GetExternalToolLinksIP() - IP-specific analysis tools
- GetExternalToolLinksEmail() - Email/domain verification
Tools include: MXToolbox (DNS, MX, SPF, DMARC, DKIM, blacklist),
DNSChecker, ViewDNS, IntoDNS, DNSViz, SecurityTrails, SSL Labs,
Shodan, Censys, IPInfo, AbuseIPDB, VirusTotal, and more.
WASM bindings expose link generators and RDAP URL builders
for use in TypeScript/browser environments.
2025-12-25 12:26:06 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 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"`
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
// 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,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: Add DNS tools with lookup, RDAP, and external tool links
Add comprehensive DNS tools module for network analysis:
DNS Lookup functionality:
- Support for A, AAAA, MX, TXT, NS, CNAME, SOA, PTR, SRV, CAA records
- DNSLookup() and DNSLookupAll() for single/complete lookups
- Configurable timeouts
- Structured result types for all record types
RDAP (new-style WHOIS) support:
- RDAPLookupDomain() for domain registration data
- RDAPLookupIP() for IP address information
- RDAPLookupASN() for autonomous system info
- Built-in server registry for common TLDs and RIRs
- ParseRDAPResponse() for extracting key domain info
External tool link generators:
- GetExternalToolLinks() - 20+ links for domain analysis
- GetExternalToolLinksIP() - IP-specific analysis tools
- GetExternalToolLinksEmail() - Email/domain verification
Tools include: MXToolbox (DNS, MX, SPF, DMARC, DKIM, blacklist),
DNSChecker, ViewDNS, IntoDNS, DNSViz, SecurityTrails, SSL Labs,
Shodan, Censys, IPInfo, AbuseIPDB, VirusTotal, and more.
WASM bindings expose link generators and RDAP URL builders
for use in TypeScript/browser environments.
2025-12-25 12:26:06 +00:00
|
|
|
// 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
|
|
|
|
|
}
|