@@ 20,10 20,12 @@ func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
m.Compress = false
m.SetEdns0(4096, true)
+ requestWasValidTsig := false
if r.IsTsig() != nil {
slog.Debug("Request is TSIG")
if w.TsigStatus() == nil {
slog.Debug("TSIG is valid")
+ requestWasValidTsig = true
// NOTE: The first argument here is the keyname.
m.SetTsig(r.Extra[len(r.Extra)-1].(*dns.TSIG).Hdr.Name, dns.HmacSHA256, 300, time.Now().Unix())
} else {
@@ 36,6 38,14 @@ func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
switch r.Opcode {
case dns.OpcodeQuery:
parseQuery(m, r)
+ case dns.OpcodeUpdate:
+ if requestWasValidTsig {
+ parseUpdate(m, r)
+ } else {
+ // Don't process updates if request wasn't tsig.
+ // NOTE: I figured FORMERR was the best for this. Do you have any objections?
+ m.SetRcode(r, dns.RcodeFormatError)
+ }
default:
slog.Info("Unsupported Opcode", slog.String("type", dns.OpcodeToString[r.Opcode]))
m.SetRcode(r, dns.RcodeNotImplemented)
@@ 0,0 1,135 @@
+/*
+ * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
+ *
+ * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
+ * more information.
+ */
+
+package dns
+
+import (
+ "log/slog"
+ "slices"
+ "strings"
+
+ "git.src.quest/~liljamo/felu/internal/db"
+ "github.com/miekg/dns"
+)
+
+func parseUpdate(m *dns.Msg, r *dns.Msg) {
+ // NOTE: We only handle updates for _acme-challenge TXT records.
+
+ // In Updates, the Zone section only contains one entry (RFC2136 Section 2.3).
+ // Question is Zone in Updates.
+ if len(r.Question) > 1 {
+ m.SetRcode(r, dns.RcodeFormatError)
+ return
+ }
+ zone := r.Question[0]
+
+ // In Updates, the Authority section contains the Update RRs.
+ // Ns is Authority in Updates.
+
+ // RFC 2136 Section 3.4.1 - Prescan
+ for _, rr := range r.Ns {
+ slog.Debug("update prescan", slog.String("rr", rr.String()))
+ hdr := rr.Header()
+
+ if !zoneOf(hdr.Name, zone.Name) {
+ m.SetRcode(r, dns.RcodeNotZone)
+ return
+ }
+ if hdr.Class == zone.Qclass {
+ invalidTypes := []uint16{dns.TypeANY, dns.TypeAXFR, dns.TypeMAILA, dns.TypeMAILB}
+ if slices.Contains(invalidTypes, hdr.Rrtype) {
+ m.SetRcode(r, dns.RcodeFormatError)
+ return
+ }
+ } else if hdr.Class == dns.ClassANY {
+ invalidTypes := []uint16{dns.TypeAXFR, dns.TypeMAILA, dns.TypeMAILB}
+ if hdr.Ttl != 0 ||
+ hdr.Rdlength != 0 ||
+ slices.Contains(invalidTypes, hdr.Rrtype) {
+ m.SetRcode(r, dns.RcodeFormatError)
+ return
+ }
+ } else if hdr.Class == dns.ClassNONE {
+ invalidTypes := []uint16{dns.TypeANY, dns.TypeAXFR, dns.TypeMAILA, dns.TypeMAILB}
+ if hdr.Ttl != 0 || slices.Contains(invalidTypes, hdr.Rrtype) {
+ m.SetRcode(r, dns.RcodeFormatError)
+ return
+ }
+ } else {
+ m.SetRcode(r, dns.RcodeFormatError)
+ return
+ }
+ }
+
+ // RFC 2136 Section 3.4.2 - Update
+ // NOTE: Or well, that's what it would be, but we just error out on anything
+ // that tries to update something else than TXT _acme-challenge, because
+ // that's the only thing we support right now.
+ for _, rr := range r.Ns {
+ slog.Debug("update", slog.String("rr", rr.String()))
+ hdr := rr.Header()
+
+ // TODO: Is ServFail really the best for these?
+ // As per above, if it doesn't start with _acme-challenge, don't handle it.
+ if !strings.HasPrefix(hdr.Name, "_acme-challenge.") {
+ m.SetRcode(r, dns.RcodeServerFailure)
+ return
+ }
+ // As per above, if it's something else than TXT, don't handle it.
+ if hdr.Rrtype != dns.TypeTXT {
+ m.SetRcode(r, dns.RcodeServerFailure)
+ return
+ }
+
+ // Remove
+ if hdr.Class == dns.ClassNONE {
+ var ddnsDomain string
+ if index := strings.IndexByte(zone.Name, '.'); index >= 0 {
+ ddnsDomain = zone.Name[:index]
+ } else {
+ m.SetRcode(r, dns.RcodeNameError)
+ return
+ }
+
+ slog.Debug("removing ACME challenge string", slog.String("ddnsDomain", ddnsDomain))
+
+ err := db.DeleteDomainAcmeChallenge(ddnsDomain)
+ if err != nil {
+ slog.Error("Failed to delete domain ACME challenge", slog.String("err", err.Error()))
+ m.SetRcode(r, dns.RcodeServerFailure)
+ return
+ }
+ }
+
+ // Insert
+ if hdr.Class == zone.Qclass {
+ // At this point, everything should be fine(TM), and we do the update.
+
+ // Split the string representation of the RR into parts.
+ parts := strings.Split(rr.String(), "\t")
+ // Index 4 is the value, trim out any quotes around it.
+ acmeChallengeString := strings.Trim(parts[4], "\"")
+
+ var ddnsDomain string
+ if index := strings.IndexByte(zone.Name, '.'); index >= 0 {
+ ddnsDomain = zone.Name[:index]
+ } else {
+ m.SetRcode(r, dns.RcodeNameError)
+ return
+ }
+
+ slog.Debug("setting ACME challenge string", slog.String("ddnsDomain", ddnsDomain))
+
+ err := db.UpdateDomainAcmeChallenge(ddnsDomain, acmeChallengeString)
+ if err != nil {
+ slog.Error("Failed to update domain ACME challenge", slog.String("err", err.Error()))
+ m.SetRcode(r, dns.RcodeServerFailure)
+ return
+ }
+ }
+ }
+}