Adding Mullvad to DNS (#4927)

* Update dns.json

* Fix Mullvad DNS review issues

* Add Mullvad secondary resolvers

* Register DoH before changing adapter DNS

* Report DNS failures to tweak workflow

* Handle non-terminating DNS assignment failures

* Preserve DNS fallback and document Mullvad options

---------

Co-authored-by: Chris Titus <contact@christitus.com>
This commit is contained in:
Graham David
2026-08-09 19:43:19 -05:00
committed by GitHub
co-authored by Chris Titus
parent afc3e1eec2
commit 0aa4ab3a40
8 changed files with 262 additions and 29 deletions
+54
View File
@@ -54,5 +54,59 @@
"Primary6": "2a10:50c0::bad1:ff",
"Secondary6": "2a10:50c0::bad2:ff",
"DohTemplate": "https://family.adguard-dns.com/dns-query"
},
"Mullvad":{
"Primary": "194.242.2.2",
"Secondary": "194.242.2.3",
"Primary6": "2a07:e340::2",
"Secondary6": "2a07:e340::3",
"DohOnly": true,
"DohTemplate": "https://dns.mullvad.net/dns-query",
"SecondaryDohTemplate": "https://adblock.dns.mullvad.net/dns-query"
},
"Mullvad_Ads_Trackers":{
"Primary": "194.242.2.3",
"Secondary": "194.242.2.2",
"Primary6": "2a07:e340::3",
"Secondary6": "2a07:e340::2",
"DohOnly": true,
"DohTemplate": "https://adblock.dns.mullvad.net/dns-query",
"SecondaryDohTemplate": "https://dns.mullvad.net/dns-query"
},
"Mullvad_Ads_Trackers_Malware":{
"Primary": "194.242.2.4",
"Secondary": "194.242.2.3",
"Primary6": "2a07:e340::4",
"Secondary6": "2a07:e340::3",
"DohOnly": true,
"DohTemplate": "https://base.dns.mullvad.net/dns-query",
"SecondaryDohTemplate": "https://adblock.dns.mullvad.net/dns-query"
},
"Mullvad_Ads_Trackers_Malware_Social":{
"Primary": "194.242.2.5",
"Secondary": "194.242.2.4",
"Primary6": "2a07:e340::5",
"Secondary6": "2a07:e340::4",
"DohOnly": true,
"DohTemplate": "https://extended.dns.mullvad.net/dns-query",
"SecondaryDohTemplate": "https://base.dns.mullvad.net/dns-query"
},
"Mullvad_Ads_Trackers_Malware_Adult_Gambling":{
"Primary": "194.242.2.6",
"Secondary": "194.242.2.5",
"Primary6": "2a07:e340::6",
"Secondary6": "2a07:e340::5",
"DohOnly": true,
"DohTemplate": "https://family.dns.mullvad.net/dns-query",
"SecondaryDohTemplate": "https://extended.dns.mullvad.net/dns-query"
},
"Mullvad_Ads_Trackers_Malware_Adult_Gambling_Social":{
"Primary": "194.242.2.9",
"Secondary": "194.242.2.6",
"Primary6": "2a07:e340::9",
"Secondary6": "2a07:e340::6",
"DohOnly": true,
"DohTemplate": "https://all.dns.mullvad.net/dns-query",
"SecondaryDohTemplate": "https://family.dns.mullvad.net/dns-query"
}
}
+1 -1
View File
@@ -1864,7 +1864,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 Google Cloudflare Cloudflare_Malware Cloudflare_Malware_Adult Open_DNS Quad9 AdGuard_Ads_Trackers AdGuard_Ads_Trackers_Malware_Adult Mullvad Mullvad_Ads_Trackers Mullvad_Ads_Trackers_Malware Mullvad_Ads_Trackers_Malware_Social Mullvad_Ads_Trackers_Malware_Adult_Gambling Mullvad_Ads_Trackers_Malware_Adult_Gambling_Social",
"link": "https://winutil.christitus.com/code-reference/tweaks/z--advanced-tweaks---caution/changedns"
},
"WPFAddUltPerf": {
+8
View File
@@ -73,6 +73,14 @@ Use the DNS section to switch both IPv4 and IPv6 DNS providers without editing a
* [**Quad9**](https://quad9.net/): Focuses on security by blocking known malicious domains.
* [**AdGuard_Ads_Trackers**](https://adguard-dns.io/en/welcome.html): AdGuard DNS blocks ads, trackers, and other unwanted DNS requests. Visit the website and sign in for a dashboard, statistics, and additional server-side customization.
* [**AdGuard_Ads_Trackers_Malware_Adult**](https://adguard-dns.io/en/welcome.html): AdGuard DNS blocks ads, trackers, malware, and adult content, and enables Safe Search and Safe Mode where possible.
* [**Mullvad**](https://mullvad.net/en/help/dns-over-https-and-dns-over-tls): Mullvad DNS without content blocking.
* [**Mullvad_Ads_Trackers**](https://mullvad.net/en/help/dns-over-https-and-dns-over-tls): Blocks ads and trackers.
* [**Mullvad_Ads_Trackers_Malware**](https://mullvad.net/en/help/dns-over-https-and-dns-over-tls): Blocks ads, trackers, and malware.
* [**Mullvad_Ads_Trackers_Malware_Social**](https://mullvad.net/en/help/dns-over-https-and-dns-over-tls): Blocks ads, trackers, malware, and social media.
* [**Mullvad_Ads_Trackers_Malware_Adult_Gambling**](https://mullvad.net/en/help/dns-over-https-and-dns-over-tls): Blocks ads, trackers, malware, adult content, and gambling.
* [**Mullvad_Ads_Trackers_Malware_Adult_Gambling_Social**](https://mullvad.net/en/help/dns-over-https-and-dns-over-tls): Applies all available Mullvad filters.
Mullvad profiles require DNS over HTTPS support in Windows. If the selected primary resolver is unavailable, WinUtil uses the closest Mullvad secondary resolver to preserve connectivity; that fallback may use a different filtering level and can be less restrictive.
### Customize Preferences
+46 -21
View File
@@ -15,7 +15,7 @@ function Set-WinUtilDNS {
if($DNSProvider -eq "Default") {
Write-WinUtilLog -Component "DNS" -Message "DNS provider is Default; no DNS changes applied."
return
return $true
}
try {
@@ -29,11 +29,17 @@ function Set-WinUtilDNS {
if($null -eq $dns) {
Write-Warning "DNS provider $DNSProvider was not found in configuration."
Write-WinUtilLog -Level "ERROR" -Component "DNS" -Message "DNS provider $DNSProvider was not found in configuration."
return
return $false
}
}
$dohSupported = [bool](Get-Command Add-DnsClientDohServerAddress -ErrorAction SilentlyContinue)
if ($DNSProvider -ne "DHCP" -and $dns.DohOnly -and -not $dohSupported) {
Write-Warning "DNS provider $DNSProvider requires DNS over HTTPS, which is not supported on this system."
Write-WinUtilLog -Level "ERROR" -Component "DNS" -Message "DNS provider $DNSProvider requires DNS over HTTPS, which is not supported on this system."
return $false
}
$dnscacheBase = "HKLM:\System\CurrentControlSet\Services\Dnscache\InterfaceSpecificParameters"
Foreach ($Adapter in $Adapters) {
@@ -64,40 +70,59 @@ function Set-WinUtilDNS {
Remove-Item -Path $dohInterfaceSettings -Recurse -Force -ErrorAction SilentlyContinue
}
} else {
Write-WinUtilLog -Component "DNS" -Message "Setting IPv4 DNS on adapter $($Adapter.Name) (ifIndex: $($Adapter.ifIndex)) to $($dns.Primary), $($dns.Secondary)."
Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ServerAddresses ($dns.Primary, $dns.Secondary)
Write-WinUtilLog -Component "DNS" -Message "Setting IPv6 DNS on adapter $($Adapter.Name) (ifIndex: $($Adapter.ifIndex)) to $($dns.Primary6), $($dns.Secondary6)."
Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ServerAddresses ($dns.Primary6, $dns.Secondary6)
$ipv4Addresses = @(@($dns.Primary, $dns.Secondary) | Where-Object { $_ })
$ipv6Addresses = @(@($dns.Primary6, $dns.Secondary6) | Where-Object { $_ })
if ($dohSupported -and $dns.DohTemplate) {
$ips = @($dns.Primary, $dns.Secondary, $dns.Primary6, $dns.Secondary6) | Where-Object { $_ }
foreach ($ip in $ips) {
$existing = Get-DnsClientDohServerAddress -ServerAddress $ip -ErrorAction SilentlyContinue
if ($existing) {
Set-DnsClientDohServerAddress -ServerAddress $ip -DohTemplate $dns.DohTemplate -AllowFallbackToUdp $false -AutoUpgrade $true -ErrorAction Stop
} else {
Write-WinUtilLog -Component "DNS" -Message "Registering DoH template for $ip."
Add-DnsClientDohServerAddress -ServerAddress $ip -DohTemplate $dns.DohTemplate -AllowFallbackToUdp $false -AutoUpgrade $true -ErrorAction Stop
try {
$ips = @($dns.Primary, $dns.Secondary, $dns.Primary6, $dns.Secondary6) | Where-Object { $_ }
foreach ($ip in $ips) {
$dohTemplate = if ($dns.SecondaryDohTemplate -and @($dns.Secondary, $dns.Secondary6) -contains $ip) {
$dns.SecondaryDohTemplate
} else {
$dns.DohTemplate
}
$existing = Get-DnsClientDohServerAddress -ServerAddress $ip -ErrorAction SilentlyContinue
if ($existing) {
Set-DnsClientDohServerAddress -ServerAddress $ip -DohTemplate $dohTemplate -AllowFallbackToUdp $false -AutoUpgrade $true -ErrorAction Stop
} else {
Write-WinUtilLog -Component "DNS" -Message "Registering DoH template for $ip."
Add-DnsClientDohServerAddress -ServerAddress $ip -DohTemplate $dohTemplate -AllowFallbackToUdp $false -AutoUpgrade $true -ErrorAction Stop
}
$leaf = if ($ip.Contains(':')) { 'Doh6' } else { 'Doh' }
$regPath = "$interfaceParams\DohInterfaceSettings\$leaf\$ip"
if (-not (Test-Path $regPath)) {
New-Item -Path $regPath -Force -ErrorAction Stop | Out-Null
}
New-ItemProperty -Path $regPath -Name "DohFlags" -Value 1 -PropertyType QWord -Force -ErrorAction Stop | Out-Null
}
$leaf = if ($ip.Contains(':')) { 'Doh6' } else { 'Doh' }
$regPath = "$interfaceParams\DohInterfaceSettings\$leaf\$ip"
if (-not (Test-Path $regPath)) {
New-Item -Path $regPath -Force -ErrorAction Stop | Out-Null
} catch {
if ($dns.DohOnly) {
throw
}
New-ItemProperty -Path $regPath -Name "DohFlags" -Value 1 -PropertyType QWord -Force -ErrorAction Stop | Out-Null
Write-Warning "DNS over HTTPS setup for provider $DNSProvider failed; continuing with plain DNS."
Write-WinUtilLog -Level "WARN" -Component "DNS" -Message "DNS over HTTPS setup for provider $DNSProvider failed; continuing with plain DNS: $($psitem.Exception.Message)"
}
}
Write-WinUtilLog -Component "DNS" -Message "Setting IPv4 DNS on adapter $($Adapter.Name) (ifIndex: $($Adapter.ifIndex)) to $($dns.Primary), $($dns.Secondary)."
Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ServerAddresses $ipv4Addresses -ErrorAction Stop
Write-WinUtilLog -Component "DNS" -Message "Setting IPv6 DNS on adapter $($Adapter.Name) (ifIndex: $($Adapter.ifIndex)) to $($dns.Primary6), $($dns.Secondary6)."
Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ServerAddresses $ipv6Addresses -ErrorAction Stop
}
}
if ($DNSProvider -ne "DHCP" -and $dohSupported -and $dns.DohTemplate) {
Clear-DnsClientCache
}
Write-WinUtilLog -Component "DNS" -Message "DNS provider change completed: $DNSProvider"
return $true
} catch {
Write-Warning "DNS provider $DNSProvider was not completed because an error occurred."
Write-Warning $psitem.Exception.Message
Write-WinUtilLog -Level "ERROR" -Component "DNS" -Message "DNS provider $DNSProvider was not completed: $($psitem.Exception.Message)"
return $false
}
}
+8 -1
View File
@@ -71,7 +71,14 @@ function Invoke-WPFtweaksbutton {
}
if ($dnsProvider -ne "Default") {
Set-WinUtilDNS -DNSProvider $dnsProvider
$dnsResult = @(Set-WinUtilDNS -DNSProvider $dnsProvider)
if ($dnsResult[-1] -ne $true) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "DNS change failed" -Percent 100
$sync.ProcessRunning = $false
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Error" -overlay "warning" }
Write-WinUtilLog -Level "ERROR" -Component "Tweaks" -Message "Tweaks workflow stopped because the DNS change failed."
return
}
}
for ($i = 0; $i -lt $tweaks.Count; $i++) {
+11
View File
@@ -374,6 +374,17 @@ Describe "UI-rendered config entries" {
}
}
It "exposes every configured DNS provider in the DNS combobox" {
$dns = Get-WinUtilConfigObject -Name "dns"
$tweaks = Get-WinUtilConfigObject -Name "tweaks"
$comboItems = @($tweaks.WPFchangedns.ComboItems -split " ")
$missingProviders = @($dns.PSObject.Properties.Name | Where-Object { $comboItems -notcontains $_ })
if ($missingProviders.Count -gt 0) {
throw "WPFchangedns missing providers: $($missingProviders -join ', ')"
}
}
It "contains required feature fields and valid configured functions" {
$feature = Get-WinUtilConfigObject -Name "feature"
$functionNames = Get-WinUtilTopLevelFunctionNames
+105 -5
View File
@@ -10,7 +10,8 @@ BeforeAll {
param(
$InterfaceIndex,
$ServerAddresses,
[switch]$ResetServerAddresses
[switch]$ResetServerAddresses,
$ErrorAction
)
}
function netsh {
@@ -52,6 +53,23 @@ Describe "Set-WinUtilDNS" {
Secondary6 = "2606:4700:4700::1001"
DohTemplate = "https://cloudflare-dns.com/dns-query"
}
Mullvad = [pscustomobject]@{
Primary = "194.242.2.2"
Secondary = "194.242.2.3"
Primary6 = "2a07:e340::2"
Secondary6 = "2a07:e340::3"
DohOnly = $true
DohTemplate = "https://dns.mullvad.net/dns-query"
SecondaryDohTemplate = "https://adblock.dns.mullvad.net/dns-query"
}
MullvadNoSecondary = [pscustomobject]@{
Primary = "194.242.2.2"
Secondary = ""
Primary6 = "2a07:e340::2"
Secondary6 = ""
DohOnly = $true
DohTemplate = "https://dns.mullvad.net/dns-query"
}
}
}
})
@@ -88,8 +106,9 @@ Describe "Set-WinUtilDNS" {
}
It "sets IPv4 and IPv6 DNS server addresses separately and applies DoH templates" {
Set-WinUtilDNS -DNSProvider "Cloudflare"
$result = Set-WinUtilDNS -DNSProvider "Cloudflare"
$result | Should -BeTrue
Should -Invoke -CommandName Set-DnsClientServerAddress -Times 1 -Exactly -ParameterFilter {
$InterfaceIndex -eq 7 -and
$ServerAddresses.Count -eq 2 -and
@@ -131,6 +150,79 @@ Describe "Set-WinUtilDNS" {
Should -Invoke -CommandName Add-DnsClientDohServerAddress -Times 3 -Exactly
}
It "filters empty DNS server addresses" {
Set-WinUtilDNS -DNSProvider "MullvadNoSecondary"
Should -Invoke -CommandName Set-DnsClientServerAddress -Times 1 -Exactly -ParameterFilter {
$InterfaceIndex -eq 7 -and
$ServerAddresses.Count -eq 1 -and
$ServerAddresses[0] -eq "194.242.2.2"
}
Should -Invoke -CommandName Set-DnsClientServerAddress -Times 1 -Exactly -ParameterFilter {
$InterfaceIndex -eq 7 -and
$ServerAddresses.Count -eq 1 -and
$ServerAddresses[0] -eq "2a07:e340::2"
}
Should -Invoke -CommandName Add-DnsClientDohServerAddress -Times 2 -Exactly
}
It "applies the matching DoH template to secondary resolvers" {
Set-WinUtilDNS -DNSProvider "Mullvad"
Should -Invoke -CommandName Add-DnsClientDohServerAddress -Times 2 -Exactly -ParameterFilter {
$ServerAddress -in @("194.242.2.2", "2a07:e340::2") -and
$DohTemplate -eq "https://dns.mullvad.net/dns-query"
}
Should -Invoke -CommandName Add-DnsClientDohServerAddress -Times 2 -Exactly -ParameterFilter {
$ServerAddress -in @("194.242.2.3", "2a07:e340::3") -and
$DohTemplate -eq "https://adblock.dns.mullvad.net/dns-query"
}
}
It "does not apply a DoH-only provider when DoH is unsupported" {
Mock Get-Command { return $null } -ParameterFilter { $Name -eq "Add-DnsClientDohServerAddress" }
$result = Set-WinUtilDNS -DNSProvider "Mullvad"
$result | Should -BeFalse
Should -Invoke -CommandName Set-DnsClientServerAddress -Times 0 -Exactly
Should -Invoke -CommandName Add-DnsClientDohServerAddress -Times 0 -Exactly
Should -Invoke -CommandName Write-Warning -Times 1 -Exactly -ParameterFilter {
$Message -eq "DNS provider Mullvad requires DNS over HTTPS, which is not supported on this system."
}
}
It "does not change adapter DNS when DoH registration fails" {
Mock Add-DnsClientDohServerAddress { throw "DoH registration failed" }
$result = Set-WinUtilDNS -DNSProvider "Mullvad"
$result | Should -BeFalse
Should -Invoke -CommandName Set-DnsClientServerAddress -Times 0 -Exactly
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Level -eq "ERROR" -and
$Component -eq "DNS" -and
$Message -like "DNS provider Mullvad was not completed: *"
}
}
It "falls back to plain DNS when optional DoH registration fails" {
Mock Add-DnsClientDohServerAddress { throw "DoH registration failed" }
$result = Set-WinUtilDNS -DNSProvider "Cloudflare"
$result | Should -BeTrue
Should -Invoke -CommandName Set-DnsClientServerAddress -Times 2 -Exactly
Should -Invoke -CommandName Write-Warning -Times 1 -Exactly -ParameterFilter {
$Message -eq "DNS over HTTPS setup for provider Cloudflare failed; continuing with plain DNS."
}
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Level -eq "WARN" -and
$Component -eq "DNS" -and
$Message -like "DNS over HTTPS setup for provider Cloudflare failed; continuing with plain DNS: *"
}
}
It "resets DNS to DHCP and removes the applied DoH configuration" {
Mock Test-Path { return $true } -ParameterFilter { $Path -like "*DohInterfaceSettings*" }
Mock Get-ChildItem {
@@ -178,15 +270,23 @@ Describe "Set-WinUtilDNS" {
Should -Invoke -CommandName Remove-DnsClientDohServerAddress -Times 4 -Exactly
}
It "catches DNS setter failures so the tweak runspace can continue" {
Mock Set-DnsClientServerAddress { throw "DNS failed" }
It "catches non-terminating DNS setter failures so the tweak runspace can continue" {
Mock Set-DnsClientServerAddress { Write-Error "DNS failed" -ErrorAction $ErrorAction }
{ Set-WinUtilDNS -DNSProvider "Cloudflare" } | Should -Not -Throw
$result = Set-WinUtilDNS -DNSProvider "Cloudflare"
$result | Should -BeFalse
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Level -eq "ERROR" -and
$Component -eq "DNS" -and
$Message -like "DNS provider Cloudflare was not completed: *"
}
}
It "returns failure for an unknown DNS provider" {
$result = Set-WinUtilDNS -DNSProvider "Unknown"
$result | Should -BeFalse
Should -Invoke -CommandName Set-DnsClientServerAddress -Times 0 -Exactly
}
}
+29 -1
View File
@@ -181,9 +181,14 @@ Describe "Invoke-WPFtweaksbutton" {
text = "Cloudflare"
}
})
$script:capturedTweaksScriptBlock = $null
Mock Invoke-WPFRunspace { [pscustomobject]@{ MockHandle = $true } }
Mock Invoke-WPFRunspace {
$script:capturedTweaksScriptBlock = $ScriptBlock
[pscustomobject]@{ MockHandle = $true }
}
Mock Invoke-WinUtilTweaks { }
Mock Set-WinUtilTweaksProgressIndicator { }
Mock Invoke-WPFUIThread { }
Mock Write-WinUtilLog { }
Mock Write-Host { }
@@ -191,6 +196,7 @@ Describe "Invoke-WPFtweaksbutton" {
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name capturedTweaksScriptBlock -Scope Script -ErrorAction SilentlyContinue
}
It "passes selected tweaks, DNS provider, and progress counters to the tweak runspace" {
@@ -236,4 +242,26 @@ Describe "Invoke-WPFtweaksbutton" {
$ParameterList[3][1] -eq 2
}
}
It "stops the tweak workflow when the DNS change fails" {
$script:sync.selectedTweaks.Add("WPFTweaksTelemetry")
Mock Set-WinUtilDNS { return $false }
Invoke-WPFtweaksbutton
& $script:capturedTweaksScriptBlock -tweaks @("WPFTweaksTelemetry") -dnsProvider "Mullvad" -completedSteps 0 -totalSteps 1
Should -Invoke -CommandName Invoke-WinUtilTweaks -Times 0 -Exactly
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "DNS change failed" -and $Percent -eq 100
}
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "Error" -overlay "warning"*'
}
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Level -eq "ERROR" -and
$Component -eq "Tweaks" -and
$Message -eq "Tweaks workflow stopped because the DNS change failed."
}
$script:sync.ProcessRunning | Should -BeFalse
}
}