From ea41dd23a01d653b01d153a868d586fac4d4381a Mon Sep 17 00:00:00 2001 From: Mohammad Yosefpor <47300215+m-yosefpor@users.noreply.github.com> Date: Thu, 25 Mar 2021 20:08:17 +0430 Subject: [PATCH] plugin/bind: exclude interface or ip address (#4543) * plugin/bind: exclude interface or ip address Signed-off-by: Mohammad Yosefpor * fix README.md Signed-off-by: Mohammad Yosefpor * Apply suggestions, Fix test Signed-off-by: Mohammad Yosefpor * Apply suggestions, move errs to setup Signed-off-by: Mohammad Yosefpor --- plugin/bind/README.md | 30 ++++++++-- plugin/bind/bind.go | 13 ++++- plugin/bind/setup.go | 114 ++++++++++++++++++++++++++++---------- plugin/bind/setup_test.go | 1 + 4 files changed, 123 insertions(+), 35 deletions(-) diff --git a/plugin/bind/README.md b/plugin/bind/README.md index e3274d3d5..26ddc1ae4 100644 --- a/plugin/bind/README.md +++ b/plugin/bind/README.md @@ -17,13 +17,25 @@ If the given argument is an interface name, and that interface has serveral IP a ## Syntax +In its basic form, a simple bind uses this syntax: + ~~~ txt -bind ADDRESS ... +bind ADDRESS|IFACE ... ~~~ -**ADDRESS** is an IP address to bind to. -When several addresses are provided a listener will be opened on each of the addresses. +You can also exclude some addresses with their IP address or interface name in expanded syntax: +~~~ +bind ADDRESS|IFACE ... { + except ADDRESS|IFACE ... +} +~~~ + + + +* **ADDRESS|IFACE** is an IP address or interface name to bind to. +When several addresses are provided a listener will be opened on each of the addresses. Please read the *Description* for more details. +* `except`, excludes interfaces or IP addresses to bind to. `except` option only excludes addresses for the current `bind` directive if multiple `bind` directives are used in the same server block. ## Examples To make your socket accessible only to that machine, bind to IP 127.0.0.1 (localhost): @@ -60,6 +72,16 @@ The following server block, binds on localhost with its interface name (both "12 } ~~~ +You can exclude some addresses by their IP or interface name (The following will only listen on `::1` or whatever addresses have been assigned to the `lo` interface): + +~~~ corefile +. { + bind lo { + except 127.0.0.1 + } +} +~~~ + ## Bugs When defining more than one server block, take care not to bind more than one server to the same @@ -78,4 +100,4 @@ a.bad.example.com { bad.example.com { forward . 5.6.7.8 } -``` \ No newline at end of file +``` diff --git a/plugin/bind/bind.go b/plugin/bind/bind.go index cfbd36597..cada8fa98 100644 --- a/plugin/bind/bind.go +++ b/plugin/bind/bind.go @@ -1,6 +1,17 @@ // Package bind allows binding to a specific interface instead of bind to all of them. package bind -import "github.com/coredns/coredns/plugin" +import ( + "github.com/coredns/coredns/plugin" +) func init() { plugin.Register("bind", setup) } + +type bind struct { + Next plugin.Handler + addrs []string + except []string +} + +// Name implements plugin.Handler. +func (b *bind) Name() string { return "bind" } diff --git a/plugin/bind/setup.go b/plugin/bind/setup.go index d75d6c0ff..471cd2803 100644 --- a/plugin/bind/setup.go +++ b/plugin/bind/setup.go @@ -1,6 +1,7 @@ package bind import ( + "errors" "fmt" "net" @@ -10,48 +11,101 @@ import ( ) func setup(c *caddy.Controller) error { - config := dnsserver.GetConfig(c) + config := dnsserver.GetConfig(c) // addresses will be consolidated over all BIND directives available in that BlocServer all := []string{} + ifaces, err := net.Interfaces() + if err != nil { + return plugin.Error("bind", fmt.Errorf("failed to get interfaces list: %s", err)) + } + for c.Next() { - args := c.RemainingArgs() - if len(args) == 0 { - return plugin.Error("bind", fmt.Errorf("at least one address or interface name is expected")) - } - - ifaces, err := net.Interfaces() + b, err := parse(c) if err != nil { - return plugin.Error("bind", fmt.Errorf("failed to get interfaces list")) + return plugin.Error("bind", err) } - var isIface bool - for _, arg := range args { - isIface = false - for _, iface := range ifaces { - if arg == iface.Name { - isIface = true - addrs, err := iface.Addrs() - if err != nil { - return plugin.Error("bind", fmt.Errorf("failed to get the IP addresses of the interface: %q", arg)) - } - for _, addr := range addrs { - if ipnet, ok := addr.(*net.IPNet); ok { - if ipnet.IP.To4() != nil || (!ipnet.IP.IsLinkLocalMulticast() && !ipnet.IP.IsLinkLocalUnicast()) { - all = append(all, ipnet.IP.String()) - } + ips, err := listIP(b.addrs, ifaces) + if err != nil { + return plugin.Error("bind", err) + } + + except, err := listIP(b.except, ifaces) + if err != nil { + return plugin.Error("bind", err) + } + + for _, ip := range ips { + if !isIn(ip, except) { + all = append(all, ip) + } + } + } + + config.ListenHosts = all + return nil +} + +func parse(c *caddy.Controller) (*bind, error) { + b := &bind{} + b.addrs = c.RemainingArgs() + if len(b.addrs) == 0 { + return nil, errors.New("at least one address or interface name is expected") + } + for c.NextBlock() { + switch c.Val() { + case "except": + b.except = c.RemainingArgs() + if len(b.except) == 0 { + return nil, errors.New("at least one address or interface must be given to except subdirective") + } + default: + return nil, fmt.Errorf("invalid option %q", c.Val()) + } + } + return b, nil +} + +// listIP returns a list of IP addresses from a list of arguments which can be either IP-Address or Interface-Name. +func listIP(args []string, ifaces []net.Interface) ([]string, error) { + all := []string{} + var isIface bool + for _, a := range args { + isIface = false + for _, iface := range ifaces { + if a == iface.Name { + isIface = true + addrs, err := iface.Addrs() + if err != nil { + return nil, fmt.Errorf("failed to get the IP addresses of the interface: %q", a) + } + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok { + if ipnet.IP.To4() != nil || (!ipnet.IP.IsLinkLocalMulticast() && !ipnet.IP.IsLinkLocalUnicast()) { + all = append(all, ipnet.IP.String()) } } } } - if !isIface { - if net.ParseIP(arg) == nil { - return plugin.Error("bind", fmt.Errorf("not a valid IP address or interface name: %q", arg)) - } - all = append(all, arg) + } + if !isIface { + if net.ParseIP(a) == nil { + return nil, fmt.Errorf("not a valid IP address or interface name: %q", a) } + all = append(all, a) } } - config.ListenHosts = all - return nil + return all, nil +} + +// isIn checks if a string array contains an element +func isIn(s string, list []string) bool { + is := false + for _, l := range list { + if s == l { + is = true + } + } + return is } diff --git a/plugin/bind/setup_test.go b/plugin/bind/setup_test.go index b0cf1087b..e8c87b8fe 100644 --- a/plugin/bind/setup_test.go +++ b/plugin/bind/setup_test.go @@ -20,6 +20,7 @@ func TestSetup(t *testing.T) { {`bind ::1 1.2.3.4 ::5 127.9.9.0`, []string{"::1", "1.2.3.4", "::5", "127.9.9.0"}, false}, {`bind ::1 1.2.3.4 ::5 127.9.9.0 noone`, nil, true}, {`bind 1.2.3.4 lo`, []string{"1.2.3.4", "127.0.0.1", "::1"}, false}, + {"bind lo {\nexcept 127.0.0.1\n}\n", []string{"::1"}, false}, } { c := caddy.NewTestController("dns", test.config) err := setup(c)