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