diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..fbddc8b --- /dev/null +++ b/.github/workflows/release.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cde0123 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +dist/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..8a31bde --- /dev/null +++ b/.goreleaser.yaml @@ -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:' diff --git a/README.md b/README.md new file mode 100644 index 0000000..12ec5b2 --- /dev/null +++ b/README.md @@ -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") +``` diff --git a/cmd/sparse_dns/main.go b/cmd/sparse_dns/main.go new file mode 100644 index 0000000..0f602db --- /dev/null +++ b/cmd/sparse_dns/main.go @@ -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) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4750532 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..647bb9a --- /dev/null +++ b/go.sum @@ -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=