Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Matchlighter committed Feb 14, 2022
0 parents commit ca2fdc4
Show file tree
Hide file tree
Showing 7 changed files with 368 additions and 0 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
on:
push:
tags:
- v*.*.*

env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GO111MODULE: "on"

name: release
jobs:
release:
name: release
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: 1.17.x
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
args: release --rm-dist
version: latest
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

dist/
32 changes: 32 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
# you may remove this if you don't need go generate
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
main: cmd/sparse_dns/main.go
archives:
- replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: i386
amd64: x86_64
format: binary
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
sparse-dns
===

A simple DNS forwarder that forwards DNS queries to various upstreams. If an upstream returns NXDomain, the next upstream is tried.

Usage
---

```
Usage of ./sparse_dns:
-debug
Debug mode
-listen string
Address to listen to (TCP and UDP) (default ":53")
-maxclients uint
Maximum number of simultaneous clients (default 1000)
-maxrtt float
Maximum mean RTT for upstream queries before marking a server as dead (default 0.25)
-upstream string
Comma-delimited list of upstream servers (default "8.8.8.8:53,8.8.4.4:53")
```
239 changes: 239 additions & 0 deletions cmd/sparse_dns/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
package main

import (
"errors"
"flag"
"fmt"
"log"
"net"
"strings"
"time"

"github.com/miekg/dns"
)

const (
// MaxUDPBufferSize UDP buffer size
MaxUDPBufferSize = 16 * 1024 * 1024
)

// QueuedResponse Response to an asynchronous query
type QueuedResponse struct {
resolved *dns.Msg
rtt time.Duration
err error
}

// QueuedRequest Asynchronous DNS request
type QueuedRequest struct {
ts time.Time
req *dns.Msg
responseChan chan QueuedResponse
}

var (
address = flag.String("listen", ":53", "Address to listen to (TCP and UDP)")
upstreamServersStr = flag.String("upstream", "8.8.8.8:53,8.8.4.4:53", "Comma-delimited list of upstream servers")
upstreamServers []string
maxClients = flag.Uint("maxclients", 1000, "Maximum number of simultaneous clients")
maxRTT = flag.Float64("maxrtt", 0.25, "Maximum mean RTT for upstream queries before marking a server as dead")
debug = flag.Bool("debug", false, "Debug mode")
resolverRing chan QueuedRequest
globalTimeout = 1 * time.Second
udpClient dns.Client
tcpClient dns.Client
)

func main() {
flag.Parse()

upstreamServers = strings.Split(*upstreamServersStr, ",") // parseUpstreamServers(*upstreamServersStr)
resolverRing = make(chan QueuedRequest, *maxClients)
udpClient = dns.Client{Net: "udp", DialTimeout: globalTimeout, ReadTimeout: globalTimeout, WriteTimeout: globalTimeout, SingleInflight: true}
tcpClient = dns.Client{Net: "tcp", DialTimeout: globalTimeout, ReadTimeout: globalTimeout, WriteTimeout: globalTimeout, SingleInflight: true}

for i := uint(0); i < *maxClients; i++ {
go func() {
resolverThread()
}()
}

dns.HandleFunc(".", route)
defer dns.HandleRemove(".")

// UDP Server
udpServer := &dns.Server{Addr: *address, Net: "udp"}
defer udpServer.Shutdown()
udpAddr, err := net.ResolveUDPAddr(udpServer.Net, udpServer.Addr)
if err != nil {
log.Fatal(err)
}
udpPacketConn, err := net.ListenUDP(udpServer.Net, udpAddr)
if err != nil {
log.Fatal(err)
}
udpServer.PacketConn = udpPacketConn
udpPacketConn.SetReadBuffer(MaxUDPBufferSize)
udpPacketConn.SetWriteBuffer(MaxUDPBufferSize)

// TCP Server
tcpServer := &dns.Server{Addr: *address, Net: "tcp"}
defer tcpServer.Shutdown()
tcpAddr, err := net.ResolveTCPAddr(tcpServer.Net, tcpServer.Addr)
if err != nil {
log.Fatal(err)
}
tcpListener, err := net.ListenTCP(tcpServer.Net, tcpAddr)
if err != nil {
log.Fatal(err)
}
tcpServer.Listener = tcpListener

// Start Servers
go func() {
log.Fatal(udpServer.ActivateAndServe())
}()
go func() {
log.Fatal(tcpServer.ActivateAndServe())
}()
fmt.Println("Ready")

select {}
}

func getMaxPayloadSize(req *dns.Msg) uint16 {
opt := req.IsEdns0()
if opt == nil {
return dns.MinMsgSize
}
maxPayloadSize := opt.UDPSize()
if maxPayloadSize < dns.MinMsgSize {
maxPayloadSize = dns.MinMsgSize
}
return maxPayloadSize
}

func pickUpstream(req *dns.Msg) (*string, error) {
res := upstreamServers[0]
return &res, nil
}

func syncResolve(req *dns.Msg) (*dns.Msg, time.Duration, error) {
var resolved *dns.Msg
var rtt time.Duration
var err error

for _, addr := range upstreamServers {
if *debug {
log.Printf("Querying %v for %v\n", addr, req.Question[0].Name)
}

resolved, rtt, err = udpClient.Exchange(req, addr)
if err != nil || (resolved != nil && resolved.Truncated) {
resolved, rtt, err = tcpClient.Exchange(req, addr)
}
if (dns.RcodeToString[resolved.Rcode] != "NOERROR") {
continue
}
if err == nil {
break
}
}

if err != nil {
return nil, 0, err
}

return resolved, rtt, nil
}

func resolverThread() {
for {
queuedRequest := <-resolverRing
if time.Since(queuedRequest.ts).Seconds() > *maxRTT {
response := QueuedResponse{resolved: nil, rtt: 0, err: errors.New("Request too old")}
queuedRequest.responseChan <- response
close(queuedRequest.responseChan)
continue
}
resolved, rtt, err := syncResolve(queuedRequest.req)
response := QueuedResponse{resolved: resolved, rtt: rtt, err: err}
queuedRequest.responseChan <- response
close(queuedRequest.responseChan)
}
}

func resolveViaResolverThreads(req *dns.Msg) (*dns.Msg, time.Duration, error) {
responseChan := make(chan QueuedResponse)
queuedRequest := QueuedRequest{ts: time.Now(), req: req, responseChan: responseChan}
for queued := false; queued == false; {
select {
case resolverRing <- queuedRequest:
queued = true
default:
old := <-resolverRing
evictedResponse := QueuedResponse{resolved: nil, rtt: 0, err: errors.New("Evicted")}
old.responseChan <- evictedResponse
}
}
response := <-responseChan
if response.err != nil {
return nil, response.rtt, response.err
}
return response.resolved, response.rtt, nil
}

func resolve(req *dns.Msg) (*dns.Msg, error) {
extra2 := []dns.RR{}
for _, extra := range req.Extra {
if extra.Header().Rrtype != dns.TypeOPT {
extra2 = append(extra2, extra)
}
}

dnssec := false
for _, extra := range req.Extra {
if extra.Header().Rrtype == dns.TypeOPT {
dnssec = extra.(*dns.OPT).Do()
}
}

req.Extra = extra2
req.SetEdns0(dns.DefaultMsgSize, dnssec)
resolved, _, err := resolveViaResolverThreads(req)
if err != nil {
return nil, err
}
resolved.Compress = true
return resolved, nil
}

func sendTruncated(w dns.ResponseWriter, msgHdr dns.MsgHdr) {
emptyResp := new(dns.Msg)
emptyResp.MsgHdr = msgHdr
emptyResp.Response = true
if _, isTCP := w.RemoteAddr().(*net.TCPAddr); isTCP {
dns.HandleFailed(w, emptyResp)
return
}
emptyResp.Truncated = true
w.WriteMsg(emptyResp)
}

func route(w dns.ResponseWriter, req *dns.Msg) {
maxPayloadSize := getMaxPayloadSize(req)

resp, err := resolve(req)
if err != nil {
w.Close()
return
}

packed, _ := resp.Pack()
packedLen := len(packed)
if uint16(packedLen) > maxPayloadSize {
sendTruncated(w, resp.MsgHdr)
} else {
w.WriteMsg(resp)
}
}
13 changes: 13 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module github.com/matchlighter/sparse-dns

go 1.17

require github.com/miekg/dns v1.1.46

require (
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 // indirect
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
)
35 changes: 35 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
github.com/miekg/dns v1.1.46 h1:uzwpxRtSVxtcIZmz/4Uz6/Rn7G11DvsaslXoy5LxQio=
github.com/miekg/dns v1.1.46/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

0 comments on commit ca2fdc4

Please sign in to comment.