├── .travis.yml ├── LICENSE ├── README.md ├── base64.go ├── cache.go ├── handler.go ├── metrics.go ├── redis.go ├── setup.go ├── setup_test.go └── ttl.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 4 | - "1.12.x" 5 | 6 | env: 7 | - TESTS="-race -v -bench=. -coverprofile=coverage.txt -covermode=atomic" 8 | - TESTS="-race -v ./..." 9 | 10 | before_install: 11 | - sudo apt-get -qq update 12 | - sudo apt-get install -y redis-server 13 | 14 | script: 15 | - go test $TESTS 16 | 17 | after_success: 18 | - bash <(curl -s https://codecov.io/bash) 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redisc 2 | 3 | ## Name 4 | 5 | *redisc* - enables a networked cache using Redis. 6 | 7 | ## Description 8 | 9 | With *redisc* responses can be cached for up to 3600s. Caching in Redis is mostly useful in 10 | a setup where multiple CoreDNS instances share a VIP. E.g. multiple CoreDNS pods in a Kubernetes 11 | cluster. 12 | 13 | If Redis is not reachable this plugin will be a noop. The *cache* and *redisc* plugin can be used 14 | together, where *cache* is the L1 and *redisc* is the L2 level cache. 15 | If multiple CoreDNS instances get a cache miss for the same item, they will all be fetching the same 16 | information from an upstream and updating the cache, i.e. there is no (extra) coordination between 17 | those instances. 18 | 19 | If Redis is not available CoreDNS will simply not cache anything if metrics are enabled this will be 20 | visible in the `set_errors_total` metric. 21 | 22 | ## Syntax 23 | 24 | ~~~ txt 25 | redisc [TTL] [ZONES...] 26 | ~~~ 27 | 28 | * **TTL** max TTL in seconds. If not specified, the maximum TTL will be used, which is 3600 for 29 | noerror responses and 1800 for denial of existence ones. 30 | Setting a TTL of 300: `redisc 300` would cache records up to 300 seconds. 31 | * **ZONES** zones it should cache for. If empty, the zones from the configuration block are used. 32 | 33 | Each element in the Redis cache is cached according to its TTL (with **TTL** as the max). For the negative 34 | cache, the SOA's MinTTL value is used. When no endpoint is specified the default of `127.0.0.1:6379` will 35 | be used. 36 | 37 | If you want more control: 38 | 39 | ~~~ txt 40 | redisc [TTL] [ZONES...] { 41 | endpoint ENDPOINT 42 | } 43 | ~~~ 44 | 45 | * **TTL** and **ZONES** as above. 46 | * `endpoint` specifies which **ENDPOINT** to use for Redis, this default to `127.0.0.1:6379`. 47 | 48 | ## Metrics 49 | 50 | If monitoring is enabled (via the *prometheus* directive) then the following metrics are exported: 51 | 52 | * `coredns_redisc_hits_total{server}` - Counter of cache hits. 53 | * `coredns_redisc_misses_total{server}` - Counter of cache misses. 54 | * `coredns_redisc_set_errors_total{server}` - Counter of errors when connecting to Redis. 55 | * `coredns_redisc_drops_total{server}` - Counter of dropped messages. 56 | 57 | The `server` label indicates which server handled the request, see the *metrics* plugin for details. 58 | 59 | ## Examples 60 | 61 | Enable caching for all zones, cache locally and also cache for up to 40s in the cluster wide Redis. 62 | 63 | ~~~ corefile 64 | . { 65 | cache 30 66 | redisc 40 { 67 | endpoint 10.0.240.1:69 68 | } 69 | whoami 70 | } 71 | ~~~ 72 | 73 | Proxy to Google Public DNS and only cache responses for example.org (and below). 74 | 75 | ~~~ corefile 76 | . { 77 | proxy . 8.8.8.8:53 78 | redisc example.org 79 | } 80 | ~~~ 81 | 82 | ## See Also 83 | 84 | See [the Redis site for more information](https://redis.io) on Redis. An external plugin called 85 | [redis](https://coredns.io/explugins/redis) already exists, hence this is named *redisc*, for 86 | "redis cache". 87 | 88 | ## Bugs 89 | 90 | There is little unit testing. 91 | -------------------------------------------------------------------------------- /base64.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "encoding/base64" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | // ToString converts the message m to a bsae64 encoded string. 10 | func ToString(m *dns.Msg) string { 11 | b, _ := m.Pack() 12 | return base64.RawStdEncoding.EncodeToString(b) 13 | } 14 | 15 | // FromString converts s back into a DNS message. 16 | func FromString(s string, ttl int) *dns.Msg { 17 | m := new(dns.Msg) 18 | b, _ := base64.RawStdEncoding.DecodeString(s) 19 | m.Unpack(b) 20 | 21 | msgTTL(m, ttl) 22 | 23 | return m 24 | } 25 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "encoding/binary" 5 | "hash/fnv" 6 | "time" 7 | 8 | "github.com/coredns/coredns/plugin/pkg/response" 9 | "github.com/coredns/coredns/request" 10 | 11 | "github.com/miekg/dns" 12 | ) 13 | 14 | // Return key under which we store the message, -1 will be returned if we don't store the message. 15 | // Currently we do not cache Truncated, errors, zone transfers or dynamic update messages. 16 | func key(m *dns.Msg, t response.Type, do bool) int { 17 | // We don't store truncated responses. 18 | if m.Truncated { 19 | return -1 20 | } 21 | // Nor errors or Meta or Update 22 | if t == response.OtherError || t == response.Meta || t == response.Update { 23 | return -1 24 | } 25 | 26 | return hash(m.Question[0].Name, m.Question[0].Qtype, do) 27 | } 28 | 29 | var ( 30 | one = []byte("1") 31 | zero = []byte("0") 32 | ) 33 | 34 | func hash(qname string, qtype uint16, do bool) int { 35 | h := fnv.New32() 36 | 37 | if do { 38 | h.Write(one) 39 | } else { 40 | h.Write(zero) 41 | } 42 | 43 | b := make([]byte, 2) 44 | binary.BigEndian.PutUint16(b, qtype) 45 | h.Write(b) 46 | 47 | for i := range qname { 48 | c := qname[i] 49 | if c >= 'A' && c <= 'Z' { 50 | c += 'a' - 'A' 51 | } 52 | h.Write([]byte{c}) 53 | } 54 | 55 | return int(h.Sum32()) 56 | } 57 | 58 | // ResponseWriter is a response writer that caches the reply message in Redis. 59 | type ResponseWriter struct { 60 | dns.ResponseWriter 61 | state request.Request 62 | *Redis 63 | server string 64 | } 65 | 66 | // WriteMsg implements the dns.ResponseWriter interface. 67 | func (w *ResponseWriter) WriteMsg(res *dns.Msg) error { 68 | do := false 69 | mt, opt := response.Typify(res, w.now().UTC()) 70 | if opt != nil { 71 | do = opt.Do() 72 | } 73 | 74 | // key returns empty string for anything we don't want to cache. 75 | key := key(res, mt, do) 76 | 77 | duration := w.pttl 78 | if mt == response.NameError || mt == response.NoData { 79 | duration = w.nttl 80 | } 81 | 82 | msgTTL := minMsgTTL(res, mt) 83 | if msgTTL < duration { 84 | duration = msgTTL 85 | } 86 | 87 | if key != -1 && duration > 0 { 88 | 89 | if w.state.Match(res) { 90 | w.set(res, key, mt, duration) 91 | } else { 92 | // Don't log it, but increment counter 93 | cacheDrops.WithLabelValues(w.server).Inc() 94 | } 95 | } 96 | 97 | // Apply capped TTL to this reply to avoid jarring TTL experience 1799 -> 8 (e.g.) 98 | ttl := uint32(duration.Seconds()) 99 | for i := range res.Answer { 100 | res.Answer[i].Header().Ttl = ttl 101 | } 102 | for i := range res.Ns { 103 | res.Ns[i].Header().Ttl = ttl 104 | } 105 | for i := range res.Extra { 106 | if res.Extra[i].Header().Rrtype != dns.TypeOPT { 107 | res.Extra[i].Header().Ttl = ttl 108 | } 109 | } 110 | return w.ResponseWriter.WriteMsg(res) 111 | } 112 | 113 | func (w *ResponseWriter) set(m *dns.Msg, key int, mt response.Type, duration time.Duration) { 114 | if key == -1 || duration == 0 { 115 | return 116 | } 117 | 118 | switch mt { 119 | case response.NoError, response.Delegation: 120 | fallthrough 121 | 122 | case response.NameError, response.NoData: 123 | if err := Add(w.pool, key, m, duration); err != nil { 124 | log.Debugf("Failed to add response to Redis cache: %s", err) 125 | 126 | redisErr.WithLabelValues(w.server).Inc() 127 | } 128 | 129 | case response.OtherError: 130 | // don't cache these 131 | default: 132 | log.Warningf("Redis called with unknown typification: %d", mt) 133 | } 134 | } 135 | 136 | // Write implements the dns.ResponseWriter interface. 137 | func (w *ResponseWriter) Write(buf []byte) (int, error) { 138 | log.Warningf("Redis called with Write: not caching reply") 139 | n, err := w.ResponseWriter.Write(buf) 140 | return n, err 141 | } 142 | 143 | const ( 144 | maxTTL = 1 * time.Hour 145 | maxNTTL = 30 * time.Minute 146 | failSafeTTL = 5 * time.Second 147 | 148 | // Success is the class for caching positive caching. 149 | Success = "success" 150 | // Denial is the class defined for negative caching. 151 | Denial = "denial" 152 | ) 153 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/coredns/coredns/plugin" 7 | "github.com/coredns/coredns/plugin/metrics" 8 | "github.com/coredns/coredns/request" 9 | 10 | "github.com/miekg/dns" 11 | ) 12 | 13 | // ServeDNS implements the plugin.Handler interface. 14 | func (re *Redis) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { 15 | state := request.Request{W: w, Req: r} 16 | 17 | zone := plugin.Zones(re.Zones).Matches(state.Name()) 18 | if zone == "" { 19 | return plugin.NextOrFailure(re.Name(), re.Next, ctx, w, r) 20 | } 21 | 22 | server := metrics.WithServer(ctx) 23 | now := re.now().UTC() 24 | 25 | m := re.get(now, state, server) 26 | if m == nil { 27 | crr := &ResponseWriter{ResponseWriter: w, Redis: re, state: state, server: metrics.WithServer(ctx)} 28 | return plugin.NextOrFailure(re.Name(), re.Next, ctx, crr, r) 29 | } 30 | 31 | m.SetReply(r) 32 | w.WriteMsg(m) 33 | 34 | return dns.RcodeSuccess, nil 35 | } 36 | 37 | // Name implements the Handler interface. 38 | func (re *Redis) Name() string { return "redisc" } 39 | -------------------------------------------------------------------------------- /metrics.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "github.com/coredns/coredns/plugin" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | "github.com/prometheus/client_golang/prometheus/promauto" 8 | ) 9 | 10 | var ( 11 | cacheHits = promauto.NewCounterVec(prometheus.CounterOpts{ 12 | Namespace: plugin.Namespace, 13 | Subsystem: "redisc", 14 | Name: "hits_total", 15 | Help: "The count of cache hits.", 16 | }, []string{"server"}) 17 | 18 | cacheMisses = promauto.NewCounterVec(prometheus.CounterOpts{ 19 | Namespace: plugin.Namespace, 20 | Subsystem: "redisc", 21 | Name: "misses_total", 22 | Help: "The count of cache misses.", 23 | }, []string{"server"}) 24 | 25 | cacheDrops = promauto.NewCounterVec(prometheus.CounterOpts{ 26 | Namespace: plugin.Namespace, 27 | Subsystem: "redisc", 28 | Name: "drops_total", 29 | Help: "The number responses that are not cached, because the reply is malformed.", 30 | }, []string{"server"}) 31 | 32 | redisErr = promauto.NewCounterVec(prometheus.CounterOpts{ 33 | Namespace: plugin.Namespace, 34 | Subsystem: "redisc", 35 | Name: "set_errors_total", 36 | Help: "The count of errors when adding entries to redis.", 37 | }, []string{"server"}) 38 | ) 39 | -------------------------------------------------------------------------------- /redis.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/coredns/coredns/plugin" 9 | "github.com/coredns/coredns/request" 10 | 11 | "github.com/mediocregopher/radix.v2/pool" 12 | "github.com/miekg/dns" 13 | ) 14 | 15 | // Redis is plugin that looks up responses in a cache and caches replies. 16 | // It has a success and a denial of existence cache. 17 | type Redis struct { 18 | Next plugin.Handler 19 | Zones []string 20 | 21 | pool *pool.Pool 22 | nttl time.Duration 23 | pttl time.Duration 24 | 25 | addr string 26 | idle int 27 | // Testing. 28 | now func() time.Time 29 | } 30 | 31 | // New returns an new initialized Redis. 32 | func New() *Redis { 33 | return &Redis{ 34 | Zones: []string{"."}, 35 | addr: "127.0.0.1:6379", 36 | idle: 10, 37 | pool: &pool.Pool{}, 38 | pttl: maxTTL, 39 | nttl: maxNTTL, 40 | now: time.Now, 41 | } 42 | } 43 | 44 | // Add adds the message m under k in Redis. 45 | func Add(p *pool.Pool, key int, m *dns.Msg, duration time.Duration) error { 46 | // SETEX key duration m 47 | conn, err := p.Get() 48 | if err != nil { 49 | return err 50 | } 51 | defer p.Put(conn) 52 | 53 | resp := conn.Cmd("SETEX", strconv.Itoa(key), int(duration.Seconds()), ToString(m)) 54 | 55 | return resp.Err 56 | } 57 | 58 | // Get returns the message under key from Redis. 59 | func Get(p *pool.Pool, key int) (*dns.Msg, error) { 60 | conn, err := p.Get() 61 | if err != nil { 62 | return nil, err 63 | } 64 | defer p.Put(conn) 65 | 66 | resp := conn.Cmd("GET", strconv.Itoa(key)) 67 | if resp.Err != nil { 68 | return nil, resp.Err 69 | } 70 | 71 | ttl := 0 // Item just expired, slap 0 TTL on it. 72 | respTTL := conn.Cmd("TTL", strconv.Itoa(key)) 73 | if respTTL.Err == nil { 74 | ttl, err = respTTL.Int() 75 | if err != nil { 76 | ttl = 0 77 | } 78 | } 79 | 80 | s, _ := resp.Str() 81 | if s == "" { 82 | return nil, errors.New("not found") 83 | } 84 | 85 | m := FromString(s, ttl) 86 | 87 | return m, nil 88 | } 89 | 90 | func (re *Redis) get(now time.Time, state request.Request, server string) *dns.Msg { 91 | k := hash(state.Name(), state.QType(), state.Do()) 92 | 93 | m, err := Get(re.pool, k) 94 | if err != nil { 95 | log.Debugf("Failed to get response from Redis cache: %s", err) 96 | cacheMisses.WithLabelValues(server).Inc() 97 | return nil 98 | } 99 | log.Debugf("Returning response from Redis cache: %s for %s", m.Question[0].Name, state.Name()) 100 | cacheHits.WithLabelValues(server).Inc() 101 | return m 102 | } 103 | 104 | func (re *Redis) connect() (err error) { 105 | re.pool, err = pool.New("tcp", re.addr, re.idle) 106 | return err 107 | } 108 | -------------------------------------------------------------------------------- /setup.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/coredns/coredns/core/dnsserver" 11 | "github.com/coredns/coredns/plugin" 12 | clog "github.com/coredns/coredns/plugin/pkg/log" 13 | 14 | "github.com/coredns/caddy" 15 | ) 16 | 17 | var log = clog.NewWithPlugin("redisc") 18 | 19 | func init() { plugin.Register("redisc", setup) } 20 | 21 | func setup(c *caddy.Controller) error { 22 | re, err := parse(c) 23 | if err != nil { 24 | return plugin.Error("redisc", err) 25 | } 26 | if err := re.connect(); err != nil { 27 | log.Warningf("Failed to connect to Redis at %s: %s", re.addr, err) 28 | } else { 29 | log.Infof("Connected to Redis at %s", re.addr) 30 | } 31 | 32 | dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { 33 | re.Next = next 34 | return re 35 | }) 36 | 37 | return nil 38 | } 39 | 40 | func parse(c *caddy.Controller) (*Redis, error) { 41 | re := New() 42 | 43 | for c.Next() { 44 | // cache [ttl] [zones..] 45 | origins := make([]string, len(c.ServerBlockKeys)) 46 | copy(origins, c.ServerBlockKeys) 47 | args := c.RemainingArgs() 48 | 49 | if len(args) > 0 { 50 | // first args may be just a number, then it is the ttl, if not it is a zone 51 | ttl, err := strconv.Atoi(args[0]) 52 | if err == nil { 53 | // Reserve 0 (and smaller for future things) 54 | if ttl <= 0 { 55 | return nil, fmt.Errorf("cache TTL can not be zero or negative: %d", ttl) 56 | } 57 | re.pttl = time.Duration(ttl) * time.Second 58 | re.nttl = time.Duration(ttl) * time.Second 59 | args = args[1:] 60 | } 61 | if len(args) > 0 { 62 | copy(origins, args) 63 | } 64 | } 65 | 66 | // Refinements? In an extra block. 67 | for c.NextBlock() { 68 | switch c.Val() { 69 | case Success: 70 | args := c.RemainingArgs() 71 | if len(args) < 1 { 72 | return nil, c.ArgErr() 73 | } 74 | pttl, err := strconv.Atoi(args[0]) 75 | if err != nil { 76 | return nil, err 77 | } 78 | // Reserve 0 (and smaller for future things) 79 | if pttl <= 0 { 80 | return nil, fmt.Errorf("cache TTL can not be zero or negative: %d", pttl) 81 | } 82 | re.pttl = time.Duration(pttl) * time.Second 83 | case Denial: 84 | args := c.RemainingArgs() 85 | if len(args) < 1 { 86 | return nil, c.ArgErr() 87 | } 88 | nttl, err := strconv.Atoi(args[0]) 89 | if err != nil { 90 | return nil, err 91 | } 92 | // Reserve 0 (and smaller for future things) 93 | if nttl <= 0 { 94 | return nil, fmt.Errorf("cache TTL can not be zero or negative: %d", nttl) 95 | } 96 | re.nttl = time.Duration(nttl) * time.Second 97 | case "endpoint": 98 | args := c.RemainingArgs() 99 | if len(args) < 1 { 100 | return nil, c.ArgErr() 101 | } 102 | h, _, err := net.SplitHostPort(args[0]) 103 | if err != nil && strings.Contains(err.Error(), "missing port in address") { 104 | if x := net.ParseIP(args[0]); x == nil { 105 | return nil, fmt.Errorf("failed to parse IP: %s", args[0]) 106 | } 107 | 108 | re.addr = net.JoinHostPort(args[0], "6379") 109 | continue 110 | } 111 | if err != nil { 112 | return nil, err 113 | } 114 | // h should be a valid IP 115 | if x := net.ParseIP(h); x == nil { 116 | return nil, fmt.Errorf("failed to parse IP: %s", h) 117 | } 118 | re.addr = args[0] 119 | 120 | default: 121 | return nil, c.ArgErr() 122 | } 123 | } 124 | 125 | for i := range origins { 126 | origins[i] = plugin.Host(origins[i]).NormalizeExact()[0] 127 | } 128 | re.Zones = origins 129 | 130 | return re, nil 131 | } 132 | 133 | return nil, nil 134 | } 135 | -------------------------------------------------------------------------------- /setup_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/coredns/caddy" 8 | ) 9 | 10 | func TestSetup(t *testing.T) { 11 | const defEndpoint = "127.0.0.1:6379" 12 | 13 | tests := []struct { 14 | input string 15 | shouldErr bool 16 | expectedNttl time.Duration 17 | expectedPttl time.Duration 18 | expectedEndpoint string 19 | }{ 20 | {`redis`, false, maxNTTL, maxTTL, defEndpoint}, 21 | {`redis example.nl { 22 | success 10 23 | }`, false, maxNTTL, 10 * time.Second, defEndpoint}, 24 | {`redis example.nl { 25 | success 10 26 | denial 15 27 | }`, false, 15 * time.Second, 10 * time.Second, defEndpoint}, 28 | {`redis { 29 | endpoint 127.0.0.2:6379 30 | }`, false, maxNTTL, maxTTL, "127.0.0.2:6379"}, 31 | {`redis { 32 | endpoint 127.0.0.3 33 | }`, false, maxNTTL, maxTTL, "127.0.0.3:6379"}, 34 | 35 | // fails 36 | {`redis example.nl { 37 | success 15 38 | denial aaa 39 | }`, true, maxTTL, maxTTL, defEndpoint}, 40 | {`redis example.nl { 41 | positive 15 42 | negative aaa 43 | }`, true, maxTTL, maxTTL, defEndpoint}, 44 | {`redis { 45 | endpoint :1:1:6379 46 | }`, true, maxTTL, maxTTL, defEndpoint}, 47 | {`redis { 48 | endpoint 127.0.0.a 49 | }`, true, maxTTL, maxTTL, defEndpoint}, 50 | } 51 | for i, test := range tests { 52 | c := caddy.NewTestController("dns", test.input) 53 | re, err := parse(c) 54 | if test.shouldErr && err == nil { 55 | t.Errorf("Test %v: Expected error but found nil", i) 56 | continue 57 | } else if !test.shouldErr && err != nil { 58 | t.Errorf("Test %v: Expected no error but found error: %v", i, err) 59 | continue 60 | } 61 | if test.shouldErr && err != nil { 62 | continue 63 | } 64 | 65 | if re.nttl != test.expectedNttl { 66 | t.Errorf("Test %v: Expected nttl %v but found: %v", i, test.expectedNttl, re.nttl) 67 | } 68 | if re.pttl != test.expectedPttl { 69 | t.Errorf("Test %v: Expected pttl %v but found: %v", i, test.expectedPttl, re.pttl) 70 | } 71 | if re.addr != test.expectedEndpoint { 72 | t.Errorf("Test %v: Expected endpoint %v but found: %v", i, test.expectedEndpoint, re.addr) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /ttl.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/coredns/coredns/plugin/pkg/response" 7 | 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | func minMsgTTL(m *dns.Msg, mt response.Type) time.Duration { 12 | if mt != response.NoError && mt != response.NameError && mt != response.NoData { 13 | return 0 14 | } 15 | 16 | // No data to examine, return a short ttl as a fail safe. 17 | if len(m.Answer)+len(m.Ns) == 0 { 18 | return failSafeTTL 19 | } 20 | 21 | minTTL := maxTTL 22 | for _, r := range append(append(m.Answer, m.Ns...), m.Extra...) { 23 | if r.Header().Rrtype == dns.TypeOPT { 24 | // OPT records use TTL field for extended rcode and flags 25 | continue 26 | } 27 | switch mt { 28 | case response.NameError, response.NoData: 29 | if r.Header().Rrtype == dns.TypeSOA { 30 | return time.Duration(r.(*dns.SOA).Minttl) * time.Second 31 | } 32 | case response.NoError, response.Delegation: 33 | if r.Header().Ttl < uint32(minTTL.Seconds()) { 34 | minTTL = time.Duration(r.Header().Ttl) * time.Second 35 | } 36 | } 37 | } 38 | return minTTL 39 | } 40 | 41 | func msgTTL(m *dns.Msg, ttl int) { 42 | for i := range m.Answer { 43 | m.Answer[i].Header().Ttl = uint32(ttl) 44 | } 45 | for i := range m.Ns { 46 | m.Ns[i].Header().Ttl = uint32(ttl) 47 | } 48 | for i := range m.Extra { 49 | if m.Extra[i].Header().Rrtype != dns.TypeOPT { 50 | m.Extra[i].Header().Ttl = uint32(ttl) 51 | } 52 | } 53 | } 54 | --------------------------------------------------------------------------------