/*
* 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"
"net"
"strings"
"git.src.quest/~liljamo/felu/internal/config"
"git.src.quest/~liljamo/felu/internal/db"
"github.com/miekg/dns"
)
func parseQuery(m *dns.Msg, r *dns.Msg) {
for _, q := range m.Question {
slog.Info("Got Query",
slog.Any("id", r.Id),
slog.String("type", dns.TypeToString[q.Qtype]),
slog.String("qname", q.Name),
)
switch q.Qtype {
case dns.TypeA:
handleARecord(&q, m, r)
case dns.TypeCNAME:
// NOTE: This is stubbed like this to make things like lego not shit themselves if they get NOTIMP.
m.SetRcode(r, dns.RcodeNameError)
case dns.TypeNS:
handleNSRecord(&q, m, r)
case dns.TypeSOA:
handleSOARecord(&q, m, r)
case dns.TypeTXT:
handleTXTRecord(&q, m, r)
default:
m.SetRcode(r, dns.RcodeNotImplemented)
}
slog.Info("Responding to Query",
slog.Any("id", r.Id),
slog.String("rcode", dns.RcodeToString[m.Rcode]),
)
}
}
func handleARecord(q *dns.Question, m *dns.Msg, r *dns.Msg) {
qName := strings.ToLower(q.Name)
// TODO: This is probably not a great way to handle this, but it is what it is. For now.
// "Root" Domain A.
if qName == config.FeluConfig.Domain {
m.Answer = append(m.Answer, &dns.A{
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60},
A: net.ParseIP(config.FeluConfig.IPv4),
})
return
}
if index := strings.IndexByte(qName, '.'); index >= 0 {
aRecord, err := db.FetchDomainARecord(qName[:index])
if err != nil {
m.SetRcode(r, dns.RcodeNameError)
} else {
m.Answer = append(m.Answer, &dns.A{
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60},
A: net.ParseIP(aRecord),
})
}
} else {
m.SetRcode(r, dns.RcodeNameError)
}
}
func handleNSRecord(q *dns.Question, m *dns.Msg, r *dns.Msg) {
qName := strings.ToLower(q.Name)
ns := &dns.NS{
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 86400},
Ns: config.FeluConfig.Domain,
}
// "Root" Domain NS.
if qName == config.FeluConfig.Domain {
m.Answer = append(m.Answer, ns)
return
}
if index := strings.IndexByte(qName, '.'); index >= 0 {
// FIXME: other way of checking that the domain exists
_, err := db.FetchDomainARecord(qName[:index])
if err != nil {
m.SetRcode(r, dns.RcodeNameError)
} else {
m.Answer = append(m.Answer, ns)
}
} else {
m.SetRcode(r, dns.RcodeNameError)
}
}
func handleSOARecord(q *dns.Question, m *dns.Msg, r *dns.Msg) {
qName := strings.ToLower(q.Name)
soa := &dns.SOA{
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 0},
Ns: config.FeluConfig.Domain,
Mbox: config.FeluConfig.SOAEmail,
Serial: 2024100301,
Refresh: 86400,
Retry: 7200,
Expire: 3600000,
Minttl: 172800,
}
// "Root" Domain SOA.
if qName == config.FeluConfig.Domain {
m.Answer = append(m.Answer, soa)
return
}
if index := strings.IndexByte(qName, '.'); index >= 0 {
// FIXME: other way of checking that the domain exists
_, err := db.FetchDomainARecord(qName[:index])
if err != nil {
m.SetRcode(r, dns.RcodeNameError)
} else {
m.Answer = append(m.Answer, soa)
}
} else {
m.SetRcode(r, dns.RcodeNameError)
}
}
func handleTXTRecord(q *dns.Question, m *dns.Msg, r *dns.Msg) {
qName := strings.ToLower(q.Name)
// NOTE: This handles only _acme-challenge queries
expectedPrefix := "_acme-challenge."
if !strings.HasPrefix(qName, expectedPrefix) {
m.SetRcode(r, dns.RcodeNameError)
return
}
qNameWithoutPrefix, ok := strings.CutPrefix(qName, expectedPrefix)
if !ok {
m.SetRcode(r, dns.RcodeFormatError)
return
}
var ddnsDomain string
if index := strings.IndexByte(qNameWithoutPrefix, '.'); index >= 0 {
ddnsDomain = qNameWithoutPrefix[:index]
} else {
m.SetRcode(r, dns.RcodeNameError)
return
}
txtRecord, err := db.FetchDomainAcmeChallenge(ddnsDomain)
if err != nil {
m.SetRcode(r, dns.RcodeNameError)
} else {
m.Answer = append(m.Answer, &dns.TXT{
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 0},
Txt: []string{txtRecord},
})
}
}