feat(hosts): add catch-all host mapping with '.' pattern

Adds a catch-all fallback to hostMapper.Lookup() that matches any
hostname when a '.' mapping exists. After the existing three-phase
matching (exact -> dot-prefix -> suffix walk) fails, the lookup now
checks for a standalone '.' entry as a final fallback.

This enables users to map all hostnames to a single IP (e.g., for SNI
proxy usage) with a simple configuration like 'hosts=.:<ip>'.

Fixes go-gost/gost#228
This commit is contained in:
ginuerzh
2026-06-20 20:47:10 +08:00
parent 548471717a
commit 48f31513c7
2 changed files with 102 additions and 5 deletions
+3
View File
@@ -136,9 +136,12 @@ func (h *hostMapper) Lookup(ctx context.Context, network, host string, opts ...h
}
}
if ips == nil {
ips = h.lookup(".")
if ips == nil {
return
}
}
switch network {
case "ip4":
+98 -4
View File
@@ -14,12 +14,26 @@ import (
// newTestMapper creates a hostMapper for testing with a nop logger to avoid nil dereference
// when Lookup uses h.options.logger directly.
// It synchronously loads the initial mappings to avoid race conditions with the background
// reload goroutine started by NewHostMapper.
// It synchronously loads the initial mappings without starting the background reload goroutine
// to prevent data races between NewHostMapper's goroutine and test lookups.
func newTestMapper(opts ...Option) *hostMapper {
opts = append(opts, LoggerOption(xlogger.Nop()))
h := NewHostMapper(opts...).(*hostMapper)
_ = h.reload(context.Background())
var options options
for _, opt := range opts {
opt(&options)
}
ctx, cancel := context.WithCancel(context.TODO())
h := &hostMapper{
mappings: make(map[string][]net.IP),
cancelFunc: cancel,
options: options,
logger: options.logger,
}
if h.logger == nil {
h.logger = xlogger.Nop()
}
_ = h.reload(ctx)
cancel()
return h
}
@@ -567,6 +581,86 @@ func TestLookupIPv4MappedIPv6(t *testing.T) {
}
}
func TestLookupCatchAll(t *testing.T) {
h := newTestMapper(MappingsOption([]Mapping{
{Hostname: ".", IP: net.ParseIP("10.0.0.1")},
}))
if err := h.Close(); err != nil {
t.Fatal(err)
}
tests := []struct {
host string
ok bool
}{
{"example.com", true},
{"anything.else.com", true},
{"deep.sub.domain.org", true},
{"singleword", true},
}
for _, tt := range tests {
t.Run(tt.host, func(t *testing.T) {
_, ok := h.Lookup(context.Background(), "ip", tt.host)
if ok != tt.ok {
t.Errorf("Lookup(%q) ok = %v, want %v", tt.host, ok, tt.ok)
}
})
}
}
func TestLookupCatchAllWithSpecific(t *testing.T) {
h := newTestMapper(MappingsOption([]Mapping{
{Hostname: ".example.com", IP: net.ParseIP("10.0.0.1")},
{Hostname: ".", IP: net.ParseIP("10.0.0.2")},
}))
if err := h.Close(); err != nil {
t.Fatal(err)
}
// Specific .suffix should take priority over catch-all
ips, ok := h.Lookup(context.Background(), "ip", "example.com")
if !ok || !ips[0].Equal(net.ParseIP("10.0.0.1")) {
t.Fatalf("expected [10.0.0.1] from .example.com, got %v (ok=%v)", ips, ok)
}
ips, ok = h.Lookup(context.Background(), "ip", "sub.example.com")
if !ok || !ips[0].Equal(net.ParseIP("10.0.0.1")) {
t.Fatalf("expected [10.0.0.1] from .example.com, got %v (ok=%v)", ips, ok)
}
// Unmatched hostname falls through to catch-all
ips, ok = h.Lookup(context.Background(), "ip", "other.com")
if !ok || !ips[0].Equal(net.ParseIP("10.0.0.2")) {
t.Fatalf("expected [10.0.0.2] from catch-all, got %v (ok=%v)", ips, ok)
}
}
func TestLookupCatchAllIP4IP6(t *testing.T) {
h := newTestMapper(MappingsOption([]Mapping{
{Hostname: ".", IP: net.ParseIP("10.0.0.1")},
{Hostname: ".", IP: net.ParseIP("::1")},
}))
if err := h.Close(); err != nil {
t.Fatal(err)
}
ips, ok := h.Lookup(context.Background(), "ip4", "anyhost.com")
if !ok || len(ips) != 1 || !ips[0].Equal(net.ParseIP("10.0.0.1")) {
t.Fatalf("expected [10.0.0.1] via ip4 catch-all, got %v (ok=%v)", ips, ok)
}
ips, ok = h.Lookup(context.Background(), "ip6", "anyhost.com")
if !ok || len(ips) != 1 || !ips[0].Equal(net.ParseIP("::1")) {
t.Fatalf("expected [::1] via ip6 catch-all, got %v (ok=%v)", ips, ok)
}
ips, ok = h.Lookup(context.Background(), "ip", "anyhost.com")
if !ok || len(ips) != 2 {
t.Fatalf("expected 2 IPs via catch-all, got %d (ok=%v)", len(ips), ok)
}
}
func TestLoaderWithListerPattern(t *testing.T) {
var _ loader.Loader = (*testLister)(nil)
var _ loader.Lister = (*testLister)(nil)