mirror of
				https://github.com/coredns/coredns.git
				synced 2025-10-27 16:24:19 -04:00 
			
		
		
		
	middleware/hosts for /etc/hosts parsing (#695)
* add hosts middleware * forgot pointer receiver * add appropriately modified hostsfile tests from golang repo * remove test artifacts, separate hostsfile parsing from caching and opening, remove unused metrics references, move middleware up the chain * refactored the logic for creating records and filtering ip address versions. also got PTR lookups working * Add README.md. Modify config to be more concise. Add zones list to config. Filter PTR responses based on zones list. * add Fallthrough and return correct dns response code otherwise * Simplified Hostsfile to only store hosts in the zones we care about, and by ip version. Added handler tests and improved other tests. * oops, goimports loaded a package from a different repo
This commit is contained in:
		| @@ -26,6 +26,7 @@ var directives = []string{ | ||||
| 	"loadbalance", | ||||
| 	"dnssec", | ||||
| 	"reverse", | ||||
| 	"hosts", | ||||
| 	"kubernetes", | ||||
| 	"file", | ||||
| 	"auto", | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import ( | ||||
| 	_ "github.com/coredns/coredns/middleware/etcd" | ||||
| 	_ "github.com/coredns/coredns/middleware/file" | ||||
| 	_ "github.com/coredns/coredns/middleware/health" | ||||
| 	_ "github.com/coredns/coredns/middleware/hosts" | ||||
| 	_ "github.com/coredns/coredns/middleware/kubernetes" | ||||
| 	_ "github.com/coredns/coredns/middleware/loadbalance" | ||||
| 	_ "github.com/coredns/coredns/middleware/log" | ||||
|   | ||||
| @@ -34,13 +34,14 @@ | ||||
| 120:loadbalance:loadbalance | ||||
| 130:dnssec:dnssec | ||||
| 140:reverse:reverse | ||||
| 150:kubernetes:kubernetes | ||||
| 160:file:file | ||||
| 170:auto:auto | ||||
| 180:secondary:secondary | ||||
| 190:etcd:etcd | ||||
| 200:proxy:proxy | ||||
| 210:whoami:whoami | ||||
| 220:erratic:erratic | ||||
| 150:hosts:hosts | ||||
| 160:kubernetes:kubernetes | ||||
| 170:file:file | ||||
| 180:auto:auto | ||||
| 190:secondary:secondary | ||||
| 200:etcd:etcd | ||||
| 210:proxy:proxy | ||||
| 220:whoami:whoami | ||||
| 230:erratic:erratic | ||||
| 500:startup:github.com/mholt/caddy/startupshutdown | ||||
| 510:shutdown:github.com/mholt/caddy/startupshutdown | ||||
|   | ||||
							
								
								
									
										45
									
								
								middleware/hosts/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								middleware/hosts/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| # hosts | ||||
|  | ||||
| *hosts* enables serving zone data from a /etc/hosts style file. | ||||
|  | ||||
| The hosts middleware is useful for serving zones from a /etc/hosts file. It serves from a preloaded  | ||||
| file that exists on disk. It checks the file for changes and updates the zones accordingly. This | ||||
| middleware only supports A, AAAA, and PTR records. The hosts middleware can be used with readily | ||||
| available hosts files that block access to advertising servers. | ||||
|  | ||||
| ## Syntax | ||||
|  | ||||
| ~~~ | ||||
| hosts FILE [ZONES...] { | ||||
|     fallthrough | ||||
| } | ||||
| ~~~ | ||||
|  | ||||
| * **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 | ||||
| * **ZONES** zones it should be authoritative for. If empty, the zones from the configuration block | ||||
|     are used.  | ||||
| * `fallthrough` If zone matches and no record can be generated, pass request to the next middleware. | ||||
|  | ||||
| ## Examples | ||||
|  | ||||
| Load /etc/hosts file | ||||
|  | ||||
| ~~~ | ||||
| hosts | ||||
| ~~~ | ||||
|  | ||||
| Load example.hosts file | ||||
|  | ||||
| ~~~ | ||||
| hosts example.hosts | ||||
| ~~~ | ||||
|  | ||||
| Load example.hosts file and only serve example.org and example.net from it and fall through to the | ||||
| next middleware if query doesn't match | ||||
|  | ||||
| ~~~ | ||||
| hosts example.hosts example.org example.net { | ||||
|     fallthrough | ||||
| } | ||||
| ~~~ | ||||
							
								
								
									
										116
									
								
								middleware/hosts/hosts.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								middleware/hosts/hosts.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| package hosts | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"net" | ||||
|  | ||||
| 	"golang.org/x/net/context" | ||||
|  | ||||
| 	"github.com/coredns/coredns/middleware" | ||||
| 	"github.com/coredns/coredns/middleware/pkg/dnsutil" | ||||
| 	"github.com/coredns/coredns/request" | ||||
| 	"github.com/miekg/dns" | ||||
| ) | ||||
|  | ||||
| // Hosts is the middleware handler | ||||
| type Hosts struct { | ||||
| 	Next middleware.Handler | ||||
| 	*Hostsfile | ||||
|  | ||||
| 	Fallthrough bool | ||||
| } | ||||
|  | ||||
| // ServeDNS implements the middleware.Handle interface. | ||||
| func (h Hosts) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { | ||||
| 	state := request.Request{W: w, Req: r} | ||||
| 	if state.QClass() != dns.ClassINET { | ||||
| 		return dns.RcodeServerFailure, middleware.Error(h.Name(), errors.New("can only deal with ClassINET")) | ||||
| 	} | ||||
| 	qname := state.Name() | ||||
|  | ||||
| 	answers := []dns.RR{} | ||||
|  | ||||
| 	zone := middleware.Zones(h.Origins).Matches(qname) | ||||
| 	if zone == "" { | ||||
| 		// PTR zones don't need to be specified in Origins | ||||
| 		if state.Type() != "PTR" { | ||||
| 			// If this doesn't match we need to fall through regardless of h.Fallthrough | ||||
| 			return middleware.NextOrFailure(h.Name(), h.Next, ctx, w, r) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	switch state.QType() { | ||||
| 	case dns.TypePTR: | ||||
| 		names := h.LookupStaticAddr(dnsutil.ExtractAddressFromReverse(qname)) | ||||
| 		if len(names) == 0 { | ||||
| 			// If this doesn't match we need to fall through regardless of h.Fallthrough | ||||
| 			return middleware.NextOrFailure(h.Name(), h.Next, ctx, w, r) | ||||
| 		} | ||||
| 		answers = h.ptr(qname, names) | ||||
| 	case dns.TypeA: | ||||
| 		ips := h.LookupStaticHostV4(qname) | ||||
| 		answers = a(qname, ips) | ||||
| 	case dns.TypeAAAA: | ||||
| 		ips := h.LookupStaticHostV6(qname) | ||||
| 		answers = aaaa(qname, ips) | ||||
| 	} | ||||
|  | ||||
| 	if len(answers) == 0 { | ||||
| 		if h.Fallthrough { | ||||
| 			return middleware.NextOrFailure(h.Name(), h.Next, ctx, w, r) | ||||
| 		} | ||||
| 		return dns.RcodeRefused, nil | ||||
| 	} | ||||
|  | ||||
| 	m := new(dns.Msg) | ||||
| 	m.SetReply(r) | ||||
| 	m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true | ||||
| 	m.Answer = answers | ||||
|  | ||||
| 	state.SizeAndDo(m) | ||||
| 	m, _ = state.Scrub(m) | ||||
| 	w.WriteMsg(m) | ||||
| 	return dns.RcodeSuccess, nil | ||||
| } | ||||
|  | ||||
| // Name implements the middleware.Handle interface. | ||||
| func (h Hosts) Name() string { return "hosts" } | ||||
|  | ||||
| // a takes a slice of net.IPs and returns a slice of A RRs. | ||||
| func a(zone string, ips []net.IP) []dns.RR { | ||||
| 	answers := []dns.RR{} | ||||
| 	for _, ip := range ips { | ||||
| 		r := new(dns.A) | ||||
| 		r.Hdr = dns.RR_Header{Name: zone, Rrtype: dns.TypeA, | ||||
| 			Class: dns.ClassINET, Ttl: 3600} | ||||
| 		r.A = ip | ||||
| 		answers = append(answers, r) | ||||
| 	} | ||||
| 	return answers | ||||
| } | ||||
|  | ||||
| // aaaa takes a slice of net.IPs and returns a slice of AAAA RRs. | ||||
| func aaaa(zone string, ips []net.IP) []dns.RR { | ||||
| 	answers := []dns.RR{} | ||||
| 	for _, ip := range ips { | ||||
| 		r := new(dns.AAAA) | ||||
| 		r.Hdr = dns.RR_Header{Name: zone, Rrtype: dns.TypeAAAA, | ||||
| 			Class: dns.ClassINET, Ttl: 3600} | ||||
| 		r.AAAA = ip | ||||
| 		answers = append(answers, r) | ||||
| 	} | ||||
| 	return answers | ||||
| } | ||||
|  | ||||
| // ptr takes a slice of host names and filters out the ones that aren't in Origins, if specified, and returns a slice of PTR RRs. | ||||
| func (h *Hosts) ptr(zone string, names []string) []dns.RR { | ||||
| 	answers := []dns.RR{} | ||||
| 	for _, n := range names { | ||||
| 		r := new(dns.PTR) | ||||
| 		r.Hdr = dns.RR_Header{Name: zone, Rrtype: dns.TypePTR, | ||||
| 			Class: dns.ClassINET, Ttl: 3600} | ||||
| 		r.Ptr = dns.Fqdn(n) | ||||
| 		answers = append(answers, r) | ||||
| 	} | ||||
| 	return answers | ||||
| } | ||||
							
								
								
									
										86
									
								
								middleware/hosts/hosts_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								middleware/hosts/hosts_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| package hosts | ||||
|  | ||||
| import ( | ||||
| 	"sort" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/coredns/coredns/middleware/pkg/dnsrecorder" | ||||
| 	"github.com/coredns/coredns/middleware/test" | ||||
|  | ||||
| 	"github.com/miekg/dns" | ||||
| 	"golang.org/x/net/context" | ||||
| ) | ||||
|  | ||||
| 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)) | ||||
|  | ||||
| 	ctx := context.TODO() | ||||
|  | ||||
| 	for _, tc := range hostsTestCases { | ||||
| 		m := tc.Msg() | ||||
|  | ||||
| 		rec := dnsrecorder.New(&test.ResponseWriter{}) | ||||
| 		_, err := h.ServeDNS(ctx, rec, m) | ||||
| 		if err != nil { | ||||
| 			t.Errorf("Expected no error, got %v\n", err) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		resp := rec.Msg | ||||
| 		sort.Sort(test.RRSet(resp.Answer)) | ||||
| 		sort.Sort(test.RRSet(resp.Ns)) | ||||
| 		sort.Sort(test.RRSet(resp.Extra)) | ||||
|  | ||||
| 		if !test.Header(t, tc, resp) { | ||||
| 			t.Logf("%v\n", resp) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if !test.Section(t, tc, test.Answer, resp.Answer) { | ||||
| 			t.Logf("%v\n", resp) | ||||
| 		} | ||||
| 		if !test.Section(t, tc, test.Ns, resp.Ns) { | ||||
| 			t.Logf("%v\n", resp) | ||||
|  | ||||
| 		} | ||||
| 		if !test.Section(t, tc, test.Extra, resp.Extra) { | ||||
| 			t.Logf("%v\n", resp) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var hostsTestCases = []test.Case{ | ||||
| 	{ | ||||
| 		Qname: "example.org.", Qtype: dns.TypeA, | ||||
| 		Answer: []dns.RR{ | ||||
| 			test.A("example.org. 3600	IN	A 10.0.0.1"), | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		Qname: "localhost.", Qtype: dns.TypeAAAA, | ||||
| 		Answer: []dns.RR{ | ||||
| 			test.AAAA("localhost. 3600	IN	AAAA ::1"), | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		Qname: "1.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR, | ||||
| 		Answer: []dns.RR{ | ||||
| 			test.PTR("1.0.0.10.in-addr.arpa. 3600 PTR example.org."), | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		Qname: "1.0.0.127.in-addr.arpa.", Qtype: dns.TypePTR, | ||||
| 		Answer: []dns.RR{ | ||||
| 			test.PTR("1.0.0.127.in-addr.arpa. 3600 PTR localhost."), | ||||
| 			test.PTR("1.0.0.127.in-addr.arpa. 3600 PTR localhost.domain."), | ||||
| 		}, | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| const hostsExample = ` | ||||
| 127.0.0.1 localhost localhost.domain | ||||
| ::1 localhost localhost.domain | ||||
| 10.0.0.1 example.org` | ||||
							
								
								
									
										193
									
								
								middleware/hosts/hostsfile.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								middleware/hosts/hostsfile.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,193 @@ | ||||
| // Copyright 2009 The Go Authors. All rights reserved. | ||||
| // Use of this source code is governed by a BSD-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| // This file is a modified version of net/hosts.go from the golang repo | ||||
|  | ||||
| package hosts | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"bytes" | ||||
| 	"io" | ||||
| 	"net" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/coredns/coredns/middleware" | ||||
| ) | ||||
|  | ||||
| const cacheMaxAge = 5 * time.Second | ||||
|  | ||||
| func parseLiteralIP(addr string) net.IP { | ||||
| 	if i := strings.Index(addr, "%"); i >= 0 { | ||||
| 		// discard ipv6 zone | ||||
| 		addr = addr[0:i] | ||||
| 	} | ||||
|  | ||||
| 	return net.ParseIP(addr) | ||||
| } | ||||
|  | ||||
| func absDomainName(b string) string { | ||||
| 	return middleware.Name(b).Normalize() | ||||
| } | ||||
|  | ||||
| // Hostsfile contains known host entries. | ||||
| type Hostsfile struct { | ||||
| 	sync.Mutex | ||||
|  | ||||
| 	// list of zones we are authoritive for | ||||
| 	Origins []string | ||||
|  | ||||
| 	// 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. | ||||
| 	// For now the key is converted to lower case for convenience. | ||||
| 	byNameV4 map[string][]net.IP | ||||
| 	byNameV6 map[string][]net.IP | ||||
|  | ||||
| 	// Key for the list of host names must be a literal IP address | ||||
| 	// including IPv6 address with zone identifier. | ||||
| 	// We don't support old-classful IP address notation. | ||||
| 	byAddr map[string][]string | ||||
|  | ||||
| 	expire time.Time | ||||
| 	path   string | ||||
| 	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() { | ||||
| 	now := time.Now() | ||||
|  | ||||
| 	if now.Before(h.expire) && len(h.byAddr) > 0 { | ||||
| 		return | ||||
| 	} | ||||
| 	stat, err := os.Stat(h.path) | ||||
| 	if err == nil && h.mtime.Equal(stat.ModTime()) && h.size == stat.Size() { | ||||
| 		h.expire = now.Add(cacheMaxAge) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var file *os.File | ||||
| 	if file, _ = os.Open(h.path); file == nil { | ||||
| 		return | ||||
| 	} | ||||
| 	defer file.Close() | ||||
|  | ||||
| 	h.Parse(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) | ||||
|  | ||||
| 	scanner := bufio.NewScanner(file) | ||||
| 	for scanner.Scan() { | ||||
| 		line := scanner.Bytes() | ||||
| 		if i := bytes.Index(line, []byte{'#'}); i >= 0 { | ||||
| 			// Discard comments. | ||||
| 			line = line[0:i] | ||||
| 		} | ||||
| 		f := bytes.Fields(line) | ||||
| 		if len(f) < 2 { | ||||
| 			continue | ||||
| 		} | ||||
| 		addr := parseLiteralIP(string(f[0])) | ||||
| 		if addr == nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		ver := ipVersion(string(f[0])) | ||||
| 		for i := 1; i < len(f); i++ { | ||||
| 			name := absDomainName(string(f[i])) | ||||
| 			if middleware.Zones(h.Origins).Matches(name) == "" { | ||||
| 				// name is not in Origins | ||||
| 				continue | ||||
| 			} | ||||
| 			switch ver { | ||||
| 			case 4: | ||||
| 				hsv4[name] = append(hsv4[name], addr) | ||||
| 			case 6: | ||||
| 				hsv6[name] = append(hsv6[name], addr) | ||||
| 			default: | ||||
| 				continue | ||||
| 			} | ||||
| 			is[addr.String()] = append(is[addr.String()], name) | ||||
| 		} | ||||
| 	} | ||||
| 	h.byNameV4 = hsv4 | ||||
| 	h.byNameV6 = hsv6 | ||||
| 	h.byAddr = is | ||||
| } | ||||
|  | ||||
| // ipVersion returns what IP version was used textually | ||||
| func ipVersion(s string) int { | ||||
| 	for i := 0; i < len(s); i++ { | ||||
| 		switch s[i] { | ||||
| 		case '.': | ||||
| 			return 4 | ||||
| 		case ':': | ||||
| 			return 6 | ||||
| 		} | ||||
| 	} | ||||
| 	return 0 | ||||
| } | ||||
|  | ||||
| // LookupStaticHostV4 looks up the IPv4 addresses for the given host from the hosts file. | ||||
| func (h *Hostsfile) LookupStaticHostV4(host string) []net.IP { | ||||
| 	h.Lock() | ||||
| 	defer h.Unlock() | ||||
| 	h.ReadHosts() | ||||
| 	if len(h.byNameV4) != 0 { | ||||
| 		if ips, ok := h.byNameV4[absDomainName(host)]; ok { | ||||
| 			ipsCp := make([]net.IP, len(ips)) | ||||
| 			copy(ipsCp, ips) | ||||
| 			return ipsCp | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // LookupStaticHostV6 looks up the IPv6 addresses for the given host from the hosts file. | ||||
| func (h *Hostsfile) LookupStaticHostV6(host string) []net.IP { | ||||
| 	h.Lock() | ||||
| 	defer h.Unlock() | ||||
| 	h.ReadHosts() | ||||
| 	if len(h.byNameV6) != 0 { | ||||
| 		if ips, ok := h.byNameV6[absDomainName(host)]; ok { | ||||
| 			ipsCp := make([]net.IP, len(ips)) | ||||
| 			copy(ipsCp, ips) | ||||
| 			return ipsCp | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // LookupStaticAddr looks up the hosts for the given address from the hosts file. | ||||
| func (h *Hostsfile) LookupStaticAddr(addr string) []string { | ||||
| 	h.Lock() | ||||
| 	defer h.Unlock() | ||||
| 	h.ReadHosts() | ||||
| 	addr = parseLiteralIP(addr).String() | ||||
| 	if addr == "" { | ||||
| 		return nil | ||||
| 	} | ||||
| 	if len(h.byAddr) != 0 { | ||||
| 		if hosts, ok := h.byAddr[addr]; ok { | ||||
| 			hostsCp := make([]string, len(hosts)) | ||||
| 			copy(hostsCp, hosts) | ||||
| 			return hostsCp | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										239
									
								
								middleware/hosts/hostsfile_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								middleware/hosts/hostsfile_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,239 @@ | ||||
| // Copyright 2009 The Go Authors. All rights reserved. | ||||
| // Use of this source code is governed by a BSD-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| package hosts | ||||
|  | ||||
| import ( | ||||
| 	"net" | ||||
| 	"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)) | ||||
| 	return h | ||||
| } | ||||
|  | ||||
| type staticHostEntry struct { | ||||
| 	in string | ||||
| 	v4 []string | ||||
| 	v6 []string | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	hosts = `255.255.255.255	broadcasthost | ||||
| 	127.0.0.2	odin | ||||
| 	127.0.0.3	odin  # inline comment  | ||||
| 	::2             odin | ||||
| 	127.1.1.1	thor | ||||
| 	# aliases | ||||
| 	127.1.1.2	ullr ullrhost | ||||
| 	fe80::1%lo0	localhost | ||||
| 	# Bogus entries that must be ignored. | ||||
| 	123.123.123	loki | ||||
| 	321.321.321.321` | ||||
| 	singlelinehosts = `127.0.0.2  odin` | ||||
| 	ipv4hosts       = `# See https://tools.ietf.org/html/rfc1123. | ||||
| 	# | ||||
| 	# The literal IPv4 address parser in the net package is a relaxed | ||||
| 	# one. It may accept a literal IPv4 address in dotted-decimal notation | ||||
| 	# with leading zeros such as "001.2.003.4". | ||||
|  | ||||
| 	# internet address and host name | ||||
| 	127.0.0.1	localhost	# inline comment separated by tab | ||||
| 	127.000.000.002	localhost       # inline comment separated by space | ||||
|  | ||||
| 	# internet address, host name and aliases | ||||
| 	127.000.000.003	localhost	localhost.localdomain` | ||||
| 	ipv6hosts = `# See https://tools.ietf.org/html/rfc5952, https://tools.ietf.org/html/rfc4007. | ||||
|  | ||||
| 	# internet address and host name | ||||
| 	::1						localhost	# inline comment separated by tab | ||||
| 	fe80:0000:0000:0000:0000:0000:0000:0001		localhost       # inline comment separated by space | ||||
|  | ||||
| 	# internet address with zone identifier and host name | ||||
| 	fe80:0000:0000:0000:0000:0000:0000:0002%lo0	localhost | ||||
|  | ||||
| 	# internet address, host name and aliases | ||||
| 	fe80::3%lo0					localhost	localhost.localdomain` | ||||
| 	casehosts = `127.0.0.1	PreserveMe	PreserveMe.local | ||||
| 		::1		PreserveMe	PreserveMe.local` | ||||
| ) | ||||
|  | ||||
| var lookupStaticHostTests = []struct { | ||||
| 	file string | ||||
| 	ents []staticHostEntry | ||||
| }{ | ||||
| 	{ | ||||
| 		hosts, | ||||
| 		[]staticHostEntry{ | ||||
| 			{"odin", []string{"127.0.0.2", "127.0.0.3"}, []string{"::2"}}, | ||||
| 			{"thor", []string{"127.1.1.1"}, []string{}}, | ||||
| 			{"ullr", []string{"127.1.1.2"}, []string{}}, | ||||
| 			{"ullrhost", []string{"127.1.1.2"}, []string{}}, | ||||
| 			{"localhost", []string{}, []string{"fe80::1"}}, | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		singlelinehosts, // see golang.org/issue/6646 | ||||
| 		[]staticHostEntry{ | ||||
| 			{"odin", []string{"127.0.0.2"}, []string{}}, | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		ipv4hosts, | ||||
| 		[]staticHostEntry{ | ||||
| 			{"localhost", []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, []string{}}, | ||||
| 			{"localhost.localdomain", []string{"127.0.0.3"}, []string{}}, | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		ipv6hosts, | ||||
| 		[]staticHostEntry{ | ||||
| 			{"localhost", []string{}, []string{"::1", "fe80::1", "fe80::2", "fe80::3"}}, | ||||
| 			{"localhost.localdomain", []string{}, []string{"fe80::3"}}, | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		casehosts, | ||||
| 		[]staticHostEntry{ | ||||
| 			{"PreserveMe", []string{"127.0.0.1"}, []string{"::1"}}, | ||||
| 			{"PreserveMe.local", []string{"127.0.0.1"}, []string{"::1"}}, | ||||
| 		}, | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| func TestLookupStaticHost(t *testing.T) { | ||||
|  | ||||
| 	for _, tt := range lookupStaticHostTests { | ||||
| 		h := testHostsfile(tt.file) | ||||
| 		for _, ent := range tt.ents { | ||||
| 			testStaticHost(t, ent, h) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func testStaticHost(t *testing.T, ent staticHostEntry, h *Hostsfile) { | ||||
| 	ins := []string{ent.in, absDomainName(ent.in), strings.ToLower(ent.in), strings.ToUpper(ent.in)} | ||||
| 	for k, in := range ins { | ||||
| 		addrsV4 := h.LookupStaticHostV4(in) | ||||
| 		if len(addrsV4) != len(ent.v4) { | ||||
| 			t.Fatalf("%d, lookupStaticHostV4(%s) = %v; want %v", k, in, addrsV4, ent.v4) | ||||
| 		} | ||||
| 		for i, v4 := range addrsV4 { | ||||
| 			if v4.String() != ent.v4[i] { | ||||
| 				t.Fatalf("%d, lookupStaticHostV4(%s) = %v; want %v", k, in, addrsV4, ent.v4) | ||||
| 			} | ||||
| 		} | ||||
| 		addrsV6 := h.LookupStaticHostV6(in) | ||||
| 		if len(addrsV6) != len(ent.v6) { | ||||
| 			t.Fatalf("%d, lookupStaticHostV6(%s) = %v; want %v", k, in, addrsV6, ent.v6) | ||||
| 		} | ||||
| 		for i, v6 := range addrsV6 { | ||||
| 			if v6.String() != ent.v6[i] { | ||||
| 				t.Fatalf("%d, lookupStaticHostV6(%s) = %v; want %v", k, in, addrsV6, ent.v6) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type staticIPEntry struct { | ||||
| 	in  string | ||||
| 	out []string | ||||
| } | ||||
|  | ||||
| var lookupStaticAddrTests = []struct { | ||||
| 	file string | ||||
| 	ents []staticIPEntry | ||||
| }{ | ||||
| 	{ | ||||
| 		hosts, | ||||
| 		[]staticIPEntry{ | ||||
| 			{"255.255.255.255", []string{"broadcasthost"}}, | ||||
| 			{"127.0.0.2", []string{"odin"}}, | ||||
| 			{"127.0.0.3", []string{"odin"}}, | ||||
| 			{"::2", []string{"odin"}}, | ||||
| 			{"127.1.1.1", []string{"thor"}}, | ||||
| 			{"127.1.1.2", []string{"ullr", "ullrhost"}}, | ||||
| 			{"fe80::1", []string{"localhost"}}, | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		singlelinehosts, // see golang.org/issue/6646 | ||||
| 		[]staticIPEntry{ | ||||
| 			{"127.0.0.2", []string{"odin"}}, | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		ipv4hosts, // see golang.org/issue/8996 | ||||
| 		[]staticIPEntry{ | ||||
| 			{"127.0.0.1", []string{"localhost"}}, | ||||
| 			{"127.0.0.2", []string{"localhost"}}, | ||||
| 			{"127.0.0.3", []string{"localhost", "localhost.localdomain"}}, | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		ipv6hosts, // see golang.org/issue/8996 | ||||
| 		[]staticIPEntry{ | ||||
| 			{"::1", []string{"localhost"}}, | ||||
| 			{"fe80::1", []string{"localhost"}}, | ||||
| 			{"fe80::2", []string{"localhost"}}, | ||||
| 			{"fe80::3", []string{"localhost", "localhost.localdomain"}}, | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		casehosts, // see golang.org/issue/12806 | ||||
| 		[]staticIPEntry{ | ||||
| 			{"127.0.0.1", []string{"PreserveMe", "PreserveMe.local"}}, | ||||
| 			{"::1", []string{"PreserveMe", "PreserveMe.local"}}, | ||||
| 		}, | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| func TestLookupStaticAddr(t *testing.T) { | ||||
| 	for _, tt := range lookupStaticAddrTests { | ||||
| 		h := testHostsfile(tt.file) | ||||
| 		for _, ent := range tt.ents { | ||||
| 			testStaticAddr(t, ent, h) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func testStaticAddr(t *testing.T, ent staticIPEntry, h *Hostsfile) { | ||||
| 	hosts := h.LookupStaticAddr(ent.in) | ||||
| 	for i := range ent.out { | ||||
| 		ent.out[i] = absDomainName(ent.out[i]) | ||||
| 	} | ||||
| 	if !reflect.DeepEqual(hosts, ent.out) { | ||||
| 		t.Errorf("%s, lookupStaticAddr(%s) = %v; want %v", h.path, ent.in, hosts, h) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestHostCacheModification(t *testing.T) { | ||||
| 	// Ensure that programs can't modify the internals of the host cache. | ||||
| 	// See https://github.com/golang/go/issues/14212. | ||||
|  | ||||
| 	h := testHostsfile(ipv4hosts) | ||||
| 	ent := staticHostEntry{"localhost", []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, []string{}} | ||||
| 	testStaticHost(t, ent, h) | ||||
| 	// Modify the addresses return by lookupStaticHost. | ||||
| 	addrs := h.LookupStaticHostV6(ent.in) | ||||
| 	for i := range addrs { | ||||
| 		addrs[i] = net.IPv4zero | ||||
| 	} | ||||
| 	testStaticHost(t, ent, h) | ||||
|  | ||||
| 	h = testHostsfile(ipv6hosts) | ||||
| 	entip := staticIPEntry{"::1", []string{"localhost"}} | ||||
| 	testStaticAddr(t, entip, h) | ||||
| 	// Modify the hosts return by lookupStaticAddr. | ||||
| 	hosts := h.LookupStaticAddr(entip.in) | ||||
| 	for i := range hosts { | ||||
| 		hosts[i] += "junk" | ||||
| 	} | ||||
| 	testStaticAddr(t, entip, h) | ||||
| } | ||||
							
								
								
									
										88
									
								
								middleware/hosts/setup.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								middleware/hosts/setup.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| package hosts | ||||
|  | ||||
| import ( | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"path" | ||||
|  | ||||
| 	"github.com/coredns/coredns/core/dnsserver" | ||||
| 	"github.com/coredns/coredns/middleware" | ||||
|  | ||||
| 	"github.com/mholt/caddy" | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| 	caddy.RegisterPlugin("hosts", caddy.Plugin{ | ||||
| 		ServerType: "dns", | ||||
| 		Action:     setup, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func setup(c *caddy.Controller) error { | ||||
| 	h, err := hostsParse(c) | ||||
| 	if err != nil { | ||||
| 		return middleware.Error("hosts", err) | ||||
| 	} | ||||
|  | ||||
| 	dnsserver.GetConfig(c).AddMiddleware(func(next middleware.Handler) middleware.Handler { | ||||
| 		h.Next = next | ||||
| 		return h | ||||
| 	}) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func hostsParse(c *caddy.Controller) (Hosts, error) { | ||||
| 	var h = Hosts{ | ||||
| 		Hostsfile: &Hostsfile{path: "/etc/hosts"}, | ||||
| 	} | ||||
| 	defer h.ReadHosts() | ||||
|  | ||||
| 	config := dnsserver.GetConfig(c) | ||||
|  | ||||
| 	for c.Next() { | ||||
| 		if c.Val() == "hosts" { // hosts [FILE] [ZONES...] | ||||
| 			args := c.RemainingArgs() | ||||
| 			if len(args) >= 1 { | ||||
| 				h.path = args[0] | ||||
| 				args = args[1:] | ||||
|  | ||||
| 				if !path.IsAbs(h.path) && config.Root != "" { | ||||
| 					h.path = path.Join(config.Root, h.path) | ||||
| 				} | ||||
| 				_, err := os.Stat(h.path) | ||||
| 				if err != nil { | ||||
| 					if os.IsNotExist(err) { | ||||
| 						log.Printf("[WARNING] File does not exist: %s", h.path) | ||||
| 					} else { | ||||
| 						return h, c.Errf("unable to access hosts file '%s': %v", h.path, err) | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			origins := make([]string, len(c.ServerBlockKeys)) | ||||
| 			copy(origins, c.ServerBlockKeys) | ||||
| 			if len(args) > 0 { | ||||
| 				origins = args | ||||
| 			} | ||||
|  | ||||
| 			for i := range origins { | ||||
| 				origins[i] = middleware.Host(origins[i]).Normalize() | ||||
| 			} | ||||
| 			h.Origins = origins | ||||
|  | ||||
| 			for c.NextBlock() { | ||||
| 				switch c.Val() { | ||||
| 				case "fallthrough": | ||||
| 					args := c.RemainingArgs() | ||||
| 					if len(args) == 0 { | ||||
| 						h.Fallthrough = true | ||||
| 						continue | ||||
| 					} | ||||
| 					return h, c.ArgErr() | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return h, nil | ||||
| } | ||||
							
								
								
									
										86
									
								
								middleware/hosts/setup_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								middleware/hosts/setup_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| package hosts | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/mholt/caddy" | ||||
| ) | ||||
|  | ||||
| func TestHostsParse(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		inputFileRules      string | ||||
| 		shouldErr           bool | ||||
| 		expectedPath        string | ||||
| 		expectedOrigins     []string | ||||
| 		expectedFallthrough bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			`hosts | ||||
| `, | ||||
| 			false, "/etc/hosts", nil, false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			`hosts /tmp`, | ||||
| 			false, "/tmp", nil, false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			`hosts /etc/hosts miek.nl.`, | ||||
| 			false, "/etc/hosts", []string{"miek.nl."}, false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			`hosts /etc/hosts miek.nl. pun.gent.`, | ||||
| 			false, "/etc/hosts", []string{"miek.nl.", "pun.gent."}, false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			`hosts { | ||||
| 				fallthrough | ||||
| 			}`, | ||||
| 			false, "/etc/hosts", nil, true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			`hosts /tmp { | ||||
| 				fallthrough | ||||
| 			}`, | ||||
| 			false, "/tmp", nil, true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			`hosts /etc/hosts miek.nl. { | ||||
| 				fallthrough | ||||
| 			}`, | ||||
| 			false, "/etc/hosts", []string{"miek.nl."}, true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			`hosts /etc/hosts miek.nl. pun.gent. { | ||||
| 				fallthrough | ||||
| 			}`, | ||||
| 			false, "/etc/hosts", []string{"miek.nl.", "pun.gent."}, true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range tests { | ||||
| 		c := caddy.NewTestController("dns", test.inputFileRules) | ||||
| 		h, err := hostsParse(c) | ||||
|  | ||||
| 		if err == nil && test.shouldErr { | ||||
| 			t.Fatalf("Test %d expected errors, but got no error", i) | ||||
| 		} else if err != nil && !test.shouldErr { | ||||
| 			t.Fatalf("Test %d expected no errors, but got '%v'", i, err) | ||||
| 		} else if !test.shouldErr { | ||||
| 			if h.path != test.expectedPath { | ||||
| 				t.Fatalf("Test %d expected %v, got %v", i, test.expectedPath, h.path) | ||||
| 			} | ||||
| 		} else { | ||||
| 			if h.Fallthrough != test.expectedFallthrough { | ||||
| 				t.Fatalf("Test %d expected fallthrough of %v, got %v", i, test.expectedFallthrough, h.Fallthrough) | ||||
| 			} | ||||
| 			if len(h.Origins) != len(test.expectedOrigins) { | ||||
| 				t.Fatalf("Test %d expected %v, got %v", i, test.expectedOrigins, h.Origins) | ||||
| 			} | ||||
| 			for j, name := range test.expectedOrigins { | ||||
| 				if h.Origins[j] != name { | ||||
| 					t.Fatalf("Test %d expected %v for %d th zone, got %v", i, name, j, h.Origins[j]) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user