mirror of
https://github.com/Li4n0/revsuit.git
synced 2026-09-21 22:30:46 +08:00
264 lines
6.0 KiB
Go
264 lines
6.0 KiB
Go
package newdns
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// Zone describes a single authoritative DNS zone.
|
|
type Zone struct {
|
|
// The FQDN of the zone e.g. "example.com.".
|
|
Name string
|
|
|
|
// The FQDN of the master mame server responsible for this zone. The FQDN
|
|
// must be returned as A and AAAA record by the parent zone.
|
|
MasterNameServer string
|
|
|
|
// A list of FQDNs to all authoritative name servers for this zone. The
|
|
// FQDNs must be returned as A and AAAA records by the parent zone. It is
|
|
// required to announce at least two distinct name servers per zone.
|
|
AllNameServers []string
|
|
|
|
// The email address of the administrator e.g. "[email protected]".
|
|
//
|
|
// Default: "hostmaster@NAME".
|
|
AdminEmail string
|
|
|
|
// The refresh interval.
|
|
//
|
|
// Default: 6h.
|
|
Refresh time.Duration
|
|
|
|
// The retry interval for the zone.
|
|
//
|
|
// Default: 1h.
|
|
Retry time.Duration
|
|
|
|
// The expiration interval of the zone.
|
|
//
|
|
// Default: 72h.
|
|
Expire time.Duration
|
|
|
|
// The TTL for the SOA record.
|
|
//
|
|
// Default: 15m.
|
|
SOATTL time.Duration
|
|
|
|
// The TTL for NS records.
|
|
//
|
|
// Default: 48h.
|
|
NSTTL time.Duration
|
|
|
|
// The minimum TTL for all records. Either this value, or the SOATTL if lower,
|
|
// is used to determine the "negative caching TTL" which is the duration
|
|
// caches are allowed to cache missing records (NXDOMAIN).
|
|
//
|
|
// Default: 5min.
|
|
MinTTL time.Duration
|
|
|
|
// The handler that responds to requests for this zone. The returned sets
|
|
// must not be altered going forward.
|
|
Handler func(name, remoteAddr string) ([]Set, error)
|
|
}
|
|
|
|
// Validate will validate the zone and ensure the documented defaults.
|
|
func (z *Zone) Validate() error {
|
|
// check name
|
|
if !IsDomain(z.Name, true) {
|
|
return errors.Errorf("name not fully qualified: %s", z.Name)
|
|
}
|
|
|
|
// check master name server
|
|
if !IsDomain(z.MasterNameServer, true) {
|
|
return errors.Errorf("master server not full qualified: %s", z.MasterNameServer)
|
|
}
|
|
|
|
// check name server count
|
|
if len(z.AllNameServers) < 1 {
|
|
return errors.Errorf("missing name servers")
|
|
}
|
|
|
|
// check name servers
|
|
var includesMaster bool
|
|
for _, ns := range z.AllNameServers {
|
|
if !IsDomain(ns, true) {
|
|
return errors.Errorf("name server not fully qualified: %s", ns)
|
|
}
|
|
|
|
if ns == z.MasterNameServer {
|
|
includesMaster = true
|
|
}
|
|
}
|
|
|
|
// check master inclusion
|
|
if !includesMaster {
|
|
return errors.Errorf("master name server not listed as name server: %s", z.MasterNameServer)
|
|
}
|
|
|
|
// set default admin email
|
|
if z.AdminEmail == "" {
|
|
z.AdminEmail = fmt.Sprintf("hostmaster@%s", z.Name)
|
|
}
|
|
|
|
// check admin email
|
|
if !IsDomain(emailToDomain(z.AdminEmail), true) {
|
|
return errors.Errorf("admin email cannot be converted to a domain name: %s", z.AdminEmail)
|
|
}
|
|
|
|
// set default refresh
|
|
if z.Refresh == 0 {
|
|
z.Refresh = 6 * time.Hour
|
|
}
|
|
|
|
// set default retry
|
|
if z.Retry == 0 {
|
|
z.Retry = time.Hour
|
|
}
|
|
|
|
// set default expire
|
|
if z.Expire == 0 {
|
|
z.Expire = 72 * time.Hour
|
|
}
|
|
|
|
// set default SOA TTL
|
|
if z.SOATTL == 0 {
|
|
z.SOATTL = 15 * time.Minute
|
|
}
|
|
|
|
// set default NS TTL
|
|
if z.NSTTL == 0 {
|
|
z.NSTTL = 48 * time.Hour
|
|
}
|
|
|
|
// set default min TTL
|
|
if z.MinTTL == 0 {
|
|
z.MinTTL = 5 * time.Minute
|
|
}
|
|
|
|
// check retry
|
|
if z.Retry >= z.Refresh {
|
|
return errors.Errorf("retry must be less than refresh: %d", z.Retry)
|
|
}
|
|
|
|
// check expire
|
|
if z.Expire < z.Refresh+z.Retry {
|
|
return errors.Errorf("expire must be bigger than the sum of refresh and retry: %d", z.Expire)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Lookup will lookup the specified name in the zone and return results for the
|
|
// specified record types. If no results are returned, the second return value
|
|
// indicates if there are other results for the specified name.
|
|
func (z *Zone) Lookup(name, remoteAddr string, needle ...Type) ([]Set, bool, error) {
|
|
// check name
|
|
if !IsDomain(name, true) {
|
|
return nil, false, errors.Errorf("invalid name: %s", name)
|
|
}
|
|
|
|
// normalize name
|
|
name = NormalizeDomain(name, true, false, false)
|
|
|
|
// check name
|
|
if !InZone(z.Name, name) {
|
|
return nil, false, errors.Errorf("name does not belong to zone: %s", name)
|
|
}
|
|
|
|
// prepare result
|
|
var result []Set
|
|
|
|
for i := 0; ; i++ {
|
|
// get sets
|
|
sets, err := z.Handler(TrimZone(z.Name, name), remoteAddr)
|
|
if err != nil {
|
|
return nil, false, errors.Wrap(err, "zone handler error")
|
|
}
|
|
|
|
// return immediately if initial set is empty
|
|
if i == 0 && len(sets) == 0 {
|
|
return nil, false, nil
|
|
}
|
|
|
|
// prepare counters
|
|
counters := map[Type]int{
|
|
A: 0,
|
|
AAAA: 0,
|
|
CNAME: 0,
|
|
MX: 0,
|
|
TXT: 0,
|
|
}
|
|
|
|
// validate sets
|
|
for _, set := range sets {
|
|
// validate set
|
|
err = set.Validate()
|
|
if err != nil {
|
|
return nil, false, errors.Wrap(err, "invalid set")
|
|
}
|
|
|
|
// check relationship
|
|
if !InZone(z.Name, set.Name) {
|
|
return nil, false, errors.Errorf("set does not belong to zone: %s", set.Name)
|
|
}
|
|
|
|
// increment counter
|
|
counters[set.Type]++
|
|
}
|
|
|
|
// check counters
|
|
for _, counter := range counters {
|
|
if counter > 1 {
|
|
return nil, false, errors.New("multiple sets for same type")
|
|
}
|
|
}
|
|
|
|
// check apex CNAME
|
|
if counters[CNAME] > 0 && name == z.Name {
|
|
return nil, false, errors.Errorf("invalid CNAME set at apex: %s", name)
|
|
}
|
|
|
|
// check CNAME is stand-alone
|
|
if counters[CNAME] > 0 && (len(sets) > 1) {
|
|
return nil, false, errors.Errorf("other sets with CNAME set: %s", name)
|
|
}
|
|
|
|
// check if CNAME and query is not CNAME
|
|
if counters[CNAME] > 0 && !typeInList(needle, CNAME) {
|
|
// add set to result
|
|
result = append(result, sets[0])
|
|
|
|
// get normalized address
|
|
address := NormalizeDomain(sets[0].Records[0].Address, true, false, false)
|
|
|
|
// continue lookup with CNAME address if address is in zone
|
|
if InZone(z.Name, address) {
|
|
name = address
|
|
continue
|
|
}
|
|
|
|
return result, false, nil
|
|
}
|
|
|
|
// add matching set
|
|
for _, set := range sets {
|
|
if typeInList(needle, set.Type) {
|
|
// add set to result
|
|
result = append(result, set)
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
// return if there are not matches, but indicate that there are sets
|
|
// available for other types
|
|
if len(result) == 0 {
|
|
return nil, true, nil
|
|
}
|
|
|
|
return result, false, nil
|
|
}
|
|
}
|