mirror of
https://github.com/coredns/coredns.git
synced 2025-10-27 08:14:18 -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