plugin/hosts: add wildcard support (#8185)

* plugin/hosts: add wildcard support for owner names

Signed-off-by: youknowforsearch <amirhebrahimzader@gmail.com>

* plugin/hosts: document wildcard owner name support

Signed-off-by: youknowforsearch <amirhebrahimzader@gmail.com>

* plugin/hosts: remove unused lookupStaticHost

Signed-off-by: Amirhossein Ebrahimzade <amirhossein.e@smartech.ir>

---------

Signed-off-by: youknowforsearch <amirhebrahimzader@gmail.com>
Signed-off-by: Amirhossein Ebrahimzade <amirhossein.e@smartech.ir>
Co-authored-by: Amirhossein Ebrahimzade <amirhossein.e@smartech.ir>
This commit is contained in:
Amirhossein Ebrahimzade
2026-06-25 08:38:08 +03:30
committed by GitHub
parent e45ad5f87a
commit 6805f6f8a0
5 changed files with 174 additions and 24 deletions

View File

@@ -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).

View File

@@ -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
`

View File

@@ -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.

18
plugin/hosts/wildcard.go Normal file
View File

@@ -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:]
}

View File

@@ -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)
}
}