├── .gitignore ├── LICENSE ├── README.md ├── Windows-DNS-AdBlocker.ps1 └── adservers.dns /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jeffrey Jansma / perplexityjeff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Windows-DNS-AdBlocker 2 | 3 | A simple to use PowerShell script that uses an open source adblocking list and imports it into the Windows Server DNS entry list to block them. To be used as a scheduled task to run and update entries. I suggest running it every weekend. 4 | 5 | For Windows 2012 Server and above, this script uses the `DnsServer` PowerShell module to make changes to the DNS, and if Active Directory is detected then it can create AD-integrated DNS zones that will replicate forest-wide. For earlier versions of Windows, only the local DNS will be updated via direct modification of the registry - if you have more than one DNS server you will need to run this script at all of them. 6 | 7 | We installed this script as a security measure and to ease deployment for all users that do not have the required rights to install an adblocker or the knowledge to do so when they want to. This way the DNS server controls everything and no need for per client installation or management. 8 | 9 | ## Caution 10 | 11 | Use at your own risk! Be sure to thoroughly test before letting loose in any kind of corporate infrastructure! 12 | 13 | There is code included to clean up the registry of old DNS entries to keep the AdBlock list up to date. This should be OK as it explicitly checks for existence of the property `DatabaseFile` having a value of `adservers.dns` on each zone entry but be sure to test it first, having a backup of your valid zones! 14 | 15 | Where Active Directory integration is used, the `Responsible Person` field of the SOA record is set to a GUID value that is checked for on deletion, so should in theory be very safe. 16 | 17 | ## Tested 18 | 19 | The script has been tested in our own environment (company-wide) on a Windows Server 2008 R2 / 2016 DNS machine but the script is flexible enough to work for the newer Operating Systems as well you need to do some edits mainly to the AdBlock list. 20 | 21 | ## AdBlock List 22 | 23 | The script currently uses a very specific AdBlock list from https://pgl.yoyo.org/adservers/ they also have a great explanation about how to setup a Windows DNS AdBlocker on the website on this page https://pgl.yoyo.org/adservers/#other and navigating to the "Microsoft DNS Server" section. You will want to read it because you may require some pre-setup before the script can run fully automated. 24 | 25 | Currently in the script we are using this specific AdBlock file: 26 | https://pgl.yoyo.org/adservers/serverlist.php?hostformat=win32reg-sp4&showintro=0&mimetype=plaintext 27 | 28 | ## Adservers.dns file 29 | 30 | The file included in this repo called `adservers.dns` is a file that should be copied to `%SystemRoot%\system32\dns` as a reference of where the entry detected needs to go when an ad has been detected. The one that I added routes everything to 0.0.0.0 making them not appear but you could customize this. The script automatically downloads the default `adservers.dns` file when it has detected that it is not found on your system. 31 | 32 | This file is used only for non-Active Directory integrated deployments. For AD-integrated mode, the default A record entries are pointed to the same location as that in `adservers.dns`. Currently, if you wish to change this you will need to edit the code. 33 | 34 | ## Cleaning up 35 | 36 | If you want to remove all the adblock zones from your DNS, run this script again with the `-Remove` switch. Please pay attention to the `Caution` paragraph above, especially if you have created the zones in non-Active Directory integrated mode. 37 | 38 | ## License 39 | 40 | This project is licensed under the [MIT license](LICENSE) 41 | 42 | ## Learning 43 | 44 | I am still a beginner in PowerShell and I am learning it through my work as I need to get things done so please be kind to me when you see any weird mistakes or things in the script. I hope it helps someone somewhere :D ! 45 | -------------------------------------------------------------------------------- /Windows-DNS-AdBlocker.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Populate Windows DNS with entries for known ad-server domains to block them 4 | 5 | .DESCRIPTION 6 | Use DNS to block known ad-server domains by redirecting requests to localhost 7 | This needs to be run with administrator privilege, and if using Active Directory integration, 8 | you should run this under an account with domain admin privilege. 9 | 10 | .PARAMETER Remove 11 | If set, remove all ad-server entries from the DNS 12 | If not set, update DNS with latest ad-server entries. 13 | 14 | .PARAMETER ActiveDirectoryIntegrated 15 | If set, detect Active Directory and create AD integrated zones if found. 16 | Integrated zones will replicate to all DNS servers in AD forest. 17 | #> 18 | param 19 | ( 20 | [switch]$Remove, 21 | [switch]$ActiveDirectoryIntegrated 22 | ) 23 | 24 | Write-Host "====================================================" 25 | Write-Host "Windows-DNS-AdBlocker" 26 | Write-Host "https://github.com/perplexityjeff/Windows-DNS-AdBlocker" 27 | Write-Host "====================================================" 28 | 29 | function Insert-Content ($file) 30 | { 31 | BEGIN 32 | { 33 | $content = Get-Content $file 34 | } 35 | PROCESS 36 | { 37 | $_ | Set-Content $file 38 | } 39 | END 40 | { 41 | $content | Add-Content $file 42 | } 43 | } 44 | 45 | #Declares 46 | $artifactPath = Join-Path $env:TEMP "DNS Blocklist" 47 | $url = "https://pgl.yoyo.org/adservers/serverlist.php?hostformat=win32reg-sp4&showintro=0&mimetype=plaintext" 48 | $adServerZoneFile = "adservers.dns" 49 | $adserversurl = "https://raw.githubusercontent.com/perplexityjeff/Windows-DNS-AdBlocker/master/$adServerZoneFile" 50 | $adserverstemp = Join-Path $artifactPath $adServerZoneFile 51 | $adServersZoneFileLocation = Join-Path $env:SYSTEMROOT (Join-Path "System32\dns" $adServerZoneFile) 52 | 53 | $date = Get-Date -format "yyyyMMdd" 54 | $blocklist = Join-Path $artifactPath ("Blocklist_" + $date + ".reg") 55 | $limit = (Get-Date).AddDays(-15) 56 | 57 | # How we will identify Active Directory integrated zones created by this tool. 58 | $responsiblePerson = "adserver-71e5831a-aba8-4890-9037-399cb92de586" 59 | 60 | # Detect Active Directory and DnsServer PowerShell module 61 | $useActiveDirectory = $false 62 | $activeDirectoryDetected = $null -ne $env:LOGONSERVER -and $null -ne $env:USERDOMAIN 63 | $haveDnsServerModule = $null -ne (Get-Module -ListAvailable DnsServer) 64 | 65 | # This sets the security used in the WebClient to TLS 1.2, if it fails like on V2 it uses another method 66 | try 67 | { 68 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 69 | } 70 | catch 71 | { 72 | $p = [Enum]::ToObject([System.Net.SecurityProtocolType], 3072); 73 | [System.Net.ServicePointManager]::SecurityProtocol = $p; 74 | } 75 | 76 | try 77 | { 78 | # Test for admin rights 79 | if (-not (New-Object Security.Principal.WindowsPrincipal -ArgumentList ([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)) 80 | { 81 | throw "Must be Administrator to run this program." 82 | } 83 | 84 | # Test if DNS server exists on this host 85 | if (-not (Get-Service -Name DNS -ErrorAction SilentlyContinue)) 86 | { 87 | throw "Local DNS server not found. Please run on a server that hosts DNS." 88 | } 89 | 90 | if (-not ([Environment]::UserInteractive)) 91 | { 92 | # Hide progress bars if running e.g. in task scheduler. 93 | $ProgressPreference = 'SilentlyContinue' 94 | } 95 | 96 | if ($haveDnsServerModule) 97 | { 98 | # For Windows 2012 and above, use DnsServer cmdlets 99 | Write-Host "Importing DnsServer module" 100 | Import-Module DnsServer 101 | } 102 | 103 | if ($activeDirectoryDetected -and $haveDnsServerModule -and ([Environment]::UserInteractive) -and -not ($ActiveDirectoryIntegrated -or $Remove)) 104 | { 105 | # Recommend AD integration if not selected. 106 | $choice = $host.ui.PromptForChoice( 107 | "Active Directory detected. It is recommended to use it as it will replicate zones to all DNS servers in the forest. Use Active Directory?", 108 | $null, 109 | @( 110 | New-Object System.Management.Automation.Host.ChoiceDescription ('&Yes', "Use Active Directory." ) 111 | New-Object System.Management.Automation.Host.ChoiceDescription ('&No', 'Use file-based zones (no replication).') 112 | ), 113 | 0 114 | ) 115 | 116 | if ($choice -eq 0) 117 | { 118 | $useActiveDirectory = $true 119 | } 120 | } 121 | 122 | if ($ActiveDirectoryIntegrated) 123 | { 124 | # If user asked for Active Directory integration, check we can actually do it. 125 | if (-not $activeDirectoryDetected) 126 | { 127 | Write-Warning "Active Directory domain not detected, or you are not using a domain login. Falling back to file-based zones" 128 | } 129 | else 130 | { 131 | # Check we have the required PowerShell module for integration 132 | if ($haveDnsServerModule) 133 | { 134 | $useActiveDirectory = $true 135 | } 136 | else 137 | { 138 | Write-Warning "PowerShell module DnsServer not detected. Cannot use AD integration. Perhaps this version of Windows is too old. Falling back to file-based zones" 139 | } 140 | } 141 | } 142 | 143 | if ($Remove) 144 | { 145 | Write-Host "Removing ad-block entries from DNS only." 146 | } 147 | else 148 | { 149 | # Testing if DNS Blocklist folder exists 150 | Write-Host "Detecting if download location exists..." 151 | if (Test-Path $artifactPath) 152 | { 153 | Write-Host "Download location exists" 154 | } 155 | else 156 | { 157 | New-Item $artifactPath -ItemType directory 158 | Write-Host "Download location has been created" 159 | } 160 | 161 | # Testing if $adServerZoneFile file exists 162 | Write-Host "Detecting if $adServerZoneFile file exists..." 163 | if (-not (Test-Path $adServersZoneFileLocation)) 164 | { 165 | Write-Host "Downloading default $adServerZoneFile file..." 166 | $client = new-object System.Net.WebClient 167 | 168 | try 169 | { 170 | $client.DownloadFile($adserversurl, $adserverstemp) 171 | } 172 | finally 173 | { 174 | if ($client) 175 | { 176 | $client.Dispose() 177 | } 178 | } 179 | 180 | Write-Host "Downloaded default $adServerZoneFile file" 181 | 182 | Write-Host "Placing downloaded $adServerZoneFile file in the systemroot..." 183 | Move-Item $adserverstemp $adServersZoneFileLocation 184 | Write-Host "Placed downloaded $adServerZoneFile file in the systemroot" 185 | } 186 | else 187 | { 188 | Write-Host "Detected $adServerZoneFile file" 189 | } 190 | 191 | # Testing if DNS Blocklist exists 192 | Write-Host "Detecting if older DNS Blocklist exists..." 193 | if (Test-Path $blocklist) 194 | { 195 | Write-Host "Deleting old DNS Blocklist..." 196 | Remove-Item ($blocklist) 197 | Write-Host "Deleted old DNS Blocklist" 198 | } 199 | else 200 | { 201 | Write-Host "No existing DNS Blocklist found" 202 | } 203 | 204 | # Downloading of the Adblock reg file 205 | Write-Host "Downloading newest AdBlock file..." 206 | if (-not (Split-Path -parent $artifactPath) -or -not (Test-Path -pathType Container (Split-Path -parent $artifactPath))) 207 | { 208 | $artifactPath = Join-Path $pwd (Split-Path -leaf $artifactPath) 209 | } 210 | try 211 | { 212 | $client = new-object System.Net.WebClient 213 | 214 | try 215 | { 216 | $client.DownloadFile($url, $blocklist) 217 | } 218 | finally 219 | { 220 | if ($client) 221 | { 222 | $client.Dispose() 223 | } 224 | } 225 | 226 | Write-Host "Downloaded newest AdBlock file" 227 | } 228 | catch 229 | { 230 | throw "Download of the DNS Blocklist failed" 231 | } 232 | 233 | Write-Host "Detecting if new DNS Blocklist exists..." 234 | if (-Not(Test-Path $blocklist)) 235 | { 236 | throw "Download of the DNS Blocklist failed" 237 | } 238 | } 239 | 240 | if (-not $haveDnsServerModule) 241 | { 242 | # With DnsServer PowerShell module, we can operate on the DNS hot. 243 | # If poking the registry, we need to do it cold. 244 | # Stopping the DNS Server. 245 | Write-Host "Stopping DNS Server..." 246 | Stop-Service -Name DNS 247 | Write-Host "Stopped DNS Server" 248 | } 249 | 250 | # Remove All Old Entries (CAUTION: Be sure to tweak this to your environment and not delete valid DNS entries) 251 | Write-Host "Deleting old Blocklist entries" 252 | $zoneKey = "HKLM:\software\Microsoft\Windows NT\CurrentVersion\DNS Server\Zones\" 253 | 254 | # Read all zones _before_ modifying anything 255 | if ($haveDnsServerModule) 256 | { 257 | # We can do some pre-filtering here 258 | $allZones = Get-DnsServerZone | Where-Object { $_.ZoneType -eq 'Primary' -and -not $_.IsReverseLookupZone -and -not $_.IsAutoCreated } 259 | } 260 | else 261 | { 262 | $allZones = (Get-ChildItem -Path $zoneKey).Name | 263 | Split-Path -Leaf | 264 | ForEach-Object { 265 | 266 | # Make these results look like a DNS Zone object returned by Get-DnsZerverZone 267 | New-Object PSObject -Property @{ 268 | ZoneName = $_ 269 | } 270 | } 271 | } 272 | 273 | # Further filter out zones matching the the user DNS domain and the TrustAnchors special domain 274 | $allZones = $allZones | 275 | Where-Object { 276 | 277 | $_.ZoneName -ne 'TrustAnchors' 278 | } | 279 | Where-Object { 280 | 281 | if ($null -eq $env:USERDNSDOMAIN) 282 | { 283 | # Include all if no DNS domain to compare with 284 | $true 285 | } 286 | else 287 | { 288 | # Include those not matching the local DNS domain 289 | -not $_.ZoneName.EndsWith($env:USERDNSDOMAIN, [StringComparison]::OrdinalIgnoreCase) 290 | } 291 | } 292 | 293 | # Now do the deletions 294 | # Use Measure-Object as it can count the input object being $null 295 | $totalZones = ($allZones | Measure-Object).Count 296 | $numRemoved = 0 297 | $numProcessed = 0 298 | 299 | $allZones | 300 | ForEach-Object { 301 | 302 | if ($numRemoved % 20 -eq 0) 303 | { 304 | # Update progress every 20 zones 305 | $percentComplete = [int](($numProcessed / $totalZones) * 100) 306 | Write-Progress -Activity "Deleting adserver DNS zones" -Status "$($percentComplete)% Complete:" -PercentComplete $percentComplete 307 | } 308 | 309 | if ($haveDnsServerModule) 310 | { 311 | $zone = Get-DnsServerZone -Name $_.ZoneName -ErrorAction SilentlyContinue 312 | 313 | if ($null -ne $zone) 314 | { 315 | # Is it one of ours? 316 | if ($zone.IsDsIntegrated) 317 | { 318 | # AD Integrated - Identify by the value we set for Responsible Person in the SOA record... 319 | 320 | # Get SOA record 321 | $soa = $zone | Get-DnsServerResourceRecord -RRType SOA 322 | 323 | if ($soa.RecordData.ResponsiblePerson.StartsWith($responsiblePerson)) 324 | { 325 | # Delete it. 326 | Write-Verbose "Removing zone: $($_.ZoneName)" 327 | $zone | Remove-DnsServerZone -Force 328 | ++$numRemoved 329 | } 330 | } 331 | else 332 | { 333 | # File zone - Identify by the zone filename 334 | if ($zone.ZoneFile -eq $adServerZoneFile) 335 | { 336 | # Delete it. 337 | Write-Verbose "Removing zone: $($_.ZoneName)" 338 | $zone | Remove-DnsServerZone -Force 339 | ++$numRemoved 340 | } 341 | } 342 | } 343 | 344 | ++$numProcessed 345 | } 346 | else 347 | { 348 | $CurrentKey = Get-ItemProperty -Path (Join-Path $zoneKey $_.ZoneName) 349 | 350 | # Cleanly detect zones that are using adservers.dns 351 | if ($CurrentKey.PSObject.Properties.Name -icontains 'DatabaseFile' -and $CurrentKey.DatabaseFile -ieq $adServerZoneFile) 352 | { 353 | Write-Verbose "Removing zone: $($CurrentKey.PSChildName)" 354 | $CurrentKey | Remove-Item -Force #-Whatif 355 | ++$numRemoved 356 | } 357 | } 358 | } 359 | 360 | # Import new adserver zones 361 | 362 | # Clear progress bar 363 | Write-Progress -Activity "Deleting adserver DNS zones" -Status "100% Complete:" -PercentComplete 100 -Completed 364 | Write-Host "Deleted $numRemoved old Blocklist entries from Registry" 365 | 366 | if (-not $Remove) 367 | { 368 | Write-Host "Importing AdBlock file..." 369 | 370 | if ($haveDnsServerModule) 371 | { 372 | $numAdded = 0 373 | 374 | # Parse the REG file for domains and add them 375 | $domains = Get-Content $blocklist | 376 | Foreach-Object { 377 | 378 | if ($_ -match '\\(?[^\\]+)\]\s*$') 379 | { 380 | $Matches.domain 381 | } 382 | } 383 | 384 | $totalZones = ($domains | Measure-Object).Count 385 | 386 | $domains | 387 | Foreach-Object { 388 | 389 | if ($numAdded % 20 -eq 0) 390 | { 391 | # Update progress every 20 zones 392 | $percentComplete = [int](($numAdded / $totalZones) * 100) 393 | Write-Progress -Activity "Adding adserver DNS zones" -Status "$($percentComplete)% Complete:" -PercentComplete $percentComplete 394 | } 395 | 396 | if ($useActiveDirectory) 397 | { 398 | # Create Active Directory integrated zone and add records to block 399 | # Set the responsible person field in the SOA record to our magic value for easy indentification on delete. 400 | $zone = Add-DnsServerPrimaryZone -Name $_ -ResponsiblePerson $responsiblePerson -ReplicationScope Forest -PassThru 401 | $zone | Add-DnsServerResourceRecordA -IPv4Address 0.0.0.0 -Name '*' -TimeToLive 01:00:00 402 | $zone | Add-DnsServerResourceRecordA -IPv4Address 0.0.0.0 -Name '@' -TimeToLive 01:00:00 403 | } 404 | else 405 | { 406 | # Create zone and point to our zone file 407 | Add-DnsServerPrimaryZone -Name $_ -ZoneFile $adServerZoneFile -LoadExisting -ResponsiblePerson $responsiblePerson 408 | } 409 | 410 | ++$numAdded 411 | } 412 | 413 | # Clear progress bar 414 | Write-Progress -Activity "Adding adserver DNS zones" -Status "100% Complete:" -PercentComplete 100 -Completed 415 | Write-Host "Imported $numAdded zones from AdBlock file" 416 | } 417 | else 418 | { 419 | # Importing the file into regedit 420 | "REGEDIT4`n" | Insert-Content $blocklist 421 | regedit.exe /s $blocklist 422 | Write-Host "Imported AdBlock file" 423 | } 424 | } 425 | 426 | if ((Get-Service -Name DNS).Status -ne 'Running') 427 | { 428 | # Starting the DNS Server 429 | Write-Host "Starting DNS Server..." 430 | Start-Service -Name DNS 431 | Write-Host "Started DNS Server" 432 | } 433 | 434 | #Removing Blocklist files older then 15 days 435 | Write-Host "Removing old AdBlock files..." 436 | Get-ChildItem -Path $artifactPath -Recurse -Force | Where-Object { -not $_.PSIsContainer -and $_.CreationTime -lt $limit } | Remove-Item -Force 437 | Write-Host "Removed old AdBlock files" 438 | 439 | #Script has been completed 440 | Write-Host "Script has been completed" 441 | } 442 | catch 443 | { 444 | Write-Host -ForegroundColor Red $_.Exception.Message 445 | exit 1 446 | } 447 | -------------------------------------------------------------------------------- /adservers.dns: -------------------------------------------------------------------------------- 1 | ; NULL Zone File for Ad Servers 2 | ; 3 | 4 | @ IN SOA ns.localhost. admin.localhost. ( 5 | 131 ; serial number 6 | 28800 ; refresh 7 | 1800 ; retry 8 | 432000 ; expire 9 | 18000 ) ; minimum TTL 10 | 11 | ; 12 | ; Zone NS records 13 | ; 14 | 15 | @ NS ns.localhost 16 | 17 | A 0.0.0.0 18 | * IN A 0.0.0.0 19 | --------------------------------------------------------------------------------