diff --git a/plugin/hosts/README.md b/plugin/hosts/README.md index dde6259a6..ea053b364 100644 --- a/plugin/hosts/README.md +++ b/plugin/hosts/README.md @@ -37,6 +37,28 @@ Examples: fdfc:a744:27b5:3b0e::1 example.com example ~~~ +### Wildcard records + +Owner names may use a `*` as the leftmost label to match one additional label below +that name. This follows the same wildcard semantics as the *file* plugin. + +Examples: + +~~~ +192.168.1.10 *.example.com +192.168.1.11 a.example.com +192.168.1.12 b.example.com +~~~ + +With the entries above: + +* `a.example.com` and `b.example.com` resolve to their explicit addresses. +* `apps.example.com` resolves to `192.168.1.10`. +* `example.com` does not match the wildcard (the zone apex is excluded). +* `deep.apps.example.com` does not match `*.example.com` (only one label is matched). + +Wildcard entries do not generate PTR records. + ### PTR records PTR records for reverse lookups are generated automatically by CoreDNS (based on the hosts file @@ -120,6 +142,20 @@ example.hosts example.org { } ~~~ +Resolve all single-label subdomains of `example.com` to one address, with explicit +exceptions, and fall through for everything else under `example.com`. + +~~~ +. { + hosts example.hosts example.com { + 192.168.1.10 *.example.com + 192.168.1.11 www.example.com + fallthrough example.com + } + forward . 8.8.8.8 +} +~~~ + ## See also The form of the entries in the `/etc/hosts` file are based on IETF [RFC 952](https://tools.ietf.org/html/rfc952) which was updated by IETF [RFC 1123](https://tools.ietf.org/html/rfc1123). diff --git a/plugin/hosts/hosts_test.go b/plugin/hosts/hosts_test.go index ccde52583..c5802919a 100644 --- a/plugin/hosts/hosts_test.go +++ b/plugin/hosts/hosts_test.go @@ -136,6 +136,18 @@ var hostsTestCases = []test.Case{ Qname: "fallthrough-example.org.", Qtype: dns.TypeAAAA, Answer: []dns.RR{}, Rcode: dns.RcodeSuccess, }, + { + Qname: "apps.example.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("apps.example.com. 3600 IN A 5.6.7.8"), + }, + }, + { + Qname: "aa.example.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("aa.example.com. 3600 IN A 1.2.3.4"), + }, + }, } const hostsExample = ` @@ -144,6 +156,8 @@ const hostsExample = ` 10.0.0.1 example.org ::FFFF:10.0.0.2 example.com 10.0.0.3 fallthrough-example.org +1.2.3.4 aa.example.com +5.6.7.8 *.apps.example.com reload 5s timeout 3600 ` diff --git a/plugin/hosts/hostsfile.go b/plugin/hosts/hostsfile.go index 84e451029..fb41b05d6 100644 --- a/plugin/hosts/hostsfile.go +++ b/plugin/hosts/hostsfile.go @@ -55,6 +55,10 @@ type Map struct { name4 map[string][]net.IP name6 map[string][]net.IP + // Wildcard owner names (e.g. *.example.com.) map to IP addresses. + wildName4 map[string][]net.IP + wildName6 map[string][]net.IP + // Key for the list of host names must be a literal IP address // including IPv6 address without zone identifier. // We don't support old-classful IP address notation. @@ -63,9 +67,11 @@ type Map struct { func newMap() *Map { return &Map{ - name4: make(map[string][]net.IP), - name6: make(map[string][]net.IP), - addr: make(map[string][]string), + name4: make(map[string][]net.IP), + name6: make(map[string][]net.IP), + wildName4: make(map[string][]net.IP), + wildName6: make(map[string][]net.IP), + addr: make(map[string][]string), } } @@ -78,6 +84,12 @@ func (h *Map) Len() int { for _, v6 := range h.name6 { l += len(v6) } + for _, v4 := range h.wildName4 { + l += len(v4) + } + for _, v6 := range h.wildName6 { + l += len(v6) + } for _, a := range h.addr { l += len(a) } @@ -184,6 +196,15 @@ func (h *Hostsfile) parse(r io.Reader) *Map { // name is not in Origins continue } + if isWildcardName(name) { + switch family { + case 1: + hmap.wildName4[name] = append(hmap.wildName4[name], addr) + case 2: + hmap.wildName6[name] = append(hmap.wildName6[name], addr) + } + continue + } switch family { case 1: hmap.name4[name] = append(hmap.name4[name], addr) @@ -202,38 +223,41 @@ func (h *Hostsfile) parse(r io.Reader) *Map { return hmap } -// lookupStaticHost looks up the IP addresses for the given host from the hosts file. -func (h *Hostsfile) lookupStaticHost(m map[string][]net.IP, host string) []net.IP { +func (h *Hostsfile) lookupStaticHostLocked(m, wild map[string][]net.IP, host string) []net.IP { + if ips, ok := m[host]; ok { + ipsCp := make([]net.IP, len(ips)) + copy(ipsCp, ips) + return ipsCp + } + if pattern := replaceWithAsteriskLabel(host); pattern != "" { + if ips, ok := wild[pattern]; ok { + ipsCp := make([]net.IP, len(ips)) + copy(ipsCp, ips) + return ipsCp + } + } + return nil +} + +func (h *Hostsfile) lookupStaticHostFamily(host string, m, wild, inlineM, inlineWild map[string][]net.IP) []net.IP { + host = strings.ToLower(host) + h.RLock() defer h.RUnlock() - if len(m) == 0 { - return nil - } - - ips, ok := m[host] - if !ok { - return nil - } - ipsCp := make([]net.IP, len(ips)) - copy(ipsCp, ips) - return ipsCp + ip1 := h.lookupStaticHostLocked(m, wild, host) + ip2 := h.lookupStaticHostLocked(inlineM, inlineWild, host) + return append(ip1, ip2...) } // LookupStaticHostV4 looks up the IPv4 addresses for the given host from the hosts file. func (h *Hostsfile) LookupStaticHostV4(host string) []net.IP { - host = strings.ToLower(host) - ip1 := h.lookupStaticHost(h.hmap.name4, host) - ip2 := h.lookupStaticHost(h.inline.name4, host) - return append(ip1, ip2...) + return h.lookupStaticHostFamily(host, h.hmap.name4, h.hmap.wildName4, h.inline.name4, h.inline.wildName4) } // LookupStaticHostV6 looks up the IPv6 addresses for the given host from the hosts file. func (h *Hostsfile) LookupStaticHostV6(host string) []net.IP { - host = strings.ToLower(host) - ip1 := h.lookupStaticHost(h.hmap.name6, host) - ip2 := h.lookupStaticHost(h.inline.name6, host) - return append(ip1, ip2...) + return h.lookupStaticHostFamily(host, h.hmap.name6, h.hmap.wildName6, h.inline.name6, h.inline.wildName6) } // LookupStaticAddr looks up the hosts for the given address from the hosts file. diff --git a/plugin/hosts/wildcard.go b/plugin/hosts/wildcard.go new file mode 100644 index 000000000..98059c74b --- /dev/null +++ b/plugin/hosts/wildcard.go @@ -0,0 +1,18 @@ +package hosts + +import "github.com/miekg/dns" + +// isWildcardName reports whether name is a wildcard owner name (*.example.com.). +func isWildcardName(name string) bool { + return len(name) >= 2 && name[0] == '*' && name[1] == '.' +} + +// replaceWithAsteriskLabel replaces the leftmost label with '*'. +func replaceWithAsteriskLabel(qname string) string { + i, shot := dns.NextLabel(qname, 0) + if shot { + return "" + } + + return "*." + qname[i:] +} diff --git a/plugin/hosts/wildcard_test.go b/plugin/hosts/wildcard_test.go new file mode 100644 index 000000000..79130d9ce --- /dev/null +++ b/plugin/hosts/wildcard_test.go @@ -0,0 +1,58 @@ +package hosts + +import ( + "testing" +) + +func TestReplaceWithAsteriskLabel(t *testing.T) { + tests := []struct { + in, out string + }{ + {".", ""}, + {"example.com.", "*.com."}, + {"foo.example.com.", "*.example.com."}, + {"bar.intern.example.com.", "*.intern.example.com."}, + } + + for _, tc := range tests { + got := replaceWithAsteriskLabel(tc.in) + if got != tc.out { + t.Errorf("replaceWithAsteriskLabel(%q) = %q, want %q", tc.in, got, tc.out) + } + } +} + +func TestReplaceWithAsteriskLabelApex(t *testing.T) { + // Apex names produce a *.parent pattern but must not match *.zone wildcards. + if got := replaceWithAsteriskLabel("example.com."); got != "*.com." { + t.Fatalf("replaceWithAsteriskLabel(example.com.) = %q, want *.com.", got) + } +} + +func TestLookupWildcardHost(t *testing.T) { + const hosts = ` +127.0.0.53 *.example.org +127.0.1.52 *.intern.example.org +127.0.0.54 foo.example.org +192.168.33.10 *.example.com +192.168.33.11 a.example.com +192.168.33.12 b.example.com +` + + h := testHostsfile(hosts) + + tests := []staticHostEntry{ + {"foo.example.org.", []string{"127.0.0.54"}, []string{}}, + {"bar.example.org.", []string{"127.0.0.53"}, []string{}}, + {"bar.intern.example.org.", []string{"127.0.1.52"}, []string{}}, + {"a.example.com.", []string{"192.168.33.11"}, []string{}}, + {"b.example.com.", []string{"192.168.33.12"}, []string{}}, + {"c.example.com.", []string{"192.168.33.10"}, []string{}}, + {"example.com.", []string{}, []string{}}, + {"deep.foo.example.org.", []string{}, []string{}}, + } + + for _, ent := range tests { + testStaticHost(t, ent, h) + } +}