DEVELOPMENT ENVIRONMENT

~liljamo/felu

ref: b5195a96196fb17b7b40459ef750d9f916b6c313 felu/internal/dns/update.go -rw-r--r-- 3.8 KiB
b5195a96Jonni Liljamo feat: handle update queries for _acme-challenge 17 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
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
			}
		}
	}
}