-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit ca2fdc4
Showing
7 changed files
with
368 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
|
||
dist/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |