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",
|
"loadbalance",
|
||||||
"dnssec",
|
"dnssec",
|
||||||
"reverse",
|
"reverse",
|
||||||
|
"hosts",
|
||||||
"kubernetes",
|
"kubernetes",
|
||||||
"file",
|
"file",
|
||||||
"auto",
|
"auto",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
_ "github.com/coredns/coredns/middleware/etcd"
|
_ "github.com/coredns/coredns/middleware/etcd"
|
||||||
_ "github.com/coredns/coredns/middleware/file"
|
_ "github.com/coredns/coredns/middleware/file"
|
||||||
_ "github.com/coredns/coredns/middleware/health"
|
_ "github.com/coredns/coredns/middleware/health"
|
||||||
|
_ "github.com/coredns/coredns/middleware/hosts"
|
||||||
_ "github.com/coredns/coredns/middleware/kubernetes"
|
_ "github.com/coredns/coredns/middleware/kubernetes"
|
||||||
_ "github.com/coredns/coredns/middleware/loadbalance"
|
_ "github.com/coredns/coredns/middleware/loadbalance"
|
||||||
_ "github.com/coredns/coredns/middleware/log"
|
_ "github.com/coredns/coredns/middleware/log"
|
||||||
|
|||||||
@@ -34,13 +34,14 @@
|
|||||||
120:loadbalance:loadbalance
|
120:loadbalance:loadbalance
|
||||||
130:dnssec:dnssec
|
130:dnssec:dnssec
|
||||||
140:reverse:reverse
|
140:reverse:reverse
|
||||||
150:kubernetes:kubernetes
|
150:hosts:hosts
|
||||||
160:file:file
|
160:kubernetes:kubernetes
|
||||||
170:auto:auto
|
170:file:file
|
||||||
180:secondary:secondary
|
180:auto:auto
|
||||||
190:etcd:etcd
|
190:secondary:secondary
|
||||||
200:proxy:proxy
|
200:etcd:etcd
|
||||||
210:whoami:whoami
|
210:proxy:proxy
|
||||||
220:erratic:erratic
|
220:whoami:whoami
|
||||||
|
230:erratic:erratic
|
||||||
500:startup:github.com/mholt/caddy/startupshutdown
|
500:startup:github.com/mholt/caddy/startupshutdown
|
||||||
510:shutdown: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