From b5195a96196fb17b7b40459ef750d9f916b6c313 Mon Sep 17 00:00:00 2001 From: Jonni Liljamo Date: Mon, 28 Oct 2024 22:03:31 +0200 Subject: [PATCH] feat: handle update queries for _acme-challenge --- internal/dns/handle.go | 10 +++ internal/dns/update.go | 135 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 internal/dns/update.go diff --git a/internal/dns/handle.go b/internal/dns/handle.go index 792c90e..7c52f43 100644 --- a/internal/dns/handle.go +++ b/internal/dns/handle.go @@ -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) diff --git a/internal/dns/update.go b/internal/dns/update.go new file mode 100644 index 0000000..72b0d38 --- /dev/null +++ b/internal/dns/update.go @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * 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 + } + } + } +} -- 2.44.1