Implement DNS resource projection helpers
This commit is contained in:
parent
b3f1c5dcd7
commit
c7438413ae
2 changed files with 383 additions and 0 deletions
|
|
@ -247,6 +247,241 @@ func (r *Resource) GetHasDS() bool {
|
|||
return r.HasDS()
|
||||
}
|
||||
|
||||
// ToNS projects NS-capable resource entries into authority NS records.
|
||||
//
|
||||
// The helper mirrors the JS reference behavior:
|
||||
// - NS, GLUE, and SYNTH entries all contribute NS targets
|
||||
// - duplicate targets are emitted once, in first-seen order
|
||||
func (r *Resource) ToNS(_ string) []NSRecord {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
authority := make([]NSRecord, 0, len(r.Records))
|
||||
seen := make(map[string]struct{}, len(r.Records))
|
||||
|
||||
for _, record := range r.Records {
|
||||
ns, ok := resourceAuthorityNS(record)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.ToLower(fqdn(ns.NS))
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
seen[key] = struct{}{}
|
||||
authority = append(authority, ns)
|
||||
}
|
||||
|
||||
return authority
|
||||
}
|
||||
|
||||
// GetToNS is an alias for ToNS.
|
||||
func (r *Resource) GetToNS(name string) []NSRecord {
|
||||
return r.ToNS(name)
|
||||
}
|
||||
|
||||
// ToGlue projects glue-capable entries into address-bearing glue records.
|
||||
//
|
||||
// GLUE records are only returned when their host falls under the requested
|
||||
// name. SYNTH records always project into their synthetic hostnames.
|
||||
func (r *Resource) ToGlue(name string) []ResourceRecord {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
additional := make([]ResourceRecord, 0, len(r.Records))
|
||||
|
||||
for _, record := range r.Records {
|
||||
switch rr := record.(type) {
|
||||
case GLUE4Record:
|
||||
if !isSubdomain(name, rr.NS) {
|
||||
continue
|
||||
}
|
||||
additional = append(additional, rr)
|
||||
case *GLUE4Record:
|
||||
if rr == nil || !isSubdomain(name, rr.NS) {
|
||||
continue
|
||||
}
|
||||
additional = append(additional, *rr)
|
||||
case GLUE6Record:
|
||||
if !isSubdomain(name, rr.NS) {
|
||||
continue
|
||||
}
|
||||
additional = append(additional, rr)
|
||||
case *GLUE6Record:
|
||||
if rr == nil || !isSubdomain(name, rr.NS) {
|
||||
continue
|
||||
}
|
||||
additional = append(additional, *rr)
|
||||
case SYNTH4Record:
|
||||
additional = append(additional, GLUE4Record{NS: rr.NS(), Address: rr.Address})
|
||||
case *SYNTH4Record:
|
||||
if rr == nil {
|
||||
continue
|
||||
}
|
||||
additional = append(additional, GLUE4Record{NS: rr.NS(), Address: rr.Address})
|
||||
case SYNTH6Record:
|
||||
additional = append(additional, GLUE6Record{NS: rr.NS(), Address: rr.Address})
|
||||
case *SYNTH6Record:
|
||||
if rr == nil {
|
||||
continue
|
||||
}
|
||||
additional = append(additional, GLUE6Record{NS: rr.NS(), Address: rr.Address})
|
||||
}
|
||||
}
|
||||
|
||||
return additional
|
||||
}
|
||||
|
||||
// GetToGlue is an alias for ToGlue.
|
||||
func (r *Resource) GetToGlue(name string) []ResourceRecord {
|
||||
return r.ToGlue(name)
|
||||
}
|
||||
|
||||
// ToDS filters DS records in first-seen order.
|
||||
func (r *Resource) ToDS(_ string) []DSRecord {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
answer := make([]DSRecord, 0, len(r.Records))
|
||||
for _, record := range r.Records {
|
||||
switch rr := record.(type) {
|
||||
case DSRecord:
|
||||
answer = append(answer, rr)
|
||||
case *DSRecord:
|
||||
if rr != nil {
|
||||
answer = append(answer, *rr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return answer
|
||||
}
|
||||
|
||||
// GetToDS is an alias for ToDS.
|
||||
func (r *Resource) GetToDS(name string) []DSRecord {
|
||||
return r.ToDS(name)
|
||||
}
|
||||
|
||||
// ToTXT filters TXT records in first-seen order.
|
||||
func (r *Resource) ToTXT(_ string) []TXTRecord {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
answer := make([]TXTRecord, 0, len(r.Records))
|
||||
for _, record := range r.Records {
|
||||
switch rr := record.(type) {
|
||||
case TXTRecord:
|
||||
answer = append(answer, rr)
|
||||
case *TXTRecord:
|
||||
if rr != nil {
|
||||
answer = append(answer, *rr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return answer
|
||||
}
|
||||
|
||||
// GetToTXT is an alias for ToTXT.
|
||||
func (r *Resource) GetToTXT(name string) []TXTRecord {
|
||||
return r.ToTXT(name)
|
||||
}
|
||||
|
||||
// ToZone projects the resource into zone-order records.
|
||||
//
|
||||
// Authority-like records are emitted first in original order, with duplicate NS
|
||||
// targets collapsed. Glue records for in-zone hosts are appended last.
|
||||
func (r *Resource) ToZone(name string) []ResourceRecord {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
zone := make([]ResourceRecord, 0, len(r.Records)*2)
|
||||
seenNS := make(map[string]struct{}, len(r.Records))
|
||||
|
||||
for _, record := range r.Records {
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if ns, ok := resourceAuthorityNS(record); ok {
|
||||
key := strings.ToLower(fqdn(ns.NS))
|
||||
if _, ok := seenNS[key]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
seenNS[key] = struct{}{}
|
||||
zone = append(zone, ns)
|
||||
continue
|
||||
}
|
||||
|
||||
switch rr := record.(type) {
|
||||
case DSRecord:
|
||||
zone = append(zone, rr)
|
||||
case *DSRecord:
|
||||
if rr != nil {
|
||||
zone = append(zone, *rr)
|
||||
}
|
||||
case TXTRecord:
|
||||
zone = append(zone, rr)
|
||||
case *TXTRecord:
|
||||
if rr != nil {
|
||||
zone = append(zone, *rr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, record := range r.Records {
|
||||
switch rr := record.(type) {
|
||||
case GLUE4Record:
|
||||
if isSubdomain(name, rr.NS) {
|
||||
zone = append(zone, rr)
|
||||
}
|
||||
case *GLUE4Record:
|
||||
if rr != nil && isSubdomain(name, rr.NS) {
|
||||
zone = append(zone, *rr)
|
||||
}
|
||||
case GLUE6Record:
|
||||
if isSubdomain(name, rr.NS) {
|
||||
zone = append(zone, rr)
|
||||
}
|
||||
case *GLUE6Record:
|
||||
if rr != nil && isSubdomain(name, rr.NS) {
|
||||
zone = append(zone, *rr)
|
||||
}
|
||||
case SYNTH4Record:
|
||||
if isSubdomain(name, rr.NS()) {
|
||||
zone = append(zone, GLUE4Record{NS: rr.NS(), Address: rr.Address})
|
||||
}
|
||||
case *SYNTH4Record:
|
||||
if rr != nil && isSubdomain(name, rr.NS()) {
|
||||
zone = append(zone, GLUE4Record{NS: rr.NS(), Address: rr.Address})
|
||||
}
|
||||
case SYNTH6Record:
|
||||
if isSubdomain(name, rr.NS()) {
|
||||
zone = append(zone, GLUE6Record{NS: rr.NS(), Address: rr.Address})
|
||||
}
|
||||
case *SYNTH6Record:
|
||||
if rr != nil && isSubdomain(name, rr.NS()) {
|
||||
zone = append(zone, GLUE6Record{NS: rr.NS(), Address: rr.Address})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return zone
|
||||
}
|
||||
|
||||
// GetToZone is an alias for ToZone.
|
||||
func (r *Resource) GetToZone(name string) []ResourceRecord {
|
||||
return r.ToZone(name)
|
||||
}
|
||||
|
||||
// ToNSEC constructs the reference NSEC helper output for the resource.
|
||||
//
|
||||
// The bitmap selection matches the JS reference helper:
|
||||
|
|
@ -485,6 +720,58 @@ func resourceRecordJSON(record ResourceRecord) any {
|
|||
}
|
||||
}
|
||||
|
||||
func resourceAuthorityNS(record ResourceRecord) (NSRecord, bool) {
|
||||
switch rr := record.(type) {
|
||||
case NSRecord:
|
||||
return rr, true
|
||||
case *NSRecord:
|
||||
if rr != nil {
|
||||
return *rr, true
|
||||
}
|
||||
case GLUE4Record:
|
||||
return NSRecord{NS: rr.NS}, true
|
||||
case *GLUE4Record:
|
||||
if rr != nil {
|
||||
return NSRecord{NS: rr.NS}, true
|
||||
}
|
||||
case GLUE6Record:
|
||||
return NSRecord{NS: rr.NS}, true
|
||||
case *GLUE6Record:
|
||||
if rr != nil {
|
||||
return NSRecord{NS: rr.NS}, true
|
||||
}
|
||||
case SYNTH4Record:
|
||||
return NSRecord{NS: rr.NS()}, true
|
||||
case *SYNTH4Record:
|
||||
if rr != nil {
|
||||
return NSRecord{NS: rr.NS()}, true
|
||||
}
|
||||
case SYNTH6Record:
|
||||
return NSRecord{NS: rr.NS()}, true
|
||||
case *SYNTH6Record:
|
||||
if rr != nil {
|
||||
return NSRecord{NS: rr.NS()}, true
|
||||
}
|
||||
}
|
||||
|
||||
return NSRecord{}, false
|
||||
}
|
||||
|
||||
func isSubdomain(parent, child string) bool {
|
||||
parent = strings.ToLower(fqdn(parent))
|
||||
child = strings.ToLower(fqdn(child))
|
||||
|
||||
if parent == "." {
|
||||
return true
|
||||
}
|
||||
|
||||
if child == parent {
|
||||
return true
|
||||
}
|
||||
|
||||
return strings.HasSuffix(trimFQDN(child), "."+trimFQDN(parent))
|
||||
}
|
||||
|
||||
func resourceRecordSize(record ResourceRecord) int {
|
||||
switch rr := record.(type) {
|
||||
case DSRecord:
|
||||
|
|
|
|||
|
|
@ -303,6 +303,102 @@ func TestResourceToNSEC(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResourceProjectionHelpers(t *testing.T) {
|
||||
resource := NewResource()
|
||||
resource.Records = []ResourceRecord{
|
||||
DSRecord{KeyTag: 7, Algorithm: 8, DigestType: 9, Digest: []byte{0xaa}},
|
||||
NSRecord{NS: "ns1.example."},
|
||||
GLUE4Record{NS: "ns1.example.", Address: netip.MustParseAddr("192.0.2.10")},
|
||||
GLUE6Record{NS: "ns2.other.", Address: netip.MustParseAddr("2001:db8::20")},
|
||||
SYNTH4Record{Address: netip.MustParseAddr("198.51.100.9")},
|
||||
TXTRecord{Entries: []string{"hello"}},
|
||||
}
|
||||
|
||||
ns := resource.ToNS("example.")
|
||||
if len(ns) != 3 {
|
||||
t.Fatalf("ToNS returned %d records, want 3", len(ns))
|
||||
}
|
||||
if ns[0].NS != "ns1.example." || ns[1].NS != "ns2.other." || ns[2].NS != "_oopm828._synth." {
|
||||
t.Fatalf("ToNS returned %#v, want ns1.example./ns2.other./_oopm828._synth.", ns)
|
||||
}
|
||||
|
||||
aliasNS := resource.GetToNS("example.")
|
||||
if len(aliasNS) != len(ns) {
|
||||
t.Fatalf("GetToNS returned %d records, want %d", len(aliasNS), len(ns))
|
||||
}
|
||||
|
||||
glue := resource.ToGlue("example.")
|
||||
if len(glue) != 2 {
|
||||
t.Fatalf("ToGlue returned %d records, want 2", len(glue))
|
||||
}
|
||||
if rr, ok := glue[0].(GLUE4Record); !ok || rr.NS != "ns1.example." || rr.Address.String() != "192.0.2.10" {
|
||||
t.Fatalf("ToGlue[0] = %#v, want GLUE4 ns1.example./192.0.2.10", glue[0])
|
||||
}
|
||||
if rr, ok := glue[1].(GLUE4Record); !ok || rr.NS != "_oopm828._synth." || rr.Address.String() != "198.51.100.9" {
|
||||
t.Fatalf("ToGlue[1] = %#v, want GLUE4 _oopm828._synth./198.51.100.9", glue[1])
|
||||
}
|
||||
|
||||
aliasGlue := resource.GetToGlue("example.")
|
||||
if len(aliasGlue) != len(glue) {
|
||||
t.Fatalf("GetToGlue returned %d records, want %d", len(aliasGlue), len(glue))
|
||||
}
|
||||
|
||||
ds := resource.ToDS("example.")
|
||||
if len(ds) != 1 || ds[0].KeyTag != 7 || ds[0].Algorithm != 8 || ds[0].DigestType != 9 || !bytes.Equal(ds[0].Digest, []byte{0xaa}) {
|
||||
t.Fatalf("ToDS returned %#v, want one DS record", ds)
|
||||
}
|
||||
|
||||
txt := resource.ToTXT("example.")
|
||||
if len(txt) != 1 || len(txt[0].Entries) != 1 || txt[0].Entries[0] != "hello" {
|
||||
t.Fatalf("ToTXT returned %#v, want one TXT record", txt)
|
||||
}
|
||||
|
||||
zone := resource.ToZone("example.")
|
||||
if len(zone) != 6 {
|
||||
t.Fatalf("ToZone returned %d records, want 6", len(zone))
|
||||
}
|
||||
if rr, ok := zone[0].(DSRecord); !ok || rr.KeyTag != 7 {
|
||||
t.Fatalf("ToZone[0] = %#v, want DS record", zone[0])
|
||||
}
|
||||
if rr, ok := zone[1].(NSRecord); !ok || rr.NS != "ns1.example." {
|
||||
t.Fatalf("ToZone[1] = %#v, want NS ns1.example.", zone[1])
|
||||
}
|
||||
if rr, ok := zone[2].(NSRecord); !ok || rr.NS != "ns2.other." {
|
||||
t.Fatalf("ToZone[2] = %#v, want NS ns2.other.", zone[2])
|
||||
}
|
||||
if rr, ok := zone[3].(NSRecord); !ok || rr.NS != "_oopm828._synth." {
|
||||
t.Fatalf("ToZone[3] = %#v, want NS _oopm828._synth.", zone[3])
|
||||
}
|
||||
if rr, ok := zone[4].(TXTRecord); !ok || len(rr.Entries) != 1 || rr.Entries[0] != "hello" {
|
||||
t.Fatalf("ToZone[4] = %#v, want TXT hello", zone[4])
|
||||
}
|
||||
if rr, ok := zone[5].(GLUE4Record); !ok || rr.NS != "ns1.example." || rr.Address.String() != "192.0.2.10" {
|
||||
t.Fatalf("ToZone[5] = %#v, want GLUE4 ns1.example./192.0.2.10", zone[5])
|
||||
}
|
||||
|
||||
aliasZone := resource.GetToZone("example.")
|
||||
if len(aliasZone) != len(zone) {
|
||||
t.Fatalf("GetToZone returned %d records, want %d", len(aliasZone), len(zone))
|
||||
}
|
||||
|
||||
var nilResource *Resource
|
||||
if got := nilResource.ToNS("example."); got != nil {
|
||||
t.Fatalf("nil ToNS = %#v, want nil", got)
|
||||
}
|
||||
if got := nilResource.ToGlue("example."); got != nil {
|
||||
t.Fatalf("nil ToGlue = %#v, want nil", got)
|
||||
}
|
||||
if got := nilResource.ToDS("example."); got != nil {
|
||||
t.Fatalf("nil ToDS = %#v, want nil", got)
|
||||
}
|
||||
if got := nilResource.ToTXT("example."); got != nil {
|
||||
t.Fatalf("nil ToTXT = %#v, want nil", got)
|
||||
}
|
||||
if got := nilResource.ToZone("example."); got != nil {
|
||||
t.Fatalf("nil ToZone = %#v, want nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceDecodeStopsAtUnknownType(t *testing.T) {
|
||||
resource := NewResource()
|
||||
resource.Records = []ResourceRecord{TXTRecord{Entries: []string{"known"}}}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue