feat(dns): add DNS referral helpers
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
887b409623
commit
1e0ae2cf07
4 changed files with 166 additions and 0 deletions
3
lns.go
3
lns.go
|
|
@ -59,6 +59,9 @@ type ResourceRecord = dnspkg.ResourceRecord
|
|||
// ResourceJSON mirrors the DNS resource JSON representation.
|
||||
type ResourceJSON = dnspkg.ResourceJSON
|
||||
|
||||
// DNSMessage mirrors the DNS response shape used by pkg/dns projection helpers.
|
||||
type DNSMessage = dnspkg.DNSMessage
|
||||
|
||||
// DSRecord mirrors the DNS DS payload entry.
|
||||
type DSRecord = dnspkg.DSRecord
|
||||
|
||||
|
|
|
|||
|
|
@ -907,6 +907,7 @@ func TestPackageDNSCommonGetters(t *testing.T) {
|
|||
|
||||
func TestPackageResourceAliases(t *testing.T) {
|
||||
_ = ResourceJSON{}
|
||||
_ = DNSMessage{}
|
||||
_ = DSRecordJSON{}
|
||||
_ = NSRecordJSON{}
|
||||
_ = GLUE4RecordJSON{}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,18 @@ type ResourceJSON struct {
|
|||
Records []json.RawMessage `json:"records"`
|
||||
}
|
||||
|
||||
// DNSMessage mirrors the reference DNS response shape used by the JS helpers.
|
||||
//
|
||||
// The Go port keeps the payload lightweight and wire-agnostic: answer,
|
||||
// authority, and additional sections carry the concrete record values that the
|
||||
// reference helpers project, while AA preserves the authoritative-answer flag.
|
||||
type DNSMessage struct {
|
||||
AA bool
|
||||
Answer []any
|
||||
Authority []any
|
||||
Additional []any
|
||||
}
|
||||
|
||||
// DSRecord mirrors the JS DS payload entry.
|
||||
type DSRecord struct {
|
||||
KeyTag uint16
|
||||
|
|
@ -518,6 +530,85 @@ func (r *Resource) GetToZone(name string) []ResourceRecord {
|
|||
return r.ToZone(name)
|
||||
}
|
||||
|
||||
// ToReferral builds the referral/negative-answer message shape used by the JS
|
||||
// reference helpers.
|
||||
//
|
||||
// The referral path returns authority NS/DS records plus glue when the
|
||||
// resource has NS data. Otherwise, or when DS referrals are disallowed for a
|
||||
// TLD lookup, the helper returns a negative proof with AA set and an NSEC
|
||||
// record in authority.
|
||||
func (r *Resource) ToReferral(name string, typ HSType, isTLD bool) DNSMessage {
|
||||
msg := DNSMessage{}
|
||||
badReferral := isTLD && typ == HSTypeDS
|
||||
|
||||
if r != nil && r.HasNS() && !badReferral {
|
||||
for _, record := range r.ToNS(name) {
|
||||
msg.Authority = append(msg.Authority, record)
|
||||
}
|
||||
|
||||
for _, record := range r.ToDS(name) {
|
||||
msg.Authority = append(msg.Authority, record)
|
||||
}
|
||||
|
||||
for _, record := range r.ToGlue(name) {
|
||||
msg.Additional = append(msg.Additional, record)
|
||||
}
|
||||
|
||||
if !r.HasDS() {
|
||||
msg.Authority = append(msg.Authority, r.ToNSEC(name))
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
msg.AA = true
|
||||
msg.Authority = append(msg.Authority, r.ToNSEC(name))
|
||||
return msg
|
||||
}
|
||||
|
||||
// GetToReferral is an alias for ToReferral.
|
||||
func (r *Resource) GetToReferral(name string, typ HSType, isTLD bool) DNSMessage {
|
||||
return r.ToReferral(name, typ, isTLD)
|
||||
}
|
||||
|
||||
// ToDNS projects the resource into the JS reference DNS response shape.
|
||||
//
|
||||
// Subdomain lookups are routed to the appropriate TLD referral. TLD TXT
|
||||
// lookups answer directly when the resource has no NS records; DS lookups and
|
||||
// all other cases fall through to the referral/negative-answer logic.
|
||||
func (r *Resource) ToDNS(name string, typ HSType) DNSMessage {
|
||||
name = fqdn(strings.ToLower(name))
|
||||
labels := strings.Split(strings.TrimSuffix(name, "."), ".")
|
||||
if len(labels) > 1 {
|
||||
return r.ToReferral(labels[len(labels)-1], typ, false)
|
||||
}
|
||||
|
||||
msg := DNSMessage{}
|
||||
switch typ {
|
||||
case HSTypeTXT:
|
||||
if r == nil || !r.HasNS() {
|
||||
msg.AA = true
|
||||
for _, record := range r.ToTXT(name) {
|
||||
msg.Answer = append(msg.Answer, record)
|
||||
}
|
||||
}
|
||||
case HSTypeDS:
|
||||
// DS TLD lookups intentionally fall through to the referral/negative
|
||||
// proof path to match the JS reference helper.
|
||||
}
|
||||
|
||||
if len(msg.Answer) == 0 && len(msg.Authority) == 0 {
|
||||
return r.ToReferral(name, typ, true)
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
// GetToDNS is an alias for ToDNS.
|
||||
func (r *Resource) GetToDNS(name string, typ HSType) DNSMessage {
|
||||
return r.ToDNS(name, typ)
|
||||
}
|
||||
|
||||
// ToNSEC constructs the reference NSEC helper output for the resource.
|
||||
//
|
||||
// The bitmap selection matches the JS reference helper:
|
||||
|
|
|
|||
|
|
@ -499,6 +499,77 @@ func TestResourceProjectionHelpers(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResourceToDNSAndReferral(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")},
|
||||
TXTRecord{Entries: []string{"hello"}},
|
||||
}
|
||||
|
||||
referral := resource.ToReferral("example.", HSTypeNS, false)
|
||||
if referral.AA {
|
||||
t.Fatal("ToReferral should not set AA for a normal referral")
|
||||
}
|
||||
if len(referral.Authority) != 2 {
|
||||
t.Fatalf("ToReferral authority = %d, want 2", len(referral.Authority))
|
||||
}
|
||||
if ns, ok := referral.Authority[0].(NSRecord); !ok || ns.NS != "ns1.example." {
|
||||
t.Fatalf("ToReferral authority[0] = %#v, want NS ns1.example.", referral.Authority[0])
|
||||
}
|
||||
if ds, ok := referral.Authority[1].(DSRecord); !ok || ds.KeyTag != 7 {
|
||||
t.Fatalf("ToReferral authority[1] = %#v, want DS keyTag=7", referral.Authority[1])
|
||||
}
|
||||
if len(referral.Additional) != 1 {
|
||||
t.Fatalf("ToReferral additional = %d, want 1", len(referral.Additional))
|
||||
}
|
||||
if glue, ok := referral.Additional[0].(GLUE4Record); !ok || glue.NS != "ns1.example." || glue.Address.String() != "192.0.2.10" {
|
||||
t.Fatalf("ToReferral additional[0] = %#v, want GLUE4 ns1.example./192.0.2.10", referral.Additional[0])
|
||||
}
|
||||
|
||||
aliasReferral := resource.GetToReferral("example.", HSTypeNS, false)
|
||||
if aliasReferral.AA != referral.AA || len(aliasReferral.Authority) != len(referral.Authority) || len(aliasReferral.Additional) != len(referral.Additional) {
|
||||
t.Fatal("GetToReferral should alias ToReferral")
|
||||
}
|
||||
|
||||
negative := resource.ToDNS("example.", HSTypeDS)
|
||||
if !negative.AA {
|
||||
t.Fatal("ToDNS should set AA for a TLD DS negative proof")
|
||||
}
|
||||
if len(negative.Answer) != 0 {
|
||||
t.Fatalf("ToDNS answer = %d, want 0", len(negative.Answer))
|
||||
}
|
||||
if len(negative.Authority) != 1 {
|
||||
t.Fatalf("ToDNS authority = %d, want 1", len(negative.Authority))
|
||||
}
|
||||
if nsec, ok := negative.Authority[0].(NSECRecord); !ok || nsec.Name != "example." || nsec.NextDomain != "example\x00." {
|
||||
t.Fatalf("ToDNS authority[0] = %#v, want NSEC example./example\\x00.", negative.Authority[0])
|
||||
}
|
||||
|
||||
txtResource := NewResource()
|
||||
txtResource.Records = []ResourceRecord{TXTRecord{Entries: []string{"hello"}}}
|
||||
|
||||
answer := txtResource.ToDNS("example.", HSTypeTXT)
|
||||
if !answer.AA {
|
||||
t.Fatal("ToDNS should set AA for an in-zone TXT answer")
|
||||
}
|
||||
if len(answer.Answer) != 1 {
|
||||
t.Fatalf("ToDNS answer = %d, want 1", len(answer.Answer))
|
||||
}
|
||||
if txt, ok := answer.Answer[0].(TXTRecord); !ok || len(txt.Entries) != 1 || txt.Entries[0] != "hello" {
|
||||
t.Fatalf("ToDNS answer[0] = %#v, want TXT hello", answer.Answer[0])
|
||||
}
|
||||
if len(answer.Authority) != 0 || len(answer.Additional) != 0 {
|
||||
t.Fatalf("ToDNS TXT answer should not populate authority/additional, got %+v", answer)
|
||||
}
|
||||
|
||||
aliasAnswer := txtResource.GetToDNS("example.", HSTypeTXT)
|
||||
if aliasAnswer.AA != answer.AA || len(aliasAnswer.Answer) != len(answer.Answer) || len(aliasAnswer.Authority) != len(answer.Authority) || len(aliasAnswer.Additional) != len(answer.Additional) {
|
||||
t.Fatal("GetToDNS should alias ToDNS")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceDecodeStopsAtUnknownType(t *testing.T) {
|
||||
resource := NewResource()
|
||||
resource.Records = []ResourceRecord{TXTRecord{Entries: []string{"known"}}}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue