diff options
-rw-r--r-- | plugin/hosts/README.md | 3 | ||||
-rw-r--r-- | plugin/hosts/hosts_test.go | 5 | ||||
-rw-r--r-- | plugin/hosts/hostsfile.go | 151 | ||||
-rw-r--r-- | plugin/hosts/hostsfile_test.go | 5 | ||||
-rw-r--r-- | plugin/hosts/setup.go | 37 | ||||
-rw-r--r-- | plugin/hosts/setup_test.go | 2 |
6 files changed, 122 insertions, 81 deletions
diff --git a/plugin/hosts/README.md b/plugin/hosts/README.md index e792ff207..0476e63bb 100644 --- a/plugin/hosts/README.md +++ b/plugin/hosts/README.md @@ -17,7 +17,8 @@ hosts [FILE [ZONES...]] { ~~~ * **FILE** the hosts file to read and parse. If the path is relative the path from the *root* - directive will be prepended to it. Defaults to /etc/hosts if omitted + directive will be prepended to it. Defaults to /etc/hosts if omitted. We scan the file for changes + every 5 seconds. * **ZONES** zones it should be authoritative for. If empty, the zones from the configuration block are used. * **INLINE** the hosts file contents inlined in Corefile. If there are any lines before fallthrough diff --git a/plugin/hosts/hosts_test.go b/plugin/hosts/hosts_test.go index 3c9405562..6d5f7955f 100644 --- a/plugin/hosts/hosts_test.go +++ b/plugin/hosts/hosts_test.go @@ -3,7 +3,6 @@ package hosts import ( "strings" "testing" - "time" "github.com/coredns/coredns/plugin/pkg/dnstest" "github.com/coredns/coredns/plugin/test" @@ -13,8 +12,8 @@ import ( ) func TestLookupA(t *testing.T) { - h := Hosts{Next: test.ErrorHandler(), Hostsfile: &Hostsfile{expire: time.Now().Add(1 * time.Hour), Origins: []string{"."}}} - h.Parse(strings.NewReader(hostsExample)) + h := Hosts{Next: test.ErrorHandler(), Hostsfile: &Hostsfile{Origins: []string{"."}}} + h.parseReader(strings.NewReader(hostsExample)) ctx := context.TODO() diff --git a/plugin/hosts/hostsfile.go b/plugin/hosts/hostsfile.go index dbbc283b2..4b912b17c 100644 --- a/plugin/hosts/hostsfile.go +++ b/plugin/hosts/hostsfile.go @@ -19,8 +19,6 @@ import ( "github.com/coredns/coredns/plugin" ) -const cacheMaxAge = 5 * time.Second - func parseLiteralIP(addr string) net.IP { if i := strings.Index(addr, "%"); i >= 0 { // discard ipv6 zone @@ -34,13 +32,7 @@ func absDomainName(b string) string { return plugin.Name(b).Normalize() } -// Hostsfile contains known host entries. -type Hostsfile struct { - sync.RWMutex - - // list of zones we are authoritive for - Origins []string - +type hostsMap struct { // Key for the list of literal IP addresses must be a host // name. It would be part of DNS labels, a FQDN or an absolute // FQDN. @@ -52,71 +44,80 @@ type Hostsfile struct { // including IPv6 address with zone identifier. // We don't support old-classful IP address notation. byAddr map[string][]string +} - // inline saves the hosts file is inlined in Corefile - // We need a copy here as we want to use inline to override - // the default /etc/hosts - inline []string - - expire time.Time - path string - mtime time.Time - size int64 +func newHostsMap() *hostsMap { + return &hostsMap{ + byNameV4: make(map[string][]net.IP), + byNameV6: make(map[string][]net.IP), + byAddr: make(map[string][]string), + } } -// ReadHosts determines if the cached data needs to be updated based on the size and modification time of the hostsfile. -func (h *Hostsfile) ReadHosts() { - now := time.Now() +// Hostsfile contains known host entries. +type Hostsfile struct { + sync.RWMutex - // Not deferring h.RUnlock() because we may need to remove the read lock and aquire a write lock - h.RLock() - if now.Before(h.expire) && len(h.byAddr) > 0 { - h.RUnlock() - return - } - stat, err := os.Stat(h.path) - if err == nil && h.mtime.Equal(stat.ModTime()) && h.size == stat.Size() { - h.RUnlock() - h.Lock() - h.expire = now.Add(cacheMaxAge) - h.Unlock() + // list of zones we are authoritive for + Origins []string + + // hosts maps for lookups + hmap *hostsMap + + // inline saves the hosts file that is inlined in a Corefile. + // We need a copy here as we want to use it to initialize the maps for parse. + inline *hostsMap + + // path to the hosts file + path string + + // mtime and size are only read and modified by a single goroutine + mtime time.Time + size int64 +} + +// readHosts determines if the cached data needs to be updated based on the size and modification time of the hostsfile. +func (h *Hostsfile) readHosts() { + file, err := os.Open(h.path) + if err != nil { + // We already log a warning if the file doesn't exist or can't be opened on setup. No need to return the error here. return } + defer file.Close() - var file *os.File - if file, _ = os.Open(h.path); file == nil { - // If this is the first time then we will try to parse inline - if len(h.byAddr) == 0 && len(h.inline) > 0 { - h.Parse(nil) - } - h.RUnlock() + stat, err := file.Stat() + if err == nil && h.mtime.Equal(stat.ModTime()) && h.size == stat.Size() { return } - defer file.Close() - h.RUnlock() h.Lock() defer h.Unlock() - h.Parse(file) + h.parseReader(file) // Update the data cache. - h.expire = now.Add(cacheMaxAge) h.mtime = stat.ModTime() h.size = stat.Size() } -// Parse reads the hostsfile and populates the byName and byAddr maps. -func (h *Hostsfile) Parse(file io.Reader) { - hsv4 := make(map[string][]net.IP) - hsv6 := make(map[string][]net.IP) - is := make(map[string][]string) - - var readers []io.Reader - if file != nil { - readers = append(readers, file) +func (h *Hostsfile) initInline(inline []string) { + if len(inline) == 0 { + return } - readers = append(readers, strings.NewReader(strings.Join(h.inline, "\n"))) - scanner := bufio.NewScanner(io.MultiReader(readers...)) + + hmap := newHostsMap() + h.inline = h.parse(strings.NewReader(strings.Join(inline, "\n")), hmap) + *h.hmap = *h.inline +} + +func (h *Hostsfile) parseReader(r io.Reader) { + h.hmap = h.parse(r, h.inline) +} + +// Parse reads the hostsfile and populates the byName and byAddr maps. +func (h *Hostsfile) parse(r io.Reader, override *hostsMap) *hostsMap { + hmap := newHostsMap() + + scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Bytes() if i := bytes.Index(line, []byte{'#'}); i >= 0 { @@ -140,18 +141,31 @@ func (h *Hostsfile) Parse(file io.Reader) { } switch ver { case 4: - hsv4[name] = append(hsv4[name], addr) + hmap.byNameV4[name] = append(hmap.byNameV4[name], addr) case 6: - hsv6[name] = append(hsv6[name], addr) + hmap.byNameV6[name] = append(hmap.byNameV6[name], addr) default: continue } - is[addr.String()] = append(is[addr.String()], name) + hmap.byAddr[addr.String()] = append(hmap.byAddr[addr.String()], name) } } - h.byNameV4 = hsv4 - h.byNameV6 = hsv6 - h.byAddr = is + + if override == nil { + return hmap + } + + for name := range override.byNameV4 { + hmap.byNameV4[name] = append(hmap.byNameV4[name], override.byNameV4[name]...) + } + for name := range override.byNameV4 { + hmap.byNameV6[name] = append(hmap.byNameV6[name], override.byNameV6[name]...) + } + for addr := range override.byAddr { + hmap.byAddr[addr] = append(hmap.byAddr[addr], override.byAddr[addr]...) + } + + return hmap } // ipVersion returns what IP version was used textually @@ -169,11 +183,10 @@ func ipVersion(s string) int { // LookupStaticHostV4 looks up the IPv4 addresses for the given host from the hosts file. func (h *Hostsfile) LookupStaticHostV4(host string) []net.IP { - h.ReadHosts() h.RLock() defer h.RUnlock() - if len(h.byNameV4) != 0 { - if ips, ok := h.byNameV4[absDomainName(host)]; ok { + if len(h.hmap.byNameV4) != 0 { + if ips, ok := h.hmap.byNameV4[absDomainName(host)]; ok { ipsCp := make([]net.IP, len(ips)) copy(ipsCp, ips) return ipsCp @@ -184,11 +197,10 @@ func (h *Hostsfile) LookupStaticHostV4(host string) []net.IP { // LookupStaticHostV6 looks up the IPv6 addresses for the given host from the hosts file. func (h *Hostsfile) LookupStaticHostV6(host string) []net.IP { - h.ReadHosts() h.RLock() defer h.RUnlock() - if len(h.byNameV6) != 0 { - if ips, ok := h.byNameV6[absDomainName(host)]; ok { + if len(h.hmap.byNameV6) != 0 { + if ips, ok := h.hmap.byNameV6[absDomainName(host)]; ok { ipsCp := make([]net.IP, len(ips)) copy(ipsCp, ips) return ipsCp @@ -199,15 +211,14 @@ func (h *Hostsfile) LookupStaticHostV6(host string) []net.IP { // LookupStaticAddr looks up the hosts for the given address from the hosts file. func (h *Hostsfile) LookupStaticAddr(addr string) []string { - h.ReadHosts() h.RLock() defer h.RUnlock() addr = parseLiteralIP(addr).String() if addr == "" { return nil } - if len(h.byAddr) != 0 { - if hosts, ok := h.byAddr[addr]; ok { + if len(h.hmap.byAddr) != 0 { + if hosts, ok := h.hmap.byAddr[addr]; ok { hostsCp := make([]string, len(hosts)) copy(hostsCp, hosts) return hostsCp diff --git a/plugin/hosts/hostsfile_test.go b/plugin/hosts/hostsfile_test.go index 65841fa42..cb923ab60 100644 --- a/plugin/hosts/hostsfile_test.go +++ b/plugin/hosts/hostsfile_test.go @@ -9,12 +9,11 @@ import ( "reflect" "strings" "testing" - "time" ) func testHostsfile(file string) *Hostsfile { - h := &Hostsfile{expire: time.Now().Add(1 * time.Hour), Origins: []string{"."}} - h.Parse(strings.NewReader(file)) + h := &Hostsfile{Origins: []string{"."}} + h.parseReader(strings.NewReader(file)) return h } diff --git a/plugin/hosts/setup.go b/plugin/hosts/setup.go index 48beb0dcb..1b281b9a3 100644 --- a/plugin/hosts/setup.go +++ b/plugin/hosts/setup.go @@ -5,6 +5,7 @@ import ( "os" "path" "strings" + "time" "github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/plugin" @@ -25,6 +26,30 @@ func setup(c *caddy.Controller) error { return plugin.Error("hosts", err) } + parseChan := make(chan bool) + + c.OnStartup(func() error { + h.readHosts() + + go func() { + ticker := time.NewTicker(5 * time.Second) + for { + select { + case <-parseChan: + return + case <-ticker.C: + h.readHosts() + } + } + }() + return nil + }) + + c.OnShutdown(func() error { + close(parseChan) + return nil + }) + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { h.Next = next return h @@ -35,12 +60,15 @@ func setup(c *caddy.Controller) error { func hostsParse(c *caddy.Controller) (Hosts, error) { var h = Hosts{ - Hostsfile: &Hostsfile{path: "/etc/hosts"}, + Hostsfile: &Hostsfile{ + path: "/etc/hosts", + hmap: newHostsMap(), + }, } - defer h.ReadHosts() config := dnsserver.GetConfig(c) + inline := []string{} for c.Next() { args := c.RemainingArgs() if len(args) >= 1 { @@ -86,12 +114,15 @@ func hostsParse(c *caddy.Controller) (Hosts, error) { default: if !h.Fallthrough { line := strings.Join(append([]string{c.Val()}, c.RemainingArgs()...), " ") - h.inline = append(h.inline, line) + inline = append(inline, line) continue } return h, c.Errf("unknown property '%s'", c.Val()) } } } + + h.initInline(inline) + return h, nil } diff --git a/plugin/hosts/setup_test.go b/plugin/hosts/setup_test.go index fefed6c72..b401d58dc 100644 --- a/plugin/hosts/setup_test.go +++ b/plugin/hosts/setup_test.go @@ -141,7 +141,7 @@ func TestHostsInlineParse(t *testing.T) { t.Fatalf("Test %d expected fallthrough of %v, got %v", i, test.expectedFallthrough, h.Fallthrough) } for k, expectedVal := range test.expectedbyAddr { - if val, ok := h.byAddr[k]; !ok { + if val, ok := h.hmap.byAddr[k]; !ok { t.Fatalf("Test %d expected %v, got no entry", i, k) } else { if len(expectedVal) != len(val) { |