mirror of
https://blitiri.com.ar/repos/chasquid
synced 2025-12-17 14:37:02 +00:00
test/util: Add mini DNS server for testing purposes
This is a mini-DNS server for testing purposes. This can be used to set up hermetic tests in containers, and work around glibc's limitation of being unable to create per-process host aliases.
This commit is contained in:
@@ -107,6 +107,10 @@ function conngen() {
|
||||
go run ${UTILDIR}/conngen.go "$@"
|
||||
}
|
||||
|
||||
function minidns() {
|
||||
go run ${UTILDIR}/minidns.go "$@"
|
||||
}
|
||||
|
||||
function success() {
|
||||
echo success
|
||||
}
|
||||
|
||||
305
test/util/minidns.go
Normal file
305
test/util/minidns.go
Normal file
@@ -0,0 +1,305 @@
|
||||
// +build ignore
|
||||
|
||||
// minidns is a trivial DNS server used for testing.
|
||||
//
|
||||
// It takes an "answers" file which contains lines with the following format:
|
||||
//
|
||||
// <domain> <type> <value>
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// blah A 1.2.3.4
|
||||
// blah MX mx1
|
||||
//
|
||||
// Supported types: A, AAAA, MX, TXT.
|
||||
//
|
||||
// It's only meant to be used for testing, so it's not robust, performant, or
|
||||
// standards compliant.
|
||||
//
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"blitiri.com.ar/go/log"
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
)
|
||||
|
||||
var (
|
||||
addr = flag.String("addr", ":53", "address to listen to (UDP)")
|
||||
zonesPath = flag.String("zones", "", "file with the zones")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
srv := &miniDNS{
|
||||
answers: map[string][]dnsmessage.Resource{},
|
||||
}
|
||||
|
||||
if *zonesPath == "" {
|
||||
log.Fatalf("-zones must be given")
|
||||
}
|
||||
var zonesFile *os.File
|
||||
if *zonesPath == "-" {
|
||||
zonesFile = os.Stdin
|
||||
} else {
|
||||
var err error
|
||||
zonesFile, err = os.Open(*zonesPath)
|
||||
if err != nil {
|
||||
log.Fatalf("error opening %v: %v", *zonesPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
srv.loadZones(zonesFile)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
srv.listenAndServeUDP(*addr)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
srv.listenAndServeTCP(*addr)
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
type miniDNS struct {
|
||||
// Domain -> Answers.
|
||||
// We always respond the same regardless of the query.
|
||||
// Not great, but does the trick.
|
||||
answers map[string][]dnsmessage.Resource
|
||||
}
|
||||
|
||||
func (m *miniDNS) listenAndServeUDP(addr string) {
|
||||
conn, err := net.ListenPacket("udp", addr)
|
||||
if err != nil {
|
||||
log.Fatalf("error listening UDP %q: %v", addr, err)
|
||||
}
|
||||
|
||||
log.Infof("listening on %v", conn.LocalAddr())
|
||||
|
||||
buf := make([]byte, 64*1024)
|
||||
for {
|
||||
n, addr, err := conn.ReadFrom(buf)
|
||||
if err != nil {
|
||||
log.Infof("error reading from udp: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
msg := &dnsmessage.Message{}
|
||||
err = msg.Unpack(buf[:n])
|
||||
if err != nil {
|
||||
log.Infof("%v error unpacking message: %v", addr, err)
|
||||
}
|
||||
|
||||
if lq := len(msg.Questions); lq != 1 {
|
||||
log.Infof("%v/%-5d dropping packet with %d questions",
|
||||
addr, msg.ID, lq)
|
||||
continue
|
||||
}
|
||||
q := msg.Questions[0]
|
||||
log.Infof("%v/%-5d Q: %s %s %s",
|
||||
addr, msg.ID, q.Name, q.Type, q.Class)
|
||||
|
||||
reply := m.handle(msg)
|
||||
rbuf, err := reply.Pack()
|
||||
if err != nil {
|
||||
log.Fatalf("error packing reply: %v", err)
|
||||
}
|
||||
|
||||
conn.WriteTo(rbuf, addr)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *miniDNS) listenAndServeTCP(addr string) {
|
||||
ls, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
log.Fatalf("error listening TCP %q: %v", addr, err)
|
||||
}
|
||||
|
||||
log.Infof("listening on %v", addr)
|
||||
|
||||
for {
|
||||
conn, err := ls.Accept()
|
||||
if err != nil {
|
||||
log.Infof("error accepting: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
msg, err := readTCPMessage(conn)
|
||||
if err != nil {
|
||||
log.Infof("%v error reading message: %v", addr, err)
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
if lq := len(msg.Questions); lq != 1 {
|
||||
log.Infof("%v/%-5d dropping packet with %d questions",
|
||||
addr, msg.ID, lq)
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
q := msg.Questions[0]
|
||||
log.Infof("%v/%-5d Q: %s %s %s",
|
||||
addr, msg.ID, q.Name, q.Type, q.Class)
|
||||
|
||||
reply := m.handle(msg)
|
||||
err = writeTCPMessage(conn, reply)
|
||||
if err != nil {
|
||||
log.Infof("error writing reply: %v", err)
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func readTCPMessage(conn net.Conn) (*dnsmessage.Message, error) {
|
||||
// Read the 2-byte length first, then the message.
|
||||
lenHdr := struct{ Len uint16 }{}
|
||||
err := binary.Read(conn, binary.BigEndian, &lenHdr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := make([]byte, lenHdr.Len)
|
||||
err = binary.Read(conn, binary.BigEndian, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg := &dnsmessage.Message{}
|
||||
err = msg.Unpack(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v error unpacking message: %v", addr, err)
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func writeTCPMessage(conn net.Conn, msg *dnsmessage.Message) error {
|
||||
rbuf, err := msg.Pack()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error packing reply: %v", err)
|
||||
}
|
||||
|
||||
lenHdr := struct{ Len uint16 }{Len: uint16(len(rbuf))}
|
||||
err = binary.Write(conn, binary.BigEndian, lenHdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = conn.Write(rbuf)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *miniDNS) handle(msg *dnsmessage.Message) *dnsmessage.Message {
|
||||
reply := &dnsmessage.Message{
|
||||
Header: dnsmessage.Header{
|
||||
ID: msg.ID,
|
||||
Response: true,
|
||||
RCode: dnsmessage.RCodeSuccess,
|
||||
},
|
||||
Questions: msg.Questions,
|
||||
}
|
||||
|
||||
q := msg.Questions[0]
|
||||
if answers, ok := m.answers[q.Name.String()]; ok {
|
||||
for _, ans := range answers {
|
||||
if q.Type == ans.Header.Type {
|
||||
log.Infof("-> %s %v", q.Type, ans.Body)
|
||||
reply.Answers = append(reply.Answers, ans)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Infof("-> NXERROR")
|
||||
reply.Header.RCode = dnsmessage.RCodeNameError
|
||||
}
|
||||
|
||||
return reply
|
||||
}
|
||||
|
||||
func (m *miniDNS) loadZones(f *os.File) {
|
||||
scanner := bufio.NewScanner(f)
|
||||
lineno := 0
|
||||
for scanner.Scan() {
|
||||
lineno++
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if strings.HasPrefix(line, "#") || line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
vs := regexp.MustCompile("\\s+").Split(line, 3)
|
||||
if len(vs) != 3 {
|
||||
log.Fatalf("line %d: invalid format", lineno)
|
||||
}
|
||||
domain, t, value := vs[0], vs[1], vs[2]
|
||||
if !strings.HasSuffix(domain, ".") {
|
||||
domain += "."
|
||||
}
|
||||
|
||||
var body dnsmessage.ResourceBody
|
||||
var qType dnsmessage.Type
|
||||
switch strings.ToLower(t) {
|
||||
case "a":
|
||||
qType = dnsmessage.TypeA
|
||||
ip := net.ParseIP(value).To4()
|
||||
if ip == nil {
|
||||
log.Fatalf("line %d: invalid IP %q", lineno, value)
|
||||
}
|
||||
a := &dnsmessage.AResource{}
|
||||
copy(a.A[:], ip[:4])
|
||||
body = a
|
||||
case "aaaa":
|
||||
qType = dnsmessage.TypeAAAA
|
||||
ip := net.ParseIP(value).To16()
|
||||
if ip == nil {
|
||||
log.Fatalf("line %d: invalid IP %q", lineno, value)
|
||||
}
|
||||
aaaa := &dnsmessage.AAAAResource{}
|
||||
copy(aaaa.AAAA[:], ip[:16])
|
||||
body = aaaa
|
||||
case "mx":
|
||||
qType = dnsmessage.TypeMX
|
||||
if !strings.HasPrefix(value, ".") {
|
||||
value += "."
|
||||
}
|
||||
|
||||
body = &dnsmessage.MXResource{
|
||||
Pref: 10,
|
||||
MX: dnsmessage.MustNewName(value),
|
||||
}
|
||||
case "txt":
|
||||
qType = dnsmessage.TypeTXT
|
||||
body = &dnsmessage.TXTResource{
|
||||
TXT: []string{value},
|
||||
}
|
||||
default:
|
||||
log.Fatalf("line %d: unknown type %q", lineno, t)
|
||||
}
|
||||
|
||||
answer := dnsmessage.Resource{
|
||||
Header: dnsmessage.ResourceHeader{
|
||||
Name: dnsmessage.MustNewName(domain),
|
||||
Type: qType,
|
||||
Class: dnsmessage.ClassINET,
|
||||
},
|
||||
Body: body,
|
||||
}
|
||||
m.answers[domain] = append(m.answers[domain], answer)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Fatalf("error reading zones: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user