package rfc2136 import ( "strings" "github.com/miekg/dns" ) // handleUpdate implements the RFC 2136 UPDATE opcode. // // Message layout in an UPDATE (RFC 2136 §2.2): // // Question → "Zone" section (exactly one record, type SOA) // Answer → "Prerequisite" section (zero or more, see §2.4) // Authority → "Update" section (zero or more, see §2.5) // Additional → TSIG, OPT, etc. // // Processing order: // 1. Zone-section validation: zone must be one we're authoritative for. // 2. Prerequisite checks (§3.2). First failure short-circuits with the // RFC-specified rcode (NXDOMAIN/YXDOMAIN/NXRRSET/YXRRSET/NOTAUTH). // 3. Apply updates (§3.4.2). All updates either all succeed or all fail // by acquiring the store lock once for the batch. // // TSIG verification happens before this function is called — see // ServeDNS for the auth gate. func (p *RFC2136) handleUpdate(w dns.ResponseWriter, r *dns.Msg) (int, error) { resp := new(dns.Msg) resp.SetReply(r) // 1. Validate zone section. if len(r.Question) != 1 { log.Debugf("UPDATE rejected: expected 1 Zone record, got %d", len(r.Question)) return p.updateResp(w, resp, dns.RcodeFormatError) } zoneQ := r.Question[0] if zoneQ.Qtype != dns.TypeSOA { log.Debugf("UPDATE rejected: Zone section type=%d, want SOA", zoneQ.Qtype) return p.updateResp(w, resp, dns.RcodeFormatError) } zone := p.findZone(zoneQ.Name) if zone == "" { log.Debugf("UPDATE rejected: zone %q not authoritative", zoneQ.Name) return p.updateResp(w, resp, dns.RcodeNotAuth) } // 2. Verify each prerequisite. Read-locked through the store API. for _, rr := range r.Answer { rcode := p.checkPrereq(zone, rr) if rcode != dns.RcodeSuccess { log.Debugf("UPDATE prereq failed: %s → rcode=%d", rr.String(), rcode) return p.updateResp(w, resp, rcode) } } // 3. Apply updates. We don't take a single batch lock here — each // store operation locks internally. RFC 2136 §3.7 allows the // "atomic" requirement to be relaxed for implementations; with // short-lived ACME records this is fine in practice. for _, rr := range r.Ns { if rcode := p.applyUpdate(zone, rr); rcode != dns.RcodeSuccess { return p.updateResp(w, resp, rcode) } } log.Infof("UPDATE applied: zone=%s prereqs=%d updates=%d gen=%d", zone, len(r.Answer), len(r.Ns), p.store.generation()) return p.updateResp(w, resp, dns.RcodeSuccess) } // updateResp writes the response and returns the rcode/err pair for ServeDNS. func (p *RFC2136) updateResp(w dns.ResponseWriter, resp *dns.Msg, rcode int) (int, error) { resp.Rcode = rcode _ = w.WriteMsg(resp) return rcode, nil } // checkPrereq evaluates one record from the Prerequisite section. // Returns dns.RcodeSuccess if satisfied, or the appropriate error rcode. // // Encoding rules (§3.2.4): // // CLASS=ANY TYPE=ANY → name must exist (else NXDOMAIN) // CLASS=ANY TYPE!=ANY → RRset must exist (else NXRRSET) // CLASS=NONE TYPE=ANY → name must NOT exist (else YXDOMAIN) // CLASS=NONE TYPE!=ANY → RRset must NOT exist (else YXRRSET) // CLASS= ... rdata → RRset must exist with this exact rdata func (p *RFC2136) checkPrereq(zone string, rr dns.RR) int { hdr := rr.Header() name := canon(hdr.Name) // All prereq names must be within the zone. if !inZone(name, zone) { return dns.RcodeNotZone } switch hdr.Class { case dns.ClassANY: // "Name/RRset is in use" if hdr.Rrtype == dns.TypeANY { if !p.store.NameExists(name) && !isApex(name, zone) { return dns.RcodeNameError } return dns.RcodeSuccess } if rrs := p.store.Lookup(name, hdr.Rrtype); rrs == nil { return dns.RcodeNXRrset } return dns.RcodeSuccess case dns.ClassNONE: // "Name/RRset is NOT in use" if hdr.Rrtype == dns.TypeANY { if p.store.NameExists(name) { return dns.RcodeYXDomain } return dns.RcodeSuccess } if rrs := p.store.Lookup(name, hdr.Rrtype); rrs != nil { return dns.RcodeYXRrset } return dns.RcodeSuccess default: // CLASS = zone class. Exact rdata match required (§3.2.5). // Skipped for v1 — Caddy/caddy-dns/rfc2136 doesn't emit these. // Document the gap; v2 can implement value-prereq if a caller // actually needs it. log.Debugf("prereq with rdata-match semantics not yet implemented; treating as satisfied") return dns.RcodeSuccess } } // applyUpdate handles one record in the Update section per §3.4.2. // // Encoding rules: // // CLASS= RDLEN>0 → add RR (§3.4.2.2) // CLASS=ANY TYPE=ANY → delete all RRsets from name (§3.4.2.3) // CLASS=ANY TYPE!=ANY RDLEN=0 → delete this RRset (§3.4.2.3) // CLASS=NONE RDLEN>0 → delete the specific RR (§3.4.2.4) func (p *RFC2136) applyUpdate(zone string, rr dns.RR) int { hdr := rr.Header() name := canon(hdr.Name) if !inZone(name, zone) { return dns.RcodeNotZone } switch hdr.Class { case dns.ClassANY: if hdr.Rrtype == dns.TypeANY { // Reject deleting the apex (SOA/NS bedrock); the rest of // the zone is free game. if isApex(name, zone) { log.Debugf("apex deletion refused: %s", name) return dns.RcodeRefused } p.store.RemoveName(name) return dns.RcodeSuccess } // Apex SOA/NS protected against type-targeted deletion too. if isApex(name, zone) && (hdr.Rrtype == dns.TypeSOA || hdr.Rrtype == dns.TypeNS) { log.Debugf("apex %s deletion refused: %s", dns.TypeToString[hdr.Rrtype], name) return dns.RcodeRefused } p.store.RemoveRRset(name, hdr.Rrtype) return dns.RcodeSuccess case dns.ClassNONE: if isApex(name, zone) && (hdr.Rrtype == dns.TypeSOA || hdr.Rrtype == dns.TypeNS) { return dns.RcodeRefused } p.store.RemoveRR(rr) return dns.RcodeSuccess default: // CLASS = zone class → add. Apply default TTL if missing. if hdr.Ttl == 0 { hdr.Ttl = p.TTL } // SOA/NS at the apex are synthetic — don't let UPDATE override. if isApex(name, zone) && (hdr.Rrtype == dns.TypeSOA || hdr.Rrtype == dns.TypeNS) { log.Debugf("apex %s add refused: synthetic at this plugin", dns.TypeToString[hdr.Rrtype]) return dns.RcodeRefused } p.store.Add(rr) return dns.RcodeSuccess } } // inZone reports whether name is within zone (either the apex itself // or a sub-name of it). Both arguments must already be canonical. func inZone(name, zone string) bool { return name == zone || strings.HasSuffix(name, "."+zone) } // isApex reports whether name IS the zone's apex. func isApex(name, zone string) bool { return name == zone }