├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── client ├── cache │ └── cache.go ├── client.go ├── customResolver.go ├── ecs │ └── ecs.go ├── handler.go ├── newClient.go └── resolver │ ├── hosts.go │ ├── https.go │ ├── https_google.go │ ├── resolve.go │ ├── tls.go │ ├── traditional.go │ └── types.go ├── config ├── config.go └── selector.go ├── go.mod ├── go.sum ├── logger.go ├── main.go ├── secure-dns.service ├── selector ├── Clock.go ├── Random.go ├── SWRR.go ├── Selector.go └── WRandom.go └── versions └── versions.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build release 2 | 3 | on: 4 | push: 5 | pull_request: 6 | release: 7 | types: 8 | - published 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v3 19 | with: 20 | go-version: stable 21 | 22 | - name: Display Go version 23 | run: go version 24 | 25 | - name: Build 26 | run: make all 27 | 28 | - name: Upload Artifact 29 | uses: actions/upload-artifact@v3 30 | with: 31 | name: secure-dns-releases 32 | path: build/secure-dns-* 33 | if-no-files-found: error 34 | 35 | release: 36 | needs: build 37 | if: github.ref_type == 'tag' && github.event_name == 'release' 38 | runs-on: ubuntu-latest 39 | permissions: 40 | contents: write 41 | steps: 42 | - name: Download Artifact 43 | uses: actions/download-artifact@v3 44 | with: 45 | name: secure-dns-releases 46 | 47 | - name: Upload release 48 | shell: bash 49 | run: | 50 | curl -fSsL \ 51 | -o release.json \ 52 | -H "Accept: application/vnd.github+json" \ 53 | -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ 54 | -H "X-GitHub-Api-Version: 2022-11-28" \ 55 | https://api.github.com/repos/${{ github.repository }}/releases/tags/${{ github.ref_name }} 56 | jq '.assets[].id' release.json | xargs -I % curl -fSsL \ 57 | -X DELETE \ 58 | -H "Accept: application/vnd.github+json" \ 59 | -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"\ 60 | -H "X-GitHub-Api-Version: 2022-11-28" \ 61 | https://api.github.com/repos/${{ github.repository }}/releases/assets/% 62 | ls secure-dns-* | xargs -I % curl -fSsL \ 63 | -X POST \ 64 | -H "Accept: application/vnd.github+json" \ 65 | -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"\ 66 | -H "X-GitHub-Api-Version: 2022-11-28" \ 67 | -H "Content-Type: application/octet-stream" \ 68 | https://uploads.github.com/repos/${{ github.repository }}/releases/`jq '.id' release.json`/assets?name=% \ 69 | --data-binary @% 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.toml 2 | 3 | build/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Liming Jin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export CGO_ENABLED=0 2 | 3 | PACKAGE=github.com/jinliming2/secure-dns 4 | VERSION=`git describe --tags --abbrev=0` 5 | HASH=`git rev-parse --short HEAD` 6 | DATE=`date +%Y%m%d%H%M%S` 7 | LDFLAGS="-X '${PACKAGE}/versions.VERSION=${VERSION} (${DATE})' -X '${PACKAGE}/versions.BUILDHASH=${HASH}' -s -w" 8 | 9 | .PHONY: all build clean 10 | 11 | all: clean build 12 | 13 | build: linux_amd64 linux_arm64 darwin_amd64 darwin_arm64 windows_386 windows_amd64 14 | 15 | linux_amd64: 16 | GOOS=linux \ 17 | GOARCH=amd64 \ 18 | go build -v -ldflags ${LDFLAGS} -o build/secure-dns-linux-amd64 19 | 20 | linux_arm64: 21 | GOOS=linux \ 22 | GOARCH=arm64 \ 23 | go build -v -ldflags ${LDFLAGS} -o build/secure-dns-linux-arm64 24 | 25 | darwin_amd64: 26 | GOOS=darwin \ 27 | GOARCH=amd64 \ 28 | go build -v -ldflags ${LDFLAGS} -o build/secure-dns-darwin-amd64 29 | 30 | darwin_arm64: 31 | GOOS=darwin \ 32 | GOARCH=arm64 \ 33 | go build -v -ldflags ${LDFLAGS} -o build/secure-dns-darwin-arm64 34 | 35 | windows_386: 36 | GOOS=windows \ 37 | GOARCH=386 \ 38 | go build -v -ldflags ${LDFLAGS} -o build/secure-dns-windows-386.exe 39 | 40 | windows_amd64: 41 | GOOS=windows \ 42 | GOARCH=amd64 \ 43 | go build -v -ldflags ${LDFLAGS} -o build/secure-dns-windows-amd64.exe 44 | 45 | clean: 46 | rm build/secure-dns-* || true 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Secure DNS 2 | 3 | [![Build Status](https://app.travis-ci.com/jinliming2/secure-dns.svg?branch=master)](https://app.travis-ci.com/jinliming2/secure-dns) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/jinliming2/secure-dns)](https://goreportcard.com/report/github.com/jinliming2/secure-dns) 5 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fjinliming2%2Fsecure-dns.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fjinliming2%2Fsecure-dns?ref=badge_shield) 6 | 7 | A DNS client which implemented DoT and DoH, with load balancing, DNS cache, custom ECS and HOSTs. 8 | 9 | ## Table of Content 10 | 11 | - [Table of Content](#table-of-content) 12 | - [Config](#config) 13 | - [Example](#example) 14 | - [Basic config](#basic-config) 15 | - [Upstream DNS](#upstream-dns) 16 | - [Traditional DNS](#traditional-dns) 17 | - [DNS over TLS (DoT)](#dns-over-tls-dot) 18 | - [DNS over HTTPS (DoH)](#dns-over-https-doh) 19 | - [Custom Hosts](#custom-hosts) 20 | - [Import domain list from file](#import-domain-list-from-file) 21 | 22 | ## Config 23 | 24 | ### Example 25 | 26 | ```toml 27 | [config] 28 | listen = [ 29 | '[::1]:53', 30 | '127.0.0.1:53', 31 | ] 32 | custom_ecs = [ 33 | '10.20.30.40', 34 | '50.60.70.80', 35 | ] 36 | round_robin = 'swrr' 37 | 38 | [[traditional]] 39 | host = ['8.8.4.4', '8.8.8.8'] 40 | bootstrap = true 41 | 42 | [[https]] 43 | host = ['dns.google'] 44 | path = '/resolve' 45 | google = true 46 | 47 | [[https]] 48 | host = ['dns.google'] 49 | weight = 10 50 | 51 | [[https]] 52 | host = ['1.1.1.1', '1.0.0.1'] 53 | 54 | [[tls]] 55 | host = ['dns.google'] 56 | 57 | [[traditional]] 58 | # Resolve private domain name using local DNS server 59 | host = ['10.0.0.1'] 60 | suffix = [ 'private.network.org' ] 61 | 62 | [hosts.'example.com'] 63 | A = [ '127.0.0.1' ] 64 | AAAA = [ '::1' ] 65 | TXT = [ 'text' ] 66 | ``` 67 | 68 | ### Basic config 69 | 70 | | Key | Type | Required | Default | Description | 71 | | :----------------- | :--------: | :------: | :-------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------- | 72 | | listen | `string[]` | ✔️ | | host and port to listen | 73 | | timeout | `uint` | | `5` | timeout in seconds for each DNS request, 0 to disable | 74 | | round_robin | `string` | | `'clock'` | upstream select round robin, can only be `'clock'`, `'random'`, `'wrandom'` or `'swrr'` | 75 | | cache_no_answer | `uint` | | `0` | Cache response for specified seconds even if query returns with no specified answer or domain was not exists | 76 | | no_cache | `boolean` | | `false` | disable global DNS result cache | 77 | | custom_ecs | `string[]` | | | custom EDNS Subnet to override | 78 | | fallback_no_ecs | `boolean` | | `false` | fallback to no_ecs=`true` when DNS request got no answer | 79 | | no_ecs | `boolean` | | `false` | disable EDNS Subnet and remove EDNS Subnet from DNS request | 80 | | user_agent | `string` | | `'secure-dns/VERSION https://github.com/jinliming2/secure-dns'` | User-Agent field for DNS over HTTPS | 81 | | no_user_agent | `boolean` | | `false` | do not send User-Agent header in DNS over HTTPS | 82 | | no_single_inflight | `boolean` | | `false` | do not suppress multiple same outstanding queries | 83 | 84 | Example: 85 | 86 | ```toml 87 | [config] 88 | listen = ['[::1]:53', '127.0.0.1:53'] 89 | custom_ecs = ['1.2.3.4', '1:2::3:4'] 90 | no_user_agent = true 91 | ``` 92 | 93 | ### Upstream DNS 94 | 95 | #### Traditional DNS 96 | 97 | | Key | Type | Required | Default | Description | 98 | | :----------------- | :--------: | :------: | :-----: | :-------------------------------------------------------------------------------------------------- | 99 | | host | `string[]` | ✔️ | | ip addresses | 100 | | port | `uint16` | | `53` | port to use | 101 | | bootstrap | `boolean` | | `false` | mark this is a bootstrap DNS server, only used to resolve names for DNS over HTTPS and DNS over TLS | 102 | | weight | `uint` | | `1` | weight used for weighted round robin, should > 0 | 103 | | domain | `string[]` | | | mark this DNS server only used to resolve specified domain names | 104 | | suffix | `string[]` | | | mark this DNS server only used to resolve domain names with specified suffixes | 105 | | custom_ecs | `string[]` | | | custom EDNS Subnet to override | 106 | | fallback_no_ecs | `boolean` | | `false` | fallback to no_ecs=`true` when DNS request got no answer | 107 | | no_ecs | `boolean` | | `false` | disable EDNS Subnet and remove EDNS Subnet from DNS request | 108 | | no_single_inflight | `boolean` | | `false` | do not suppress multiple same outstanding queries | 109 | 110 | Example: 111 | 112 | ```toml 113 | [[traditional]] 114 | host = ['8.8.4.4', '8.8.8.8'] 115 | 116 | [[traditional]] 117 | host = ['1.1.1.1', '1.0.0.1'] 118 | bootstrap = true 119 | 120 | [[traditional]] 121 | host = ['9.9.9.9'] 122 | suffix = [ 123 | 'example.com', 124 | ] 125 | ``` 126 | 127 | #### DNS over TLS (DoT) 128 | 129 | | Key | Type | Required | Default | Description | 130 | | :----------------- | :--------: | :------: | :-----: | :----------------------------------------------------------------------------- | 131 | | host | `string[]` | ✔️ | | ip addresses or host names | 132 | | port | `uint16` | | `853` | port to use | 133 | | hostname | `string` | | | hostname for ip addresses | 134 | | weight | `uint` | | `1` | weight used for weighted round robin, should > 0 | 135 | | domain | `string[]` | | | mark this DNS server only used to resolve specified domain names | 136 | | suffix | `string[]` | | | mark this DNS server only used to resolve domain names with specified suffixes | 137 | | custom_ecs | `string[]` | | | custom EDNS Subnet to override | 138 | | fallback_no_ecs | `boolean` | | `false` | fallback to no_ecs=`true` when DNS request got no answer | 139 | | no_ecs | `boolean` | | `false` | disable EDNS Subnet and remove EDNS Subnet from DNS request | 140 | | no_single_inflight | `boolean` | | `false` | do not suppress multiple same outstanding queries | 141 | 142 | Example: 143 | 144 | ```toml 145 | [[tls]] 146 | host = ['dns.google'] 147 | 148 | [[tls]] 149 | host = ['1.1.1.1'] 150 | hostname = 'cloudflare-dns.com' 151 | domain = [ 152 | 'example.com', 153 | ] 154 | ``` 155 | 156 | > Note: If you want to specify hostname in host field, you must specify a traditional DNS server that marked with `bootstrap = true`. 157 | 158 | #### DNS over HTTPS (DoH) 159 | 160 | | Key | Type | Required | Default | Description | 161 | | :----------------- | :--------: | :------: | :-------------------------------------------------------------: | :----------------------------------------------------------------------------- | 162 | | host | `string[]` | ✔️ | | ip addresses or host names | 163 | | port | `uint16` | | `443` | port to use | 164 | | hostname | `string` | | | hostname for ip addresses | 165 | | path | `string` | | `'/dns-query'` | HTTP URI path to use | 166 | | google | `boolean` | | `false` | use google's DoH query structure | 167 | | cookie | `boolean` | | `false` | enable cookie support for this server | 168 | | weight | `uint` | | `1` | weight used for weighted round robin, should > 0 | 169 | | domain | `string[]` | | | mark this DNS server only used to resolve specified domain names | 170 | | suffix | `string[]` | | | mark this DNS server only used to resolve domain names with specified suffixes | 171 | | custom_ecs | `string[]` | | | custom EDNS Subnet to override | 172 | | fallback_no_ecs | `boolean` | | `false` | fallback to no_ecs=`true` when DNS request got no answer | 173 | | no_ecs | `boolean` | | `false` | disable EDNS Subnet and remove EDNS Subnet from DNS request | 174 | | user_agent | `string` | | `'secure-dns/VERSION https://github.com/jinliming2/secure-dns'` | User-Agent field for DNS over HTTPS | 175 | | no_user_agent | `boolean` | | `false` | do not send User-Agent header in DNS over HTTPS | 176 | | no_single_inflight | `boolean` | | `false` | do not suppress multiple same outstanding queries | 177 | 178 | Example: 179 | 180 | ```toml 181 | [[https]] 182 | host = ['dns.google'] 183 | 184 | [[https]] 185 | host = ['8.8.4.4', '8.8.8.8'] 186 | hostname = 'dns.google' 187 | path = '/resolve' 188 | google = true 189 | 190 | [[https]] 191 | host = ['1.1.1.1'] 192 | hostname = 'cloudflare-dns.com' 193 | domain = [ 194 | 'example.com', 195 | ] 196 | ``` 197 | 198 | > Note: If you want to specify hostname in host field, you must specify a traditional DNS server that marked with `bootstrap = true`. 199 | 200 | ### Custom Hosts 201 | 202 | Example: 203 | 204 | ```toml 205 | [hosts.'example.com'] 206 | A = [ 207 | '127.0.0.1', 208 | '192.168.1.1', 209 | ] 210 | AAAA = ['::1'] 211 | TXT = ['this matches example.com'] 212 | 213 | [hosts.'*.example.com'] 214 | A = ['0.0.0.0'] 215 | TXT = ['this matches example.com a.example.com a.b.example.com a.b.c.example.com ...'] 216 | 217 | [hosts.'*.blocked.domain'] 218 | # Answered with no record 219 | ``` 220 | 221 | #### Import domain list from file 222 | 223 | When defining hosts, you can use the `=#` or `$#` prefix followed by a relative or absolute file path to import the domain list from the specified file. The file path is related to the TOML config file path. 224 | 225 | The domain list file is a plain text file that contains domains in each line, split by the `\n` character. Lines starting with the `#` character are ignored. 226 | 227 | `=#` means to use the domain list to resolve specified domain names. 228 | 229 | `$#` means to use the domain list to resolve domain names with specified suffixes. 230 | 231 | Example: 232 | 233 | ```toml 234 | [hosts.'$#./domains.txt'] 235 | A = [ '127.0.0.1' ] 236 | 237 | [hosts.'=#./domains.txt'] 238 | 239 | [hosts.'$#/etc/secure-dns/domains.txt'] 240 | 241 | [hosts.'=#/mnt/txt'] 242 | ``` 243 | 244 | ## License 245 | 246 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fjinliming2%2Fsecure-dns.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fjinliming2%2Fsecure-dns?ref=badge_large) 247 | -------------------------------------------------------------------------------- /client/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type cacheItem struct { 9 | eol time.Time 10 | storeTime time.Time 11 | data interface{} 12 | } 13 | 14 | // Cache dns results 15 | type Cache struct { 16 | mu sync.RWMutex 17 | caches map[string]map[uint16]map[uint16]*cacheItem 18 | done chan<- bool 19 | } 20 | 21 | // NewCache return new Cache obj 22 | func NewCache() (cache *Cache) { 23 | ticker := time.NewTicker(30 * time.Second) 24 | done := make(chan bool, 0) 25 | 26 | cache = &Cache{ 27 | caches: make(map[string]map[uint16]map[uint16]*cacheItem), 28 | done: done, 29 | } 30 | 31 | go func() { 32 | defer ticker.Stop() 33 | 34 | for { 35 | select { 36 | case <-ticker.C: 37 | cache.clean() 38 | case <-done: 39 | return 40 | } 41 | } 42 | }() 43 | 44 | return 45 | } 46 | 47 | // Get item from cache, got nil if no cache available 48 | func (cache *Cache) Get(keyName string, keyType, keyClass uint16) (interface{}, time.Duration) { 49 | cache.mu.RLock() 50 | defer cache.mu.RUnlock() 51 | 52 | now := time.Now() 53 | 54 | if nameDict, ok := cache.caches[keyName]; ok { 55 | if typeDict, ok := nameDict[keyType]; ok { 56 | if item, ok := typeDict[keyClass]; ok { 57 | if item.eol.After(now) { 58 | return item.data, now.Sub(item.storeTime) 59 | } 60 | } 61 | } 62 | } 63 | return nil, 0 64 | } 65 | 66 | // SetDataTTL set item into cache with ttl 67 | func (cache *Cache) SetDataTTL(keyName string, keyType, keyClass uint16, data interface{}, ttl time.Duration) { 68 | cache.mu.Lock() 69 | defer cache.mu.Unlock() 70 | 71 | if _, ok := cache.caches[keyName]; !ok { 72 | cache.caches[keyName] = make(map[uint16]map[uint16]*cacheItem) 73 | } 74 | nameDict := cache.caches[keyName] 75 | if _, ok := nameDict[keyType]; !ok { 76 | nameDict[keyType] = make(map[uint16]*cacheItem) 77 | } 78 | typeDict := nameDict[keyType] 79 | 80 | now := time.Now() 81 | typeDict[keyClass] = &cacheItem{ 82 | storeTime: now, 83 | eol: now.Add(ttl), 84 | data: data, 85 | } 86 | } 87 | 88 | func (cache *Cache) clean() { 89 | cache.mu.Lock() 90 | defer cache.mu.Unlock() 91 | 92 | now := time.Now() 93 | 94 | for keyName, nameDict := range cache.caches { 95 | for keyType, typeDict := range nameDict { 96 | for keyClass, item := range typeDict { 97 | if item.eol.Before(now) { 98 | delete(typeDict, keyClass) 99 | } 100 | } 101 | if len(typeDict) == 0 { 102 | delete(nameDict, keyType) 103 | } 104 | } 105 | if len(nameDict) == 0 { 106 | delete(cache.caches, keyName) 107 | } 108 | } 109 | } 110 | 111 | // Destroy caches, stop cleaning tick 112 | func (cache *Cache) Destroy() { 113 | close(cache.done) 114 | for keyName := range cache.caches { 115 | delete(cache.caches, keyName) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "time" 7 | 8 | "github.com/jinliming2/secure-dns/client/cache" 9 | "github.com/jinliming2/secure-dns/selector" 10 | "github.com/miekg/dns" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | // Client handles DNS requests 15 | type Client struct { 16 | logger *zap.SugaredLogger 17 | timeout time.Duration 18 | 19 | bootstrap *net.Resolver 20 | upstream selector.Selector 21 | 22 | custom []*customResolver 23 | 24 | servers []*dns.Server 25 | 26 | cacher *cache.Cache 27 | cacheNoAnswer uint32 28 | } 29 | 30 | func startDNSServer(server *dns.Server, logger *zap.SugaredLogger, results chan error) { 31 | err := server.ListenAndServe() 32 | if err != nil { 33 | logger.Errorf("server %s://%s exited with error: %s", server.Net, server.Addr, err.Error()) 34 | } 35 | results <- err 36 | } 37 | 38 | // ListenAndServe listen on addresses and serve DNS service 39 | func (client *Client) ListenAndServe(addr []string) error { 40 | client.servers = make([]*dns.Server, 0, 2*len(addr)) 41 | 42 | results := make(chan error) 43 | 44 | client.logger.Info("creating server...") 45 | for _, address := range addr { 46 | client.logger.Debugf("new server: %s", address) 47 | udpServer := &dns.Server{ 48 | Addr: address, 49 | Net: "udp", 50 | Handler: dns.HandlerFunc(client.udpHandlerFunc), 51 | UDPSize: dns.DefaultMsgSize, 52 | } 53 | tcpServer := &dns.Server{ 54 | Addr: address, 55 | Net: "tcp", 56 | Handler: dns.HandlerFunc(client.tcpHandlerFunc), 57 | } 58 | go startDNSServer(udpServer, client.logger, results) 59 | go startDNSServer(tcpServer, client.logger, results) 60 | client.servers = append(client.servers, udpServer, tcpServer) 61 | } 62 | 63 | for i := 0; i < 2*len(addr); i++ { 64 | if err := <-results; err != nil { 65 | client.Shutdown() 66 | return err 67 | } 68 | } 69 | 70 | close(results) 71 | return nil 72 | } 73 | 74 | // Shutdown shuts down a server 75 | func (client *Client) Shutdown() []error { 76 | return client.ShutdownContext(context.Background()) 77 | } 78 | 79 | // ShutdownContext shuts down a server 80 | func (client *Client) ShutdownContext(ctx context.Context) (errors []error) { 81 | client.logger.Info("shutting down servers") 82 | for _, server := range client.servers { 83 | if server != nil { 84 | client.logger.Debugf("shutting down server %s://%s", server.Net, server.Addr) 85 | if err := server.ShutdownContext(ctx); err != nil { 86 | errors = append(errors, err) 87 | } 88 | } 89 | } 90 | if client.cacher != nil { 91 | client.cacher.Destroy() 92 | } 93 | return 94 | } 95 | -------------------------------------------------------------------------------- /client/customResolver.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/jinliming2/secure-dns/client/resolver" 7 | ) 8 | 9 | type customResolver struct { 10 | matcher func(string) bool 11 | resolver resolver.DNSClient 12 | } 13 | 14 | func newCustomResolver(resolver resolver.DNSClient, domain, suffix []string) *customResolver { 15 | domainList := make([]string, len(domain)) 16 | for index, d := range domain { 17 | domainList[index] = strings.Trim(d, ".") 18 | } 19 | 20 | suffixList := make([]string, len(suffix)) 21 | for index, s := range suffix { 22 | suffixList[index] = "." + strings.Trim(s, ".") 23 | } 24 | 25 | return &customResolver{ 26 | matcher: func(domain string) bool { 27 | trimmedDomain := strings.Trim(domain, ".") 28 | 29 | for _, d := range domainList { 30 | if trimmedDomain == d { 31 | return true 32 | } 33 | } 34 | 35 | trimmedDomain = "." + trimmedDomain 36 | for _, s := range suffixList { 37 | if strings.HasSuffix(trimmedDomain, s) { 38 | return true 39 | } 40 | } 41 | 42 | return false 43 | }, 44 | resolver: resolver, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/ecs/ecs.go: -------------------------------------------------------------------------------- 1 | package ecs 2 | 3 | import ( 4 | "math/rand" 5 | "net" 6 | "time" 7 | 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | var randomSource = rand.New(rand.NewSource(time.Now().UnixNano())) 12 | 13 | // SetECS set EDNS Client Subnet for specified DNS request Message 14 | func SetECS(r *dns.Msg, noECS bool, ecs []net.IP) { 15 | opt := r.IsEdns0() 16 | 17 | if opt == nil { 18 | if noECS || len(ecs) == 0 { 19 | return 20 | } 21 | 22 | r.SetEdns0(dns.DefaultMsgSize, false) 23 | opt = r.IsEdns0() 24 | } 25 | 26 | var eDNS0Subnet *dns.EDNS0_SUBNET 27 | 28 | for index, option := range opt.Option { 29 | if option.Option() == dns.EDNS0SUBNET { 30 | 31 | eDNS0Subnet = option.(*dns.EDNS0_SUBNET) 32 | 33 | if eDNS0Subnet.Address.IsUnspecified() && eDNS0Subnet.SourceNetmask == 0 { 34 | // +subnet=0 35 | // got an EDNS CLIENT-SUBNET option with an empty address and a source prefix-length of zero, 36 | // which signals a resolver that the client's address information must not be used when resolving this query. 37 | // so we should just return 38 | return 39 | } 40 | 41 | if noECS { 42 | // specified no_ecs in configuration, so we omit the subnet option 43 | opt.Option[index] = opt.Option[len(opt.Option)-1] 44 | opt.Option = opt.Option[:len(opt.Option)-1] 45 | return 46 | } 47 | 48 | break 49 | } 50 | } 51 | 52 | if noECS || len(ecs) == 0 { 53 | return 54 | } 55 | 56 | if eDNS0Subnet == nil { 57 | eDNS0Subnet = new(dns.EDNS0_SUBNET) 58 | eDNS0Subnet.Code = dns.EDNS0SUBNET 59 | eDNS0Subnet.SourceScope = 0 60 | opt.Option = append(opt.Option, eDNS0Subnet) 61 | } 62 | 63 | ip := ecs[randomSource.Intn(len(ecs))] 64 | ip4 := ip.To4() 65 | 66 | if ip4 != nil { 67 | eDNS0Subnet.Family = 1 68 | eDNS0Subnet.SourceNetmask = 24 69 | eDNS0Subnet.Address = ip4 70 | } else { 71 | eDNS0Subnet.Family = 2 72 | eDNS0Subnet.SourceNetmask = 56 73 | eDNS0Subnet.Address = ip 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /client/handler.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/jinliming2/secure-dns/client/resolver" 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | func (client *Client) tcpHandlerFunc(w dns.ResponseWriter, r *dns.Msg) { 12 | client.handlerFunc(w, r, true) 13 | } 14 | 15 | func (client *Client) udpHandlerFunc(w dns.ResponseWriter, r *dns.Msg) { 16 | client.handlerFunc(w, r, false) 17 | } 18 | 19 | func (client *Client) handlerFunc(w dns.ResponseWriter, r *dns.Msg, useTCP bool) { 20 | if r.Response { 21 | client.logger.Warn("received a response packet") 22 | return 23 | } 24 | 25 | if len(r.Question) != 1 { 26 | client.logger.Warn("request packet contains more than 1 question is not allowed") 27 | reply := new(dns.Msg).SetReply(r).SetRcodeFormatError(r) 28 | w.WriteMsg(reply) 29 | return 30 | } 31 | 32 | question := &r.Question[0] 33 | qName := question.Name 34 | qClass := "" 35 | qType := "" 36 | 37 | if class, ok := dns.ClassToString[question.Qclass]; ok { 38 | qClass = class 39 | } else { 40 | qClass = fmt.Sprintf("%d", question.Qclass) 41 | } 42 | 43 | if t, ok := dns.TypeToString[question.Qtype]; ok { 44 | qType = t 45 | } else { 46 | qType = fmt.Sprintf("%d", question.Qtype) 47 | } 48 | 49 | client.logger.Infow(fmt.Sprintf("[%d] request", r.Id), "name", qName, "class", qClass, "type", qType) 50 | 51 | if client.cacher != nil { 52 | if cached, delta := client.cacher.Get(question.Name, question.Qtype, question.Qclass); cached != nil { 53 | response := cached.(*dns.Msg).Copy() 54 | response.Id = r.Id 55 | if delta > 0 { 56 | for _, rr := range response.Answer { 57 | resolver.FixRecordTTL(rr, delta) 58 | } 59 | for _, rr := range response.Ns { 60 | resolver.FixRecordTTL(rr, delta) 61 | } 62 | for _, rr := range response.Extra { 63 | resolver.FixRecordTTL(rr, delta) 64 | } 65 | } 66 | client.logger.Debugf("[%d] using cache for %s", r.Id, qName) 67 | w.WriteMsg(response) 68 | return 69 | } 70 | } 71 | 72 | var c *resolver.DNSClient 73 | 74 | for _, custom := range client.custom { 75 | if custom.matcher(qName) { 76 | c = &custom.resolver 77 | client.logger.Debugf("[%d] using %s for %s [condition]", r.Id, (*c).String(), qName) 78 | break 79 | } 80 | } 81 | 82 | if c == nil { 83 | if client.upstream.Empty() { 84 | client.logger.Warnf("no upstream to use for querying %s", qName) 85 | reply := new(dns.Msg).SetRcode(r, dns.RcodeServerFailure) 86 | w.WriteMsg(reply) 87 | return 88 | } 89 | 90 | c = client.upstream.Get().Client 91 | client.logger.Debugf("[%d] using %s for %s", r.Id, (*c).String(), qName) 92 | } 93 | 94 | response, err := (*c).Resolve(r, useTCP, false) 95 | if err != nil { 96 | client.logger.Warn(err.Error()) 97 | } 98 | if (len(response.Answer) == 0 || !answerHasType(response.Answer, question.Qtype)) && (!(*c).ECSDisabled()) && (*c).FallbackNoECSEnabled() { 99 | client.logger.Debugf("[%d] retring resolve %s with ECS disabled", r.Id, qName) 100 | response, err = (*c).Resolve(r, useTCP, true) 101 | if err != nil { 102 | client.logger.Warn(err.Error()) 103 | } 104 | } 105 | w.WriteMsg(response) 106 | 107 | if client.cacher != nil && err == nil { 108 | var minttl uint32 109 | if response.Rcode == dns.RcodeNameError || len(response.Answer)+len(response.Ns)+len(response.Extra) == 0 { 110 | minttl = client.cacheNoAnswer 111 | } else if response.Rcode == dns.RcodeSuccess { 112 | for _, answer := range response.Answer { 113 | ttl := answer.Header().Ttl 114 | if ttl > 0 && (minttl == 0 || ttl < minttl) { 115 | minttl = ttl 116 | } 117 | } 118 | for _, ns := range response.Ns { 119 | ttl := ns.Header().Ttl 120 | if ttl > 0 && (minttl == 0 || ttl < minttl) { 121 | minttl = ttl 122 | } 123 | } 124 | for _, extra := range response.Extra { 125 | ttl := extra.Header().Ttl 126 | if ttl > 0 && (minttl == 0 || ttl < minttl) { 127 | minttl = ttl 128 | } 129 | } 130 | } 131 | if minttl > 0 { 132 | client.cacher.SetDataTTL(question.Name, question.Qtype, question.Qclass, response, time.Duration(minttl)*time.Second) 133 | } 134 | } 135 | } 136 | 137 | func answerHasType(answer []dns.RR, qType uint16) bool { 138 | for _, a := range answer { 139 | if a.Header().Rrtype == qType { 140 | return true 141 | } 142 | } 143 | return false 144 | } 145 | -------------------------------------------------------------------------------- /client/newClient.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "math/rand" 8 | "net" 9 | "path/filepath" 10 | "strings" 11 | "time" 12 | 13 | "github.com/jinliming2/secure-dns/client/cache" 14 | "github.com/jinliming2/secure-dns/client/resolver" 15 | "github.com/jinliming2/secure-dns/config" 16 | "github.com/jinliming2/secure-dns/selector" 17 | "go.uber.org/zap" 18 | ) 19 | 20 | // NewClient returns a client with dnsClients 21 | func NewClient(logger *zap.SugaredLogger, conf *config.Config) (client *Client, err error) { 22 | timeout := time.Duration(*conf.Config.Timeout) * time.Second 23 | client = &Client{logger: logger, timeout: timeout, cacheNoAnswer: conf.Config.CacheNoAnswer} 24 | 25 | switch conf.Config.RoundRobin { 26 | case config.SelectorClock: 27 | client.upstream = &selector.Clock{} 28 | case config.SelectorRandom: 29 | client.upstream = &selector.Random{} 30 | case config.SelectorSWRR: 31 | client.upstream = &selector.SWrr{} 32 | case config.SelectorWRandom: 33 | client.upstream = &selector.WRandom{} 34 | default: 35 | err = fmt.Errorf("no such round robin: %s", conf.Config.RoundRobin) 36 | return 37 | } 38 | 39 | logger.Info("creating clients...") 40 | 41 | for domain, b := range conf.Hosts { 42 | c := resolver.NewHostsDNSClient(b) 43 | if strings.HasPrefix(domain, "$#") || strings.HasPrefix(domain, "=#") { 44 | fileName := domain[2:] 45 | if filepath.IsLocal(fileName) { 46 | fileName = filepath.Join(filepath.Dir(conf.ConfigFile), fileName) 47 | } 48 | var data []byte 49 | data, err = ioutil.ReadFile(fileName) 50 | if err != nil { 51 | return 52 | } 53 | lines := strings.Split(string(data), "\n") 54 | domains := lines[:0] 55 | for _, line := range lines { 56 | trimLine := strings.TrimSpace(line) 57 | if !strings.HasPrefix(trimLine, "#") { 58 | domains = append(domains, trimLine) 59 | } 60 | } 61 | if strings.HasPrefix(domain, "$#") { 62 | logger.Debugf("new HOSTS resolver: %d record(s) from file %s (for suffix match)", len(domains), fileName) 63 | cr := newCustomResolver(c, []string{}, domains) 64 | client.custom = append(client.custom, cr) 65 | } else { 66 | logger.Debugf("new HOSTS resolver: %d record(s) from file %s", len(domains), fileName) 67 | cr := newCustomResolver(c, domains, []string{}) 68 | client.custom = append(client.custom, cr) 69 | } 70 | } else if strings.HasPrefix(domain, "*.") { 71 | domain = domain[2:] 72 | logger.Debugf("new HOSTS resolver: %s (for wildcard domain)", domain) 73 | cr := newCustomResolver(c, []string{}, []string{domain}) 74 | client.custom = append(client.custom, cr) 75 | } else { 76 | logger.Debugf("new HOSTS resolver: %s", domain) 77 | cr := newCustomResolver(c, []string{domain}, []string{}) 78 | client.custom = append(client.custom, cr) 79 | } 80 | } 81 | 82 | var randomSource = rand.New(rand.NewSource(time.Now().UnixNano())) 83 | 84 | traditionalLoop: 85 | for _, traditional := range conf.Traditional { 86 | if traditional.Bootstrap { 87 | logger.Debugf("new traditional resolver: %s (for bootstrap)", fmt.Sprintf("dns://%s:%d", traditional.Host, traditional.Port)) 88 | if client.bootstrap != nil { 89 | logger.Warnf("only one bootstrap resolver allowed, ignoring %s...", fmt.Sprintf("dns://%s:%d", traditional.Host, traditional.Port)) 90 | continue 91 | } 92 | if len(traditional.Domain)+len(traditional.Suffix) > 0 { 93 | logger.Warn("domain and suffix doesn't support for bootstrap resolver") 94 | } 95 | count := len(traditional.Host) 96 | addresses := make([]string, count) 97 | for index, host := range traditional.Host { 98 | if addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", host, traditional.Port)); err == nil { 99 | addresses[index] = addr.String() 100 | } else if addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("[%s]:%d", host, traditional.Port)); err == nil { 101 | addresses[index] = addr.String() 102 | } else { 103 | logger.Warnf("parse bootstrap address failed: %s:%d, [%s]:%d", host, traditional.Port, host, traditional.Port) 104 | continue traditionalLoop 105 | } 106 | } 107 | client.bootstrap = &net.Resolver{ 108 | Dial: func(ctx context.Context, network, address string) (net.Conn, error) { 109 | return (&net.Dialer{}).DialContext(ctx, network, addresses[randomSource.Intn(count)]) 110 | }, 111 | } 112 | continue 113 | } 114 | 115 | dnsConfig := config.DNSSettings{ 116 | CustomECS: append(traditional.CustomECS, conf.Config.CustomECS...), 117 | FallbackNoECS: conf.Config.FallbackNoECS || traditional.FallbackNoECS, 118 | NoECS: conf.Config.NoECS || traditional.NoECS, 119 | NoSingleInflight: conf.Config.NoSingleInflight || traditional.NoSingleInflight, 120 | } 121 | c := resolver.NewTraditionalDNSClient(traditional.Host, traditional.Port, timeout, dnsConfig) 122 | 123 | if len(traditional.Domain)+len(traditional.Suffix) > 0 { 124 | logger.Debugf("new traditional resolver: %s (for specified domain or suffix use)", c.String()) 125 | cr := newCustomResolver(c, traditional.Domain, traditional.Suffix) 126 | client.custom = append(client.custom, cr) 127 | } else { 128 | logger.Debugf("new traditional resolver: %s", c.String()) 129 | client.upstream.Add(traditional.Weight, c) 130 | } 131 | } 132 | 133 | for _, tls := range conf.TLS { 134 | dnsConfig := config.DNSSettings{ 135 | CustomECS: append(tls.CustomECS, conf.Config.CustomECS...), 136 | FallbackNoECS: conf.Config.FallbackNoECS || tls.FallbackNoECS, 137 | NoECS: conf.Config.NoECS || tls.NoECS, 138 | NoSingleInflight: conf.Config.NoSingleInflight || tls.NoSingleInflight, 139 | } 140 | c, err := resolver.NewTLSDNSClient(tls.Host, tls.Port, tls.Hostname, timeout, dnsConfig, client.bootstrap) 141 | if err != nil { 142 | logger.Error(err) 143 | continue 144 | } 145 | 146 | if len(tls.Domain)+len(tls.Suffix) > 0 { 147 | logger.Debugf("new TLS resolver: %s (for specified domain or suffix use)", c.String()) 148 | cr := newCustomResolver(c, tls.Domain, tls.Suffix) 149 | client.custom = append(client.custom, cr) 150 | } else { 151 | logger.Debugf("new TLS resolver: %s", c.String()) 152 | client.upstream.Add(tls.Weight, c) 153 | } 154 | } 155 | 156 | for _, https := range conf.HTTPS { 157 | dnsConfig := config.DNSSettings{ 158 | CustomECS: append(https.CustomECS, conf.Config.CustomECS...), 159 | FallbackNoECS: conf.Config.FallbackNoECS || https.FallbackNoECS, 160 | NoECS: conf.Config.NoECS || https.NoECS, 161 | NoUserAgent: conf.Config.NoUserAgent || https.NoUserAgent, 162 | NoSingleInflight: conf.Config.NoSingleInflight || https.NoSingleInflight, 163 | } 164 | if https.UserAgent != "" { 165 | dnsConfig.UserAgent = https.UserAgent 166 | } else if conf.Config.UserAgent != "" { 167 | dnsConfig.UserAgent = conf.Config.UserAgent 168 | } 169 | var c resolver.DNSClient 170 | var err error 171 | if https.Google { 172 | c, err = resolver.NewHTTPSGoogleDNSClient( 173 | https.Host, 174 | https.Port, 175 | https.Hostname, 176 | https.Path, 177 | https.Cookie, 178 | timeout, 179 | dnsConfig, 180 | client.bootstrap, 181 | logger, 182 | ) 183 | } else { 184 | c, err = resolver.NewHTTPSDNSClient( 185 | https.Host, 186 | https.Port, 187 | https.Hostname, 188 | https.Path, 189 | https.Cookie, 190 | timeout, 191 | dnsConfig, 192 | client.bootstrap, 193 | logger, 194 | ) 195 | } 196 | if err != nil { 197 | logger.Error(err) 198 | continue 199 | } 200 | 201 | if len(https.Domain)+len(https.Suffix) > 0 { 202 | logger.Debugf("new HTTPS resolver: %s (for specified domain or suffix use)", c.String()) 203 | cr := newCustomResolver(c, https.Domain, https.Suffix) 204 | client.custom = append(client.custom, cr) 205 | } else { 206 | logger.Debugf("new HTTPS resolver: %s", c.String()) 207 | client.upstream.Add(https.Weight, c) 208 | } 209 | } 210 | 211 | client.upstream.Start() 212 | logger.Infof("using round robin: %s", client.upstream.Name()) 213 | 214 | if !conf.Config.NoCache { 215 | client.cacher = cache.NewCache() 216 | } 217 | 218 | return 219 | } 220 | -------------------------------------------------------------------------------- /client/resolver/hosts.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | // HostsDNSClient resolves DNS with Hosts 10 | type HostsDNSClient struct { 11 | records map[string][]string 12 | } 13 | 14 | // NewHostsDNSClient returns a new hosts DNS client 15 | func NewHostsDNSClient(records map[string][]string) *HostsDNSClient { 16 | return &HostsDNSClient{records: records} 17 | } 18 | 19 | func (client *HostsDNSClient) String() string { 20 | return "HOSTS resolver" 21 | } 22 | 23 | func (client *HostsDNSClient) ECSDisabled() bool { 24 | return true 25 | } 26 | 27 | func (client *HostsDNSClient) FallbackNoECSEnabled() bool { 28 | return false 29 | } 30 | 31 | // Resolve DNS 32 | func (client *HostsDNSClient) Resolve(request *dns.Msg, useTCP bool, forceNoECS bool) (reply *dns.Msg, _ error) { 33 | reply = getEmptyResponse(request) 34 | 35 | question := request.Question[0] 36 | 37 | var questionType string 38 | var ok bool 39 | if questionType, ok = dns.TypeToString[question.Qtype]; !ok { 40 | reply.Rcode = dns.RcodeFormatError 41 | return 42 | } 43 | 44 | var records []string 45 | if records, ok = client.records[questionType]; !ok { 46 | reply.Answer = make([]dns.RR, 0) 47 | return 48 | } 49 | 50 | reply.Answer = make([]dns.RR, len(records)) 51 | 52 | for index, record := range records { 53 | zone := fmt.Sprintf("%s 0 IN %s %s", question.Name, questionType, record) 54 | if rr, err := dns.NewRR(zone); err == nil { 55 | reply.Answer[index] = rr 56 | } else { 57 | reply.Rcode = dns.RcodeServerFailure 58 | reply.Answer = nil 59 | return 60 | } 61 | } 62 | 63 | return 64 | } 65 | -------------------------------------------------------------------------------- /client/resolver/https.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "io/ioutil" 8 | "net" 9 | "net/http" 10 | "net/http/cookiejar" 11 | "time" 12 | 13 | "github.com/jinliming2/secure-dns/client/ecs" 14 | "github.com/jinliming2/secure-dns/config" 15 | "github.com/jinliming2/secure-dns/versions" 16 | "github.com/miekg/dns" 17 | "go.uber.org/zap" 18 | "golang.org/x/sync/singleflight" 19 | ) 20 | 21 | // HTTPSDNSClient resolves DNS with DNS-over-HTTPS 22 | type HTTPSDNSClient struct { 23 | host []string 24 | port uint16 25 | addresses []addressHostname 26 | client *http.Client 27 | path string 28 | timeout time.Duration 29 | singleInflight *singleflight.Group 30 | logger *zap.SugaredLogger 31 | config.DNSSettings 32 | } 33 | 34 | // NewHTTPSDNSClient returns a new HTTPS DNS client 35 | func NewHTTPSDNSClient( 36 | host []string, 37 | port uint16, 38 | hostname, path string, 39 | cookie bool, 40 | timeout time.Duration, 41 | settings config.DNSSettings, 42 | bootstrap *net.Resolver, 43 | logger *zap.SugaredLogger, 44 | ) (*HTTPSDNSClient, error) { 45 | 46 | addresses := make([]addressHostname, len(host)) 47 | for index, h := range host { 48 | if ip := net.ParseIP(h); ip != nil { 49 | if ip.To4() == nil { 50 | addresses[index] = addressHostname{address: fmt.Sprintf("[%s]:%d", h, port), hostname: hostname} 51 | } else { 52 | addresses[index] = addressHostname{address: fmt.Sprintf("%s:%d", h, port), hostname: hostname} 53 | } 54 | } else { 55 | addresses[index] = addressHostname{address: fmt.Sprintf("%s:%d", h, port)} 56 | } 57 | } 58 | 59 | transport := http.DefaultTransport.(*http.Transport).Clone() 60 | dialer := &net.Dialer{ 61 | Resolver: bootstrap, 62 | } 63 | transport.DialContext = dialer.DialContext 64 | 65 | var jar http.CookieJar 66 | if cookie { 67 | jar, _ = cookiejar.New(nil) 68 | } 69 | 70 | var sf *singleflight.Group 71 | if !settings.NoSingleInflight { 72 | sf = &singleflight.Group{} 73 | } 74 | 75 | return &HTTPSDNSClient{ 76 | host: host, 77 | port: port, 78 | addresses: addresses, 79 | client: &http.Client{ 80 | Transport: transport, 81 | Jar: jar, 82 | Timeout: timeout, 83 | }, 84 | path: path, 85 | timeout: timeout, 86 | singleInflight: sf, 87 | logger: logger, 88 | DNSSettings: settings, 89 | }, nil 90 | } 91 | 92 | func (client *HTTPSDNSClient) String() string { 93 | return fmt.Sprintf("https://%s:%d%s", client.host, client.port, client.path) 94 | } 95 | 96 | func (client *HTTPSDNSClient) ECSDisabled() bool { 97 | return client.NoECS 98 | } 99 | 100 | func (client *HTTPSDNSClient) FallbackNoECSEnabled() bool { 101 | return client.FallbackNoECS 102 | } 103 | 104 | // Resolve DNS 105 | func (client *HTTPSDNSClient) Resolve(request *dns.Msg, useTCP bool, forceNoECS bool) (*dns.Msg, error) { 106 | return httpsSingleInflightRequest(request, forceNoECS, client.singleInflight, client.resolve) 107 | } 108 | 109 | func (client *HTTPSDNSClient) resolve(request *dns.Msg, forceNoECS bool) (*dns.Msg, error) { 110 | ecs.SetECS(request, forceNoECS || client.NoECS, client.CustomECS) 111 | 112 | msg, err := request.Pack() 113 | if err != nil { 114 | reply := getEmptyErrorResponse(request) 115 | reply.Rcode = dns.RcodeFormatError 116 | return reply, err 117 | } 118 | 119 | data := base64.RawURLEncoding.EncodeToString(msg) 120 | 121 | address := client.addresses[randomSource.Intn(len(client.addresses))] 122 | 123 | url := fmt.Sprintf("https://%s%s?dns=%s", address.address, client.path, data) 124 | 125 | var req *http.Request 126 | if len(url) < 2048 { 127 | req, err = http.NewRequest(http.MethodGet, url, nil) 128 | client.logger.Debugf("[%d] GET %s", request.Id, url) 129 | if err != nil { 130 | return getEmptyErrorResponse(request), err 131 | } 132 | } else { 133 | req, err = http.NewRequest(http.MethodPost, fmt.Sprintf("https://%s%s", address.address, client.path), bytes.NewReader(msg)) 134 | if err != nil { 135 | return getEmptyErrorResponse(request), err 136 | } 137 | req.ContentLength = int64(len(msg)) 138 | req.Header.Set("content-type", mimeDNSMsg) 139 | client.logger.Debugf("[%d] POST %s with %d bytes body", request.Id, req.URL, req.ContentLength) 140 | } 141 | req.Header.Set("accept", mimeDNSMsg) 142 | req.Close = false 143 | if address.hostname != "" { 144 | req.Host = address.hostname 145 | } 146 | 147 | if client.NoUserAgent { 148 | req.Header.Set("user-agent", "") 149 | } else if client.UserAgent != "" { 150 | req.Header.Set("user-agent", client.UserAgent) 151 | } else { 152 | req.Header.Set("user-agent", versions.USERAGENT) 153 | } 154 | 155 | return httpsGetDNSMessage(request, req, client.client, address, client.path, client.logger) 156 | } 157 | 158 | func httpsSingleInflightRequest( 159 | request *dns.Msg, 160 | forceNoECS bool, 161 | singleInflight *singleflight.Group, 162 | resolve func(request *dns.Msg, forceNoECS bool) (*dns.Msg, error), 163 | ) (*dns.Msg, error) { 164 | if singleInflight == nil { 165 | return resolve(request, forceNoECS) 166 | } 167 | 168 | question := request.Question[0] 169 | key := fmt.Sprintf("%s:%d:%d", question.Name, question.Qtype, question.Qclass) 170 | 171 | result := <-singleInflight.DoChan(key, func() (interface{}, error) { 172 | return resolve(request, forceNoECS) 173 | }) 174 | 175 | if result.Err != nil || result.Val == nil { 176 | return getEmptyErrorResponse(request), result.Err 177 | } 178 | 179 | reply := result.Val.(*dns.Msg) 180 | if result.Shared { 181 | reply = reply.Copy() 182 | } 183 | reply.Id = request.Id 184 | 185 | return reply, nil 186 | } 187 | 188 | func httpsGetDNSMessage( 189 | request *dns.Msg, 190 | req *http.Request, 191 | client *http.Client, 192 | address addressHostname, 193 | path string, 194 | logger *zap.SugaredLogger, 195 | ) (*dns.Msg, error) { 196 | res, err := client.Do(req) 197 | if res != nil && res.Body != nil { 198 | defer res.Body.Close() 199 | } 200 | if err != nil { 201 | return getEmptyErrorResponse(request), err 202 | } 203 | 204 | if res.StatusCode >= 300 || res.StatusCode < 200 { 205 | return getEmptyErrorResponse(request), fmt.Errorf("HTTP error from %s%s: %d %s", address, path, res.StatusCode, res.Status) 206 | } 207 | contentType := res.Header.Get("content-type") 208 | if !regexDNSMsg.MatchString(contentType) { 209 | return getEmptyErrorResponse(request), fmt.Errorf("HTTP unsupported MIME type: %s", contentType) 210 | } 211 | 212 | logger.Debugf("[%d] %s: %s", request.Id, res.Status, contentType) 213 | 214 | body, err := ioutil.ReadAll(res.Body) 215 | if err != nil { 216 | return getEmptyErrorResponse(request), err 217 | } 218 | 219 | reply := new(dns.Msg) 220 | err = reply.Unpack(body) 221 | if err != nil { 222 | return getEmptyErrorResponse(request), err 223 | } 224 | reply.Id = request.Id 225 | 226 | headerLastModified := res.Header.Get("last-modified") 227 | if headerLastModified != "" { 228 | if modifiedTime, err := time.Parse(http.TimeFormat, headerLastModified); err == nil { 229 | now := time.Now().UTC() 230 | headerDate := res.Header.Get("date") 231 | if headerDate != "" { 232 | if date, err := time.Parse(http.TimeFormat, headerDate); err == nil { 233 | now = date 234 | } 235 | } 236 | delta := now.Sub(modifiedTime) 237 | if delta > 0 { 238 | for _, rr := range reply.Answer { 239 | FixRecordTTL(rr, delta) 240 | } 241 | for _, rr := range reply.Ns { 242 | FixRecordTTL(rr, delta) 243 | } 244 | for _, rr := range reply.Extra { 245 | FixRecordTTL(rr, delta) 246 | } 247 | } 248 | } 249 | } 250 | 251 | return reply, nil 252 | } 253 | -------------------------------------------------------------------------------- /client/resolver/https_google.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "net/http/cookiejar" 8 | "net/url" 9 | "strconv" 10 | "time" 11 | 12 | "github.com/jinliming2/secure-dns/client/ecs" 13 | "github.com/jinliming2/secure-dns/config" 14 | "github.com/jinliming2/secure-dns/versions" 15 | "github.com/miekg/dns" 16 | "go.uber.org/zap" 17 | "golang.org/x/sync/singleflight" 18 | ) 19 | 20 | // HTTPSGoogleDNSClient resolves DNS with DNS-over-HTTPS Google API 21 | type HTTPSGoogleDNSClient struct { 22 | host []string 23 | port uint16 24 | addresses []addressHostname 25 | client *http.Client 26 | path string 27 | timeout time.Duration 28 | singleInflight *singleflight.Group 29 | logger *zap.SugaredLogger 30 | config.DNSSettings 31 | } 32 | 33 | // NewHTTPSGoogleDNSClient returns a new HTTPS DNS client using Google API 34 | func NewHTTPSGoogleDNSClient( 35 | host []string, 36 | port uint16, 37 | hostname, path string, 38 | cookie bool, 39 | timeout time.Duration, 40 | settings config.DNSSettings, 41 | bootstrap *net.Resolver, 42 | logger *zap.SugaredLogger, 43 | ) (*HTTPSGoogleDNSClient, error) { 44 | 45 | addresses := make([]addressHostname, len(host)) 46 | for index, h := range host { 47 | if ip := net.ParseIP(h); ip != nil { 48 | if ip.To4() == nil { 49 | addresses[index] = addressHostname{address: fmt.Sprintf("[%s]:%d", h, port), hostname: hostname} 50 | } else { 51 | addresses[index] = addressHostname{address: fmt.Sprintf("%s:%d", h, port), hostname: hostname} 52 | } 53 | } else { 54 | addresses[index] = addressHostname{address: fmt.Sprintf("%s:%d", h, port)} 55 | } 56 | } 57 | 58 | transport := http.DefaultTransport.(*http.Transport).Clone() 59 | dialer := &net.Dialer{ 60 | Resolver: bootstrap, 61 | } 62 | transport.DialContext = dialer.DialContext 63 | 64 | var jar http.CookieJar 65 | if cookie { 66 | jar, _ = cookiejar.New(nil) 67 | } 68 | 69 | var sf *singleflight.Group 70 | if !settings.NoSingleInflight { 71 | sf = &singleflight.Group{} 72 | } 73 | 74 | return &HTTPSGoogleDNSClient{ 75 | host: host, 76 | port: port, 77 | addresses: addresses, 78 | client: &http.Client{ 79 | Transport: transport, 80 | Jar: jar, 81 | Timeout: timeout, 82 | }, 83 | path: path, 84 | timeout: timeout, 85 | singleInflight: sf, 86 | logger: logger, 87 | DNSSettings: settings, 88 | }, nil 89 | } 90 | 91 | func (client *HTTPSGoogleDNSClient) String() string { 92 | return fmt.Sprintf("https+google://%s:%d%s", client.host, client.port, client.path) 93 | } 94 | 95 | func (client *HTTPSGoogleDNSClient) ECSDisabled() bool { 96 | return client.NoECS 97 | } 98 | 99 | func (client *HTTPSGoogleDNSClient) FallbackNoECSEnabled() bool { 100 | return client.FallbackNoECS 101 | } 102 | 103 | // Resolve DNS 104 | func (client *HTTPSGoogleDNSClient) Resolve(request *dns.Msg, useTCP bool, forceNoECS bool) (*dns.Msg, error) { 105 | return httpsSingleInflightRequest(request, forceNoECS, client.singleInflight, client.resolve) 106 | } 107 | 108 | func (client *HTTPSGoogleDNSClient) resolve(request *dns.Msg, forceNoECS bool) (*dns.Msg, error) { 109 | ecs.SetECS(request, forceNoECS || client.NoECS, client.CustomECS) 110 | 111 | query := url.Values{} 112 | query.Set("name", request.Question[0].Name) 113 | query.Set("type", strconv.FormatUint(uint64(request.Question[0].Qtype), 10)) 114 | if request.CheckingDisabled { 115 | query.Set("cd", "1") 116 | } 117 | query.Set("ct", mimeDNSMsg) 118 | if opt := request.IsEdns0(); opt != nil { 119 | if opt.Do() { 120 | query.Set("do", "1") 121 | } 122 | for _, option := range opt.Option { 123 | if option.Option() == dns.EDNS0SUBNET { 124 | eDNS0Subnet := option.(*dns.EDNS0_SUBNET) 125 | subnet := fmt.Sprintf("%s/%d", eDNS0Subnet.Address.String(), eDNS0Subnet.SourceNetmask) 126 | query.Set("edns_client_subnet", subnet) 127 | } 128 | } 129 | } 130 | // TODO: random padding 131 | // query.Set("random_padding", "") 132 | 133 | address := client.addresses[randomSource.Intn(len(client.addresses))] 134 | 135 | url := fmt.Sprintf("https://%s%s?%s", address.address, client.path, query.Encode()) 136 | 137 | req, err := http.NewRequest(http.MethodGet, url, nil) 138 | client.logger.Debugf("[%d] GET %s", request.Id, url) 139 | if err != nil { 140 | return getEmptyErrorResponse(request), err 141 | } 142 | req.Header.Set("accept", mimeDNSMsg) 143 | req.Close = false 144 | if address.hostname != "" { 145 | req.Host = address.hostname 146 | } 147 | 148 | if client.NoUserAgent { 149 | req.Header.Set("user-agent", "") 150 | } else if client.UserAgent != "" { 151 | req.Header.Set("user-agent", client.UserAgent) 152 | } else { 153 | req.Header.Set("user-agent", versions.USERAGENT) 154 | } 155 | 156 | return httpsGetDNSMessage(request, req, client.client, address, client.path, client.logger) 157 | } 158 | -------------------------------------------------------------------------------- /client/resolver/resolve.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | // FixRecordTTL of reply dns msg 10 | func FixRecordTTL(rr dns.RR, delta time.Duration) { 11 | header := rr.Header() 12 | if header.Rrtype == dns.TypeOPT { 13 | return 14 | } 15 | old := time.Duration(header.Ttl) * time.Second 16 | new := old - delta 17 | if new > 0 { 18 | header.Ttl = uint32(new / time.Second) 19 | } else { 20 | header.Ttl = 0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /client/resolver/tls.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net" 7 | "time" 8 | 9 | "github.com/jinliming2/secure-dns/client/ecs" 10 | "github.com/jinliming2/secure-dns/config" 11 | "github.com/miekg/dns" 12 | ) 13 | 14 | // TLSDNSClient resolves DNS with DNS-over-TLS 15 | type TLSDNSClient struct { 16 | host []string 17 | port uint16 18 | addresses []string 19 | client *dns.Client 20 | timeout time.Duration 21 | config.DNSSettings 22 | } 23 | 24 | // NewTLSDNSClient returns a new TLS DNS client 25 | func NewTLSDNSClient( 26 | host []string, 27 | port uint16, 28 | hostname string, 29 | timeout time.Duration, 30 | settings config.DNSSettings, 31 | bootstrap *net.Resolver, 32 | ) (*TLSDNSClient, error) { 33 | 34 | addresses := make([]string, len(host)) 35 | for index, h := range host { 36 | if ip := net.ParseIP(h); ip != nil && ip.To4() == nil { 37 | addresses[index] = fmt.Sprintf("[%s]:%d", h, port) 38 | } else { 39 | addresses[index] = fmt.Sprintf("%s:%d", h, port) 40 | } 41 | } 42 | 43 | return &TLSDNSClient{ 44 | host: host, 45 | port: port, 46 | addresses: addresses, 47 | client: &dns.Client{ 48 | Net: "tcp-tls", 49 | TLSConfig: &tls.Config{ 50 | ServerName: hostname, 51 | ClientSessionCache: tls.NewLRUClientSessionCache(-1), 52 | }, 53 | Dialer: &net.Dialer{ 54 | Resolver: bootstrap, 55 | }, 56 | Timeout: timeout, 57 | SingleInflight: !settings.NoSingleInflight, 58 | }, 59 | timeout: timeout, 60 | DNSSettings: settings, 61 | }, nil 62 | } 63 | 64 | func (client *TLSDNSClient) String() string { 65 | return fmt.Sprintf("tls://%s:%d", client.host, client.port) 66 | } 67 | 68 | func (client *TLSDNSClient) ECSDisabled() bool { 69 | return client.NoECS 70 | } 71 | 72 | func (client *TLSDNSClient) FallbackNoECSEnabled() bool { 73 | return client.FallbackNoECS 74 | } 75 | 76 | // Resolve DNS 77 | func (client *TLSDNSClient) Resolve(request *dns.Msg, useTCP bool, forceNoECS bool) (*dns.Msg, error) { 78 | ecs.SetECS(request, forceNoECS || client.NoECS, client.CustomECS) 79 | res, _, err := client.client.Exchange(request, client.addresses[randomSource.Intn(len(client.addresses))]) 80 | if err != nil { 81 | return getEmptyErrorResponse(request), fmt.Errorf("failed to resolve %s using %s: %s", request.Question[0].Name, client.String(), err.Error()) 82 | } 83 | // https://github.com/miekg/dns/issues/1145 84 | res.Id = request.Id 85 | return res, nil 86 | } 87 | -------------------------------------------------------------------------------- /client/resolver/traditional.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "time" 7 | 8 | "github.com/jinliming2/secure-dns/client/ecs" 9 | "github.com/jinliming2/secure-dns/config" 10 | "github.com/miekg/dns" 11 | ) 12 | 13 | // TraditionalDNSClient resolves DNS with traditional DNS client 14 | type TraditionalDNSClient struct { 15 | host []string 16 | port uint16 17 | addresses []string 18 | udpClient *dns.Client 19 | tcpClient *dns.Client 20 | timeout time.Duration 21 | config.DNSSettings 22 | } 23 | 24 | // NewTraditionalDNSClient returns a new traditional DNS client 25 | func NewTraditionalDNSClient(host []string, port uint16, timeout time.Duration, settings config.DNSSettings) *TraditionalDNSClient { 26 | addresses := make([]string, len(host)) 27 | for index, h := range host { 28 | if ip := net.ParseIP(h); ip != nil && ip.To4() == nil { 29 | addresses[index] = fmt.Sprintf("[%s]:%d", h, port) 30 | } else { 31 | addresses[index] = fmt.Sprintf("%s:%d", h, port) 32 | } 33 | } 34 | return &TraditionalDNSClient{ 35 | host: host, 36 | port: port, 37 | addresses: addresses, 38 | udpClient: &dns.Client{ 39 | Net: "udp", 40 | UDPSize: dns.DefaultMsgSize, 41 | Timeout: timeout, 42 | SingleInflight: !settings.NoSingleInflight, 43 | }, 44 | tcpClient: &dns.Client{ 45 | Net: "tcp", 46 | Timeout: timeout, 47 | SingleInflight: !settings.NoSingleInflight, 48 | }, 49 | timeout: timeout, 50 | DNSSettings: settings, 51 | } 52 | } 53 | 54 | func (client *TraditionalDNSClient) String() string { 55 | return fmt.Sprintf("dns://%s:%d", client.host, client.port) 56 | } 57 | 58 | func (client *TraditionalDNSClient) ECSDisabled() bool { 59 | return client.NoECS 60 | } 61 | 62 | func (client *TraditionalDNSClient) FallbackNoECSEnabled() bool { 63 | return client.FallbackNoECS 64 | } 65 | 66 | // Resolve DNS 67 | func (client *TraditionalDNSClient) Resolve(request *dns.Msg, useTCP bool, forceNoECS bool) (*dns.Msg, error) { 68 | var c *dns.Client 69 | if useTCP { 70 | c = client.tcpClient 71 | } else { 72 | c = client.udpClient 73 | } 74 | ecs.SetECS(request, forceNoECS || client.NoECS, client.CustomECS) 75 | res, _, err := c.Exchange(request, client.addresses[randomSource.Intn(len(client.addresses))]) 76 | if err != nil { 77 | return getEmptyErrorResponse(request), fmt.Errorf("failed to resolve %s using %s: %s", request.Question[0].Name, client.String(), err.Error()) 78 | } 79 | // https://github.com/miekg/dns/issues/1145 80 | res.Id = request.Id 81 | return res, nil 82 | } 83 | -------------------------------------------------------------------------------- /client/resolver/types.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "math/rand" 5 | "regexp" 6 | "time" 7 | 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | var ( 12 | regexDNSMsg = regexp.MustCompile(`\bapplication/dns-message\b`) 13 | 14 | mimeDNSMsg = "application/dns-message" 15 | 16 | randomSource = rand.New(rand.NewSource(time.Now().UnixNano())) 17 | ) 18 | 19 | // DNSClient is a DNS client 20 | type DNSClient interface { 21 | String() string 22 | ECSDisabled() bool 23 | FallbackNoECSEnabled() bool 24 | Resolve(*dns.Msg, bool, bool) (*dns.Msg, error) 25 | } 26 | 27 | type addressHostname struct { 28 | address string 29 | hostname string 30 | } 31 | 32 | func getEmptyResponse(request *dns.Msg) *dns.Msg { 33 | return new(dns.Msg).SetReply(request) 34 | } 35 | 36 | func getEmptyErrorResponse(request *dns.Msg) *dns.Msg { 37 | return new(dns.Msg).SetRcode(request, dns.RcodeServerFailure) 38 | } 39 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | 7 | "github.com/BurntSushi/toml" 8 | ) 9 | 10 | // DNSSettings described general settings of DNS resolver 11 | type DNSSettings struct { 12 | CustomECS []net.IP `toml:"custom_ecs"` 13 | FallbackNoECS bool `toml:"fallback_no_ecs"` 14 | NoECS bool `toml:"no_ecs"` 15 | UserAgent string `toml:"user_agent"` 16 | NoUserAgent bool `toml:"no_user_agent"` 17 | NoSingleInflight bool `toml:"no_single_inflight"` 18 | } 19 | 20 | type typeCustomSpecified struct { 21 | Domain []string `toml:"domain"` 22 | Suffix []string `toml:"suffix"` 23 | } 24 | 25 | type typeGeneralConfig struct { 26 | Listen []string `toml:"listen"` 27 | Timeout *uint `toml:"timeout"` // seconds 28 | RoundRobin Selectors `toml:"round_robin"` // default: clock 29 | CacheNoAnswer uint32 `toml:"cache_no_answer"` 30 | NoCache bool `toml:"no_cache"` 31 | DNSSettings 32 | } 33 | 34 | type typeUpstreamHTTPS struct { 35 | Host []string `toml:"host"` 36 | Port uint16 `toml:"port"` // default: 443 37 | Hostname string `toml:"hostname"` 38 | Path string `toml:"path"` // default: /dns-query 39 | Google bool `toml:"google"` 40 | Cookie bool `toml:"cookie"` 41 | Weight int32 `toml:"weight"` // default: 1 42 | typeCustomSpecified 43 | DNSSettings 44 | } 45 | 46 | type typeUpstreamTLS struct { 47 | Host []string `toml:"host"` 48 | Port uint16 `toml:"port"` // default: 853 49 | Hostname string `toml:"hostname"` 50 | Weight int32 `toml:"weight"` // default: 1 51 | typeCustomSpecified 52 | DNSSettings 53 | } 54 | 55 | type typeTraditional struct { 56 | Host []string `toml:"host"` 57 | Port uint16 `toml:"port"` // default: 53 58 | Bootstrap bool `toml:"bootstrap"` 59 | Weight int32 `toml:"weight"` // default: 1 60 | typeCustomSpecified 61 | DNSSettings 62 | } 63 | 64 | // Config described user configuration 65 | type Config struct { 66 | ConfigFile string `toml:"-"` 67 | Config typeGeneralConfig `toml:"config"` 68 | HTTPS []typeUpstreamHTTPS `toml:"https"` 69 | TLS []typeUpstreamTLS `toml:"tls"` 70 | Traditional []typeTraditional `toml:"traditional"` 71 | Hosts map[string]map[string][]string `toml:"hosts"` 72 | } 73 | 74 | // LoadConfig from configuration file 75 | func LoadConfig(configPath string) (config *Config, err error) { 76 | config = &Config{ConfigFile: configPath} 77 | _, err = toml.DecodeFile(configPath, &config) 78 | if err != nil { 79 | return 80 | } 81 | 82 | if len(config.Config.Listen) == 0 { 83 | err = errors.New("no listen address") 84 | return 85 | } 86 | if config.Config.Timeout == nil { 87 | config.Config.Timeout = new(uint) 88 | *config.Config.Timeout = 5 89 | } 90 | 91 | if config.Config.RoundRobin == "" { 92 | config.Config.RoundRobin = SelectorClock 93 | } 94 | 95 | for index := range config.HTTPS { 96 | https := &config.HTTPS[index] 97 | if https.Port == 0 { 98 | https.Port = 443 99 | } 100 | if https.Path == "" { 101 | https.Path = "/dns-query" 102 | } 103 | if https.Weight < 1 { 104 | https.Weight = 1 105 | } 106 | } 107 | 108 | for index := range config.TLS { 109 | tls := &config.TLS[index] 110 | if tls.Port == 0 { 111 | tls.Port = 853 112 | } 113 | if tls.Weight < 1 { 114 | tls.Weight = 1 115 | } 116 | } 117 | 118 | for index := range config.Traditional { 119 | traditional := &config.Traditional[index] 120 | if traditional.Port == 0 { 121 | traditional.Port = 53 122 | } 123 | if traditional.Weight < 1 { 124 | traditional.Weight = 1 125 | } 126 | } 127 | 128 | return 129 | } 130 | -------------------------------------------------------------------------------- /config/selector.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Selectors type 4 | type Selectors string 5 | 6 | const ( 7 | // SelectorClock use clock selector 8 | SelectorClock = Selectors("clock") 9 | // SelectorRandom use random selector 10 | SelectorRandom = Selectors("random") 11 | // SelectorSWRR use Smooth-weighted-round-robin selector 12 | SelectorSWRR = Selectors("swrr") 13 | // SelectorWRandom use Weighted-random selector 14 | SelectorWRandom = Selectors("wrandom") 15 | ) 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jinliming2/secure-dns 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.2.1 7 | github.com/miekg/dns v1.1.51 8 | go.uber.org/atomic v1.10.0 // indirect 9 | go.uber.org/multierr v1.10.0 // indirect 10 | go.uber.org/zap v1.24.0 11 | golang.org/x/sync v0.1.0 12 | golang.org/x/tools v0.7.0 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= 2 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 4 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 9 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 10 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 11 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 12 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 13 | github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo= 14 | github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c= 15 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 16 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 21 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 22 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 24 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 25 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 26 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 27 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 28 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 29 | go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= 30 | go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 31 | go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= 32 | go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 33 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 34 | go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= 35 | go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 36 | go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= 37 | go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= 38 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 39 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 40 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 41 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 42 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 43 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 44 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 45 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 46 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 47 | golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= 48 | golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 49 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 50 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 51 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 52 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 53 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 54 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 55 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 56 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 57 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= 58 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 59 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 60 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 61 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 62 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 63 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 64 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 65 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 67 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 69 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 70 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 71 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 72 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 74 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 75 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 77 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 78 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 79 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 80 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 81 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 82 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 83 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 84 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 85 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 86 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 87 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 88 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 89 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 90 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 91 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 92 | golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= 93 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 94 | golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= 95 | golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= 96 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 97 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 98 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 99 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 100 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 101 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 102 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 103 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 104 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 105 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "go.uber.org/zap" 4 | 5 | var ( 6 | logger *zap.SugaredLogger 7 | loggerConfig = zap.Config{ 8 | Level: zap.NewAtomicLevelAt(zap.ErrorLevel), 9 | Development: false, 10 | DisableCaller: true, 11 | Encoding: "console", 12 | EncoderConfig: zap.NewDevelopmentEncoderConfig(), 13 | OutputPaths: []string{"stdout"}, 14 | ErrorOutputPaths: []string{"stderr"}, 15 | } 16 | ) 17 | 18 | func init() { 19 | basicLogger, err := loggerConfig.Build() 20 | if err != nil { 21 | panic("cannot open logger") 22 | } 23 | logger = basicLogger.Sugar() 24 | defer logger.Sync() 25 | } 26 | 27 | func setLogLevel(level string) { 28 | switch level { 29 | case "error": 30 | loggerConfig.Level.SetLevel(zap.ErrorLevel) 31 | case "warn": 32 | loggerConfig.Level.SetLevel(zap.WarnLevel) 33 | case "info": 34 | loggerConfig.Level.SetLevel(zap.InfoLevel) 35 | case "verbose": 36 | loggerConfig.Level.SetLevel(zap.DebugLevel) 37 | default: 38 | panic("wrong log level, only error, warn, info and verbose are support") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "github.com/jinliming2/secure-dns/client" 11 | "github.com/jinliming2/secure-dns/config" 12 | "github.com/jinliming2/secure-dns/versions" 13 | ) 14 | 15 | func main() { 16 | var ( 17 | configFile = flag.String("config", "/etc/"+versions.PROGRAM+"/config.toml", "Config file") 18 | version = flag.Bool("version", false, "Show version") 19 | logLevel = flag.String("logLevel", "info", "Set log level (error, warn, info, verbose)") 20 | verbose = flag.Bool("verbose", false, "Set log level to verbose") 21 | ) 22 | 23 | flag.Parse() 24 | 25 | if *version { 26 | versions.PrintVersion() 27 | return 28 | } 29 | 30 | if *verbose { 31 | setLogLevel("verbose") 32 | } else { 33 | setLogLevel(*logLevel) 34 | } 35 | 36 | logger.Infof("Reading configuration file: %s", *configFile) 37 | config, err := config.LoadConfig(*configFile) 38 | if err != nil { 39 | logger.Errorf("Error while handling configuration file: %s", err.Error()) 40 | os.Exit(1) 41 | } 42 | if loggerConfig.Level.Level() < 0 { 43 | json, _ := json.MarshalIndent(*config, "", " ") 44 | logger.Debugf("Configuration file: %s", json) 45 | } 46 | 47 | dnsClient, err := client.NewClient(logger, config) 48 | if err != nil { 49 | logger.Error(err) 50 | os.Exit(1) 51 | } 52 | 53 | go func() { 54 | err := dnsClient.ListenAndServe(config.Config.Listen) 55 | if err != nil { 56 | os.Exit(1) 57 | } else { 58 | os.Exit(0) 59 | } 60 | }() 61 | 62 | sig := make(chan os.Signal, 1) 63 | signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL) 64 | 65 | <-sig 66 | logger.Info("Exiting") 67 | dnsClient.Shutdown() 68 | } 69 | -------------------------------------------------------------------------------- /secure-dns.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Secure DNS Client 3 | Documentation=https://github.com/jinliming2/secure-dns#readme 4 | After=network.target 5 | Before=nss-lookup.target 6 | 7 | [Service] 8 | User=nobody 9 | Group=nobody 10 | AmbientCapabilities=CAP_NET_BIND_SERVICE 11 | 12 | Type=simple 13 | ExecStart=/usr/local/bin/secure-dns 14 | 15 | Restart=always 16 | RestartSec=3 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | -------------------------------------------------------------------------------- /selector/Clock.go: -------------------------------------------------------------------------------- 1 | package selector 2 | 3 | import ( 4 | "sync/atomic" 5 | 6 | "github.com/jinliming2/secure-dns/client/resolver" 7 | ) 8 | 9 | type clockData struct { 10 | health int32 11 | } 12 | 13 | // Clock return items one by one 14 | type Clock struct { 15 | clients []*Item 16 | 17 | length int32 18 | index int32 19 | } 20 | 21 | // Name of selector 22 | func (clock *Clock) Name() string { 23 | return "Clock" 24 | } 25 | 26 | // Add item to list 27 | func (clock *Clock) Add(weight int32, client resolver.DNSClient) { 28 | clock.clients = append(clock.clients, &Item{ 29 | Client: &client, 30 | weight: weight, 31 | data: &clockData{ 32 | health: 10, 33 | }, 34 | }) 35 | clock.length++ 36 | } 37 | 38 | // Empty Selector? 39 | func (clock *Clock) Empty() bool { 40 | return clock.length == 0 41 | } 42 | 43 | // Start set index 44 | func (clock *Clock) Start() { 45 | if clock.Empty() { 46 | return 47 | } 48 | clock.index = randomSource.Int31n(clock.length) 49 | } 50 | 51 | // Get an item 52 | func (clock *Clock) Get() *Item { 53 | if clock.Empty() { 54 | return nil 55 | } 56 | 57 | index := clock.index 58 | i := index + 1 59 | if i >= clock.length { 60 | i = 0 61 | } 62 | 63 | for i != index { 64 | if atomic.LoadInt32(&clock.clients[i].data.(*clockData).health) > 0 { 65 | atomic.StoreInt32(&clock.index, i) 66 | return clock.clients[i] 67 | } 68 | i++ 69 | if i >= clock.length { 70 | i = 0 71 | } 72 | } 73 | 74 | i++ 75 | if i >= clock.length { 76 | i = 0 77 | } 78 | 79 | atomic.StoreInt32(&clock.index, i) 80 | return clock.clients[i] 81 | } 82 | 83 | // SetHealth of an item 84 | func (clock *Clock) SetHealth(item *Item, score int32) { 85 | atomic.AddInt32(&item.data.(*clockData).health, score) 86 | } 87 | -------------------------------------------------------------------------------- /selector/Random.go: -------------------------------------------------------------------------------- 1 | package selector 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jinliming2/secure-dns/client/resolver" 7 | ) 8 | 9 | // Random return items randomally 10 | type Random struct { 11 | clients []*Item 12 | 13 | length int 14 | } 15 | 16 | // Name of selector 17 | func (random *Random) Name() string { 18 | return "Random" 19 | } 20 | 21 | // Add item to list 22 | func (random *Random) Add(weight int32, client resolver.DNSClient) { 23 | random.clients = append(random.clients, &Item{ 24 | Client: &client, 25 | weight: weight, 26 | }) 27 | random.length++ 28 | } 29 | 30 | // Empty Selector? 31 | func (random *Random) Empty() bool { 32 | return random.length == 0 33 | } 34 | 35 | // Start set index 36 | func (random *Random) Start() { 37 | randomSource.Seed(time.Now().UnixNano()) 38 | } 39 | 40 | // Get an item 41 | func (random *Random) Get() *Item { 42 | if random.Empty() { 43 | return nil 44 | } 45 | return random.clients[randomSource.Intn(random.length)] 46 | } 47 | 48 | // SetHealth of an item 49 | func (random *Random) SetHealth(item *Item, score int32) {} 50 | -------------------------------------------------------------------------------- /selector/SWRR.go: -------------------------------------------------------------------------------- 1 | package selector 2 | 3 | import ( 4 | "sync/atomic" 5 | 6 | "github.com/jinliming2/secure-dns/client/resolver" 7 | ) 8 | 9 | type sWrrData struct { 10 | currentWeight int32 11 | } 12 | 13 | // SWrr implemented smooth weighted round robin 14 | type SWrr struct { 15 | clients []*Item 16 | 17 | length int32 18 | index int32 19 | } 20 | 21 | // Name of selector 22 | func (sWrr *SWrr) Name() string { 23 | return "Smooth weighted round robin" 24 | } 25 | 26 | // Add item to list 27 | func (sWrr *SWrr) Add(weight int32, client resolver.DNSClient) { 28 | sWrr.clients = append(sWrr.clients, &Item{ 29 | Client: &client, 30 | weight: weight, 31 | data: &sWrrData{ 32 | currentWeight: 0, 33 | }, 34 | }) 35 | sWrr.length++ 36 | } 37 | 38 | // Empty Selector? 39 | func (sWrr *SWrr) Empty() bool { 40 | return sWrr.length == 0 41 | } 42 | 43 | // Start set index 44 | func (sWrr *SWrr) Start() { 45 | if sWrr.Empty() { 46 | return 47 | } 48 | count := randomSource.Int31n(sWrr.length) 49 | for i := int32(0); i < count; i++ { 50 | sWrr.Get() 51 | } 52 | } 53 | 54 | // Get an item 55 | func (sWrr *SWrr) Get() *Item { 56 | if sWrr.Empty() { 57 | return nil 58 | } 59 | 60 | var ( 61 | total int32 62 | best *Item 63 | ) 64 | 65 | for _, item := range sWrr.clients { 66 | atomic.AddInt32(&item.data.(*sWrrData).currentWeight, item.weight) 67 | total += item.weight 68 | 69 | if best == nil || atomic.LoadInt32(&item.data.(*sWrrData).currentWeight) > atomic.LoadInt32(&best.data.(*sWrrData).currentWeight) { 70 | best = item 71 | } 72 | } 73 | 74 | if best != nil { 75 | atomic.AddInt32(&best.data.(*sWrrData).currentWeight, -total) 76 | } 77 | 78 | return best 79 | } 80 | 81 | // SetHealth of an item 82 | func (sWrr *SWrr) SetHealth(item *Item, score int32) { 83 | n := atomic.AddInt32(&item.data.(*sWrrData).currentWeight, score) 84 | if n < 1 { 85 | atomic.StoreInt32(&item.data.(*sWrrData).currentWeight, 1) 86 | } else if n > item.weight { 87 | atomic.StoreInt32(&item.data.(*sWrrData).currentWeight, item.weight) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /selector/Selector.go: -------------------------------------------------------------------------------- 1 | package selector 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/jinliming2/secure-dns/client/resolver" 8 | ) 9 | 10 | var randomSource = rand.New(rand.NewSource(time.Now().UnixNano())) 11 | 12 | // Item is an item in Selector 13 | type Item struct { 14 | Client *resolver.DNSClient 15 | weight int32 16 | 17 | data interface{} 18 | } 19 | 20 | // Selector implemented round robins 21 | type Selector interface { 22 | Name() string 23 | Add(weight int32, client resolver.DNSClient) 24 | Empty() bool 25 | Start() 26 | Get() *Item 27 | SetHealth(item *Item, score int32) 28 | } 29 | -------------------------------------------------------------------------------- /selector/WRandom.go: -------------------------------------------------------------------------------- 1 | package selector 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jinliming2/secure-dns/client/resolver" 7 | ) 8 | 9 | type wRandomInfo struct { 10 | min int32 11 | max int32 12 | client *Item 13 | } 14 | 15 | // WRandom return items randomally with weight 16 | type WRandom struct { 17 | clients []wRandomInfo 18 | 19 | length int32 20 | } 21 | 22 | // Name of selector 23 | func (wRandom *WRandom) Name() string { 24 | return "Weighted random" 25 | } 26 | 27 | // Add item to list 28 | func (wRandom *WRandom) Add(weight int32, client resolver.DNSClient) { 29 | wRandom.clients = append(wRandom.clients, wRandomInfo{ 30 | min: wRandom.length, // [ 31 | max: wRandom.length + weight, // ) 32 | client: &Item{ 33 | Client: &client, 34 | weight: weight, 35 | }, 36 | }) 37 | wRandom.length += weight 38 | } 39 | 40 | // Empty Selector? 41 | func (wRandom *WRandom) Empty() bool { 42 | return wRandom.length == 0 43 | } 44 | 45 | // Start set index 46 | func (wRandom *WRandom) Start() { 47 | randomSource.Seed(time.Now().UnixNano()) 48 | } 49 | 50 | // Get an item 51 | func (wRandom *WRandom) Get() *Item { 52 | if wRandom.Empty() { 53 | return nil 54 | } 55 | index := randomSource.Int31n(wRandom.length) 56 | for _, info := range wRandom.clients { 57 | if info.min <= index && index < info.max { 58 | return info.client 59 | } 60 | } 61 | return nil 62 | } 63 | 64 | // SetHealth of an item 65 | func (wRandom *WRandom) SetHealth(item *Item, score int32) {} 66 | -------------------------------------------------------------------------------- /versions/versions.go: -------------------------------------------------------------------------------- 1 | package versions 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | var ( 9 | // PROGRAM is the name of this software 10 | PROGRAM = "secure-dns" 11 | // VERSION should be replaced at compile 12 | VERSION = "UNKNOWN" 13 | // BUILDHASH should be replaced at compile 14 | BUILDHASH = "" 15 | 16 | // USERAGENT for DNS over HTTPS 17 | USERAGENT = PROGRAM + "/" + VERSION + " https://github.com/jinliming2/secure-dns" 18 | ) 19 | 20 | // PrintVersion print version information 21 | func PrintVersion() { 22 | fmt.Printf("%s/%s %s %s/%s\n", PROGRAM, VERSION, BUILDHASH, runtime.GOOS, runtime.GOARCH) 23 | fmt.Printf("Build with Go %s (%s)\n", runtime.Version(), runtime.Compiler) 24 | } 25 | --------------------------------------------------------------------------------