fix panic: cached NXDOMAIN returned an empty CNAME chain as success
ResolveCNAME/ResolveIPs cached negative results as an empty Result but the cache-hit path returned (empty, true), so enrichHost did chain[1:] on a zero-length slice and the process panicked (exit 2). Negative cache hits now return false, plus a defensive length guard in enrichHost and a regression test.
This commit is contained in:
1 parent
fe5c01666b
commit
81e53f8755
3 files changed
+23
-1
No files matched your search
@@ -100,6 +100,11 @@ func (e *Enricher) ResolveCNAME(ctx context.Context, host string) ([]string, boo
|
|||||||
key := "c:" + host
|
key := "c:" + host
|
||||||
if e.Cache != nil {
|
if e.Cache != nil {
|
||||||
if res, ok := e.Cache.Get(key); ok {
|
if res, ok := e.Cache.Get(key); ok {
|
||||||
|
if len(res.Chain) == 0 {
|
||||||
|
// Negatively cached: remember the failure, do not report a
|
||||||
|
// (empty) successful chain to callers.
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
if e.Metrics != nil {
|
if e.Metrics != nil {
|
||||||
e.Metrics.DNSCacheHits.Add(1)
|
e.Metrics.DNSCacheHits.Add(1)
|
||||||
}
|
}
|
||||||
@@ -135,6 +140,9 @@ func (e *Enricher) ResolveIPs(ctx context.Context, host string) ([]net.IP, bool)
|
|||||||
key := "ip:" + host
|
key := "ip:" + host
|
||||||
if e.Cache != nil {
|
if e.Cache != nil {
|
||||||
if res, ok := e.Cache.Get(key); ok {
|
if res, ok := e.Cache.Get(key); ok {
|
||||||
|
if len(res.IPs) == 0 {
|
||||||
|
return nil, false // negatively cached
|
||||||
|
}
|
||||||
if e.Metrics != nil {
|
if e.Metrics != nil {
|
||||||
e.Metrics.DNSCacheHits.Add(1)
|
e.Metrics.DNSCacheHits.Add(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,20 @@ func (ctxResolver) LookupIP(ctx context.Context, host string) ([]net.IP, error)
|
|||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveCNAMENegativeCacheDoesNotReturnChain(t *testing.T) {
|
||||||
|
r := &fakeResolver{err: &net.DNSError{Err: "no such host", IsNotFound: true}}
|
||||||
|
e := &Enricher{Resolver: r, Cache: NewTTLCache[Result](16, 2), PositiveTTL: time.Minute, NegativeTTL: time.Minute, Timeout: time.Second, MaxDepth: 8}
|
||||||
|
if _, ok := e.ResolveCNAME(context.Background(), "missing.example.com"); ok {
|
||||||
|
t.Fatal("first NXDOMAIN lookup must fail")
|
||||||
|
}
|
||||||
|
// The negative entry must not come back as a successful empty chain: an
|
||||||
|
// empty chain with ok=true caused a slice panic in pipeline.enrichHost.
|
||||||
|
chain, ok := e.ResolveCNAME(context.Background(), "missing.example.com")
|
||||||
|
if ok || len(chain) != 0 {
|
||||||
|
t.Fatalf("cached NXDOMAIN returned chain=%v ok=%v, want empty/false", chain, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolveContextCancellation(t *testing.T) {
|
func TestResolveContextCancellation(t *testing.T) {
|
||||||
e := &Enricher{Resolver: ctxResolver{}, Cache: NewTTLCache[Result](16, 2), PositiveTTL: time.Minute, NegativeTTL: time.Minute, MaxDepth: 8}
|
e := &Enricher{Resolver: ctxResolver{}, Cache: NewTTLCache[Result](16, 2), PositiveTTL: time.Minute, NegativeTTL: time.Minute, MaxDepth: 8}
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ func (p *Pipeline) enrichHost(ctx context.Context, host string) {
|
|||||||
// CNAME-only during enrichment: this is the high-volume path and A/AAAA is
|
// CNAME-only during enrichment: this is the high-volume path and A/AAAA is
|
||||||
// not needed to classify the target. IPs are resolved only for matches.
|
// not needed to classify the target. IPs are resolved only for matches.
|
||||||
chain, ok := p.cfg.Enricher.ResolveCNAME(ctx, host)
|
chain, ok := p.cfg.Enricher.ResolveCNAME(ctx, host)
|
||||||
if !ok {
|
if !ok || len(chain) < 2 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, target := range chain[1:] {
|
for _, target := range chain[1:] {
|
||||||
|
|||||||
Reference in new issue
Block a user