diff --git a/.gitignore b/.gitignore index 485dee6..4f29079 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .idea +tests diff --git a/README.md b/README.md index 1eb0511..236f42b 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,15 @@ ## Description -The split plugin allows filtering DNS Server response Records based on network definitions. That way +The split plugin allows filtering DNS Server responses Records based on network definitions. That way you do not need to run multiple DNS servers to handle split DNS. +If there are multiple A Records in the response, only the records matching the defined network will be returned +to a matching querier, and the records not matching the network to the other sources. + +This plugin is not about security, it is design only to give a better answer to the incoming source IP, +if you need to apply security filtering rules, please consider using the [**coredns** *acl*](https://coredns.io/plugins/acl/) plugin. + ## Compilation This package will always be compiled as part of CoreDNS and not in a standalone way. It will require you to use `go get` or as a dependency on [plugin.cfg](https://github.com/coredns/coredns/blob/master/plugin.cfg). @@ -21,7 +27,7 @@ A simple way to consume this plugin, is by adding the following on [plugin.cfg]( split:go.linka.cloud/coredns-split ~~~ -Put this lower in the plugin list, so that *split* is executed after any of the other plugins. +Put this higher in the plugin list, so that *split* is before after any of the other plugins. After this you can compile coredns by: @@ -57,22 +63,17 @@ This plugin reports readiness to the ready plugin. It will be immediately ready. ## Examples -In this configuration, we forward all queries to 9.9.9.9 and print "example" whenever we receive -a query. +In this configuration, we forward all queries to 9.9.9.9 and filter out A records pointing to an IP address +in the 10.10.10.0/24 network except for queries coming from the 192.168.0.0/24 and 192.168.1.0/24 networks. ~~~ corefile . { + example { + 10.10.10.0/24 { + net 192.168.0.0/24 192.168.1.0/24 + } + } forward . 9.9.9.9 - example -} -~~~ - -Or without any external connectivity: - -~~~ corefile -. { - whoami - example } ~~~ diff --git a/ready.go b/ready.go index 7ce8fa4..bee97dc 100644 --- a/ready.go +++ b/ready.go @@ -2,4 +2,6 @@ package split // Ready implements the ready.Readiness interface, once this flips to true CoreDNS // assumes this plugin is ready for queries; it is not checked again. -func (e Split) Ready() bool { return true } +func (s Split) Ready() bool { + return true +} diff --git a/setup.go b/setup.go index 19c3f62..9e15d53 100644 --- a/setup.go +++ b/setup.go @@ -1,6 +1,8 @@ package split import ( + "net" + "github.com/coredns/caddy" "github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/plugin" @@ -12,17 +14,46 @@ func init() { plugin.Register("split", setup) } // setup is the function that gets called when the config parser see the token "example". Setup is responsible // for parsing any extra options the example plugin may have. The first token this function sees is "example". func setup(c *caddy.Controller) error { - c.Next() // Ignore "example" and give us the next token. - if c.NextArg() { - // If there was another token, return an error, because we don't have any configuration. - // Any errors returned from this setup function should be wrapped with plugin.Error, so we - // can present a slightly nicer error message to the user. - return plugin.Error("split", c.ArgErr()) + s := Split{} + for c.Next() { + r := rule{} + args := c.RemainingArgs() + r.zones = plugin.OriginsFromArgsOrServerBlock(args, c.ServerBlockKeys) + if c.NextBlock() { + n := network{} + _, ipnet, err := net.ParseCIDR(c.Val()) + if err != nil { + return err + } + n.record = ipnet + for c.NextBlock() { + for c.NextLine() { + a := c.Val() + _ = a + argsLoop: + for _, v := range c.RemainingArgs() { + _, ipnet, err := net.ParseCIDR(v) + if err != nil { + return err + } + for _, vv := range n.allowed { + if vv.Contains(ipnet.IP) { + continue argsLoop + } + } + n.allowed = append(n.allowed, ipnet) + } + } + r.networks = append(r.networks, n) + } + s.Rule = append(s.Rule, r) + } } // Add the Plugin to CoreDNS, so Servers can use it in their plugin chain. dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { - return Split{Next: next} + s.Next = next + return s }) // All OK, return a nil error. diff --git a/setup_test.go b/setup_test.go index cb0950a..3b23bf2 100644 --- a/setup_test.go +++ b/setup_test.go @@ -13,9 +13,4 @@ func TestSetup(t *testing.T) { if err := setup(c); err != nil { t.Fatalf("Expected no errors, but got: %v", err) } - - c = caddy.NewTestController("dns", `split more`) - if err := setup(c); err == nil { - t.Fatalf("Expected errors, but got: %v", err) - } } diff --git a/split.go b/split.go index 2abb293..a8dfb9d 100644 --- a/split.go +++ b/split.go @@ -5,11 +5,12 @@ package split import ( "context" + "net" "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin/metrics" clog "github.com/coredns/coredns/plugin/pkg/log" - + "github.com/coredns/coredns/request" "github.com/miekg/dns" ) @@ -20,44 +21,108 @@ var log = clog.NewWithPlugin("split") // Split is an example plugin to show how to write a plugin. type Split struct { Next plugin.Handler + + Rule []rule +} + +type rule struct { + zones []string + networks []network +} + +type network struct { + record *net.IPNet + allowed []*net.IPNet } // ServeDNS implements the plugin.Handler interface. This method gets called when example is used // in a Server. -func (e Split) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { +func (s Split) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { // This function could be simpler. I.e. just fmt.Println("example") here, but we want to show // a slightly more complex example as to make this more interesting. // Here we wrap the dns.ResponseWriter in a new ResponseWriter and call the next plugin, when the // answer comes back, it will print "example". - // Debug log that we've have seen the query. This will only be shown when the debug plugin is loaded. + // Debug log that we've seen the query. This will only be shown when the debug plugin is loaded. log.Debug("Received response") // Wrap. - pw := NewResponsePrinter(w) + pw := s.NewResponsePrinter(w, r) // Export metric with the server label set to the current server handling the request. requestCount.WithLabelValues(metrics.WithServer(ctx)).Inc() // Call next plugin (if any). - return plugin.NextOrFailure(e.Name(), e.Next, ctx, pw, r) + return plugin.NextOrFailure(s.Name(), s.Next, ctx, pw, r) } // Name implements the Handler interface. -func (e Split) Name() string { return "split" } +func (s Split) Name() string { return "split" } // ResponsePrinter wrap a dns.ResponseWriter and will write example to standard output when WriteMsg is called. type ResponsePrinter struct { dns.ResponseWriter + state request.Request + r *dns.Msg + src net.IP + rules []rule } // NewResponsePrinter returns ResponseWriter. -func NewResponsePrinter(w dns.ResponseWriter) *ResponsePrinter { - return &ResponsePrinter{ResponseWriter: w} +func (s Split) NewResponsePrinter(w dns.ResponseWriter, r *dns.Msg) *ResponsePrinter { + state := request.Request{W: w, Req: r} + ip := net.ParseIP(state.IP()) + return &ResponsePrinter{ResponseWriter: w, r: r, src: ip, rules: s.Rule, state: state} } // WriteMsg calls the underlying ResponseWriter's WriteMsg method and prints "example" to standard output. func (r *ResponsePrinter) WriteMsg(res *dns.Msg) error { - log.Info("example") + var rule rule + for _, v := range r.rules { + zone := plugin.Zones(v.zones).Matches(r.state.Name()) + if zone == "" { + continue + } + rule = v + break + } + var answers []dns.RR + var netAnswers []dns.RR + for _, v := range res.Answer { + rec, ok := v.(*dns.A) + if !ok { + answers = append(answers, v) + continue + } + var net *network + for _, vv := range rule.networks { + if vv.record.Contains(rec.A) { + net = &vv + break + } + } + if net == nil { + answers = append(answers, v) + continue + } + allowed := false + for _, vv := range net.allowed { + if vv.Contains(r.src) { + allowed = true + break + } + } + if allowed { + answers = append(answers, v) + netAnswers = append(netAnswers, v) + continue + } + log.Infof("request source %s: %s: filtering %s", r.src.String(), rec.Hdr.Name, rec.A) + } + if len(netAnswers) != 0 { + res.Answer = netAnswers + } else { + res.Answer = answers + } return r.ResponseWriter.WriteMsg(res) } diff --git a/split_test.go b/split_test.go index 1e0f8bb..5dda339 100644 --- a/split_test.go +++ b/split_test.go @@ -1,10 +1,7 @@ package split import ( - "bytes" "context" - golog "log" - "strings" "testing" "github.com/coredns/coredns/plugin/pkg/dnstest" @@ -17,11 +14,6 @@ func TestExample(t *testing.T) { // Create a new Split Plugin. Use the test.ErrorHandler as the next plugin. x := Split{Next: test.ErrorHandler()} - // Setup a new output buffer that is *not* standard output, so we can check if - // example is really being printed. - b := &bytes.Buffer{} - golog.SetOutput(b) - ctx := context.TODO() r := new(dns.Msg) r.SetQuestion("example.org.", dns.TypeA) @@ -31,7 +23,5 @@ func TestExample(t *testing.T) { // Call our plugin directly, and check the result. x.ServeDNS(ctx, rec, r) - if a := b.String(); !strings.Contains(a, "[INFO] plugin/split: example") { - t.Errorf("Failed to print '%s', got %s", "[INFO] plugin/split: example", a) - } + t.Log(rec.Msg) }