From 48f31513c769183d4d2a794cfdf1ae846ee5805d Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Sat, 20 Jun 2026 20:47:10 +0800 Subject: [PATCH] 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=.:'. Fixes go-gost/gost#228 --- hosts/hosts.go | 5 ++- hosts/hosts_test.go | 102 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 102 insertions(+), 5 deletions(-) diff --git a/hosts/hosts.go b/hosts/hosts.go index 4e80a023..421a591b 100644 --- a/hosts/hosts.go +++ b/hosts/hosts.go @@ -137,7 +137,10 @@ func (h *hostMapper) Lookup(ctx context.Context, network, host string, opts ...h } if ips == nil { - return + ips = h.lookup(".") + if ips == nil { + return + } } switch network { diff --git a/hosts/hosts_test.go b/hosts/hosts_test.go index 270e34be..68c4872e 100644 --- a/hosts/hosts_test.go +++ b/hosts/hosts_test.go @@ -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)