├── README.md ├── benchmark_test.go ├── handler.go ├── lookup_test.go ├── redis.go ├── setup.go └── types.go /README.md: -------------------------------------------------------------------------------- 1 | *redis* enables reading zone data from redis database. 2 | this plugin should be located right next to *etcd* in *plugins.cfg* 3 | 4 | ## syntax 5 | 6 | ~~~ 7 | redis 8 | ~~~ 9 | 10 | redis loads authoritative zones from redis server 11 | 12 | Address will default to local redis server (localhost:6379) 13 | ~~~ 14 | redis { 15 | address ADDR 16 | password PWD 17 | prefix PREFIX 18 | suffix SUFFIX 19 | connect_timeout TIMEOUT 20 | read_timeout TIMEOUT 21 | ttl TTL 22 | } 23 | ~~~ 24 | 25 | * `address` is redis server address to connect in the form of *host:port* or *ip:port*. 26 | * `password` is redis server *auth* key 27 | * `connect_timeout` time in ms to wait for redis server to connect 28 | * `read_timeout` time in ms to wait for redis server to respond 29 | * `ttl` default ttl for dns records, 300 if not provided 30 | * `prefix` add PREFIX to all redis keys 31 | * `suffix` add SUFFIX to all redis keys 32 | 33 | ## examples 34 | 35 | ~~~ corefile 36 | . { 37 | redis example.com { 38 | address localhost:6379 39 | password foobared 40 | connect_timeout 100 41 | read_timeout 100 42 | ttl 360 43 | prefix _dns: 44 | } 45 | } 46 | ~~~ 47 | 48 | ## reverse zones 49 | 50 | reverse zones is not supported yet 51 | 52 | ## proxy 53 | 54 | proxy is not supported yet 55 | 56 | ## zone format in redis db 57 | 58 | ### zones 59 | 60 | each zone is stored in redis as a hash map with *zone* as key 61 | 62 | ~~~ 63 | redis-cli>KEYS * 64 | 1) "example.com." 65 | 2) "example.net." 66 | redis-cli> 67 | ~~~ 68 | 69 | ### dns RRs 70 | 71 | dns RRs are stored in redis as json strings inside a hash map using address as field key. 72 | *@* is used for zone's own RR values. 73 | 74 | #### A 75 | 76 | ~~~json 77 | { 78 | "a":{ 79 | "ip" : "1.2.3.4", 80 | "ttl" : 360 81 | } 82 | } 83 | ~~~ 84 | 85 | #### AAAA 86 | 87 | ~~~json 88 | { 89 | "aaaa":{ 90 | "ip" : "::1", 91 | "ttl" : 360 92 | } 93 | } 94 | ~~~ 95 | 96 | #### CNAME 97 | 98 | ~~~json 99 | { 100 | "cname":{ 101 | "host" : "x.example.com.", 102 | "ttl" : 360 103 | } 104 | } 105 | ~~~ 106 | 107 | #### TXT 108 | 109 | ~~~json 110 | { 111 | "txt":{ 112 | "text" : "this is a text", 113 | "ttl" : 360 114 | } 115 | } 116 | ~~~ 117 | 118 | #### NS 119 | 120 | ~~~json 121 | { 122 | "ns":{ 123 | "host" : "ns1.example.com.", 124 | "ttl" : 360 125 | } 126 | } 127 | ~~~ 128 | 129 | #### MX 130 | 131 | ~~~json 132 | { 133 | "mx":{ 134 | "host" : "mx1.example.com", 135 | "priority" : 10, 136 | "ttl" : 360 137 | } 138 | } 139 | ~~~ 140 | 141 | #### SRV 142 | 143 | ~~~json 144 | { 145 | "srv":{ 146 | "host" : "sip.example.com.", 147 | "port" : 555, 148 | "priority" : 10, 149 | "weight" : 100, 150 | "ttl" : 360 151 | } 152 | } 153 | ~~~ 154 | 155 | #### SOA 156 | 157 | ~~~json 158 | { 159 | "soa":{ 160 | "ttl" : 100, 161 | "mbox" : "hostmaster.example.com.", 162 | "ns" : "ns1.example.com.", 163 | "refresh" : 44, 164 | "retry" : 55, 165 | "expire" : 66 166 | } 167 | } 168 | ~~~ 169 | 170 | #### CAA 171 | 172 | ~~~json 173 | { 174 | "caa":{ 175 | "flag" : 0, 176 | "tag" : "issue", 177 | "value" : "letsencrypt.org" 178 | } 179 | } 180 | ~~~ 181 | 182 | #### example 183 | 184 | ~~~ 185 | $ORIGIN example.net. 186 | example.net. 300 IN SOA 187 | example.net. 300 NS ns1.example.net. 188 | example.net. 300 NS ns2.example.net. 189 | *.example.net. 300 TXT "this is a wildcard" 190 | *.example.net. 300 MX 10 host1.example.net. 191 | sub.*.example.net. 300 TXT "this is not a wildcard" 192 | host1.example.net. 300 A 5.5.5.5 193 | _ssh.tcp.host1.example.net. 300 SRV 194 | _ssh.tcp.host2.example.net. 300 SRV 195 | subdel.example.net. 300 NS ns1.subdel.example.net. 196 | subdel.example.net. 300 NS ns2.subdel.example.net. 197 | host2.example.net CAA 0 issue "letsencrypt.org" 198 | ~~~ 199 | 200 | above zone data should be stored at redis as follow: 201 | 202 | ~~~ 203 | redis-cli> hgetall example.net. 204 | 1) "_ssh._tcp.host1" 205 | 2) "{\"srv\":[{\"ttl\":300, \"target\":\"tcp.example.com.\",\"port\":123,\"priority\":10,\"weight\":100}]}" 206 | 3) "*" 207 | 4) "{\"txt\":[{\"ttl\":300, \"text\":\"this is a wildcard\"}],\"mx\":[{\"ttl\":300, \"host\":\"host1.example.net.\",\"preference\": 10}]}" 208 | 5) "host1" 209 | 6) "{\"a\":[{\"ttl\":300, \"ip\":\"5.5.5.5\"}]}" 210 | 7) "sub.*" 211 | 8) "{\"txt\":[{\"ttl\":300, \"text\":\"this is not a wildcard\"}]}" 212 | 9) "_ssh._tcp.host2" 213 | 10) "{\"srv\":[{\"ttl\":300, \"target\":\"tcp.example.com.\",\"port\":123,\"priority\":10,\"weight\":100}]}" 214 | 11) "subdel" 215 | 12) "{\"ns\":[{\"ttl\":300, \"host\":\"ns1.subdel.example.net.\"},{\"ttl\":300, \"host\":\"ns2.subdel.example.net.\"}]}" 216 | 13) "@" 217 | 14) "{\"soa\":{\"ttl\":300, \"minttl\":100, \"mbox\":\"hostmaster.example.net.\",\"ns\":\"ns1.example.net.\",\"refresh\":44,\"retry\":55,\"expire\":66},\"ns\":[{\"ttl\":300, \"host\":\"ns1.example.net.\"},{\"ttl\":300, \"host\":\"ns2.example.net.\"}]}" 218 | 15) "host2" 219 | 16)"{\"caa\":[{\"flag\":0, \"tag\":\"issue\", \"value\":\"letsencrypt.org\"}]}" 220 | redis-cli> 221 | ~~~ 222 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "testing" 5 | "fmt" 6 | "math/rand" 7 | 8 | "github.com/coredns/coredns/plugin/pkg/dnstest" 9 | "github.com/coredns/coredns/plugin/test" 10 | 11 | "github.com/miekg/dns" 12 | ) 13 | 14 | var zone string = "example.com." 15 | 16 | var benchmarkEntries = [][]string{ 17 | {"@", 18 | "{\"a\":[{\"ttl\":300, \"ip\":\"2.2.2.2\"}]}", 19 | }, 20 | {"x", 21 | "{\"a\":[{\"ttl\":300, \"ip\":\"3.3.3.3\"}]}", 22 | }, 23 | {"y", 24 | "{\"a\":[{\"ttl\":300, \"ip\":\"4.4.4.4\"}]}", 25 | }, 26 | {"z", 27 | "{\"a\":[{\"ttl\":300, \"ip\":\"5.5.5.5\"}]}", 28 | }, 29 | } 30 | 31 | var testCasesHit = []test.Case { 32 | { 33 | Qname: "example.com.", Qtype: dns.TypeA, 34 | Answer: []dns.RR{ 35 | test.A("example.com. 300 IN A 2.2.2.2"), 36 | }, 37 | }, 38 | { 39 | Qname: "x.example.com.", Qtype: dns.TypeA, 40 | Answer: []dns.RR{ 41 | test.A("x.example.com. 300 IN A 3.3.3.3"), 42 | }, 43 | }, 44 | { 45 | Qname: "y.example.com.", Qtype: dns.TypeA, 46 | Answer: []dns.RR{ 47 | test.A("y.example.com. 300 IN A 4.4.4.4"), 48 | }, 49 | }, 50 | { 51 | Qname: "z.example.com.", Qtype: dns.TypeA, 52 | Answer: []dns.RR{ 53 | test.A("z.example.com. 300 IN A 5.5.5.5"), 54 | }, 55 | }, 56 | } 57 | 58 | var testCasesMiss = []test.Case { 59 | { 60 | Qname: "q.example.com.", Qtype: dns.TypeA, 61 | Rcode: dns.RcodeNameError, 62 | }, 63 | { 64 | Qname: "w.example.com.", Qtype: dns.TypeA, 65 | Rcode: dns.RcodeNameError, 66 | }, 67 | { 68 | Qname: "e.example.com.", Qtype: dns.TypeA, 69 | Rcode: dns.RcodeNameError, 70 | }, 71 | { 72 | Qname: "r.example.com.", Qtype: dns.TypeA, 73 | Rcode: dns.RcodeNameError, 74 | }, 75 | } 76 | 77 | func BenchmarkHit(b *testing.B) { 78 | fmt.Println("benchmark test") 79 | r := newRedisPlugin() 80 | conn := r.Pool.Get() 81 | defer conn.Close() 82 | conn.Do("EVAL", "return redis.call('del', unpack(redis.call('keys', ARGV[1])))", 0, r.keyPrefix + "*" + r.keySuffix) 83 | for _, cmd := range benchmarkEntries { 84 | err := r.save(zone, cmd[0], cmd[1]) 85 | if err != nil { 86 | fmt.Println("error in redis", err) 87 | } 88 | } 89 | b.ResetTimer() 90 | for i :=0; i zoneUpdateTime { 22 | redis.LoadZones() 23 | } 24 | 25 | zone := plugin.Zones(redis.Zones).Matches(qname) 26 | // fmt.Println("zone : ", zone) 27 | if zone == "" { 28 | return plugin.NextOrFailure(qname, redis.Next, ctx, w, r) 29 | } 30 | 31 | z := redis.load(zone) 32 | if z == nil { 33 | return redis.errorResponse(state, zone, dns.RcodeServerFailure, nil) 34 | } 35 | 36 | if qtype == "AXFR" { 37 | records := redis.AXFR(z) 38 | 39 | ch := make(chan *dns.Envelope) 40 | tr := new(dns.Transfer) 41 | tr.TsigSecret = nil 42 | 43 | go func(ch chan *dns.Envelope) { 44 | j, l := 0, 0 45 | 46 | for i, r := range records { 47 | l += dns.Len(r) 48 | if l > transferLength { 49 | ch <- &dns.Envelope{RR: records[j:i]} 50 | l = 0 51 | j = i 52 | } 53 | } 54 | if j < len(records) { 55 | ch <- &dns.Envelope{RR: records[j:]} 56 | } 57 | close(ch) 58 | }(ch) 59 | 60 | err := tr.Out(w, r, ch) 61 | if err != nil { 62 | fmt.Println(err) 63 | } 64 | w.Hijack() 65 | return dns.RcodeSuccess, nil 66 | } 67 | 68 | location := redis.findLocation(qname, z) 69 | if len(location) == 0 { // empty, no results 70 | return redis.errorResponse(state, zone, dns.RcodeNameError, nil) 71 | } 72 | 73 | answers := make([]dns.RR, 0, 10) 74 | extras := make([]dns.RR, 0, 10) 75 | 76 | record := redis.get(location, z) 77 | 78 | switch qtype { 79 | case "A": 80 | answers, extras = redis.A(qname, z, record) 81 | case "AAAA": 82 | answers, extras = redis.AAAA(qname, z, record) 83 | case "CNAME": 84 | answers, extras = redis.CNAME(qname, z, record) 85 | case "TXT": 86 | answers, extras = redis.TXT(qname, z, record) 87 | case "NS": 88 | answers, extras = redis.NS(qname, z, record) 89 | case "MX": 90 | answers, extras = redis.MX(qname, z, record) 91 | case "SRV": 92 | answers, extras = redis.SRV(qname, z, record) 93 | case "SOA": 94 | answers, extras = redis.SOA(qname, z, record) 95 | case "CAA": 96 | answers, extras = redis.CAA(qname, z, record) 97 | 98 | default: 99 | return redis.errorResponse(state, zone, dns.RcodeNotImplemented, nil) 100 | } 101 | 102 | m := new(dns.Msg) 103 | m.SetReply(r) 104 | m.Authoritative, m.RecursionAvailable, m.Compress = true, false, true 105 | 106 | m.Answer = append(m.Answer, answers...) 107 | m.Extra = append(m.Extra, extras...) 108 | 109 | state.SizeAndDo(m) 110 | m = state.Scrub(m) 111 | _ = w.WriteMsg(m) 112 | return dns.RcodeSuccess, nil 113 | } 114 | 115 | // Name implements the Handler interface. 116 | func (redis *Redis) Name() string { return "redis" } 117 | 118 | func (redis *Redis) errorResponse(state request.Request, zone string, rcode int, err error) (int, error) { 119 | m := new(dns.Msg) 120 | m.SetRcode(state.Req, rcode) 121 | m.Authoritative, m.RecursionAvailable, m.Compress = true, false, true 122 | 123 | state.SizeAndDo(m) 124 | _ = state.W.WriteMsg(m) 125 | // Return success as the rcode to signal we have written to the client. 126 | return dns.RcodeSuccess, err 127 | } 128 | -------------------------------------------------------------------------------- /lookup_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "fmt" 7 | 8 | "github.com/coredns/coredns/plugin/pkg/dnstest" 9 | "github.com/coredns/coredns/plugin/test" 10 | 11 | "github.com/miekg/dns" 12 | ) 13 | 14 | var zones = []string { 15 | "example.com.", "example.net.", 16 | } 17 | 18 | var lookupEntries = [][][]string { 19 | { 20 | {"@", 21 | "{\"soa\":{\"ttl\":300, \"minttl\":100, \"mbox\":\"hostmaster.example.com.\",\"ns\":\"ns1.example.com.\",\"refresh\":44,\"retry\":55,\"expire\":66}}", 22 | }, 23 | {"x", 24 | "{\"a\":[{\"ttl\":300, \"ip\":\"1.2.3.4\"},{\"ttl\":300, \"ip\":\"5.6.7.8\"}]," + 25 | "\"aaaa\":[{\"ttl\":300, \"ip\":\"::1\"}]," + 26 | "\"txt\":[{\"ttl\":300, \"text\":\"foo\"},{\"ttl\":300, \"text\":\"bar\"}]," + 27 | "\"ns\":[{\"ttl\":300, \"host\":\"ns1.example.com.\"},{\"ttl\":300, \"host\":\"ns2.example.com.\"}]," + 28 | "\"mx\":[{\"ttl\":300, \"host\":\"mx1.example.com.\", \"preference\":10},{\"ttl\":300, \"host\":\"mx2.example.com.\", \"preference\":10}]}", 29 | }, 30 | {"y", 31 | "{\"cname\":[{\"ttl\":300, \"host\":\"x.example.com.\"}]}", 32 | }, 33 | {"ns1", 34 | "{\"a\":[{\"ttl\":300, \"ip\":\"2.2.2.2\"}]}", 35 | }, 36 | {"ns2", 37 | "{\"a\":[{\"ttl\":300, \"ip\":\"3.3.3.3\"}]}", 38 | }, 39 | {"_sip._tcp", 40 | "{\"srv\":[{\"ttl\":300, \"target\":\"sip.example.com.\",\"port\":555,\"priority\":10,\"weight\":100}]}", 41 | }, 42 | {"sip", 43 | "{\"a\":[{\"ttl\":300, \"ip\":\"7.7.7.7\"}]," + 44 | "\"aaaa\":[{\"ttl\":300, \"ip\":\"::1\"}]}", 45 | }, 46 | }, 47 | { 48 | {"@", 49 | "{\"soa\":{\"ttl\":300, \"minttl\":100, \"mbox\":\"hostmaster.example.net.\",\"ns\":\"ns1.example.net.\",\"refresh\":44,\"retry\":55,\"expire\":66}," + 50 | "\"ns\":[{\"ttl\":300, \"host\":\"ns1.example.net.\"},{\"ttl\":300, \"host\":\"ns2.example.net.\"}]}", 51 | }, 52 | {"sub.*", 53 | "{\"txt\":[{\"ttl\":300, \"text\":\"this is not a wildcard\"}]}", 54 | }, 55 | {"host1", 56 | "{\"a\":[{\"ttl\":300, \"ip\":\"5.5.5.5\"}]}", 57 | }, 58 | {"subdel", 59 | "{\"ns\":[{\"ttl\":300, \"host\":\"ns1.subdel.example.net.\"},{\"ttl\":300, \"host\":\"ns2.subdel.example.net.\"}]}", 60 | }, 61 | {"*", 62 | "{\"txt\":[{\"ttl\":300, \"text\":\"this is a wildcard\"}]," + 63 | "\"mx\":[{\"ttl\":300, \"host\":\"host1.example.net.\",\"preference\": 10}]}", 64 | }, 65 | {"_ssh._tcp.host1", 66 | "{\"srv\":[{\"ttl\":300, \"target\":\"tcp.example.com.\",\"port\":123,\"priority\":10,\"weight\":100}]}", 67 | }, 68 | {"_ssh._tcp.host2", 69 | "{\"srv\":[{\"ttl\":300, \"target\":\"tcp.example.com.\",\"port\":123,\"priority\":10,\"weight\":100}]}", 70 | }, 71 | }, 72 | } 73 | 74 | var testCases = [][]test.Case{ 75 | // basic tests 76 | { 77 | // A Test 78 | { 79 | Qname: "x.example.com.", Qtype: dns.TypeA, 80 | Answer: []dns.RR{ 81 | test.A("x.example.com. 300 IN A 1.2.3.4"), 82 | test.A("x.example.com. 300 IN A 5.6.7.8"), 83 | }, 84 | }, 85 | // AAAA Test 86 | { 87 | Qname: "x.example.com.", Qtype: dns.TypeAAAA, 88 | Answer: []dns.RR{ 89 | test.AAAA("x.example.com. 300 IN AAAA ::1"), 90 | }, 91 | }, 92 | // TXT Test 93 | { 94 | Qname: "x.example.com.", Qtype: dns.TypeTXT, 95 | Answer: []dns.RR{ 96 | test.TXT("x.example.com. 300 IN TXT bar"), 97 | test.TXT("x.example.com. 300 IN TXT foo"), 98 | }, 99 | }, 100 | // CNAME Test 101 | { 102 | Qname: "y.example.com.", Qtype: dns.TypeCNAME, 103 | Answer: []dns.RR{ 104 | test.CNAME("y.example.com. 300 IN CNAME x.example.com."), 105 | }, 106 | }, 107 | // NS Test 108 | { 109 | Qname: "x.example.com.", Qtype: dns.TypeNS, 110 | Answer: []dns.RR{ 111 | test.NS("x.example.com. 300 IN NS ns1.example.com."), 112 | test.NS("x.example.com. 300 IN NS ns2.example.com."), 113 | }, 114 | Extra: []dns.RR{ 115 | test.A("ns1.example.com. 300 IN A 2.2.2.2"), 116 | test.A("ns2.example.com. 300 IN A 3.3.3.3"), 117 | }, 118 | }, 119 | // MX Test 120 | { 121 | Qname: "x.example.com.", Qtype: dns.TypeMX, 122 | Answer: []dns.RR{ 123 | test.MX("x.example.com. 300 IN MX 10 mx1.example.com."), 124 | test.MX("x.example.com. 300 IN MX 10 mx2.example.com."), 125 | }, 126 | }, 127 | // SRV Test 128 | { 129 | Qname: "_sip._tcp.example.com.", Qtype: dns.TypeSRV, 130 | Answer: []dns.RR{ 131 | test.SRV("_sip._tcp.example.com. 300 IN SRV 10 100 555 sip.example.com."), 132 | }, 133 | Extra: []dns.RR{ 134 | test.A("sip.example.com. 300 IN A 7.7.7.7"), 135 | test.AAAA("sip.example.com 300 IN AAAA ::1"), 136 | }, 137 | }, 138 | // NXDOMAIN Test 139 | { 140 | Qname: "notexists.example.com.", Qtype: dns.TypeA, 141 | Rcode: dns.RcodeNameError, 142 | }, 143 | // SOA Test 144 | { 145 | Qname: "example.com.", Qtype: dns.TypeSOA, 146 | Answer: []dns.RR{ 147 | test.SOA("example.com. 300 IN SOA ns1.example.com. hostmaster.example.com. 1460498836 44 55 66 100"), 148 | }, 149 | }, 150 | }, 151 | // Wildcard Tests 152 | { 153 | { 154 | Qname: "host3.example.net.", Qtype: dns.TypeMX, 155 | Answer: []dns.RR{ 156 | test.MX("host3.example.net. 300 IN MX 10 host1.example.net."), 157 | }, 158 | Extra: []dns.RR{ 159 | test.A("host1.example.net. 300 IN A 5.5.5.5"), 160 | }, 161 | }, 162 | { 163 | Qname: "host3.example.net.", Qtype: dns.TypeA, 164 | }, 165 | { 166 | Qname: "foo.bar.example.net.", Qtype: dns.TypeTXT, 167 | Answer: []dns.RR{ 168 | test.TXT("foo.bar.example.net. 300 IN TXT \"this is a wildcard\""), 169 | }, 170 | }, 171 | { 172 | Qname: "host1.example.net.", Qtype: dns.TypeMX, 173 | }, 174 | { 175 | Qname: "sub.*.example.net.", Qtype: dns.TypeMX, 176 | }, 177 | { 178 | Qname: "host.subdel.example.net.", Qtype: dns.TypeA, 179 | Rcode: dns.RcodeNameError, 180 | }, 181 | { 182 | Qname: "ghost.*.example.net.", Qtype: dns.TypeMX, 183 | Rcode: dns.RcodeNameError, 184 | }, 185 | { 186 | Qname: "f.h.g.f.t.r.e.example.net.", Qtype: dns.TypeTXT, 187 | Answer: []dns.RR{ 188 | test.TXT("f.h.g.f.t.r.e.example.net. 300 IN TXT \"this is a wildcard\""), 189 | }, 190 | }, 191 | }, 192 | } 193 | 194 | func newRedisPlugin() *Redis { 195 | ctxt = context.TODO() 196 | 197 | redis := new(Redis) 198 | redis.keyPrefix = "" 199 | redis.keySuffix = "" 200 | redis.Ttl = 300 201 | redis.redisAddress = "localhost:6379" 202 | redis.redisPassword = "" 203 | redis.Connect() 204 | redis.LoadZones() 205 | return redis 206 | /* 207 | return &Redis { 208 | keyPrefix: "", 209 | keySuffix:"", 210 | redisc: client, 211 | Ttl: 300, 212 | } redis := new(Redis) 213 | */ 214 | } 215 | 216 | func TestAnswer(t *testing.T) { 217 | fmt.Println("lookup test") 218 | r := newRedisPlugin() 219 | conn := r.Pool.Get() 220 | defer conn.Close() 221 | 222 | for i, zone := range zones { 223 | conn.Do("EVAL", "return redis.call('del', unpack(redis.call('keys', ARGV[1])))", 0, r.keyPrefix + zone + r.keySuffix) 224 | for _, cmd := range lookupEntries[i] { 225 | err := r.save(zone, cmd[0], cmd[1]) 226 | if err != nil { 227 | fmt.Println("error in redis", err) 228 | t.Fail() 229 | } 230 | } 231 | for _, tc := range testCases[i] { 232 | m := tc.Msg() 233 | 234 | rec := dnstest.NewRecorder(&test.ResponseWriter{}) 235 | r.ServeDNS(ctxt, rec, m) 236 | 237 | resp := rec.Msg 238 | 239 | // TODO(arash): this shouldn't happen, check plugin's empty response 240 | if resp == nil { 241 | resp = new(dns.Msg) 242 | } 243 | test.SortAndCheck(t, resp, tc) 244 | } 245 | } 246 | } 247 | 248 | var ctxt context.Context 249 | -------------------------------------------------------------------------------- /redis.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/miekg/dns" 7 | "strings" 8 | "time" 9 | 10 | "github.com/coredns/coredns/plugin" 11 | 12 | redisCon "github.com/gomodule/redigo/redis" 13 | ) 14 | 15 | type Redis struct { 16 | Next plugin.Handler 17 | Pool *redisCon.Pool 18 | redisAddress string 19 | redisPassword string 20 | connectTimeout int 21 | readTimeout int 22 | keyPrefix string 23 | keySuffix string 24 | Ttl uint32 25 | Zones []string 26 | LastZoneUpdate time.Time 27 | } 28 | 29 | func (redis *Redis) LoadZones() { 30 | var ( 31 | reply interface{} 32 | err error 33 | zones []string 34 | ) 35 | 36 | conn := redis.Pool.Get() 37 | if conn == nil { 38 | fmt.Println("error connecting to redis") 39 | return 40 | } 41 | defer conn.Close() 42 | 43 | reply, err = conn.Do("KEYS", redis.keyPrefix + "*" + redis.keySuffix) 44 | if err != nil { 45 | return 46 | } 47 | zones, err = redisCon.Strings(reply, nil) 48 | for i, _ := range zones { 49 | zones[i] = strings.TrimPrefix(zones[i], redis.keyPrefix) 50 | zones[i] = strings.TrimSuffix(zones[i], redis.keySuffix) 51 | } 52 | redis.LastZoneUpdate = time.Now() 53 | redis.Zones = zones 54 | } 55 | 56 | func (redis *Redis) A(name string, z *Zone, record *Record) (answers, extras []dns.RR) { 57 | for _, a := range record.A { 58 | if a.Ip == nil { 59 | continue 60 | } 61 | r := new(dns.A) 62 | r.Hdr = dns.RR_Header{Name: dns.Fqdn(name), Rrtype: dns.TypeA, 63 | Class: dns.ClassINET, Ttl: redis.minTtl(a.Ttl)} 64 | r.A = a.Ip 65 | answers = append(answers, r) 66 | } 67 | return 68 | } 69 | 70 | func (redis Redis) AAAA(name string, z *Zone, record *Record) (answers, extras []dns.RR) { 71 | for _, aaaa := range record.AAAA { 72 | if aaaa.Ip == nil { 73 | continue 74 | } 75 | r := new(dns.AAAA) 76 | r.Hdr = dns.RR_Header{Name: dns.Fqdn(name), Rrtype: dns.TypeAAAA, 77 | Class: dns.ClassINET, Ttl: redis.minTtl(aaaa.Ttl)} 78 | r.AAAA = aaaa.Ip 79 | answers = append(answers, r) 80 | } 81 | return 82 | } 83 | 84 | func (redis *Redis) CNAME(name string, z *Zone, record *Record) (answers, extras []dns.RR) { 85 | for _, cname := range record.CNAME { 86 | if len(cname.Host) == 0 { 87 | continue 88 | } 89 | r := new(dns.CNAME) 90 | r.Hdr = dns.RR_Header{Name: dns.Fqdn(name), Rrtype: dns.TypeCNAME, 91 | Class: dns.ClassINET, Ttl: redis.minTtl(cname.Ttl)} 92 | r.Target = dns.Fqdn(cname.Host) 93 | answers = append(answers, r) 94 | } 95 | return 96 | } 97 | 98 | func (redis *Redis) TXT(name string, z *Zone, record *Record) (answers, extras []dns.RR) { 99 | for _, txt := range record.TXT { 100 | if len(txt.Text) == 0 { 101 | continue 102 | } 103 | r:= new(dns.TXT) 104 | r.Hdr = dns.RR_Header{Name: dns.Fqdn(name), Rrtype: dns.TypeTXT, 105 | Class: dns.ClassINET, Ttl: redis.minTtl(txt.Ttl)} 106 | r.Txt = split255(txt.Text) 107 | answers = append(answers, r) 108 | } 109 | return 110 | } 111 | 112 | func (redis *Redis) NS(name string, z *Zone, record *Record) (answers, extras []dns.RR) { 113 | for _, ns := range record.NS { 114 | if len(ns.Host) == 0 { 115 | continue 116 | } 117 | r := new(dns.NS) 118 | r.Hdr = dns.RR_Header{Name: dns.Fqdn(name), Rrtype: dns.TypeNS, 119 | Class: dns.ClassINET, Ttl: redis.minTtl(ns.Ttl)} 120 | r.Ns = ns.Host 121 | answers = append(answers, r) 122 | extras = append(extras, redis.hosts(ns.Host, z)...) 123 | } 124 | return 125 | } 126 | 127 | func (redis *Redis) MX(name string, z *Zone, record *Record) (answers, extras []dns.RR) { 128 | for _, mx := range record.MX { 129 | if len(mx.Host) == 0 { 130 | continue 131 | } 132 | r := new(dns.MX) 133 | r.Hdr = dns.RR_Header{Name: dns.Fqdn(name), Rrtype: dns.TypeMX, 134 | Class: dns.ClassINET, Ttl: redis.minTtl(mx.Ttl)} 135 | r.Mx = mx.Host 136 | r.Preference = mx.Preference 137 | answers = append(answers, r) 138 | extras = append(extras, redis.hosts(mx.Host, z)...) 139 | } 140 | return 141 | } 142 | 143 | func (redis *Redis) SRV(name string, z *Zone, record *Record) (answers, extras []dns.RR) { 144 | for _, srv := range record.SRV { 145 | if len(srv.Target) == 0 { 146 | continue 147 | } 148 | r := new(dns.SRV) 149 | r.Hdr = dns.RR_Header{Name: dns.Fqdn(name), Rrtype: dns.TypeSRV, 150 | Class: dns.ClassINET, Ttl: redis.minTtl(srv.Ttl)} 151 | r.Target = srv.Target 152 | r.Weight = srv.Weight 153 | r.Port = srv.Port 154 | r.Priority = srv.Priority 155 | answers = append(answers, r) 156 | extras = append(extras, redis.hosts(srv.Target, z)...) 157 | } 158 | return 159 | } 160 | 161 | func (redis *Redis) SOA(name string, z *Zone, record *Record) (answers, extras []dns.RR) { 162 | r := new(dns.SOA) 163 | if record.SOA.Ns == "" { 164 | r.Hdr = dns.RR_Header{Name: dns.Fqdn(name), Rrtype: dns.TypeSOA, 165 | Class: dns.ClassINET, Ttl: redis.Ttl} 166 | r.Ns = "ns1." + name 167 | r.Mbox = "hostmaster." + name 168 | r.Refresh = 86400 169 | r.Retry = 7200 170 | r.Expire = 3600 171 | r.Minttl = redis.Ttl 172 | } else { 173 | r.Hdr = dns.RR_Header{Name: dns.Fqdn(z.Name), Rrtype: dns.TypeSOA, 174 | Class: dns.ClassINET, Ttl: redis.minTtl(record.SOA.Ttl)} 175 | r.Ns = record.SOA.Ns 176 | r.Mbox = record.SOA.MBox 177 | r.Refresh = record.SOA.Refresh 178 | r.Retry = record.SOA.Retry 179 | r.Expire = record.SOA.Expire 180 | r.Minttl = record.SOA.MinTtl 181 | } 182 | r.Serial = redis.serial() 183 | answers = append(answers, r) 184 | return 185 | } 186 | 187 | func (redis *Redis) CAA(name string, z *Zone, record *Record) (answers, extras []dns.RR) { 188 | if record == nil { 189 | return 190 | } 191 | for _, caa := range record.CAA { 192 | if caa.Value == "" || caa.Tag == ""{ 193 | continue 194 | } 195 | r := new(dns.CAA) 196 | r.Hdr = dns.RR_Header{Name: dns.Fqdn(name), Rrtype: dns.TypeCAA, Class: dns.ClassINET} 197 | r.Flag = caa.Flag 198 | r.Tag = caa.Tag 199 | r.Value = caa.Value 200 | answers = append(answers, r) 201 | } 202 | return 203 | } 204 | 205 | func (redis *Redis) AXFR(z *Zone) (records []dns.RR) { 206 | //soa, _ := redis.SOA(z.Name, z, record) 207 | soa := make([]dns.RR, 0) 208 | answers := make([]dns.RR, 0, 10) 209 | extras := make([]dns.RR, 0, 10) 210 | 211 | // Allocate slices for rr Records 212 | records = append(records, soa...) 213 | for key := range z.Locations { 214 | if key == "@" { 215 | location := redis.findLocation(z.Name, z) 216 | record := redis.get(location, z) 217 | soa, _ = redis.SOA(z.Name, z, record) 218 | } else { 219 | fqdnKey := dns.Fqdn(key) + z.Name 220 | var as []dns.RR 221 | var xs []dns.RR 222 | 223 | location := redis.findLocation(fqdnKey, z) 224 | record := redis.get(location, z) 225 | 226 | // Pull all zone records 227 | as, xs = redis.A(fqdnKey, z, record) 228 | answers = append(answers, as...) 229 | extras = append(extras, xs...) 230 | 231 | as, xs = redis.AAAA(fqdnKey, z, record) 232 | answers = append(answers, as...) 233 | extras = append(extras, xs...) 234 | 235 | as, xs = redis.CNAME(fqdnKey, z, record) 236 | answers = append(answers, as...) 237 | extras = append(extras, xs...) 238 | 239 | as, xs = redis.MX(fqdnKey, z, record) 240 | answers = append(answers, as...) 241 | extras = append(extras, xs...) 242 | 243 | as, xs = redis.SRV(fqdnKey, z, record) 244 | answers = append(answers, as...) 245 | extras = append(extras, xs...) 246 | 247 | as, xs = redis.TXT(fqdnKey, z, record) 248 | answers = append(answers, as...) 249 | extras = append(extras, xs...) 250 | } 251 | } 252 | 253 | records = soa 254 | records = append(records, answers...) 255 | records = append(records, extras...) 256 | records = append(records, soa...) 257 | 258 | fmt.Println(records) 259 | return 260 | } 261 | 262 | func (redis *Redis) hosts(name string, z *Zone) []dns.RR { 263 | var ( 264 | record *Record 265 | answers []dns.RR 266 | ) 267 | location := redis.findLocation(name, z) 268 | if location == "" { 269 | return nil 270 | } 271 | record = redis.get(location, z) 272 | a, _ := redis.A(name, z, record) 273 | answers = append(answers, a...) 274 | aaaa, _ := redis.AAAA(name, z, record) 275 | answers = append(answers, aaaa...) 276 | cname, _ := redis.CNAME(name, z, record) 277 | answers = append(answers, cname...) 278 | return answers 279 | } 280 | 281 | func (redis *Redis) serial() uint32 { 282 | return uint32(time.Now().Unix()) 283 | } 284 | 285 | func (redis *Redis) minTtl(ttl uint32) uint32 { 286 | if redis.Ttl == 0 && ttl == 0 { 287 | return defaultTtl 288 | } 289 | if redis.Ttl == 0 { 290 | return ttl 291 | } 292 | if ttl == 0 { 293 | return redis.Ttl 294 | } 295 | if redis.Ttl < ttl { 296 | return redis.Ttl 297 | } 298 | return ttl 299 | } 300 | 301 | func (redis *Redis) findLocation(query string, z *Zone) string { 302 | var ( 303 | ok bool 304 | closestEncloser, sourceOfSynthesis string 305 | ) 306 | 307 | // request for zone records 308 | if query == z.Name { 309 | return query 310 | } 311 | 312 | query = strings.TrimSuffix(query, "." + z.Name) 313 | 314 | if _, ok = z.Locations[query]; ok { 315 | return query 316 | } 317 | 318 | closestEncloser, sourceOfSynthesis, ok = splitQuery(query) 319 | for ok { 320 | ceExists := keyMatches(closestEncloser, z) || keyExists(closestEncloser, z) 321 | ssExists := keyExists(sourceOfSynthesis, z) 322 | if ceExists { 323 | if ssExists { 324 | return sourceOfSynthesis 325 | } else { 326 | return "" 327 | } 328 | } else { 329 | closestEncloser, sourceOfSynthesis, ok = splitQuery(closestEncloser) 330 | } 331 | } 332 | return "" 333 | } 334 | 335 | func (redis *Redis) get(key string, z *Zone) *Record { 336 | var ( 337 | err error 338 | reply interface{} 339 | val string 340 | ) 341 | conn := redis.Pool.Get() 342 | if conn == nil { 343 | fmt.Println("error connecting to redis") 344 | return nil 345 | } 346 | defer conn.Close() 347 | 348 | var label string 349 | if key == z.Name { 350 | label = "@" 351 | } else { 352 | label = key 353 | } 354 | 355 | reply, err = conn.Do("HGET", redis.keyPrefix + z.Name + redis.keySuffix, label) 356 | if err != nil { 357 | return nil 358 | } 359 | val, err = redisCon.String(reply, nil) 360 | if err != nil { 361 | return nil 362 | } 363 | r := new(Record) 364 | err = json.Unmarshal([]byte(val), r) 365 | if err != nil { 366 | fmt.Println("parse error : ", val, err) 367 | return nil 368 | } 369 | return r 370 | } 371 | 372 | func keyExists(key string, z *Zone) bool { 373 | _, ok := z.Locations[key] 374 | return ok 375 | } 376 | 377 | func keyMatches(key string, z *Zone) bool { 378 | for value := range z.Locations { 379 | if strings.HasSuffix(value, key) { 380 | return true 381 | } 382 | } 383 | return false 384 | } 385 | 386 | func splitQuery(query string) (string, string, bool) { 387 | if query == "" { 388 | return "", "", false 389 | } 390 | var ( 391 | splits []string 392 | closestEncloser string 393 | sourceOfSynthesis string 394 | ) 395 | splits = strings.SplitAfterN(query, ".", 2) 396 | if len(splits) == 2 { 397 | closestEncloser = splits[1] 398 | sourceOfSynthesis = "*." + closestEncloser 399 | } else { 400 | closestEncloser = "" 401 | sourceOfSynthesis = "*" 402 | } 403 | return closestEncloser, sourceOfSynthesis, true 404 | } 405 | 406 | func (redis *Redis) Connect() { 407 | redis.Pool = &redisCon.Pool{ 408 | Dial: func () (redisCon.Conn, error) { 409 | opts := []redisCon.DialOption{} 410 | if redis.redisPassword != "" { 411 | opts = append(opts, redisCon.DialPassword(redis.redisPassword)) 412 | } 413 | if redis.connectTimeout != 0 { 414 | opts = append(opts, redisCon.DialConnectTimeout(time.Duration(redis.connectTimeout)*time.Millisecond)) 415 | } 416 | if redis.readTimeout != 0 { 417 | opts = append(opts, redisCon.DialReadTimeout(time.Duration(redis.readTimeout)*time.Millisecond)) 418 | } 419 | 420 | return redisCon.Dial("tcp", redis.redisAddress, opts...) 421 | }, 422 | } 423 | } 424 | 425 | func (redis *Redis) save(zone string, subdomain string, value string) error { 426 | var err error 427 | 428 | conn := redis.Pool.Get() 429 | if conn == nil { 430 | fmt.Println("error connecting to redis") 431 | return nil 432 | } 433 | defer conn.Close() 434 | 435 | _, err = conn.Do("HSET", redis.keyPrefix + zone + redis.keySuffix, subdomain, value) 436 | return err 437 | } 438 | 439 | func (redis *Redis) load(zone string) *Zone { 440 | var ( 441 | reply interface{} 442 | err error 443 | vals []string 444 | ) 445 | 446 | conn := redis.Pool.Get() 447 | if conn == nil { 448 | fmt.Println("error connecting to redis") 449 | return nil 450 | } 451 | defer conn.Close() 452 | 453 | reply, err = conn.Do("HKEYS", redis.keyPrefix + zone + redis.keySuffix) 454 | if err != nil { 455 | return nil 456 | } 457 | z := new(Zone) 458 | z.Name = zone 459 | vals, err = redisCon.Strings(reply, nil) 460 | if err != nil { 461 | return nil 462 | } 463 | z.Locations = make(map[string]struct{}) 464 | for _, val := range vals { 465 | z.Locations[val] = struct{}{} 466 | } 467 | 468 | return z 469 | } 470 | 471 | func split255(s string) []string { 472 | if len(s) < 255 { 473 | return []string{s} 474 | } 475 | sx := []string{} 476 | p, i := 0, 255 477 | for { 478 | if i <= len(s) { 479 | sx = append(sx, s[p:i]) 480 | } else { 481 | sx = append(sx, s[p:]) 482 | break 483 | 484 | } 485 | p, i = p+255, i+255 486 | } 487 | 488 | return sx 489 | } 490 | 491 | const ( 492 | defaultTtl = 360 493 | hostmaster = "hostmaster" 494 | zoneUpdateTime = 10*time.Minute 495 | transferLength = 1000 496 | ) -------------------------------------------------------------------------------- /setup.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/caddyserver/caddy" 7 | "github.com/coredns/coredns/core/dnsserver" 8 | "github.com/coredns/coredns/plugin" 9 | ) 10 | 11 | func init() { 12 | caddy.RegisterPlugin("redis", caddy.Plugin{ 13 | ServerType: "dns", 14 | Action: setup, 15 | }) 16 | } 17 | 18 | func setup(c *caddy.Controller) error { 19 | r, err := redisParse(c) 20 | if err != nil { 21 | return plugin.Error("redis", err) 22 | } 23 | 24 | dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { 25 | r.Next = next 26 | return r 27 | }) 28 | 29 | return nil 30 | } 31 | 32 | func redisParse(c *caddy.Controller) (*Redis, error) { 33 | redis := Redis { 34 | keyPrefix:"", 35 | keySuffix:"", 36 | Ttl:300, 37 | } 38 | var ( 39 | err error 40 | ) 41 | 42 | for c.Next() { 43 | if c.NextBlock() { 44 | for { 45 | switch c.Val() { 46 | case "address": 47 | if !c.NextArg() { 48 | return &Redis{}, c.ArgErr() 49 | } 50 | redis.redisAddress = c.Val() 51 | case "password": 52 | if !c.NextArg() { 53 | return &Redis{}, c.ArgErr() 54 | } 55 | redis.redisPassword = c.Val() 56 | case "prefix": 57 | if !c.NextArg() { 58 | return &Redis{}, c.ArgErr() 59 | } 60 | redis.keyPrefix = c.Val() 61 | case "suffix": 62 | if !c.NextArg() { 63 | return &Redis{}, c.ArgErr() 64 | } 65 | redis.keySuffix = c.Val() 66 | case "connect_timeout": 67 | if !c.NextArg() { 68 | return &Redis{}, c.ArgErr() 69 | } 70 | redis.connectTimeout, err = strconv.Atoi(c.Val()) 71 | if err != nil { 72 | redis.connectTimeout = 0 73 | } 74 | case "read_timeout": 75 | if !c.NextArg() { 76 | return &Redis{}, c.ArgErr() 77 | } 78 | redis.readTimeout, err = strconv.Atoi(c.Val()) 79 | if err != nil { 80 | redis.readTimeout = 0; 81 | } 82 | case "ttl": 83 | if !c.NextArg() { 84 | return &Redis{}, c.ArgErr() 85 | } 86 | var val int 87 | val, err = strconv.Atoi(c.Val()) 88 | if err != nil { 89 | val = defaultTtl 90 | } 91 | redis.Ttl = uint32(val) 92 | default: 93 | if c.Val() != "}" { 94 | return &Redis{}, c.Errf("unknown property '%s'", c.Val()) 95 | } 96 | } 97 | 98 | if !c.Next() { 99 | break 100 | } 101 | } 102 | 103 | } 104 | 105 | redis.Connect() 106 | redis.LoadZones() 107 | 108 | return &redis, nil 109 | } 110 | return &Redis{}, nil 111 | } 112 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import "net" 4 | 5 | type Zone struct { 6 | Name string 7 | Locations map[string]struct{} 8 | } 9 | 10 | type Record struct { 11 | A []A_Record `json:"a,omitempty"` 12 | AAAA []AAAA_Record `json:"aaaa,omitempty"` 13 | TXT []TXT_Record `json:"txt,omitempty"` 14 | CNAME []CNAME_Record `json:"cname,omitempty"` 15 | NS []NS_Record `json:"ns,omitempty"` 16 | MX []MX_Record `json:"mx,omitempty"` 17 | SRV []SRV_Record `json:"srv,omitempty"` 18 | CAA []CAA_Record `json:"caa,omitempty"` 19 | SOA SOA_Record `json:"soa,omitempty"` 20 | } 21 | 22 | type A_Record struct { 23 | Ttl uint32 `json:"ttl,omitempty"` 24 | Ip net.IP `json:"ip"` 25 | } 26 | 27 | type AAAA_Record struct { 28 | Ttl uint32 `json:"ttl,omitempty"` 29 | Ip net.IP `json:"ip"` 30 | } 31 | 32 | type TXT_Record struct { 33 | Ttl uint32 `json:"ttl,omitempty"` 34 | Text string `json:"text"` 35 | } 36 | 37 | type CNAME_Record struct { 38 | Ttl uint32 `json:"ttl,omitempty"` 39 | Host string `json:"host"` 40 | } 41 | 42 | type NS_Record struct { 43 | Ttl uint32 `json:"ttl,omitempty"` 44 | Host string `json:"host"` 45 | } 46 | 47 | type MX_Record struct { 48 | Ttl uint32 `json:"ttl,omitempty"` 49 | Host string `json:"host"` 50 | Preference uint16 `json:"preference"` 51 | } 52 | 53 | type SRV_Record struct { 54 | Ttl uint32 `json:"ttl,omitempty"` 55 | Priority uint16 `json:"priority"` 56 | Weight uint16 `json:"weight"` 57 | Port uint16 `json:"port"` 58 | Target string `json:"target"` 59 | } 60 | 61 | type SOA_Record struct { 62 | Ttl uint32 `json:"ttl,omitempty"` 63 | Ns string `json:"ns"` 64 | MBox string `json:"MBox"` 65 | Refresh uint32 `json:"refresh"` 66 | Retry uint32 `json:"retry"` 67 | Expire uint32 `json:"expire"` 68 | MinTtl uint32 `json:"minttl"` 69 | } 70 | 71 | type CAA_Record struct { 72 | Flag uint8 `json:"flag"` 73 | Tag string `json:"tag"` 74 | Value string `json:"value"` 75 | } --------------------------------------------------------------------------------