├── History.md ├── Readme.md ├── cmd └── sdns │ └── main.go ├── config └── config.go ├── examples ├── ec2.yml └── echo.yml ├── sdns.go └── server ├── domain.go ├── server.go ├── upstream.go └── utils.go /History.md: -------------------------------------------------------------------------------- 1 | 2 | v0.1.0 / 2015-05-16 3 | =================== 4 | 5 | * fix echo example to output arrays 6 | * clean up logging 7 | * fix ec2 example 8 | * add json stdin and package for aiding resolver commands 9 | * install docs 10 | * names are hard 11 | * add some digs 12 | * Initial commit 13 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # sdns 3 | 4 | [![GoDoc](https://godoc.org/github.com/tj/sdns?status.svg)](https://godoc.org/github.com/tj/sdns) 5 | 6 | Little nameserver resolving via arbitrary command(s). 7 | 8 | __Warning__: This is a work-in-progress, you have been warned! 9 | 10 | ## Installation 11 | 12 | Via binary [releases](https://github.com/tj/sdns/releases) or: 13 | 14 | ``` 15 | $ go get github.com/tj/sdns/cmd/sdns 16 | ``` 17 | 18 | ## Usage 19 | 20 | Run with config: 21 | 22 | ``` 23 | $ sdns < domains.yml 24 | ``` 25 | 26 | Configuration example: 27 | 28 | ```yml 29 | bind: ":5000" 30 | upstream: 31 | - 8.8.8.8 32 | - 8.8.4.4 33 | domains: 34 | - name: foo 35 | command: | 36 | echo '[{ "type": "A", "value": "1.1.1.1", "ttl": 60 }, { "type": "A", "value": "1.1.1.4", "ttl": 60 }]' 37 | - name: bar 38 | command: | 39 | echo '[{ "type": "A", "value": "1.1.1.2", "ttl": 60 }]' 40 | - name: foo.bar 41 | command: | 42 | echo '[{ "type": "A", "value": "1.1.1.3", "ttl": 300 }]' 43 | - name: boom 44 | command: | 45 | echo 'something goes boom' && exit 1 46 | ``` 47 | 48 | Dig it: 49 | 50 | ``` 51 | $ dig @127.0.0.1 -p 5000 something.foo +short 52 | 1.1.1.1 53 | 1.1.1.4 54 | 55 | $ dig @127.0.0.1 -p 5000 something.bar +short 56 | 1.1.1.2 57 | 58 | $ dig @127.0.0.1 -p 5000 something.foo.bar +short 59 | 1.1.1.3 60 | 61 | $ dig @127.0.0.1 -p 5000 segment.com +short 62 | 54.213.169.105 63 | ``` 64 | 65 | ## Resolvers 66 | 67 | - [sdns-ec2](https://github.com/tj/sdns-ec2): resolves AWS EC2 hosts via the Name tag 68 | 69 | # License 70 | 71 | MIT -------------------------------------------------------------------------------- /cmd/sdns/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/tj/sdns/server" 4 | import "github.com/tj/sdns/config" 5 | import "github.com/tj/go-gracefully" 6 | import "github.com/tj/docopt" 7 | import "log" 8 | import "os" 9 | 10 | var Version = "0.1.0" 11 | 12 | const Usage = ` 13 | Usage: 14 | sdns 15 | sdns -h | --help 16 | sdns --version 17 | 18 | Options: 19 | -h, --help output help information 20 | -v, --version output version 21 | 22 | ` 23 | 24 | func main() { 25 | _, err := docopt.Parse(Usage, nil, true, Version, false) 26 | if err != nil { 27 | log.Fatalf("[error] %s", err) 28 | } 29 | 30 | c, err := config.Read(os.Stdin) 31 | if err != nil { 32 | log.Fatalf("[error] parsing config: %s", err) 33 | } 34 | 35 | s := server.New(c) 36 | 37 | log.Printf("[info] starting sdns %s", Version) 38 | err = s.Start() 39 | if err != nil { 40 | log.Fatalf("[error] %s", err) 41 | } 42 | 43 | log.Printf("[info] binding on %s", s.Config.Bind) 44 | gracefully.Shutdown() 45 | 46 | log.Printf("[info] stopping") 47 | err = s.Stop() 48 | if err != nil { 49 | log.Fatalf("[error] %s", err) 50 | } 51 | 52 | log.Printf("[info] bye :)") 53 | } 54 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "gopkg.in/yaml.v2" 4 | import "io/ioutil" 5 | import "io" 6 | import "os" 7 | 8 | // Config for bind address, domain, and upstream NS. 9 | type Config struct { 10 | Bind string 11 | Domains []*Domain 12 | Upstream []string 13 | } 14 | 15 | // Domain represents a domain and its resolver command. 16 | type Domain struct { 17 | Name string 18 | Command string 19 | } 20 | 21 | // Read config from io.Reader. 22 | func Read(r io.Reader) (*Config, error) { 23 | b, err := ioutil.ReadAll(r) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | c := new(Config) 29 | err = yaml.Unmarshal(b, c) 30 | return c, err 31 | } 32 | 33 | // ReadFile reads config from `path`. 34 | func ReadFile(path string) (*Config, error) { 35 | f, err := os.Open(path) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return Read(f) 41 | } 42 | -------------------------------------------------------------------------------- /examples/ec2.yml: -------------------------------------------------------------------------------- 1 | bind: ":5000" 2 | upstream: 3 | - 8.8.8.8 4 | - 8.8.4.4 5 | domains: 6 | - name: ec2.local. 7 | command: sdns-ec2 8 | - name: ec2. 9 | command: sdns-ec2 10 | -------------------------------------------------------------------------------- /examples/echo.yml: -------------------------------------------------------------------------------- 1 | bind: ":5000" 2 | upstream: 3 | - 8.8.8.8 4 | - 8.8.4.4 5 | domains: 6 | - name: foo 7 | command: | 8 | echo '[{ "type": "A", "value": "1.1.1.1", "ttl": 60 }, { "type": "A", "value": "1.1.1.4", "ttl": 60 }]' 9 | - name: bar 10 | command: | 11 | echo '[{ "type": "A", "value": "1.1.1.2", "ttl": 60 }]' 12 | - name: foo.bar 13 | command: | 14 | echo '[{ "type": "A", "value": "1.1.1.3", "ttl": 300 }]' 15 | - name: boom 16 | command: | 17 | echo 'something goes boom' && exit 1 -------------------------------------------------------------------------------- /sdns.go: -------------------------------------------------------------------------------- 1 | // 2 | // SDNS is a recursive nameserver supporting pluggable resovers 3 | // via arbitrary commands. 4 | // 5 | // Resolver commands accept a Question encoded as JSON via stdin, 6 | // and write Answers to stdout encoded as JSON. 7 | // 8 | package sdns 9 | 10 | import "encoding/json" 11 | import "net" 12 | import "fmt" 13 | import "io" 14 | 15 | // Question query. 16 | type Question struct { 17 | Name string `json:"name"` 18 | Type string `json:"type"` 19 | Class string `json:"class"` 20 | } 21 | 22 | // String representation. 23 | func (q *Question) String() string { 24 | return fmt.Sprintf("name=%s type=%s class=%s", q.Name, q.Type, q.Class) 25 | } 26 | 27 | // Answer record(s). 28 | type Answers []*Answer 29 | 30 | // Validate the records. 31 | func (a Answers) Validate() error { 32 | for i, rr := range a { 33 | err := rr.Validate() 34 | if err != nil { 35 | return fmt.Errorf("record[%v]: %s", i, err) 36 | } 37 | } 38 | return nil 39 | } 40 | 41 | // Answer record. 42 | type Answer struct { 43 | Type string `json:"type"` 44 | Value string `json:"value"` 45 | TTL uint32 `json:"ttl"` 46 | } 47 | 48 | // String representation. 49 | func (a *Answer) String() string { 50 | return fmt.Sprintf("type=%s value=%s ttl=%d", a.Type, a.Value, a.TTL) 51 | } 52 | 53 | // Validate the record. 54 | func (a *Answer) Validate() error { 55 | switch a.Type { 56 | case "A": 57 | ip := net.ParseIP(a.Value) 58 | if ip == nil { 59 | return fmt.Errorf("invalid A record") 60 | } 61 | } 62 | return nil 63 | } 64 | 65 | // IP address. 66 | func (a *Answer) IP() net.IP { 67 | return net.ParseIP(a.Value) 68 | } 69 | 70 | // Read question from the given reader. 71 | func Read(r io.Reader) (*Question, error) { 72 | q := new(Question) 73 | err := json.NewDecoder(r).Decode(q) 74 | return q, err 75 | } 76 | 77 | // Write answers to the given writer. 78 | func Write(a Answers, w io.Writer) error { 79 | return json.NewEncoder(w).Encode(a) 80 | } 81 | -------------------------------------------------------------------------------- /server/domain.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "github.com/tj/sdns/config" 4 | import "github.com/miekg/dns" 5 | import "github.com/tj/sdns" 6 | import "encoding/json" 7 | import "strings" 8 | import "os/exec" 9 | import "bytes" 10 | import "time" 11 | import "log" 12 | import "fmt" 13 | 14 | // Domain resolver. 15 | type Domain struct { 16 | *config.Domain 17 | } 18 | 19 | // Strip suffix from the domain, for example "api-02.ec2." becomes "api-02". 20 | func (d *Domain) strip(name string) string { 21 | return strings.Replace(name, "."+d.Name, "", 1) 22 | } 23 | 24 | // ServeDNS resolution. 25 | func (d *Domain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { 26 | for _, q := range r.Question { 27 | log.Printf("[info] [%v] <-- %s %s %v\n", r.Id, 28 | dns.ClassToString[q.Qclass], 29 | dns.TypeToString[q.Qtype], 30 | q.Name) 31 | } 32 | 33 | res := new(dns.Msg) 34 | res.SetReply(r) 35 | res.Authoritative = true 36 | 37 | if r.Question[0].Qtype == dns.TypeSOA { 38 | res.Ns = append(res.Ns, d.soa()) 39 | } 40 | 41 | start := time.Now() 42 | answers, err := d.resolve(&r.Question[0]) 43 | if err != nil { 44 | log.Printf("[error] [%v] executing command: %s", r.Id, err) 45 | msg := new(dns.Msg) 46 | msg.SetRcode(r, dns.RcodeServerFailure) 47 | w.WriteMsg(msg) 48 | return 49 | } 50 | 51 | err = answers.Validate() 52 | if err != nil { 53 | log.Printf("[error] [%v] invalid answers: %s", r.Id, err) 54 | msg := new(dns.Msg) 55 | msg.SetRcode(r, dns.RcodeServerFailure) 56 | w.WriteMsg(msg) 57 | return 58 | } 59 | 60 | for _, answer := range answers { 61 | switch answer.Type { 62 | case "A": 63 | rr := &dns.A{ 64 | Hdr: dns.RR_Header{ 65 | Name: r.Question[0].Name, 66 | Rrtype: dns.TypeA, 67 | Class: dns.ClassINET, 68 | Ttl: answer.TTL, 69 | }, 70 | A: answer.IP(), 71 | } 72 | res.Answer = append(res.Answer, rr) 73 | } 74 | } 75 | 76 | log.Printf("[info] [%v] --> %s %s", r.Id, answers, time.Since(start)) 77 | 78 | err = w.WriteMsg(res) 79 | if err != nil { 80 | log.Printf("[error] [%v] failed to respond – %s", r.Id, err) 81 | } 82 | } 83 | 84 | // SOA record. 85 | func (d *Domain) soa() *dns.SOA { 86 | return &dns.SOA{ 87 | Hdr: dns.RR_Header{ 88 | Name: d.Name, 89 | Rrtype: dns.TypeSOA, 90 | Class: dns.ClassINET, 91 | Ttl: 0, 92 | }, 93 | Ns: "ns." + d.Name, 94 | Mbox: "hostmaster." + d.Name, 95 | Serial: uint32(time.Now().Unix()), 96 | Refresh: 3600, 97 | Retry: 900, 98 | Expire: 172800, 99 | Minttl: 0, 100 | } 101 | } 102 | 103 | // Resolve query via command. 104 | func (d *Domain) resolve(q *dns.Question) (sdns.Answers, error) { 105 | stdin := new(bytes.Buffer) 106 | stdout := new(bytes.Buffer) 107 | stderr := new(bytes.Buffer) 108 | 109 | query := &sdns.Question{ 110 | Name: d.strip(q.Name), 111 | Type: dns.TypeToString[q.Qtype], 112 | Class: dns.ClassToString[q.Qclass], 113 | } 114 | 115 | err := json.NewEncoder(stdin).Encode(query) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | cmd := exec.Command("sh", "-c", d.Command) 121 | cmd.Stdin = stdin 122 | cmd.Stdout = stdout 123 | cmd.Stderr = stderr 124 | 125 | err = cmd.Run() 126 | if err != nil { 127 | return nil, fmt.Errorf("%s: %s", err, stderr.String()) 128 | } 129 | 130 | var answers sdns.Answers 131 | err = json.NewDecoder(stdout).Decode(&answers) 132 | if err != nil { 133 | return nil, fmt.Errorf("failed to parse json: %s", stdout.String()) 134 | } 135 | 136 | return answers, nil 137 | } 138 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "github.com/tj/sdns/config" 4 | import "github.com/miekg/dns" 5 | import "log" 6 | 7 | // Server. 8 | type Server struct { 9 | udp *dns.Server 10 | tcp *dns.Server 11 | mux *dns.ServeMux 12 | *config.Config 13 | } 14 | 15 | // New server. 16 | func New(config *config.Config) *Server { 17 | return &Server{ 18 | Config: config, 19 | } 20 | } 21 | 22 | // Start server. 23 | func (s *Server) Start() error { 24 | s.mux = dns.NewServeMux() 25 | 26 | s.udp = &dns.Server{ 27 | Addr: s.Bind, 28 | Net: "udp", 29 | Handler: s.mux, 30 | UDPSize: 65535, 31 | } 32 | 33 | s.tcp = &dns.Server{ 34 | Addr: s.Bind, 35 | Net: "tcp", 36 | Handler: s.mux, 37 | } 38 | 39 | for _, domain := range s.Domains { 40 | s.mux.Handle(dns.Fqdn(domain.Name), &Domain{domain}) 41 | } 42 | 43 | s.mux.Handle(".", &RandomUpstream{s.Upstream}) 44 | 45 | go func() { 46 | if err := s.udp.ListenAndServe(); err != nil { 47 | log.Fatalf("[error] failed to bind udp server: %v", err) 48 | } 49 | }() 50 | 51 | go func() { 52 | if err := s.tcp.ListenAndServe(); err != nil { 53 | log.Fatalf("[error] failed to bind tcp server: %v", err) 54 | } 55 | }() 56 | 57 | return nil 58 | } 59 | 60 | // Stop the server. 61 | func (s *Server) Stop() error { 62 | err := s.tcp.Shutdown() 63 | if err != nil { 64 | return err 65 | } 66 | 67 | return s.udp.Shutdown() 68 | } 69 | -------------------------------------------------------------------------------- /server/upstream.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "github.com/miekg/dns" 4 | import "math/rand" 5 | import "log" 6 | 7 | // RandomUpstream resolves using a random NS in the set. 8 | type RandomUpstream struct { 9 | upstream []string 10 | } 11 | 12 | // ServeDNS resolution. 13 | func (h *RandomUpstream) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { 14 | ns := h.upstream[rand.Intn(len(h.upstream))] 15 | ns = defaultPort(ns) 16 | 17 | for _, q := range r.Question { 18 | log.Printf("[info] [%v] <== %s %s %v (ns %s)\n", r.Id, 19 | dns.ClassToString[q.Qclass], 20 | dns.TypeToString[q.Qtype], 21 | q.Name, 22 | ns) 23 | } 24 | 25 | client := &dns.Client{ 26 | Net: w.RemoteAddr().Network(), 27 | } 28 | 29 | res, rtt, err := client.Exchange(r, ns) 30 | if err != nil { 31 | msg := new(dns.Msg) 32 | msg.SetRcode(r, dns.RcodeServerFailure) 33 | w.WriteMsg(msg) 34 | return 35 | } 36 | 37 | log.Printf("[info] [%v] ==> %s:", r.Id, rtt) 38 | for _, a := range res.Answer { 39 | log.Printf("[info] [%v] ----> %s\n", r.Id, a) 40 | } 41 | 42 | err = w.WriteMsg(res) 43 | if err != nil { 44 | log.Printf("[error] [%v] failed to respond – %s", r.Id, err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server/utils.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "strings" 4 | 5 | // Default port to the standard. 6 | func defaultPort(addr string) string { 7 | if !strings.Contains(addr, ":") { 8 | return addr + ":53" 9 | } 10 | 11 | return addr 12 | } 13 | --------------------------------------------------------------------------------