├── .github └── workflows │ └── go.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── chinaip ├── chinaip.go ├── chinaip_test.go ├── db.go ├── requirements.txt └── update_db.py ├── freedns ├── dns_cache.go ├── dns_cache_test.go ├── freedns.go ├── freedns_test.go ├── resolve.go ├── resolve_test.go ├── upstream_provider.go ├── upstream_provider_test.go ├── utils.go └── utils_test.go ├── go.mod ├── go.sum └── main.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | 5 | build_test: 6 | name: Build 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - name: Set up Go 1.13 11 | uses: actions/setup-go@v1 12 | with: 13 | go-version: 1.13 14 | id: go 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v1 18 | 19 | - name: Test 20 | run: make test 21 | 22 | - name: Build for All Platforms 23 | run: make build_all 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | freedns-go 3 | .vscode -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:alpine as update_db 2 | WORKDIR /usr/src/app 3 | COPY chinaip . 4 | RUN pip3 install -r requirements.txt 5 | RUN python3 update_db.py 6 | 7 | FROM golang:alpine as builder 8 | WORKDIR /go/src/github.com/tuna/freedns-go 9 | COPY go.* ./ 10 | RUN go mod download 11 | COPY . . 12 | COPY --from=update_db /usr/src/app/db.go chinaip/ 13 | RUN go build -o ./build/freedns-go 14 | 15 | 16 | FROM alpine 17 | COPY --from=builder /go/src/github.com/tuna/freedns-go/build/freedns-go ./ 18 | ENTRYPOINT ["./freedns-go"] 19 | CMD ["-f", "114.114.114.114:53", "-c", "8.8.8.8:53", "-l", "0.0.0.0:53"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tsinghua University TUNA Association 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build_all 2 | 3 | build_all: 4 | mkdir -p ./build 5 | env GOOS=linux GOARCH=amd64 go build -o ./build/freedns-go-linux-amd64 6 | env GOOS=linux GOARCH=arm64 go build -o ./build/freedns-go-linux-arm64 7 | env GOOS=linux GOARCH=arm go build -o ./build/freedns-go-linux-arm 8 | env GOOS=linux GOARCH=mips go build -o ./build/freedns-go-linux-mips 9 | env GOOS=linux GOARCH=mipsle go build -o ./build/freedns-go-linux-mipsle 10 | env GOOS=linux GOARCH=mips64 go build -o ./build/freedns-go-linux-mips64 11 | env GOOS=linux GOARCH=mips64le go build -o ./build/freedns-go-linux-mips64le 12 | env GOOS=darwin GOARCH=amd64 go build -o ./build/freedns-go-macos-amd64 13 | env GOOS=darwin GOARCH=arm64 go build -o ./build/freedns-go-macos-arm64 14 | 15 | update_db: 16 | python3 ./chinaip/update_db.py 17 | mv ./db.go ./chinaip/db.go 18 | 19 | test: 20 | go test ./chinaip 21 | go test ./freedns 22 | 23 | .PHONY: build_all update_db test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # freedns-go 2 | 3 | Optimized DNS Server for Chinese users. 4 | 5 | `freedns-go` is CDN friendly since it prefers the domestic DNS upstream for websites having servers in mainland China. 6 | 7 | ## Usage 8 | 9 | Download the prebuilt binary from the [releases](https://github.com/Chenyao2333/freedns-go/releases) page. Use `-f 114.114.114.114:53` to set the upstream in China, and use `-c 8.8.8.8:53` to set the upstream that returns usable results for foreign sites. 10 | 11 | ``` 12 | sudo ./freedns-go -f 114.114.114.114:53 -c 8.8.8.8:53 -l 0.0.0.0:53 13 | ``` 14 | 15 | Issue a request to the server just started: 16 | 17 | ``` 18 | host baidu.com 127.0.0.1 19 | host google.com 127.0.0.1 20 | ``` 21 | 22 | `baidu.com` is dispatched to `114.114.114.114`, but `google.com` is dispatched to `8.8.8.8` because its server is not located in China. 23 | 24 | ![](https://pppublic.oss-cn-beijing.aliyuncs.com/pics/%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202018-05-08%20%E4%B8%8B%E5%8D%889.49.36.png) 25 | 26 | ### How does it work? 27 | 28 | `freedns-go` tries to dispatch the request to a DNS upstream located in China, which is fast but maybe poisoned. If it detected any IP addresses not belonged to China, which means there is a chance that the domain is spoofed, then `freedns-go` uses the foreign upstream. 29 | 30 | The cache policy is lazy cache. If there are some records are expired but in the cache, it will return the cached records and update it on the background. 31 | 32 | **Note: freedns-go just dispatches your queries to the optimal upstreams. Your network should be able to reach those upstreams (e.g. 8.8.8.8). You can do that by port forwarding, or any ways you like..** 33 | -------------------------------------------------------------------------------- /chinaip/chinaip.go: -------------------------------------------------------------------------------- 1 | package chinaip 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | type Error string 9 | 10 | func (e Error) Error() string { 11 | return string(e) 12 | } 13 | 14 | // IP2Int converts IP from string format to int format 15 | func IP2Int(ip string) (uint32, error) { 16 | strs := strings.Split(ip, ".") 17 | if len(strs) != 4 { 18 | return 0, Error("not ipv4 addr") 19 | } 20 | ret := uint32(0) 21 | mul := uint32(1) 22 | for i := 3; i >= 0; i-- { 23 | a, err := strconv.Atoi(strs[i]) 24 | if err != nil { 25 | return 0, err 26 | } 27 | ret += uint32(a) * mul 28 | mul *= 256 29 | } 30 | return ret, nil 31 | } 32 | 33 | // IsChinaIP returns whether an IPv4 address belongs to China 34 | func IsChinaIP(ip string) bool { 35 | var i, err = IP2Int(ip) 36 | if err != nil { 37 | return false 38 | } 39 | var l = 0 40 | var r = len(chinaIPs) - 1 41 | for l <= r { 42 | var mid = int((l + r) / 2) 43 | if i < chinaIPs[mid][0] { 44 | r = mid - 1 45 | } else if i > chinaIPs[mid][1] { 46 | l = mid + 1 47 | } else { 48 | return true 49 | } 50 | } 51 | return false 52 | } 53 | -------------------------------------------------------------------------------- /chinaip/chinaip_test.go: -------------------------------------------------------------------------------- 1 | package chinaip_test 2 | 3 | import "testing" 4 | import "github.com/tuna/freedns-go/chinaip" 5 | 6 | func TestIsChinaIP(t *testing.T) { 7 | var cn_ips = []string{"114.114.114.114", "220.181.57.216"} 8 | var non_cn_ips = []string{"8.8.8.8", "172.217.14.78", "255.255.255.255", "wtf", "114.114.114"} 9 | 10 | for _, ip := range cn_ips { 11 | if !chinaip.IsChinaIP(ip) { 12 | t.Errorf("%s is China IP!", ip) 13 | } 14 | } 15 | 16 | for _, ip := range non_cn_ips { 17 | if chinaip.IsChinaIP(ip) { 18 | t.Errorf("%s isn't China IP!", ip) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /chinaip/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /chinaip/update_db.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | import requests 4 | 5 | IP_LIST_URL = "https://raw.githubusercontent.com/17mon/china_ip_list/master/china_ip_list.txt" 6 | 7 | def cidr_list(): 8 | r = requests.get(IP_LIST_URL) 9 | if r.status_code != 200: 10 | raise Exception("%s status code is %d" % (IP_LIST_URL, r.status_code)) 11 | 12 | return r.text.split() 13 | 14 | def to_int(a, b, c, d): 15 | return a*256*256*256 + b*256*256 + c*256 + d 16 | 17 | def parse(cidr): 18 | ip, mask = cidr.split("/") 19 | mask = int(mask) 20 | a, b, c, d = list(map(int, ip.split("."))) 21 | i = to_int(a, b, c, d) 22 | start = i >> (32 - mask) << (32 - mask) 23 | end = start + 2**(32 - mask) - 1 24 | return start, end 25 | 26 | def gen(): 27 | s = """package chinaip 28 | 29 | var chinaIPs = [][]uint32{ 30 | """ 31 | cidrs = cidr_list() 32 | for cidr in cidrs: 33 | if (len(cidr) > 3): 34 | start, end = parse(cidr) 35 | s += " {%d, %d}, // %s\n" % (start, end, cidr) 36 | 37 | s += "}\n" 38 | return s 39 | 40 | def main(): 41 | s = gen() 42 | with open("db.go", "w") as f: 43 | f.write(s) 44 | 45 | if __name__ == "__main__": 46 | main() 47 | -------------------------------------------------------------------------------- /freedns/dns_cache.go: -------------------------------------------------------------------------------- 1 | package freedns 2 | 3 | import ( 4 | "time" 5 | 6 | goc "github.com/louchenyao/golang-cache" 7 | "github.com/miekg/dns" 8 | ) 9 | 10 | type cacheEntry struct { 11 | putin time.Time 12 | reply *dns.Msg 13 | } 14 | 15 | type dnsCache struct { 16 | backend *goc.Cache 17 | } 18 | 19 | func newDNSCache(maxCap int) *dnsCache { 20 | c, _ := goc.NewCache("lru", maxCap) 21 | return &dnsCache{ 22 | backend: c, 23 | } 24 | } 25 | 26 | func (c *dnsCache) set(res *dns.Msg, net string) { 27 | key := requestToString(res.Question[0], res.RecursionDesired, net) 28 | 29 | c.backend.Set(key, cacheEntry{ 30 | putin: time.Now(), 31 | reply: res.Copy(), // .Copy() is mandatory 32 | }) 33 | } 34 | 35 | func (c *dnsCache) lookup(q dns.Question, recursion bool, net string) (*dns.Msg, bool) { 36 | key := requestToString(q, recursion, net) 37 | ci, ok := c.backend.Get(key) 38 | if ok { 39 | entry := ci.(cacheEntry) 40 | res := entry.reply.Copy() // .Copy() is mandatory 41 | delta := time.Now().Sub(entry.putin).Seconds() 42 | needUpdate := subTTL(res, int(delta)) 43 | 44 | return res, needUpdate 45 | } 46 | return nil, true 47 | } 48 | 49 | // requestToString generates a string that uniquely identifies the request. 50 | func requestToString(q dns.Question, recursion bool, net string) string { 51 | s := q.Name + "_" + dns.TypeToString[q.Qtype] + "_" + dns.ClassToString[q.Qclass] 52 | if recursion { 53 | s += "_1" 54 | } else { 55 | s += "_0" 56 | } 57 | s += "_" + net 58 | return s 59 | } 60 | 61 | // subTTL substracts the ttl of `res` by delta in place, 62 | // and returns true if it will be expired in 3 seconds. 63 | func subTTL(res *dns.Msg, delta int) bool { 64 | needUpdate := false 65 | S := func(rr []dns.RR) { 66 | for i := 0; i < len(rr); i++ { 67 | newTTL := int(rr[i].Header().Ttl) 68 | newTTL -= delta 69 | 70 | if newTTL <= 3 { 71 | newTTL = 3 72 | needUpdate = true 73 | } 74 | 75 | rr[i].Header().Ttl = uint32(newTTL) 76 | } 77 | } 78 | 79 | S(res.Answer) 80 | S(res.Ns) 81 | S(res.Extra) 82 | 83 | return needUpdate 84 | } 85 | -------------------------------------------------------------------------------- /freedns/dns_cache_test.go: -------------------------------------------------------------------------------- 1 | package freedns 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | "time" 7 | 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | func TestAll(t *testing.T) { 12 | req := &dns.Msg{ 13 | MsgHdr: dns.MsgHdr{ 14 | RecursionDesired: true, 15 | }, 16 | Question: []dns.Question{dns.Question{ 17 | Name: "example.com", 18 | Qtype: dns.TypeA, 19 | Qclass: dns.ClassANY, 20 | }}, 21 | Answer: []dns.RR{ 22 | &dns.A{ 23 | Hdr: dns.RR_Header{ 24 | Name: "example.com", 25 | Ttl: 5, 26 | }, 27 | A: net.IPv4(127, 0, 0, 1), 28 | }, 29 | }, 30 | } 31 | 32 | c := newDNSCache(10) 33 | c.set(req, "udp") 34 | 35 | // query 1 36 | time.Sleep(1 * time.Second) 37 | res, upd := c.lookup(req.Question[0], req.RecursionDesired, "udp") 38 | if res.Answer[0].(*dns.A).Hdr.Name != req.Answer[0].(*dns.A).Hdr.Name { 39 | t.Errorf("lookup returns wrong result!") 40 | } 41 | if upd || res.Answer[0].(*dns.A).Hdr.Ttl <= 3 { 42 | t.Errorf("the ttl should be 4 and do not need to update") 43 | } 44 | 45 | // query 2 46 | time.Sleep(1 * time.Second) 47 | res, upd = c.lookup(req.Question[0], req.RecursionDesired, "udp") 48 | if !upd || res.Answer[0].(*dns.A).Hdr.Ttl > 3 { 49 | t.Errorf("the tll should be no more than 3 and need to update") 50 | } 51 | 52 | // query 3 53 | req.Question[0].Name = "random.org" 54 | res, upd = c.lookup(req.Question[0], req.RecursionDesired, "udp") 55 | if res != nil { 56 | t.Errorf("res should be nil") 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /freedns/freedns.go: -------------------------------------------------------------------------------- 1 | package freedns 2 | 3 | import ( 4 | "github.com/miekg/dns" 5 | "github.com/sirupsen/logrus" 6 | ) 7 | 8 | // Config stores the configuration for the Server 9 | type Config struct { 10 | FastUpstream string 11 | CleanUpstream string 12 | Listen string 13 | CacheCap int // the maximum items can be cached 14 | LogLevel string 15 | } 16 | 17 | // Server is type of the freedns server instance 18 | type Server struct { 19 | config Config 20 | 21 | udpServer *dns.Server 22 | tcpServer *dns.Server 23 | 24 | resolver *spoofingProofResolver 25 | recordsCache *dnsCache 26 | } 27 | 28 | var log = logrus.New() 29 | 30 | // Error is the freedns error type 31 | type Error string 32 | 33 | func (e Error) Error() string { 34 | return string(e) 35 | } 36 | 37 | // NewServer creates a new freedns server instance. 38 | func NewServer(cfg Config) (*Server, error) { 39 | s := &Server{} 40 | 41 | // set log level 42 | if level, parseError := logrus.ParseLevel(cfg.LogLevel); parseError == nil { 43 | log.SetLevel(level) 44 | } 45 | 46 | // normalize and setlisten address 47 | if cfg.Listen == "" { 48 | cfg.Listen = "127.0.0.1" 49 | } 50 | var err error 51 | if cfg.Listen, err = normalizeDnsAddress(cfg.Listen); err != nil { 52 | return nil, err 53 | } 54 | 55 | s.config = cfg 56 | 57 | var fastUpstreamProvider, cleanUpstreamProvider upstreamProvider 58 | fastUpstreamProvider, err = newUpstreamProvider(cfg.FastUpstream) 59 | if err != nil { 60 | return nil, err 61 | } 62 | cleanUpstreamProvider, err = newUpstreamProvider(cfg.CleanUpstream) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | s.config = cfg 68 | s.udpServer = &dns.Server{ 69 | Addr: s.config.Listen, 70 | Net: "udp", 71 | Handler: dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) { 72 | s.handle(w, req, "udp") 73 | }), 74 | } 75 | 76 | s.tcpServer = &dns.Server{ 77 | Addr: s.config.Listen, 78 | Net: "tcp", 79 | Handler: dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) { 80 | s.handle(w, req, "tcp") 81 | }), 82 | } 83 | 84 | s.recordsCache = newDNSCache(cfg.CacheCap) 85 | 86 | s.resolver = newSpoofingProofResolver(fastUpstreamProvider, cleanUpstreamProvider, cfg.CacheCap) 87 | 88 | return s, nil 89 | } 90 | 91 | // Run tcp and udp server. 92 | func (s *Server) Run() error { 93 | errChan := make(chan error, 2) 94 | 95 | go func() { 96 | err := s.tcpServer.ListenAndServe() 97 | errChan <- err 98 | }() 99 | 100 | go func() { 101 | err := s.udpServer.ListenAndServe() 102 | errChan <- err 103 | }() 104 | 105 | select { 106 | case err := <-errChan: 107 | s.tcpServer.Shutdown() 108 | s.udpServer.Shutdown() 109 | return err 110 | } 111 | } 112 | 113 | // Shutdown shuts down the freedns server 114 | func (s *Server) Shutdown() { 115 | s.tcpServer.Shutdown() 116 | s.udpServer.Shutdown() 117 | } 118 | 119 | func (s *Server) handle(w dns.ResponseWriter, req *dns.Msg, net string) { 120 | res := &dns.Msg{} 121 | 122 | if len(req.Question) < 1 { 123 | res.SetRcode(req, dns.RcodeBadName) 124 | w.WriteMsg(res) 125 | log.WithFields(logrus.Fields{ 126 | "op": "handle", 127 | "msg": "request without questions", 128 | }).Warn() 129 | return 130 | } 131 | 132 | res, upstream := s.lookup(req, net) 133 | w.WriteMsg(res) 134 | 135 | // logging 136 | l := log.WithFields(logrus.Fields{ 137 | "op": "handle", 138 | "domain": req.Question[0].Name, 139 | "type": dns.TypeToString[req.Question[0].Qtype], 140 | "upstream": upstream, 141 | "status": dns.RcodeToString[res.Rcode], 142 | }) 143 | if res.Rcode == dns.RcodeSuccess { 144 | l.Info() 145 | } else { 146 | l.Warn() 147 | } 148 | } 149 | 150 | // lookup queries the dns request `q` on either the local cache or upstreams, 151 | // and returns the result and which upstream is used. It updates the local cache 152 | // if necessary. 153 | func (s *Server) lookup(req *dns.Msg, net string) (*dns.Msg, string) { 154 | // 1. lookup the cache first 155 | res, upd := s.recordsCache.lookup(req.Question[0], req.RecursionDesired, net) 156 | var upstream string 157 | 158 | if res != nil { 159 | if upd { 160 | go func() { 161 | r, u := s.resolver.resolve(req.Question[0], req.RecursionDesired, net) 162 | if r.Rcode == dns.RcodeSuccess { 163 | log.WithFields(logrus.Fields{ 164 | "op": "update_cache", 165 | "domain": req.Question[0].Name, 166 | "type": dns.TypeToString[req.Question[0].Qtype], 167 | "upstream": u, 168 | }).Info() 169 | s.recordsCache.set(r, net) 170 | } 171 | }() 172 | } 173 | upstream = "cache" 174 | } else { 175 | res, upstream = s.resolver.resolve(req.Question[0], req.RecursionDesired, net) 176 | if res.Rcode == dns.RcodeSuccess { 177 | log.WithFields(logrus.Fields{ 178 | "op": "update_cache", 179 | "domain": req.Question[0].Name, 180 | "type": dns.TypeToString[req.Question[0].Qtype], 181 | "upstream": upstream, 182 | }).Info() 183 | s.recordsCache.set(res, net) 184 | } 185 | } 186 | 187 | // dns.Msg.SetReply() always set the Rcode to RcodeSuccess which we do not want 188 | rcode := res.Rcode 189 | res.SetReply(req) 190 | res.Rcode = rcode 191 | return res, upstream 192 | } 193 | -------------------------------------------------------------------------------- /freedns/freedns_test.go: -------------------------------------------------------------------------------- 1 | package freedns 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | func TestSmokingNewRunAndShutdown(t *testing.T) { 10 | // new the server 11 | s, err := NewServer(Config{ 12 | FastUpstream: "114.114.114.114", 13 | CleanUpstream: "8.8.8.8", 14 | Listen: "127.0.0.1:52345", 15 | CacheCap: 1024 * 5, 16 | }) 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | 21 | // run the server 22 | shut := make(chan bool, 2) 23 | go func() { 24 | err := s.Run() 25 | if err != nil { 26 | t.Error(err) 27 | } 28 | _ = <-shut 29 | s.Shutdown() 30 | }() 31 | 32 | tests := []struct { 33 | domain string 34 | qtype uint16 35 | net string 36 | expectedUpstream string 37 | }{ 38 | {"ustc.edu.cn.", dns.TypeMX, "udp", "8.8.8.8:53"}, 39 | {"ustc.edu.cn.", dns.TypeA, "udp", "114.114.114.114:53"}, 40 | {"ustc.edu.cn.", dns.TypeMX, "udp", "114.114.114.114:53"}, 41 | {"google.com.", dns.TypeA, "udp", "8.8.8.8:53"}, 42 | {"mi.cn.", dns.TypeA, "udp", "114.114.114.114:53"}, 43 | {"xiaomi.com.", dns.TypeA, "udp", "114.114.114.114:53"}, 44 | {"youtube.com.", dns.TypeA, "udp", "8.8.8.8:53"}, 45 | {"twitter.com.", dns.TypeA, "tcp", "8.8.8.8:53"}, 46 | } 47 | 48 | for _, tt := range tests { 49 | q := dns.Question{ 50 | Name: tt.domain, 51 | Qtype: tt.qtype, 52 | Qclass: dns.ClassINET, 53 | } 54 | 55 | want, _ := naiveResolve(q, true, tt.net, tt.expectedUpstream) 56 | got, err := naiveResolve(q, true, tt.net, "127.0.0.1:52345") 57 | 58 | if err != nil { 59 | t.Error(err) 60 | } 61 | 62 | if want == nil { 63 | t.Errorf("want is nil") 64 | continue 65 | } 66 | 67 | if got == nil { 68 | t.Errorf("got is nil") 69 | continue 70 | } 71 | 72 | if len(want.Answer) != len(got.Answer) || len(want.Question) != len(got.Question) || len(want.Extra) != len(got.Extra) { 73 | t.Errorf("got different resolve results from expectedUpstream and freedns") 74 | } 75 | } 76 | 77 | shut <- true 78 | } 79 | -------------------------------------------------------------------------------- /freedns/resolve.go: -------------------------------------------------------------------------------- 1 | package freedns 2 | 3 | import ( 4 | "time" 5 | 6 | goc "github.com/louchenyao/golang-cache" 7 | "github.com/miekg/dns" 8 | "github.com/sirupsen/logrus" 9 | "github.com/tuna/freedns-go/chinaip" 10 | ) 11 | 12 | // spoofingProofResolver can resolve the DNS request with 100% confidence. 13 | type spoofingProofResolver struct { 14 | fastUpstreamProvider upstreamProvider 15 | cleanUpstreamProvider upstreamProvider 16 | 17 | // cnDomains caches if a domain belongs to China. 18 | cnDomains *goc.Cache 19 | } 20 | 21 | func newSpoofingProofResolver(fastUpstreamProvider upstreamProvider, cleanUpstreamProvider upstreamProvider, cacheCap int) *spoofingProofResolver { 22 | c, _ := goc.NewCache("lru", cacheCap) 23 | return &spoofingProofResolver{ 24 | fastUpstreamProvider: fastUpstreamProvider, 25 | cleanUpstreamProvider: cleanUpstreamProvider, 26 | cnDomains: c, 27 | } 28 | } 29 | 30 | // resovle returns the response and which upstream is used 31 | func (resolver *spoofingProofResolver) resolve(q dns.Question, recursion bool, net string) (*dns.Msg, string) { 32 | type result struct { 33 | res *dns.Msg 34 | err error 35 | } 36 | fastCh := make(chan result, 4) 37 | cleanCh := make(chan result, 4) 38 | 39 | fail := &dns.Msg{ 40 | MsgHdr: dns.MsgHdr{ 41 | Rcode: dns.RcodeServerFailure, 42 | }, 43 | } 44 | 45 | Q := func(ch chan result, upstream string) { 46 | res, err := naiveResolve(q, recursion, net, upstream) 47 | if res == nil { 48 | res = fail 49 | } 50 | ch <- result{res, err} 51 | } 52 | 53 | cleanUpstream := resolver.cleanUpstreamProvider.GetUpstream() 54 | fastUpstream := resolver.fastUpstreamProvider.GetUpstream() 55 | 56 | go Q(cleanCh, cleanUpstream) 57 | go Q(fastCh, fastUpstream) 58 | 59 | // send timeout results 60 | go func() { 61 | time.Sleep(1900 * time.Millisecond) 62 | fastCh <- result{fail, Error("timeout")} 63 | cleanCh <- result{fail, Error("timeout")} 64 | }() 65 | 66 | var r result 67 | var upstream string 68 | 69 | for i := 0; i < 1; i++ { 70 | // 1. if we can distinguish if it is a china domain, we directly uses the right upstream 71 | isCN, ok := resolver.cnDomains.Get(q.Name) 72 | if ok { 73 | if isCN.(bool) { 74 | r = <-fastCh 75 | upstream = fastUpstream 76 | } else { 77 | r = <-cleanCh 78 | upstream = cleanUpstream 79 | } 80 | break 81 | } 82 | 83 | // 2. try to resolve by fast dns. if it contains A record which means we can decide if this is a china domain 84 | r = <-fastCh 85 | upstream = fastUpstream 86 | if r.res != nil && r.res.Rcode == dns.RcodeSuccess && containsA(r.res) && containsChinaip(r.res) { 87 | break 88 | } 89 | 90 | // 3. the domain may not belong to China, use the clean upstream 91 | r = <-cleanCh 92 | upstream = cleanUpstream 93 | } 94 | 95 | // update cnDomains cache 96 | if r.res != nil && r.res.Rcode == dns.RcodeSuccess && containsA(r.res) { 97 | resolver.cnDomains.Set(q.Name, containsChinaip(r.res)) 98 | } 99 | 100 | return r.res, upstream 101 | } 102 | 103 | func naiveResolve(q dns.Question, recursion bool, net string, upstream string) (*dns.Msg, error) { 104 | r := &dns.Msg{ 105 | MsgHdr: dns.MsgHdr{ 106 | Id: dns.Id(), 107 | RecursionDesired: recursion, 108 | }, 109 | Question: []dns.Question{q}, 110 | } 111 | c := &dns.Client{Net: net} 112 | 113 | res, _, err := c.Exchange(r, upstream) 114 | 115 | if err != nil { 116 | log.WithFields(logrus.Fields{ 117 | "op": "naive_resolve", 118 | "upstream": upstream, 119 | "domain": q.Name, 120 | }).Error(err) 121 | // In case the Rcode is initialized as RcodeSuccess but the error occurs. 122 | // Without this, the wrong result may be cached and returned. 123 | if res != nil && res.Rcode == dns.RcodeSuccess { 124 | res = nil 125 | } 126 | } 127 | 128 | return res, err 129 | } 130 | 131 | func containsA(res *dns.Msg) bool { 132 | var rrs []dns.RR 133 | 134 | rrs = append(rrs, res.Answer...) 135 | rrs = append(rrs, res.Ns...) 136 | rrs = append(rrs, res.Extra...) 137 | 138 | for i := 0; i < len(rrs); i++ { 139 | _, ok := rrs[i].(*dns.A) 140 | if ok { 141 | return true 142 | } 143 | } 144 | return false 145 | } 146 | 147 | // containChinaIP check if the resoponse contains IP belonging to China. 148 | func containsChinaip(res *dns.Msg) bool { 149 | var rrs []dns.RR 150 | 151 | rrs = append(rrs, res.Answer...) 152 | rrs = append(rrs, res.Ns...) 153 | rrs = append(rrs, res.Extra...) 154 | 155 | for i := 0; i < len(rrs); i++ { 156 | rr, ok := rrs[i].(*dns.A) 157 | if ok { 158 | ip := rr.A.String() 159 | if chinaip.IsChinaIP(ip) { 160 | return true 161 | } 162 | } 163 | } 164 | return false 165 | } 166 | -------------------------------------------------------------------------------- /freedns/resolve_test.go: -------------------------------------------------------------------------------- 1 | package freedns 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/miekg/dns" 8 | ) 9 | 10 | func Test_spoofing_proof_resolver_resolve(t *testing.T) { 11 | resolver := newSpoofingProofResolver(&staticUpstreamProvider{"114.114.114.114:53"}, &staticUpstreamProvider{"8.8.8.8:53"}, 1024) 12 | 13 | tests := []struct { 14 | domain string 15 | qtype uint16 16 | net string 17 | expectedUpstream string 18 | }{ 19 | // expect 8.8.8.8 as the upstream b/c the resolver have 20 | // no way to identify this is an China domain without A records 21 | {"ustc.edu.cn.", dns.TypeMX, "udp", "8.8.8.8:53"}, 22 | {"ustc.edu.cn.", dns.TypeA, "udp", "114.114.114.114:53"}, 23 | // after querying the A record of ustc.edu.cn, 24 | // the resolver should know this is an China domain 25 | {"ustc.edu.cn.", dns.TypeMX, "udp", "114.114.114.114:53"}, 26 | {"google.com.", dns.TypeA, "udp", "8.8.8.8:53"}, 27 | {"mi.cn.", dns.TypeA, "udp", "114.114.114.114:53"}, 28 | {"xiaomi.com.", dns.TypeA, "udp", "114.114.114.114:53"}, 29 | {"youtube.com.", dns.TypeA, "udp", "8.8.8.8:53"}, 30 | 31 | // This fails just because the GitHub CI server has slow and unstable connection with the 114 server. 32 | //{"www.tsinghua.edu.cn.", dns.TypeA, "tcp", "114.114.114.114:53"}, 33 | {"twitter.com.", dns.TypeA, "tcp", "8.8.8.8:53"}, 34 | } 35 | for _, tt := range tests { 36 | t.Run(tt.domain, func(t *testing.T) { 37 | q := dns.Question{ 38 | Name: tt.domain, 39 | Qtype: tt.qtype, 40 | Qclass: dns.ClassINET, 41 | } 42 | 43 | start := time.Now() 44 | res, upstream := resolver.resolve(q, true, tt.net) 45 | end := time.Now() 46 | elapsed := end.Sub(start) 47 | if upstream != tt.expectedUpstream { 48 | t.Errorf("spoofing_proof_resolver.resolve() got1 = %v, want %v", upstream, tt.expectedUpstream) 49 | } 50 | if len(res.Answer) == 0 { 51 | t.Errorf("Expect returning at least one answer") 52 | } 53 | t.Logf("spoofing_proof_resolver.resolve() domain = %v, net = %v, elapsed = %v, record = %v", tt.domain, tt.net, elapsed, res) 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /freedns/upstream_provider.go: -------------------------------------------------------------------------------- 1 | package freedns 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | 7 | "github.com/fsnotify/fsnotify" 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | type upstreamProvider interface { 12 | GetUpstream() string 13 | } 14 | 15 | type staticUpstreamProvider struct { 16 | upstream string 17 | } 18 | 19 | func (provider *staticUpstreamProvider) GetUpstream() string { 20 | return provider.upstream 21 | } 22 | 23 | type resolvconfUpstreamProvider struct { 24 | filename string 25 | // keep last valid servers even if file becomes invalid 26 | servers []string 27 | serversMutex sync.RWMutex 28 | } 29 | 30 | func (provider *resolvconfUpstreamProvider) GetUpstream() string { 31 | provider.serversMutex.RLock() 32 | defer provider.serversMutex.RUnlock() 33 | // Always use the first one for now 34 | return provider.servers[0] 35 | } 36 | 37 | func parseServersFromResolvconf(filename string) ([]string, error) { 38 | parsedConfig, err := dns.ClientConfigFromFile(filename) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | servers := make([]string, 0) 44 | for _, addr := range parsedConfig.Servers { 45 | if normalized, err := normalizeDnsAddress(addr); err == nil { 46 | servers = append(servers, normalized) 47 | } 48 | } 49 | 50 | if len(servers) == 0 { 51 | return nil, Error("No servers found in " + filename) 52 | } 53 | 54 | return servers, nil 55 | } 56 | 57 | func newResolvconfUpstreamProvider(filename string) (*resolvconfUpstreamProvider, error) { 58 | servers, err := parseServersFromResolvconf(filename) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | provider := &resolvconfUpstreamProvider{ 64 | filename: filename, 65 | servers: servers, 66 | } 67 | 68 | watcher, err := fsnotify.NewWatcher() 69 | if err != nil { 70 | return nil, err 71 | } 72 | if err := watcher.Add(filename); err != nil { 73 | return nil, err 74 | } 75 | go func() { 76 | // TODO: `watcher` will outlive upstream provider 77 | // we need to add a Close method to upstreamProvider 78 | defer watcher.Close() 79 | logger := log.WithField("filename", filename) 80 | logger.Info("Start watching") 81 | for { 82 | select { 83 | case _, ok := <-watcher.Events: 84 | if !ok { 85 | logger.Warn("Watch failed") 86 | return 87 | } 88 | case err, ok := <-watcher.Errors: 89 | logger.WithField("error", err).WithField("ok", ok).Warn("Watch failed") 90 | return 91 | } 92 | logger.Info("Reload resolvconf") 93 | 94 | servers, err := parseServersFromResolvconf(filename) 95 | if err != nil { 96 | logger.WithField("error", err).Warn("Cannot read servers, ignore") 97 | continue 98 | } 99 | 100 | provider.serversMutex.Lock() 101 | provider.servers = servers 102 | provider.serversMutex.Unlock() 103 | } 104 | }() 105 | 106 | return provider, nil 107 | } 108 | 109 | // Create upstream provider based on upstream name 110 | // 111 | // Possible name values are: 112 | // IP address (with optional port) :: use this IP as static upstream 113 | // Filename :: parse the file as resolv.conf, read upstreams from the file (monitor file change) 114 | func newUpstreamProvider(name string) (upstreamProvider, error) { 115 | if addr, err := normalizeDnsAddress(name); err == nil { 116 | return &staticUpstreamProvider{ 117 | upstream: addr, 118 | }, nil 119 | } 120 | if fileinfo, err := os.Stat(name); err == nil && !fileinfo.IsDir() { 121 | return newResolvconfUpstreamProvider(name) 122 | } 123 | return nil, Error("Invalid upstream name " + name) 124 | } 125 | -------------------------------------------------------------------------------- /freedns/upstream_provider_test.go: -------------------------------------------------------------------------------- 1 | package freedns 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestInvalidUpstreamProvider(t *testing.T) { 11 | cases := []string{ 12 | "asdfasdf", 13 | "/dev/null", 14 | } 15 | for _, name := range cases { 16 | if _, err := newUpstreamProvider(name); err == nil { 17 | t.Errorf("Should not create provider for %s", name) 18 | } 19 | } 20 | } 21 | 22 | func TestStaticUpstreamProvider(t *testing.T) { 23 | provider, err := newUpstreamProvider("8.8.4.4") 24 | if err != nil || provider == nil { 25 | t.Errorf("Cannot create static upstream provider") 26 | return 27 | } 28 | if upstream := provider.GetUpstream(); upstream != "8.8.4.4:53" { 29 | t.Errorf("Static upstream provider invalid result %s", upstream) 30 | } 31 | } 32 | 33 | func TestResolvconfUpstreamProvider(t *testing.T) { 34 | tempfile, err := ioutil.TempFile("", "test_resolvconf") 35 | if err != nil { 36 | t.Errorf("Cannot create temp file: %s", err.Error()) 37 | return 38 | } 39 | filename := tempfile.Name() 40 | tempfile.Close() 41 | 42 | WriteContent := func(content string) { 43 | tempfile, err := os.Create(filename) 44 | if err != nil { 45 | t.Errorf("Cannot open temp file: %s", err.Error()) 46 | return 47 | } 48 | defer tempfile.Close() 49 | tempfile.Write([]byte(content)) 50 | } 51 | 52 | defer os.Remove(filename) 53 | 54 | WriteContent("nameserver 1.2.3.4\n") 55 | 56 | provider, err := newUpstreamProvider(filename) 57 | if err != nil { 58 | t.Errorf("Cannot create resolvconf upstream provider for %s: %s", filename, err.Error()) 59 | return 60 | } 61 | 62 | if upstream := provider.GetUpstream(); upstream != "1.2.3.4:53" { 63 | t.Errorf("Bad result %s", upstream) 64 | } 65 | 66 | WriteContent("nameserver 8.8.8.8\nnameserver8.8.4.4\n") 67 | time.Sleep(100 * time.Millisecond) 68 | // should be updated 69 | if upstream := provider.GetUpstream(); upstream != "8.8.8.8:53" { 70 | t.Errorf("Bad result %s", upstream) 71 | } 72 | 73 | WriteContent("some invalid content") 74 | time.Sleep(100 * time.Millisecond) 75 | // should not be updated 76 | if upstream := provider.GetUpstream(); upstream != "8.8.8.8:53" { 77 | t.Errorf("Bad result %s", upstream) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /freedns/utils.go: -------------------------------------------------------------------------------- 1 | package freedns 2 | 3 | import "net" 4 | 5 | // Parse ip with optional port, return normalized ip:port string 6 | // For ips without port, default 53 port is appended 7 | func normalizeDnsAddress(addr string) (string, error) { 8 | host, port, err := net.SplitHostPort(addr) 9 | if err != nil { 10 | // no port, try parse addr as host with default port 11 | host = addr 12 | port = "53" 13 | } else if host == "" { 14 | // for addrs like ":53", use default host 15 | host = "0.0.0.0" 16 | } 17 | 18 | if net.ParseIP(host) == nil { 19 | return "", Error("Invalid IP addr: " + host) 20 | } 21 | return net.JoinHostPort(host, port), nil 22 | } 23 | -------------------------------------------------------------------------------- /freedns/utils_test.go: -------------------------------------------------------------------------------- 1 | package freedns 2 | 3 | import "testing" 4 | 5 | func Test_normalizeDnsAddress(t *testing.T) { 6 | assertError := func(addr string) { 7 | if res, err := normalizeDnsAddress(addr); res != "" || err == nil { 8 | t.Errorf("%s should not be normalized as dns address", addr) 9 | } 10 | } 11 | assertResult := func(addr, expected string) { 12 | if res, err := normalizeDnsAddress(addr); res != expected || err != nil { 13 | t.Errorf("%s should be normalized as %s, got %s (%s)", addr, expected, res, err.Error()) 14 | } 15 | } 16 | 17 | assertError("") 18 | assertError("hello world") 19 | assertError("123:456") 20 | assertError("/hello/world:filename") 21 | assertError("1.2.3.4.5") 22 | 23 | assertResult("1.2.3.4", "1.2.3.4:53") 24 | assertResult("127.0.0.1:3333", "127.0.0.1:3333") 25 | assertResult("::1", "[::1]:53") 26 | assertResult("[::]:5300", "[::]:5300") 27 | assertResult(":5300", "0.0.0.0:5300") 28 | } 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tuna/freedns-go 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.4.9 7 | github.com/louchenyao/golang-cache v0.0.0-20190309153624-1d1c4bb01145 8 | github.com/miekg/dns v1.1.27 9 | github.com/sirupsen/logrus v1.4.2 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 4 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 5 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 6 | github.com/louchenyao/golang-cache v0.0.0-20190309153624-1d1c4bb01145 h1:a6W9GKXRz9iiJxokZ5znNa2S7DhsAfPlj++6dG/1stY= 7 | github.com/louchenyao/golang-cache v0.0.0-20190309153624-1d1c4bb01145/go.mod h1:qo/Jbijoez5mIuriNYfgydZnCXt7xiP1tS82aoxk6yE= 8 | github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM= 9 | github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 13 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 14 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 15 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 16 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 17 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 18 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= 19 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 20 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 21 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 22 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 23 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= 24 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 25 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 26 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 27 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 28 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 29 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 30 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M= 31 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= 33 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 34 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 35 | golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 36 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 37 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | 8 | _ "net/http/pprof" 9 | 10 | "github.com/tuna/freedns-go/freedns" 11 | ) 12 | 13 | func main() { 14 | /* 15 | go func() { 16 | log.Println(http.ListenAndServe("localhost:6060", nil)) 17 | }() 18 | */ 19 | 20 | var ( 21 | fastUpstream string 22 | cleanUpstream string 23 | listen string 24 | logLevel string 25 | // cache bool 26 | ) 27 | 28 | flag.StringVar(&fastUpstream, "f", "114.114.114.114:53", "The fast/local DNS upstream, ip:port or resolv.conf file") 29 | flag.StringVar(&cleanUpstream, "c", "8.8.8.8:53", "The clean/remote DNS upstream., ip:port or resolv.conf file") 30 | flag.StringVar(&listen, "l", "0.0.0.0:53", "Listening address.") 31 | // flag.BoolVar(&cache, "cache", true, "Enable cache.") 32 | flag.StringVar(&logLevel, "log-level", "", "Set log level: info/warn/error.") 33 | 34 | flag.Parse() 35 | 36 | s, err := freedns.NewServer(freedns.Config{ 37 | FastUpstream: fastUpstream, 38 | CleanUpstream: cleanUpstream, 39 | Listen: listen, 40 | CacheCap: 1024 * 10, 41 | LogLevel: logLevel, 42 | }) 43 | if err != nil { 44 | log.Fatalln(err) 45 | os.Exit(-1) 46 | } 47 | 48 | log.Fatalln(s.Run()) 49 | os.Exit(-1) 50 | } 51 | --------------------------------------------------------------------------------