diff --git a/samples/audit/squid.conf b/samples/audit/squid.conf index 42551a9d7..a4955270e 100644 --- a/samples/audit/squid.conf +++ b/samples/audit/squid.conf @@ -74,6 +74,9 @@ cache deny all # DNS settings - Squid resolves all domains for HTTP/HTTPS traffic dns_nameservers 8.8.8.8 8.8.4.4 +negative_dns_ttl 1 seconds +dns_retransmit_interval 1 seconds +dns_timeout 10 seconds # Forwarded headers forwarded_for delete diff --git a/src/squid/config-sections.test.ts b/src/squid/config-sections.test.ts index 137d0bac8..45e094a0f 100644 --- a/src/squid/config-sections.test.ts +++ b/src/squid/config-sections.test.ts @@ -13,6 +13,34 @@ function buildWithDefaults(overrides: Partial line.startsWith(`${directive} `)); + const value = directiveLine?.split(/\s+/)[1]; + if (!value) { + throw new Error(`Missing ${directive} directive`); + } + return Number(value); +} + +function canReachFallbackResolver(options: { + dnsRetransmitIntervalSeconds: number; + dnsTimeoutSeconds: number; + resolverOutcomes: ('stall' | 'success')[]; +}): boolean { + const { dnsRetransmitIntervalSeconds, dnsTimeoutSeconds, resolverOutcomes } = options; + let elapsedSeconds = 0; + for (const resolverOutcome of resolverOutcomes) { + if (elapsedSeconds >= dnsTimeoutSeconds) { + return false; + } + if (resolverOutcome === 'success') { + return true; + } + elapsedSeconds += dnsRetransmitIntervalSeconds; + } + return false; +} + describe('buildConfigSections', () => { describe('portConfig', () => { it('emits http_port with the configured port', () => { @@ -186,7 +214,31 @@ describe('buildConfigSections', () => { it('uses custom DNS servers when provided', () => { const { dnsSection } = buildWithDefaults({ dnsServers: ['1.1.1.1', '1.0.0.1'] }); -expect(dnsSection).toBe('dns_nameservers 1.1.1.1 1.0.0.1'); + expect(dnsSection).toMatch(/^dns_nameservers 1\.1\.1\.1 1\.0\.0\.1$/m); + }); + + it('shrinks negative_dns_ttl to avoid caching a single transient SERVFAIL', () => { + const { dnsSection } = buildWithDefaults(); + expect(dnsSection).toMatch(/^negative_dns_ttl 1 seconds$/m); + }); + + it('uses a short retransmit interval with enough total timeout for resolver fallback', () => { + const { dnsSection } = buildWithDefaults(); + expect(dnsSection).toMatch(/^dns_retransmit_interval 1 seconds$/m); + expect(dnsSection).toMatch(/^dns_timeout 10 seconds$/m); + }); + + it('keeps DNS timeout above retransmit interval so fallback nameservers are queried', () => { + const { dnsSection } = buildWithDefaults(); + const dnsRetransmitIntervalSeconds = parseSecondsDirective(dnsSection, 'dns_retransmit_interval'); + const dnsTimeoutSeconds = parseSecondsDirective(dnsSection, 'dns_timeout'); + + expect(dnsRetransmitIntervalSeconds).toBeLessThan(dnsTimeoutSeconds); + expect(canReachFallbackResolver({ + dnsRetransmitIntervalSeconds, + dnsTimeoutSeconds, + resolverOutcomes: ['stall', 'success'], + })).toBe(true); }); }); diff --git a/src/squid/config-sections.ts b/src/squid/config-sections.ts index d4e000590..e45bda299 100644 --- a/src/squid/config-sections.ts +++ b/src/squid/config-sections.ts @@ -201,7 +201,22 @@ function generateAllowedIpSection(domains: string[]): string { } function generateDnsSection(dnsServers?: string[]): string { - return `dns_nameservers ${(dnsServers && dnsServers.length > 0) ? dnsServers.join(' ') : DEFAULT_DNS_SERVERS.join(' ')}`; + const servers = (dnsServers && dnsServers.length > 0) ? dnsServers.join(' ') : DEFAULT_DNS_SERVERS.join(' '); + return `dns_nameservers ${servers} +# A single transient upstream DNS failure (e.g. a SERVFAIL from an overloaded +# resolver during concurrent container startup) must not turn into a sustained +# false-positive block of an allowlisted domain. Squid's default +# negative_dns_ttl (1 minute) caches that one failure and rejects every +# request for the same domain for up to 60 seconds. Shrinking it means the +# very next lookup attempt re-queries the resolver instead of replaying the +# cached failure. +negative_dns_ttl 1 seconds +# Retry another configured nameserver quickly while keeping the total timeout +# above the retry interval. If dns_timeout is equal to Squid's default +# dns_retransmit_interval (5 seconds), Squid can hit the total timeout before +# sending the retry to the fallback nameserver. +dns_retransmit_interval 1 seconds +dns_timeout 10 seconds`; } function generateConfigSections(options: {