From 8e3998d9fd46e9a9996bba077baefb01425b7aeb Mon Sep 17 00:00:00 2001 From: KEERTHIVASAN K Date: Sun, 20 Sep 2026 04:46:16 +0530 Subject: [PATCH] feat(dns): Add real-time DNS speed benchmark and 'Fastest' auto-selection option (#4935) * feat(dns): Add real-time DNS speed benchmark and 'Fastest' auto-selection option * fix(dns): Address CodeRabbit & Codex PR review feedback * fix(ci): Remove .biomeignore to keep repository root clean * Fix Fastest DNS eligibility and failed probe handling --------- Co-authored-by: Chris Titus --- SPEC.md | 1 + config/dns.json | 2 + config/tweaks.json | 2 +- docs/src/content/docs/guides/tweaks.mdx | 1 + functions/private/Get-WinUtilDNSBenchmark.ps1 | 84 +++++++++++++++++++ functions/private/Set-WinUtilDNS.ps1 | 15 ++++ pester/dns-benchmark.Tests.ps1 | 79 +++++++++++++++++ pester/dns.Tests.ps1 | 30 +++++++ 8 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 functions/private/Get-WinUtilDNSBenchmark.ps1 create mode 100644 pester/dns-benchmark.Tests.ps1 diff --git a/SPEC.md b/SPEC.md index 13649757..30289b4d 100644 --- a/SPEC.md +++ b/SPEC.md @@ -81,6 +81,7 @@ Because the final script is concatenated, code cannot rely on runtime module imp ## Configuration Contract - Config files must remain valid JSON and compile cleanly through `ConvertFrom-Json`. +- `config/dns.json` opts unfiltered providers into Fastest selection with `BenchmarkEligible: true`; missing or false values exclude a provider from the TCP latency benchmark. - `config/applications.json` defines installable applications; each entry includes the fields expected by tests and UI code, such as package manager IDs, category, display content, description, and link. - `config/tweaks.json` defines Windows tweaks; registry and service changes include original values or original states when applicable so undo workflows can restore user systems. - Preset and navigation files reference valid config keys. Renaming a config key requires updating all presets, UI references, docs, and code paths together. diff --git a/config/dns.json b/config/dns.json index 02499902..da534943 100644 --- a/config/dns.json +++ b/config/dns.json @@ -1,5 +1,6 @@ { "Google":{ + "BenchmarkEligible": true, "Primary": "8.8.8.8", "Secondary": "8.8.4.4", "Primary6": "2001:4860:4860::8888", @@ -7,6 +8,7 @@ "DohTemplate": "https://dns.google/dns-query" }, "Cloudflare":{ + "BenchmarkEligible": true, "Primary": "1.1.1.1", "Secondary": "1.0.0.1", "Primary6": "2606:4700:4700::1111", diff --git a/config/tweaks.json b/config/tweaks.json index fbf518dd..b46930de 100644 --- a/config/tweaks.json +++ b/config/tweaks.json @@ -1885,7 +1885,7 @@ "category": "z__Advanced Tweaks - CAUTION", "panel": "1", "Type": "Combobox", - "ComboItems": "Default DHCP Google Cloudflare Cloudflare_Malware Cloudflare_Malware_Adult Open_DNS Quad9 AdGuard_Ads_Trackers AdGuard_Ads_Trackers_Malware_Adult", + "ComboItems": "Default DHCP Fastest Google Cloudflare Cloudflare_Malware Cloudflare_Malware_Adult Open_DNS Quad9 AdGuard_Ads_Trackers AdGuard_Ads_Trackers_Malware_Adult", "link": "https://winutil.christitus.com/code-reference/tweaks/z--advanced-tweaks---caution/changedns" }, "WPFAddUltPerf": { diff --git a/docs/src/content/docs/guides/tweaks.mdx b/docs/src/content/docs/guides/tweaks.mdx index 1c1465e9..fb6fefa7 100644 --- a/docs/src/content/docs/guides/tweaks.mdx +++ b/docs/src/content/docs/guides/tweaks.mdx @@ -65,6 +65,7 @@ Use the DNS section to switch both IPv4 and IPv6 DNS providers without editing a * **Default**: Uses the default DNS settings configured by your ISP or network. * **DHCP**: Automatically acquires DNS settings from the DHCP server. +* **Fastest**: Compares TCP port 53 connection times to Google and Cloudflare, then applies the quicker provider. Filtering providers are excluded. If both probes fail, existing DNS settings are preserved. The scan takes up to about three seconds; connection time is only a rough latency estimate, not a DNS lookup or DNS-over-HTTPS performance test. * [**Google**](https://developers.google.com/speed/public-dns?hl=en): A reliable and fast DNS service provided by Google. * [**Cloudflare**](https://developers.cloudflare.com/1.1.1.1/): Known for speed and privacy, Cloudflare DNS is a popular choice for enhancing internet performance. * [**Cloudflare_Malware**](https://developers.cloudflare.com/1.1.1.1/setup/#:~:text=Use%20the%20following%20DNS%20resolvers%20to%20block%20malicious%20content%3A): Provides additional protection by blocking malware sites. diff --git a/functions/private/Get-WinUtilDNSBenchmark.ps1 b/functions/private/Get-WinUtilDNSBenchmark.ps1 new file mode 100644 index 00000000..d1e4a936 --- /dev/null +++ b/functions/private/Get-WinUtilDNSBenchmark.ps1 @@ -0,0 +1,84 @@ +function Get-WinUtilDNSBenchmark { + <# + + .SYNOPSIS + Benchmarks neutral DNS providers by measuring TCP port 53 latency (RTT in ms) to determine the fastest DNS server. + + .PARAMETER TimeoutMs + Maximum timeout in milliseconds for each connection test. Default is 1500ms. + + .OUTPUTS + Array of PSCustomObjects containing Provider, PrimaryIP, and LatencyMs sorted by lowest latency. + + .EXAMPLE + $results = Get-WinUtilDNSBenchmark + $fastest = $results[0] + + #> + [CmdletBinding()] + param( + [ValidateRange(1, 9998)] + [int]$TimeoutMs = 1500 + ) + + Write-WinUtilLog -Component "DNS" -Message "Starting DNS latency benchmark scan (TCP port 53)..." + + $dnsConfigs = $sync.configs.dns + if ($null -eq $dnsConfigs) { + Write-Warning "DNS configurations not found in `$sync.configs.dns." + Write-WinUtilLog -Level "ERROR" -Component "DNS" -Message "DNS configurations not found in `$sync.configs.dns." + return @() + } + + $results = [System.Collections.Generic.List[PSObject]]::new() + + foreach ($prop in $dnsConfigs.PSObject.Properties) { + $providerName = $prop.Name + $primaryIp = $prop.Value.Primary + if (-not $primaryIp) { continue } + + # Providers must explicitly opt in so new filtering services are never auto-selected. + if ($prop.Value.BenchmarkEligible -ne $true) { + continue + } + + $latency = 9999 + $client = $null + try { + $client = New-Object System.Net.Sockets.TcpClient + $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() + $asyncResult = $client.BeginConnect($primaryIp, 53, $null, $null) + $success = $asyncResult.AsyncWaitHandle.WaitOne($TimeoutMs, $false) + $stopwatch.Stop() + + if ($success) { + $client.EndConnect($asyncResult) + $latency = [int]$stopwatch.ElapsedMilliseconds + } else { + $latency = 9999 + } + } catch { + $latency = 9999 + } finally { + if ($null -ne $client) { + $client.Dispose() + } + } + + $results.Add([PSCustomObject]@{ + Provider = $providerName + PrimaryIP = $primaryIp + LatencyMs = $latency + }) + } + + $sortedResults = @($results | Sort-Object LatencyMs) + if ($sortedResults.Count -gt 0 -and $sortedResults[0].LatencyMs -lt 9999) { + $fastest = $sortedResults[0] + Write-WinUtilLog -Component "DNS" -Message "DNS Benchmark completed. Fastest neutral provider: $($fastest.Provider) ($($fastest.LatencyMs) ms)" + } else { + Write-WinUtilLog -Component "DNS" -Message "DNS Benchmark completed. Could not determine latency for providers." + } + + return $sortedResults +} diff --git a/functions/private/Set-WinUtilDNS.ps1 b/functions/private/Set-WinUtilDNS.ps1 index 44afd9d8..12159c9e 100644 --- a/functions/private/Set-WinUtilDNS.ps1 +++ b/functions/private/Set-WinUtilDNS.ps1 @@ -18,6 +18,21 @@ function Set-WinUtilDNS { return $true } + if($DNSProvider -eq "Fastest") { + Write-WinUtilLog -Component "DNS" -Message "Auto-detecting fastest DNS provider via latency benchmark..." + $benchmark = Get-WinUtilDNSBenchmark + $validFastest = $benchmark | Where-Object { $_.LatencyMs -lt 9999 } | Select-Object -First 1 + if ($validFastest) { + $DNSProvider = $validFastest.Provider + Write-Host "Auto-selected fastest DNS provider: $DNSProvider ($($validFastest.LatencyMs) ms)" + Write-WinUtilLog -Component "DNS" -Message "Auto-selected fastest DNS provider: $DNSProvider ($($validFastest.LatencyMs) ms)" + } else { + Write-Warning "Could not measure DNS latency to any provider; keeping current network adapter DNS settings." + Write-WinUtilLog -Component "DNS" -Message "Benchmark timeout or all probes failed; aborting DNS change to preserve existing settings." + return $false + } + } + try { $Adapters = Get-NetAdapter | Where-Object {$_.Status -eq "Up"} Write-Host "Ensuring DNS is set to $DNSProvider on the following interfaces:" diff --git a/pester/dns-benchmark.Tests.ps1 b/pester/dns-benchmark.Tests.ps1 new file mode 100644 index 00000000..c28a1f48 --- /dev/null +++ b/pester/dns-benchmark.Tests.ps1 @@ -0,0 +1,79 @@ +BeforeAll { + . "$PSScriptRoot/../functions/private/Get-WinUtilDNSBenchmark.ps1" + function Write-WinUtilLog { param($Message, $Level, $Component) } +} + +Describe 'Get-WinUtilDNSBenchmark' { + BeforeEach { + $script:sync = @{ configs = @{ dns = Get-Content "$PSScriptRoot/../config/dns.json" -Raw | ConvertFrom-Json } } + $script:probeCompleted = $true + $script:connectFails = $false + $script:disposed = 0 + $script:ended = 0 + $script:probed = @() + Mock Write-WinUtilLog { } + Mock New-Object { + $client = [pscustomobject]@{} + $client | Add-Member ScriptMethod BeginConnect { + param($Address, $Port, $Callback, $State) + $script:probed += $Address + $handle = [pscustomobject]@{} + $handle | Add-Member ScriptMethod WaitOne { param($Timeout, $ExitContext) return $script:probeCompleted } + return [pscustomobject]@{ AsyncWaitHandle = $handle } + } + $client | Add-Member ScriptMethod EndConnect { + param($Result) + $script:ended++ + if ($script:connectFails) { throw 'Connection refused' } + } + $client | Add-Member ScriptMethod Dispose { $script:disposed++ } + return $client + } -ParameterFilter { $TypeName -eq 'System.Net.Sockets.TcpClient' } + } + + AfterEach { + Remove-Variable sync -Scope Script + } + + It 'only probes explicitly eligible unfiltered providers, including when an unknown provider is added' { + $script:sync.configs.dns | Add-Member NoteProperty UnknownProvider ([pscustomobject]@{ Primary = '192.0.2.1' }) + $results = @(Get-WinUtilDNSBenchmark) + $results.Count | Should -Be 2 + $results.Provider | Should -Contain 'Google' + $results.Provider | Should -Contain 'Cloudflare' + $script:probed.Count | Should -Be 2 + $script:probed | Should -Contain '8.8.8.8' + $script:probed | Should -Contain '1.1.1.1' + @($results | Where-Object LatencyMs -ge 9999).Count | Should -Be 0 + $script:ended | Should -Be 2 + $script:disposed | Should -Be 2 + } + + It 'disposes timed out probes and marks them unavailable' { + $script:probeCompleted = $false + $results = @(Get-WinUtilDNSBenchmark) + @($results | Where-Object LatencyMs -eq 9999).Count | Should -Be 2 + $script:disposed | Should -Be 2 + $script:ended | Should -Be 0 + } + + It 'does not treat EndConnect failures as successful latency measurements' { + $script:connectFails = $true + $results = @(Get-WinUtilDNSBenchmark) + @($results | Where-Object LatencyMs -eq 9999).Count | Should -Be 2 + $script:disposed | Should -Be 2 + $script:ended | Should -Be 2 + } + + It 'returns no results when no providers are eligible' { + $script:sync.configs.dns.Google.BenchmarkEligible = $false + $script:sync.configs.dns.Cloudflare.BenchmarkEligible = $false + @(Get-WinUtilDNSBenchmark).Count | Should -Be 0 + Should -Invoke New-Object -Times 0 -Exactly + } + + It 'rejects infinite or out-of-range timeouts' { + { Get-WinUtilDNSBenchmark -TimeoutMs -1 } | Should -Throw + { Get-WinUtilDNSBenchmark -TimeoutMs 10000 } | Should -Throw + } +} diff --git a/pester/dns.Tests.ps1 b/pester/dns.Tests.ps1 index e353e1a2..a558d765 100644 --- a/pester/dns.Tests.ps1 +++ b/pester/dns.Tests.ps1 @@ -39,6 +39,7 @@ BeforeAll { function Clear-DnsClientCache { } . (Join-Path $script:repoRoot "functions\private\Set-WinUtilDNS.ps1") + . (Join-Path $script:repoRoot "functions\private\Get-WinUtilDNSBenchmark.ps1") } Describe "Set-WinUtilDNS" { @@ -131,6 +132,35 @@ Describe "Set-WinUtilDNS" { Should -Invoke -CommandName Clear-DnsClientCache -Times 1 -Exactly } + It "preserves DNS when Fastest has no successful probes" { + Mock Get-WinUtilDNSBenchmark { + [pscustomobject]@{ Provider = 'Cloudflare'; LatencyMs = 9999 } + } + Set-WinUtilDNS -DNSProvider 'Fastest' | Should -BeFalse + Should -Invoke Get-NetAdapter -Times 0 -Exactly + Should -Invoke Set-DnsClientServerAddress -Times 0 -Exactly + Should -Invoke New-ItemProperty -Times 0 -Exactly + } + + It "preserves DNS when Fastest returns no results" { + Mock Get-WinUtilDNSBenchmark { } + Set-WinUtilDNS -DNSProvider 'Fastest' | Should -BeFalse + Should -Invoke Get-NetAdapter -Times 0 -Exactly + Should -Invoke Set-DnsClientServerAddress -Times 0 -Exactly + } + + It "applies the successful Fastest provider through the existing DNS and DoH path" { + Mock Get-WinUtilDNSBenchmark { + [pscustomobject]@{ Provider = 'Cloudflare'; LatencyMs = 12 } + [pscustomobject]@{ Provider = 'Google'; LatencyMs = 9999 } + } + Set-WinUtilDNS -DNSProvider 'Fastest' | Should -BeTrue + Should -Invoke Set-DnsClientServerAddress -Times 1 -Exactly -ParameterFilter { + $ServerAddresses[0] -eq '1.1.1.1' -and $ServerAddresses[1] -eq '1.0.0.1' + } + Should -Invoke Add-DnsClientDohServerAddress -Times 4 -Exactly + } + It "updates an existing DoH entry with the selected provider settings" { Mock Get-DnsClientDohServerAddress { if ($ServerAddress -eq "1.1.1.1") {