526 lines
13 KiB
Go
526 lines
13 KiB
Go
|
|
package server
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"net"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"sync"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/miekg/dns"
|
||
|
|
)
|
||
|
|
|
||
|
|
// RecordType represents supported DNS record types.
|
||
|
|
type RecordType string
|
||
|
|
|
||
|
|
const (
|
||
|
|
TypeA RecordType = "A"
|
||
|
|
TypeAAAA RecordType = "AAAA"
|
||
|
|
TypeCNAME RecordType = "CNAME"
|
||
|
|
TypeMX RecordType = "MX"
|
||
|
|
TypeTXT RecordType = "TXT"
|
||
|
|
TypeNS RecordType = "NS"
|
||
|
|
TypeSRV RecordType = "SRV"
|
||
|
|
TypePTR RecordType = "PTR"
|
||
|
|
TypeSOA RecordType = "SOA"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Record is a single DNS record.
|
||
|
|
type Record struct {
|
||
|
|
ID string `json:"id"`
|
||
|
|
Type RecordType `json:"type"`
|
||
|
|
Name string `json:"name"`
|
||
|
|
Value string `json:"value"`
|
||
|
|
TTL uint32 `json:"ttl"`
|
||
|
|
Priority uint16 `json:"priority,omitempty"` // MX, SRV
|
||
|
|
Weight uint16 `json:"weight,omitempty"` // SRV
|
||
|
|
Port uint16 `json:"port,omitempty"` // SRV
|
||
|
|
}
|
||
|
|
|
||
|
|
// Zone represents a DNS zone with its records.
|
||
|
|
type Zone struct {
|
||
|
|
Domain string `json:"domain"`
|
||
|
|
SOA SOARecord `json:"soa"`
|
||
|
|
Records []Record `json:"records"`
|
||
|
|
DNSSEC bool `json:"dnssec"`
|
||
|
|
CreatedAt string `json:"created_at"`
|
||
|
|
UpdatedAt string `json:"updated_at"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// SOARecord holds SOA-specific fields.
|
||
|
|
type SOARecord struct {
|
||
|
|
PrimaryNS string `json:"primary_ns"`
|
||
|
|
AdminEmail string `json:"admin_email"`
|
||
|
|
Serial uint32 `json:"serial"`
|
||
|
|
Refresh uint32 `json:"refresh"`
|
||
|
|
Retry uint32 `json:"retry"`
|
||
|
|
Expire uint32 `json:"expire"`
|
||
|
|
MinTTL uint32 `json:"min_ttl"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// ZoneStore manages zones on disk and in memory.
|
||
|
|
type ZoneStore struct {
|
||
|
|
mu sync.RWMutex
|
||
|
|
zones map[string]*Zone
|
||
|
|
zonesDir string
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewZoneStore creates a store backed by a directory.
|
||
|
|
func NewZoneStore(dir string) *ZoneStore {
|
||
|
|
os.MkdirAll(dir, 0755)
|
||
|
|
return &ZoneStore{
|
||
|
|
zones: make(map[string]*Zone),
|
||
|
|
zonesDir: dir,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// LoadAll reads all zone files from disk.
|
||
|
|
func (s *ZoneStore) LoadAll() error {
|
||
|
|
entries, err := os.ReadDir(s.zonesDir)
|
||
|
|
if err != nil {
|
||
|
|
if os.IsNotExist(err) {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
for _, e := range entries {
|
||
|
|
if filepath.Ext(e.Name()) != ".json" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
data, err := os.ReadFile(filepath.Join(s.zonesDir, e.Name()))
|
||
|
|
if err != nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
var z Zone
|
||
|
|
if err := json.Unmarshal(data, &z); err != nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
s.zones[dns.Fqdn(z.Domain)] = &z
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Save writes a zone to disk.
|
||
|
|
func (s *ZoneStore) Save(z *Zone) error {
|
||
|
|
z.UpdatedAt = time.Now().UTC().Format(time.RFC3339)
|
||
|
|
data, err := json.MarshalIndent(z, "", " ")
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
fname := filepath.Join(s.zonesDir, z.Domain+".json")
|
||
|
|
return os.WriteFile(fname, data, 0644)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get returns a zone by domain.
|
||
|
|
func (s *ZoneStore) Get(domain string) *Zone {
|
||
|
|
s.mu.RLock()
|
||
|
|
defer s.mu.RUnlock()
|
||
|
|
return s.zones[dns.Fqdn(domain)]
|
||
|
|
}
|
||
|
|
|
||
|
|
// List returns all zones.
|
||
|
|
func (s *ZoneStore) List() []*Zone {
|
||
|
|
s.mu.RLock()
|
||
|
|
defer s.mu.RUnlock()
|
||
|
|
result := make([]*Zone, 0, len(s.zones))
|
||
|
|
for _, z := range s.zones {
|
||
|
|
result = append(result, z)
|
||
|
|
}
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create adds a new zone.
|
||
|
|
func (s *ZoneStore) Create(domain string) (*Zone, error) {
|
||
|
|
fqdn := dns.Fqdn(domain)
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
|
||
|
|
if _, exists := s.zones[fqdn]; exists {
|
||
|
|
return nil, fmt.Errorf("zone %s already exists", domain)
|
||
|
|
}
|
||
|
|
|
||
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
||
|
|
z := &Zone{
|
||
|
|
Domain: domain,
|
||
|
|
SOA: SOARecord{
|
||
|
|
PrimaryNS: "ns1." + domain,
|
||
|
|
AdminEmail: "admin." + domain,
|
||
|
|
Serial: uint32(time.Now().Unix()),
|
||
|
|
Refresh: 3600,
|
||
|
|
Retry: 600,
|
||
|
|
Expire: 86400,
|
||
|
|
MinTTL: 300,
|
||
|
|
},
|
||
|
|
Records: []Record{
|
||
|
|
{ID: "ns1", Type: TypeNS, Name: domain + ".", Value: "ns1." + domain + ".", TTL: 3600},
|
||
|
|
},
|
||
|
|
CreatedAt: now,
|
||
|
|
UpdatedAt: now,
|
||
|
|
}
|
||
|
|
s.zones[fqdn] = z
|
||
|
|
return z, s.Save(z)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Delete removes a zone.
|
||
|
|
func (s *ZoneStore) Delete(domain string) error {
|
||
|
|
fqdn := dns.Fqdn(domain)
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
|
||
|
|
if _, exists := s.zones[fqdn]; !exists {
|
||
|
|
return fmt.Errorf("zone %s not found", domain)
|
||
|
|
}
|
||
|
|
delete(s.zones, fqdn)
|
||
|
|
fname := filepath.Join(s.zonesDir, domain+".json")
|
||
|
|
os.Remove(fname)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// AddRecord adds a record to a zone.
|
||
|
|
func (s *ZoneStore) AddRecord(domain string, rec Record) error {
|
||
|
|
fqdn := dns.Fqdn(domain)
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
|
||
|
|
z, ok := s.zones[fqdn]
|
||
|
|
if !ok {
|
||
|
|
return fmt.Errorf("zone %s not found", domain)
|
||
|
|
}
|
||
|
|
|
||
|
|
if rec.ID == "" {
|
||
|
|
rec.ID = fmt.Sprintf("r%d", time.Now().UnixNano())
|
||
|
|
}
|
||
|
|
if rec.TTL == 0 {
|
||
|
|
rec.TTL = 300
|
||
|
|
}
|
||
|
|
|
||
|
|
z.Records = append(z.Records, rec)
|
||
|
|
z.SOA.Serial++
|
||
|
|
return s.Save(z)
|
||
|
|
}
|
||
|
|
|
||
|
|
// DeleteRecord removes a record by ID.
|
||
|
|
func (s *ZoneStore) DeleteRecord(domain, recordID string) error {
|
||
|
|
fqdn := dns.Fqdn(domain)
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
|
||
|
|
z, ok := s.zones[fqdn]
|
||
|
|
if !ok {
|
||
|
|
return fmt.Errorf("zone %s not found", domain)
|
||
|
|
}
|
||
|
|
|
||
|
|
for i, r := range z.Records {
|
||
|
|
if r.ID == recordID {
|
||
|
|
z.Records = append(z.Records[:i], z.Records[i+1:]...)
|
||
|
|
z.SOA.Serial++
|
||
|
|
return s.Save(z)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return fmt.Errorf("record %s not found", recordID)
|
||
|
|
}
|
||
|
|
|
||
|
|
// UpdateRecord updates a record by ID.
|
||
|
|
func (s *ZoneStore) UpdateRecord(domain, recordID string, rec Record) error {
|
||
|
|
fqdn := dns.Fqdn(domain)
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
|
||
|
|
z, ok := s.zones[fqdn]
|
||
|
|
if !ok {
|
||
|
|
return fmt.Errorf("zone %s not found", domain)
|
||
|
|
}
|
||
|
|
|
||
|
|
for i, r := range z.Records {
|
||
|
|
if r.ID == recordID {
|
||
|
|
rec.ID = recordID
|
||
|
|
z.Records[i] = rec
|
||
|
|
z.SOA.Serial++
|
||
|
|
return s.Save(z)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return fmt.Errorf("record %s not found", recordID)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Lookup finds records matching a query name and type within all zones.
|
||
|
|
func (s *ZoneStore) Lookup(name string, qtype uint16) []dns.RR {
|
||
|
|
s.mu.RLock()
|
||
|
|
defer s.mu.RUnlock()
|
||
|
|
|
||
|
|
fqdn := dns.Fqdn(name)
|
||
|
|
var results []dns.RR
|
||
|
|
|
||
|
|
// Find the zone for this name
|
||
|
|
for zoneDomain, z := range s.zones {
|
||
|
|
if !dns.IsSubDomain(zoneDomain, fqdn) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
// Check records
|
||
|
|
for _, rec := range z.Records {
|
||
|
|
recFQDN := dns.Fqdn(rec.Name)
|
||
|
|
if recFQDN != fqdn {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if rr := recordToRR(rec, fqdn); rr != nil {
|
||
|
|
if qtype == dns.TypeANY || rr.Header().Rrtype == qtype {
|
||
|
|
results = append(results, rr)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// SOA for zone apex
|
||
|
|
if fqdn == zoneDomain && (qtype == dns.TypeSOA || qtype == dns.TypeANY) {
|
||
|
|
soa := &dns.SOA{
|
||
|
|
Hdr: dns.RR_Header{Name: zoneDomain, Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: z.SOA.MinTTL},
|
||
|
|
Ns: dns.Fqdn(z.SOA.PrimaryNS),
|
||
|
|
Mbox: dns.Fqdn(z.SOA.AdminEmail),
|
||
|
|
Serial: z.SOA.Serial,
|
||
|
|
Refresh: z.SOA.Refresh,
|
||
|
|
Retry: z.SOA.Retry,
|
||
|
|
Expire: z.SOA.Expire,
|
||
|
|
Minttl: z.SOA.MinTTL,
|
||
|
|
}
|
||
|
|
results = append(results, soa)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return results
|
||
|
|
}
|
||
|
|
|
||
|
|
func recordToRR(rec Record, fqdn string) dns.RR {
|
||
|
|
hdr := dns.RR_Header{Name: fqdn, Class: dns.ClassINET, Ttl: rec.TTL}
|
||
|
|
|
||
|
|
switch rec.Type {
|
||
|
|
case TypeA:
|
||
|
|
hdr.Rrtype = dns.TypeA
|
||
|
|
rr := &dns.A{Hdr: hdr}
|
||
|
|
rr.A = parseIP(rec.Value)
|
||
|
|
if rr.A == nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return rr
|
||
|
|
case TypeAAAA:
|
||
|
|
hdr.Rrtype = dns.TypeAAAA
|
||
|
|
rr := &dns.AAAA{Hdr: hdr}
|
||
|
|
rr.AAAA = parseIP(rec.Value)
|
||
|
|
if rr.AAAA == nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return rr
|
||
|
|
case TypeCNAME:
|
||
|
|
hdr.Rrtype = dns.TypeCNAME
|
||
|
|
return &dns.CNAME{Hdr: hdr, Target: dns.Fqdn(rec.Value)}
|
||
|
|
case TypeMX:
|
||
|
|
hdr.Rrtype = dns.TypeMX
|
||
|
|
return &dns.MX{Hdr: hdr, Preference: rec.Priority, Mx: dns.Fqdn(rec.Value)}
|
||
|
|
case TypeTXT:
|
||
|
|
hdr.Rrtype = dns.TypeTXT
|
||
|
|
return &dns.TXT{Hdr: hdr, Txt: []string{rec.Value}}
|
||
|
|
case TypeNS:
|
||
|
|
hdr.Rrtype = dns.TypeNS
|
||
|
|
return &dns.NS{Hdr: hdr, Ns: dns.Fqdn(rec.Value)}
|
||
|
|
case TypeSRV:
|
||
|
|
hdr.Rrtype = dns.TypeSRV
|
||
|
|
return &dns.SRV{Hdr: hdr, Priority: rec.Priority, Weight: rec.Weight, Port: rec.Port, Target: dns.Fqdn(rec.Value)}
|
||
|
|
case TypePTR:
|
||
|
|
hdr.Rrtype = dns.TypePTR
|
||
|
|
return &dns.PTR{Hdr: hdr, Ptr: dns.Fqdn(rec.Value)}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func parseIP(s string) net.IP {
|
||
|
|
return net.ParseIP(s)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ExportZoneFile exports a zone in BIND zone file format.
|
||
|
|
func (s *ZoneStore) ExportZoneFile(domain string) (string, error) {
|
||
|
|
s.mu.RLock()
|
||
|
|
defer s.mu.RUnlock()
|
||
|
|
|
||
|
|
z, ok := s.zones[dns.Fqdn(domain)]
|
||
|
|
if !ok {
|
||
|
|
return "", fmt.Errorf("zone %s not found", domain)
|
||
|
|
}
|
||
|
|
|
||
|
|
var b strings.Builder
|
||
|
|
b.WriteString(fmt.Sprintf("; Zone file for %s\n", z.Domain))
|
||
|
|
b.WriteString(fmt.Sprintf("; Exported at %s\n", time.Now().UTC().Format(time.RFC3339)))
|
||
|
|
b.WriteString(fmt.Sprintf("$ORIGIN %s.\n", z.Domain))
|
||
|
|
b.WriteString(fmt.Sprintf("$TTL %d\n\n", z.SOA.MinTTL))
|
||
|
|
|
||
|
|
// SOA
|
||
|
|
b.WriteString(fmt.Sprintf("@ IN SOA %s. %s. (\n", z.SOA.PrimaryNS, z.SOA.AdminEmail))
|
||
|
|
b.WriteString(fmt.Sprintf(" %d ; serial\n", z.SOA.Serial))
|
||
|
|
b.WriteString(fmt.Sprintf(" %d ; refresh\n", z.SOA.Refresh))
|
||
|
|
b.WriteString(fmt.Sprintf(" %d ; retry\n", z.SOA.Retry))
|
||
|
|
b.WriteString(fmt.Sprintf(" %d ; expire\n", z.SOA.Expire))
|
||
|
|
b.WriteString(fmt.Sprintf(" %d ; minimum TTL\n)\n\n", z.SOA.MinTTL))
|
||
|
|
|
||
|
|
// Records grouped by type
|
||
|
|
for _, rec := range z.Records {
|
||
|
|
name := rec.Name
|
||
|
|
// Make relative to origin
|
||
|
|
suffix := "." + z.Domain + "."
|
||
|
|
if strings.HasSuffix(name, suffix) {
|
||
|
|
name = strings.TrimSuffix(name, suffix)
|
||
|
|
} else if name == z.Domain+"." {
|
||
|
|
name = "@"
|
||
|
|
}
|
||
|
|
|
||
|
|
switch rec.Type {
|
||
|
|
case TypeMX:
|
||
|
|
b.WriteString(fmt.Sprintf("%-24s %d IN MX %d %s\n", name, rec.TTL, rec.Priority, rec.Value))
|
||
|
|
case TypeSRV:
|
||
|
|
b.WriteString(fmt.Sprintf("%-24s %d IN SRV %d %d %d %s\n", name, rec.TTL, rec.Priority, rec.Weight, rec.Port, rec.Value))
|
||
|
|
default:
|
||
|
|
b.WriteString(fmt.Sprintf("%-24s %d IN %-6s %s\n", name, rec.TTL, rec.Type, rec.Value))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return b.String(), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// ImportZoneFile parses a BIND-style zone file and adds records.
|
||
|
|
// Returns number of records added.
|
||
|
|
func (s *ZoneStore) ImportZoneFile(domain, content string) (int, error) {
|
||
|
|
fqdn := dns.Fqdn(domain)
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
|
||
|
|
z, ok := s.zones[fqdn]
|
||
|
|
if !ok {
|
||
|
|
return 0, fmt.Errorf("zone %s not found — create it first", domain)
|
||
|
|
}
|
||
|
|
|
||
|
|
added := 0
|
||
|
|
zp := dns.NewZoneParser(strings.NewReader(content), dns.Fqdn(domain), "")
|
||
|
|
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
|
||
|
|
hdr := rr.Header()
|
||
|
|
rec := Record{
|
||
|
|
ID: fmt.Sprintf("imp%d", time.Now().UnixNano()+int64(added)),
|
||
|
|
Name: hdr.Name,
|
||
|
|
TTL: hdr.Ttl,
|
||
|
|
}
|
||
|
|
|
||
|
|
switch v := rr.(type) {
|
||
|
|
case *dns.A:
|
||
|
|
rec.Type = TypeA
|
||
|
|
rec.Value = v.A.String()
|
||
|
|
case *dns.AAAA:
|
||
|
|
rec.Type = TypeAAAA
|
||
|
|
rec.Value = v.AAAA.String()
|
||
|
|
case *dns.CNAME:
|
||
|
|
rec.Type = TypeCNAME
|
||
|
|
rec.Value = v.Target
|
||
|
|
case *dns.MX:
|
||
|
|
rec.Type = TypeMX
|
||
|
|
rec.Value = v.Mx
|
||
|
|
rec.Priority = v.Preference
|
||
|
|
case *dns.TXT:
|
||
|
|
rec.Type = TypeTXT
|
||
|
|
rec.Value = strings.Join(v.Txt, " ")
|
||
|
|
case *dns.NS:
|
||
|
|
rec.Type = TypeNS
|
||
|
|
rec.Value = v.Ns
|
||
|
|
case *dns.SRV:
|
||
|
|
rec.Type = TypeSRV
|
||
|
|
rec.Value = v.Target
|
||
|
|
rec.Priority = v.Priority
|
||
|
|
rec.Weight = v.Weight
|
||
|
|
rec.Port = v.Port
|
||
|
|
case *dns.PTR:
|
||
|
|
rec.Type = TypePTR
|
||
|
|
rec.Value = v.Ptr
|
||
|
|
default:
|
||
|
|
continue // Skip unsupported types
|
||
|
|
}
|
||
|
|
|
||
|
|
z.Records = append(z.Records, rec)
|
||
|
|
added++
|
||
|
|
}
|
||
|
|
|
||
|
|
if added > 0 {
|
||
|
|
z.SOA.Serial++
|
||
|
|
s.Save(z)
|
||
|
|
}
|
||
|
|
return added, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// CloneZone duplicates a zone under a new domain.
|
||
|
|
func (s *ZoneStore) CloneZone(srcDomain, dstDomain string) (*Zone, error) {
|
||
|
|
srcFQDN := dns.Fqdn(srcDomain)
|
||
|
|
dstFQDN := dns.Fqdn(dstDomain)
|
||
|
|
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
|
||
|
|
src, ok := s.zones[srcFQDN]
|
||
|
|
if !ok {
|
||
|
|
return nil, fmt.Errorf("source zone %s not found", srcDomain)
|
||
|
|
}
|
||
|
|
if _, exists := s.zones[dstFQDN]; exists {
|
||
|
|
return nil, fmt.Errorf("destination zone %s already exists", dstDomain)
|
||
|
|
}
|
||
|
|
|
||
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
||
|
|
z := &Zone{
|
||
|
|
Domain: dstDomain,
|
||
|
|
SOA: SOARecord{
|
||
|
|
PrimaryNS: strings.Replace(src.SOA.PrimaryNS, srcDomain, dstDomain, -1),
|
||
|
|
AdminEmail: strings.Replace(src.SOA.AdminEmail, srcDomain, dstDomain, -1),
|
||
|
|
Serial: uint32(time.Now().Unix()),
|
||
|
|
Refresh: src.SOA.Refresh,
|
||
|
|
Retry: src.SOA.Retry,
|
||
|
|
Expire: src.SOA.Expire,
|
||
|
|
MinTTL: src.SOA.MinTTL,
|
||
|
|
},
|
||
|
|
CreatedAt: now,
|
||
|
|
UpdatedAt: now,
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clone records, replacing domain references
|
||
|
|
for _, rec := range src.Records {
|
||
|
|
newRec := rec
|
||
|
|
newRec.ID = fmt.Sprintf("c%d", time.Now().UnixNano())
|
||
|
|
newRec.Name = strings.Replace(rec.Name, srcDomain, dstDomain, -1)
|
||
|
|
newRec.Value = strings.Replace(rec.Value, srcDomain, dstDomain, -1)
|
||
|
|
z.Records = append(z.Records, newRec)
|
||
|
|
time.Sleep(time.Nanosecond) // Ensure unique IDs
|
||
|
|
}
|
||
|
|
|
||
|
|
s.zones[dstFQDN] = z
|
||
|
|
return z, s.Save(z)
|
||
|
|
}
|
||
|
|
|
||
|
|
// BulkAddRecords adds multiple records at once.
|
||
|
|
func (s *ZoneStore) BulkAddRecords(domain string, records []Record) (int, error) {
|
||
|
|
fqdn := dns.Fqdn(domain)
|
||
|
|
s.mu.Lock()
|
||
|
|
defer s.mu.Unlock()
|
||
|
|
|
||
|
|
z, ok := s.zones[fqdn]
|
||
|
|
if !ok {
|
||
|
|
return 0, fmt.Errorf("zone %s not found", domain)
|
||
|
|
}
|
||
|
|
|
||
|
|
added := 0
|
||
|
|
for _, rec := range records {
|
||
|
|
if rec.ID == "" {
|
||
|
|
rec.ID = fmt.Sprintf("b%d", time.Now().UnixNano()+int64(added))
|
||
|
|
}
|
||
|
|
if rec.TTL == 0 {
|
||
|
|
rec.TTL = 300
|
||
|
|
}
|
||
|
|
z.Records = append(z.Records, rec)
|
||
|
|
added++
|
||
|
|
}
|
||
|
|
|
||
|
|
if added > 0 {
|
||
|
|
z.SOA.Serial++
|
||
|
|
s.Save(z)
|
||
|
|
}
|
||
|
|
return added, nil
|
||
|
|
}
|