/*
* 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
}
}
}
}