Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified PiHoleShell/PiHoleShell.psd1
Binary file not shown.
2 changes: 1 addition & 1 deletion PiHoleShell/PiHoleShell.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
147 changes: 147 additions & 0 deletions PiHoleShell/Public/DnsControl/Add-PiHoleDnsRecord.ps1
Original file line number Diff line number Diff line change
@@ -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
}
}
113 changes: 113 additions & 0 deletions PiHoleShell/Public/DnsControl/Get-PiHoleDnsRecord.ps1
Original file line number Diff line number Diff line change
@@ -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 "<ip> <hostname>" - 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 "<alias>,<target>[,<ttl>]" - 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
}
}
Loading
Loading