From 5469ff0a060991b53d93dcac43fdccae92b364b1 Mon Sep 17 00:00:00 2001 From: Kelvin Chua Date: Mon, 1 Feb 2021 06:49:00 +0800 Subject: [PATCH] introduce the match and reload features --- README.md | 28 ++++++++--- matchrelay.go | 17 ++++--- reload.go | 40 +++++++++++++++ setup.go | 131 ++++++++++++++++++++++++++++++++++++++------------ 4 files changed, 172 insertions(+), 44 deletions(-) create mode 100644 reload.go diff --git a/README.md b/README.md index 90831c3..e7f616c 100644 --- a/README.md +++ b/README.md @@ -13,20 +13,29 @@ is normally doing. This module has a dependency on the forward module and support multi proxies and resource optimizations as with the forward module. -to build, add this line into plugin.cfg +to build, pull coredns code +~~~ txt +git clone https://github.com/coredns/coredns.git +~~~ + +add this line into plugin.cfg ~~~ txt ... -chaos:chaos -loadbalance:loadbalance +etcd:etcd +loop:loop matchrelay:github.com/kelchy/matchrelay -cache:cache -rewrite:rewrite +forward:forward +grpc:grpc ... ~~~ take note of the order as ordinality of the plugins matter for coredns +since cache is above matchrelay, cache may serve responses without hitting matchrelay +this may cause unexpected behaviours, avoid using cache with matchrelay if the order of +plugins is made this way + you may need to set git to use ssh ~~~ txt git config --global url."git@github.com:".insteadOf "https://github.com/" @@ -54,6 +63,8 @@ go build ~~~ txt matchrelay { + match ./list.txt + reload 10s net relay } @@ -72,7 +83,12 @@ example.org { } ~~~ -or by importing a file +or by importing a file instead of using the internal +match and reload mechanism. note that if you use reload +module, the whole Corefile will be loaded in each reload. +if the number of zones or list is high, this may cause huge +spikes in CPU which may bring down performance. For very +dynamic environments, use the match and reload mechanism ~~~ corefile example.org { diff --git a/matchrelay.go b/matchrelay.go index 80c2eaf..c171a3c 100644 --- a/matchrelay.go +++ b/matchrelay.go @@ -4,7 +4,7 @@ package matchrelay import ( "context" "net" -// "strconv" + "time" "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin/forward" @@ -16,13 +16,16 @@ import ( // MatchRelay is a plugin that matches your IP address used for connecting to CoreDNS. type MatchRelay struct{ - fwd *forward.Forward Next plugin.Handler - Rules []rule + + fwd *forward.Forward + rules []rule + zones []string + interval time.Duration + filename string } type rule struct { - zones []string policies []policy } @@ -42,12 +45,12 @@ func (mr MatchRelay) SetProxy(proxy string) { } // ServeDNS implements the plugin.Handler interface. -func (mr MatchRelay) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { +func (mr *MatchRelay) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { state := request.Request{W: w, Req: r} - for _, rule := range mr.Rules { + for _, rule := range mr.rules { // check zone. - zone := plugin.Zones(rule.zones).Matches(state.Name()) + zone := plugin.Zones(mr.zones).Matches(state.Name()) if zone == "" { continue } diff --git a/reload.go b/reload.go new file mode 100644 index 0000000..408c8dc --- /dev/null +++ b/reload.go @@ -0,0 +1,40 @@ +package matchrelay + +import ( + "io/ioutil" + "strings" + + "github.com/coredns/coredns/plugin/pkg/log" +) + +func (mr *MatchRelay) Reload(buf []byte) { + mr.rules = nil + lines := strings.Split(string(buf), "\n") + r := rule{} + for _, line := range lines { + fields := strings.Split(line, " ") + if fields[0] == "net" { + id := fields[0] + fields = fields[1:] + p := makePolicy(fields) + if p.filter != nil { + p.ftype = id + r.policies = append(r.policies, p) + + } + } + } + if len(r.policies) > 0 { + mr.rules = append(mr.rules, r) + } +} + +func fileOpen(fileName string) ([]byte, error) { + + file, err := ioutil.ReadFile(fileName) + if err != nil { + log.Errorf("error opening file %s", fileName) + return file, err + } + return file, nil +} diff --git a/setup.go b/setup.go index 6cfe3f7..768bce7 100644 --- a/setup.go +++ b/setup.go @@ -3,10 +3,14 @@ package matchrelay import ( "net" "strings" + "time" + "path/filepath" + "crypto/md5" "github.com/coredns/caddy" "github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/log" "github.com/infobloxopen/go-trees/iptree" ) @@ -30,9 +34,51 @@ func setup(c *caddy.Controller) error { return plugin.Error(pluginName, err) } + loop := make(chan bool) + c.OnStartup(func() error { + if mr.interval == 0 || mr.filename == "" { + return nil + } + s, e := fileOpen(mr.filename) + if e != nil { + log.Errorf("error opening matchrelay file %s", mr.filename) + return e + } + md5sum := md5.Sum(s) + mr.Reload(s) + + go func() { + ticker := time.NewTicker(mr.interval) + for { + select { + case <-loop: + return + case <-ticker.C: + s, e := fileOpen(mr.filename) + if e != nil { + log.Errorf("error opening matchrelay file %s", mr.filename) + return + } + ms := md5.Sum(s) + if md5sum != ms { + log.Infof("Matchrelay new config MD5 = %x\n", ms) + md5sum = ms + mr.Reload(s) + } + } + } + }() + return nil + }) + + c.OnShutdown(func() error { + close(loop) + return nil + }) + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { mr.Next = next - return mr + return &mr }) return nil @@ -40,45 +86,36 @@ func setup(c *caddy.Controller) error { func parse(c *caddy.Controller) (MatchRelay, error) { mr := New() + // matchrelay takes zone details from server block, not on config block + mr.zones = make([]string, len(c.ServerBlockKeys)) + copy(mr.zones, c.ServerBlockKeys) + for i := range mr.zones { + mr.zones[i] = plugin.Host(mr.zones[i]).Normalize() + } for c.Next() { r := rule{} - r.zones = c.RemainingArgs() - if len(r.zones) == 0 { - // if empty, the zones from the configuration block are used. - r.zones = make([]string, len(c.ServerBlockKeys)) - copy(r.zones, c.ServerBlockKeys) - } - for i := range r.zones { - r.zones[i] = plugin.Host(r.zones[i]).Normalize() - } for c.NextBlock() { - p := policy{} - id := strings.ToLower(c.Val()) - if id == "net" { - p.ftype = id - p.filter = iptree.NewTree() - } else if id != "relay" { - return mr, c.Errf("unexpected token %q; expect 'net' or 'relay'", c.Val()) - } - remainingTokens := c.RemainingArgs() if len(remainingTokens) == 0 { return mr, c.Errf("empty token") } - if id == "net" { - token := strings.ToLower(remainingTokens[0]) - if token == "*" { - p.filter = newDefaultFilter() - break + switch id { + case "net": + // static rules + p := makePolicy(remainingTokens) + if p.filter != nil { + p.ftype = id + r.policies = append(r.policies, p) } - token = normalize(token) - _, source, err := net.ParseCIDR(token) + case "reload": + // TODO: add jitter + d, err := time.ParseDuration(remainingTokens[0]) if err != nil { - return mr, c.Errf("illegal CIDR notation %q", token) + return mr, plugin.Error("invalid reload timer", err) } - p.filter.InplaceInsertNet(source, struct{}{}) - } else { + mr.interval = d + case "relay": for len(remainingTokens) > 0 { i := 0 for ; i < len(remainingTokens) ; i++ { @@ -87,14 +124,46 @@ func parse(c *caddy.Controller) (MatchRelay, error) { } remainingTokens = remainingTokens[i:] } + case "match": + // file based rules with own reload mechanism compatible with static rules above + fileName := strings.ToLower(remainingTokens[0]) + config := dnsserver.GetConfig(c) + if !filepath.IsAbs(fileName) && config.Root != "" { + fileName = filepath.Join(config.Root, fileName) + } + mr.filename = fileName + default: + return mr, c.Errf("unexpected token %q; expect 'net', 'match', 'reload' or 'relay'", id) } - r.policies = append(r.policies, p) } - mr.Rules = append(mr.Rules, r) + if len(r.policies) > 0 { + mr.rules = append(mr.rules, r) + } } return mr, nil } +// take the cidrs and build the policy +func makePolicy(rule []string) policy { + p := policy{} + + // TODO: handle multiple CIDR, watch out for inline comments which may end up in rule slice + token := strings.ToLower(rule[0]) + if token == "*" { + p.filter = newDefaultFilter() + return p + } + token = normalize(token) + _, source, err := net.ParseCIDR(token) + if err != nil { + log.Errorf("illegal CIDR notation %q", token) + return p + } + p.filter = iptree.NewTree() + p.filter.InplaceInsertNet(source, struct{}{}) + return p +} + // normalize appends '/32' for any single IPv4 address and '/128' for IPv6. func normalize(rawNet string) string { if idx := strings.IndexAny(rawNet, "/"); idx >= 0 {