├── .gitignore ├── .travis.yml ├── addrbag.go ├── dnsmessage.go ├── store_test.go ├── rind └── main.go ├── LICENSE ├── README.md ├── rest.go ├── store.go └── dns.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | rind/rind -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.11.x 4 | 5 | go_import_path: github.com/owlwalks/rind 6 | 7 | script: 8 | - go get -t -v ./... 9 | - go tool vet . 10 | - go test -v -race ./... -------------------------------------------------------------------------------- /addrbag.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Rind Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package rind 6 | 7 | import ( 8 | "net" 9 | "sync" 10 | ) 11 | 12 | type addrBag struct { 13 | sync.RWMutex 14 | data map[string][]net.UDPAddr 15 | } 16 | 17 | func (b *addrBag) get(key string) ([]net.UDPAddr, bool) { 18 | b.RLock() 19 | val, ok := b.data[key] 20 | b.RUnlock() 21 | return val, ok 22 | } 23 | 24 | func (b *addrBag) set(key string, addr net.UDPAddr) { 25 | b.Lock() 26 | if _, ok := b.data[key]; ok { 27 | b.data[key] = append(b.data[key], addr) 28 | } else { 29 | b.data[key] = []net.UDPAddr{addr} 30 | } 31 | b.Unlock() 32 | } 33 | 34 | func (b *addrBag) remove(key string) bool { 35 | b.Lock() 36 | _, ok := b.data[key] 37 | delete(b.data, key) 38 | b.Unlock() 39 | return ok 40 | } 41 | -------------------------------------------------------------------------------- /dnsmessage.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Rind Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package rind 6 | 7 | import ( 8 | "fmt" 9 | "strings" 10 | 11 | "golang.org/x/net/dns/dnsmessage" 12 | ) 13 | 14 | // question to string 15 | func qString(q dnsmessage.Question) string { 16 | b := make([]byte, q.Name.Length+2) 17 | for i := 0; i < int(q.Name.Length); i++ { 18 | b[i] = q.Name.Data[i] 19 | } 20 | b[q.Name.Length] = uint8(q.Type >> 8) 21 | b[q.Name.Length+1] = uint8(q.Type) 22 | 23 | return string(b) 24 | } 25 | 26 | // resource name and type to string 27 | func ntString(rName dnsmessage.Name, rType dnsmessage.Type) string { 28 | b := make([]byte, rName.Length+2) 29 | for i := 0; i < int(rName.Length); i++ { 30 | b[i] = rName.Data[i] 31 | } 32 | b[rName.Length] = uint8(rType >> 8) 33 | b[rName.Length+1] = uint8(rType) 34 | 35 | return string(b) 36 | } 37 | 38 | // resource to string 39 | func rString(r dnsmessage.Resource) string { 40 | var sb strings.Builder 41 | sb.Write(r.Header.Name.Data[:]) 42 | sb.WriteString(r.Header.Type.String()) 43 | sb.WriteString(r.Body.GoString()) 44 | 45 | return sb.String() 46 | } 47 | 48 | // packet to string 49 | func pString(p Packet) string { 50 | return fmt.Sprint(p.message.ID) 51 | } 52 | -------------------------------------------------------------------------------- /store_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Rind Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package rind 6 | 7 | import ( 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "testing" 12 | 13 | "golang.org/x/net/dns/dnsmessage" 14 | ) 15 | 16 | func Test_store_save_load(t *testing.T) { 17 | dirPath, err := ioutil.TempDir("", "") 18 | if err != nil { 19 | t.Skip(err) 20 | } 21 | name, _ := dnsmessage.NewName("test") 22 | data := map[string]entry{ 23 | "test": { 24 | Resources: []dnsmessage.Resource{ 25 | { 26 | Header: dnsmessage.ResourceHeader{ 27 | Name: name, 28 | Type: dnsmessage.TypeA, 29 | Class: dnsmessage.ClassINET, 30 | }, 31 | Body: &dnsmessage.AResource{A: [4]byte{127, 0, 0, 1}}, 32 | }, 33 | }, 34 | }, 35 | } 36 | book := store{data: data, rwDirPath: dirPath} 37 | book.save() 38 | main, _ := os.Stat(filepath.Join(dirPath, storeName)) 39 | bk, _ := os.Stat(filepath.Join(dirPath, storeBkName)) 40 | if main.Size() != 664 || bk.Size() != 0 { 41 | t.Fail() 42 | } 43 | bookNew := store{rwDirPath: dirPath} 44 | bookNew.load() 45 | r, _ := bookNew.get("test") 46 | body, _ := r[0].Body.(*dnsmessage.AResource) 47 | if body.A != [4]byte{127, 0, 0, 1} { 48 | t.Fail() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /rind/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "net/http" 7 | 8 | "github.com/owlwalks/rind" 9 | "flag" 10 | "os" 11 | "github.com/golang/glog" 12 | ) 13 | 14 | var rwDirPath = flag.String("rwdir","/var/dns","dns storage dir") 15 | var listenIP = flag.String("listenip", "8.8.8.8", "dns forward ip") 16 | var listenPort = flag.Int("listenport", 53, "dns forward port") 17 | 18 | func main() { 19 | flag.Parse() 20 | if err := os.MkdirAll(*rwDirPath, 0666); err != nil { 21 | glog.Errorf("create rwdirpath: %v error: %v", *rwDirPath, err) 22 | return 23 | } 24 | glog.Info("starting rind") 25 | dns := rind.Start(*rwDirPath, []net.UDPAddr{{IP: net.ParseIP(*listenIP), Port: *listenPort}}) 26 | rest := rind.RestService{Dn: dns} 27 | 28 | dnsHandler := func() http.HandlerFunc { 29 | return func(w http.ResponseWriter, r *http.Request) { 30 | switch r.Method { 31 | case http.MethodPost: 32 | rest.Create(w, r) 33 | case http.MethodGet: 34 | rest.Read(w, r) 35 | case http.MethodPut: 36 | rest.Update(w, r) 37 | case http.MethodDelete: 38 | rest.Delete(w, r) 39 | } 40 | } 41 | } 42 | 43 | withAuth := func(h http.HandlerFunc) http.HandlerFunc { 44 | // authentication intercepting 45 | var _ = "intercept" 46 | return func(w http.ResponseWriter, r *http.Request) { 47 | h(w, r) 48 | } 49 | } 50 | 51 | http.Handle("/dns", withAuth(dnsHandler())) 52 | log.Fatal(http.ListenAndServe(":80", nil)) 53 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 The Rind Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | * Neither the name of the copyright holder nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UNMAINTAINED, PLEASE SEE OTHER ALTERNATIVES BELOW 2 | 3 | * [coredns](https://github.com/coredns/coredns) 4 | 5 | ## Rind 6 | 7 | [![GoDoc](https://godoc.org/github.com/owlwalks/rind?status.svg)](https://godoc.org/github.com/owlwalks/rind) 8 | [![Build Status](https://travis-ci.com/owlwalks/rind.svg?branch=master)](https://travis-ci.com/owlwalks/rind) 9 | 10 | Rind is a DNS server with REST interface for records management, best use is for your local service discovery, DNS forwarding and caching. 11 | 12 | ## Examples 13 | See complete example [here](https://github.com/owlwalks/rind/blob/master/rind/main.go) 14 | 15 | Start DNS server: 16 | ```golang 17 | import github.com/owlwalks/rind 18 | 19 | rind.Start("rw-dirpath", []net.UDPAddr{{IP: net.IP{1, 1, 1, 1}, Port: 53}}) 20 | ``` 21 | 22 | ## Manage records 23 | ```shell 24 | // Add a SRV record 25 | curl -X POST \ 26 | http://localhost/dns \ 27 | -H 'Content-Type: application/json' \ 28 | -d '{ 29 | "Host": "_sip._tcp.example.com.", 30 | "TTL": 300, 31 | "Type": "SRV", 32 | "SRV": { 33 | "Priority": 0, 34 | "Weight": 5, 35 | "Port": 5060, 36 | "Target": "sipserver.example.com." 37 | } 38 | }' 39 | 40 | // Update an A record from 124.108.115.87 to 127.0.0.1 41 | curl -X PUT \ 42 | http://localhost/dns \ 43 | -H 'Content-Type: application/json' \ 44 | -d '{ 45 | "Host": "example.com.", 46 | "TTL": 600, 47 | "Type": "A", 48 | "OldData": "124.108.115.87", 49 | "Data": "127.0.0.1" 50 | }' 51 | 52 | // Delete a record 53 | curl -X DELETE \ 54 | http://localhost/dns \ 55 | -H 'Content-Type: application/json' \ 56 | -d '{ 57 | "Host": "example.com.", 58 | "Type": "A" 59 | }' 60 | ``` 61 | 62 | ## Features: 63 | - [x] DNS server 64 | - [x] DNS forwarding 65 | - [x] DNS caching 66 | - [x] A record 67 | - [x] NS record 68 | - [x] CNAME record 69 | - [x] SOA record 70 | - [x] PTR record 71 | - [x] MX record 72 | - [x] AAAA record 73 | - [x] SRV record 74 | - [x] REST server 75 | - [x] Create records 76 | - [x] Read records 77 | - [x] Update records 78 | - [x] Delete records 79 | 80 | Todo: 81 | - [ ] Primary, secondary model 82 | -------------------------------------------------------------------------------- /rest.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Rind Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package rind 6 | 7 | import ( 8 | "encoding/json" 9 | "net/http" 10 | ) 11 | 12 | // RestServer will do CRUD on DNS records 13 | type RestServer interface { 14 | Create() http.HandlerFunc 15 | Read() http.HandlerFunc 16 | Update() http.HandlerFunc 17 | Delete() http.HandlerFunc 18 | } 19 | 20 | // RestService is an implementation of RestServer interface. 21 | type RestService struct { 22 | Dn *DNSService 23 | } 24 | 25 | type request struct { 26 | Host string 27 | TTL uint32 28 | Type string 29 | Data string 30 | OldData string 31 | SOA requestSOA 32 | OldSOA requestSOA 33 | MX requestMX 34 | OldMX requestMX 35 | SRV requestSRV 36 | OldSRV requestSRV 37 | } 38 | 39 | type requestSOA struct { 40 | NS string 41 | MBox string 42 | Serial uint32 43 | Refresh uint32 44 | Retry uint32 45 | Expire uint32 46 | MinTTL uint32 47 | } 48 | 49 | type requestMX struct { 50 | Pref uint16 51 | MX string 52 | } 53 | 54 | type requestSRV struct { 55 | Priority uint16 56 | Weight uint16 57 | Port uint16 58 | Target string 59 | } 60 | 61 | type get struct { 62 | Host string 63 | TTL uint32 64 | Type string 65 | Data string 66 | } 67 | 68 | // Create is HTTP handler of POST request. 69 | // Use for adding new record to DNS server. 70 | func (s *RestService) Create(w http.ResponseWriter, r *http.Request) { 71 | var req request 72 | if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 73 | http.Error(w, err.Error(), http.StatusBadRequest) 74 | return 75 | } 76 | 77 | resource, err := toResource(req) 78 | if err != nil { 79 | http.Error(w, err.Error(), http.StatusBadRequest) 80 | return 81 | } 82 | 83 | s.Dn.save(ntString(resource.Header.Name, resource.Header.Type), resource, nil) 84 | w.WriteHeader(http.StatusCreated) 85 | } 86 | 87 | // Read is HTTP handler of GET request. 88 | // Use for reading existed records on DNS server. 89 | func (s *RestService) Read(w http.ResponseWriter, r *http.Request) { 90 | json.NewEncoder(w).Encode(s.Dn.all()) 91 | } 92 | 93 | // Update is HTTP handler of PUT request. 94 | // Use for updating existed records on DNS server. 95 | func (s *RestService) Update(w http.ResponseWriter, r *http.Request) { 96 | var req request 97 | if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 98 | http.Error(w, err.Error(), http.StatusBadRequest) 99 | return 100 | } 101 | 102 | oldReq := request{Host: req.Host, Type: req.Type, Data: req.OldData} 103 | old, err := toResource(oldReq) 104 | if err != nil { 105 | http.Error(w, err.Error(), http.StatusBadRequest) 106 | return 107 | } 108 | 109 | resource, err := toResource(req) 110 | if err != nil { 111 | http.Error(w, err.Error(), http.StatusBadRequest) 112 | return 113 | } 114 | 115 | ok := s.Dn.save(ntString(resource.Header.Name, resource.Header.Type), resource, &old) 116 | if ok { 117 | w.WriteHeader(http.StatusOK) 118 | return 119 | } 120 | 121 | http.Error(w, "", http.StatusNotFound) 122 | } 123 | 124 | // Delete is HTTP handler of DELETE request. 125 | // Use for removing records on DNS server. 126 | func (s *RestService) Delete(w http.ResponseWriter, r *http.Request) { 127 | var req request 128 | if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 129 | http.Error(w, err.Error(), http.StatusBadRequest) 130 | return 131 | } 132 | 133 | ok := false 134 | h, err := toResourceHeader(req.Host, req.Type) 135 | if err == nil { 136 | ok = s.Dn.remove(ntString(h.Name, h.Type), nil) 137 | } 138 | 139 | if ok { 140 | w.WriteHeader(http.StatusOK) 141 | return 142 | } 143 | 144 | http.Error(w, "", http.StatusNotFound) 145 | } 146 | -------------------------------------------------------------------------------- /store.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Rind Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package rind 6 | 7 | import ( 8 | "encoding/gob" 9 | "io" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | "sync" 14 | "time" 15 | 16 | "golang.org/x/net/dns/dnsmessage" 17 | "github.com/golang/glog" 18 | ) 19 | 20 | const ( 21 | storeName string = "store" 22 | storeBkName string = "store_bk" 23 | ) 24 | 25 | func init() { 26 | gob.Register(&dnsmessage.AResource{}) 27 | gob.Register(&dnsmessage.NSResource{}) 28 | gob.Register(&dnsmessage.CNAMEResource{}) 29 | gob.Register(&dnsmessage.SOAResource{}) 30 | gob.Register(&dnsmessage.PTRResource{}) 31 | gob.Register(&dnsmessage.MXResource{}) 32 | gob.Register(&dnsmessage.AAAAResource{}) 33 | gob.Register(&dnsmessage.SRVResource{}) 34 | gob.Register(&dnsmessage.TXTResource{}) 35 | gob.Register(&dnsmessage.PTRResource{}) 36 | } 37 | 38 | type store struct { 39 | sync.RWMutex 40 | data map[string]entry 41 | rwDirPath string 42 | } 43 | 44 | type entry struct { 45 | Resources []dnsmessage.Resource 46 | TTL uint32 47 | Created int64 48 | } 49 | 50 | func (s *store) get(key string) ([]dnsmessage.Resource, bool) { 51 | s.RLock() 52 | e, ok := s.data[key] 53 | s.RUnlock() 54 | now := time.Now().Unix() 55 | if e.TTL > 1 && (e.Created+int64(e.TTL) < now) { 56 | s.remove(key, nil) 57 | return nil, false 58 | } 59 | return e.Resources, ok 60 | } 61 | 62 | func (s *store) set(key string, resource dnsmessage.Resource, old *dnsmessage.Resource) bool { 63 | changed := false 64 | s.Lock() 65 | if _, ok := s.data[key]; ok { 66 | if old != nil { 67 | for i, rec := range s.data[key].Resources { 68 | if rString(rec) == rString(*old) { 69 | s.data[key].Resources[i] = resource 70 | changed = true 71 | break 72 | } 73 | } 74 | } else { 75 | e := s.data[key] 76 | e.Resources = append(e.Resources, resource) 77 | s.data[key] = e 78 | changed = true 79 | } 80 | } else { 81 | e := entry{ 82 | Resources: []dnsmessage.Resource{resource}, 83 | TTL: resource.Header.TTL, 84 | Created: time.Now().Unix(), 85 | } 86 | s.data[key] = e 87 | changed = true 88 | } 89 | s.Unlock() 90 | 91 | return changed 92 | } 93 | 94 | func (s *store) override(key string, resources []dnsmessage.Resource) { 95 | s.Lock() 96 | e := entry{ 97 | Resources: resources, 98 | Created: time.Now().Unix(), 99 | } 100 | if len(resources) > 0 { 101 | e.TTL = resources[0].Header.TTL 102 | } 103 | s.data[key] = e 104 | s.Unlock() 105 | } 106 | 107 | func (s *store) remove(key string, r *dnsmessage.Resource) bool { 108 | ok := false 109 | s.Lock() 110 | if r == nil { 111 | _, ok = s.data[key] 112 | delete(s.data, key) 113 | } else { 114 | if _, ok = s.data[key]; ok { 115 | for i, rec := range s.data[key].Resources { 116 | if rString(rec) == rString(*r) { 117 | e := s.data[key] 118 | copy(e.Resources[i:], e.Resources[i+1:]) 119 | var blank dnsmessage.Resource 120 | e.Resources[len(e.Resources)-1] = blank 121 | e.Resources = e.Resources[:len(e.Resources)-1] 122 | s.data[key] = e 123 | ok = true 124 | break 125 | } 126 | } 127 | } 128 | } 129 | s.Unlock() 130 | return ok 131 | } 132 | 133 | func (s *store) save() { 134 | bk, err := os.OpenFile(filepath.Join(s.rwDirPath, storeBkName), os.O_RDWR|os.O_CREATE, 0666) 135 | if err != nil { 136 | glog.Errorf("err open store bak file %v",err) 137 | return 138 | } 139 | defer bk.Close() 140 | 141 | dst, err := os.OpenFile(filepath.Join(s.rwDirPath, storeName), os.O_RDWR|os.O_CREATE, 0666) 142 | if err != nil { 143 | glog.Errorf("err open store file %v",err) 144 | return 145 | } 146 | defer dst.Close() 147 | 148 | // backing up current store 149 | _, err = io.Copy(bk, dst) 150 | if err != nil { 151 | glog.Errorf("err copy store file %v",err) 152 | return 153 | } 154 | 155 | enc := gob.NewEncoder(dst) 156 | book := s.clone() 157 | if err = enc.Encode(book); err != nil { 158 | // main store file is corrupted 159 | log.Fatal(err) 160 | } 161 | } 162 | 163 | func (s *store) load() { 164 | fReader, err := os.Open(filepath.Join(s.rwDirPath, storeName)) 165 | if err != nil { 166 | glog.Errorf("err load store file %v maybe first start,please ignore",err) 167 | return 168 | } 169 | defer fReader.Close() 170 | 171 | dec := gob.NewDecoder(fReader) 172 | 173 | s.Lock() 174 | defer s.Unlock() 175 | 176 | if err = dec.Decode(&s.data); err != nil { 177 | glog.Fatalf("err decode store file %v",err) 178 | } 179 | } 180 | 181 | func (s *store) clone() map[string]entry { 182 | cp := make(map[string]entry) 183 | s.RLock() 184 | for k, v := range s.data { 185 | cp[k] = v 186 | } 187 | s.RUnlock() 188 | return cp 189 | } 190 | -------------------------------------------------------------------------------- /dns.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Rind Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package rind 6 | 7 | import ( 8 | "errors" 9 | "log" 10 | "net" 11 | "strings" 12 | 13 | "golang.org/x/net/dns/dnsmessage" 14 | ) 15 | 16 | // DNSServer will do Listen, Query and Send. 17 | type DNSServer interface { 18 | Listen() 19 | Query(Packet) 20 | } 21 | 22 | // DNSService is an implementation of DNSServer interface. 23 | type DNSService struct { 24 | conn *net.UDPConn 25 | book store 26 | memo addrBag 27 | forwarders []net.UDPAddr 28 | } 29 | 30 | // Packet carries DNS packet payload and sender address. 31 | type Packet struct { 32 | addr net.UDPAddr 33 | message dnsmessage.Message 34 | } 35 | 36 | const ( 37 | // DNS server default port 38 | udpPort int = 53 39 | // DNS packet max length 40 | packetLen int = 512 41 | ) 42 | 43 | var ( 44 | errTypeNotSupport = errors.New("type not support") 45 | errIPInvalid = errors.New("invalid IP address") 46 | ) 47 | 48 | // Listen starts a DNS server on port 53 49 | func (s *DNSService) Listen() { 50 | var err error 51 | s.conn, err = net.ListenUDP("udp", &net.UDPAddr{Port: udpPort}) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | defer s.conn.Close() 56 | 57 | for { 58 | buf := make([]byte, packetLen) 59 | _, addr, err := s.conn.ReadFromUDP(buf) 60 | if err != nil { 61 | log.Println(err) 62 | continue 63 | } 64 | var m dnsmessage.Message 65 | err = m.Unpack(buf) 66 | if err != nil { 67 | log.Println(err) 68 | continue 69 | } 70 | if len(m.Questions) == 0 { 71 | continue 72 | } 73 | go s.Query(Packet{*addr, m}) 74 | } 75 | } 76 | 77 | // Query lookup answers for DNS message. 78 | func (s *DNSService) Query(p Packet) { 79 | // got response from forwarder, send it back to client 80 | if p.message.Header.Response { 81 | pKey := pString(p) 82 | if addrs, ok := s.memo.get(pKey); ok { 83 | for _, addr := range addrs { 84 | go sendPacket(s.conn, p.message, addr) 85 | } 86 | s.memo.remove(pKey) 87 | go s.saveBulk(qString(p.message.Questions[0]), p.message.Answers) 88 | } 89 | return 90 | } 91 | 92 | // was checked before entering this routine 93 | q := p.message.Questions[0] 94 | 95 | // answer the question 96 | val, ok := s.book.get(qString(q)) 97 | 98 | if ok { 99 | p.message.Answers = append(p.message.Answers, val...) 100 | go sendPacket(s.conn, p.message, p.addr) 101 | } else { 102 | // forwarding 103 | for i := 0; i < len(s.forwarders); i++ { 104 | s.memo.set(pString(p), p.addr) 105 | go sendPacket(s.conn, p.message, s.forwarders[i]) 106 | } 107 | } 108 | } 109 | 110 | func sendPacket(conn *net.UDPConn, message dnsmessage.Message, addr net.UDPAddr) { 111 | packed, err := message.Pack() 112 | if err != nil { 113 | log.Println(err) 114 | return 115 | } 116 | 117 | _, err = conn.WriteToUDP(packed, &addr) 118 | if err != nil { 119 | log.Println(err) 120 | } 121 | } 122 | 123 | // New setups a DNSService, rwDirPath is read-writable directory path for storing dns records. 124 | func New(rwDirPath string, forwarders []net.UDPAddr) DNSService { 125 | return DNSService{ 126 | book: store{data: make(map[string]entry), rwDirPath: rwDirPath}, 127 | memo: addrBag{data: make(map[string][]net.UDPAddr)}, 128 | forwarders: forwarders, 129 | } 130 | } 131 | 132 | // Start conveniently init every parts of DNS service. 133 | func Start(rwDirPath string, forwarders []net.UDPAddr) *DNSService { 134 | s := New(rwDirPath, forwarders) 135 | s.book.load() 136 | go s.Listen() 137 | 138 | return &s 139 | } 140 | 141 | func (s *DNSService) save(key string, resource dnsmessage.Resource, old *dnsmessage.Resource) bool { 142 | ok := s.book.set(key, resource, old) 143 | go s.book.save() 144 | 145 | return ok 146 | } 147 | 148 | func (s *DNSService) saveBulk(key string, resources []dnsmessage.Resource) { 149 | s.book.override(key, resources) 150 | go s.book.save() 151 | } 152 | 153 | func (s *DNSService) all() []get { 154 | book := s.book.clone() 155 | var recs []get 156 | for _, r := range book { 157 | for _, v := range r.Resources { 158 | body := v.Body.GoString() 159 | i := strings.Index(body, "{") 160 | recs = append(recs, get{ 161 | Host: v.Header.Name.String(), 162 | TTL: v.Header.TTL, 163 | Type: v.Header.Type.String()[4:], 164 | Data: body[i : len(body)-1], // get content within "{" and "}" 165 | }) 166 | } 167 | } 168 | return recs 169 | } 170 | 171 | func (s *DNSService) remove(key string, r *dnsmessage.Resource) bool { 172 | ok := s.book.remove(key, r) 173 | if ok { 174 | go s.book.save() 175 | } 176 | return ok 177 | } 178 | 179 | func toResource(req request) (dnsmessage.Resource, error) { 180 | rName, err := dnsmessage.NewName(req.Host) 181 | none := dnsmessage.Resource{} 182 | if err != nil { 183 | return none, err 184 | } 185 | 186 | var rType dnsmessage.Type 187 | var rBody dnsmessage.ResourceBody 188 | 189 | switch req.Type { 190 | case "A": 191 | rType = dnsmessage.TypeA 192 | ip := net.ParseIP(req.Data) 193 | if ip == nil { 194 | return none, errIPInvalid 195 | } 196 | rBody = &dnsmessage.AResource{A: [4]byte{ip[12], ip[13], ip[14], ip[15]}} 197 | case "NS": 198 | rType = dnsmessage.TypeNS 199 | ns, err := dnsmessage.NewName(req.Data) 200 | if err != nil { 201 | return none, err 202 | } 203 | rBody = &dnsmessage.NSResource{NS: ns} 204 | case "CNAME": 205 | rType = dnsmessage.TypeCNAME 206 | cname, err := dnsmessage.NewName(req.Data) 207 | if err != nil { 208 | return none, err 209 | } 210 | rBody = &dnsmessage.CNAMEResource{CNAME: cname} 211 | case "SOA": 212 | rType = dnsmessage.TypeSOA 213 | soa := req.SOA 214 | soaNS, err := dnsmessage.NewName(soa.NS) 215 | if err != nil { 216 | return none, err 217 | } 218 | soaMBox, err := dnsmessage.NewName(soa.MBox) 219 | if err != nil { 220 | return none, err 221 | } 222 | rBody = &dnsmessage.SOAResource{NS: soaNS, MBox: soaMBox, Serial: soa.Serial, Refresh: soa.Refresh, Retry: soa.Retry, Expire: soa.Expire} 223 | case "PTR": 224 | rType = dnsmessage.TypePTR 225 | ptr, err := dnsmessage.NewName(req.Data) 226 | if err != nil { 227 | return none, err 228 | } 229 | rBody = &dnsmessage.PTRResource{PTR: ptr} 230 | case "MX": 231 | rType = dnsmessage.TypeMX 232 | mxName, err := dnsmessage.NewName(req.MX.MX) 233 | if err != nil { 234 | return none, err 235 | } 236 | rBody = &dnsmessage.MXResource{Pref: req.MX.Pref, MX: mxName} 237 | case "AAAA": 238 | rType = dnsmessage.TypeAAAA 239 | ip := net.ParseIP(req.Data) 240 | if ip == nil { 241 | return none, errIPInvalid 242 | } 243 | var ipV6 [16]byte 244 | copy(ipV6[:], ip) 245 | rBody = &dnsmessage.AAAAResource{AAAA: ipV6} 246 | case "SRV": 247 | rType = dnsmessage.TypeSRV 248 | srv := req.SRV 249 | srvTarget, err := dnsmessage.NewName(srv.Target) 250 | if err != nil { 251 | return none, err 252 | } 253 | rBody = &dnsmessage.SRVResource{Priority: srv.Priority, Weight: srv.Weight, Port: srv.Port, Target: srvTarget} 254 | case "TXT": 255 | fallthrough 256 | case "OPT": 257 | fallthrough 258 | default: 259 | return none, errTypeNotSupport 260 | } 261 | 262 | return dnsmessage.Resource{ 263 | Header: dnsmessage.ResourceHeader{ 264 | Name: rName, 265 | Type: rType, 266 | Class: dnsmessage.ClassINET, 267 | TTL: req.TTL, 268 | }, 269 | Body: rBody, 270 | }, nil 271 | } 272 | 273 | func toRType(sType string) dnsmessage.Type { 274 | switch sType { 275 | case "A": 276 | return dnsmessage.TypeA 277 | case "NS": 278 | return dnsmessage.TypeNS 279 | case "CNAME": 280 | return dnsmessage.TypeCNAME 281 | case "SOA": 282 | return dnsmessage.TypeSOA 283 | case "PTR": 284 | return dnsmessage.TypePTR 285 | case "MX": 286 | return dnsmessage.TypeMX 287 | case "AAAA": 288 | return dnsmessage.TypeAAAA 289 | case "SRV": 290 | return dnsmessage.TypeSRV 291 | case "TXT": 292 | return dnsmessage.TypeTXT 293 | case "OPT": 294 | return dnsmessage.TypeOPT 295 | default: 296 | return 0 297 | } 298 | } 299 | 300 | func toResourceHeader(name string, sType string) (h dnsmessage.ResourceHeader, err error) { 301 | h.Name, err = dnsmessage.NewName(name) 302 | if err != nil { 303 | return 304 | } 305 | h.Type = toRType(sType) 306 | return 307 | } 308 | --------------------------------------------------------------------------------