diff --git a/PiHoleShell/PiHoleShell.psd1 b/PiHoleShell/PiHoleShell.psd1 index 9cfbe97..bc9c9cb 100644 Binary files a/PiHoleShell/PiHoleShell.psd1 and b/PiHoleShell/PiHoleShell.psd1 differ diff --git a/PiHoleShell/PiHoleShell.psm1 b/PiHoleShell/PiHoleShell.psm1 index 72c39c0..d73e2ab 100644 --- a/PiHoleShell/PiHoleShell.psm1 +++ b/PiHoleShell/PiHoleShell.psm1 @@ -22,7 +22,7 @@ Export-ModuleMember -Function @( #GroupManagement 'Get-PiHoleGroup', 'New-PiHoleGroup', 'Update-PiHoleGroup', 'Remove-PiHoleGroup', ` #DnsControl - 'Get-PiHoleDnsBlockingStatus', 'Set-PiHoleDnsBlocking', ` + 'Get-PiHoleDnsBlockingStatus', 'Set-PiHoleDnsBlocking', 'Add-PiHoleDnsRecord', 'Remove-PiHoleDnsRecord', 'Get-PiHoleDnsRecord', ` #Config 'Get-PiHoleConfig', 'Set-PiHoleConfig', 'Add-PiHoleConfigArrayItem', 'Remove-PiHoleConfigArrayItem', 'Get-PiHoleConfigProperty', ` #Padd diff --git a/PiHoleShell/Public/DnsControl/Add-PiHoleDnsRecord.ps1 b/PiHoleShell/Public/DnsControl/Add-PiHoleDnsRecord.ps1 new file mode 100644 index 0000000..d69dba1 --- /dev/null +++ b/PiHoleShell/Public/DnsControl/Add-PiHoleDnsRecord.ps1 @@ -0,0 +1,147 @@ +function Add-PiHoleDnsRecord { + <# +.SYNOPSIS +Add a local DNS record + +.DESCRIPTION +Adds a local DNS record to Pi-hole: an A record (IPv4), AAAA record (IPv6), or CNAME record. +This is a DNS-specific convenience wrapper around Add-PiHoleConfigArrayItem, which it calls +internally - using that directly is equivalent, just less discoverable for this specific case. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER Type +The kind of record to add: A (IPv4), AAAA (IPv6), or CNAME + +.PARAMETER Hostname +The hostname to add. For -Type A or AAAA, this is the name that resolves to the given address. +For -Type CNAME, this is the alias being created + +.PARAMETER IPv4Address +The IPv4 address -Hostname should resolve to. Required when -Type is "A" + +.PARAMETER IPv6Address +The IPv6 address -Hostname should resolve to. Required when -Type is "AAAA" + +.PARAMETER Target +The hostname -Hostname should be an alias for. Required when -Type is "CNAME" + +.PARAMETER Ttl +Optional TTL (in seconds) for a CNAME record. Only used when -Type is "CNAME" + +.PARAMETER Restart +Whether to restart FTL immediately if this change requires it. Defaults to $true. Set to +$false to defer the restart, e.g. when adding several records independently rather than all at +once - you'll need to restart FTL manually later for the changes to take effect + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Add-PiHoleDnsRecord -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Type A -Hostname "test.local" -IPv4Address "192.168.1.1" + +.EXAMPLE +Add-PiHoleDnsRecord -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Type AAAA -Hostname "test.local" -IPv6Address "::1" + +.EXAMPLE +Add-PiHoleDnsRecord -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Type CNAME -Hostname "alias.local" -Target "test.local" -Ttl 300 + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#put-/config/-element-/-value-')] + [Diagnostics.CodeAnalysis.SuppressMessage("PSUseShouldProcessForStateChangingFunctions", "", Justification = "Thin wrapper - Add-PiHoleConfigArrayItem (called internally) already carries this same suppression")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Parameter(Mandatory = $true)] + [ValidateSet('A', 'AAAA', 'CNAME')] + [string]$Type, + [Parameter(Mandatory = $true)] + [string]$Hostname, + [string]$IPv4Address, + [string]$IPv6Address, + [string]$Target, + [Nullable[int]]$Ttl, + [Nullable[bool]]$Restart, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + switch ($Type) { + 'A' { + if (-not $IPv4Address) { + throw "-IPv4Address is required when -Type is 'A'" + } + $ParsedAddress = $null + if (-not [System.Net.IPAddress]::TryParse($IPv4Address, [ref]$ParsedAddress) -or $ParsedAddress.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetwork) { + throw "'$IPv4Address' is not a valid IPv4 address" + } + $Element = 'dns/hosts' + $Value = "$IPv4Address $Hostname" + } + 'AAAA' { + if (-not $IPv6Address) { + throw "-IPv6Address is required when -Type is 'AAAA'" + } + $ParsedAddress = $null + if (-not [System.Net.IPAddress]::TryParse($IPv6Address, [ref]$ParsedAddress) -or $ParsedAddress.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetworkV6) { + throw "'$IPv6Address' is not a valid IPv6 address" + } + $Element = 'dns/hosts' + $Value = "$IPv6Address $Hostname" + } + 'CNAME' { + if (-not $Target) { + throw "-Target is required when -Type is 'CNAME'" + } + $Element = 'dns/cnameRecords' + $Value = "$Hostname,$Target" + if ($PSBoundParameters.ContainsKey('Ttl')) { + $Value += ",$Ttl" + } + } + } + + $InnerParams = @{ + PiHoleServer = $PiHoleServer + Password = $Password + Element = $Element + Value = $Value + IgnoreSsl = $IgnoreSsl + RawOutput = $RawOutput + } + if ($PSBoundParameters.ContainsKey('Restart')) { + $InnerParams.Restart = $Restart + } + + $Response = Add-PiHoleConfigArrayItem @InnerParams -ErrorAction Stop + + if ($RawOutput) { + Write-Output $Response + } + else { + $Object = [PSCustomObject]@{ + Type = $Type + Hostname = $Hostname + IPv4Address = $IPv4Address + IPv6Address = $IPv6Address + Target = $Target + Ttl = $Ttl + Status = $Response.Status + } + Write-Output $Object + } + } + + catch { + Write-Error -Message $_.Exception.Message + } +} diff --git a/PiHoleShell/Public/DnsControl/Get-PiHoleDnsRecord.ps1 b/PiHoleShell/Public/DnsControl/Get-PiHoleDnsRecord.ps1 new file mode 100644 index 0000000..0b98a36 --- /dev/null +++ b/PiHoleShell/Public/DnsControl/Get-PiHoleDnsRecord.ps1 @@ -0,0 +1,113 @@ +function Get-PiHoleDnsRecord { + <# +.SYNOPSIS +Get local DNS records + +.DESCRIPTION +Returns Pi-hole's local DNS records (A, AAAA, and CNAME) as a flat list of objects shaped the +same way as Add-PiHoleDnsRecord/Remove-PiHoleDnsRecord, so the result can be piped straight into +Remove-PiHoleDnsRecord. This is a DNS-specific convenience wrapper around Get-PiHoleConfig, +which it calls internally - using that directly (.Dns.Hosts / .Dns.CnameRecords) is equivalent, +just not already split into A/AAAA/CNAME or filterable by -Type/-Hostname. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER Type +Only return records of this kind: A (IPv4), AAAA (IPv6), or CNAME. If omitted, all are returned + +.PARAMETER Hostname +Only return the record(s) for this exact hostname (the alias, for a CNAME). If omitted, every +record is returned + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Get-PiHoleDnsRecord -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" + +.EXAMPLE +Get-PiHoleDnsRecord -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Type CNAME + +.EXAMPLE +Get-PiHoleDnsRecord -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Hostname "test.local" | Remove-PiHoleDnsRecord -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/config')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [ValidateSet('A', 'AAAA', 'CNAME')] + [string]$Type, + [string]$Hostname, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Config = Get-PiHoleConfig -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl -ErrorAction Stop + + if ($RawOutput) { + $Object = [PSCustomObject]@{ + Hosts = $Config.Dns.Hosts + CnameRecords = $Config.Dns.CnameRecords + } + Write-Output $Object + } + else { + $Records = foreach ($Entry in $Config.Dns.Hosts) { + # Each entry is " " - hostnames never contain spaces, so splitting + # into at most 2 parts on the first space is safe even if an IPv6 address itself + # cannot contain one. + $Parts = $Entry.Split(' ', 2) + $Address = $Parts[0] + $RecordHostname = $Parts[1] + + $ParsedAddress = $null + $IsIPv6 = [System.Net.IPAddress]::TryParse($Address, [ref]$ParsedAddress) -and $ParsedAddress.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6 + + [PSCustomObject]@{ + Type = if ($IsIPv6) { 'AAAA' } else { 'A' } + Hostname = $RecordHostname + IPv4Address = if ($IsIPv6) { $null } else { $Address } + IPv6Address = if ($IsIPv6) { $Address } else { $null } + Target = $null + Ttl = $null + } + } + + $Records += foreach ($Entry in $Config.Dns.CnameRecords) { + # Each entry is ",[,]" - the TTL segment is optional. + $Parts = $Entry.Split(',') + [PSCustomObject]@{ + Type = 'CNAME' + Hostname = $Parts[0] + IPv4Address = $null + IPv6Address = $null + Target = $Parts[1] + Ttl = if ($Parts.Count -gt 2) { [int]$Parts[2] } else { $null } + } + } + + if ($PSBoundParameters.ContainsKey('Type')) { + $Records = $Records | Where-Object { $_.Type -eq $Type } + } + if ($PSBoundParameters.ContainsKey('Hostname')) { + $Records = $Records | Where-Object { $_.Hostname -eq $Hostname } + } + + Write-Output $Records + } + } + + catch { + Write-Error -Message $_.Exception.Message + } +} diff --git a/PiHoleShell/Public/DnsControl/Remove-PiHoleDnsRecord.ps1 b/PiHoleShell/Public/DnsControl/Remove-PiHoleDnsRecord.ps1 new file mode 100644 index 0000000..e031278 --- /dev/null +++ b/PiHoleShell/Public/DnsControl/Remove-PiHoleDnsRecord.ps1 @@ -0,0 +1,155 @@ +function Remove-PiHoleDnsRecord { + <# +.SYNOPSIS +Remove a local DNS record + +.DESCRIPTION +Removes a local DNS record from Pi-hole: an A record (IPv4), AAAA record (IPv6), or CNAME +record. This is a DNS-specific convenience wrapper around Remove-PiHoleConfigArrayItem, which +it calls internally - using that directly is equivalent, just less discoverable for this +specific case. + +The values given must match the record exactly as it was created (see Get-PiHoleDnsRecord, or +Get-PiHoleConfig under .Dns.Hosts/.Dns.CnameRecords, to check the exact value if removal fails) - +for a CNAME record created with a TTL, -Ttl must be passed here too, with the same value. + +Accepts pipeline input by property name, so it can be chained directly from Get-PiHoleDnsRecord. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER Type +The kind of record to remove: A (IPv4), AAAA (IPv6), or CNAME + +.PARAMETER Hostname +The hostname of the record to remove. For -Type A or AAAA, this is the name that resolves to +the given address. For -Type CNAME, this is the alias being removed + +.PARAMETER IPv4Address +The IPv4 address the record resolves to. Required when -Type is "A" + +.PARAMETER IPv6Address +The IPv6 address the record resolves to. Required when -Type is "AAAA" + +.PARAMETER Target +The hostname the record is an alias for. Required when -Type is "CNAME" + +.PARAMETER Ttl +The TTL (in seconds) the CNAME record was created with, if any. Only used when -Type is +"CNAME" - must match exactly or the removal will not find the record + +.PARAMETER Restart +Whether to restart FTL immediately if this change requires it. Defaults to $true. Set to +$false to defer the restart, e.g. when removing several records independently rather than all +at once - you'll need to restart FTL manually later for the changes to take effect + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Remove-PiHoleDnsRecord -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Type A -Hostname "test.local" -IPv4Address "192.168.1.1" + +.EXAMPLE +Remove-PiHoleDnsRecord -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Type CNAME -Hostname "alias.local" -Target "test.local" -Ttl 300 + +.EXAMPLE +Get-PiHoleDnsRecord -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Hostname "test.local" | Remove-PiHoleDnsRecord -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" + #> + [CmdletBinding(SupportsShouldProcess = $true, HelpUri = 'https://ftl.pi-hole.net/master/docs/#delete-/config/-element-/-value-')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [ValidateSet('A', 'AAAA', 'CNAME')] + [string]$Type, + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [string]$Hostname, + [Parameter(ValueFromPipelineByPropertyName = $true)] + [string]$IPv4Address, + [Parameter(ValueFromPipelineByPropertyName = $true)] + [string]$IPv6Address, + [Parameter(ValueFromPipelineByPropertyName = $true)] + [string]$Target, + [Parameter(ValueFromPipelineByPropertyName = $true)] + [Nullable[int]]$Ttl, + [Nullable[bool]]$Restart, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + process { + try { + switch ($Type) { + 'A' { + if (-not $IPv4Address) { + throw "-IPv4Address is required when -Type is 'A'" + } + $Element = 'dns/hosts' + $Value = "$IPv4Address $Hostname" + } + 'AAAA' { + if (-not $IPv6Address) { + throw "-IPv6Address is required when -Type is 'AAAA'" + } + $Element = 'dns/hosts' + $Value = "$IPv6Address $Hostname" + } + 'CNAME' { + if (-not $Target) { + throw "-Target is required when -Type is 'CNAME'" + } + $Element = 'dns/cnameRecords' + $Value = "$Hostname,$Target" + if ($PSBoundParameters.ContainsKey('Ttl')) { + $Value += ",$Ttl" + } + } + } + + if ($PSCmdlet.ShouldProcess("Pi-Hole DNS record '$Value' ($Type)", "Remove DNS record")) { + $InnerParams = @{ + PiHoleServer = $PiHoleServer + Password = $Password + Element = $Element + Value = $Value + IgnoreSsl = $IgnoreSsl + RawOutput = $RawOutput + Confirm = $false + } + if ($PSBoundParameters.ContainsKey('Restart')) { + $InnerParams.Restart = $Restart + } + + $Response = Remove-PiHoleConfigArrayItem @InnerParams -ErrorAction Stop + + if ($RawOutput) { + Write-Output $Response + } + else { + $Object = [PSCustomObject]@{ + Type = $Type + Hostname = $Hostname + IPv4Address = $IPv4Address + IPv6Address = $IPv6Address + Target = $Target + Ttl = $Ttl + Status = $Response.Status + } + Write-Output $Object + } + } + } + + catch { + Write-Error -Message $_.Exception.Message + } + } +} diff --git a/README.md b/README.md index e4dac0d..311be4e 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,10 @@ See [docs/EXAMPLES.md](docs/EXAMPLES.md) for real, captured output from every fu | Function | Description | |---|---| +| `Add-PiHoleDnsRecord` | Add a local DNS record | | `Get-PiHoleDnsBlockingStatus` | Get Pi-hole's current DNS blocking status | +| `Get-PiHoleDnsRecord` | Get local DNS records | +| `Remove-PiHoleDnsRecord` | Remove a local DNS record | | `Set-PiHoleDnsBlocking` | Enable or disable Pi-hole's DNS blocking | ### Group Management diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index 0602c4e..4faea3f 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -86,6 +86,58 @@ Set-PiHoleDnsBlocking -PiHoleServer $PiHoleServer -Password $Password -Blocking _Not captured this run - re-run with -IncludeDisruptive to include a live example._ ``` +### Add-PiHoleDnsRecord + +```powershell +Add-PiHoleDnsRecord -PiHoleServer $PiHoleServer -Password $Password -Type A -Hostname "piholeshell-docs-dns-example.com" -IPv4Address "192.0.2.2" +``` + +``` + +Type : A +Hostname : piholeshell-docs-dns-example.com +IPv4Address : 192.0.2.2 +IPv6Address : +Target : +Ttl : +Status : Added +``` + +### Get-PiHoleDnsRecord + +_Can be filtered with -Type and/or -Hostname, and piped directly into Remove-PiHoleDnsRecord._ + +```powershell +Get-PiHoleDnsRecord -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Type : A +Hostname : piholeshell-docs-dns-example.com +IPv4Address : 192.0.2.2 +IPv6Address : +Target : +Ttl : +``` + +### Remove-PiHoleDnsRecord + +```powershell +Remove-PiHoleDnsRecord -PiHoleServer $PiHoleServer -Password $Password -Type A -Hostname "piholeshell-docs-dns-example.com" -IPv4Address "192.0.2.2" +``` + +``` + +Type : A +Hostname : piholeshell-docs-dns-example.com +IPv4Address : 192.0.2.2 +IPv6Address : +Target : +Ttl : +Status : Removed +``` + ## Group Management ### New-PiHoleGroup @@ -99,9 +151,9 @@ New-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -GroupName "PiHo Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : True -Id : 63 -DateAdded : 10/6/2026 10:06:30 PM -DateModified : 10/6/2026 10:06:30 PM +Id : 48 +DateAdded : 10/12/2026 6:38:10 PM +DateModified : 10/12/2026 6:38:10 PM ``` ### Get-PiHoleGroup @@ -143,9 +195,9 @@ DateModified : 9/30/2026 8:48:26 AM Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : True -Id : 63 -DateAdded : 10/6/2026 10:06:30 PM -DateModified : 10/6/2026 10:06:30 PM +Id : 48 +DateAdded : 10/12/2026 6:38:10 PM +DateModified : 10/12/2026 6:38:10 PM ``` ### Update-PiHoleGroup @@ -159,9 +211,9 @@ Update-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -GroupName "P Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : False -Id : 63 -DateAdded : 10/6/2026 10:06:30 PM -DateModified : 10/6/2026 10:06:48 PM +Id : 48 +DateAdded : 10/12/2026 6:38:10 PM +DateModified : 10/12/2026 6:38:42 PM ``` ### Remove-PiHoleGroup @@ -190,9 +242,9 @@ Address : https://blocklistproject.github.io/Lists/alt-version/ransomware Comment : Example list Groups : {Default} Enabled : True -Id : 81 -DateAdded : 10/6/2026 10:07:25 PM -DateModified : 10/6/2026 10:07:25 PM +Id : 67 +DateAdded : 10/12/2026 6:39:53 PM +DateModified : 10/12/2026 6:39:53 PM Type : Block DateUpdated : Number : 0 @@ -218,7 +270,7 @@ DateAdded : 7/6/2025 2:20:13 AM DateModified : 7/6/2025 2:20:13 AM Type : block DateUpdated : 7/6/2025 2:20:13 AM -Number : 72234 +Number : 72526 InvalidDomains : 1 AbpEntries : 0 Status : 2 @@ -246,7 +298,7 @@ DateAdded : 7/6/2025 2:20:46 AM DateModified : 7/6/2025 2:20:46 AM Type : block DateUpdated : 7/6/2025 2:20:46 AM -Number : 176582 +Number : 178722 InvalidDomains : 0 AbpEntries : 0 Status : 2 @@ -294,9 +346,9 @@ Address : https://blocklistproject.github.io/Lists/alt-version/ransomware Comment : Example list Groups : {Default} Enabled : False -Id : 81 -DateAdded : 10/6/2026 10:07:25 PM -DateModified : 10/6/2026 10:07:58 PM +Id : 67 +DateAdded : 10/12/2026 6:39:53 PM +DateModified : 10/12/2026 6:40:54 PM Type : Block DateUpdated : Number : 0 @@ -322,8 +374,8 @@ Type : block Groups : {0} DateAdded : 7/6/2025 2:20:13 AM DateModified : 7/6/2025 2:20:13 AM -DateUpdated : 10/2/2026 9:10:24 PM -Number : 72234 +DateUpdated : 10/7/2026 4:41:49 PM +Number : 72526 InvalidDomains : 1 AbpEntries : 0 Status : 2 @@ -352,8 +404,8 @@ Type : block Groups : {0} DateAdded : 7/6/2025 2:20:46 AM DateModified : 7/6/2025 2:20:46 AM -DateUpdated : 10/2/2026 9:43:40 AM -Number : 176582 +DateUpdated : 10/10/2026 7:37:55 PM +Number : 178722 InvalidDomains : 0 AbpEntries : 0 Status : 2 @@ -422,8 +474,8 @@ Comment : Example domain Groups : {Default} Enabled : True Id : 18 -DateAdded : 10/6/2026 10:08:36 PM -DateModified : 10/6/2026 10:08:36 PM +DateAdded : 10/12/2026 6:42:07 PM +DateModified : 10/12/2026 6:42:07 PM ``` ### Get-PiHoleDomain @@ -442,8 +494,8 @@ Comment : Example domain Groups : {Default} Enabled : True Id : 18 -DateAdded : 10/6/2026 10:08:36 PM -DateModified : 10/6/2026 10:08:36 PM +DateAdded : 10/12/2026 6:42:07 PM +DateModified : 10/12/2026 6:42:07 PM ``` ### Update-PiHoleDomain @@ -462,8 +514,8 @@ Comment : Example domain Groups : {Default} Enabled : False Id : 18 -DateAdded : 10/6/2026 10:08:36 PM -DateModified : 10/6/2026 10:09:10 PM +DateAdded : 10/12/2026 6:42:07 PM +DateModified : 10/12/2026 6:43:09 PM ``` ### Remove-PiHoleDomain @@ -495,8 +547,8 @@ Name : Comment : Example client Groups : {Default} Id : 20 -DateAdded : 10/6/2026 10:09:40 PM -DateModified : 10/6/2026 10:09:40 PM +DateAdded : 10/12/2026 6:44:07 PM +DateModified : 10/12/2026 6:44:07 PM ``` ### Get-PiHoleClient @@ -512,8 +564,8 @@ Name : Comment : Example client Groups : {Default} Id : 20 -DateAdded : 10/6/2026 10:09:40 PM -DateModified : 10/6/2026 10:09:40 PM +DateAdded : 10/12/2026 6:44:07 PM +DateModified : 10/12/2026 6:44:07 PM ``` ### Get-PiHoleClientSuggestion @@ -526,13 +578,13 @@ Get-PiHoleClientSuggestion -PiHoleServer $PiHoleServer -Password $Password HwAddr : ip-127.0.0.1 MacVendor : -LastQuery : 10/6/2026 10:06:14 PM +LastQuery : 10/12/2026 6:37:42 PM Addresses : 127.0.0.1 Names : localhost HwAddr : 74:56:3c:bb:f4:0b MacVendor : Giga-Byte Technology Co.,Ltd. -LastQuery : 10/6/2026 10:03:16 PM +LastQuery : 10/12/2026 6:31:26 PM Addresses : 192.168.1.162 Names : ``` @@ -550,8 +602,8 @@ Name : Comment : Updated comment Groups : {Default} Id : 20 -DateAdded : 10/6/2026 10:09:40 PM -DateModified : 10/6/2026 10:10:18 PM +DateAdded : 10/12/2026 6:44:07 PM +DateModified : 10/12/2026 6:45:19 PM ``` ### Remove-PiHoleClient @@ -576,18 +628,18 @@ Get-PiHoleStatsSummary -PiHoleServer $PiHoleServer -Password $Password ``` -Total : 72 -Blocked : 8 -PercentBlocked : 11.1111106872559 +Total : 41 +Blocked : 9 +PercentBlocked : 21.9512195587158 UniqueDomains : 16 -Forwarded : 27 -Cached : 37 +Forwarded : 24 +Cached : 8 Frequency : 0 -Types : @{A=18; AAAA=17; ANY=0; SRV=0; SOA=0; PTR=37; TXT=0; NAPTR=0; MX=0; DS=0; RRSIG=0; DNSKEY=0; NS=0; SVCB=0; HTTPS=0; OTHER=0} -Status : @{Unknown=0; Gravity=8; Forwarded=27; Cache=37; Regex=0; DenyList=0; ExternalBlockedIp=0; ExternalBlockedNull=0; ExternalBlockedNxra=0; GravityCname=0; RegexCname=0; DenyListCname=0; Retired=0; RetiredDnssec=0; InProgress=0; Dbbusy=0; SpecialDomain=0; CacheStale=0; ExternalBlockedEde15=0} -Replies : @{Unknown=7; Nodata=3; Nxdomain=2; Cname=0; Ip=25; Domain=35; Rrname=0; ServFail=0; Refused=0; Notimp=0; Other=0; Dnssec=0; None=0; Blob=0} +Types : @{A=16; AAAA=17; ANY=0; SRV=0; SOA=0; PTR=8; TXT=0; NAPTR=0; MX=0; DS=0; RRSIG=0; DNSKEY=0; NS=0; SVCB=0; HTTPS=0; OTHER=0} +Status : @{Unknown=0; Gravity=9; Forwarded=24; Cache=8; Regex=0; DenyList=0; ExternalBlockedIp=0; ExternalBlockedNull=0; ExternalBlockedNxra=0; GravityCname=0; RegexCname=0; DenyListCname=0; Retired=0; RetiredDnssec=0; InProgress=0; Dbbusy=0; SpecialDomain=0; CacheStale=0; ExternalBlockedEde15=0} +Replies : @{Unknown=4; Nodata=3; Nxdomain=2; Cname=0; Ip=26; Domain=6; Rrname=0; ServFail=0; Refused=0; Notimp=0; Other=0; Dnssec=0; None=0; Blob=0} Clients : @{Active=2; Total=2} -Gravity : @{DomainsBeingBlocked=526957; LastUpdate=1791140144} +Gravity : @{DomainsBeingBlocked=560453; LastUpdate=1791845621} ``` ### Get-PiHoleStatsRecentBlocked @@ -598,7 +650,7 @@ Get-PiHoleStatsRecentBlocked -PiHoleServer $PiHoleServer -Password $Password ``` -Blocked : googlesyndication.com +Blocked : googleadservices.com ``` ### Get-PiHoleStatsQueryType @@ -610,7 +662,7 @@ Get-PiHoleStatsQueryType -PiHoleServer $PiHoleServer -Password $Password ``` Type : A -Count : 18 +Count : 16 Type : AAAA Count : 17 @@ -635,20 +687,20 @@ Get-PiHoleStatsTopDomain -PiHoleServer $PiHoleServer -Password $Password ``` -Domain : 1.0.0.127.in-addr.arpa -Count : 31 +Domain : wikipedia.org +Count : 4 Domain : bbc.com Count : 4 -Domain : example.com -Count : 4 +Domain : mozilla.org +Count : 3 -Domain : python.org -Count : 4 +Domain : apple.com +Count : 3 -Domain : wikipedia.org -Count : 4 +Domain : github.com +Count : 3 _(showing 5 of 10 results)_ ``` @@ -661,13 +713,13 @@ Get-PiHoleStatsTopClient -PiHoleServer $PiHoleServer -Password $Password ``` -IP : 127.0.0.1 -Name : localhost -Count : 37 - IP : 192.168.1.162 Name : -Count : 35 +Count : 33 + +IP : 127.0.0.1 +Name : localhost +Count : 8 ``` ### Get-PiHoleStatsUpstream @@ -678,9 +730,9 @@ Get-PiHoleStatsUpstream -PiHoleServer $PiHoleServer -Password $Password ``` -TotalQueries : 72 -ForwardedQueries : 27 -Upstreams : {@{Ip=blocklist; Name=blocklist; Port=-1; Count=8; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=37; ResponseTime=0; Variance=0}, @{Ip=8.8.4.4; Name=dns.google; Port=53; Count=23; ResponseTime=0; Variance=0}, @{Ip=8.8.8.8; Name=dns.google; Port=53; Count=4; ResponseTime=0; Variance=0}} +TotalQueries : 41 +ForwardedQueries : 24 +Upstreams : {@{Ip=blocklist; Name=blocklist; Port=-1; Count=9; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=8; ResponseTime=0; Variance=0}, @{Ip=8.8.4.4; Name=dns.google; Port=53; Count=22; ResponseTime=0; Variance=0}, @{Ip=8.8.8.8; Name=dns.google; Port=53; Count=2; ResponseTime=0; Variance=0}} ``` ### Get-PiHoleStatsQuerySuggestions @@ -691,8 +743,8 @@ Get-PiHoleStatsQuerySuggestions -PiHoleServer $PiHoleServer -Password $Password ``` -Domain : {1.0.0.127.in-addr.arpa, bbc.com, example.com, python.org…} -ClientIp : {127.0.0.1, 192.168.1.162} +Domain : {wikipedia.org, bbc.com, mozilla.org, apple.com…} +ClientIp : {192.168.1.162, 127.0.0.1} ClientName : {localhost} Upstream : {blocklist, cache, 8.8.4.4#53 (dns.google), 8.8.8.8#53 (dns.google)…} Type : {A, AAAA, ANY, SRV…} @@ -711,10 +763,10 @@ Get-PiHoleStatsDatabaseSummary -PiHoleServer $PiHoleServer -Password $Password ``` -SumQueries : 56 -SumBlocked : 8 -PercentBlocked : 14.2857141494751 -TotalClients : 2 +SumQueries : 41 +SumBlocked : 9 +PercentBlocked : 21.9512195587158 +TotalClients : 3 ``` ### Get-PiHoleStatsDatabaseQueryType @@ -740,7 +792,7 @@ Type : SRV Count : 0 Type : SOA -Count : 21 +Count : 8 _(showing 5 of 16 results)_ ``` @@ -755,20 +807,20 @@ Get-PiHoleStatsDatabaseTopDomain -PiHoleServer $PiHoleServer -Password $Password ``` -Domain : 1.0.0.127.in-addr.arpa -Count : 15 - -Domain : python.org -Count : 4 - Domain : bbc.com Count : 4 Domain : wikipedia.org Count : 4 -Domain : example.com -Count : 4 +Domain : mozilla.org +Count : 3 + +Domain : stackoverflow.com +Count : 3 + +Domain : apple.com +Count : 3 _(showing 5 of 10 results)_ ``` @@ -785,11 +837,15 @@ Get-PiHoleStatsDatabaseTopClient -PiHoleServer $PiHoleServer -Password $Password IP : 192.168.1.162 Name : -Count : 27 +Count : 24 IP : 127.0.0.1 Name : localhost -Count : 21 +Count : 5 + +IP : 127.0.0.1 +Name : +Count : 3 ``` ### Get-PiHoleStatsDatabaseUpstream @@ -802,9 +858,9 @@ Get-PiHoleStatsDatabaseUpstream -PiHoleServer $PiHoleServer -Password $Password ``` -TotalQueries : 56 -ForwardedQueries : 27 -Upstreams : {@{Ip=1; Name=; Port=-1; Count=23; ResponseTime=0; Variance=0}, @{Ip=2; Name=; Port=-1; Count=4; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=21; ResponseTime=0; Variance=0}, @{Ip=blocklist; Name=blocklist; Port=-1; Count=8; ResponseTime=0; Variance=0}} +TotalQueries : 41 +ForwardedQueries : 24 +Upstreams : {@{Ip=1; Name=; Port=-1; Count=22; ResponseTime=0; Variance=0}, @{Ip=2; Name=; Port=-1; Count=2; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=8; ResponseTime=0; Variance=0}, @{Ip=blocklist; Name=blocklist; Port=-1; Count=9; ResponseTime=0; Variance=0}} ``` ### Get-PiHoleQuery @@ -817,69 +873,69 @@ Get-PiHoleQuery -PiHoleServer $PiHoleServer -Password $Password -Length 5 ``` -Id : 394 -Time : 10/6/2026 10:06:15 PM +Id : 40 +Time : 10/12/2026 6:37:42 PM Type : PTR -Domain : 4.4.8.8.in-addr.arpa +Domain : 8.8.8.8.in-addr.arpa Cname : Status : FORWARDED Client : @{Ip=127.0.0.1; Name=localhost} Dnssec : UNKNOWN -Reply : @{Type=DOMAIN; Time=0.0241849422454834} +Reply : @{Type=DOMAIN; Time=0.0698070526123047} ListId : Upstream : 8.8.8.8#53 Ede : @{Code=-1; Text=} -Id : 393 -Time : 10/6/2026 10:06:15 PM +Id : 39 +Time : 10/12/2026 6:37:41 PM Type : PTR -Domain : 8.8.8.8.in-addr.arpa +Domain : 4.4.8.8.in-addr.arpa Cname : Status : FORWARDED Client : @{Ip=127.0.0.1; Name=localhost} Dnssec : UNKNOWN -Reply : @{Type=DOMAIN; Time=0.0249054431915283} +Reply : @{Type=DOMAIN; Time=0.0369069576263428} ListId : Upstream : 8.8.8.8#53 Ede : @{Code=-1; Text=} -Id : 392 -Time : 10/6/2026 10:06:15 PM +Id : 38 +Time : 10/12/2026 6:37:41 PM Type : PTR -Domain : 162.1.168.192.in-addr.arpa +Domain : 1.0.0.127.in-addr.arpa Cname : Status : CACHE Client : @{Ip=127.0.0.1; Name=localhost} Dnssec : UNKNOWN -Reply : @{Type=NXDOMAIN; Time=0.0157008171081543} +Reply : @{Type=DOMAIN; Time=0.00117111206054688} ListId : Upstream : Ede : @{Code=-1; Text=} -Id : 391 -Time : 10/6/2026 10:06:15 PM +Id : 37 +Time : 10/12/2026 6:37:41 PM Type : PTR -Domain : 1.0.0.127.in-addr.arpa +Domain : 162.1.168.192.in-addr.arpa Cname : Status : CACHE Client : @{Ip=127.0.0.1; Name=localhost} Dnssec : UNKNOWN -Reply : @{Type=DOMAIN; Time=0.0260829925537109} +Reply : @{Type=NXDOMAIN; Time=0.0176899433135986} ListId : Upstream : Ede : @{Code=-1; Text=} -Id : 390 -Time : 10/6/2026 10:03:17 PM +Id : 36 +Time : 10/12/2026 6:31:28 PM Type : PTR -Domain : 4.4.8.8.in-addr.arpa +Domain : 1.0.0.127.in-addr.arpa Cname : -Status : FORWARDED +Status : CACHE Client : @{Ip=127.0.0.1; Name=localhost} Dnssec : UNKNOWN -Reply : @{Type=DOMAIN; Time=0.0180368423461914} +Reply : @{Type=DOMAIN; Time=0.00136184692382812} ListId : -Upstream : 8.8.4.4#53 +Upstream : Ede : @{Code=-1; Text=} ``` @@ -992,20 +1048,20 @@ CpuPercent : 0 MemoryPercent : 0 ActiveClients : 2 Blocking : enabled -Cache : @{Size=10000; Inserted=34; Evicted=0} +Cache : @{Size=10000; Inserted=30; Evicted=0} Config : @{DhcpActive=False; DhcpStart=; DhcpEnd=; DhcpIpv6=False; DnsDnssec=False; DnsDomain=lan; DnsNumUpstreams=2; DnsPort=53; DnsrevServerAactive=False; PrivacyLevel=0} -GravitySize : 526957 +GravitySize : 560453 HostModel : Raspberry Pi Zero W Rev 1.1 IFace : @{v4=; v6=} NodeName : dns3.localdomain -Pid : 389 -Queries : @{Total=69; Blocked=8; PercentBlocked=11.5942029953003; QueryFrequency=0} -RecentBlocked : googlesyndication.com -Sensors : @{CpuTemp=40.622; HotLimit=60; Unit=C} -System : @{Uptime=384699; Memory=; Procs=70; Cpu=; Ftl=} -TopBlocked : analytics.google.com +Pid : 388 +Queries : @{Total=37; Blocked=9; PercentBlocked=24.3243236541748; QueryFrequency=0} +RecentBlocked : googleadservices.com +Sensors : @{CpuTemp=43.312; HotLimit=60; Unit=C} +System : @{Uptime=156002; Memory=; Procs=74; Cpu=; Ftl=} +TopBlocked : doubleclick.net TopClient : 192.168.1.162 -TopDomain : 1.0.0.127.in-addr.arpa +TopDomain : bbc.com Version : @{Core=; Web=; Ftl=; Docker=} ``` @@ -1042,11 +1098,11 @@ Get-PiHoleInfoSystem -PiHoleServer $PiHoleServer -Password $Password ``` -Uptime : 384709 +Uptime : 156022 Memory : @{Ram=; Swap=} -Procs : 70 -Cpu : @{NumProcessors=1; PercentCpu=96.8000030517578; Load=} -Ftl : @{PercentMemory=3.02243185043335; PercentCpu=96.5} +Procs : 74 +Cpu : @{NumProcessors=1; PercentCpu=100.099998474121; Load=} +Ftl : @{PercentMemory=2.43389201164246; PercentCpu=99.3000030517578} ``` ### Get-PiHoleInfoFtl @@ -1057,16 +1113,16 @@ Get-PiHoleInfoFtl -PiHoleServer $PiHoleServer -Password $Password ``` -Database : @{Gravity=526957; Antigravity=0; Groups=4; Lists=14; Clients=0; Domains=; Regex=} +Database : @{Gravity=560453; Antigravity=0; Groups=4; Lists=14; Clients=0; Domains=; Regex=} PrivacyLevel : 0 QueryFrequency : 0 Clients : @{Total=2; Active=2} -Pid : 389 -Uptime : 528324.275473 -PercentMemory : 3.02243185043335 -PercentCpu : 96.5 +Pid : 388 +Uptime : 979769.972744 +PercentMemory : 2.42485022544861 +PercentCpu : 99.1999969482422 AllowDestructive : True -Dnsmasq : @{DnsCacheInserted=34; DnsCacheLiveFreed=0; DnsQueriesForwarded=25; DnsAuthAnswered=0; DnsLocalAnswered=15; DnsStaleAnswered=0; DnsUnanswered=0; DnssecMaxCryptoUse=0; DnssecMaxSigFail=0; DnssecMaxWork=0; Bootp=0; Pxe=0; DhcpAck=0; DhcpDecline=0; DhcpDiscover=0; DhcpInform=0; DhcpNak=0; DhcpOffer=0; DhcpRelease=0; DhcpRequest=0; Noanswer=0; LeasesAllocated4=0; LeasesPruned4=0; LeasesAllocated6=0; LeasesPruned6=0; TcpConnections=0; DhcpLeasequery=0; DhcpLeaseUnassigned=0; DhcpLeaseActve=0; DhcpLeaseUnknown=0} +Dnsmasq : @{DnsCacheInserted=30; DnsCacheLiveFreed=0; DnsQueriesForwarded=22; DnsAuthAnswered=0; DnsLocalAnswered=15; DnsStaleAnswered=0; DnsUnanswered=0; DnssecMaxCryptoUse=0; DnssecMaxSigFail=0; DnssecMaxWork=0; Bootp=0; Pxe=0; DhcpAck=0; DhcpDecline=0; DhcpDiscover=0; DhcpInform=0; DhcpNak=0; DhcpOffer=0; DhcpRelease=0; DhcpRequest=0; Noanswer=0; LeasesAllocated4=0; LeasesPruned4=0; LeasesAllocated6=0; LeasesPruned6=0; TcpConnections=0; DhcpLeasequery=0; DhcpLeaseUnassigned=0; DhcpLeaseActve=0; DhcpLeaseUnknown=0} ``` ### Get-PiHoleInfoSensors @@ -1078,7 +1134,7 @@ Get-PiHoleInfoSensors -PiHoleServer $PiHoleServer -Password $Password ``` List : {@{Name=cpu_thermal; Path=hwmon0; Source=devices/virtual/thermal/thermal_zone0; Temps=}, @{Name=rpi_volt; Path=hwmon1; Source=devices/platform/soc/soc:firmware/raspberrypi-hwmon; Temps=}} -CpuTemp : 41.16 +CpuTemp : 43.312 HotLimit : 60 Unit : C ``` @@ -1095,13 +1151,13 @@ Size : 13983744 Type : Regular file Mode : rw-r----- AccessTime : 6/25/2025 12:32:44 AM -ModifiedTime : 10/6/2026 9:56:20 PM -ChangeTime : 10/6/2026 9:56:20 PM +ModifiedTime : 10/12/2026 6:19:17 PM +ChangeTime : 10/12/2026 6:19:17 PM Owner : @{User=; Group=} -Queries : 69 -EarliestTimestamp : 10/5/2026 10:00:00 PM -QueriesDisk : 391 -EarliestTimestampDisk : 10/4/2026 2:31:09 PM +Queries : 37 +EarliestTimestamp : 10/11/2026 6:30:00 PM +QueriesDisk : 37 +EarliestTimestampDisk : SqliteVersion : 3.53.1 ``` @@ -1168,7 +1224,12 @@ Get-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $Password ``` ``` -(no output) + +Id : 1 +Timestamp : 10/12/2026 6:34:19 PM +Type : LOAD +Plain : Long-term load (15min avg) larger than number of processors: 1.9 > 1 +Html : Long-term load (15min avg) larger than number of processors: 1.9 > 1
This may slow down DNS resolution and can cause bottlenecks. ``` ### Get-PiHoleInfoMessageCount @@ -1179,7 +1240,7 @@ Get-PiHoleInfoMessageCount -PiHoleServer $PiHoleServer -Password $Password ``` -Count : 0 +Count : 1 ``` ### Remove-PiHoleInfoMessage @@ -1203,9 +1264,9 @@ Get-PiHoleLogWebserver -PiHoleServer $PiHoleServer -Password $Password ``` -Log : {@{Timestamp=10/6/2026 9:56:22 PM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=}, @{Timestamp=10/6/2026 10:04:28 PM; Message=ACCESS: 192.168.1.162 - - [07/Oct/2026:03:04:27 +0000] "PATCH /api/config?restart=false HTTP/1.1" 200 4588 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=10/6/2026 10:04:29 PM; Message=ACCESS: 192.168.1.162 - - [07/Oct/2026:03:04:28 +0000] "DELETE /api/auth HTTP/1.1" 204 558 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=10/6/2026 10:04:33 PM; Message=ACCESS: 192.168.1.162 - - [07/Oct/2026:03:04:28 +0000] "POST /api/auth HTTP/1.1" 200 808 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}} -NextID : 4 -Pid : 389 +Log : {@{Timestamp=10/12/2026 6:19:19 PM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=}, @{Timestamp=10/12/2026 6:33:39 PM; Message=ACCESS: 192.168.1.162 - - [12/Oct/2026:23:33:38 +0000] "PATCH /api/config?restart=false HTTP/1.1" 200 4589 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=10/12/2026 6:33:39 PM; Message=ACCESS: 192.168.1.162 - - [12/Oct/2026:23:33:38 +0000] "DELETE /api/auth HTTP/1.1" 204 558 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=10/12/2026 6:33:42 PM; Message=ACCESS: 192.168.1.162 - - [12/Oct/2026:23:33:33 +0000] "POST /api/auth HTTP/1.1" 200 808 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}…} +NextID : 7 +Pid : 388 File : /var/log/pihole/webserver.log ``` @@ -1217,9 +1278,9 @@ Get-PiHoleLogDnsmasq -PiHoleServer $PiHoleServer -Password $Password ``` -Log : {@{Timestamp=10/6/2026 9:56:22 PM; Message=started, version pi-hole-v2.93 cachesize 10000; Priority=}, @{Timestamp=10/6/2026 9:56:22 PM; Message=DNS service limited to local subnets; Priority=}, @{Timestamp=10/6/2026 9:56:22 PM; Message=compile time options: IPv6 GNU-getopt no-DBus no-UBus no-i18n IDN2 DHCP DHCPv6 Lua TFTP no-conntrack ipset no-nftset auth DNSSEC loop-detect inotify dumpfile; Priority=}, @{Timestamp=10/6/2026 9:56:22 PM; Message=using nameserver 8.8.8.8#53; Priority=}…} -NextID : 21 -Pid : 389 +Log : {@{Timestamp=10/12/2026 6:19:19 PM; Message=started, version pi-hole-v2.93 cachesize 10000; Priority=}, @{Timestamp=10/12/2026 6:19:19 PM; Message=DNS service limited to local subnets; Priority=}, @{Timestamp=10/12/2026 6:19:19 PM; Message=compile time options: IPv6 GNU-getopt no-DBus no-UBus no-i18n IDN2 DHCP DHCPv6 Lua TFTP no-conntrack ipset no-nftset auth DNSSEC loop-detect inotify dumpfile; Priority=}, @{Timestamp=10/12/2026 6:19:19 PM; Message=using nameserver 8.8.8.8#53; Priority=}…} +NextID : 39 +Pid : 388 File : /var/log/pihole/pihole.log ``` @@ -1231,9 +1292,9 @@ Get-PiHoleLogFtl -PiHoleServer $PiHoleServer -Password $Password ``` -Log : {@{Timestamp=10/6/2026 9:56:21 PM; Message=PID of FTL process: 389; Priority=INFO}, @{Timestamp=10/6/2026 9:56:21 PM; Message=listening on 0.0.0.0 port 53; Priority=INFO}, @{Timestamp=10/6/2026 9:56:21 PM; Message=listening on :: port 53; Priority=INFO}, @{Timestamp=10/6/2026 9:56:21 PM; Message=Database version is 22; Priority=INFO}…} -NextID : 109 -Pid : 389 +Log : {@{Timestamp=10/12/2026 6:19:18 PM; Message=PID of FTL process: 388; Priority=INFO}, @{Timestamp=10/12/2026 6:19:18 PM; Message=listening on 0.0.0.0 port 53; Priority=INFO}, @{Timestamp=10/12/2026 6:19:18 PM; Message=listening on :: port 53; Priority=INFO}, @{Timestamp=10/12/2026 6:19:18 PM; Message=Database version is 22; Priority=INFO}…} +NextID : 237 +Pid : 388 File : /var/log/pihole/FTL.log ``` @@ -1274,31 +1335,31 @@ Get-PiHoleHistory -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 10/5/2026 10:15:00 PM +Timestamp : 10/11/2026 6:45:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 10/5/2026 10:25:00 PM +Timestamp : 10/11/2026 6:55:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 10/5/2026 10:35:00 PM +Timestamp : 10/11/2026 7:05:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 10/5/2026 10:45:00 PM +Timestamp : 10/11/2026 7:15:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 10/5/2026 10:55:00 PM +Timestamp : 10/11/2026 7:25:00 PM Total : 0 Cached : 0 Blocked : 0 @@ -1315,20 +1376,20 @@ Get-PiHoleHistoryClient -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 10/5/2026 10:15:00 PM -Clients : {@{IP=127.0.0.1; Name=localhost; Count=0}, @{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} +Timestamp : 10/11/2026 6:45:00 PM +Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=127.0.0.1; Name=localhost; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 10/5/2026 10:25:00 PM -Clients : {@{IP=127.0.0.1; Name=localhost; Count=0}, @{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} +Timestamp : 10/11/2026 6:55:00 PM +Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=127.0.0.1; Name=localhost; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 10/5/2026 10:35:00 PM -Clients : {@{IP=127.0.0.1; Name=localhost; Count=0}, @{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} +Timestamp : 10/11/2026 7:05:00 PM +Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=127.0.0.1; Name=localhost; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 10/5/2026 10:45:00 PM -Clients : {@{IP=127.0.0.1; Name=localhost; Count=0}, @{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} +Timestamp : 10/11/2026 7:15:00 PM +Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=127.0.0.1; Name=localhost; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 10/5/2026 10:55:00 PM -Clients : {@{IP=127.0.0.1; Name=localhost; Count=0}, @{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} +Timestamp : 10/11/2026 7:25:00 PM +Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=127.0.0.1; Name=localhost; Count=0}, @{IP=others; Name=; Count=0}} _(showing 5 of 145 results)_ ``` @@ -1343,37 +1404,11 @@ Get-PiHoleHistoryDatabase -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 10/6/2026 3:00:00 PM -Total : 1 -Cached : 1 -Blocked : 0 -Forwarded : 0 - -Timestamp : 10/6/2026 4:00:00 PM -Total : 1 -Cached : 1 -Blocked : 0 -Forwarded : 0 - -Timestamp : 10/6/2026 5:00:00 PM -Total : 1 -Cached : 1 -Blocked : 0 -Forwarded : 0 - -Timestamp : 10/6/2026 6:00:00 PM -Total : 1 -Cached : 1 -Blocked : 0 -Forwarded : 0 - -Timestamp : 10/6/2026 7:00:00 PM -Total : 1 -Cached : 1 -Blocked : 0 -Forwarded : 0 - -_(showing 5 of 10 results)_ +Timestamp : 10/12/2026 6:30:00 PM +Total : 41 +Cached : 8 +Blocked : 9 +Forwarded : 24 ``` ### Get-PiHoleHistoryDatabaseClient @@ -1386,22 +1421,8 @@ Get-PiHoleHistoryDatabaseClient -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 10/6/2026 10:00:00 PM -Clients : {@{ClientId=5; Count=1}, @{ClientId=3; Count=1}} - -Timestamp : 10/6/2026 9:50:00 PM -Clients : @{ClientId=3; Count=1} - -Timestamp : 10/6/2026 9:40:00 PM -Clients : @{ClientId=3; Count=1} - -Timestamp : 10/6/2026 9:00:00 PM -Clients : @{ClientId=3; Count=1} - -Timestamp : 10/6/2026 8:00:00 PM -Clients : @{ClientId=3; Count=1} - -_(showing 5 of 10 results)_ +Timestamp : 10/12/2026 6:30:00 PM +Clients : {@{ClientId=3; Count=1}, @{ClientId=2; Count=1}, @{ClientId=5; Count=1}} ``` ### Get-PiHoleNetworkGateway @@ -1497,7 +1518,7 @@ ProtoDown : False Address : 00:00:00:00:00:00 Broadcast : 00:00:00:00:00:00 Stats : @{RxBytes=; TxBytes=; Bits=64} -Addresses : {@{Family=inet; Scope=host; Flags=permanent; Prefixlen=8; Address=127.0.0.1; AddressType=loopback; Local=127.0.0.1; LocalType=loopback; Label=lo; Prefered=4294967295; Valid=4294967295; Cstamp=1790957600.02; Tstamp=1790957600.02}, @{Family=inet6; Scope=host; Flags=permanent; Prefixlen=128; Address=::1; AddressType=loopback; Prefered=4294967295; Valid=4294967295; Cstamp=1790957600.02; Tstamp=1790957600.02}} +Addresses : {@{Family=inet; Scope=host; Flags=permanent; Prefixlen=8; Address=127.0.0.1; AddressType=loopback; Local=127.0.0.1; LocalType=loopback; Label=lo; Prefered=4294967295; Valid=4294967295; Cstamp=1791692112.02; Tstamp=1791692112.02}, @{Family=inet6; Scope=host; Flags=permanent; Prefixlen=128; Address=::1; AddressType=loopback; Prefered=4294967295; Valid=4294967295; Cstamp=1791692112.02; Tstamp=1791692112.02}} Name : wlan0 Speed : @@ -1510,7 +1531,7 @@ Address : b8:27:eb:11:a4:d5 Broadcast : ff:ff:ff:ff:ff:ff PermAddress : b8:27:eb:11:a4:d5 Stats : @{RxBytes=; TxBytes=; Bits=64} -Addresses : {@{Family=inet; Scope=universe; Flags=; Prefixlen=24; Address=192.168.1.248; AddressType=private; Local=192.168.1.248; LocalType=private; Broadcast=192.168.1.255; BroadcastType=private; Label=wlan0; Prefered=62449; Valid=62449; Cstamp=1790957620.44; Tstamp=1791318773.93}, @{Family=inet6; Scope=link; Flags=permanent; Prefixlen=64; Address=fe80::ba27:ebff:fe11:a4d5; AddressType=link-local (LL); Prefered=4294967295; Valid=4294967295; Cstamp=1790957619.8; Tstamp=1790957619.8}} +Addresses : {@{Family=inet; Scope=universe; Flags=; Prefixlen=24; Address=192.168.1.248; AddressType=private; Local=192.168.1.248; LocalType=private; Broadcast=192.168.1.255; BroadcastType=private; Label=wlan0; Prefered=70695; Valid=70695; Cstamp=1791692138.78; Tstamp=1791833220.34}, @{Family=inet6; Scope=link; Flags=permanent; Prefixlen=64; Address=fe80::ba27:ebff:fe11:a4d5; AddressType=link-local (LL); Prefered=4294967295; Valid=4294967295; Cstamp=1791692131.87; Tstamp=1791692131.87}} ``` ### Get-PiHoleNetworkDevice @@ -1526,20 +1547,20 @@ Get-PiHoleNetworkDevice -PiHoleServer $PiHoleServer -Password $Password Id : 2 HwAddr : ip-127.0.0.1 Interface : lo -FirstSeen : 10/4/2026 2:31:10 PM -LastQuery : 10/6/2026 10:06:14 PM -NumQueries : 82 +FirstSeen : 10/12/2026 6:31:26 PM +LastQuery : 10/12/2026 6:37:42 PM +NumQueries : 9 MacVendor : -Ips : @{Ip=127.0.0.1; Name=localhost; LastSeen=10/6/2026 10:12:00 PM; NameUpdated=10/6/2026 10:12:00 PM} +Ips : @{Ip=127.0.0.1; Name=localhost; LastSeen=10/12/2026 6:48:00 PM; NameUpdated=10/12/2026 6:48:00 PM} Id : 1 HwAddr : 74:56:3c:bb:f4:0b Interface : wlan0 -FirstSeen : 10/4/2026 2:31:09 PM -LastQuery : 10/6/2026 10:03:16 PM -NumQueries : 318 +FirstSeen : 10/12/2026 6:31:26 PM +LastQuery : 10/12/2026 6:31:26 PM +NumQueries : 34 MacVendor : Giga-Byte Technology Co.,Ltd. -Ips : @{Ip=192.168.1.162; Name=; LastSeen=10/6/2026 10:12:00 PM; NameUpdated=12/31/1969 6:00:00 PM} +Ips : @{Ip=192.168.1.162; Name=; LastSeen=10/12/2026 6:48:00 PM; NameUpdated=12/31/1969 6:00:00 PM} ``` ### Remove-PiHoleNetworkDevice @@ -1592,8 +1613,8 @@ Get-PiHoleAuthStatus -PiHoleServer $PiHoleServer -Password $Password Valid : True Totp : False -Sid : +dfslUHafhrdo0TwRT5nxQ= -Csrf : Em6pqkbJ6cuWxjQw7EtUFw= +Sid : zH7Z6NLKzAHxBNREdG6N5g= +Csrf : CgJVzV6Gp2nM1JY5ibWxpQ= Validity : 1800 Message : correct password ``` @@ -1613,8 +1634,8 @@ Algorithm : SHA1 Digits : 6 Period : 30 Offset : 0 -Secret : EG7DLOTZKTQWIRWBFR2JJLS5Q3BYNUN3 -Codes : {201435, 619358, 736784, 226002…} +Secret : NQFB3X5S7SUPU3UZKSLNTHJEXLKJPZP4 +Codes : {282924, 721698, 735368, 749478…} ``` ### Get-PiHoleCurrentAuthSession @@ -1630,9 +1651,9 @@ CurrentSession : False Valid : True TlsLogin : True TlsMixed : False -LoginAt : 10/6/2026 9:44:13 PM -LastActive : 10/6/2026 9:44:17 PM -ValidUntil : 10/6/2026 10:14:17 PM +LoginAt : 10/12/2026 6:19:07 PM +LastActive : 10/12/2026 6:19:15 PM +ValidUntil : 10/12/2026 6:49:15 PM RemoteAddress : 192.168.1.162 UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6 XForwardedFor : @@ -1644,9 +1665,9 @@ CurrentSession : False Valid : True TlsLogin : True TlsMixed : False -LoginAt : 10/6/2026 9:44:59 PM -LastActive : 10/6/2026 9:44:59 PM -ValidUntil : 10/6/2026 10:14:59 PM +LoginAt : 10/12/2026 6:19:11 PM +LastActive : 10/12/2026 6:19:11 PM +ValidUntil : 10/12/2026 6:49:11 PM RemoteAddress : 192.168.1.162 UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6 XForwardedFor : @@ -1654,13 +1675,27 @@ App : True Cli : False Id : 2 +CurrentSession : False +Valid : True +TlsLogin : True +TlsMixed : False +LoginAt : 10/12/2026 6:31:46 PM +LastActive : 10/12/2026 6:31:46 PM +ValidUntil : 10/12/2026 7:01:46 PM +RemoteAddress : 192.168.1.162 +UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6 +XForwardedFor : +App : True +Cli : False + +Id : 3 CurrentSession : True Valid : True TlsLogin : True TlsMixed : False -LoginAt : 10/6/2026 10:03:33 PM -LastActive : 10/6/2026 10:03:37 PM -ValidUntil : 10/6/2026 10:33:37 PM +LoginAt : 10/12/2026 6:31:53 PM +LastActive : 10/12/2026 6:32:01 PM +ValidUntil : 10/12/2026 7:02:01 PM RemoteAddress : 192.168.1.162 UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6 XForwardedFor : diff --git a/tests/DnsControl/Add-PiHoleDnsRecord.Integration.Tests.ps1 b/tests/DnsControl/Add-PiHoleDnsRecord.Integration.Tests.ps1 new file mode 100644 index 0000000..a1a26d1 --- /dev/null +++ b/tests/DnsControl/Add-PiHoleDnsRecord.Integration.Tests.ps1 @@ -0,0 +1,111 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. +# +# NOTE: these tests add/remove fake records using TEST-NET-1/documentation (RFC 5737/3849) +# reserved addresses that will never route anywhere real. They require the app password used +# here to have "app_sudo" enabled in Pi-hole (Settings > All Settings), same as +# Add-PiHoleConfigArrayItem (called internally) - see its .DESCRIPTION. -Restart $false is used +# throughout to avoid the brief FTL restart a real config change otherwise triggers. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Add-PiHoleDnsRecord (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $script:AHostname = 'piholeshell-test-a.example.com' + $script:AIPv4 = '192.0.2.201' + $script:AaaaHostname = 'piholeshell-test-aaaa.example.com' + $script:AaaaIPv6 = '2001:db8::201' + $script:CnameHostname = 'piholeshell-test-cname.example.com' + $script:CnameTarget = 'piholeshell-test-a.example.com' + $script:CnameTtl = 300 + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Defensive cleanup in case a previous failed run left these behind + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/hosts' -Value "$script:AIPv4 $script:AHostname" -Restart $false -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/hosts' -Value "$script:AaaaIPv6 $script:AaaaHostname" -Restart $false -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/cnameRecords' -Value "$script:CnameHostname,$script:CnameTarget,$script:CnameTtl" -Restart $false -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + } + } + + AfterAll { + if ($script:PiHoleServer) { + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/hosts' -Value "$script:AIPv4 $script:AHostname" -Restart $false -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/hosts' -Value "$script:AaaaIPv6 $script:AaaaHostname" -Restart $false -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/cnameRecords' -Value "$script:CnameHostname,$script:CnameTarget,$script:CnameTtl" -Restart $false -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + } + } + + It 'adds an A record and returns a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Add-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Type A -Hostname $script:AHostname -IPv4Address $script:AIPv4 -Restart $false + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Added' + + $hosts = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Dns.Hosts + $hosts | Should -Contain "$script:AIPv4 $script:AHostname" + + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/hosts' -Value "$script:AIPv4 $script:AHostname" -Restart $false -Confirm:$false | Out-Null + } + + It 'adds an AAAA record and returns a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Add-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Type AAAA -Hostname $script:AaaaHostname -IPv6Address $script:AaaaIPv6 -Restart $false + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Added' + + $hosts = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Dns.Hosts + $hosts | Should -Contain "$script:AaaaIPv6 $script:AaaaHostname" + + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/hosts' -Value "$script:AaaaIPv6 $script:AaaaHostname" -Restart $false -Confirm:$false | Out-Null + } + + It 'adds a CNAME record with a TTL and returns a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Add-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Type CNAME -Hostname $script:CnameHostname -Target $script:CnameTarget -Ttl $script:CnameTtl -Restart $false + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Added' + + $cnames = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Dns.CnameRecords + $cnames | Should -Contain "$script:CnameHostname,$script:CnameTarget,$script:CnameTtl" + + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/cnameRecords' -Value "$script:CnameHostname,$script:CnameTarget,$script:CnameTtl" -Restart $false -Confirm:$false | Out-Null + } + + It 'errors when -IPv4Address is missing for -Type A' -Skip:(-not $script:ConfigAvailable) { + $result = Add-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Type A -Hostname $script:AHostname -Restart $false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } + + It 'errors when -IPv4Address is not a valid IPv4 address' -Skip:(-not $script:ConfigAvailable) { + $result = Add-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Type A -Hostname $script:AHostname -IPv4Address '2001:db8::1' -Restart $false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } + + It 'errors when -Target is missing for -Type CNAME' -Skip:(-not $script:ConfigAvailable) { + $result = Add-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Type CNAME -Hostname $script:CnameHostname -Restart $false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Add-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Type A -Hostname $script:AHostname -IPv4Address $script:AIPv4 -Restart $false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/DnsControl/Get-PiHoleDnsRecord.Integration.Tests.ps1 b/tests/DnsControl/Get-PiHoleDnsRecord.Integration.Tests.ps1 new file mode 100644 index 0000000..637ef28 --- /dev/null +++ b/tests/DnsControl/Get-PiHoleDnsRecord.Integration.Tests.ps1 @@ -0,0 +1,106 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. +# +# NOTE: these tests add/remove fake records using TEST-NET-1/documentation (RFC 5737/3849) +# reserved addresses that will never route anywhere real. -Restart $false is used throughout to +# avoid the brief FTL restart a real config change otherwise triggers. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Get-PiHoleDnsRecord (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $script:AHostname = 'piholeshell-test-get-a.example.com' + $script:AIPv4 = '192.0.2.203' + $script:CnameHostname = 'piholeshell-test-get-cname.example.com' + $script:CnameTarget = 'piholeshell-test-get-a.example.com' + $script:CnameTtl = 300 + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Defensive cleanup in case a previous failed run left these behind + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/hosts' -Value "$script:AIPv4 $script:AHostname" -Restart $false -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/cnameRecords' -Value "$script:CnameHostname,$script:CnameTarget,$script:CnameTtl" -Restart $false -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/hosts' -Value "$script:AIPv4 $script:AHostname" -Restart $false | Out-Null + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/cnameRecords' -Value "$script:CnameHostname,$script:CnameTarget,$script:CnameTtl" -Restart $false | Out-Null + } + } + + AfterAll { + if ($script:PiHoleServer) { + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/hosts' -Value "$script:AIPv4 $script:AHostname" -Restart $false -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/cnameRecords' -Value "$script:CnameHostname,$script:CnameTarget,$script:CnameTtl" -Restart $false -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + } + } + + It 'returns both the A and CNAME test records, correctly typed' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Where-Object { $_.Hostname -in @($script:AHostname, $script:CnameHostname) } | Format-Table | Out-String | Write-Host + + $aRecord = $result | Where-Object { $_.Hostname -eq $script:AHostname } + $aRecord | Should -Not -BeNullOrEmpty + $aRecord.Type | Should -Be 'A' + $aRecord.IPv4Address | Should -Be $script:AIPv4 + + $cnameRecord = $result | Where-Object { $_.Hostname -eq $script:CnameHostname } + $cnameRecord | Should -Not -BeNullOrEmpty + $cnameRecord.Type | Should -Be 'CNAME' + $cnameRecord.Target | Should -Be $script:CnameTarget + $cnameRecord.Ttl | Should -Be $script:CnameTtl + } + + It 'filters by -Type' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Type CNAME + + $result | Should -Not -BeNullOrEmpty + ($result.Type | Select-Object -Unique) | Should -Be 'CNAME' + $result.Hostname | Should -Contain $script:CnameHostname + } + + It 'filters by -Hostname' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Hostname $script:AHostname + + $result | Should -Not -BeNullOrEmpty + @($result).Count | Should -Be 1 + $result.Type | Should -Be 'A' + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + + $result.PSObject.Properties.Name | Should -Contain 'Hosts' + $result.PSObject.Properties.Name | Should -Contain 'CnameRecords' + $result.Hosts | Should -Contain "$script:AIPv4 $script:AHostname" + } + + It 'can be piped directly into Remove-PiHoleDnsRecord' -Skip:(-not $script:ConfigAvailable) { + $removed = Get-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Hostname $script:AHostname | + Remove-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Restart $false -Confirm:$false + $removed | Format-List | Out-String | Write-Host + + $removed | Should -Not -BeNullOrEmpty + $removed.Status | Should -Be 'Removed' + + $hosts = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Dns.Hosts + $hosts | Should -Not -Contain "$script:AIPv4 $script:AHostname" + + # Recreate it so AfterAll's cleanup (and other tests, if re-run) have a consistent state + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/hosts' -Value "$script:AIPv4 $script:AHostname" -Restart $false | Out-Null + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/DnsControl/Remove-PiHoleDnsRecord.Integration.Tests.ps1 b/tests/DnsControl/Remove-PiHoleDnsRecord.Integration.Tests.ps1 new file mode 100644 index 0000000..2845ae3 --- /dev/null +++ b/tests/DnsControl/Remove-PiHoleDnsRecord.Integration.Tests.ps1 @@ -0,0 +1,102 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. +# +# NOTE: these tests add/remove fake records using TEST-NET-1/documentation (RFC 5737/3849) +# reserved addresses that will never route anywhere real. -Restart $false is used throughout to +# avoid the brief FTL restart a real config change otherwise triggers. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Remove-PiHoleDnsRecord (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $script:AHostname = 'piholeshell-test-remove-a.example.com' + $script:AIPv4 = '192.0.2.202' + $script:CnameHostname = 'piholeshell-test-remove-cname.example.com' + $script:CnameTarget = 'piholeshell-test-remove-a.example.com' + $script:CnameTtl = 300 + + $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Defensive cleanup in case a previous failed run left these behind + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/hosts' -Value "$script:AIPv4 $script:AHostname" -Restart $false -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/cnameRecords' -Value "$script:CnameHostname,$script:CnameTarget,$script:CnameTtl" -Restart $false -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + } + } + + AfterAll { + if ($script:PiHoleServer) { + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/hosts' -Value "$script:AIPv4 $script:AHostname" -Restart $false -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/cnameRecords' -Value "$script:CnameHostname,$script:CnameTarget,$script:CnameTtl" -Restart $false -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + } + } + + It 'removes an A record and returns a formatted object' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/hosts' -Value "$script:AIPv4 $script:AHostname" -Restart $false | Out-Null + + $result = Remove-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Type A -Hostname $script:AHostname -IPv4Address $script:AIPv4 -Restart $false -Confirm:$false + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Removed' + + $hosts = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Dns.Hosts + $hosts | Should -Not -Contain "$script:AIPv4 $script:AHostname" + } + + It 'removes a CNAME record with a matching TTL' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/cnameRecords' -Value "$script:CnameHostname,$script:CnameTarget,$script:CnameTtl" -Restart $false | Out-Null + + $result = Remove-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Type CNAME -Hostname $script:CnameHostname -Target $script:CnameTarget -Ttl $script:CnameTtl -Restart $false -Confirm:$false + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Removed' + + $cnames = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Dns.CnameRecords + $cnames | Should -Not -Contain "$script:CnameHostname,$script:CnameTarget,$script:CnameTtl" + } + + It 'does not remove anything when -WhatIf is set' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/hosts' -Value "$script:AIPv4 $script:AHostname" -Restart $false | Out-Null + + Remove-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Type A -Hostname $script:AHostname -IPv4Address $script:AIPv4 -Restart $false -WhatIf + + $hosts = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Dns.Hosts + $hosts | Should -Contain "$script:AIPv4 $script:AHostname" + + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/hosts' -Value "$script:AIPv4 $script:AHostname" -Restart $false -Confirm:$false | Out-Null + } + + It 'accepts multiple records piped from Get-PiHoleDnsRecord in a single call' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/hosts' -Value "$script:AIPv4 $script:AHostname" -Restart $false | Out-Null + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/cnameRecords' -Value "$script:CnameHostname,$script:CnameTarget,$script:CnameTtl" -Restart $false | Out-Null + + $results = Get-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl | + Where-Object { $_.Hostname -in @($script:AHostname, $script:CnameHostname) } | + Remove-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Restart $false -Confirm:$false + $results | Format-Table | Out-String | Write-Host + + @($results).Count | Should -Be 2 + $results.Status | Should -Be @('Removed', 'Removed') + + $config = Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $config.Dns.Hosts | Should -Not -Contain "$script:AIPv4 $script:AHostname" + $config.Dns.CnameRecords | Should -Not -Contain "$script:CnameHostname,$script:CnameTarget,$script:CnameTtl" + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Remove-PiHoleDnsRecord -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Type A -Hostname $script:AHostname -IPv4Address $script:AIPv4 -Restart $false -Confirm:$false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tools/Update-ExampleOutput.ps1 b/tools/Update-ExampleOutput.ps1 index 35cb802..cbbff6e 100644 --- a/tools/Update-ExampleOutput.ps1 +++ b/tools/Update-ExampleOutput.ps1 @@ -193,6 +193,22 @@ if ($appSudoEnabled) { Add-Example -Category Config -FunctionName 'Remove-PiHoleConfigArrayItem' ` -Invocation "Remove-PiHoleConfigArrayItem -PiHoleServer `$PiHoleServer -Password `$Password -Element `"dns/hosts`" -Value `"$docsHostEntry`"" ` -Result (Remove-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Element 'dns/hosts' -Value $docsHostEntry -Restart $false -Confirm:$false) + + $docsDnsHostname = 'piholeshell-docs-dns-example.com' + Invoke-Quietly { Remove-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Element 'dns/hosts' -Value "192.0.2.2 $docsDnsHostname" -Restart $false -Confirm:$false } + + Add-Example -Category DnsControl -FunctionName 'Add-PiHoleDnsRecord' ` + -Invocation "Add-PiHoleDnsRecord -PiHoleServer `$PiHoleServer -Password `$Password -Type A -Hostname `"$docsDnsHostname`" -IPv4Address `"192.0.2.2`"" ` + -Result (Add-PiHoleDnsRecord -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Type A -Hostname $docsDnsHostname -IPv4Address '192.0.2.2' -Restart $false) + + Add-Example -Category DnsControl -FunctionName 'Get-PiHoleDnsRecord' ` + -Invocation 'Get-PiHoleDnsRecord -PiHoleServer $PiHoleServer -Password $Password' ` + -Note 'Can be filtered with -Type and/or -Hostname, and piped directly into Remove-PiHoleDnsRecord.' ` + -Result (Get-PiHoleDnsRecord -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Hostname $docsDnsHostname) + + Add-Example -Category DnsControl -FunctionName 'Remove-PiHoleDnsRecord' ` + -Invocation "Remove-PiHoleDnsRecord -PiHoleServer `$PiHoleServer -Password `$Password -Type A -Hostname `"$docsDnsHostname`" -IPv4Address `"192.0.2.2`"" ` + -Result (Remove-PiHoleDnsRecord -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Type A -Hostname $docsDnsHostname -IPv4Address '192.0.2.2' -Restart $false -Confirm:$false) } else { $appSudoNote = 'Requires "app_sudo" enabled for this app password (Settings > All Settings in your Pi-hole admin UI) - not captured this run since it is disabled here.' @@ -205,6 +221,15 @@ else { Add-Example -Category Config -FunctionName 'Remove-PiHoleConfigArrayItem' ` -Invocation 'Remove-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $Password -Element "dns/hosts" -Value "192.0.2.1 example.com"' ` -Note $appSudoNote -Skipped + Add-Example -Category DnsControl -FunctionName 'Add-PiHoleDnsRecord' ` + -Invocation 'Add-PiHoleDnsRecord -PiHoleServer $PiHoleServer -Password $Password -Type A -Hostname "example.com" -IPv4Address "192.0.2.1"' ` + -Note $appSudoNote -Skipped + Add-Example -Category DnsControl -FunctionName 'Get-PiHoleDnsRecord' ` + -Invocation 'Get-PiHoleDnsRecord -PiHoleServer $PiHoleServer -Password $Password' ` + -Note $appSudoNote -Skipped + Add-Example -Category DnsControl -FunctionName 'Remove-PiHoleDnsRecord' ` + -Invocation 'Remove-PiHoleDnsRecord -PiHoleServer $PiHoleServer -Password $Password -Type A -Hostname "example.com" -IPv4Address "192.0.2.1"' ` + -Note $appSudoNote -Skipped } Add-Example -Category Config -FunctionName 'Get-PiHolePadd' `