├── .github └── workflows │ └── release.yml ├── Readme.md ├── dnsfwd.go ├── go.mod └── go.sum /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up Go 14 | uses: actions/setup-go@v2 15 | with: 16 | go-version: 1.15 17 | 18 | - name: Set version tag 19 | run: | 20 | echo "RELEASE_VERSION=$(echo ${GITHUB_REF:10})" >> $GITHUB_ENV 21 | 22 | - name: Build 23 | run: | 24 | GOOS=windows go build -ldflags "-s -w -X main.version=${RELEASE_VERSION}" -o "${{ github.event.repository.name }}_win.exe" 25 | GOOS=darwin go build -ldflags "-s -w -X main.version=${RELEASE_VERSION}" -o "${{ github.event.repository.name }}_mac" 26 | GOOS=linux go build -ldflags "-s -w -X main.version=${RELEASE_VERSION}" -o "${{ github.event.repository.name }}_linux" 27 | 28 | - name: Create Release 29 | id: create_release 30 | uses: actions/create-release@v1.0.0 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | with: 34 | tag_name: ${{ github.ref }} 35 | release_name: Release ${{ github.ref }} 36 | draft: false 37 | prerelease: false 38 | 39 | - name: Upload win Asset 40 | id: upload-release-asset-win 41 | uses: actions/upload-release-asset@v1.0.1 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | with: 45 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 46 | asset_path: ./${{ github.event.repository.name }}_win.exe 47 | asset_name: ${{ github.event.repository.name }}_win.exe 48 | asset_content_type: application/vnd.microsoft.portable-executable 49 | 50 | - name: Upload mac Asset 51 | id: upload-release-asset-mac 52 | uses: actions/upload-release-asset@v1.0.1 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | with: 56 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 57 | asset_path: ./${{ github.event.repository.name }}_mac 58 | asset_name: ${{ github.event.repository.name }}_mac 59 | asset_content_type: application/octet-stream 60 | - name: Upload nix Asset 61 | id: upload-release-asset-nix 62 | uses: actions/upload-release-asset@v1.0.1 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | with: 66 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 67 | asset_path: ./${{ github.event.repository.name }}_linux 68 | asset_name: ${{ github.event.repository.name }}_linux 69 | asset_content_type: application/x-elf 70 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # DNSFWD 2 | 3 | Redirect DNS traffic to an upstream. 4 | 5 | Get Latest: 6 | 7 | - `wget https://github.com/C-Sto/dnsfwd/releases/latest/download/dnsfwd_linux` (replace linux with darwin or windows.exe for other OS versions) 8 | 9 | Example Terraform compatible provisioner section (why is resolved so painful, pls give me a better solution): 10 | 11 | ``` 12 | provisioner "remote-exec" { 13 | inline = [ 14 | "sudo systemctl disable systemd-resolved", 15 | "sudo systemctl stop systemd-resolved", 16 | "sed -i 's/127.0.0.53/1.1.1.1/g' /etc/resolv.conf", 17 | "wget https://github.com/C-Sto/dnsfwd/releases/latest/download/dnsfwd_linux", 18 | "chmod +x dnsfwd_linux", 19 | "tmux new -d './dnsfwd_linux -v -o -u ${var.upstream} -d ${var.zone}'" 20 | ] 21 | } 22 | ``` 23 | 24 | Example: 25 | 26 | This will forward all subdomains of example.com, and google.com to a host listening on 1053 at 192.168.0.53. It will not produce verbose output, and will not log to a file (see other options for that) 27 | 28 | ``` 29 | ./dnsfwd -d example.com,google.com -u 192.168.0.53:1053 30 | ``` 31 | 32 | ``` 33 | -d string 34 | highest level domain you'd like to filter on (can specify multiple, split on commas) 35 | -full 36 | log full dns queries and responses 37 | -l string 38 | Local address to listen on. Defaults to all interfaces on 53. (default "0.0.0.0:53") 39 | -o Log output to file (there will probably be a lot of junk here if verbose, and full queries are turned on) 40 | -of string 41 | Path of log file location (defaults to local dir) (default "dnsfwd.log") 42 | -t string 43 | Transport to use. Options are the Net value for a DNS Server (udp, udp4, udp6tcp, tcp4, tcp6, tcp-tls, tcp4-tls, tcp6-tls). Multiple can be supplied - comma separate (default "tcp,udp") 44 | -timeout int 45 | default timeout value for read/write/dial (default 2) 46 | -u string 47 | Upstream server to send requests to. Requires port!! (default "127.0.0.1:5353") 48 | -ut string 49 | Transport to use for upstream. Defaults to UDP. (default "udp") 50 | -v enable verbose 51 | -version 52 | show version and exit 53 | ``` 54 | -------------------------------------------------------------------------------- /dnsfwd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | "github.com/miekg/dns" 14 | ) 15 | 16 | var version string 17 | 18 | var domain string 19 | var upstream string 20 | 21 | var domainsplits []string 22 | var verbose bool 23 | var versionflag bool 24 | var localbind string 25 | var transport string 26 | var upstreamtransport string 27 | var outfile string 28 | var logfile bool 29 | var fullquery bool 30 | var timeout int 31 | 32 | func main() { 33 | 34 | flag.StringVar(&domain, "d", "", "highest level domain you'd like to filter on (can specify multiple, split on commas)") 35 | flag.StringVar(&upstream, "u", "127.0.0.1:5353", "Upstream server to send requests to. Requires port!!") 36 | flag.StringVar(&localbind, "l", "0.0.0.0:53", "Local address to listen on. Defaults to all interfaces on 53.") 37 | flag.StringVar(&transport, "t", "tcp,udp", "Transport to use. Options are the Net value for a DNS Server (udp, udp4, udp6tcp, tcp4, tcp6, tcp-tls, tcp4-tls, tcp6-tls). Multiple can be supplied - comma separate") 38 | flag.StringVar(&upstreamtransport, "ut", "udp", "Transport to use for upstream. Defaults to UDP.") 39 | flag.StringVar(&outfile, "of", "dnsfwd.log", "Path of log file location (defaults to local dir)") 40 | flag.IntVar(&timeout, "timeout", 2, "default timeout value for read/write/dial") 41 | flag.BoolVar(&logfile, "o", false, "Log output to file (there will probably be a lot of junk here if verbose, and full queries are turned on)") 42 | flag.BoolVar(&verbose, "v", false, "enable verbose") 43 | flag.BoolVar(&fullquery, "full", false, "log full dns queries and responses") 44 | flag.BoolVar(&versionflag, "version", false, "show version and exit") 45 | flag.Parse() 46 | 47 | if versionflag { 48 | if version == "" { 49 | fmt.Println("dnsfwd UNTAGGED LOCAL BUILD") 50 | return 51 | } 52 | fmt.Println("dnsfwd " + version) 53 | return 54 | } 55 | if logfile { 56 | f, err := os.OpenFile(outfile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 57 | if err != nil { 58 | log.Printf("error opening file: %v", err) 59 | } else { 60 | defer f.Close() 61 | w := io.MultiWriter(os.Stdout, f) 62 | log.SetOutput(w) 63 | } 64 | } 65 | 66 | //split up the monitored domains if provided on the cli 67 | domainsplits = strings.Split(domain, ",") 68 | 69 | transportSplits := strings.Split(transport, ",") 70 | 71 | x := sync.WaitGroup{} 72 | for _, transp := range transportSplits { 73 | 74 | x.Add(1) 75 | go startServer(transp) 76 | time.Sleep(time.Millisecond * 200) 77 | } 78 | 79 | x.Wait() 80 | } 81 | 82 | func startServer(transport string) { 83 | //listen via udp on localhost 84 | s := dns.Server{Addr: localbind, Net: transport} 85 | //handling all 86 | dns.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) { checkQuery(w, r, transport) }) 87 | for { 88 | if verbose { 89 | log.Printf("[%s] Listening for domains: %v", transport, domainsplits) 90 | log.Printf("[%s] Sending to %s", transport, upstream) 91 | } 92 | e := s.ListenAndServe() 93 | log.Printf("[%s] error: %s", transport, e) 94 | log.Printf("[%s] Sleeping for 5 seconds before retrying...", transport) 95 | time.Sleep(time.Second * 5) 96 | } 97 | } 98 | 99 | func checkQuery(w dns.ResponseWriter, r *dns.Msg, transport string) { 100 | for _, x := range r.Question { 101 | if len(domainsplits) > 0 { 102 | onematch := false 103 | for _, y := range domainsplits { 104 | if strings.HasSuffix(strings.ToLower(x.Name), strings.ToLower(y+".")) { 105 | onematch = true 106 | break 107 | } 108 | } 109 | if !onematch { 110 | if verbose { 111 | log.Printf("[%s] Rejected query for %s from %s", transport, x.Name, w.RemoteAddr().String()) 112 | } 113 | return 114 | } 115 | } 116 | if verbose { 117 | log.Printf("[%s] Query for %s from %s", transport, x.Name, w.RemoteAddr().String()) 118 | } 119 | } 120 | m := new(dns.Msg) 121 | m.Question = r.Question 122 | m.Compress = false 123 | m.Authoritative = true 124 | c := dns.Client{} 125 | if timeout != 2 { 126 | c.Timeout = time.Second * time.Duration(timeout) 127 | } 128 | c.Net = upstreamtransport 129 | c.UDPSize = 0xffff 130 | r2, _, err := c.Exchange(r, upstream) 131 | if err != nil { 132 | if verbose { 133 | log.Printf("[%s] Error communicating to upstream: %s", transport, err) 134 | } 135 | return 136 | } 137 | if fullquery { 138 | log.Printf("[%s] Response:\n%s", transport, r2) 139 | } 140 | w.WriteMsg(r2) 141 | } 142 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/C-Sto/dnsfwd 2 | 3 | go 1.15 4 | 5 | require github.com/miekg/dns v1.1.41 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= 2 | github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= 3 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= 4 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 5 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 6 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 7 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 8 | golang.org/x/sys v0.0.0-20210303074136-134d130e1a04 h1:cEhElsAv9LUt9ZUUocxzWe05oFLVd+AA2nstydTeI8g= 9 | golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 10 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 11 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 12 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 13 | --------------------------------------------------------------------------------