mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 08:16:39 +00:00
[client] Add support to wildcard custom records (#5125)
* **New Features** * Wildcard DNS fallback for eligible query types (excluding NS/SOA): attempts wildcard records when no exact match, rewrites wildcard names back to the original query, and rotates responses; preserves CNAME resolution. * **Tests** * Vastly expanded coverage for wildcard behaviors, precedence, multi-record round‑robin, multi-type chains, multi-hop and cross-zone scenarios, and edge cases (NXDOMAIN/NODATA, fallthrough). * **Chores** * CI lint config updated to ignore an additional codespell entry.
This commit is contained in:
2
.github/workflows/golangci-lint.yml
vendored
2
.github/workflows/golangci-lint.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
- name: codespell
|
- name: codespell
|
||||||
uses: codespell-project/actions-codespell@v2
|
uses: codespell-project/actions-codespell@v2
|
||||||
with:
|
with:
|
||||||
ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe,cros
|
ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe,cros,ans
|
||||||
skip: go.mod,go.sum
|
skip: go.mod,go.sum
|
||||||
golangci:
|
golangci:
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ func (d *Resolver) determineRcode(question dns.Question, result lookupResult) in
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No records found, but domain exists with different record types (NODATA)
|
// No records found, but domain exists with different record types (NODATA)
|
||||||
if d.hasRecordsForDomain(domain.Domain(question.Name)) {
|
if d.hasRecordsForDomain(domain.Domain(question.Name), question.Qtype) {
|
||||||
return dns.RcodeSuccess
|
return dns.RcodeSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,11 +164,15 @@ func (d *Resolver) continueToNext(logger *log.Entry, w dns.ResponseWriter, r *dn
|
|||||||
}
|
}
|
||||||
|
|
||||||
// hasRecordsForDomain checks if any records exist for the given domain name regardless of type
|
// hasRecordsForDomain checks if any records exist for the given domain name regardless of type
|
||||||
func (d *Resolver) hasRecordsForDomain(domainName domain.Domain) bool {
|
func (d *Resolver) hasRecordsForDomain(domainName domain.Domain, qType uint16) bool {
|
||||||
d.mu.RLock()
|
d.mu.RLock()
|
||||||
defer d.mu.RUnlock()
|
defer d.mu.RUnlock()
|
||||||
|
|
||||||
_, exists := d.domains[domainName]
|
_, exists := d.domains[domainName]
|
||||||
|
if !exists && supportsWildcard(qType) {
|
||||||
|
testWild := transformDomainToWildcard(string(domainName))
|
||||||
|
_, exists = d.domains[domain.Domain(testWild)]
|
||||||
|
}
|
||||||
return exists
|
return exists
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +199,12 @@ type lookupResult struct {
|
|||||||
func (d *Resolver) lookupRecords(logger *log.Entry, question dns.Question) lookupResult {
|
func (d *Resolver) lookupRecords(logger *log.Entry, question dns.Question) lookupResult {
|
||||||
d.mu.RLock()
|
d.mu.RLock()
|
||||||
records, found := d.records[question]
|
records, found := d.records[question]
|
||||||
|
usingWildcard := false
|
||||||
|
wildQuestion := transformToWildcard(question)
|
||||||
|
if !found && supportsWildcard(question.Qtype) {
|
||||||
|
records, found = d.records[wildQuestion]
|
||||||
|
usingWildcard = found
|
||||||
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
d.mu.RUnlock()
|
d.mu.RUnlock()
|
||||||
@@ -216,18 +226,53 @@ func (d *Resolver) lookupRecords(logger *log.Entry, question dns.Question) looku
|
|||||||
// if there's more than one record, rotate them (round-robin)
|
// if there's more than one record, rotate them (round-robin)
|
||||||
if len(recordsCopy) > 1 {
|
if len(recordsCopy) > 1 {
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
records = d.records[question]
|
q := question
|
||||||
|
if usingWildcard {
|
||||||
|
q = wildQuestion
|
||||||
|
}
|
||||||
|
records = d.records[q]
|
||||||
if len(records) > 1 {
|
if len(records) > 1 {
|
||||||
first := records[0]
|
first := records[0]
|
||||||
records = append(records[1:], first)
|
records = append(records[1:], first)
|
||||||
d.records[question] = records
|
d.records[q] = records
|
||||||
}
|
}
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if usingWildcard {
|
||||||
|
return responseFromWildRecords(question.Name, wildQuestion.Name, recordsCopy)
|
||||||
|
}
|
||||||
|
|
||||||
return lookupResult{records: recordsCopy, rcode: dns.RcodeSuccess}
|
return lookupResult{records: recordsCopy, rcode: dns.RcodeSuccess}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func transformToWildcard(question dns.Question) dns.Question {
|
||||||
|
wildQuestion := question
|
||||||
|
wildQuestion.Name = transformDomainToWildcard(wildQuestion.Name)
|
||||||
|
return wildQuestion
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformDomainToWildcard(domain string) string {
|
||||||
|
s := strings.Split(domain, ".")
|
||||||
|
s[0] = "*"
|
||||||
|
return strings.Join(s, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
func supportsWildcard(queryType uint16) bool {
|
||||||
|
return queryType != dns.TypeNS && queryType != dns.TypeSOA
|
||||||
|
}
|
||||||
|
|
||||||
|
func responseFromWildRecords(originalName, wildName string, wildRecords []dns.RR) lookupResult {
|
||||||
|
records := make([]dns.RR, len(wildRecords))
|
||||||
|
for i, record := range wildRecords {
|
||||||
|
copiedRecord := dns.Copy(record)
|
||||||
|
copiedRecord.Header().Name = originalName
|
||||||
|
records[i] = copiedRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
return lookupResult{records: records, rcode: dns.RcodeSuccess}
|
||||||
|
}
|
||||||
|
|
||||||
// lookupCNAMEChain follows a CNAME chain and returns the CNAME records along with
|
// lookupCNAMEChain follows a CNAME chain and returns the CNAME records along with
|
||||||
// the final resolved record of the requested type. This is required for musl libc
|
// the final resolved record of the requested type. This is required for musl libc
|
||||||
// compatibility, which expects the full answer chain rather than just the CNAME.
|
// compatibility, which expects the full answer chain rather than just the CNAME.
|
||||||
@@ -237,6 +282,13 @@ func (d *Resolver) lookupCNAMEChain(logger *log.Entry, cnameQuestion dns.Questio
|
|||||||
|
|
||||||
for range maxDepth {
|
for range maxDepth {
|
||||||
cnameRecords := d.getRecords(cnameQuestion)
|
cnameRecords := d.getRecords(cnameQuestion)
|
||||||
|
if len(cnameRecords) == 0 && supportsWildcard(targetType) {
|
||||||
|
wildQuestion := transformToWildcard(cnameQuestion)
|
||||||
|
if wildRecords := d.getRecords(wildQuestion); len(wildRecords) > 0 {
|
||||||
|
cnameRecords = responseFromWildRecords(cnameQuestion.Name, wildQuestion.Name, wildRecords).records
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(cnameRecords) == 0 {
|
if len(cnameRecords) == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -303,7 +355,7 @@ func (d *Resolver) resolveCNAMETarget(logger *log.Entry, targetName string, targ
|
|||||||
}
|
}
|
||||||
|
|
||||||
// domain exists locally but not this record type (NODATA)
|
// domain exists locally but not this record type (NODATA)
|
||||||
if d.hasRecordsForDomain(domain.Domain(targetName)) {
|
if d.hasRecordsForDomain(domain.Domain(targetName), targetType) {
|
||||||
return lookupResult{rcode: dns.RcodeSuccess}
|
return lookupResult{rcode: dns.RcodeSuccess}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user