Files
dns-server/main.go
2026-07-21 10:45:33 +02:00

183 lines
4.4 KiB
Go

package main
import (
"encoding/base64"
"encoding/binary"
"fmt"
"io"
"net"
"net/http"
"os"
"strings"
)
const (
TYPE_A = 1
TYPE_AAAA = 28
CLASS_IN = 1
)
const TTL uint32 = 60
var records = map[string]net.IP{}
func main() {
data, err := os.ReadFile("records.ini")
if err != nil {
panic("failed to open records.ini")
}
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
for _, line := range lines {
parts := strings.Split(strings.TrimSpace(line), "=")
ip := net.ParseIP(parts[1])
if ip == nil {
panic("failed to parse IP")
}
records[strings.ToLower(parts[0])] = ip
}
go func() {
fmt.Println("Listening on :8053...")
http.HandleFunc("/dns-query", handleHTTP)
if err := http.ListenAndServe(":8053", nil); err != nil {
panic(err)
}
}()
addr, err := net.ResolveUDPAddr("udp", "0.0.0.0:53")
if err != nil {
panic(err)
}
listener, err := net.ListenUDP("udp", addr)
if err != nil {
panic(err)
}
fmt.Println("Listening on :53...")
for {
req := make([]byte, 4096)
n, addr, err := listener.ReadFromUDP(req)
if err != nil {
fmt.Println("ERROR:", err)
continue
}
go handleUDP(listener, req[:n], addr)
}
}
func handleHTTP(w http.ResponseWriter, r *http.Request) {
var req []byte
var err error
switch r.Method {
case http.MethodGet:
dnsParam := r.URL.Query().Get("dns")
if dnsParam == "" {
http.Error(w, "missing dns parameter", http.StatusBadRequest)
return
}
req, err = base64.RawURLEncoding.DecodeString(dnsParam)
if err != nil {
http.Error(w, "invalid base64 encoding", http.StatusBadRequest)
return
}
case http.MethodPost:
req, err = io.ReadAll(io.LimitReader(r.Body, 65536))
if err != nil {
http.Error(w, "failed to read request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
resp := handleReq(ip, req)
w.Header().Set("Content-Type", "application/dns-message")
w.WriteHeader(http.StatusOK)
w.Write(resp)
}
func handleUDP(listener *net.UDPConn, req []byte, addr *net.UDPAddr) {
resp := handleReq(addr.IP.String(), req)
if _, err := listener.WriteToUDP(resp, addr); err != nil {
fmt.Println(err)
}
}
func handleReq(ip string, req []byte) []byte {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered from panic: CLIENT=%s: %v\n", ip, r)
}
}()
// we assume that QDCOUNT=1 but i think that's reasonable
// https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.2
// TODO: we should probably handle compression but from what i know clients don't generally use it
labels := []string{}
offset := 12
for {
length := int(req[offset])
offset++
if length == 0 || length > 63 {
break
}
labels = append(labels, string(req[offset:offset+length]))
offset += length
}
rawName := req[12:offset]
name := strings.ToLower(strings.Join(labels, "."))
// https://datatracker.ietf.org/doc/html/rfc1035#section-3.2.2
questionType := binary.BigEndian.Uint16(req[offset:])
offset += 2
// https://datatracker.ietf.org/doc/html/rfc1035#section-3.2.4
questionClass := binary.BigEndian.Uint16(req[offset:])
offset += 2
fmt.Printf("CLIENT=%s QNAME=%s TYPE=%d CLASS=%d\n", ip, name, questionType, questionClass)
resp := make([]byte, offset)
copy(resp, req[:offset])
resp[2] |= 0x80 // QR=1 (response)
resp[3] &= 0xf0 // clear RCODE
resp[2] |= 0x04 // authoritative answer
resp[3] &= 0x7f // recursion not available
binary.BigEndian.PutUint16(resp[6:], 0) // ANCOUNT = 0
binary.BigEndian.PutUint16(resp[8:], 0) // NSCOUNT = 0
binary.BigEndian.PutUint16(resp[10:], 0) // ARCOUNT = 0
data := []byte{}
if record, ok := records[name]; questionClass == CLASS_IN && ok {
ip4 := record.To4()
if questionType == TYPE_A && ip4 != nil {
data = ip4
} else if questionType == TYPE_AAAA && ip4 == nil {
data = record
}
} else {
resp[3] |= 0x03 // RCODE=NXDOMAIN
}
if len(data) > 0 {
// https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.3
resp = append(resp, rawName...)
resp = binary.BigEndian.AppendUint16(resp, questionType)
resp = binary.BigEndian.AppendUint16(resp, questionClass)
resp = binary.BigEndian.AppendUint32(resp, TTL)
resp = binary.BigEndian.AppendUint16(resp, uint16(len(data)))
resp = append(resp, data...)
binary.BigEndian.PutUint16(resp[6:], uint16(1)) // ANCOUNT=1
}
return resp
}