Simple implementation and operation of golang DNS server

Time:2021-10-15

Simple DNS server

Provide a simple DNS server that can query domain names and reverse queries.

The dig command is mainly used to query the host address information from the DNS domain name server.

Find the IP record of www.baidu.com:

Command: dig @ 127.0.0.1 www.baidu.com

在这里插入图片描述

Find the corresponding domain name (PTR record) according to IP:

Command: dig @ 127.0.0.1 – x 220.181.38.150

在这里插入图片描述

Source code:


package main
import (
	"fmt"
	"net"
	"golang.org/x/net/dns/dnsmessage"
)
func main() {
	conn, err := net.ListenUDP("udp", &net.UDPAddr{Port: 53})
	if err != nil {
		panic(err)
	}
	defer conn.Close()
	fmt.Println("Listing ...")
	for {
		buf := make([]byte, 512)
		_, addr, _ := conn.ReadFromUDP(buf)
		var msg dnsmessage.Message
		if err := msg.Unpack(buf); err != nil {
			fmt.Println(err)
			continue
		}
		go ServerDNS(addr, conn, msg)
	}
}
// address books
var (
	addressBookOfA = map[string][4]byte{
		"www.baidu.com.": [4]byte{220, 181, 38, 150},
	}
	addressBookOfPTR = map[string]string{
		"150.38.181.220.in-addr.arpa.": "www.baidu.com.",
	}
)
// ServerDNS serve
func ServerDNS(addr *net.UDPAddr, conn *net.UDPConn, msg dnsmessage.Message) {
	// query info
	if len(msg.Questions) < 1 {
		return
	}
	question := msg.Questions[0]
	var (
		queryTypeStr = question.Type.String()
		queryNameStr = question.Name.String()
		queryType    = question.Type
		queryName, _ = dnsmessage.NewName(queryNameStr)
	)
	fmt.Printf("[%s] queryName: [%s]\n", queryTypeStr, queryNameStr)
	// find record
	var resource dnsmessage.Resource
	switch queryType {
	case dnsmessage.TypeA:
		if rst, ok := addressBookOfA[queryNameStr]; ok {
			resource = NewAResource(queryName, rst)
		} else {
			fmt.Printf("not fount A record queryName: [%s] \n", queryNameStr)
			Response(addr, conn, msg)
			return
		}
	case dnsmessage.TypePTR:
		if rst, ok := addressBookOfPTR[queryName.String()]; ok {
			resource = NewPTRResource(queryName, rst)
		} else {
			fmt.Printf("not fount PTR record queryName: [%s] \n", queryNameStr)
			Response(addr, conn, msg)
			return
		}
	default:
		fmt.Printf("not support dns queryType: [%s] \n", queryTypeStr)
		return
	}
	// send response
	msg.Response = true
	msg.Answers = append(msg.Answers, resource)
	Response(addr, conn, msg)
}
// Response return
func Response(addr *net.UDPAddr, conn *net.UDPConn, msg dnsmessage.Message) {
	packed, err := msg.Pack()
	if err != nil {
		fmt.Println(err)
		return
	}
	if _, err := conn.WriteToUDP(packed, addr); err != nil {
		fmt.Println(err)
	}
}
// NewAResource A record
func NewAResource(query dnsmessage.Name, a [4]byte) dnsmessage.Resource {
	return dnsmessage.Resource{
		Header: dnsmessage.ResourceHeader{
			Name:  query,
			Class: dnsmessage.ClassINET,
			TTL:   600,
		},
		Body: &dnsmessage.AResource{
			A: a,
		},
	}
}
// NewPTRResource PTR record
func NewPTRResource(query dnsmessage.Name, ptr string) dnsmessage.Resource {
	name, _ := dnsmessage.NewName(ptr)
	return dnsmessage.Resource{
		Header: dnsmessage.ResourceHeader{
			Name:  query,
			Class: dnsmessage.ClassINET,
		},
		Body: &dnsmessage.PTRResource{
			PTR: name,
		},
	}
}

Supplement: golang custom DNS nameserver

In some cases, we want the program to query the domain name through the user-defined nameserver rather than the nameserver given by the operating system. This paper introduces how to implement the user-defined nameserver in golang.

DNS resolution process

In golang, domain name resolution is generally realized through lookuphost (CTX context. Context, host string) (addrs [] string, err, error) of net.resolver,

The parsing process is as follows:

Check whether there is a resolution record in the local hosts file. If there is, the resolution address is returned

If it does not exist, i.e. a recursive query is initiated according to the nameserver read in resolv.conf

Nameserver constantly initiates iterative queries to the superior nameserver

The nameserver finally returns the query results to the requester

Users can add a specific nameserver by modifying / etc / resolv.conf, but we do not want to change the system configuration in some scenarios. For example, in kubernetes, as a sidecar service, you need to access other services in the cluster through the service. You must change the dnspolicy to clusterfirst, but this may affect the DNS query efficiency of other containers.

Custom nameserver

To customize the nameserver in golang, we need to implement a resolver ourselves. For httpclient, we need to customize dialcontext ()

Resolver is implemented as follows:

//Default dialer
dialer := &net.Dialer{
  Timeout: 1 * time.Second,
}
//Define resolver
resolver := &net.Resolver{
 Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
  Return dialer. Dialcontext (CTX, "TCP", nameserver) // request nameserver to resolve the domain name through TCP
 },
}

The custom dialer is as follows:

type Dialer struct {
 dialer     *net.Dialer
 resolver   *net.Resolver
 nameserver string
}
// NewDialer create a Dialer with user's nameserver.
func NewDialer(dialer *net.Dialer, nameserver string) (*Dialer, error) {
 conn, err := dialer.Dial("tcp", nameserver)
 if err != nil {
  return nil, err
 }
 defer conn.Close()
 return &Dialer{
  dialer: dialer,
  resolver: &net.Resolver{
   Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
    return dialer.DialContext(ctx, "tcp", nameserver)
   },
  },
  Nameserver: nameserver, // nameserver set by the user
 }, nil
}
// DialContext connects to the address on the named network using
// the provided context.
func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
 host, port, err := net.SplitHostPort(address)
 if err != nil {
  return nil, err
 }
 IPS, err: = d.resolver.lookuphost (CTX, host) // query the domain name through the custom nameserver
 for _, ip := range ips {
    //Create link
  conn, err := d.dialer.DialContext(ctx, network, ip+":"+port)
  if err == nil {
   return conn, nil
  }
 }
 return d.dialer.DialContext(ctx, network, address)
}

The custom dialcontext () in httpclient is as follows:


ndialer, _ := NewDialer(dialer, nameserver)
client := &http.Client{
  Transport: &http.Transport{
    DialContext:         ndialer.DialContext,
    TLSHandshakeTimeout: 10 * time.Second,
  },
  Timeout: timeout,
}

summary

Through the above implementation, you can customize the nameserver, or add a cache in the dailer to realize DNS caching.

The above is my personal experience. I hope I can give you a reference, and I hope you can support developpaer. If you have any mistakes or don’t consider completely, please don’t hesitate to comment.