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 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
PTR records for reverse lookups are generated automatically by CoreDNS (based on the hosts file 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 ## 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). 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, Qname: "fallthrough-example.org.", Qtype: dns.TypeAAAA,
Answer: []dns.RR{}, Rcode: dns.RcodeSuccess, 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 = ` const hostsExample = `
@@ -144,6 +156,8 @@ const hostsExample = `
10.0.0.1 example.org 10.0.0.1 example.org
::FFFF:10.0.0.2 example.com ::FFFF:10.0.0.2 example.com
10.0.0.3 fallthrough-example.org 10.0.0.3 fallthrough-example.org
1.2.3.4 aa.example.com
5.6.7.8 *.apps.example.com
reload 5s reload 5s
timeout 3600 timeout 3600
` `

View File

@@ -55,6 +55,10 @@ type Map struct {
name4 map[string][]net.IP name4 map[string][]net.IP
name6 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 // Key for the list of host names must be a literal IP address
// including IPv6 address without zone identifier. // including IPv6 address without zone identifier.
// We don't support old-classful IP address notation. // We don't support old-classful IP address notation.
@@ -63,9 +67,11 @@ type Map struct {
func newMap() *Map { func newMap() *Map {
return &Map{ return &Map{
name4: make(map[string][]net.IP), name4: make(map[string][]net.IP),
name6: make(map[string][]net.IP), name6: make(map[string][]net.IP),
addr: make(map[string][]string), 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 { for _, v6 := range h.name6 {
l += len(v6) l += len(v6)
} }
for _, v4 := range h.wildName4 {
l += len(v4)
}
for _, v6 := range h.wildName6 {
l += len(v6)
}
for _, a := range h.addr { for _, a := range h.addr {
l += len(a) l += len(a)
} }
@@ -184,6 +196,15 @@ func (h *Hostsfile) parse(r io.Reader) *Map {
// name is not in Origins // name is not in Origins
continue 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 { switch family {
case 1: case 1:
hmap.name4[name] = append(hmap.name4[name], addr) hmap.name4[name] = append(hmap.name4[name], addr)
@@ -202,38 +223,41 @@ func (h *Hostsfile) parse(r io.Reader) *Map {
return hmap return hmap
} }
// lookupStaticHost looks up the IP addresses for the given host from the hosts file. func (h *Hostsfile) lookupStaticHostLocked(m, wild map[string][]net.IP, host string) []net.IP {
func (h *Hostsfile) lookupStaticHost(m 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() h.RLock()
defer h.RUnlock() defer h.RUnlock()
if len(m) == 0 { ip1 := h.lookupStaticHostLocked(m, wild, host)
return nil ip2 := h.lookupStaticHostLocked(inlineM, inlineWild, host)
} return append(ip1, ip2...)
ips, ok := m[host]
if !ok {
return nil
}
ipsCp := make([]net.IP, len(ips))
copy(ipsCp, ips)
return ipsCp
} }
// LookupStaticHostV4 looks up the IPv4 addresses for the given host from the hosts file. // LookupStaticHostV4 looks up the IPv4 addresses for the given host from the hosts file.
func (h *Hostsfile) LookupStaticHostV4(host string) []net.IP { func (h *Hostsfile) LookupStaticHostV4(host string) []net.IP {
host = strings.ToLower(host) return h.lookupStaticHostFamily(host, h.hmap.name4, h.hmap.wildName4, h.inline.name4, h.inline.wildName4)
ip1 := h.lookupStaticHost(h.hmap.name4, host)
ip2 := h.lookupStaticHost(h.inline.name4, host)
return append(ip1, ip2...)
} }
// LookupStaticHostV6 looks up the IPv6 addresses for the given host from the hosts file. // LookupStaticHostV6 looks up the IPv6 addresses for the given host from the hosts file.
func (h *Hostsfile) LookupStaticHostV6(host string) []net.IP { func (h *Hostsfile) LookupStaticHostV6(host string) []net.IP {
host = strings.ToLower(host) return h.lookupStaticHostFamily(host, h.hmap.name6, h.hmap.wildName6, h.inline.name6, h.inline.wildName6)
ip1 := h.lookupStaticHost(h.hmap.name6, host)
ip2 := h.lookupStaticHost(h.inline.name6, host)
return append(ip1, ip2...)
} }
// LookupStaticAddr looks up the hosts for the given address from the hosts file. // 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)
}
}