├── dnsRecords.yml ├── docker-compose.yml ├── env └── env.go ├── Dockerfile ├── go.mod ├── README.md ├── go.sum └── main.go /dnsRecords.yml: -------------------------------------------------------------------------------- 1 | --- 2 | myserver.com.: 192.0.2.3 3 | testservers.com.: 192.0.2.4 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | dns-server: 3 | build: . 4 | ports: 5 | - "9090:9090/udp" # Add UDP protocol explicitly 6 | - "9090:9090" # Also map TCP for safety 7 | - "3001:3000" # Web interface 8 | restart: always -------------------------------------------------------------------------------- /env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | func GetStringEnv(key, fallback string) string { 10 | 11 | val, exists := os.LookupEnv(key) 12 | if !exists { 13 | return fallback 14 | } 15 | return val 16 | } 17 | 18 | func GetIntEnv(key string, fallback int) int { 19 | 20 | val, exists := os.LookupEnv(key) 21 | if !exists { 22 | return fallback 23 | } 24 | 25 | valInt, err := strconv.Atoi(val) 26 | if err != nil { 27 | log.Fatalf("Error parsing ENV key %s : %s", key, err.Error()) 28 | 29 | } 30 | return valInt 31 | } 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Golang image as the base image 2 | FROM golang:1.22-alpine 3 | 4 | # Set the Current Working Directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy go.mod and go.sum files 8 | COPY go.mod go.sum ./ 9 | 10 | # Download all dependencies. Dependencies will be cached if the go.mod and go.sum files are not changed 11 | RUN go mod download 12 | 13 | # Copy the source code into the container 14 | COPY . . 15 | 16 | # Build the Go app 17 | RUN go build -o main . 18 | 19 | # Expose port 9090 for the DNS server 20 | EXPOSE 9090 21 | 22 | # Command to run the executable 23 | CMD ["./main"] -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Denyme24/go-dns-server 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/gofiber/fiber/v2 v2.52.6 7 | github.com/miekg/dns v1.1.64 8 | ) 9 | 10 | require ( 11 | github.com/andybalholm/brotli v1.1.0 // indirect 12 | github.com/google/uuid v1.6.0 // indirect 13 | github.com/klauspost/compress v1.17.9 // indirect 14 | github.com/mattn/go-colorable v0.1.13 // indirect 15 | github.com/mattn/go-isatty v0.0.20 // indirect 16 | github.com/mattn/go-runewidth v0.0.16 // indirect 17 | github.com/rivo/uniseg v0.2.0 // indirect 18 | github.com/valyala/bytebufferpool v1.0.0 // indirect 19 | github.com/valyala/fasthttp v1.51.0 // indirect 20 | github.com/valyala/tcplisten v1.0.0 // indirect 21 | golang.org/x/mod v0.23.0 // indirect 22 | golang.org/x/net v0.35.0 // indirect 23 | golang.org/x/sync v0.11.0 // indirect 24 | golang.org/x/sys v0.30.0 // indirect 25 | golang.org/x/tools v0.30.0 // indirect 26 | gopkg.in/yaml.v3 v3.0.1 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go DNS Server 2 | 3 | A simple DNS server written in Go with a web management interface using Fiber. Perfect for learning DNS fundamentals and local network management. 4 | 5 | ## 📖 Detailed Blog Post Explaining DNS & This Project 6 | For a deep dive into **DNS fundamentals** and the **development journey** behind this server, read my blog post: 7 | [DNS Explained: From Basics to Building My Own DNS Server](https://dev.to/denyme24/dns-explained-from-basics-to-building-my-own-dns-server-25o6) 8 | *(Includes DNS resolution flow, record types, and implementation challenges!)* 9 | 10 | ## Features 11 | 12 | - 🚀 Basic DNS (A record) resolution 13 | - 🌐 Web-based management API (add/remove records) 14 | - 🔧 In-memory DNS record storage 15 | - 🔄 UDP-based DNS server 16 | - 🔒 Custom port support 17 | 18 | ## Prerequisites 19 | 20 | - [Go 1.16+](https://golang.org/dl/) 21 | - Admin/root access (for port binding) 22 | 23 | ## Installation 24 | 25 | 1. Clone the repository: 26 | ```bash 27 | git clone https://github.com/Denyme24/my-dns-server.git 28 | cd go-dns-server 29 | ``` 30 | 2. Install dependencies: 31 | ```bash 32 | go get github.com/gofiber/fiber/v2 33 | go get github.com/miekg/dns 34 | ``` 35 | ## Configuration 36 | 1. Edit initial DNS records in main.go: 37 | ``` 38 | var dnsRecords = map[string]string{ 39 | "example.com.": "192.0.2.1", 40 | "test.com.": "203.0.113.42", 41 | } 42 | ``` 43 | 2.(Optional) Change ports: 44 | ``` 45 | // DNS Server port (default: 9090) 46 | server.Addr = ":9090" 47 | 48 | // Web Server port (default: 3000) 49 | app.Listen(":3000") 50 | ``` 51 | ## Usage 52 | 1. Start the server : 53 | ``` 54 | go run main.go 55 | ``` 56 | 2. Add DNS records via API: 57 | ``` 58 | curl -X POST -H "Content-Type: application/json" \ 59 | -d '{"domain":"mysite.com.","ip":"your_ip"}' \ 60 | http://localhost:3000/records 61 | ``` 62 | 3. Query DNS records: 63 | ``` 64 | dig @127.0.0.1 -p 9090 example.com 65 | ``` 66 | Replace 127.0.0.1 with your actual windows host IP 67 | ``` 68 | ipconfig | findstr /i "IPv4 Address" 69 | ``` 70 | # or for Windows: 71 | ``` 72 | nslookup -port=9090 example.com 127.0.0.1 73 | ``` 74 | 4.View all records: 75 | ``` 76 | curl http://localhost:3000/records 77 | ``` 78 | 79 | ## Troubleshooting 80 | ### Connection refused? 81 | - Check firewall rules (allow UDP/TCP on 9090): 82 | ``` 83 | New-NetFirewallRule -DisplayName "DNS-Server" -Direction Inbound -Protocol UDP -LocalPort 9090 -Action Allow 84 | ``` 85 | - Run as admin/root (required for ports < 1024) 86 | - Verify server is running: 87 | ``` 88 | netstat -anu | grep 9090 # Linux 89 | Get-NetUDPEndpoint -LocalPort 9090 # PowerShell 90 | ``` 91 | ### Web interface not working? 92 | - Ensure Fiber server is running on port 3000 93 | - Check for port conflicts: 94 | ``` 95 | # Check specific port (e.g., 9090): 96 | Get-NetTCPConnection -LocalPort 9090 97 | ``` 98 | 99 | ## Things to keep in mind: 100 | - In-memory storage (records lost on restart) 101 | - Only handles A records 102 | 103 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= 2 | github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= 3 | github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= 4 | github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= 5 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 6 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 7 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 8 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 9 | github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= 10 | github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 11 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 12 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 13 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 14 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 15 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 16 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 17 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 18 | github.com/miekg/dns v1.1.64 h1:wuZgD9wwCE6XMT05UU/mlSko71eRSXEAm2EbjQXLKnQ= 19 | github.com/miekg/dns v1.1.64/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= 20 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 21 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 22 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 23 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 24 | github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= 25 | github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= 26 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 27 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 28 | golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= 29 | golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 30 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 31 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 32 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 33 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 34 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 35 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 37 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 38 | golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= 39 | golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 41 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 42 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 43 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "maps" 7 | "os" 8 | 9 | "github.com/Denyme24/go-dns-server/env" 10 | "github.com/gofiber/fiber/v2" 11 | "github.com/miekg/dns" 12 | "gopkg.in/yaml.v3" 13 | ) 14 | 15 | // Our DNS records database (in-memory for this example) 16 | var dnsRecords = map[string]string{ 17 | "example.com.": "192.0.2.1", 18 | "test.com.": "203.0.113.42", 19 | "namanraj.tech": "192.0.2.123", 20 | } 21 | 22 | func main() { 23 | // Start DNS server in a goroutine 24 | go startDNSServer() 25 | 26 | // Start Fiber web server 27 | startWebServer() 28 | } 29 | 30 | func startDNSServer() { 31 | // Set up DNS server 32 | server := &dns.Server{ 33 | Addr: env.GetStringEnv("ADDRESS", "0.0.0.0:9090"), // Bind to all interfaces 34 | Net: env.GetStringEnv("PROTOCOL", "udp"), 35 | } 36 | 37 | // pulls additional dns records from yaml file 38 | recordFileName := env.GetStringEnv("DNS_RECORD_FILE", "dnsRecords.yml") 39 | savedRecords, err := recordsFromFile(recordFileName) 40 | if err!= nil { 41 | log.Println("Error pulling additional records from yaml file: ", err.Error()) 42 | } 43 | 44 | // merge dns records - if they exist - into the existing map 45 | // if a key is already in dnsRecords, the corresponding value will be updated 46 | // with the one in the file... 47 | 48 | maps.Copy(dnsRecords, savedRecords) 49 | 50 | // Handle DNS requests 51 | dns.HandleFunc(".", handleDNSRequest) 52 | 53 | // Start server 54 | log.Printf("Starting DNS server on %s...", server.Addr) 55 | err = server.ListenAndServe() 56 | if err != nil { 57 | log.Fatalf("Failed to start DNS server: %v", err) 58 | } else { 59 | log.Printf("DNS server is running") 60 | } 61 | } 62 | 63 | func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) { 64 | m := new(dns.Msg) 65 | m.SetReply(r) 66 | m.Authoritative = true // Mark this server as authoritative 67 | found := false 68 | 69 | for _, q := range r.Question { 70 | log.Printf("Query: %s (%s)", q.Name, dns.TypeToString[q.Qtype]) 71 | if q.Qtype == dns.TypeA { 72 | if ip, exists := dnsRecords[q.Name]; exists { 73 | rr, _ := dns.NewRR(fmt.Sprintf("%s A %s", q.Name, ip)) 74 | m.Answer = append(m.Answer, rr) 75 | found = true 76 | } 77 | } 78 | } 79 | 80 | if !found { 81 | m.Rcode = dns.RcodeNameError // NXDOMAIN 82 | } 83 | 84 | w.WriteMsg(m) 85 | } 86 | 87 | func startWebServer() { 88 | app := fiber.New() 89 | 90 | // Web interface to manage DNS records 91 | app.Get("/records", func(c *fiber.Ctx) error { 92 | return c.JSON(dnsRecords) 93 | }) 94 | 95 | app.Post("/records", func(c *fiber.Ctx) error { 96 | type Record struct { 97 | Domain string `json:"domain"` 98 | IP string `json:"ip"` 99 | } 100 | 101 | var record Record 102 | if err := c.BodyParser(&record); err != nil { 103 | return c.Status(400).SendString("Bad request") 104 | } 105 | 106 | // Ensure domain ends with dot (DNS standard) 107 | if record.Domain[len(record.Domain)-1] != '.' { 108 | record.Domain += "." 109 | } 110 | 111 | dnsRecords[record.Domain] = record.IP 112 | return c.SendString("Record added successfully") 113 | }) 114 | 115 | log.Printf("Starting web server on :3000...") 116 | log.Fatal(app.Listen(":3000")) 117 | } 118 | 119 | func recordsFromFile(recordFileName string) (map[string]string, error) { 120 | 121 | // pulls additional dns records from yaml file 122 | 123 | content, err := os.ReadFile(recordFileName) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | var records map[string]string 129 | if err = yaml.Unmarshal([]byte(content), &records); err != nil{ 130 | return nil, err 131 | } 132 | return records, nil 133 | } 134 | --------------------------------------------------------------------------------