├── .github └── stale.yml ├── .gitignore ├── LICENSE ├── README.md ├── appveyor.yml ├── build.py ├── codecov.yml ├── config.sample.yml ├── config.test.yml ├── core ├── cache │ └── cache.go ├── common │ ├── common.go │ ├── edns.go │ ├── ipset.go │ ├── ipset_test.go │ └── upstream.go ├── config │ └── config.go ├── control.go ├── errors │ └── normal_error.go ├── finder │ ├── finder.go │ ├── full │ │ └── map.go │ └── regex │ │ └── list.go ├── hosts │ ├── hosts.go │ └── hosts_test.go ├── inbound │ └── server.go ├── matcher │ ├── final │ │ └── default.go │ ├── full │ │ ├── list.go │ │ └── map.go │ ├── matcher.go │ ├── mix │ │ └── list.go │ ├── regex │ │ └── list.go │ └── suffix │ │ ├── tree.go │ │ └── tree_test.go └── outbound │ ├── clients │ ├── cache.go │ ├── local.go │ ├── remote.go │ ├── remote_bundle.go │ └── resolver │ │ ├── address.go │ │ ├── address_test.go │ │ ├── base_resolver.go │ │ ├── https_resolver.go │ │ ├── resolver_test.go │ │ ├── tcp_resolver.go │ │ ├── tcptls_resolver.go │ │ └── udp_resolver.go │ ├── dispatcher.go │ └── dispatcher_test.go ├── go.mod ├── go.sum └── main ├── main.go └── main_test.go /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 90 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 14 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - enhancement 10 | - bug 11 | # Label to use when marking an issue as stale 12 | staleLabel: stale 13 | # Comment to post when marking an issue as stale. Set to `false` to disable 14 | markComment: > 15 | This issue has been automatically marked as stale because it has not had 16 | recent activity. It will be closed if no further activity occurs. Thank you 17 | for your contributions. 18 | # Comment to post when closing a stale issue. Set to `false` to disable 19 | closeComment: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 4 | *.o 5 | *.a 6 | *.so 7 | 8 | # Folders 9 | _obj 10 | _test 11 | 12 | # Architecture specific extensions/prefixes 13 | *.[568vq] 14 | [568vq].out 15 | 16 | *.cgo1.go 17 | *.cgo2.c 18 | _cgo_defun.c 19 | _cgo_gotypes.go 20 | _cgo_export.* 21 | 22 | _testmain.go 23 | 24 | *.exe 25 | *.test 26 | *.prof 27 | 28 | # Output of the go coverage tool, specifically when used with LiteIDE 29 | *.out 30 | 31 | # external packages folder 32 | vendor/ 33 | 34 | .idea/ 35 | .DS_Store 36 | 37 | *.txt 38 | *_sample 39 | *_temp 40 | 41 | overture-* 42 | *.log 43 | 44 | config.dev.* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 shawn1m 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # overture 2 | [![Build status](https://ci.appveyor.com/api/projects/status/gqrixsfcmmrcaohr/branch/master?svg=true)](https://ci.appveyor.com/project/shawn1m/overture/branch/master) 3 | [![GoDoc](https://godoc.org/github.com/shawn1m/overture?status.svg)](https://godoc.org/github.com/shawn1m/overture) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/shawn1m/overture)](https://goreportcard.com/report/github.com/shawn1m/overture) 5 | [![codecov](https://codecov.io/gh/shawn1m/overture/branch/master/graph/badge.svg)](https://codecov.io/gh/shawn1m/overture) 6 | 7 | Overture is a customized DNS relay server. 8 | 9 | Overture means the orchestral piece at the beginning of a classical music composition, just like DNS which is nearly the 10 | first step of surfing the Internet. 11 | 12 | **Please note:** 13 | - Read the **entire README first** is necessary if you want to use overture **safely** or **create an issue** for this project . 14 | - **Production usage is not recommended and there is no guarantee or warranty of it.** 15 | 16 | ## Features 17 | 18 | + Multiple DNS upstream 19 | + Via UDP/TCP with custom port 20 | + Via SOCKS5 proxy (TCP only) 21 | + With EDNS Client Subnet (ECS) [RFC7871](https://tools.ietf.org/html/rfc7871) 22 | + Dispatcher 23 | + Custom domain 24 | + Custom IP network 25 | + IPv6 record (AAAA) redirection 26 | + Full IPv6 support 27 | + Minimum TTL modification 28 | + Hosts (Both IPv4 and IPv6 are supported and IPs will be returned in a random order. If you want to use regex match hosts, please understand how regex works first) 29 | + Cache with ECS and Redis(Persistence) support 30 | + DNS over HTTP server support 31 | 32 | ### Dispatch process 33 | 34 | DNS queries with certain domain will be forced to use selected DNS when matched. 35 | 36 | For the IP network dispatch, overture will send queries to primary DNS first. Then, If that answer is empty or not matched, the alternative DNS servers will be used instead. 37 | 38 | ## Installation 39 | 40 | The binary releases are available in [releases](https://github.com/shawn1m/overture/releases). 41 | 42 | ## Usages 43 | 44 | Start with the default config file `./config.yml` 45 | 46 | **Only file having a `.json` suffix will be considered as json format for compatibility and that support is deprecated.** 47 | 48 | $ ./overture 49 | 50 | Or use your own config file: 51 | 52 | $ ./overture -c /path/to/config.yml 53 | 54 | Verbose mode: 55 | 56 | $ ./overture -v 57 | 58 | Log to file: 59 | 60 | $ ./overture -l /path/to/overture.log 61 | 62 | For other options, please check the helping menu: 63 | 64 | $ ./overture -h 65 | 66 | Tips: 67 | 68 | + Root privilege might be required if you want to let overture listen on port 53 or one of other system ports. 69 | 70 | ### Configuration Syntax 71 | 72 | Configuration file is "config.yml" by default: 73 | 74 | ```yaml 75 | bindAddress: :53 76 | debugHTTPAddress: 127.0.0.1:5555 77 | dohEnabled: false 78 | primaryDNS: 79 | - name: DNSPod 80 | address: 119.29.29.29:53 81 | protocol: udp 82 | socks5Address: 83 | timeout: 6 84 | ednsClientSubnet: 85 | policy: disable 86 | externalIP: 87 | noCookie: true 88 | alternativeDNS: 89 | - name: 114DNS 90 | address: 114.114.114.114:53 91 | protocol: udp 92 | socks5Address: 93 | timeout: 6 94 | ednsClientSubnet: 95 | policy: disable 96 | externalIP: 97 | noCookie: true 98 | onlyPrimaryDNS: false 99 | ipv6UseAlternativeDNS: false 100 | alternativeDNSConcurrent: false 101 | whenPrimaryDNSAnswerNoneUse: primaryDNS 102 | ipNetworkFile: 103 | primary: ./ip_network_primary_sample 104 | alternative: ./ip_network_alternative_sample 105 | domainFile: 106 | primary: ./domain_primary_sample 107 | alternative: ./domain_alternative_sample 108 | matcher: full-map 109 | hostsFile: 110 | hostsFile: ./hosts_sample 111 | finder: full-map 112 | minimumTTL: 0 113 | domainTTLFile: ./domain_ttl_sample 114 | cacheSize: 0 115 | cacheRedisUrl: redis://localhost:6379/0 116 | cacheRedisConnectionPoolSize: 10 117 | rejectQType: 118 | - 255 119 | ``` 120 | 121 | Tips: 122 | 123 | + bindAddress: Specifying any port (e.g. `:53`) will let overture listen on all available addresses (both IPv4 and 124 | IPv6). Overture will handle both TCP and UDP requests. Literal IPv6 addresses are enclosed in square brackets (e.g. `[2001:4860:4860::8888]:53`) 125 | + debugHTTPAddress: Specifying an HTTP port for debug (**`5555` is the default port despite it is also acknowledged as the android Wi-Fi adb listener port**), currently used to dump DNS cache, and the request url is `/cache`, available query argument is `nobody`(boolean) 126 | 127 | * true(default): only get the cache size; 128 | 129 | ```bash 130 | $ curl 127.0.0.1:5555/cache | jq 131 | { 132 | "length": 1, 133 | "capacity": 100, 134 | "body": {} 135 | } 136 | ``` 137 | 138 | * false: get cache size along with cache detail. 139 | 140 | ```bash 141 | $ curl 127.0.0.1:5555/cache?nobody=false | jq 142 | { 143 | "length": 1, 144 | "capacity": 100, 145 | "body": { 146 | "www.baidu.com. 1": [ 147 | { 148 | "name": "www.baidu.com.", 149 | "ttl": 1140, 150 | "type": "CNAME", 151 | "rdata": "www.a.shifen.com." 152 | }, 153 | { 154 | "name": "www.a.shifen.com.", 155 | "ttl": 300, 156 | "type": "CNAME", 157 | "rdata": "www.wshifen.com." 158 | }, 159 | { 160 | "name": "www.wshifen.com.", 161 | "ttl": 300, 162 | "type": "A", 163 | "rdata": "104.193.88.123" 164 | }, 165 | { 166 | "name": "www.wshifen.com.", 167 | "ttl": 300, 168 | "type": "A", 169 | "rdata": "104.193.88.77" 170 | } 171 | ] 172 | } 173 | } 174 | ``` 175 | + dohEnabled: Enable DNS over HTTP server using `DebugHTTPAddress` above with url path `/dns-query`. DNS over HTTPS server can be easily achieved helping by another web server software like caddy or nginx. 176 | + primaryDNS/alternativeDNS: 177 | + name: This field is only used for logging. 178 | + address: Same rule as BindAddress. 179 | + protocol: `tcp`, `udp`, `tcp-tls` or `https` 180 | + `tcp-tls`: Address format is "servername:port@serverAddress", try one.one.one.one:853 or one.one.one.one:853@1.1.1.1 181 | + `https`: Just try https://cloudflare-dns.com/dns-query 182 | + Check [DNS Privacy Public Resolvers](https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Public+Resolvers) for more public `tcp-tls`, `https` resolvers. 183 | + socks5Address: Forward dns query to this SOCKS5 proxy, `“”` to disable. 184 | + ednsClientSubnet: Use this to improve DNS accuracy for many reasons. Please check [RFC7871](https://tools.ietf.org/html/rfc7871) for 185 | details. 186 | + policy 187 | + `auto`: If the client IP is not in the reserved IP network, use the client IP. Otherwise, use the external IP. 188 | + `manual`: Use the external IP if this field is not empty, otherwise use the client IP if it is not one of the reserved IPs. 189 | + `disable`: Disable this feature. 190 | + externalIP: If this field is empty, ECS will be disabled when the inbound IP is not an external IP. 191 | + noCookie: Disable cookie. 192 | + onlyPrimaryDNS: Disable dispatcher feature, use primary DNS only. 193 | + ipv6UseAlternativeDNS: For to redirect IPv6 DNS queries to alternative DNS servers. 194 | + alternativeDNSConcurrent: Query the primaryDNS and alternativeDNS at the same time. 195 | + whenPrimaryDNSAnswerNoneUse: If the response of primaryDNS exists and there is no `ANSWER SECTION` in it, the final chosen DNS upstream should be defined here. (There is no `AAAA` record for most domains right now) 196 | + *File: Both relative like `./file` or absolute path like `/path/to/file` are supported. Especially, for Windows users, please use properly escaped path like 197 | `C:\\path\\to\\file.txt` in the configuration. 198 | + domainFile.Matcher: Matching policy and implementation, including "full-list", "full-map", "regex-list", "mix-list", "suffix-tree" and "final". Default value is "full-map". 199 | + hostsFile.Finder: Finder policy and implementation, including "full-map", "regex-list". Default value is "full-map". 200 | + domainTTLFile: Regex match only for now; 201 | + minimumTTL: Set the minimum TTL value (in seconds) in order to improve caching efficiency, use `0` to disable. 202 | + cacheSize: The number of query record to cache, use `0` to disable. 203 | + cacheRedisUrl, cacheRedisConnectionPoolSize: Use redis cache instead of local cache. 204 | + rejectQType: Reject query with specific DNS record types, check [List of DNS record types](https://en.wikipedia.org/wiki/List_of_DNS_record_types) for details. 205 | 206 | #### Domain file example (full match) 207 | 208 | example.com 209 | 210 | #### Domain file example (regex match) 211 | 212 | ^xxx.xx 213 | 214 | #### IP network file example (CIDR match) 215 | 216 | 1.0.1.0/24 217 | ::1/128 218 | 219 | #### Domain TTL file example (regex match) 220 | 221 | example.com$ 100 222 | 223 | #### Hosts file example (full match) 224 | 225 | 127.0.0.1 localhost 226 | ::1 localhost 227 | 228 | #### Hosts file example (regex match) 229 | 230 | 10.8.0.1 example.com$ 231 | 232 | #### DNS servers with ECS support 233 | 234 | + DNSPod 119.29.29.29:53 235 | 236 | For DNSPod, ECS might only work via udp, you can test it by [patched dig](https://www.gsic.uva.es/~jnisigl/dig-edns-client-subnet.html) to certify this argument by comparing answers. 237 | 238 | **The accuracy depends on the server side.** 239 | 240 | ``` 241 | $ dig @119.29.29.29 www.qq.com +client=119.29.29.29 242 | 243 | ; <<>> DiG 9.9.3 <<>> @119.29.29.29 www.qq.com +client=119.29.29.29 244 | ; (1 server found) 245 | ;; global options: +cmd 246 | ;; Got answer: 247 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 64995 248 | ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 249 | 250 | ;; OPT PSEUDOSECTION: 251 | ; EDNS: version: 0, flags:; udp: 4096 252 | ; CLIENT-SUBNET: 119.29.29.29/32/24 253 | ;; QUESTION SECTION: 254 | ;www.qq.com. IN A 255 | 256 | ;; ANSWER SECTION: 257 | www.qq.com. 300 IN A 101.226.103.106 258 | 259 | ;; Query time: 52 msec 260 | ;; SERVER: 119.29.29.29#53(119.29.29.29) 261 | ;; WHEN: Wed Mar 08 18:00:52 CST 2017 262 | ;; MSG SIZE rcvd: 67 263 | ``` 264 | 265 | ``` 266 | $ dig @119.29.29.29 www.qq.com +client=119.29.29.29 +tcp 267 | 268 | ; <<>> DiG 9.9.3 <<>> @119.29.29.29 www.qq.com +client=119.29.29.29 +tcp 269 | ; (1 server found) 270 | ;; global options: +cmd 271 | ;; Got answer: 272 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58331 273 | ;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1 274 | 275 | ;; OPT PSEUDOSECTION: 276 | ; EDNS: version: 0, flags:; udp: 4096 277 | ;; QUESTION SECTION: 278 | ;www.qq.com. IN A 279 | 280 | ;; ANSWER SECTION: 281 | www.qq.com. 43 IN A 59.37.96.63 282 | www.qq.com. 43 IN A 14.17.32.211 283 | www.qq.com. 43 IN A 14.17.42.40 284 | 285 | ;; Query time: 81 msec 286 | ;; SERVER: 119.29.29.29#53(119.29.29.29) 287 | ;; WHEN: Wed Mar 08 18:01:32 CST 2017 288 | ;; MSG SIZE rcvd: 87 289 | ``` 290 | 291 | ## Acknowledgements 292 | 293 | + [dns](https://github.com/miekg/dns): BSD-3-Clause 294 | + [skydns](https://github.com/skynetservices/skydns): MIT 295 | + [go-dnsmasq](https://github.com/janeczku/go-dnsmasq): MIT 296 | + [All Contributors](https://github.com/shawn1m/overture/graphs/contributors) 297 | 298 | ## License 299 | 300 | This project is under the MIT license. See the [LICENSE](LICENSE) file for the full license text. 301 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: 2 | - Visual Studio 2022 3 | stack: go 1.18.2 4 | 5 | for: 6 | - 7 | matrix: 8 | only: 9 | - image: macOS 10 | 11 | environment: 12 | GOPATH: /tmp/ 13 | clone_folder: /tmp/go/src/github.com/shawn1m/overture 14 | before_build: 15 | - "echo Building from macOS" 16 | - "go env" 17 | - "ls -al" 18 | - "python3 ./build.py -create-sample" 19 | - "python3 ./build.py -build-ios" 20 | 21 | - 22 | matrix: 23 | only: 24 | - image: Ubuntu 25 | 26 | init: 27 | environment: 28 | APPVEYOR_SSH_KEY: "" 29 | GOPATH: /usr/go/ 30 | ANDROID_NDK_ROOT: /tmp/android-ndk-r16b 31 | clone_folder: /usr/go/src/github.com/shawn1m/overture 32 | before_build: 33 | - "wget https://dl.google.com/android/repository/android-ndk-r16b-linux-x86_64.zip -O /tmp/android-ndk.zip" 34 | - "unzip -q /tmp/android-ndk.zip -d /tmp" 35 | - "python $ANDROID_NDK_ROOT/build/tools/make_standalone_toolchain.py --api=16 --install-dir=$ANDROID_NDK_ROOT/bin/arm-linux-androideabi/ --arch=arm" 36 | - "python $ANDROID_NDK_ROOT/build/tools/make_standalone_toolchain.py --api=21 --install-dir=$ANDROID_NDK_ROOT/bin/aarch64-linux-android/ --arch=arm64" 37 | - "python $ANDROID_NDK_ROOT/build/tools/make_standalone_toolchain.py --api=16 --install-dir=$ANDROID_NDK_ROOT/bin/i686-linux-android/ --arch=x86" 38 | - "python $ANDROID_NDK_ROOT/build/tools/make_standalone_toolchain.py --api=21 --install-dir=$ANDROID_NDK_ROOT/bin/x86_64-linux-android/ --arch=x86_64" 39 | - "python3 ./build.py -create-sample" 40 | - "python3 ./build.py -build-android" 41 | - "python3 ./build.py -build" 42 | on_finish: 43 | - "go test -cover ./... -race -coverprofile=coverage.txt -covermode=atomic" 44 | - "bash <(curl -s https://codecov.io/bash)" 45 | 46 | branches: 47 | only: 48 | - master 49 | 50 | artifacts: 51 | - path: 'overture-*.zip' 52 | 53 | deploy: 54 | description: 'Release description' 55 | provider: GitHub 56 | auth_token: 57 | secure: bkFCV3S/fgcuuZevFwUJicGIhL1s5aUM8ML2Tc1IHL9/CTWY5hi//KBqM+83knPM 58 | draft: false 59 | prerelease: true 60 | on: 61 | branch: master # release from master branch only 62 | APPVEYOR_REPO_TAG: true # deploy on tag push only -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import subprocess 4 | import sys 5 | 6 | GO_OS_ARCH_LIST = [ 7 | ["darwin", "amd64"], 8 | ["linux", "386"], 9 | ["linux", "amd64"], 10 | ["linux", "arm"], 11 | ["linux", "arm64"], 12 | ["linux", "mips", "softfloat"], 13 | ["linux", "mips", "hardfloat"], 14 | ["linux", "mipsle", "softfloat"], 15 | ["linux", "mipsle", "hardfloat"], 16 | ["linux", "mips64"], 17 | ["linux", "mips64le"], 18 | ["freebsd", "386"], 19 | ["freebsd", "amd64"], 20 | ["windows", "386"], 21 | ["windows", "amd64"] 22 | ] 23 | 24 | GO_IOS_ARCH_LIST = [ 25 | ["darwin", "arm64"], 26 | ["darwin", "arm"] 27 | ] 28 | 29 | GO_ANDROID_ARCH_LIST = [ 30 | ["android", "arm", "arm-linux-androideabi"], 31 | ["android", "arm64", "aarch64-linux-android"], 32 | ["android", "386", "i686-linux-android"], 33 | ["android", "amd64", "x86_64-linux-android"], 34 | ] 35 | 36 | 37 | def go_build_desktop(binary_name, version, o, a, p): 38 | mipsflag = (" GOMIPS=" + (p[0] if p else "") if p else "") 39 | subprocess.check_call("GOOS=" + o + " GOARCH=" + a + mipsflag + " CGO_ENABLED=0" + " go build -ldflags \"-s -w " + 40 | "-X main.version=" + version + "\" -o " + binary_name + " main/main.go", shell=True) 41 | 42 | def go_build_ios(binary_name, version, o, a, p): 43 | subprocess.check_call("CC=$(go env GOROOT)/misc/ios/clangwrap.sh GOOS=" + o + " GOARCH=" + a + " CGO_ENABLED=1" + " go build -ldflags \"-s -w " + 44 | "-X main.version=" + version + "\" -o " + binary_name + " main/main.go", shell=True) 45 | 46 | def go_build_android(binary_name, version, o, a, p): 47 | triple = p[0] 48 | subprocess.check_call("CC=$ANDROID_NDK_ROOT/bin/" + triple + "/bin/clang GOOS=" + o + " GOARCH=" + a + " CGO_ENABLED=1" + " go build -ldflags \"-s -w " + 49 | "-X main.version=" + version + "\" -o " + binary_name + " main/main.go", shell=True) 50 | 51 | def go_build_zip(arches, builder): 52 | subprocess.check_call("GOOS=windows go get -v github.com/shawn1m/overture/main", shell=True) 53 | for o, a, *p in arches: 54 | zip_name = "overture-" + o + "-" + a + ("-" + (p[0] if p else "") if p else "") 55 | binary_name = zip_name + (".exe" if o == "windows" else "") 56 | version = subprocess.check_output("git describe --tags", shell=True).decode() 57 | try: 58 | builder(binary_name, version, o, a, p) 59 | subprocess.check_call("zip " + zip_name + ".zip " + binary_name + " " + "hosts_sample " 60 | "ip_network_primary_sample " 61 | "ip_network_alternative_sample " 62 | "domain_primary_sample " 63 | "domain_alternative_sample " 64 | "domain_ttl_sample " 65 | "config.yml", shell=True) 66 | except subprocess.CalledProcessError: 67 | print(o + " " + a + " " + (p[0] if p else "") + " failed.") 68 | 69 | 70 | def create_sample_file(): 71 | with open("./hosts_sample", "w") as f: 72 | f.write("127.0.0.1 localhost") 73 | with open("./ip_network_primary_sample", "w") as f: 74 | f.write("127.0.0.9/32") 75 | with open("./ip_network_alternative_sample", "w") as f: 76 | f.write("127.0.0.10/32") 77 | with open("./domain_primary_sample", "w") as f: 78 | f.write("primary.example") 79 | with open("./domain_alternative_sample", "w") as f: 80 | f.write("alternative.example") 81 | with open("./domain_ttl_sample", "w") as f: 82 | f.write("ttl.example 1000") 83 | 84 | 85 | if __name__ == "__main__": 86 | 87 | subprocess.check_call("cp config.sample.yml config.yml", shell=True) 88 | 89 | if "-create-sample" in sys.argv: 90 | create_sample_file() 91 | 92 | if "-build" in sys.argv: 93 | go_build_zip(GO_OS_ARCH_LIST, go_build_desktop) 94 | 95 | if "-build-ios" in sys.argv: 96 | go_build_zip(GO_IOS_ARCH_LIST, go_build_ios) 97 | 98 | if "-build-android" in sys.argv: 99 | go_build_zip(GO_ANDROID_ARCH_LIST, go_build_android) 100 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 10% 7 | informational: true 8 | patch: off -------------------------------------------------------------------------------- /config.sample.yml: -------------------------------------------------------------------------------- 1 | bindAddress: :53 2 | debugHTTPAddress: 127.0.0.1:5555 3 | dohEnabled: false 4 | primaryDNS: 5 | - name: DNSPod 6 | address: 119.29.29.29:53 7 | protocol: udp 8 | socks5Address: 9 | timeout: 6 10 | ednsClientSubnet: 11 | policy: disable 12 | externalIP: 13 | noCookie: true 14 | alternativeDNS: 15 | - name: 114DNS 16 | address: 114.114.114.114:53 17 | protocol: udp 18 | socks5Address: 19 | timeout: 6 20 | ednsClientSubnet: 21 | policy: disable 22 | externalIP: 23 | noCookie: true 24 | onlyPrimaryDNS: false 25 | ipv6UseAlternativeDNS: false 26 | alternativeDNSConcurrent: false 27 | whenPrimaryDNSAnswerNoneUse: primaryDNS 28 | ipNetworkFile: 29 | primary: ./ip_network_primary_sample 30 | alternative: ./ip_network_alternative_sample 31 | domainFile: 32 | primary: ./domain_primary_sample 33 | alternative: ./domain_alternative_sample 34 | matcher: full-map 35 | hostsFile: 36 | hostsFile: ./hosts_sample 37 | finder: full-map 38 | minimumTTL: 0 39 | domainTTLFile: ./domain_ttl_sample 40 | cacheSize: 0 41 | cacheRedisUrl: 42 | cacheRedisConnectionPoolSize: 43 | rejectQType: 44 | - 255 -------------------------------------------------------------------------------- /config.test.yml: -------------------------------------------------------------------------------- 1 | bindAddress: :53 2 | debugHTTPAddress: 127.0.0.1:5555 3 | dohEnabled: true 4 | primaryDNS: 5 | - name: DNSPod 6 | address: 119.29.29.29:53 7 | protocol: udp 8 | socks5Address: 9 | timeout: 6 10 | ednsClientSubnet: 11 | policy: disable 12 | externalIP: 13 | noCookie: true 14 | alternativeDNS: 15 | - name: 114DNS 16 | address: 114.114.114.114:53 17 | protocol: udp 18 | socks5Address: 19 | timeout: 6 20 | ednsClientSubnet: 21 | policy: disable 22 | externalIP: 23 | noCookie: true 24 | onlyPrimaryDNS: false 25 | ipv6UseAlternativeDNS: false 26 | alternativeDNSConcurrent: false 27 | whenPrimaryDNSAnswerNoneUse: primaryDNS 28 | ipNetworkFile: 29 | primary: ./ip_network_primary_sample 30 | alternative: ./ip_network_alternative_sample 31 | domainFile: 32 | primary: ./domain_primary_sample 33 | alternative: ./domain_alternative_sample 34 | matcher: full-map 35 | hostsFile: 36 | hostsFile: ./hosts_sample 37 | finder: full-map 38 | minimumTTL: 86400 39 | domainTTLFile: ./domain_ttl_sample 40 | cacheSize: 10000 41 | cacheRedisUrl: 42 | cacheRedisConnectionPoolSize: 43 | rejectQType: 44 | - 255 -------------------------------------------------------------------------------- /core/cache/cache.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. 2 | // Use of this source code is governed by The MIT License (MIT) that can be 3 | // found in the LICENSE file. 4 | 5 | // Package cache implements dns cache feature with edns-client-subnet support. 6 | package cache 7 | 8 | // Cache that holds RRs. 9 | 10 | import ( 11 | "context" 12 | "encoding/json" 13 | "fmt" 14 | "sync" 15 | "time" 16 | 17 | "github.com/go-redis/redis/v8" 18 | "github.com/miekg/dns" 19 | log "github.com/sirupsen/logrus" 20 | ) 21 | 22 | // Elem hold an answer and additional section that returned from the cache. 23 | // The signature is put in answer, extra is empty there. This wastes some memory. 24 | type elem struct { 25 | expiration time.Time // time added + TTL, after this the elem is invalid 26 | msg *dns.Msg 27 | } 28 | 29 | type elemData struct { 30 | Expiration time.Time 31 | Msg []byte // dns.Msg cannot be converted to the json format successfully thus using its pack() method instead 32 | } 33 | 34 | func (e *elem) MarshalBinary() (data []byte, err error) { 35 | msgBytes, _ := e.msg.Pack() 36 | ed := elemData{e.expiration, msgBytes} 37 | return json.Marshal(ed) 38 | } 39 | 40 | func (e *elem) UnmarshalBinary(data []byte) error { 41 | var ed elemData 42 | err := json.Unmarshal(data, &ed) 43 | if err != nil { 44 | return err 45 | } 46 | e.expiration = ed.Expiration 47 | e.msg = &dns.Msg{} 48 | return e.msg.Unpack(ed.Msg) 49 | } 50 | 51 | // Cache is a cache that holds on the a number of RRs or DNS messages. The cache 52 | // eviction is randomized. 53 | type Cache struct { 54 | sync.RWMutex 55 | 56 | capacity int 57 | table map[string]*elem 58 | redisClient *redis.Client 59 | } 60 | 61 | // New returns a new cache with the capacity and the ttl specified. 62 | func New(capacity int, redisUrl string, cacheRedisConnectionPoolSize int) *Cache { 63 | if capacity <= 0 { 64 | return nil 65 | } 66 | c := new(Cache) 67 | c.table = make(map[string]*elem) 68 | c.capacity = capacity 69 | 70 | opt, err := redis.ParseURL(redisUrl) 71 | if err != nil { 72 | if redisUrl != "" { 73 | log.Error("redisUrl error ", redisUrl, err) 74 | } 75 | } else { 76 | if cacheRedisConnectionPoolSize > 0 { 77 | opt.PoolSize = cacheRedisConnectionPoolSize 78 | } else { 79 | log.Warn("cacheRedisConnectionPoolSize is ignored", cacheRedisConnectionPoolSize) 80 | } 81 | c.redisClient = redis.NewClient(opt) 82 | log.Info("Cache redis connected! ", c.redisClient.String()) 83 | } 84 | 85 | return c 86 | } 87 | 88 | func (c *Cache) Capacity() int { return c.capacity } 89 | 90 | func (c *Cache) Remove(s string) { 91 | if c.redisClient != nil { 92 | return 93 | } 94 | c.Lock() 95 | delete(c.table, s) 96 | c.Unlock() 97 | } 98 | 99 | // EvictRandom removes a random member a the cache. 100 | // Must be called under a write lock. 101 | func (c *Cache) EvictRandom() { 102 | cacheLength := len(c.table) 103 | if cacheLength <= c.capacity { 104 | return 105 | } 106 | i := c.capacity - cacheLength 107 | for k := range c.table { 108 | delete(c.table, k) 109 | i-- 110 | if i == 0 { 111 | break 112 | } 113 | } 114 | } 115 | 116 | // InsertMessage inserts a message in the Cache. We will cache it for ttl seconds, which 117 | // should be a small (60...300) integer. 118 | func (c *Cache) InsertMessage(s string, m *dns.Msg, mTTL uint32) { 119 | if c.capacity <= 0 || m == nil { 120 | return 121 | } 122 | var err error 123 | if c.redisClient == nil { 124 | c.InsertMessageToLocal(s, m, mTTL) 125 | } else { 126 | err = c.InsertMessageToRedis(s, m, mTTL) 127 | } 128 | if err != nil { 129 | log.Warn("Insert cache failed", s, err) 130 | } else { 131 | log.Debugf("Cached: %s", s) 132 | } 133 | } 134 | 135 | func (c *Cache) InsertMessageToRedis(s string, m *dns.Msg, mTTL uint32) error { 136 | 137 | ttlDuration := convertToTTLDuration(m, mTTL) 138 | if _, ok := c.table[s]; !ok { 139 | e := &elem{time.Now().Add(ttlDuration), m.Copy()} 140 | cmd := c.redisClient.Set(context.TODO(), s, e, ttlDuration) 141 | if cmd.Err() != nil { 142 | log.Warn("Redis set for cache failed!", cmd.Err()) 143 | return cmd.Err() 144 | } 145 | } 146 | return nil 147 | 148 | } 149 | func (c *Cache) InsertMessageToLocal(s string, m *dns.Msg, mTTL uint32) { 150 | 151 | c.Lock() 152 | ttlDuration := convertToTTLDuration(m, mTTL) 153 | if _, ok := c.table[s]; !ok { 154 | e := &elem{time.Now().Add(ttlDuration), m.Copy()} 155 | c.table[s] = e 156 | } 157 | 158 | c.EvictRandom() 159 | c.Unlock() 160 | } 161 | 162 | func convertToTTLDuration(m *dns.Msg, mTTL uint32) time.Duration { 163 | var ttl uint32 164 | if len(m.Answer) == 0 { 165 | ttl = mTTL 166 | } else { 167 | ttl = m.Answer[0].Header().Ttl 168 | } 169 | return time.Duration(ttl) * time.Second 170 | } 171 | 172 | // Search returns a dns.Msg, the expiration time and a boolean indicating if we found something 173 | // in the cache. 174 | func (c *Cache) Search(s string) (*dns.Msg, time.Time, bool) { 175 | if c.capacity <= 0 { 176 | return nil, time.Time{}, false 177 | } 178 | if c.redisClient == nil { 179 | return c.SearchFromLocal(s) 180 | } else { 181 | return c.SearchFromRedis(s) 182 | } 183 | } 184 | 185 | func (c *Cache) SearchFromRedis(s string) (*dns.Msg, time.Time, bool) { 186 | var e elem 187 | err := c.redisClient.Get(context.TODO(), s).Scan(&e) 188 | if err != nil { 189 | if err.Error() == "redis: nil" { 190 | log.Debug("Redis get return nil for ", s, err) 191 | } else { 192 | log.Warn("Redis get return nil for ", s, err) 193 | } 194 | return nil, time.Time{}, false 195 | } 196 | return e.msg, e.expiration, true 197 | 198 | } 199 | 200 | // todo: use finder implementation 201 | func (c *Cache) SearchFromLocal(s string) (*dns.Msg, time.Time, bool) { 202 | c.RLock() 203 | if e, ok := c.table[s]; ok { 204 | e1 := e.msg.Copy() 205 | c.RUnlock() 206 | return e1, e.expiration, true 207 | } 208 | c.RUnlock() 209 | return nil, time.Time{}, false 210 | } 211 | 212 | // Key creates a hash key from a question section. 213 | func Key(q dns.Question, ednsIP string) string { 214 | return fmt.Sprintf("%s %d %s", q.Name, q.Qtype, ednsIP) 215 | } 216 | 217 | // Hit returns a dns message from the cache. If the message's TTL is expired, nil 218 | // will be returned and the message is removed from the cache. 219 | func (c *Cache) Hit(key string, msgid uint16) *dns.Msg { 220 | m, exp, hit := c.Search(key) 221 | if hit { 222 | // Cache hit! \o/ 223 | if time.Since(exp) < 0 { 224 | m.Id = msgid 225 | m.Compress = true 226 | // Even if something ended up with the TC bit *in* the cache, set it to off 227 | m.Truncated = false 228 | for _, a := range m.Answer { 229 | a.Header().Ttl = uint32(time.Since(exp).Seconds() * -1) 230 | } 231 | return m 232 | } 233 | // Expired! /o\ 234 | c.Remove(key) 235 | } 236 | return nil 237 | } 238 | 239 | // Dump returns all local dns cache information for debugging 240 | func (c *Cache) Dump(nobody bool) (rs map[string][]string, l int) { 241 | if c.capacity <= 0 { 242 | return 243 | } 244 | 245 | l = len(c.table) 246 | 247 | rs = make(map[string][]string) 248 | 249 | if nobody { 250 | return 251 | } 252 | 253 | c.RLock() 254 | defer c.RUnlock() 255 | 256 | for k, e := range c.table { 257 | var vs []string 258 | 259 | for _, a := range e.msg.Answer { 260 | vs = append(vs, a.String()) 261 | } 262 | rs[k] = vs 263 | } 264 | return 265 | } 266 | -------------------------------------------------------------------------------- /core/common/common.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 shawn1m. All rights reserved. 2 | // Use of this source code is governed by The MIT License (MIT) that can be 3 | // found in the LICENSE file. 4 | 5 | // Package common provides common functions. 6 | package common 7 | 8 | import ( 9 | "net" 10 | "regexp" 11 | "strings" 12 | 13 | "github.com/miekg/dns" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var ReservedIPNetworkList = getReservedIPNetworkList() 18 | 19 | func IsDomainMatchRule(pattern string, domain string) bool { 20 | matched, err := regexp.MatchString(pattern, domain) 21 | if err != nil { 22 | log.Warnf("Error matching domain %s with pattern %s: %s", domain, pattern, err) 23 | } 24 | return matched 25 | } 26 | 27 | func HasAnswer(m *dns.Msg) bool { return m != nil && len(m.Answer) != 0 } 28 | 29 | func HasSubDomain(s string, sub string) bool { 30 | return strings.HasSuffix(sub, "."+s) || s == sub 31 | } 32 | 33 | func getReservedIPNetworkList() *IPSet { 34 | var ipNetList []*net.IPNet 35 | localCIDR := []string{"127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "100.64.0.0/10"} 36 | for _, c := range localCIDR { 37 | _, ipNet, err := net.ParseCIDR(c) 38 | if err != nil { 39 | break 40 | } 41 | ipNetList = append(ipNetList, ipNet) 42 | } 43 | return NewIPSet(ipNetList) 44 | } 45 | 46 | func FindRecordByType(msg *dns.Msg, t uint16) string { 47 | if msg == nil { 48 | return "" 49 | } 50 | for _, rr := range msg.Answer { 51 | if rr.Header().Rrtype == t { 52 | items := strings.SplitN(rr.String(), "\t", 5) 53 | return items[4] 54 | } 55 | } 56 | 57 | return "" 58 | } 59 | 60 | func SetMinimumTTL(msg *dns.Msg, minimumTTL uint32) { 61 | if minimumTTL == 0 { 62 | return 63 | } 64 | for _, a := range msg.Answer { 65 | if a.Header().Ttl < minimumTTL { 66 | a.Header().Ttl = minimumTTL 67 | } 68 | } 69 | } 70 | 71 | func SetTTLByMap(msg *dns.Msg, domainTTLMap map[string]uint32) { 72 | if len(domainTTLMap) == 0 { 73 | return 74 | } 75 | for _, a := range msg.Answer { 76 | name := a.Header().Name[:len(a.Header().Name)-1] 77 | for k, v := range domainTTLMap { 78 | if IsDomainMatchRule(k, name) { 79 | a.Header().Ttl = v 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /core/common/edns.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | type EDNSClientSubnetType struct { 10 | Policy string `yaml:"policy" json:"policy"` 11 | ExternalIP string `yaml:"externalIP" json:"externalIP"` 12 | NoCookie bool `yaml:"noCookie"json:"noCookie"` 13 | } 14 | 15 | func SetEDNSClientSubnet(m *dns.Msg, ip string, isNoCookie bool) { 16 | if ip == "" { 17 | return 18 | } 19 | 20 | o := m.IsEdns0() 21 | if o == nil { 22 | o = new(dns.OPT) 23 | o.SetUDPSize(4096) 24 | o.Hdr.Name = "." 25 | o.Hdr.Rrtype = dns.TypeOPT 26 | m.Extra = append(m.Extra, o) 27 | } 28 | 29 | es := IsEDNSClientSubnet(o) 30 | if es == nil || es.Address.IsUnspecified() { 31 | nes := new(dns.EDNS0_SUBNET) 32 | nes.Code = dns.EDNS0SUBNET 33 | nes.Address = net.ParseIP(ip) 34 | if nes.Address.To4() != nil { 35 | nes.Family = 1 // 1 for IPv4 source address, 2 for IPv6 36 | nes.SourceNetmask = 24 // 24 for IPV4, 56 for IPv6 37 | } else { 38 | nes.Family = 2 // 1 for IPv4 source address, 2 for IPv6 39 | nes.SourceNetmask = 56 // 24 for IPV4, 56 for IPv6 40 | } 41 | nes.SourceScope = 0 42 | if es != nil && es.Address.IsUnspecified() { 43 | var edns0 []dns.EDNS0 44 | for _, s := range o.Option { 45 | switch e := s.(type) { 46 | case *dns.EDNS0_SUBNET: 47 | default: 48 | edns0 = append(edns0, e) 49 | } 50 | } 51 | o.Option = edns0 52 | } 53 | o.Option = append(o.Option, nes) 54 | if isNoCookie { 55 | deleteCookie(o) 56 | } 57 | } 58 | } 59 | 60 | func deleteCookie(o *dns.OPT) { 61 | for i, e0 := range o.Option { 62 | switch e0.(type) { 63 | case *dns.EDNS0_COOKIE: 64 | o.Option = append(o.Option[:i], o.Option[i+1:]...) 65 | } 66 | } 67 | } 68 | 69 | func IsEDNSClientSubnet(o *dns.OPT) *dns.EDNS0_SUBNET { 70 | for _, s := range o.Option { 71 | switch e := s.(type) { 72 | case *dns.EDNS0_SUBNET: 73 | return e 74 | } 75 | } 76 | return nil 77 | } 78 | 79 | func GetEDNSClientSubnetIP(m *dns.Msg) string { 80 | o := m.IsEdns0() 81 | if o != nil { 82 | for _, s := range o.Option { 83 | switch e := s.(type) { 84 | case *dns.EDNS0_SUBNET: 85 | return e.Address.String() 86 | } 87 | } 88 | } 89 | return "" 90 | } 91 | -------------------------------------------------------------------------------- /core/common/ipset.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bytes" 5 | log "github.com/sirupsen/logrus" 6 | "net" 7 | "sort" 8 | ) 9 | 10 | type ipRange struct { 11 | start net.IP 12 | end net.IP 13 | } 14 | 15 | type ipRanges []*ipRange 16 | 17 | func (s ipRanges) Len() int { return len(s) } 18 | func (s ipRanges) Swap(i, j int) { 19 | s[i], s[j] = s[j], s[i] 20 | } 21 | func (s ipRanges) Less(i, j int) bool { 22 | return bytes.Compare(s[i].start, s[j].start) < 0 23 | } 24 | 25 | type IPSet struct { 26 | ipv4 ipRanges 27 | ipv6 ipRanges 28 | } 29 | 30 | func (s ipRanges) contains(ip net.IP) bool { 31 | l, r := 0, len(s)-1 32 | for l <= r { 33 | mid := (l + r) / 2 34 | if bytes.Compare(ip, s[mid].start) < 0 { 35 | r = mid - 1 36 | } else { 37 | l = mid + 1 38 | } 39 | } 40 | return r >= 0 && bytes.Compare(s[r].start, ip) <= 0 && bytes.Compare(ip, s[r].end) <= 0 41 | } 42 | 43 | func (ipSet *IPSet) Contains(ip net.IP, isLog bool, name string) bool { 44 | result := false 45 | if ipSet != nil { 46 | if ipv4 := ip.To4(); ipv4 != nil { 47 | result = ipSet.ipv4.contains(ipv4) 48 | } else if ipv6 := ip.To16(); ipv6 != nil { 49 | result = ipSet.ipv6.contains(ipv6) 50 | } 51 | if result && isLog { 52 | log.Debugf("Matched: IP network %s %s", name, ip.String()) 53 | } 54 | } else { 55 | log.Debug("IP network list is nil, not checking") 56 | } 57 | return result 58 | } 59 | 60 | func toRange(ip net.IP, mask net.IPMask) *ipRange { 61 | // assert len(ip) == len(mask) 62 | ipLen := len(ip) 63 | start, end := make(net.IP, ipLen), make(net.IP, ipLen) 64 | for i := 0; i < ipLen; i++ { 65 | start[i] = ip[i] & mask[i] 66 | end[i] = ip[i] | ^mask[i] 67 | } 68 | return &ipRange{start, end} 69 | } 70 | 71 | func allFF(ip []byte) bool { 72 | for _, c := range ip { 73 | if c != 0xff { 74 | return false 75 | } 76 | } 77 | return true 78 | } 79 | 80 | func addOne(ip net.IP) net.IP { 81 | ipLen := len(ip) 82 | to := make(net.IP, ipLen) 83 | var carry uint = 1 84 | for i := ipLen - 1; i >= 0; i-- { 85 | carry += uint(ip[i]) 86 | to[i] = byte(carry) 87 | carry >>= 8 88 | } 89 | return to 90 | } 91 | 92 | func sortAndMerge(rr ipRanges) ipRanges { 93 | if len(rr) < 2 { 94 | return rr 95 | } 96 | sort.Sort(rr) 97 | 98 | res := make(ipRanges, 0, len(rr)) 99 | now := rr[0] 100 | start, end := now.start, now.end 101 | for i, count := 1, len(rr); i < count; i++ { 102 | now := rr[i] 103 | if allFF(end) || bytes.Compare(addOne(end), now.start) >= 0 { 104 | if bytes.Compare(end, now.end) < 0 { 105 | end = now.end 106 | } 107 | } else { 108 | res = append(res, &ipRange{start, end}) 109 | start, end = now.start, now.end 110 | } 111 | } 112 | return append(res, &ipRange{start, end}) 113 | } 114 | 115 | func NewIPSet(ipNetList []*net.IPNet) *IPSet { 116 | result := &IPSet{} 117 | for _, ipNet := range ipNetList { 118 | ip, mask := ipNet.IP, ipNet.Mask 119 | if ipv4 := ip.To4(); ipv4 != nil { 120 | ip = ipv4 121 | } 122 | if len(ip) == net.IPv4len && len(mask) == net.IPv6len && allFF(mask[:12]) { 123 | mask = mask[12:] 124 | } 125 | if lenIp := len(ip); lenIp == len(mask) { 126 | r := toRange(ip, mask) 127 | switch lenIp { 128 | case net.IPv4len: 129 | result.ipv4 = append(result.ipv4, r) 130 | case net.IPv6len: 131 | result.ipv6 = append(result.ipv6, r) 132 | default: 133 | // invalid ip length, should not happen 134 | } 135 | } else { 136 | // invalid IPNet, should not happen 137 | } 138 | } 139 | if result.ipv4 == nil && result.ipv6 == nil { 140 | return nil 141 | } 142 | result.ipv6 = sortAndMerge(result.ipv6) 143 | result.ipv4 = sortAndMerge(result.ipv4) 144 | return result 145 | } 146 | -------------------------------------------------------------------------------- /core/common/ipset_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | ) 7 | 8 | func contains(ipNets []*net.IPNet, ip net.IP) bool { 9 | for _, ipNet := range ipNets { 10 | if ipNet.Contains(ip) { 11 | return true 12 | } 13 | } 14 | return false 15 | } 16 | 17 | func TestIPSet(t *testing.T) { 18 | var ipNetList []*net.IPNet 19 | for _, s := range []string{ 20 | "::ffff:192.168.1.0/120", 21 | "192.168.2.0/24", 22 | "192.168.4.0/24", 23 | "::fffe:0:0/95", // a range covers all IPv4-mapped Address 24 | "fe80::1234:5678:9000/120", 25 | } { 26 | _, ipNet, _ := net.ParseCIDR(s) 27 | ipNetList = append(ipNetList, ipNet) 28 | } 29 | ipSet := NewIPSet(ipNetList) 30 | 31 | for _, s := range []string{ 32 | "192.168.3.0", 33 | "192.168.2.1", 34 | "::ffff:192.168.2.1", 35 | "192.168.1.1", 36 | "fe80::1234:5678:8fff", 37 | "fe80::1234:5678:9000", 38 | "fe80::1234:5678:90ff", 39 | "fe80::1234:5678:9100", 40 | "invalid ip", 41 | } { 42 | ip := net.ParseIP(s) 43 | expect := contains(ipNetList, ip) 44 | result := ipSet.Contains(ip, true, s) 45 | if expect != result { 46 | t.Errorf("expect %v, but got %v: '%v'", expect, result, s) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /core/common/upstream.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | type DNSUpstream struct { 4 | Name string `yaml:"name" json:"name"` 5 | Address string `yaml:"address" json:"address"` 6 | Protocol string `yaml:"protocol" json:"protocol"` 7 | SOCKS5Address string `yaml:"socks5Address" json:"socks5Address"` 8 | Timeout int `yaml:"timeout" json:"timeout"` 9 | EDNSClientSubnet *EDNSClientSubnetType `yaml:"ednsClientSubnet" json:"ednsClientSubnet"` 10 | TCPPoolConfig struct { 11 | Enable bool `yaml:"enable" json:"enable"` 12 | InitialCapacity int `yaml:"initialCapacity" json:"initialCapacity"` 13 | MaxCapacity int `yaml:"maxCapacity" json:"maxCapacity"` 14 | IdleTimeout int `yaml:"idleTimeout" json:"idleTimeout"` 15 | } `yaml:"tcpPoolConfig" json:"tcpPoolConfig"` 16 | } 17 | -------------------------------------------------------------------------------- /core/config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 shawn1m. All rights reserved. 2 | // Use of this source code is governed by The MIT License (MIT) that can be 3 | // found in the LICENSE file. 4 | 5 | package config 6 | 7 | import ( 8 | "bufio" 9 | "encoding/json" 10 | "io" 11 | "io/ioutil" 12 | "net" 13 | "os" 14 | "strconv" 15 | "strings" 16 | 17 | "github.com/shawn1m/overture/core/cache" 18 | "github.com/shawn1m/overture/core/common" 19 | "github.com/shawn1m/overture/core/finder" 20 | finderfull "github.com/shawn1m/overture/core/finder/full" 21 | finderregex "github.com/shawn1m/overture/core/finder/regex" 22 | "github.com/shawn1m/overture/core/hosts" 23 | "github.com/shawn1m/overture/core/matcher" 24 | matcherfinal "github.com/shawn1m/overture/core/matcher/final" 25 | matcherfull "github.com/shawn1m/overture/core/matcher/full" 26 | matchermix "github.com/shawn1m/overture/core/matcher/mix" 27 | matcherregex "github.com/shawn1m/overture/core/matcher/regex" 28 | matchersuffix "github.com/shawn1m/overture/core/matcher/suffix" 29 | log "github.com/sirupsen/logrus" 30 | "gopkg.in/yaml.v2" 31 | ) 32 | 33 | type Config struct { 34 | FilePath string `yaml:"-" json:"-"` 35 | BindAddress string `yaml:"bindAddress" json:"bindAddress"` 36 | DebugHTTPAddress string `yaml:"debugHTTPAddress" json:"debugHTTPAddress"` 37 | DohEnabled bool `yaml:"dohEnabled" json:"dohEnabled"` 38 | PrimaryDNS []*common.DNSUpstream `yaml:"primaryDNS" json:"primaryDNS"` 39 | AlternativeDNS []*common.DNSUpstream `yaml:"alternativeDNS" json:"alternativeDNS"` 40 | OnlyPrimaryDNS bool `yaml:"onlyPrimaryDNS" json:"onlyPrimaryDNS"` 41 | IPv6UseAlternativeDNS bool `yaml:"ipv6UseAlternativeDNS" json:"ipv6UseAlternativeDNS"` 42 | AlternativeDNSConcurrent bool `yaml:"alternativeDNSConcurrent" json:"alternativeDNSConcurrent"` 43 | WhenPrimaryDNSAnswerNoneUse string `yaml:"whenPrimaryDNSAnswerNoneUse" json:"whenPrimaryDNSAnswerNoneUse"` 44 | IPNetworkFile struct { 45 | Primary string `yaml:"primary" json:"primary"` 46 | Alternative string `yaml:"alternative" json:"alternative"` 47 | } `yaml:"ipNetworkFile" json:"ipNetworkFile"` 48 | DomainFile struct { 49 | Primary string `yaml:"primary" json:"primary"` 50 | Alternative string `yaml:"alternative" json:"alternative"` 51 | PrimaryMatcher string `yaml:"primaryMatcher" json:"primaryMatcher"` 52 | AlternativeMatcher string `yaml:"alternativeMatcher" json:"alternativeMatcher"` 53 | Matcher string `yaml:"matcher" json:"matcher"` 54 | } `yaml:"domainFile" json:"domainFile"` 55 | HostsFile struct { 56 | HostsFile string `yaml:"hostsFile" json:"hostsFile"` 57 | Finder string `yaml:"finder" json:"finder"` 58 | } `yaml:"hostsFile" json:"hostsFile"` 59 | MinimumTTL int `yaml:"minimumTTL" json:"minimumTTL"` 60 | DomainTTLFile string `yaml:"domainTTLFile" json:"domainTTLFile"` 61 | CacheSize int `yaml:"cacheSize" json:"cacheSize"` 62 | CacheRedisUrl string `yaml:"cacheRedisUrl" json:"cacheRedisUrl"` 63 | CacheRedisConnectionPoolSize int `yaml:"cacheRedisConnectionPoolSize" json:"cacheRedisConnectionPoolSize"` 64 | RejectQType []uint16 `yaml:"rejectQType" json:"rejectQType"` 65 | 66 | DomainTTLMap map[string]uint32 `yaml:"-" json:"-"` 67 | DomainPrimaryList matcher.Matcher `yaml:"-" json:"-"` 68 | DomainAlternativeList matcher.Matcher `yaml:"-" json:"-"` 69 | IPNetworkPrimarySet *common.IPSet `yaml:"-" json:"-"` 70 | IPNetworkAlternativeSet *common.IPSet `yaml:"-" json:"-"` 71 | Hosts *hosts.Hosts `yaml:"-" json:"-"` 72 | Cache *cache.Cache `yaml:"-" json:"-"` 73 | } 74 | 75 | // New config with config file and do some other initiate works 76 | func NewConfig(configFile string) *Config { 77 | config := parseConfigFile(configFile) 78 | config.FilePath = configFile 79 | 80 | config.DomainTTLMap = getDomainTTLMap(config.DomainTTLFile) 81 | 82 | config.DomainPrimaryList = initDomainMatcher(config.DomainFile.Primary, config.DomainFile.PrimaryMatcher, config.DomainFile.Matcher) 83 | config.DomainAlternativeList = initDomainMatcher(config.DomainFile.Alternative, config.DomainFile.AlternativeMatcher, config.DomainFile.Matcher) 84 | 85 | config.IPNetworkPrimarySet = getIPNetworkSet(config.IPNetworkFile.Primary) 86 | config.IPNetworkAlternativeSet = getIPNetworkSet(config.IPNetworkFile.Alternative) 87 | 88 | if config.MinimumTTL > 0 { 89 | log.Infof("Minimum TTL has been set to %d", config.MinimumTTL) 90 | } else { 91 | log.Info("Minimum TTL is disabled") 92 | } 93 | 94 | config.Cache = cache.New(config.CacheSize, config.CacheRedisUrl, config.CacheRedisConnectionPoolSize) 95 | if config.CacheSize > 0 { 96 | log.Infof("CacheSize is %d", config.CacheSize) 97 | } else { 98 | log.Info("Cache is disabled") 99 | } 100 | 101 | h, err := hosts.New(config.HostsFile.HostsFile, getFinder(config.HostsFile.Finder)) 102 | if err != nil { 103 | log.Warnf("Failed to load hosts file: %s", err) 104 | } else { 105 | config.Hosts = h 106 | log.Info("Hosts file has been loaded successfully") 107 | } 108 | 109 | return config 110 | } 111 | 112 | func parseConfigFile(path string) *Config { 113 | b, err := ioutil.ReadFile(path) 114 | if err != nil { 115 | log.Fatalf("Failed to read config file: %s", err) 116 | os.Exit(1) 117 | } 118 | 119 | config := new(Config) 120 | if strings.HasSuffix(path, "json") { 121 | err = json.Unmarshal(b, config) 122 | } else { 123 | err = yaml.Unmarshal(b, config) 124 | } 125 | 126 | if err != nil { 127 | log.Fatalf("Failed to parse config file: %s", err) 128 | os.Exit(1) 129 | } 130 | 131 | return config 132 | } 133 | 134 | func getDomainTTLMap(file string) map[string]uint32 { 135 | if file == "" { 136 | return map[string]uint32{} 137 | } 138 | 139 | f, err := os.Open(file) 140 | if err != nil { 141 | log.Errorf("Failed to open domain TTL file %s: %s", file, err) 142 | return nil 143 | } 144 | defer f.Close() 145 | 146 | successes := 0 147 | failures := 0 148 | var failedLines []string 149 | 150 | dtl := map[string]uint32{} 151 | 152 | scanner := bufio.NewScanner(f) 153 | 154 | for scanner.Scan() { 155 | line := scanner.Text() 156 | if len(line) == 0 { 157 | continue 158 | } 159 | words := strings.Fields(line) 160 | if len(words) > 1 { 161 | tempInt64, err := strconv.ParseUint(words[1], 10, 32) 162 | dtl[words[0]] = uint32(tempInt64) 163 | if err != nil { 164 | log.WithFields(log.Fields{"domain": words[0], "ttl": words[1]}).Warnf("Invalid TTL for domain %s: %s", words[0], words[1]) 165 | failures++ 166 | failedLines = append(failedLines, line) 167 | } 168 | successes++ 169 | } else { 170 | failedLines = append(failedLines, line) 171 | failures++ 172 | } 173 | if line == "" && err == io.EOF { 174 | log.Debugf("Reading domain TTL file %s reached EOF", file) 175 | break 176 | } 177 | } 178 | 179 | if len(dtl) > 0 { 180 | log.Infof("Domain TTL file %s has been loaded with %d records (%d failed)", file, successes, failures) 181 | if len(failedLines) > 0 { 182 | log.Debugf("Failed lines (%s):", file) 183 | for _, line := range failedLines { 184 | log.Debug(line) 185 | } 186 | } 187 | } else { 188 | log.Warnf("No element has been loaded from domain TTL file: %s", file) 189 | if len(failedLines) > 0 { 190 | log.Debugf("Failed lines (%s):", file) 191 | for _, line := range failedLines { 192 | log.Debug(line) 193 | } 194 | } 195 | } 196 | 197 | return dtl 198 | } 199 | 200 | func getDomainMatcher(name string) (m matcher.Matcher) { 201 | switch name { 202 | case "suffix-tree": 203 | return matchersuffix.DefaultDomainTree() 204 | case "full-map": 205 | return &matcherfull.Map{DataMap: make(map[string]struct{}, 100)} 206 | case "full-list": 207 | return &matcherfull.List{} 208 | case "regex-list": 209 | return &matcherregex.List{} 210 | case "mix-list": 211 | return &matchermix.List{} 212 | case "final": 213 | return &matcherfinal.Default{} 214 | default: 215 | log.Warnf("Matcher %s does not exist, using full-map matcher as default", name) 216 | return &matcherfull.Map{DataMap: make(map[string]struct{}, 100)} 217 | } 218 | } 219 | 220 | func getFinder(name string) (f finder.Finder) { 221 | switch name { 222 | case "regex-list": 223 | return &finderregex.List{RegexMap: make(map[string][]string, 100)} 224 | case "full-map": 225 | return &finderfull.Map{DataMap: make(map[string][]string, 100)} 226 | default: 227 | log.Warnf("Finder %s does not exist, using full-map finder as default", name) 228 | return &finderfull.Map{DataMap: make(map[string][]string, 100)} 229 | } 230 | } 231 | 232 | func initDomainMatcher(file string, name string, defaultName string) (m matcher.Matcher) { 233 | if name == "" { 234 | name = defaultName 235 | } 236 | m = getDomainMatcher(name) 237 | if name == "final" { 238 | return m 239 | } 240 | if file == "" { 241 | return 242 | } 243 | 244 | f, err := os.Open(file) 245 | if err != nil { 246 | log.Errorf("Failed to open domain file %s: %s", file, err) 247 | return nil 248 | } 249 | defer f.Close() 250 | 251 | lines := 0 252 | scanner := bufio.NewScanner(f) 253 | for scanner.Scan() { 254 | line := scanner.Text() 255 | if len(line) == 0 { 256 | continue 257 | } 258 | line = strings.TrimSpace(line) 259 | if line != "" { 260 | _ = m.Insert(line) 261 | lines++ 262 | } 263 | if line == "" && err == io.EOF { 264 | log.Debugf("Reading domain file %s reached EOF", file) 265 | break 266 | } 267 | } 268 | 269 | if lines > 0 { 270 | log.Infof("Domain file %s has been loaded with %d records (%s)", file, lines, m.Name()) 271 | } else { 272 | log.Warnf("No element has been loaded from domain file: %s", file) 273 | } 274 | 275 | return 276 | } 277 | 278 | func getIPNetworkSet(file string) *common.IPSet { 279 | ipNetList := make([]*net.IPNet, 0) 280 | 281 | f, err := os.Open(file) 282 | if err != nil { 283 | log.Errorf("Failed to open IP network file: %s", err) 284 | return nil 285 | } 286 | defer f.Close() 287 | 288 | successes := 0 289 | failures := 0 290 | var failedLines []string 291 | 292 | scanner := bufio.NewScanner(f) 293 | for scanner.Scan() { 294 | line := scanner.Text() 295 | if len(line) == 0 { 296 | continue 297 | } 298 | _, ipNet, err := net.ParseCIDR(strings.TrimSuffix(line, "\n")) 299 | if err != nil { 300 | log.Errorf("Error parsing IP network CIDR %s: %s", line, err) 301 | failures++ 302 | failedLines = append(failedLines, line) 303 | continue 304 | } 305 | ipNetList = append(ipNetList, ipNet) 306 | successes++ 307 | } 308 | if len(ipNetList) > 0 { 309 | log.Infof("IP network file %s has been loaded with %d records", file, successes) 310 | if failures > 0 { 311 | log.Debugf("Failed lines (%s):", file) 312 | for _, line := range failedLines { 313 | log.Debug(line) 314 | } 315 | } 316 | } else { 317 | log.Warnf("No element has been loaded from IP network file: %s", file) 318 | if failures > 0 { 319 | log.Debugf("Failed lines (%s):", file) 320 | for _, line := range failedLines { 321 | log.Debug(line) 322 | } 323 | } 324 | } 325 | 326 | return common.NewIPSet(ipNetList) 327 | } 328 | -------------------------------------------------------------------------------- /core/control.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 shawn1m. All rights reserved. 2 | // Use of this source code is governed by The MIT License (MIT) that can be 3 | // found in the LICENSE file. 4 | 5 | // Package core implements the essential features. 6 | package core 7 | 8 | import ( 9 | "encoding/json" 10 | "io" 11 | "io/ioutil" 12 | "net/http" 13 | "time" 14 | 15 | "github.com/shawn1m/overture/core/config" 16 | "github.com/shawn1m/overture/core/inbound" 17 | "github.com/shawn1m/overture/core/outbound" 18 | log "github.com/sirupsen/logrus" 19 | ) 20 | 21 | var ( 22 | srv *inbound.Server 23 | conf *config.Config 24 | ) 25 | 26 | // Initiate the server with config file 27 | func InitServer(configFilePath string) { 28 | conf = config.NewConfig(configFilePath) 29 | Start() 30 | } 31 | 32 | func Start() { 33 | // New dispatcher without RemoteClientBundle, RemoteClientBundle must be initiated when server is running 34 | dispatcher := outbound.Dispatcher{ 35 | PrimaryDNS: conf.PrimaryDNS, 36 | AlternativeDNS: conf.AlternativeDNS, 37 | OnlyPrimaryDNS: conf.OnlyPrimaryDNS, 38 | WhenPrimaryDNSAnswerNoneUse: conf.WhenPrimaryDNSAnswerNoneUse, 39 | IPNetworkPrimarySet: conf.IPNetworkPrimarySet, 40 | IPNetworkAlternativeSet: conf.IPNetworkAlternativeSet, 41 | DomainPrimaryList: conf.DomainPrimaryList, 42 | DomainAlternativeList: conf.DomainAlternativeList, 43 | 44 | RedirectIPv6Record: conf.IPv6UseAlternativeDNS, 45 | AlternativeDNSConcurrent: conf.AlternativeDNSConcurrent, 46 | MinimumTTL: conf.MinimumTTL, 47 | DomainTTLMap: conf.DomainTTLMap, 48 | 49 | Hosts: conf.Hosts, 50 | Cache: conf.Cache, 51 | } 52 | dispatcher.Init() 53 | 54 | srv = inbound.NewServer(conf.BindAddress, conf.DebugHTTPAddress, dispatcher, conf.RejectQType, conf.DohEnabled) 55 | srv.HTTPMux.HandleFunc("/reload/config", ReloadConfigHandler) 56 | srv.HTTPMux.HandleFunc("/reload", ReloadHandler) 57 | srv.HTTPMux.HandleFunc("/config", ConfigHandler) 58 | 59 | go srv.Run() 60 | } 61 | 62 | // Stop server 63 | func Stop() { 64 | srv.Stop() 65 | } 66 | 67 | // ReloadHandler is passed to http.Server for handle "/reload" request 68 | func ReloadHandler(w http.ResponseWriter, r *http.Request) { 69 | conf = config.NewConfig(conf.FilePath) 70 | Reload() 71 | io.WriteString(w, "Reloaded") 72 | } 73 | 74 | func ConfigHandler(w http.ResponseWriter, r *http.Request) { 75 | w.Header().Add("Content-Type", "application/json") 76 | jsonBinary, _ := json.Marshal(conf) 77 | io.WriteString(w, string(jsonBinary)) 78 | } 79 | 80 | func ReloadConfigHandler(w http.ResponseWriter, r *http.Request) { 81 | b, err := ioutil.ReadAll(r.Body) 82 | defer r.Body.Close() 83 | if err != nil { 84 | http.Error(w, err.Error(), 500) 85 | return 86 | } 87 | err = json.Unmarshal(b, &conf) 88 | if err != nil { 89 | http.Error(w, err.Error(), 500) 90 | return 91 | } 92 | 93 | Reload() 94 | io.WriteString(w, "Reloaded") 95 | } 96 | 97 | // Reload config and restart server 98 | func Reload() { 99 | log.Infof("Reloading") 100 | Stop() 101 | // Have to wait seconds (may be waiting for server shutdown completly) or we will get config parse ERROR. Unknown reason. 102 | time.Sleep(time.Second) 103 | Start() 104 | } 105 | -------------------------------------------------------------------------------- /core/errors/normal_error.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | type NormalError struct { 4 | Message string 5 | } 6 | 7 | func (e *NormalError) Error() string { 8 | return e.Message 9 | } 10 | -------------------------------------------------------------------------------- /core/finder/finder.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 shawn1m. All rights reserved. 3 | * Use of this source code is governed by The MIT License (MIT) that can be 4 | * found in the LICENSE file.. 5 | */ 6 | 7 | package finder 8 | 9 | type Finder interface { 10 | Insert(k string, v string) error 11 | Get(k string) []string 12 | Name() string 13 | } 14 | -------------------------------------------------------------------------------- /core/finder/full/map.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 shawn1m. All rights reserved. 3 | * Use of this source code is governed by The MIT License (MIT) that can be 4 | * found in the LICENSE file.. 5 | */ 6 | 7 | package full 8 | 9 | type Map struct { 10 | DataMap map[string][]string 11 | } 12 | 13 | func (m *Map) Insert(k string, v string) error { 14 | if m.DataMap[k] == nil { 15 | m.DataMap[k] = []string{v} 16 | } else { 17 | m.DataMap[k] = append(m.DataMap[k], v) 18 | } 19 | return nil 20 | } 21 | 22 | func (m *Map) Get(k string) []string { 23 | return m.DataMap[k] 24 | } 25 | 26 | func (m *Map) Name() string { 27 | return "full-map" 28 | } 29 | -------------------------------------------------------------------------------- /core/finder/regex/list.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 shawn1m. All rights reserved. 3 | * Use of this source code is governed by The MIT License (MIT) that can be 4 | * found in the LICENSE file.. 5 | */ 6 | 7 | package regex 8 | 9 | import "github.com/shawn1m/overture/core/common" 10 | 11 | type List struct { 12 | RegexMap map[string][]string 13 | } 14 | 15 | func (r *List) Insert(k string, v string) error { 16 | if r.RegexMap[k] == nil { 17 | r.RegexMap[k] = []string{v} 18 | } else { 19 | r.RegexMap[k] = append(r.RegexMap[k], v) 20 | } 21 | return nil 22 | } 23 | 24 | func (r *List) Get(str string) []string { 25 | var result []string 26 | for k, v := range r.RegexMap { 27 | if common.IsDomainMatchRule(k, str) { 28 | result = append(result, v...) 29 | } 30 | } 31 | if len(result) == 0 { 32 | return nil 33 | } 34 | return result 35 | } 36 | 37 | func (r *List) Name() string { 38 | return "regex-list" 39 | } 40 | -------------------------------------------------------------------------------- /core/hosts/hosts.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Jan Broer. All rights reserved. 2 | // Use of this source code is governed by The MIT License (MIT) that can be 3 | // found in the LICENSE file. 4 | 5 | // Package hosts provides address lookups from hosts file. 6 | package hosts 7 | 8 | import ( 9 | "bufio" 10 | "net" 11 | "os" 12 | "strings" 13 | "time" 14 | 15 | "github.com/shawn1m/overture/core/errors" 16 | "github.com/shawn1m/overture/core/finder" 17 | log "github.com/sirupsen/logrus" 18 | ) 19 | 20 | // Hosts represents a file containing hosts_sample 21 | type Hosts struct { 22 | filePath string 23 | finder finder.Finder 24 | } 25 | 26 | type hostsLine struct { 27 | domain string 28 | ip net.IP 29 | isIpv6 bool 30 | } 31 | 32 | func New(path string, finder finder.Finder) (*Hosts, error) { 33 | if path == "" { 34 | return nil, nil 35 | } 36 | 37 | h := &Hosts{filePath: path, finder: finder} 38 | if err := h.initHosts(); err != nil { 39 | return nil, err 40 | } 41 | 42 | return h, nil 43 | } 44 | 45 | func (h *Hosts) Find(name string) (ipv4List []net.IP, ipv6List []net.IP) { 46 | name = strings.TrimSuffix(name, ".") 47 | hostsLines := h.findHosts(name) 48 | for _, hostLine := range hostsLines { 49 | if hostLine.isIpv6 { 50 | ipv6List = append(ipv6List, hostLine.ip) 51 | } else { 52 | ipv4List = append(ipv4List, hostLine.ip) 53 | } 54 | } 55 | return ipv4List, ipv6List 56 | } 57 | 58 | func (h *Hosts) initHosts() error { 59 | f, err := os.Open(h.filePath) 60 | if err != nil { 61 | return err 62 | } 63 | defer f.Close() 64 | defer log.Debugf("%s took %s", "Load hosts", time.Since(time.Now())) 65 | 66 | scanner := bufio.NewScanner(f) 67 | for scanner.Scan() { 68 | if err := h.parseLine(scanner.Text()); err != nil { 69 | log.Warnf("Bad formatted hosts file line: %s", err) 70 | } 71 | } 72 | return nil 73 | } 74 | 75 | func (h *Hosts) findHosts(name string) []hostsLine { 76 | var result []hostsLine 77 | ips := h.finder.Get(name) 78 | for _, ipString := range ips { 79 | ip := net.ParseIP(ipString) 80 | var isIPv6 bool 81 | switch { 82 | case ip.To4() != nil: 83 | isIPv6 = false 84 | case ip.To16() != nil: 85 | isIPv6 = true 86 | default: 87 | log.Warnf("Invalid IP address found in hosts file: %s", ip) 88 | return []hostsLine{} 89 | } 90 | result = append(result, hostsLine{ 91 | domain: name, 92 | ip: ip, 93 | isIpv6: isIPv6, 94 | }) 95 | } 96 | return result 97 | } 98 | 99 | func (h *Hosts) parseLine(line string) error { 100 | if len(line) == 0 { 101 | return nil 102 | } 103 | 104 | // Parse leading # for disabled lines 105 | if line[0:1] == "#" { 106 | return nil 107 | } 108 | 109 | // Parse other #s for actual comments 110 | line = strings.Split(line, "#")[0] 111 | 112 | // Replace tabs and spaces with single spaces throughout 113 | line = strings.Replace(line, "\t", " ", -1) 114 | for strings.Contains(line, " ") { 115 | line = strings.Replace(line, " ", " ", -1) 116 | } 117 | 118 | line = strings.TrimSpace(line) 119 | 120 | // Break line into words 121 | words := strings.Split(line, " ") 122 | 123 | if len(words) < 2 { 124 | log.Warn("Wrong format") 125 | return &errors.NormalError{Message: "Wrong format"} 126 | } 127 | for i, word := range words { 128 | words[i] = strings.TrimSpace(word) 129 | } 130 | // Separate the first bit (the ip) from the other bits (the domains) 131 | a, host := words[0], words[1] 132 | 133 | ip := net.ParseIP(a) 134 | 135 | err := h.finder.Insert(host, ip.String()) 136 | if err != nil { 137 | return err 138 | } 139 | return nil 140 | } 141 | -------------------------------------------------------------------------------- /core/hosts/hosts_test.go: -------------------------------------------------------------------------------- 1 | package hosts 2 | 3 | import ( 4 | "io/ioutil" 5 | "net" 6 | "os" 7 | "testing" 8 | 9 | "github.com/shawn1m/overture/core/finder/full" 10 | ) 11 | 12 | func TestHosts_Find(t *testing.T) { 13 | 14 | hostLinesString := []string{"1.2.3.4 abc.com\n", "::1 abc.com\n", "2.3.4.5 abc.com\n", "::2 abc.com\n", 15 | "::1 localhost\n", "127.0.0.1 localhost\n"} 16 | hostsFile, err := generateHostsFile(hostLinesString) 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | 21 | hosts, err := New(hostsFile, &full.Map{DataMap: make(map[string][]string, 100)}) 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | 26 | ipv4List, ipv6List := hosts.Find("abc.com") 27 | if !find(ipv4List, net.ParseIP("1.2.3.4")) { 28 | t.Error() 29 | } 30 | if !find(ipv4List, net.ParseIP("2.3.4.5")) { 31 | t.Error() 32 | } 33 | if !find(ipv6List, net.ParseIP("::1")) { 34 | t.Error() 35 | } 36 | if !find(ipv6List, net.ParseIP("::2")) { 37 | t.Error() 38 | } 39 | 40 | ipv4List, ipv6List = hosts.Find("localhost") 41 | if !find(ipv4List, net.ParseIP("127.0.0.1")) { 42 | t.Error() 43 | } 44 | if !find(ipv6List, net.ParseIP("::1")) { 45 | t.Error() 46 | } 47 | } 48 | 49 | func generateHostsFile(hostLinesString []string) (string, error) { 50 | 51 | var f *os.File 52 | f, err := ioutil.TempFile("", "hosts_test") 53 | if err != nil { 54 | return "", err 55 | } 56 | for _, hostLineString := range hostLinesString { 57 | if _, err := f.WriteString(hostLineString); err != nil { 58 | return "", err 59 | } 60 | } 61 | if err := f.Close(); err != nil { 62 | return "", err 63 | } 64 | return f.Name(), nil 65 | } 66 | 67 | func find(a []net.IP, x net.IP) bool { 68 | for _, n := range a { 69 | if x.Equal(n) { 70 | return true 71 | } 72 | } 73 | return false 74 | } 75 | -------------------------------------------------------------------------------- /core/inbound/server.go: -------------------------------------------------------------------------------- 1 | // Package inbound implements dns server for inbound connection. 2 | package inbound 3 | 4 | import ( 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net" 10 | "net/http" 11 | "net/http/pprof" 12 | "os" 13 | "strconv" 14 | "strings" 15 | "sync" 16 | "time" 17 | 18 | "github.com/coredns/coredns/plugin/pkg/dnsutil" 19 | "github.com/coredns/coredns/plugin/pkg/doh" 20 | "github.com/coredns/coredns/plugin/pkg/response" 21 | "github.com/miekg/dns" 22 | "github.com/shawn1m/overture/core/common" 23 | log "github.com/sirupsen/logrus" 24 | 25 | "github.com/shawn1m/overture/core/outbound" 26 | ) 27 | 28 | type Server struct { 29 | bindAddress string 30 | debugHttpAddress string 31 | dispatcher outbound.Dispatcher 32 | rejectQType []uint16 33 | HTTPMux *http.ServeMux 34 | ctx context.Context 35 | cancel context.CancelFunc 36 | dohEnabled bool 37 | } 38 | 39 | func NewServer(bindAddress string, debugHTTPAddress string, dispatcher outbound.Dispatcher, rejectQType []uint16, dohEnabled bool) *Server { 40 | s := &Server{ 41 | bindAddress: bindAddress, 42 | debugHttpAddress: debugHTTPAddress, 43 | dispatcher: dispatcher, 44 | rejectQType: rejectQType, 45 | dohEnabled: dohEnabled, 46 | } 47 | s.ctx, s.cancel = context.WithCancel(context.Background()) 48 | s.HTTPMux = http.NewServeMux() 49 | return s 50 | } 51 | 52 | func (s *Server) ServeDNSHttp(w http.ResponseWriter, r *http.Request) { 53 | if r.URL.Path != doh.Path { 54 | http.Error(w, "", http.StatusNotFound) 55 | return 56 | } 57 | 58 | q, err := doh.RequestToMsg(r) 59 | if err != nil { 60 | http.Error(w, err.Error(), http.StatusBadRequest) 61 | return 62 | } 63 | 64 | // Create a DoHWriter with the correct addresses in it. 65 | inboundIP, _, _ := net.SplitHostPort(r.RemoteAddr) 66 | forwardIP := r.Header.Get("X-Forwarded-For") 67 | if net.ParseIP(forwardIP) != nil && common.ReservedIPNetworkList.Contains(net.ParseIP(inboundIP), false, "") { 68 | inboundIP = forwardIP 69 | } 70 | log.Debugf("Question from %s: %s", inboundIP, q.Question[0].String()) 71 | 72 | for _, qt := range s.rejectQType { 73 | if isQuestionType(q, qt) { 74 | log.Debugf("Reject %s: %s", inboundIP, q.Question[0].String()) 75 | http.Error(w, "Rejected", http.StatusForbidden) 76 | return 77 | } 78 | } 79 | 80 | responseMessage := s.dispatcher.Exchange(q, inboundIP) 81 | 82 | if responseMessage == nil { 83 | http.Error(w, "No response", http.StatusInternalServerError) 84 | return 85 | } 86 | 87 | buf, _ := responseMessage.Pack() 88 | 89 | mt, _ := response.Typify(responseMessage, time.Now().UTC()) 90 | age := dnsutil.MinimalTTL(responseMessage, mt) 91 | 92 | w.Header().Set("Content-Type", doh.MimeType) 93 | w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%f", age.Seconds())) 94 | w.Header().Set("Content-Length", strconv.Itoa(len(buf))) 95 | w.WriteHeader(http.StatusOK) 96 | 97 | w.Write(buf) 98 | } 99 | 100 | func (s *Server) DumpCache(w http.ResponseWriter, req *http.Request) { 101 | if s.dispatcher.Cache == nil { 102 | io.WriteString(w, "error: cache not enabled") 103 | return 104 | } 105 | 106 | type answer struct { 107 | Name string `json:"name"` 108 | TTL int `json:"ttl"` 109 | Type string `json:"type"` 110 | Rdata string `json:"rdata"` 111 | } 112 | 113 | type response struct { 114 | Length int `json:"length"` 115 | Capacity int `json:"capacity"` 116 | Body map[string][]*answer `json:"body"` 117 | } 118 | 119 | query := req.URL.Query() 120 | nobody := true 121 | if t := query.Get("nobody"); strings.ToLower(t) == "false" { 122 | nobody = false 123 | } 124 | 125 | rs, l := s.dispatcher.Cache.Dump(nobody) 126 | body := make(map[string][]*answer) 127 | 128 | for k, es := range rs { 129 | var answers []*answer 130 | for _, e := range es { 131 | ts := strings.Split(e, "\t") 132 | ttl, _ := strconv.Atoi(ts[1]) 133 | r := &answer{ 134 | Name: ts[0], 135 | TTL: ttl, 136 | Type: ts[3], 137 | Rdata: ts[4], 138 | } 139 | answers = append(answers, r) 140 | } 141 | body[strings.TrimSpace(k)] = answers 142 | } 143 | 144 | res := response{ 145 | Body: body, 146 | Length: l, 147 | Capacity: s.dispatcher.Cache.Capacity(), 148 | } 149 | 150 | responseBytes, err := json.Marshal(&res) 151 | if err != nil { 152 | io.WriteString(w, err.Error()) 153 | return 154 | } 155 | 156 | io.WriteString(w, string(responseBytes)) 157 | } 158 | 159 | func (s *Server) Run() { 160 | 161 | mux := dns.NewServeMux() 162 | mux.Handle(".", s) 163 | 164 | wg := new(sync.WaitGroup) 165 | wg.Add(2) 166 | 167 | log.Infof("Overture is listening on %s", s.bindAddress) 168 | 169 | for _, p := range [2]string{"tcp", "udp"} { 170 | go func(p string) { 171 | 172 | // Manual create server inorder to have a way to close it. 173 | srv := &dns.Server{Addr: s.bindAddress, Net: p, Handler: mux} 174 | go func() { 175 | <-s.ctx.Done() 176 | log.Warnf("Shutting down the server on protocol %s", p) 177 | srv.ShutdownContext(s.ctx) 178 | }() 179 | err := srv.ListenAndServe() 180 | if err != nil { 181 | log.Fatalf("Listening on port %s failed: %s", p, err) 182 | os.Exit(1) 183 | } 184 | wg.Done() 185 | }(p) 186 | } 187 | 188 | if s.debugHttpAddress != "" { 189 | s.HTTPMux.HandleFunc("/cache", s.DumpCache) 190 | s.HTTPMux.HandleFunc("/debug/pprof/", pprof.Index) 191 | s.HTTPMux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 192 | s.HTTPMux.HandleFunc("/debug/pprof/profile", pprof.Profile) 193 | s.HTTPMux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 194 | s.HTTPMux.HandleFunc("/debug/pprof/trace", pprof.Trace) 195 | if s.dohEnabled { 196 | log.Info("Dns over http server started!") 197 | s.HTTPMux.HandleFunc(doh.Path, s.ServeDNSHttp) 198 | } 199 | 200 | wg.Add(1) 201 | go func() { 202 | // Manual create server inorder to have a way to close it. 203 | srv := &http.Server{ 204 | Addr: s.debugHttpAddress, 205 | Handler: s.HTTPMux, 206 | } 207 | go func() { 208 | <-s.ctx.Done() 209 | log.Warnf("Shutting down debug HTTP server") 210 | srv.Shutdown(s.ctx) 211 | }() 212 | 213 | err := srv.ListenAndServe() 214 | if err != http.ErrServerClosed { 215 | log.Fatalf("Debug HTTP Server Listen on port %s faild: %s", s.debugHttpAddress, err) 216 | os.Exit(1) 217 | } 218 | wg.Done() 219 | }() 220 | } 221 | 222 | wg.Wait() 223 | } 224 | 225 | func (s *Server) Stop() { 226 | s.cancel() 227 | } 228 | 229 | func (s *Server) ServeDNS(w dns.ResponseWriter, q *dns.Msg) { 230 | inboundIP, _, _ := net.SplitHostPort(w.RemoteAddr().String()) 231 | 232 | log.Debugf("Question from %s: %s", inboundIP, q.Question[0].String()) 233 | 234 | for _, qt := range s.rejectQType { 235 | if isQuestionType(q, qt) { 236 | log.Debugf("Reject %s: %s", inboundIP, q.Question[0].String()) 237 | dns.HandleFailed(w, q) 238 | return 239 | } 240 | } 241 | 242 | responseMessage := s.dispatcher.Exchange(q, inboundIP) 243 | 244 | if responseMessage == nil { 245 | dns.HandleFailed(w, q) 246 | return 247 | } 248 | 249 | err := w.WriteMsg(responseMessage) 250 | if err != nil { 251 | log.Warnf("Write message failed, message: %s, error: %s", responseMessage, err) 252 | return 253 | } 254 | } 255 | 256 | func isQuestionType(q *dns.Msg, qt uint16) bool { return q.Question[0].Qtype == qt } 257 | -------------------------------------------------------------------------------- /core/matcher/final/default.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 shawn1m. All rights reserved. 3 | * Use of this source code is governed by The MIT License (MIT) that can be 4 | * found in the LICENSE file.. 5 | */ 6 | 7 | package final 8 | 9 | type Default struct { 10 | } 11 | 12 | func (s *Default) Insert(str string) error { 13 | return nil 14 | } 15 | 16 | func (s *Default) Has(str string) bool { 17 | return true 18 | } 19 | 20 | func (s *Default) Name() string { 21 | return "final" 22 | } 23 | -------------------------------------------------------------------------------- /core/matcher/full/list.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 shawn1m. All rights reserved. 3 | * Use of this source code is governed by The MIT License (MIT) that can be 4 | * found in the LICENSE file.. 5 | */ 6 | 7 | package full 8 | 9 | type List struct { 10 | DataList []string 11 | } 12 | 13 | func (s *List) Insert(str string) error { 14 | s.DataList = append(s.DataList, str) 15 | return nil 16 | } 17 | 18 | func (s *List) Has(str string) bool { 19 | for _, data := range s.DataList { 20 | if data == str { 21 | return true 22 | } 23 | } 24 | return false 25 | } 26 | 27 | func (s *List) Name() string { 28 | return "full-list" 29 | } 30 | -------------------------------------------------------------------------------- /core/matcher/full/map.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 shawn1m. All rights reserved. 3 | * Use of this source code is governed by The MIT License (MIT) that can be 4 | * found in the LICENSE file.. 5 | */ 6 | 7 | package full 8 | 9 | type Map struct { 10 | DataMap map[string]struct{} 11 | } 12 | 13 | func (m *Map) Insert(str string) error { 14 | m.DataMap[str] = struct{}{} 15 | return nil 16 | } 17 | 18 | func (m *Map) Has(str string) bool { 19 | if _, ok := m.DataMap[str]; ok { 20 | return true 21 | } 22 | return false 23 | } 24 | 25 | func (m *Map) Name() string { 26 | return "full-map" 27 | } 28 | -------------------------------------------------------------------------------- /core/matcher/matcher.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 shawn1m. All rights reserved. 3 | * Use of this source code is governed by The MIT License (MIT) that can be 4 | * found in the LICENSE file.. 5 | */ 6 | 7 | package matcher 8 | 9 | type Matcher interface { 10 | Insert(string) error 11 | Has(string) bool 12 | Name() string 13 | } 14 | -------------------------------------------------------------------------------- /core/matcher/mix/list.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 shawn1m. All rights reserved. 3 | * Use of this source code is governed by The MIT License (MIT) that can be 4 | * found in the LICENSE file.. 5 | */ 6 | 7 | package mix 8 | 9 | import ( 10 | "fmt" 11 | "regexp" 12 | "strings" 13 | ) 14 | 15 | type Data struct { 16 | Type string 17 | Content string 18 | } 19 | 20 | type List struct { 21 | DataList []Data 22 | } 23 | 24 | func (s *List) Insert(str string) error { 25 | kv := strings.Split(str, ":") 26 | 27 | switch len(kv) { 28 | case 1: 29 | s.DataList = append(s.DataList, 30 | Data{ 31 | Type: "domain", 32 | Content: strings.ToLower(kv[0])}) 33 | case 2: 34 | s.DataList = append(s.DataList, 35 | Data{ 36 | Type: strings.ToLower(kv[0]), 37 | Content: strings.ToLower(kv[1])}) 38 | default: 39 | return fmt.Errorf("invalid format: %s", str) 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func (s *List) Has(str string) bool { 46 | for _, data := range s.DataList { 47 | switch data.Type { 48 | case "domain": 49 | idx := len(str) - len(data.Content) 50 | if idx >= 0 && data.Content == str[idx:] { 51 | if idx >= 1 && (str[idx-1] != '.') { 52 | return false 53 | } 54 | return true 55 | } 56 | case "regex": 57 | reg := regexp.MustCompile(data.Content) 58 | if reg.MatchString(str) { 59 | return true 60 | } 61 | case "keyword": 62 | if strings.Contains(str, data.Content) { 63 | return true 64 | } 65 | case "full": 66 | if data.Content == str { 67 | return true 68 | } 69 | } 70 | } 71 | return false 72 | } 73 | 74 | func (s *List) Name() string { 75 | return "mix-list" 76 | } 77 | -------------------------------------------------------------------------------- /core/matcher/regex/list.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 shawn1m. All rights reserved. 3 | * Use of this source code is governed by The MIT License (MIT) that can be 4 | * found in the LICENSE file.. 5 | */ 6 | 7 | package regex 8 | 9 | import "github.com/shawn1m/overture/core/common" 10 | 11 | type List struct { 12 | RegexList []string 13 | } 14 | 15 | func (r *List) Insert(s string) error { 16 | r.RegexList = append(r.RegexList, s) 17 | return nil 18 | } 19 | 20 | func (r *List) Has(s string) bool { 21 | for _, regex := range r.RegexList { 22 | if common.IsDomainMatchRule(regex, s) { 23 | return true 24 | } 25 | } 26 | return false 27 | } 28 | 29 | func (r *List) Name() string { 30 | return "regex-list" 31 | } 32 | -------------------------------------------------------------------------------- /core/matcher/suffix/tree.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 shawn1m. All rights reserved. 3 | * Use of this source code is governed by The MIT License (MIT) that can be 4 | * found in the LICENSE file.. 5 | */ 6 | 7 | package suffix 8 | 9 | import ( 10 | "errors" 11 | "strings" 12 | ) 13 | 14 | type Domain string 15 | 16 | type Tree struct { 17 | mark uint8 18 | sub domainMap 19 | final bool 20 | } 21 | 22 | func (dt *Tree) Name() string { 23 | return "suffix-tree" 24 | } 25 | 26 | type domainMap map[Domain]*Tree 27 | 28 | func (d Domain) nextLevel() Domain { 29 | if pointIndex := strings.LastIndex(string(d), "."); pointIndex == -1 { 30 | return "" 31 | } else { 32 | return d[:pointIndex] 33 | } 34 | } 35 | 36 | func (d Domain) topLevel() Domain { 37 | if pointIndex := strings.LastIndex(string(d), "."); pointIndex == -1 { 38 | return d 39 | } else { 40 | return d[pointIndex+1:] 41 | } 42 | } 43 | 44 | func DefaultDomainTree() *Tree { 45 | return NewDomainTree() 46 | } 47 | 48 | func NewDomainTree() (dt *Tree) { 49 | dt = new(Tree) 50 | dt.sub = make(domainMap) 51 | return 52 | } 53 | 54 | func (dt *Tree) has(d Domain) bool { 55 | if len(dt.sub) == 0 || dt.final { 56 | return true 57 | } 58 | 59 | if sub, ok := dt.sub[d.topLevel()]; ok { 60 | return sub.has(d.nextLevel()) 61 | } 62 | return false 63 | } 64 | 65 | func (dt *Tree) Has(d string) bool { 66 | if len(dt.sub) == 0 { 67 | return false 68 | } 69 | return dt.has(Domain(d)) 70 | } 71 | 72 | func (dt *Tree) insert(sections []Domain) { 73 | 74 | if len(sections) == 0 { 75 | dt.final = true 76 | return 77 | } 78 | 79 | var lastIndex, lastSec = len(sections) - 1, sections[len(sections)-1] 80 | 81 | if sec, ok := dt.sub[lastSec]; ok { 82 | sec.insert(sections[:lastIndex]) 83 | } else { 84 | dt.sub[lastSec] = NewDomainTree() 85 | dt.sub[lastSec].insert(sections[:lastIndex]) 86 | } 87 | } 88 | 89 | func (dt *Tree) Insert(d string) error { 90 | sections := strings.Split(d, ".") 91 | if len(sections) == 0 { 92 | return errors.New("Split Domain error\n") 93 | } 94 | 95 | domainSec := make([]Domain, len(sections)) 96 | 97 | for i := range sections { 98 | domainSec[i] = Domain(sections[i]) 99 | } 100 | 101 | dt.insert(domainSec) 102 | return nil 103 | } 104 | -------------------------------------------------------------------------------- /core/matcher/suffix/tree_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 shawn1m. All rights reserved. 3 | * Use of this source code is governed by The MIT License (MIT) that can be 4 | * found in the LICENSE file.. 5 | */ 6 | 7 | package suffix 8 | 9 | import ( 10 | "testing" 11 | ) 12 | 13 | func TestTree_Has(t *testing.T) { 14 | tree := DefaultDomainTree() 15 | for _, d := range []string{ 16 | "1.abc.com", 17 | "2.abc.com", 18 | "1.2.abc.com", 19 | } { 20 | tree.Insert(d) 21 | } 22 | for _, d := range []string{ 23 | "1.abc.com", 24 | "2.abc.com", 25 | "1.2.abc.com", 26 | } { 27 | if !tree.Has(d) { 28 | t.Fail() 29 | } 30 | } 31 | if tree.Has("abc.com") { 32 | t.Fail() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/outbound/clients/cache.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 shawn1m. All rights reserved. 3 | * Use of this source code is governed by The MIT License (MIT) that can be 4 | * found in the LICENSE file.. 5 | */ 6 | 7 | // Package outbound implements multiple dns client and dispatcher for outbound connection. 8 | package clients 9 | 10 | import ( 11 | "github.com/miekg/dns" 12 | log "github.com/sirupsen/logrus" 13 | 14 | "github.com/shawn1m/overture/core/cache" 15 | ) 16 | 17 | type CacheClient struct { 18 | responseMessage *dns.Msg 19 | questionMessage *dns.Msg 20 | 21 | ednsClientSubnetIP string 22 | 23 | cache *cache.Cache 24 | } 25 | 26 | func NewCacheClient(q *dns.Msg, ip string, cache *cache.Cache) *CacheClient { 27 | return &CacheClient{questionMessage: q.Copy(), ednsClientSubnetIP: ip, cache: cache} 28 | } 29 | 30 | func (c *CacheClient) Exchange() *dns.Msg { 31 | if c.exchangeFromCache() { 32 | return c.responseMessage 33 | } 34 | 35 | return nil 36 | } 37 | 38 | func (c *CacheClient) exchangeFromCache() bool { 39 | if c.cache == nil { 40 | return false 41 | } 42 | 43 | m := c.cache.Hit(cache.Key(c.questionMessage.Question[0], c.ednsClientSubnetIP), c.questionMessage.Id) 44 | if m != nil { 45 | log.Debugf("Cache hit: %s", cache.Key(c.questionMessage.Question[0], c.ednsClientSubnetIP)) 46 | c.responseMessage = m 47 | return true 48 | } 49 | 50 | return false 51 | } 52 | -------------------------------------------------------------------------------- /core/outbound/clients/local.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 shawn1m. All rights reserved. 3 | * Use of this source code is governed by The MIT License (MIT) that can be 4 | * found in the LICENSE file.. 5 | */ 6 | 7 | // Package outbound implements multiple dns client and dispatcher for outbound connection. 8 | package clients 9 | 10 | import ( 11 | "math/rand" 12 | "net" 13 | "time" 14 | 15 | "github.com/miekg/dns" 16 | 17 | "github.com/shawn1m/overture/core/common" 18 | "github.com/shawn1m/overture/core/hosts" 19 | ) 20 | 21 | type LocalClient struct { 22 | responseMessage *dns.Msg 23 | questionMessage *dns.Msg 24 | 25 | minimumTTL int 26 | domainTTLMap map[string]uint32 27 | 28 | hosts *hosts.Hosts 29 | rawName string 30 | } 31 | 32 | func NewLocalClient(q *dns.Msg, h *hosts.Hosts, minimumTTL int, domainTTLMap map[string]uint32) *LocalClient { 33 | c := &LocalClient{questionMessage: q.Copy(), hosts: h, minimumTTL: minimumTTL, domainTTLMap: domainTTLMap} 34 | c.rawName = c.questionMessage.Question[0].Name 35 | return c 36 | } 37 | 38 | func (c *LocalClient) Exchange() *dns.Msg { 39 | if c.exchangeFromHosts() || c.exchangeFromIP() { 40 | if c.responseMessage != nil { 41 | common.SetMinimumTTL(c.responseMessage, uint32(c.minimumTTL)) 42 | common.SetTTLByMap(c.responseMessage, c.domainTTLMap) 43 | } 44 | return c.responseMessage 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func (c *LocalClient) exchangeFromHosts() bool { 51 | if c.hosts == nil { 52 | return false 53 | } 54 | 55 | name := c.rawName[:len(c.rawName)-1] 56 | ipv4List, ipv6List := c.hosts.Find(name) 57 | 58 | if c.questionMessage.Question[0].Qtype == dns.TypeA && len(ipv4List) > 0 { 59 | var rrl []dns.RR 60 | for _, ip := range ipv4List { 61 | a, _ := dns.NewRR(c.rawName + " IN A " + ip.String()) 62 | rrl = append(rrl, a) 63 | } 64 | c.setLocalResponseMessage(rrl) 65 | if c.responseMessage != nil { 66 | return true 67 | } 68 | } else if c.questionMessage.Question[0].Qtype == dns.TypeAAAA && len(ipv6List) > 0 { 69 | var rrl []dns.RR 70 | for _, ip := range ipv6List { 71 | aaaa, _ := dns.NewRR(c.rawName + " IN AAAA " + ip.String()) 72 | rrl = append(rrl, aaaa) 73 | } 74 | c.setLocalResponseMessage(rrl) 75 | if c.responseMessage != nil { 76 | return true 77 | } 78 | } 79 | 80 | if (len(ipv4List) > 0 || len(ipv6List) > 0) && 81 | (c.questionMessage.Question[0].Qtype == dns.TypeA || c.questionMessage.Question[0].Qtype == dns.TypeAAAA) { 82 | c.setLocalResponseMessage(nil) 83 | return true 84 | } 85 | 86 | return false 87 | } 88 | 89 | func (c *LocalClient) exchangeFromIP() bool { 90 | name := c.rawName[:len(c.rawName)-1] 91 | ip := net.ParseIP(name) 92 | if ip == nil { 93 | return false 94 | } 95 | if ip.To4() == nil && ip.To16() != nil && c.questionMessage.Question[0].Qtype == dns.TypeAAAA { 96 | aaaa, _ := dns.NewRR(c.rawName + " IN AAAA " + ip.String()) 97 | c.setLocalResponseMessage([]dns.RR{aaaa}) 98 | return true 99 | } else if ip.To4() != nil && c.questionMessage.Question[0].Qtype == dns.TypeA { 100 | a, _ := dns.NewRR(c.rawName + " IN A " + ip.String()) 101 | c.setLocalResponseMessage([]dns.RR{a}) 102 | return true 103 | } 104 | 105 | return false 106 | } 107 | 108 | func (c *LocalClient) setLocalResponseMessage(rrl []dns.RR) { 109 | shuffleRRList := func(rrl []dns.RR) { 110 | rand.Seed(time.Now().UnixNano()) 111 | for i := range rrl { 112 | j := rand.Intn(i + 1) 113 | rrl[i], rrl[j] = rrl[j], rrl[i] 114 | } 115 | } 116 | 117 | c.responseMessage = new(dns.Msg) 118 | for _, rr := range rrl { 119 | c.responseMessage.Answer = append(c.responseMessage.Answer, rr) 120 | } 121 | shuffleRRList(c.responseMessage.Answer) 122 | c.responseMessage.SetReply(c.questionMessage) 123 | c.responseMessage.RecursionAvailable = true 124 | } 125 | -------------------------------------------------------------------------------- /core/outbound/clients/remote.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 shawn1m. All rights reserved. 3 | * Use of this source code is governed by The MIT License (MIT) that can be 4 | * found in the LICENSE file.. 5 | */ 6 | 7 | // Package outbound implements multiple dns client and dispatcher for outbound connection. 8 | package clients 9 | 10 | import ( 11 | "github.com/miekg/dns" 12 | log "github.com/sirupsen/logrus" 13 | "net" 14 | 15 | "github.com/shawn1m/overture/core/cache" 16 | "github.com/shawn1m/overture/core/common" 17 | "github.com/shawn1m/overture/core/outbound/clients/resolver" 18 | ) 19 | 20 | type RemoteClient struct { 21 | responseMessage *dns.Msg 22 | questionMessage *dns.Msg 23 | 24 | dnsUpstream *common.DNSUpstream 25 | ednsClientSubnetIP string 26 | inboundIP string 27 | dnsResolver resolver.Resolver 28 | 29 | cache *cache.Cache 30 | } 31 | 32 | func NewClient(q *dns.Msg, u *common.DNSUpstream, resolver resolver.Resolver, ip string, cache *cache.Cache) *RemoteClient { 33 | c := &RemoteClient{questionMessage: q.Copy(), dnsUpstream: u, dnsResolver: resolver, inboundIP: ip, cache: cache} 34 | 35 | if c.dnsUpstream.EDNSClientSubnet != nil { 36 | c.getEDNSClientSubnetIP() 37 | } 38 | 39 | return c 40 | } 41 | 42 | func (c *RemoteClient) getEDNSClientSubnetIP() { 43 | switch c.dnsUpstream.EDNSClientSubnet.Policy { 44 | case "auto": 45 | if !common.ReservedIPNetworkList.Contains(net.ParseIP(c.inboundIP), false, "") { 46 | c.ednsClientSubnetIP = c.inboundIP 47 | } else { 48 | c.ednsClientSubnetIP = c.dnsUpstream.EDNSClientSubnet.ExternalIP 49 | } 50 | case "manual": 51 | if c.dnsUpstream.EDNSClientSubnet.ExternalIP != "" && 52 | !common.ReservedIPNetworkList.Contains(net.ParseIP(c.dnsUpstream.EDNSClientSubnet.ExternalIP), false, "") { 53 | c.ednsClientSubnetIP = c.dnsUpstream.EDNSClientSubnet.ExternalIP 54 | return 55 | } 56 | case "disable": 57 | } 58 | } 59 | 60 | func (c *RemoteClient) ExchangeFromCache() *dns.Msg { 61 | cacheClient := NewCacheClient(c.questionMessage, c.ednsClientSubnetIP, c.cache) 62 | c.responseMessage = cacheClient.Exchange() 63 | if c.responseMessage != nil { 64 | return c.responseMessage 65 | } 66 | return nil 67 | } 68 | 69 | func (c *RemoteClient) Exchange(isLog bool) *dns.Msg { 70 | common.SetEDNSClientSubnet(c.questionMessage, c.ednsClientSubnetIP, 71 | c.dnsUpstream.EDNSClientSubnet.NoCookie) 72 | log.Debugf("Use " + c.ednsClientSubnetIP + " as original ednsClientSubnetIP") 73 | c.ednsClientSubnetIP = common.GetEDNSClientSubnetIP(c.questionMessage) 74 | log.Debugf("Use " + c.ednsClientSubnetIP + " as ednsClientSubnetIP") 75 | 76 | if c.responseMessage != nil { 77 | return c.responseMessage 78 | } 79 | 80 | var temp *dns.Msg 81 | var err error 82 | temp, err = c.dnsResolver.Exchange(c.questionMessage) 83 | 84 | if err != nil { 85 | log.Debugf("%s Fail: %s", c.dnsUpstream.Name, err) 86 | return nil 87 | } 88 | if temp == nil { 89 | log.Debugf("%s Fail: Response message returned nil, maybe timeout? Please check your query or DNS configuration", c.dnsUpstream.Name) 90 | return nil 91 | } 92 | 93 | c.responseMessage = temp 94 | 95 | if isLog { 96 | c.logAnswer("") 97 | } 98 | 99 | return c.responseMessage 100 | } 101 | 102 | func (c *RemoteClient) logAnswer(indicator string) { 103 | 104 | for _, a := range c.responseMessage.Answer { 105 | var name string 106 | if indicator != "" { 107 | name = indicator 108 | } else { 109 | name = c.dnsUpstream.Name 110 | } 111 | log.Debugf("Answer from %s: %s", name, a.String()) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /core/outbound/clients/remote_bundle.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 shawn1m. All rights reserved. 3 | * Use of this source code is governed by The MIT License (MIT) that can be 4 | * found in the LICENSE file.. 5 | */ 6 | 7 | package clients 8 | 9 | import ( 10 | "github.com/miekg/dns" 11 | "github.com/shawn1m/overture/core/outbound/clients/resolver" 12 | log "github.com/sirupsen/logrus" 13 | 14 | "github.com/shawn1m/overture/core/cache" 15 | "github.com/shawn1m/overture/core/common" 16 | ) 17 | 18 | type RemoteClientBundle struct { 19 | responseMessage *dns.Msg 20 | questionMessage *dns.Msg 21 | 22 | clients []*RemoteClient 23 | 24 | dnsUpstreams []*common.DNSUpstream 25 | inboundIP string 26 | minimumTTL int 27 | domainTTLMap map[string]uint32 28 | 29 | cache *cache.Cache 30 | Name string 31 | 32 | dnsResolvers []resolver.Resolver 33 | } 34 | 35 | func NewClientBundle(q *dns.Msg, ul []*common.DNSUpstream, resolvers []resolver.Resolver, ip string, minimumTTL int, cache *cache.Cache, name string, domainTTLMap map[string]uint32) *RemoteClientBundle { 36 | cb := &RemoteClientBundle{questionMessage: q.Copy(), dnsUpstreams: ul, dnsResolvers: resolvers, inboundIP: ip, minimumTTL: minimumTTL, cache: cache, Name: name, domainTTLMap: domainTTLMap} 37 | 38 | for i, u := range ul { 39 | c := NewClient(cb.questionMessage, u, cb.dnsResolvers[i], cb.inboundIP, cb.cache) 40 | cb.clients = append(cb.clients, c) 41 | } 42 | 43 | return cb 44 | } 45 | 46 | func (cb *RemoteClientBundle) Exchange(isCache bool, isLog bool) *dns.Msg { 47 | ch := make(chan *RemoteClient, len(cb.clients)) 48 | 49 | for _, o := range cb.clients { 50 | go func(c *RemoteClient, ch chan *RemoteClient) { 51 | c.Exchange(isLog) 52 | ch <- c 53 | }(o, ch) 54 | } 55 | 56 | var ec *RemoteClient 57 | 58 | for i := 0; i < len(cb.clients); i++ { 59 | c := <-ch 60 | if c != nil { 61 | ec = c 62 | if ec.responseMessage != nil && ec.responseMessage.Answer != nil { 63 | break 64 | } 65 | log.Debugf("DNSUpstream has %s returned None answer which will be discarded and wait for the next one", ec.dnsUpstream.Address) 66 | } 67 | } 68 | 69 | if ec != nil && ec.responseMessage != nil { 70 | cb.responseMessage = ec.responseMessage 71 | cb.questionMessage = ec.questionMessage 72 | 73 | common.SetMinimumTTL(cb.responseMessage, uint32(cb.minimumTTL)) 74 | common.SetTTLByMap(cb.responseMessage, cb.domainTTLMap) 75 | 76 | if isCache { 77 | cb.CacheResultIfNeeded() 78 | } 79 | } 80 | 81 | return cb.responseMessage 82 | } 83 | 84 | func (cb *RemoteClientBundle) ExchangeFromCache() *dns.Msg { 85 | for _, o := range cb.clients { 86 | cb.responseMessage = o.ExchangeFromCache() 87 | if cb.responseMessage != nil { 88 | return cb.responseMessage 89 | } 90 | } 91 | return cb.responseMessage 92 | } 93 | 94 | func (cb *RemoteClientBundle) CacheResultIfNeeded() { 95 | if cb.cache != nil { 96 | cb.cache.InsertMessage(cache.Key(cb.questionMessage.Question[0], common.GetEDNSClientSubnetIP(cb.questionMessage)), cb.responseMessage, uint32(cb.minimumTTL)) 97 | } 98 | } 99 | 100 | func (cb *RemoteClientBundle) IsType(t uint16) bool { 101 | return t == cb.questionMessage.Question[0].Qtype 102 | } 103 | 104 | func (cb *RemoteClientBundle) GetFirstQuestionDomain() string { 105 | return cb.questionMessage.Question[0].Name[:len(cb.questionMessage.Question[0].Name)-1] 106 | } 107 | 108 | func (cb *RemoteClientBundle) GetResponseMessage() *dns.Msg { 109 | return cb.responseMessage 110 | } 111 | -------------------------------------------------------------------------------- /core/outbound/clients/resolver/address.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 shawn1m. All rights reserved. 3 | * Use of this source code is governed by The MIT License (MIT) that can be 4 | * found in the LICENSE file.. 5 | */ 6 | 7 | // Package outbound implements multiple dns client and dispatcher for outbound connection. 8 | package resolver 9 | 10 | import ( 11 | "errors" 12 | "net" 13 | "net/url" 14 | "strings" 15 | 16 | log "github.com/sirupsen/logrus" 17 | ) 18 | 19 | func getDefaultPort(protocol string) (port string) { 20 | switch protocol { 21 | case "udp", "tcp": 22 | port = "53" 23 | case "tcp-tls": 24 | port = "853" 25 | case "https": 26 | port = "443" 27 | case "socks5": 28 | port = "1080" 29 | } 30 | return port 31 | } 32 | 33 | // ToNetwork convert dns protocol to network 34 | func ToNetwork(protocol string) string { 35 | switch protocol { 36 | case "udp": 37 | return "udp" 38 | case "tcp", "tcp-tls", "https": 39 | return "tcp" 40 | default: 41 | return "" 42 | } 43 | } 44 | 45 | // support two formats: scheme://127.0.0.1:1080 or 127.0.0.1:1080 46 | func extractUrl(rawAddress string, protocol string) (host string, port string, err error) { 47 | 48 | if !strings.Contains(rawAddress, "://") { 49 | rawAddress = protocol + "://" + rawAddress 50 | } 51 | 52 | uri, err := url.Parse(rawAddress) 53 | if err != nil { 54 | log.Warnf("url %s is invalid", rawAddress) 55 | return "", "", errors.New("url is invalid") 56 | } 57 | host = uri.Hostname() 58 | 59 | if len(uri.Scheme) == 0 || uri.Scheme != protocol { 60 | return "", "", errors.New("url is invalid") 61 | } 62 | 63 | port = uri.Port() 64 | if len(port) == 0 { 65 | port = getDefaultPort(protocol) 66 | } 67 | return 68 | } 69 | 70 | func ExtractFullUrl(rawAddress string, protocol string) (string, error) { 71 | host, port, err := extractUrl(rawAddress, protocol) 72 | return net.JoinHostPort(host, port), err 73 | } 74 | 75 | func extractTLSDNSAddress(rawAddress string, protocol string) (host string, port string, err error) { 76 | rawAddress = protocol + "://" + rawAddress 77 | s := strings.Split(rawAddress, "@") 78 | 79 | host, port, err = extractUrl(s[0], protocol) 80 | 81 | if err != nil { 82 | return "", "", nil 83 | } 84 | 85 | if len(s) == 2 && isJustIP(s[1]) { 86 | host = generateLiteralIPv6AddressIfNecessary(s[1]) 87 | } else { 88 | log.Warnf("dns server address %s is invalid", rawAddress) 89 | return "", "", errors.New("dns up server address is invalid") 90 | } 91 | return host, port, nil 92 | } 93 | 94 | func ExtractTLSDNSHostName(rawAddress string) (host string, err error) { 95 | rawAddress = "tcp-tls" + "://" + rawAddress 96 | s := strings.Split(rawAddress, "@") 97 | 98 | host, _, err = extractUrl(s[0], "tcp-tls") 99 | return host, err 100 | } 101 | 102 | func isJustIP(rawAddress string) bool { 103 | // If this rawAddress is not like "[::1]:5353", change [::1] to ::1 104 | if !strings.Contains(rawAddress, "]:") { 105 | rawAddress = generateLiteralIPv6AddressIfNecessary(rawAddress) 106 | } 107 | return net.ParseIP(rawAddress) != nil 108 | } 109 | 110 | func generateLiteralIPv6AddressIfNecessary(rawAddress string) string { 111 | rawAddress = strings.Replace(rawAddress, "[", "", 1) 112 | rawAddress = strings.Replace(rawAddress, "]", "", 1) 113 | return rawAddress 114 | } 115 | 116 | // ExtractDNSAddress parse all format, return literal IPv6 address 117 | func ExtractDNSAddress(rawAddress string, protocol string) (host string, port string, err error) { 118 | switch protocol { 119 | case "tcp-tls": 120 | host, port, err = extractTLSDNSAddress(rawAddress, protocol) 121 | default: 122 | host, port, err = extractUrl(rawAddress, protocol) 123 | } 124 | return host, port, err 125 | } 126 | -------------------------------------------------------------------------------- /core/outbound/clients/resolver/address_test.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import "testing" 4 | 5 | const ( 6 | ipv4Address = "8.8.8.8" 7 | ipv6Address = "[2001:4860:4860::8888]" 8 | literalIpa6Address = "2001:4860:4860::8888" 9 | ) 10 | 11 | func TestExtractDNSAddress(t *testing.T) { 12 | var tests = []struct { 13 | rawAddress string 14 | protocol string 15 | host string 16 | port string 17 | err error 18 | }{ 19 | {"dns.google:853@" + ipv6Address, "tcp-tls", literalIpa6Address, "853", nil}, 20 | {"dns.google:853@" + ipv4Address, "tcp-tls", ipv4Address, "853", nil}, 21 | {ipv4Address + ":5353", "tcp", ipv4Address, "5353", nil}, 22 | {ipv6Address + ":5353", "tcp", literalIpa6Address, "5353", nil}, 23 | {ipv4Address + ":5353", "udp", ipv4Address, "5353", nil}, 24 | {ipv6Address + ":5353", "udp", literalIpa6Address, "5353", nil}, 25 | {ipv4Address, "udp", ipv4Address, "53", nil}, 26 | {ipv6Address, "udp", literalIpa6Address, "53", nil}, 27 | {"https://dns.google/dns-query", "https", "dns.google", "443", nil}, 28 | {"dns.google/dns-query", "https", "dns.google", "443", nil}, 29 | {"https://dns.google:888/dns-query", "https", "dns.google", "888", nil}, 30 | } 31 | for _, tt := range tests { 32 | t.Run(tt.rawAddress+", "+tt.protocol, func(t *testing.T) { 33 | host, port, err := ExtractDNSAddress(tt.rawAddress, tt.protocol) 34 | testEqual(t, host, tt.host) 35 | testEqual(t, port, tt.port) 36 | testErr(t, err) 37 | }) 38 | } 39 | } 40 | 41 | func TestExtractFullUrl(t *testing.T) { 42 | var tests = []struct { 43 | url string 44 | protocol string 45 | out string 46 | }{ 47 | {"socks5://" + ipv4Address + ":80", "socks5", ipv4Address + ":80"}, 48 | {ipv4Address + ":80", "socks5", ipv4Address + ":80"}, 49 | {ipv6Address + ":80", "socks5", ipv6Address + ":80"}, 50 | {ipv6Address, "socks5", ipv6Address + ":1080"}, 51 | {ipv6Address, "https", ipv6Address + ":443"}, 52 | {"tcp-tls://" + ipv6Address, "tcp-tls", ipv6Address + ":853"}, 53 | {"" + ipv4Address + ":80", "socks5", ipv4Address + ":80"}, 54 | {"" + ipv6Address + ":80", "socks5", ipv6Address + ":80"}, 55 | {"" + ipv6Address, "socks5", ipv6Address + ":1080"}, 56 | {"abc.com", "socks5", "abc.com:1080"}, 57 | } 58 | for _, tt := range tests { 59 | t.Run(tt.url, func(t *testing.T) { 60 | url, err := ExtractFullUrl(tt.url, tt.protocol) 61 | testEqual(t, url, tt.out) 62 | testErr(t, err) 63 | }) 64 | } 65 | } 66 | 67 | func testEqual(t *testing.T, got string, want string) { 68 | if got != want { 69 | t.Errorf("got %s, want %s", got, want) 70 | } 71 | } 72 | 73 | func testErr(t *testing.T, err error) { 74 | if err != nil { 75 | t.Errorf("err is not nil: %s", err) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /core/outbound/clients/resolver/base_resolver.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 shawn1m. All rights reserved. 3 | * Use of this source code is governed by The MIT License (MIT) that can be 4 | * found in the LICENSE file.. 5 | */ 6 | package resolver 7 | 8 | import ( 9 | "github.com/miekg/dns" 10 | "github.com/silenceper/pool" 11 | log "github.com/sirupsen/logrus" 12 | "golang.org/x/net/proxy" 13 | "net" 14 | "time" 15 | 16 | "github.com/shawn1m/overture/core/common" 17 | ) 18 | 19 | type Resolver interface { 20 | Exchange(*dns.Msg) (*dns.Msg, error) 21 | Init() error 22 | } 23 | 24 | type BaseResolver struct { 25 | dnsUpstream *common.DNSUpstream 26 | } 27 | 28 | func (r *BaseResolver) Exchange(q *dns.Msg) (*dns.Msg, error) { 29 | conn, err := r.CreateBaseConn() 30 | if err != nil { 31 | return nil, err 32 | } 33 | defer conn.Close() 34 | return r.exchangeByConnWithoutClose(q, conn) 35 | } 36 | 37 | func (r *BaseResolver) exchangeByConnWithoutClose(q *dns.Msg, conn net.Conn) (msg *dns.Msg, err error) { 38 | if conn == nil { 39 | log.Fatal("Conn not initialized for exchangeByDNSClient") 40 | return nil, err 41 | } 42 | 43 | r.setTimeout(conn) 44 | dc := &dns.Conn{Conn: conn, UDPSize: 65535} 45 | err = dc.WriteMsg(q) 46 | if err != nil { 47 | log.Warnf("%s Fail: Send question message failed", r.dnsUpstream.Name) 48 | return nil, err 49 | } 50 | return dc.ReadMsg() 51 | } 52 | 53 | func (r *BaseResolver) Init() error { 54 | if r.dnsUpstream.TCPPoolConfig.Enable { 55 | if r.dnsUpstream.TCPPoolConfig.IdleTimeout != 0 { 56 | IdleTimeout = time.Duration(r.dnsUpstream.TCPPoolConfig.IdleTimeout) * time.Second 57 | } 58 | if r.dnsUpstream.TCPPoolConfig.MaxCapacity != 0 { 59 | MaxCapacity = r.dnsUpstream.TCPPoolConfig.MaxCapacity 60 | } 61 | if r.dnsUpstream.TCPPoolConfig.InitialCapacity != 0 { 62 | InitialCapacity = r.dnsUpstream.TCPPoolConfig.InitialCapacity 63 | } 64 | } 65 | return nil 66 | } 67 | 68 | func NewResolver(u *common.DNSUpstream) Resolver { 69 | var resolver Resolver 70 | switch u.Protocol { 71 | case "udp": 72 | resolver = &UDPResolver{BaseResolver: BaseResolver{u}} 73 | case "tcp": 74 | resolver = &TCPResolver{BaseResolver: BaseResolver{u}} 75 | case "tcp-tls": 76 | resolver = &TCPTLSResolver{BaseResolver: BaseResolver{u}} 77 | case "https": 78 | resolver = &HTTPSResolver{BaseResolver: BaseResolver{u}} 79 | default: 80 | log.Fatalf("Unsupported protocol: %s", u.Protocol) 81 | log.Errorf("Create resolver for %s failed", u.Name) 82 | return nil 83 | } 84 | err := resolver.Init() 85 | if err != nil { 86 | log.Errorf("Init resolver for %s failed", u.Name) 87 | } else { 88 | log.Debugf("Init resolver for %s succeed", u.Name) 89 | } 90 | return resolver 91 | } 92 | 93 | func (r *BaseResolver) CreateBaseConn() (net.Conn, error) { 94 | dialer := net.Dialer{Timeout: r.getDialTimeout()} 95 | dialerFunc := dialer.Dial 96 | if r.dnsUpstream.SOCKS5Address != "" { 97 | socksAddress, err := ExtractFullUrl(r.dnsUpstream.SOCKS5Address, "socks5") 98 | if err != nil { 99 | return nil, err 100 | } 101 | network := ToNetwork(r.dnsUpstream.Protocol) 102 | s, err := proxy.SOCKS5(network, socksAddress, nil, proxy.Direct) 103 | if err != nil { 104 | log.Warnf("Failed to connect to SOCKS5 proxy: %s", err) 105 | return nil, err 106 | } 107 | dialerFunc = s.Dial 108 | } 109 | 110 | network := ToNetwork(r.dnsUpstream.Protocol) 111 | host, port, err := ExtractDNSAddress(r.dnsUpstream.Address, r.dnsUpstream.Protocol) 112 | if err != nil { 113 | return nil, err 114 | } 115 | address := net.JoinHostPort(host, port) 116 | log.Debugf("Creating new connection to %s:%s", host, port) 117 | var conn net.Conn 118 | if conn, err = dialerFunc(network, address); err != nil { 119 | log.Warnf("Failed to connect to DNS upstream: %s", err) 120 | return nil, err 121 | } 122 | 123 | // the Timeout setting is now moved to each resolver to support pool's idle timeout 124 | // r.setTimeout(conn) 125 | return conn, err 126 | } 127 | 128 | var InitialCapacity = 0 129 | var IdleTimeout = 30 * time.Second 130 | var MaxCapacity = 15 131 | 132 | func (r *BaseResolver) setTimeout(conn net.Conn) { 133 | dnsTimeout := time.Duration(r.dnsUpstream.Timeout) * time.Second / 3 134 | conn.SetDeadline(time.Now().Add(dnsTimeout)) 135 | conn.SetReadDeadline(time.Now().Add(dnsTimeout)) 136 | conn.SetWriteDeadline(time.Now().Add(dnsTimeout)) 137 | } 138 | 139 | func (r *BaseResolver) getDialTimeout() time.Duration { 140 | return time.Duration(r.dnsUpstream.Timeout) * time.Second / 3 141 | } 142 | 143 | func (r *BaseResolver) setIdleTimeout(conn net.Conn) { 144 | conn.SetDeadline(time.Now().Add(IdleTimeout)) 145 | conn.SetReadDeadline(time.Now().Add(IdleTimeout)) 146 | conn.SetWriteDeadline(time.Now().Add(IdleTimeout)) 147 | } 148 | 149 | func (r *BaseResolver) createConnectionPool(connCreate func() (interface{}, error), connClose func(interface{}) error) (pool.Pool, error) { 150 | poolConfig := &pool.Config{ 151 | InitialCap: InitialCapacity, 152 | MaxCap: MaxCapacity, 153 | Factory: connCreate, 154 | Close: connClose, 155 | //Ping: ping, 156 | IdleTimeout: IdleTimeout, 157 | } 158 | return pool.NewChannelPool(poolConfig) 159 | } 160 | 161 | func (r *BaseResolver) exchangeByPool(q *dns.Msg, poolConn pool.Pool) (msg *dns.Msg, err error) { 162 | _conn, err := poolConn.Get() 163 | if err != nil { 164 | return nil, err 165 | } 166 | conn := _conn.(net.Conn) 167 | ret, err := r.exchangeByConnWithoutClose(q, conn) 168 | if err != nil { 169 | poolConn.Close(conn) 170 | } else { 171 | r.setIdleTimeout(conn) 172 | poolConn.Put(conn) 173 | } 174 | return ret, err 175 | } 176 | -------------------------------------------------------------------------------- /core/outbound/clients/resolver/https_resolver.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "bytes" 5 | "github.com/miekg/dns" 6 | "io/ioutil" 7 | "net" 8 | "net/http" 9 | ) 10 | 11 | type HTTPSResolver struct { 12 | BaseResolver 13 | client http.Client 14 | } 15 | 16 | func (r *HTTPSResolver) Exchange(q *dns.Msg) (*dns.Msg, error) { 17 | request, err := q.Pack() 18 | resp, err := r.client.Post(r.dnsUpstream.Address, "application/dns-message", 19 | bytes.NewBuffer(request)) 20 | if err != nil { 21 | return nil, err 22 | } 23 | defer resp.Body.Close() 24 | data, err := ioutil.ReadAll(resp.Body) 25 | if err != nil { 26 | return nil, err 27 | } 28 | msg := new(dns.Msg) 29 | err = msg.Unpack(data) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return msg, nil 34 | } 35 | 36 | func (r *HTTPSResolver) Init() error { 37 | err := r.BaseResolver.Init() 38 | if err != nil { 39 | return err 40 | } 41 | r.client = http.Client{ 42 | Transport: &http.Transport{ 43 | Dial: func(network, addr string) (net.Conn, error) { 44 | return r.CreateBaseConn() 45 | }, 46 | }, 47 | } 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /core/outbound/clients/resolver/resolver_test.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "github.com/miekg/dns" 5 | "github.com/shawn1m/overture/core/common" 6 | "net" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | var questionDomain = "www.yahoo.com." 12 | var udpUpstream = &common.DNSUpstream{ 13 | Name: "Test-UDP", 14 | Address: "114.114.114.114", 15 | Protocol: "udp", 16 | SOCKS5Address: "", 17 | Timeout: 6, 18 | EDNSClientSubnet: &common.EDNSClientSubnetType{ 19 | Policy: "disable", 20 | ExternalIP: "", 21 | NoCookie: false, 22 | }, 23 | } 24 | 25 | var tcpUpstream = &common.DNSUpstream{ 26 | Name: "Test-TCP", 27 | Address: "1.1.1.1", 28 | Protocol: "tcp", 29 | SOCKS5Address: "", 30 | Timeout: 6, 31 | EDNSClientSubnet: &common.EDNSClientSubnetType{ 32 | Policy: "disable", 33 | ExternalIP: "", 34 | NoCookie: false, 35 | }, 36 | } 37 | 38 | var tcpTlsUpstream = &common.DNSUpstream{ 39 | Name: "Test-TCP-TLS", 40 | Address: "dns.google@8.8.8.8", 41 | Protocol: "tcp-tls", 42 | SOCKS5Address: "", 43 | Timeout: 8, 44 | EDNSClientSubnet: &common.EDNSClientSubnetType{ 45 | Policy: "disable", 46 | ExternalIP: "", 47 | NoCookie: false, 48 | }, 49 | } 50 | 51 | var httpsUpstream = &common.DNSUpstream{ 52 | Name: "Test-HTTPS", 53 | Address: "https://dns.google/dns-query", 54 | Protocol: "https", 55 | SOCKS5Address: "", 56 | Timeout: 8, 57 | EDNSClientSubnet: &common.EDNSClientSubnetType{ 58 | Policy: "disable", 59 | ExternalIP: "", 60 | NoCookie: false, 61 | }, 62 | } 63 | 64 | func init() { 65 | os.Chdir("../..") 66 | } 67 | 68 | func TestDispatcher(t *testing.T) { 69 | testUDP(t) 70 | testTCP(t) 71 | testTCPTLS(t) 72 | testHTTPS(t) 73 | } 74 | 75 | func testUDP(t *testing.T) { 76 | q := getQueryMsg(questionDomain, dns.TypeA) 77 | resolver := NewResolver(udpUpstream) 78 | resp, err := resolver.Exchange(q) 79 | if err != nil { 80 | t.Errorf("Got error: %s", err) 81 | } 82 | if net.ParseIP(common.FindRecordByType(resp, dns.TypeA)).To4() == nil { 83 | t.Error(questionDomain + " should have A record") 84 | } 85 | } 86 | 87 | func testTCP(t *testing.T) { 88 | q := getQueryMsg(questionDomain, dns.TypeA) 89 | resolver := NewResolver(tcpUpstream) 90 | resp, _ := resolver.Exchange(q) 91 | if net.ParseIP(common.FindRecordByType(resp, dns.TypeA)).To4() == nil { 92 | t.Error(questionDomain + " should have A record") 93 | } 94 | } 95 | 96 | func testTCPTLS(t *testing.T) { 97 | q := getQueryMsg(questionDomain, dns.TypeA) 98 | resolver := NewResolver(tcpTlsUpstream) 99 | resp, _ := resolver.Exchange(q) 100 | if net.ParseIP(common.FindRecordByType(resp, dns.TypeA)).To4() == nil { 101 | t.Error(questionDomain + " should have A record") 102 | } 103 | } 104 | 105 | func testHTTPS(t *testing.T) { 106 | q := getQueryMsg(questionDomain, dns.TypeA) 107 | resolver := NewResolver(httpsUpstream) 108 | resp, _ := resolver.Exchange(q) 109 | if net.ParseIP(common.FindRecordByType(resp, dns.TypeA)).To4() == nil { 110 | t.Error(questionDomain + " should have A record") 111 | } 112 | } 113 | 114 | func getQueryMsg(z string, t uint16) *dns.Msg { 115 | q := new(dns.Msg) 116 | q.SetQuestion(z, t) 117 | return q 118 | } 119 | -------------------------------------------------------------------------------- /core/outbound/clients/resolver/tcp_resolver.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/miekg/dns" 7 | "github.com/silenceper/pool" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | type TCPResolver struct { 12 | BaseResolver 13 | poolConn pool.Pool 14 | } 15 | 16 | func (r *TCPResolver) Exchange(q *dns.Msg) (*dns.Msg, error) { 17 | if r.dnsUpstream.TCPPoolConfig.Enable { 18 | return r.BaseResolver.exchangeByPool(q, r.poolConn) 19 | } else { 20 | return r.BaseResolver.Exchange(q) 21 | } 22 | } 23 | 24 | func (r *TCPResolver) Init() error { 25 | err := r.BaseResolver.Init() 26 | if err != nil { 27 | return err 28 | } 29 | if r.dnsUpstream.TCPPoolConfig.Enable { 30 | r.poolConn, err = r.createConnectionPool( 31 | func() (interface{}, error) { return r.CreateBaseConn() }, 32 | func(v interface{}) error { return v.(net.Conn).Close() }) 33 | if err != nil { 34 | log.Debugf("Set %s pool's IdleTimeout to %d, InitialCapacity to %d, MaxCapacity to %d", r.dnsUpstream.Name, r.dnsUpstream.TCPPoolConfig.IdleTimeout, r.dnsUpstream.TCPPoolConfig.InitialCapacity, r.dnsUpstream.TCPPoolConfig.MaxCapacity) 35 | } 36 | } else { 37 | return nil 38 | } 39 | return err 40 | } 41 | -------------------------------------------------------------------------------- /core/outbound/clients/resolver/tcptls_resolver.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | 7 | "github.com/miekg/dns" 8 | "github.com/silenceper/pool" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type TCPTLSResolver struct { 13 | BaseResolver 14 | poolConn pool.Pool 15 | } 16 | 17 | func (r *TCPTLSResolver) Exchange(q *dns.Msg) (*dns.Msg, error) { 18 | if r.dnsUpstream.TCPPoolConfig.Enable { 19 | return r.BaseResolver.exchangeByPool(q, r.poolConn) 20 | } else { 21 | conn, err := r.createTlsConn() 22 | if err != nil { 23 | log.Warnf("createTlsConn failed: %s", err) 24 | return nil, err 25 | } 26 | defer conn.Close() 27 | return r.exchangeByConnWithoutClose(q, conn) 28 | } 29 | } 30 | 31 | func (r *TCPTLSResolver) createTlsConn() (conn net.Conn, err error) { 32 | conn, err = r.CreateBaseConn() 33 | if err != nil { 34 | return nil, err 35 | } 36 | host, err := ExtractTLSDNSHostName(r.dnsUpstream.Address) 37 | if err != nil { 38 | return nil, err 39 | } 40 | conf := &tls.Config{ 41 | InsecureSkipVerify: false, 42 | ServerName: host, 43 | } 44 | conn = tls.Client(conn, conf) 45 | 46 | return conn, nil 47 | } 48 | 49 | func (r *TCPTLSResolver) Init() error { 50 | err := r.BaseResolver.Init() 51 | if err != nil { 52 | return err 53 | } 54 | if r.dnsUpstream.TCPPoolConfig.Enable { 55 | r.poolConn, err = r.createConnectionPool( 56 | func() (interface{}, error) { return r.createTlsConn() }, 57 | func(v interface{}) error { return v.(net.Conn).Close() }) 58 | if err != nil { 59 | log.Debugf("Set %s pool's IdleTimeout to %d, InitialCapacity to %d, MaxCapacity to %d", r.dnsUpstream.Name, r.dnsUpstream.TCPPoolConfig.IdleTimeout, r.dnsUpstream.TCPPoolConfig.InitialCapacity, r.dnsUpstream.TCPPoolConfig.MaxCapacity) 60 | } 61 | } else { 62 | return nil 63 | } 64 | return err 65 | } 66 | -------------------------------------------------------------------------------- /core/outbound/clients/resolver/udp_resolver.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "github.com/miekg/dns" 5 | ) 6 | 7 | type UDPResolver struct { 8 | BaseResolver 9 | } 10 | 11 | func (r *UDPResolver) Exchange(q *dns.Msg) (*dns.Msg, error) { 12 | return r.BaseResolver.Exchange(q) 13 | } 14 | 15 | func (r *UDPResolver) Init() error { 16 | err := r.BaseResolver.Init() 17 | if err != nil { 18 | return err 19 | } 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /core/outbound/dispatcher.go: -------------------------------------------------------------------------------- 1 | package outbound 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/miekg/dns" 7 | "github.com/shawn1m/overture/core/outbound/clients/resolver" 8 | log "github.com/sirupsen/logrus" 9 | 10 | "github.com/shawn1m/overture/core/cache" 11 | "github.com/shawn1m/overture/core/common" 12 | "github.com/shawn1m/overture/core/hosts" 13 | "github.com/shawn1m/overture/core/matcher" 14 | "github.com/shawn1m/overture/core/outbound/clients" 15 | ) 16 | 17 | type Dispatcher struct { 18 | PrimaryDNS []*common.DNSUpstream 19 | AlternativeDNS []*common.DNSUpstream 20 | OnlyPrimaryDNS bool 21 | 22 | WhenPrimaryDNSAnswerNoneUse string 23 | IPNetworkPrimarySet *common.IPSet 24 | IPNetworkAlternativeSet *common.IPSet 25 | DomainPrimaryList matcher.Matcher 26 | DomainAlternativeList matcher.Matcher 27 | RedirectIPv6Record bool 28 | AlternativeDNSConcurrent bool 29 | 30 | MinimumTTL int 31 | DomainTTLMap map[string]uint32 32 | 33 | Hosts *hosts.Hosts 34 | Cache *cache.Cache 35 | 36 | primaryResolvers []resolver.Resolver 37 | alternativeResolvers []resolver.Resolver 38 | } 39 | 40 | func createResolver(ul []*common.DNSUpstream) (resolvers []resolver.Resolver) { 41 | resolvers = make([]resolver.Resolver, len(ul)) 42 | for i, u := range ul { 43 | resolvers[i] = resolver.NewResolver(u) 44 | } 45 | return resolvers 46 | } 47 | 48 | func (d *Dispatcher) Init() { 49 | d.primaryResolvers = createResolver(d.PrimaryDNS) 50 | d.alternativeResolvers = createResolver(d.AlternativeDNS) 51 | } 52 | 53 | func (d *Dispatcher) Exchange(query *dns.Msg, inboundIP string) *dns.Msg { 54 | PrimaryClientBundle := clients.NewClientBundle(query, d.PrimaryDNS, d.primaryResolvers, inboundIP, d.MinimumTTL, d.Cache, "Primary", d.DomainTTLMap) 55 | AlternativeClientBundle := clients.NewClientBundle(query, d.AlternativeDNS, d.alternativeResolvers, inboundIP, d.MinimumTTL, d.Cache, "Alternative", d.DomainTTLMap) 56 | 57 | var ActiveClientBundle *clients.RemoteClientBundle 58 | 59 | localClient := clients.NewLocalClient(query, d.Hosts, d.MinimumTTL, d.DomainTTLMap) 60 | resp := localClient.Exchange() 61 | if resp != nil { 62 | return resp 63 | } 64 | 65 | for _, cb := range []*clients.RemoteClientBundle{PrimaryClientBundle, AlternativeClientBundle} { 66 | resp := cb.ExchangeFromCache() 67 | if resp != nil { 68 | return resp 69 | } 70 | } 71 | 72 | if d.OnlyPrimaryDNS || d.isSelectDomain(PrimaryClientBundle, d.DomainPrimaryList) { 73 | ActiveClientBundle = PrimaryClientBundle 74 | return ActiveClientBundle.Exchange(true, true) 75 | } 76 | 77 | if ok := d.isExchangeForIPv6(query) || d.isSelectDomain(AlternativeClientBundle, d.DomainAlternativeList); ok { 78 | ActiveClientBundle = AlternativeClientBundle 79 | return ActiveClientBundle.Exchange(true, true) 80 | } 81 | 82 | ActiveClientBundle = d.selectByIPNetwork(PrimaryClientBundle, AlternativeClientBundle) 83 | 84 | // Only try to Cache result before return 85 | ActiveClientBundle.CacheResultIfNeeded() 86 | return ActiveClientBundle.GetResponseMessage() 87 | } 88 | 89 | func (d *Dispatcher) isExchangeForIPv6(query *dns.Msg) bool { 90 | if query.Question[0].Qtype == dns.TypeAAAA && d.RedirectIPv6Record { 91 | log.Debug("Finally use alternative DNS") 92 | return true 93 | } 94 | 95 | return false 96 | } 97 | 98 | func (d *Dispatcher) isSelectDomain(rcb *clients.RemoteClientBundle, dt matcher.Matcher) bool { 99 | if dt != nil { 100 | qn := rcb.GetFirstQuestionDomain() 101 | 102 | if dt.Has(qn) { 103 | log.WithFields(log.Fields{ 104 | "DNS": rcb.Name, 105 | "question": qn, 106 | "domain": qn, 107 | }).Debug("Matched") 108 | log.Debugf("Finally use %s DNS", rcb.Name) 109 | return true 110 | } 111 | 112 | log.Debugf("Domain %s match fail", rcb.Name) 113 | } else { 114 | log.Debug("Domain matcher is nil, not checking") 115 | } 116 | 117 | return false 118 | } 119 | 120 | func (d *Dispatcher) selectByIPNetwork(PrimaryClientBundle, AlternativeClientBundle *clients.RemoteClientBundle) *clients.RemoteClientBundle { 121 | primaryOut := make(chan *dns.Msg) 122 | alternateOut := make(chan *dns.Msg) 123 | go func() { 124 | primaryOut <- PrimaryClientBundle.Exchange(false, true) 125 | }() 126 | alternateFunc := func() { 127 | alternateOut <- AlternativeClientBundle.Exchange(false, true) 128 | } 129 | waitAlternateResp := func() { 130 | if !d.AlternativeDNSConcurrent { 131 | go alternateFunc() 132 | } 133 | <-alternateOut 134 | } 135 | if d.AlternativeDNSConcurrent { 136 | go alternateFunc() 137 | } 138 | primaryResponse := <-primaryOut 139 | 140 | if primaryResponse != nil { 141 | if primaryResponse.Answer == nil { 142 | if d.WhenPrimaryDNSAnswerNoneUse != "alternativeDNS" && d.WhenPrimaryDNSAnswerNoneUse != "AlternativeDNS" { 143 | log.Debug("primaryDNS response has no answer section but exist, finally use primaryDNS") 144 | return PrimaryClientBundle 145 | } else { 146 | log.Debug("primaryDNS response has no answer section but exist, finally use alternativeDNS") 147 | waitAlternateResp() 148 | return AlternativeClientBundle 149 | } 150 | } 151 | } else { 152 | log.Debug("Primary DNS return nil, finally use alternative DNS") 153 | waitAlternateResp() 154 | return AlternativeClientBundle 155 | } 156 | 157 | for _, a := range PrimaryClientBundle.GetResponseMessage().Answer { 158 | log.Debug("Try to match response ip address with IP network") 159 | var ip net.IP 160 | if a.Header().Rrtype == dns.TypeA { 161 | ip = net.ParseIP(a.(*dns.A).A.String()) 162 | } else if a.Header().Rrtype == dns.TypeAAAA { 163 | ip = net.ParseIP(a.(*dns.AAAA).AAAA.String()) 164 | } else { 165 | continue 166 | } 167 | if d.IPNetworkPrimarySet.Contains(ip, true, "primary") { 168 | log.Debug("Finally use primary DNS") 169 | return PrimaryClientBundle 170 | } 171 | if d.IPNetworkAlternativeSet.Contains(ip, true, "alternative") { 172 | log.Debug("Finally use alternative DNS") 173 | waitAlternateResp() 174 | return AlternativeClientBundle 175 | } 176 | } 177 | log.Debug("IP network match failed, finally use alternative DNS") 178 | waitAlternateResp() 179 | return AlternativeClientBundle 180 | } 181 | -------------------------------------------------------------------------------- /core/outbound/dispatcher_test.go: -------------------------------------------------------------------------------- 1 | package outbound 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/miekg/dns" 10 | 11 | "github.com/shawn1m/overture/core/common" 12 | "github.com/shawn1m/overture/core/config" 13 | ) 14 | 15 | var dispatcher Dispatcher 16 | var questionDomain = "www.yahoo.com." 17 | 18 | func init() { 19 | os.Chdir("../..") 20 | conf := config.NewConfig("config.test.yml") 21 | dispatcher = Dispatcher{ 22 | PrimaryDNS: conf.PrimaryDNS, 23 | AlternativeDNS: conf.AlternativeDNS, 24 | OnlyPrimaryDNS: conf.OnlyPrimaryDNS, 25 | WhenPrimaryDNSAnswerNoneUse: conf.WhenPrimaryDNSAnswerNoneUse, 26 | IPNetworkPrimarySet: conf.IPNetworkPrimarySet, 27 | IPNetworkAlternativeSet: conf.IPNetworkAlternativeSet, 28 | DomainPrimaryList: conf.DomainPrimaryList, 29 | DomainAlternativeList: conf.DomainAlternativeList, 30 | 31 | RedirectIPv6Record: conf.IPv6UseAlternativeDNS, 32 | AlternativeDNSConcurrent: conf.AlternativeDNSConcurrent, 33 | MinimumTTL: conf.MinimumTTL, 34 | DomainTTLMap: conf.DomainTTLMap, 35 | 36 | Hosts: conf.Hosts, 37 | Cache: conf.Cache, 38 | } 39 | dispatcher.Init() 40 | } 41 | 42 | func TestDispatcher(t *testing.T) { 43 | 44 | testA(t) 45 | testAAAA(t) 46 | testHosts(t) 47 | testIPResponse(t) 48 | testCache(t) 49 | } 50 | 51 | func testA(t *testing.T) { 52 | 53 | resp := exchange(questionDomain, dns.TypeA) 54 | if net.ParseIP(common.FindRecordByType(resp, dns.TypeA)).To4() == nil { 55 | t.Error(questionDomain + " should have A record") 56 | } 57 | } 58 | 59 | func testAAAA(t *testing.T) { 60 | 61 | resp := exchange(questionDomain, dns.TypeAAAA) 62 | if net.ParseIP(common.FindRecordByType(resp, dns.TypeAAAA)).To16() == nil { 63 | t.Error(questionDomain + " should have AAAA record") 64 | } 65 | } 66 | 67 | func testHosts(t *testing.T) { 68 | 69 | resp := exchange("localhost.", dns.TypeA) 70 | if common.FindRecordByType(resp, dns.TypeA) != "127.0.0.1" { 71 | t.Error("localhost should be 127.0.0.1") 72 | } 73 | } 74 | 75 | func testIPResponse(t *testing.T) { 76 | 77 | resp := exchange("127.0.0.1.", dns.TypeA) 78 | if common.FindRecordByType(resp, dns.TypeA) != "127.0.0.1" { 79 | t.Error("127.0.0.1 should be 127.0.0.1") 80 | } 81 | 82 | resp = exchange("fe80::7f:4f42:3f4d:f4c8.", dns.TypeAAAA) 83 | if common.FindRecordByType(resp, dns.TypeAAAA) != "fe80::7f:4f42:3f4d:f4c8" { 84 | t.Error("fe80::7f:4f42:3f4d:f4c8 should be fe80::7f:4f42:3f4d:f4c8") 85 | } 86 | } 87 | 88 | func testCache(t *testing.T) { 89 | 90 | exchange(questionDomain, dns.TypeA) 91 | now := time.Now() 92 | exchange(questionDomain, dns.TypeA) 93 | if time.Since(now) > 10*time.Millisecond { 94 | t.Error(time.Since(now).String() + " " + "Cache response slower than 10ms") 95 | } 96 | } 97 | 98 | func exchange(z string, t uint16) *dns.Msg { 99 | 100 | q := new(dns.Msg) 101 | q.SetQuestion(z, t) 102 | return dispatcher.Exchange(q, "") 103 | } 104 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/shawn1m/overture 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/coredns/coredns v1.9.2 7 | github.com/go-redis/redis/v8 v8.11.5 8 | github.com/miekg/dns v1.1.49 9 | github.com/silenceper/pool v1.0.0 10 | github.com/sirupsen/logrus v1.8.1 11 | golang.org/x/net v0.0.0-20220517181318-183a9ca12b87 12 | gopkg.in/yaml.v2 v2.4.0 13 | ) 14 | 15 | require ( 16 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 17 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 18 | golang.org/x/mod v0.4.2 // indirect 19 | golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba // indirect 20 | golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect 21 | golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 2 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 3 | github.com/coredns/coredns v1.9.2 h1:r1uPYQ/HKQq8zoQ3NP2V4k1hxb3Yw2xN9AXcXzofh6U= 4 | github.com/coredns/coredns v1.9.2/go.mod h1:U44W7RM94WPp8soWjsm8g08oOQ1A6D1xu4VyCYJ79cc= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 8 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 9 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 10 | github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= 11 | github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= 12 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 13 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 14 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 15 | github.com/miekg/dns v1.1.49 h1:qe0mQU3Z/XpFeE+AEBo2rqaS1IPBJ3anmqZ4XiZJVG8= 16 | github.com/miekg/dns v1.1.49/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= 17 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 18 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 19 | github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= 20 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 21 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 22 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 23 | github.com/prometheus/common v0.34.0 h1:RBmGO9d/FVjqHT0yUGQwBJhkwKV+wPCn7KGpvfab0uE= 24 | github.com/silenceper/pool v1.0.0 h1:JTCaA+U6hJAA0P8nCx+JfsRCHMwLTfatsm5QXelffmU= 25 | github.com/silenceper/pool v1.0.0/go.mod h1:3DN13bqAbq86Lmzf6iUXWEPIWFPOSYVfaoceFvilKKI= 26 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 27 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 28 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 29 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 30 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 31 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 32 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 33 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 34 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 35 | golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= 36 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 37 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 38 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 39 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 40 | golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 41 | golang.org/x/net v0.0.0-20220517181318-183a9ca12b87 h1:cCR+9mKLOGyX4Zx+uBZDXEDAQsvKQ/XbW4vreG5v1jU= 42 | golang.org/x/net v0.0.0-20220517181318-183a9ca12b87/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 43 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 44 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 45 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 46 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 47 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 48 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 49 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 50 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 53 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 54 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 55 | golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba h1:AyHWHCBVlIYI5rgEM3o+1PLd0sLPcIAoaUckGQMaWtw= 56 | golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 57 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 58 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 59 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 60 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 61 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 62 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 63 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 64 | golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0= 65 | golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 66 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 67 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 68 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 69 | golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= 70 | golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 71 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 72 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 73 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 74 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 75 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 76 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 77 | -------------------------------------------------------------------------------- /main/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 shawn1m. All rights reserved. 2 | // Use of this source code is governed by The MIT License (MIT) that can be 3 | // found in the LICENSE file. 4 | 5 | // Package main is the entry point of whole program. 6 | package main 7 | 8 | import ( 9 | "flag" 10 | "fmt" 11 | "io" 12 | "os" 13 | "os/signal" 14 | "runtime" 15 | "syscall" 16 | 17 | log "github.com/sirupsen/logrus" 18 | 19 | "github.com/shawn1m/overture/core" 20 | ) 21 | 22 | // For auto version building 23 | // go build -ldflags "-X main.version=version" 24 | var ( 25 | version string 26 | 27 | configPath = flag.String("c", "./config.yml", "config file path") 28 | logPath = flag.String("l", "", "log file path") 29 | isLogVerbose = flag.Bool("v", false, "verbose mode") 30 | processorNumber = flag.Int("p", runtime.NumCPU(), "number of processor to use") 31 | isShowVersion = flag.Bool("V", false, "current version of overture") 32 | ) 33 | 34 | func main() { 35 | flag.Parse() 36 | 37 | if *isShowVersion { 38 | fmt.Println(version) 39 | return 40 | } 41 | 42 | log.SetFormatter(&log.TextFormatter{ 43 | FullTimestamp: true, 44 | TimestampFormat: "2006-01-02 15:04:05", 45 | }) 46 | 47 | if *isLogVerbose { 48 | log.SetLevel(log.DebugLevel) 49 | } else { 50 | log.SetLevel(log.InfoLevel) 51 | } 52 | 53 | if *logPath != "" { 54 | lf, err := os.OpenFile(*logPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0640) 55 | if err != nil { 56 | log.Errorf("Unable to open log file for writing: %s", err) 57 | } else { 58 | log.SetOutput(io.MultiWriter(lf, os.Stdout)) 59 | } 60 | } 61 | 62 | log.Infof("Overture %s", version) 63 | log.Info("If you want to use overture safe and sound, please read the README.md first from the project's repo: https://github.com/shawn1m/overture") 64 | 65 | runtime.GOMAXPROCS(*processorNumber) 66 | 67 | // Waiting for SIGTERM to close app. InitServer() always return. 68 | stop := make(chan os.Signal, 1) 69 | signal.Notify(stop, syscall.SIGTERM) 70 | 71 | core.InitServer(*configPath) 72 | <-stop 73 | } 74 | -------------------------------------------------------------------------------- /main/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestMain(m *testing.M) { 9 | // call flag.Parse() here if TestMain uses flags 10 | setup() 11 | os.Exit(m.Run()) 12 | shutdown() 13 | } 14 | 15 | func setup() { 16 | 17 | } 18 | 19 | func shutdown() { 20 | 21 | } 22 | --------------------------------------------------------------------------------