feature/sz/master-slave-compatible #1

Open
simon wants to merge 2 commits from feature/sz/master-slave-compatible into master
8 changed files with 136 additions and 16 deletions
Showing only changes of commit 87db76cf39 - Show all commits

View File

@ -18,6 +18,12 @@ records = [
# specify that auth.example.org will resolve any *.auth.example.org records # specify that auth.example.org will resolve any *.auth.example.org records
"auth.example.org. NS auth.example.org.", "auth.example.org. NS auth.example.org.",
] ]
# path to cache SOA serial
serialpath = "soa-serial.save"
# slaves to notify on update and allowed to request AXFR
slaves = [
# "10.5.1.1"
]
# debug messages from CORS etc # debug messages from CORS etc
debug = false debug = false

View File

@ -21,4 +21,6 @@ type AcmednsNS interface {
SetOwnAuthKey(key string) SetOwnAuthKey(key string)
SetNotifyStartedFunc(func()) SetNotifyStartedFunc(func())
ParseRecords() ParseRecords()
BumpSerial() error
}
} }

View File

@ -25,6 +25,8 @@ type general struct {
Nsadmin string Nsadmin string
Debug bool Debug bool
StaticRecords []string `toml:"records"` StaticRecords []string `toml:"records"`
Serialpath string
SlaveHosts []string `toml:"slaves"`
} }
type dbsettings struct { type dbsettings struct {

View File

@ -15,10 +15,11 @@ import (
) )
type AcmednsAPI struct { type AcmednsAPI struct {
Config *acmedns.AcmeDnsConfig Config *acmedns.AcmeDnsConfig
DB acmedns.AcmednsDB DB acmedns.AcmednsDB
Logger *zap.SugaredLogger Logger *zap.SugaredLogger
errChan chan error errChan chan error
dnsServers []acmedns.AcmednsNS
} }
func Init(config *acmedns.AcmeDnsConfig, db acmedns.AcmednsDB, logger *zap.SugaredLogger, errChan chan error) AcmednsAPI { func Init(config *acmedns.AcmeDnsConfig, db acmedns.AcmednsDB, logger *zap.SugaredLogger, errChan chan error) AcmednsAPI {
@ -27,6 +28,8 @@ func Init(config *acmedns.AcmeDnsConfig, db acmedns.AcmednsDB, logger *zap.Sugar
} }
func (a *AcmednsAPI) Start(dnsservers []acmedns.AcmednsNS) { func (a *AcmednsAPI) Start(dnsservers []acmedns.AcmednsNS) {
//we need the dnsservers later to bump serial
a.dnsServers = dnsservers
var err error var err error
//TODO: do we want to debug log the HTTP server? //TODO: do we want to debug log the HTTP server?
stderrorlog, err := zap.NewStdLogAt(a.Logger.Desugar(), zap.ErrorLevel) stderrorlog, err := zap.NewStdLogAt(a.Logger.Desugar(), zap.ErrorLevel)

View File

@ -49,6 +49,10 @@ func (a *AcmednsAPI) webUpdatePost(w http.ResponseWriter, r *http.Request, _ htt
upd = []byte("{\"txt\": \"" + atxt.Value + "\"}") upd = []byte("{\"txt\": \"" + atxt.Value + "\"}")
} }
} }
for _, s := range a.dnsServers {
//bump SOA serial on update (and notify slaves if configured)
s.BumpSerial()
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(updStatus) w.WriteHeader(updStatus)
_, _ = w.Write(upd) _, _ = w.Write(upd)

View File

@ -71,6 +71,9 @@ func (n *Nameserver) answer(q dns.Question) ([]dns.RR, int, bool, error) {
r = append(r, txtRRs...) r = append(r, txtRRs...)
} }
} }
if q.Qtype == dns.TypeSOA {
r = append(r, n.SOA)
}
if len(r) > 0 { if len(r) > 0 {
// Make sure that we return NOERROR if there were dynamic records for the domain // Make sure that we return NOERROR if there were dynamic records for the domain
rcode = dns.RcodeSuccess rcode = dns.RcodeSuccess

View File

@ -14,6 +14,7 @@ import (
// Records is a slice of ResourceRecords // Records is a slice of ResourceRecords
type Records struct { type Records struct {
Records []dns.RR Records []dns.RR
NS []dns.RR
} }
type Nameserver struct { type Nameserver struct {
@ -23,7 +24,7 @@ type Nameserver struct {
Server *dns.Server Server *dns.Server
OwnDomain string OwnDomain string
NotifyStartedFunc func() NotifyStartedFunc func()
SOA dns.RR SOA *dns.SOA
mu sync.RWMutex mu sync.RWMutex
personalAuthKey string personalAuthKey string
Domains map[string]Records Domains map[string]Records

View File

@ -2,12 +2,64 @@ package nameserver
import ( import (
"fmt" "fmt"
"os"
"strconv"
"strings" "strings"
"time" "time"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
func loadSerial(path string) (uint32, error) {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return 0, nil // first start
}
return 0, err
}
s := strings.TrimSpace(string(data))
val, err := strconv.ParseUint(s, 10, 32)
if err != nil {
return 0, err
}
return uint32(val), nil
}
func saveSerial(path string, serial uint32) error {
tmp := path + ".tmp"
data := []byte(fmt.Sprintf("%d\n", serial))
// write temp file
if err := os.WriteFile(tmp, data, 0644); err != nil {
return err
}
// atomic replace
return os.Rename(tmp, path)
}
func nextSerial(old uint32) uint32 {
today := time.Now().Format("20060102")
oldStr := fmt.Sprintf("%d", old)
if strings.HasPrefix(oldStr, today) {
return old + 1
}
newSerial, _ := strconv.Atoi(today + "00")
if uint32(newSerial) <= old {
return old + 1
}
return uint32(newSerial)
}
// ParseRecords parses a slice of DNS record string // ParseRecords parses a slice of DNS record string
func (n *Nameserver) ParseRecords() { func (n *Nameserver) ParseRecords() {
for _, v := range n.Config.General.StaticRecords { for _, v := range n.Config.General.StaticRecords {
@ -22,25 +74,72 @@ func (n *Nameserver) ParseRecords() {
n.appendRR(rr) n.appendRR(rr)
} }
// Create serial // Create serial
serial := time.Now().Format("2006010215") serial, err := loadSerial(n.Config.General.Serialpath)
// Add SOA
SOAstring := fmt.Sprintf("%s. SOA %s. %s. %s 28800 7200 604800 86400", strings.ToLower(n.Config.General.Domain), strings.ToLower(n.Config.General.Nsname), strings.ToLower(n.Config.General.Nsadmin), serial)
soarr, err := dns.NewRR(SOAstring)
if err != nil { if err != nil {
n.Logger.Errorw("Error while adding SOA record", n.Logger.Errorw("Could not load temp serial",
"error", err.Error(), "error", err.Error())
"soa", SOAstring)
} else {
n.appendRR(soarr)
n.SOA = soarr
} }
if serial == 0 {
serial = uint32(time.Now().Unix())
}
// Add SOA
//Refresh = 30s → Slaves fragen alle 30s nach Änderungen
//Retry = 10s → Wenn Master nicht erreichbar, probiert der Slave alle 10s erneut
//Expire = 604800s (1w) → Wie lange der Slave die Zone noch behält, falls Master ausfällt
//Minimum TTL = 20s → Resolver cachen die TXT-Einträge nur kurz
//SOAstring := fmt.Sprintf("%s. SOA %s. %s. %s 5 10 604800 20", strings.ToLower(n.Config.General.Domain), strings.ToLower(n.Config.General.Nsname), strings.ToLower(n.Config.General.Nsadmin), serial)
n.SOA = &dns.SOA{
Hdr: dns.RR_Header{
Name: dns.Fqdn(n.Config.General.Domain),
Rrtype: dns.TypeSOA,
Class: dns.ClassINET,
Ttl: 5,
},
Ns: dns.Fqdn(n.Config.General.Nsname),
Mbox: dns.Fqdn(n.Config.General.Nsadmin),
Serial: serial,
Refresh: 5,
Retry: 10,
Expire: 604800,
Minttl: 1,
}
}
func sendNotify(zone string, slaveAddr string) error {
m := new(dns.Msg)
m.SetNotify(dns.Fqdn(zone)) // Set opcode to NOTIFY
m.Authoritative = true // Must be authoritative
c := new(dns.Client)
_, _, err := c.Exchange(m, slaveAddr)
return err
}
func (n *Nameserver) BumpSerial() error {
n.mu.Lock()
defer n.mu.Unlock()
n.SOA.Serial = nextSerial(n.SOA.Serial)
for _, slave := range n.Config.General.SlaveHosts {
slave := slave + ":53"
if err := sendNotify(n.SOA.Hdr.Name, slave); err != nil {
n.Logger.Errorw("Failed to notify slave", "slave", slave, "err", err)
} else {
n.Logger.Debugw("Notify send to slave", "slave", slave)
}
}
return saveSerial(n.Config.General.Serialpath, n.SOA.Serial)
} }
func (n *Nameserver) appendRR(rr dns.RR) { func (n *Nameserver) appendRR(rr dns.RR) {
addDomain := rr.Header().Name addDomain := rr.Header().Name
_, ok := n.Domains[addDomain] _, ok := n.Domains[addDomain]
if !ok { if !ok {
n.Domains[addDomain] = Records{[]dns.RR{rr}} n.Domains[addDomain] = Records{
Records: []dns.RR{rr}, // initialisiere Records
NS: []dns.RR{}, // leeres NS-Slice, sonst Fehler
}
} else { } else {
drecs := n.Domains[addDomain] drecs := n.Domains[addDomain]
drecs.Records = append(drecs.Records, rr) drecs.Records = append(drecs.Records, rr)