From 298791ef95b528ebbbe60f440b1b75b72185b98c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Dec 2025 12:38:32 +0000 Subject: [PATCH] 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 --- dns_tools.go | 156 +++++++++++++++++++ dns_tools_test.go | 265 +++++++++++++++++++++++++++++++++ npm/poindexter-wasm/index.d.ts | 116 ++++++++++++++- npm/poindexter-wasm/loader.js | 4 +- wasm/main.go | 39 ++++- 5 files changed, 573 insertions(+), 7 deletions(-) diff --git a/dns_tools.go b/dns_tools.go index f1b31fa..de57110 100644 --- a/dns_tools.go +++ b/dns_tools.go @@ -21,6 +21,7 @@ import ( type DNSRecordType string const ( + // Standard record types DNSRecordA DNSRecordType = "A" DNSRecordAAAA DNSRecordType = "AAAA" DNSRecordMX DNSRecordType = "MX" @@ -31,6 +32,21 @@ const ( 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 @@ -66,6 +82,146 @@ type SOARecord struct { 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"` diff --git a/dns_tools_test.go b/dns_tools_test.go index 91e830d..ddcbfca 100644 --- a/dns_tools_test.go +++ b/dns_tools_test.go @@ -156,6 +156,136 @@ func TestDNSRecordTypes(t *testing.T) { } } +func TestDNSRecordTypesExtended(t *testing.T) { + // Test all ClouDNS record types are defined + types := []DNSRecordType{ + DNSRecordALIAS, + DNSRecordRP, + DNSRecordSSHFP, + DNSRecordTLSA, + DNSRecordDS, + DNSRecordDNSKEY, + DNSRecordNAPTR, + DNSRecordLOC, + DNSRecordHINFO, + DNSRecordCERT, + DNSRecordSMIMEA, + DNSRecordWR, + DNSRecordSPF, + } + + expected := []string{"ALIAS", "RP", "SSHFP", "TLSA", "DS", "DNSKEY", "NAPTR", "LOC", "HINFO", "CERT", "SMIMEA", "WR", "SPF"} + + for i, typ := range types { + if string(typ) != expected[i] { + t.Errorf("expected type %s, got %s", expected[i], typ) + } + } +} + +func TestGetDNSRecordTypeInfo(t *testing.T) { + info := GetDNSRecordTypeInfo() + + if len(info) == 0 { + t.Error("GetDNSRecordTypeInfo should return non-empty list") + } + + // Check that common types exist + commonFound := 0 + for _, r := range info { + if r.Common { + commonFound++ + } + // Each entry should have type, name, and description + if r.Type == "" { + t.Error("Record type should not be empty") + } + if r.Name == "" { + t.Error("Record name should not be empty") + } + if r.Description == "" { + t.Error("Record description should not be empty") + } + } + + if commonFound < 10 { + t.Errorf("Expected at least 10 common record types, got %d", commonFound) + } + + // Check for specific types + typeMap := make(map[DNSRecordType]DNSRecordTypeInfo) + for _, r := range info { + typeMap[r.Type] = r + } + + if _, ok := typeMap[DNSRecordA]; !ok { + t.Error("A record type should be in info") + } + if _, ok := typeMap[DNSRecordALIAS]; !ok { + t.Error("ALIAS record type should be in info") + } + if _, ok := typeMap[DNSRecordTLSA]; !ok { + t.Error("TLSA record type should be in info") + } + if _, ok := typeMap[DNSRecordWR]; !ok { + t.Error("WR (Web Redirect) record type should be in info") + } +} + +func TestGetCommonDNSRecordTypes(t *testing.T) { + types := GetCommonDNSRecordTypes() + + if len(types) == 0 { + t.Error("GetCommonDNSRecordTypes should return non-empty list") + } + + // Check that standard types are present + typeSet := make(map[DNSRecordType]bool) + for _, typ := range types { + typeSet[typ] = true + } + + if !typeSet[DNSRecordA] { + t.Error("A record should be in common types") + } + if !typeSet[DNSRecordAAAA] { + t.Error("AAAA record should be in common types") + } + if !typeSet[DNSRecordMX] { + t.Error("MX record should be in common types") + } + if !typeSet[DNSRecordTXT] { + t.Error("TXT record should be in common types") + } + if !typeSet[DNSRecordALIAS] { + t.Error("ALIAS record should be in common types") + } +} + +func TestGetAllDNSRecordTypes(t *testing.T) { + types := GetAllDNSRecordTypes() + + if len(types) < 20 { + t.Errorf("GetAllDNSRecordTypes should return at least 20 types, got %d", len(types)) + } + + // Check for ClouDNS-specific types + typeSet := make(map[DNSRecordType]bool) + for _, typ := range types { + typeSet[typ] = true + } + + if !typeSet[DNSRecordWR] { + t.Error("WR (Web Redirect) should be in all types") + } + if !typeSet[DNSRecordNAPTR] { + t.Error("NAPTR should be in all types") + } + if !typeSet[DNSRecordDS] { + t.Error("DS should be in all types") + } +} + func TestDNSLookupResultStructure(t *testing.T) { result := DNSLookupResult{ Domain: "example.com", @@ -400,6 +530,141 @@ func TestSOARecordStructure(t *testing.T) { } } +// ============================================================================ +// Extended Record Type Structure Tests +// ============================================================================ + +func TestCAARecordStructure(t *testing.T) { + caa := CAARecord{ + Flag: 0, + Tag: "issue", + Value: "letsencrypt.org", + } + + if caa.Tag != "issue" { + t.Error("Tag should be 'issue'") + } + if caa.Value != "letsencrypt.org" { + t.Error("Value should be set") + } +} + +func TestSSHFPRecordStructure(t *testing.T) { + sshfp := SSHFPRecord{ + Algorithm: 4, // Ed25519 + FPType: 2, // SHA-256 + Fingerprint: "abc123def456", + } + + if sshfp.Algorithm != 4 { + t.Error("Algorithm should be 4 (Ed25519)") + } + if sshfp.FPType != 2 { + t.Error("FPType should be 2 (SHA-256)") + } +} + +func TestTLSARecordStructure(t *testing.T) { + tlsa := TLSARecord{ + Usage: 3, // Domain-issued certificate + Selector: 1, // SubjectPublicKeyInfo + MatchingType: 1, // SHA-256 + CertData: "abcd1234", + } + + if tlsa.Usage != 3 { + t.Error("Usage should be 3") + } + if tlsa.Selector != 1 { + t.Error("Selector should be 1") + } +} + +func TestDSRecordStructure(t *testing.T) { + ds := DSRecord{ + KeyTag: 12345, + Algorithm: 13, // ECDSAP256SHA256 + DigestType: 2, // SHA-256 + Digest: "deadbeef", + } + + if ds.KeyTag != 12345 { + t.Error("KeyTag should be 12345") + } + if ds.Algorithm != 13 { + t.Error("Algorithm should be 13") + } +} + +func TestNAPTRRecordStructure(t *testing.T) { + naptr := NAPTRRecord{ + Order: 100, + Preference: 10, + Flags: "U", + Service: "E2U+sip", + Regexp: "!^.*$!sip:info@example.com!", + Replacement: ".", + } + + if naptr.Order != 100 { + t.Error("Order should be 100") + } + if naptr.Service != "E2U+sip" { + t.Error("Service should be E2U+sip") + } +} + +func TestRPRecordStructure(t *testing.T) { + rp := RPRecord{ + Mailbox: "admin.example.com", + TxtDom: "info.example.com", + } + + if rp.Mailbox != "admin.example.com" { + t.Error("Mailbox should be set") + } +} + +func TestLOCRecordStructure(t *testing.T) { + loc := LOCRecord{ + Latitude: 51.5074, + Longitude: -0.1278, + Altitude: 11, + Size: 10, + HPrecis: 10, + VPrecis: 10, + } + + if loc.Latitude < 51.5 || loc.Latitude > 51.6 { + t.Error("Latitude should be near 51.5074") + } +} + +func TestALIASRecordStructure(t *testing.T) { + alias := ALIASRecord{ + Target: "target.example.com", + } + + if alias.Target != "target.example.com" { + t.Error("Target should be set") + } +} + +func TestWebRedirectRecordStructure(t *testing.T) { + wr := WebRedirectRecord{ + URL: "https://www.example.com", + RedirectType: 301, + Frame: false, + } + + if wr.URL != "https://www.example.com" { + t.Error("URL should be set") + } + if wr.RedirectType != 301 { + t.Error("RedirectType should be 301") + } +} + // ============================================================================ // Helper Function Tests // ============================================================================ diff --git a/npm/poindexter-wasm/index.d.ts b/npm/poindexter-wasm/index.d.ts index 8837fe9..6c55e1c 100644 --- a/npm/poindexter-wasm/index.d.ts +++ b/npm/poindexter-wasm/index.d.ts @@ -193,8 +193,118 @@ export interface InitOptions { // DNS Tools Types // ============================================================================ -/** DNS record types */ -export type DNSRecordType = 'A' | 'AAAA' | 'MX' | 'TXT' | 'NS' | 'CNAME' | 'SOA' | 'PTR' | 'SRV' | 'CAA'; +/** DNS record types - standard and extended (ClouDNS compatible) */ +export type DNSRecordType = + // Standard record types + | 'A' + | 'AAAA' + | 'MX' + | 'TXT' + | 'NS' + | 'CNAME' + | 'SOA' + | 'PTR' + | 'SRV' + | 'CAA' + // Additional record types (ClouDNS and others) + | 'ALIAS' // Virtual A record - CNAME-like for apex domain + | 'RP' // Responsible Person + | 'SSHFP' // SSH Fingerprint + | 'TLSA' // DANE TLS Authentication + | 'DS' // DNSSEC Delegation Signer + | 'DNSKEY' // DNSSEC Key + | 'NAPTR' // Naming Authority Pointer + | 'LOC' // Geographic Location + | 'HINFO' // Host Information + | 'CERT' // Certificate record + | 'SMIMEA' // S/MIME Certificate Association + | 'WR' // Web Redirect (ClouDNS specific) + | 'SPF'; // Sender Policy Framework (legacy) + +/** DNS record type metadata */ +export interface DNSRecordTypeInfo { + type: DNSRecordType; + name: string; + description: string; + rfc?: string; + common: boolean; +} + +/** CAA record */ +export interface CAARecord { + flag: number; + tag: string; // "issue", "issuewild", "iodef" + value: string; +} + +/** SSHFP record */ +export interface SSHFPRecord { + algorithm: number; // 1=RSA, 2=DSA, 3=ECDSA, 4=Ed25519 + fpType: number; // 1=SHA-1, 2=SHA-256 + fingerprint: string; +} + +/** TLSA (DANE) record */ +export interface TLSARecord { + usage: number; // 0-3: CA constraint, Service cert, Trust anchor, Domain-issued + selector: number; // 0=Full cert, 1=SubjectPublicKeyInfo + matchingType: number; // 0=Exact, 1=SHA-256, 2=SHA-512 + certData: string; +} + +/** DS (DNSSEC Delegation Signer) record */ +export interface DSRecord { + keyTag: number; + algorithm: number; + digestType: number; + digest: string; +} + +/** DNSKEY record */ +export interface DNSKEYRecord { + flags: number; + protocol: number; + algorithm: number; + publicKey: string; +} + +/** NAPTR record */ +export interface NAPTRRecord { + order: number; + preference: number; + flags: string; + service: string; + regexp: string; + replacement: string; +} + +/** RP (Responsible Person) record */ +export interface RPRecord { + mailbox: string; // Email as DNS name (user.domain.com) + txtDom: string; // Domain with TXT record containing more info +} + +/** LOC (Location) record */ +export interface LOCRecord { + latitude: number; + longitude: number; + altitude: number; + size: number; + hPrecision: number; + vPrecision: number; +} + +/** ALIAS record (provider-specific) */ +export interface ALIASRecord { + target: string; +} + +/** Web Redirect record (ClouDNS specific) */ +export interface WebRedirectRecord { + url: string; + redirectType: number; // 301, 302, etc. + frame: boolean; // Frame redirect vs HTTP redirect +} /** External tool links for domain/IP/email analysis */ export interface ExternalToolLinks { @@ -429,6 +539,8 @@ export interface PxAPI { buildRDAPIPURL(ip: string): Promise; buildRDAPASNURL(asn: string): Promise; getDNSRecordTypes(): Promise; + getDNSRecordTypeInfo(): Promise; + getCommonDNSRecordTypes(): Promise; } export function init(options?: InitOptions): Promise; diff --git a/npm/poindexter-wasm/loader.js b/npm/poindexter-wasm/loader.js index 3d32d2c..e8da38d 100644 --- a/npm/poindexter-wasm/loader.js +++ b/npm/poindexter-wasm/loader.js @@ -109,7 +109,9 @@ export async function init(options = {}) { buildRDAPDomainURL: async (domain) => call('pxBuildRDAPDomainURL', domain), buildRDAPIPURL: async (ip) => call('pxBuildRDAPIPURL', ip), buildRDAPASNURL: async (asn) => call('pxBuildRDAPASNURL', asn), - getDNSRecordTypes: async () => call('pxGetDNSRecordTypes') + getDNSRecordTypes: async () => call('pxGetDNSRecordTypes'), + getDNSRecordTypeInfo: async () => call('pxGetDNSRecordTypeInfo'), + getCommonDNSRecordTypes: async () => call('pxGetCommonDNSRecordTypes') }; return api; diff --git a/wasm/main.go b/wasm/main.go index a124ad0..373b827 100644 --- a/wasm/main.go +++ b/wasm/main.go @@ -664,10 +664,39 @@ func buildRDAPASNURL(_ js.Value, args []js.Value) (any, error) { } func getDNSRecordTypes(_ js.Value, _ []js.Value) (any, error) { - // Returns available DNS record types - return []string{ - "A", "AAAA", "MX", "TXT", "NS", "CNAME", "SOA", "PTR", "SRV", "CAA", - }, nil + // Returns all available DNS record types + types := pd.GetAllDNSRecordTypes() + result := make([]string, len(types)) + for i, t := range types { + result[i] = string(t) + } + return result, nil +} + +func getDNSRecordTypeInfo(_ js.Value, _ []js.Value) (any, error) { + // Returns detailed info about all DNS record types + info := pd.GetDNSRecordTypeInfo() + result := make([]any, len(info)) + for i, r := range info { + result[i] = map[string]any{ + "type": string(r.Type), + "name": r.Name, + "description": r.Description, + "rfc": r.RFC, + "common": r.Common, + } + } + return result, nil +} + +func getCommonDNSRecordTypes(_ js.Value, _ []js.Value) (any, error) { + // Returns only commonly used DNS record types + types := pd.GetCommonDNSRecordTypes() + result := make([]string, len(types)) + for i, t := range types { + result[i] = string(t) + } + return result, nil } func main() { @@ -709,6 +738,8 @@ func main() { export("pxBuildRDAPIPURL", buildRDAPIPURL) export("pxBuildRDAPASNURL", buildRDAPASNURL) export("pxGetDNSRecordTypes", getDNSRecordTypes) + export("pxGetDNSRecordTypeInfo", getDNSRecordTypeInfo) + export("pxGetCommonDNSRecordTypes", getCommonDNSRecordTypes) // Keep running select {}