Scripting pfSense DHCP static assignments

For a while, I’ve used Microsoft’s DHCP server in my home lab. Within the last year though, I’ve started having issues where my Unifi WAP’s would have trouble getting an IP address. If I rebooted the WAP’s, everything was fine. Because of this, I don’t really think its a problem with my DHCP server or configuration, but I wanted to be absolutely sure and rule it out.

I had a slight problem. Although I use DHCP, I use DHCP reservations for almost everything on my network. The only things I don’t reserve are guest devices. I have an excel spreadsheet that I maintain that has all my devices and their reservations. I needed a way to easily and automatically import these reservations into pfSense for when I make additions and/or changes, just like I do for Microsoft’s DHCP server currently.

I already have a powershell script to do this for Microsoft DHCP, so I used that to get started. It wasn’t as difficult as I thought it would be. A few lines of powershell later, and I have a working powershell script that takes my excel file and dumps it into pfSense’s DHCP configuration, then restarts the service.

# 1) Install-Module ImportExcel -Scope CurrentUser
# 2) Save this script in the same directory as your spreadsheet, or adjust the relevant lines

Clear

Set-Location $PSScriptRoot

$pfSenseUrl = "https://x.x.x.x"
$excelFile = "Home Network.xlsx"

# Prompt for credentials to login to pfsense
# The only permission this user needs is "WebCfg - Diagnostics: Backup & Restore"
# If you want the script to be able to restart the dhcp service, then you 
# also need:
#   Allow access to the 'Services: DHCP Server' page.
#   and
#   Allow access to the 'Status: Services' page.
if ( $cred -eq $null ) {
    $cred = (Get-Credential)
}


# get csrf token for request
$lastRequest = (Invoke-WebRequest $pfSenseUrl/diag_backup.php -SessionVariable pfSenseSession)
$csrf = $lastRequest.Forms.Fields["__csrf_magic"]

# login
$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($cred.Password)
$postParams = @{login='Login';usernamefld=$cred.UserName;passwordfld=([System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr));__csrf_magic=$csrf}
$lastRequest = (Invoke-WebRequest $pfSenseUrl/diag_backup.php -WebSession $pfSenseSession -Method Post -Body $postParams)

# get csrf token for next request
$lastRequest = (Invoke-WebRequest $pfSenseUrl/diag_backup.php -WebSession $pfSenseSession)
$csrf = $lastRequest.Forms.Fields["__csrf_magic"]

# download current dhcp server configuration
$postParams = @{download='download';donotbackuprrd='yes';__csrf_magic=$csrf;backuparea='dhcpd';}
$lastRequest = (Invoke-WebRequest $pfSenseUrl/diag_backup.php -WebSession $pfSenseSession -Method Post -Body $postParams)

#remove all current static mappings
[System.Xml.XmlDocument]$xml = [System.Xml.XmlDocument]::new()
$xml.LoadXml([System.Text.ASCIIEncoding]::UTF8.GetString($lastRequest.Content))
$xml.dhcpd.lan.SelectNodes("staticmap") | ForEach-Object {
    [void]$_.ParentNode.RemoveChild($_)
}
$lastRequest = $null

# convert the excel file to csv for easy manipulation in powershell
Import-Excel $excelFile | Export-Csv -Path "./$($excelFile).csv" -NoTypeInformation

# parse the generated csv file and add mappings to the xml document in memory
Import-Csv "$($excelFile).csv" | ForEach-Object {

    $mac = $_.UniqueID
    
    if ( $mac.Length -gt 0 ) {

        $desc = $_.HostName
        if ($_.Description.Length -gt 0) {
            $desc = $_.Description
        }

        
        if ($r -eq $null) {
            Write-Host Adding reservation for $_

            $reservation = $xml.CreateElement("staticmap")

            $macEle = $xml.CreateElement("mac")
            $macEle.InnerText = $mac

            $descEle = $xml.CreateElement("descr")
            $descEle.InnerText = $desc

            $ipEle = $xml.CreateElement("ipaddr")
            $ipEle.InnerText = $_.IP

            $hostEle = $xml.CreateElement("hostname")
            $hostEle.InnerText = $_.HostName

            [void]$reservation.AppendChild($macEle)
            [void]$reservation.AppendChild($ipEle)
            [void]$reservation.AppendChild($hostEle)
            [void]$reservation.AppendChild($descEle)

            [void]$xml.dhcpd.lan.AppendChild($reservation)
        }
    }
}


$LF = "`r`n"
$boundary = [System.Guid]::NewGuid().ToString()
$contentType = "multipart/form-data; boundary=`"$boundary`""

# generate the multipart/form-data body with required form variables and the config "file"
$bodyLines = (
 "--$boundary",
 "Content-Disposition: form-data; name=`"__csrf_magic`"$LF",
 $csrf,

 "--$boundary",
 "Content-Disposition: form-data; name=`"restorearea`"$LF",
 "dhcpd",

  "--$boundary",
 "Content-Disposition: form-data; name=`"conffile`"; filename=`"dhcpd.xml`"",
 "Content-Type: text/xml$LF",
 $xml.OuterXml,
 
 "--$boundary",
 "Content-Disposition: form-data; name=`"restore`"$LF",
 "Restore Configuration"
 ) -join $LF


 try {
    # restore the dhcpd configuration to pfSense
    $lastRequest = ( Invoke-WebRequest $pfSenseUrl/diag_backup.php -ContentType $contentType -WebSession $pfSenseSession -Method Post -Body $bodyLines -ErrorAction SilentlyContinue)
} catch {}



if ( $lastRequest.StatusCode -eq 200 ) {
    Write-Host
    Write-Host Success! -ForegroundColor Green
    Write-Host

    Write-Host Restarting DHCP service

    # get csrf token for request
    $csrf = $lastRequest.Forms.Fields["__csrf_magic"]

    #restart dhcp service
    $postParams = @{ajax='ajax';mode='restartservice';__csrf_magic=$csrf;service='dhcpd';}
    $lastRequest = (Invoke-WebRequest $pfSenseUrl/status_services.php -Method Post -WebSession $pfSenseSession -Body $postParams)
}
else { 
    Write-Host Failure! -ForegroundColor Red
}

$bstr = $null
$postParams = $null
$lastRequest = $null

11 comments on “Scripting pfSense DHCP static assignments

  • Patrick says:

    fgfgfghey, I happened to find this script and I wanna use it to populate a bunch of DHCP static mappings on my newly configured PfSense install but before I run your script, can you post a screenshot (or the column headers) for your csv file? I assume it’s mac, descr, ipaddr, hostname but wanna be sure.
    You wouldn’t believe how many people would find this useful.
    thanks for writing it!

    • Chad McCune says:

      The columns that matter are:

      HostName
      Description
      UniqueID
      IP

      They’re referenced in the script as properties on the automatic variable $_.

      For example, $_.HostName, $.Description, $_.UniqueID, and $_.IP

      The other columns are just columns I’m using for my own personal organization.

      Glad it is helpful.

  • Patrick says:

    Sorry to bug you on this but I am getting an error. I am stepping through this with powershell ISE and
    line 41 is giving me trouble.. producing the following error:
    $lastRequest = $null
    At line:1 char:14
    + $xml = ::new()
    + ~
    An expression was expected after ‘(‘.
    + CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : ExpectedExpression

  • Chad McCune says:

    It looks like your $xml declaration might be incorrect.

    Try “[xml]$xml = [xml]::new()” instead of “$xml = ::new()”

  • Chad McCune says:

    Thanks for the reddit post.

    After reading it, I went and checked my original post, and the syntax is correct in the post, but it appears that the syntax highlighter doesn’t like it and it’s getting removed there :/

    Edit: Changed it from the shortcut [xml] to [System.Xml.XmlDocument] and it seems to have fixed the formatting.

  • Tobias Sorensson says:

    Hello

    to skip the ssl error i included
    add-type @”
    using System.Net;
    using System.Security.Cryptography.X509Certificates;
    public class TrustAllCertsPolicy : ICertificatePolicy {
    public bool CheckValidationResult(
    ServicePoint srvPoint, X509Certificate certificate,
    WebRequest request, int certificateProblem) {
    return true;
    }
    }
    “@
    [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy

    so it looks like this

    $pfSenseUrl = “https://x.x.x.x”
    $excelFile = “Home Network.xlsx”

    add-type @”
    using System.Net;
    using System.Security.Cryptography.X509Certificates;
    public class TrustAllCertsPolicy : ICertificatePolicy {
    public bool CheckValidationResult(
    ServicePoint srvPoint, X509Certificate certificate,
    WebRequest request, int certificateProblem) {
    return true;
    }
    }
    “@
    [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy

    what this dose it tells the script to trust all certs so the selfsigned cert will not complain ๐Ÿ™‚

  • Tobias Sorensson says:

    Hello

    can you also make this work if you have multiple dns servers in pfsense for vlans? ๐Ÿ™‚

    like a sheet per vlan with just the vlan id or the name of the dhcp server ๐Ÿ™‚

  • Chad McCune says:

    My pfSense setup is pretty basic and don’t have multiple vlans/dns servers. It still should be relatively easy since I’m just using a backup/restore process to do it. Can you send me a sample backup that contains just your dns area?

    • Tobias Sorensson says:

      Hello

      no idea how to do that..

      can you tell me how to do it and il gladly do it ๐Ÿ™‚

  • Tobias Sorensson says:

    update: with the release of 22.01 it seems like this scrip is no longer working.

    i get this error

    :37 char:17
    + … tRequest = (Invoke-WebRequest $pfSenseUrl/diag_backup.php -SessionVar …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

    Cannot index into a null array.
    At C:\Users\TS\Dokument\pfsense\pfsensenetworkimport.ps1:38 char:1
    + $csrf = $lastRequest.Forms.Fields[“__csrf_magic”]
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : NullArray

Leave a Reply

Your email address will not be published. Required fields are marked *