I wanted to know how often dynamic IP addresses change, and be alerted when they do. In fact, some colleagues have reported that even “static” IPs are sometimes changed without warning, so this script would address that as well.
The script saves two files, an .xml file with the latest public IP address, and a .log file listing the history of IP addresses. Specify as the first parameter where you want those files stored. If you don’t specify a log file path, the files will be stored in the same folder where the script is located. (Note the use of the %IT_Scripts% environment variable in the screen shot below—see this post for details.)
By default, the script uses whatismyip.akamai.com to retrieve the machine’s public IP address. You can change this with the second and third parameters. See the parameter notes in the script.
The script returns 0 if the IP address has not changed and 1001 if it has. Deploy this as a 24×7 Check with with Max Remote Management (MaxRM). Depending on your settings, that will run the script every 5-60 minutes. Each execution takes less than a second. If you want to receive an email when the IP changes, set the Alert Settings to email you on an “Outage”. Note that the first time the script is run, it will report an IP change because it doesn’t have any history to compare to.
Script output is available in the MaxRM dashboard:
Cut and paste the script below and save it e.g. as “MCB.TrackPublicIPHistory.ps1”.
<# .Synopsis Keep a history of the computer's public IP in a text file. Return "error" code 1001 when the IP changes so we can optionally set up monitoring software to send an email notification. Copyright (c) 2016 by MCB Systems. All rights reserved. Free for personal or commercial use. May not be sold. No warranties. Use at your own risk. .Notes Name: MCB.TrackPublicIPHistory.ps1 Author: Mark Berry, MCB Systems Created: 03/31/2016 Last Edit: 03/31/2016 .Changes 03/31/2016 Initial version. .Description Retrieve the public IP address. If it has changed, return 1001 and record the change to a text file. .Parameter ScriptLogFilesPath Where to store the status and log files for this script. If not specified, files go in the same directory as the script. Default: "". .Parameter PublicIPSource The HTTP address from which to retrieve an external (WAN) IP address. Default: http://whatismyip.akamai.com/ .Parameter PublicIPXMLXPath The optional XPATH to use for extracting the text block containing the IP address from the text returned by PublicIPSource. For example, if the IP is in the Body of an HTML page, use "/html/body". http://whatismyip.akamai.com/ does not return any HTML with the IP, so the default is empty. Default: "" Note: AFTER extracting this XML element, a regular expression will extract just the IP address, so it's okay if there is some text before and/or after the IP. .Parameter LogFile Path to a log file. Required by MaxRM script player. Not used here. Default: "". .Example MCB.TrackPublicIPHistory ` -ScriptLogFilesPath ($Env:IT_Scripts + 'LogFiles\') ` -PublicIPSource "http://whatismyip.akamai.com/" ` -PublicIPXMLXPath "" ` #> ################################################################################ # STEP 1: Get command line arguments. ################################################################################ # Must be the first statement in the script param( [Parameter(Mandatory = $false, Position = 0, ValueFromPipelineByPropertyName = $true)] [String]$ScriptLogFilesPath="", [Parameter(Mandatory = $false, Position = 1, ValueFromPipelineByPropertyName = $true)] [String]$PublicIPSource="http://whatismyip.akamai.com/", [Parameter(Mandatory = $false, Position = 2, ValueFromPipelineByPropertyName = $true)] [String]$PublicIPXMLXPath="", [Parameter(Mandatory = $false, Position = 3, ValueFromPipelineByPropertyName = $true)] [String]$LogFile="" ) ################################################################################ # STEP 2: Script setup ################################################################################ # Set up and start stopwatch so we can print out how long it takes to run script # http://stackoverflow.com/questions/3513650/timing-a-commands-execution-in-powershell $StopWatch = [Diagnostics.Stopwatch]::StartNew() # In case this script was retrieved from Path, get its directory $executingScriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path # Also get its name, without extension--used to name the .xml and .log files $executingScriptBaseName = (Get-Item $MyInvocation.MyCommand.Name).BaseName # Override $executingScriptBaseName with a fixed name--comment out to use script's name $executingScriptBaseName = "MCB.TrackPublicIPHistory" if ($ScriptLogFilesPath -eq "") { # if $ScriptLogFilesPath parameter not specified $ScriptLogFilesPath = $executingScriptDirectory } elseif ( !(Test-Path $ScriptLogFilesPath) ) { # $ScriptLogFilesPath parameter was specified but folder doesn't exist $ScriptLogFilesPath = $executingScriptDirectory } # Use Join-Path to get full paths--adds "\" if necessary $StatusFileFullPath = Join-Path -Path $ScriptLogFilesPath -ChildPath "$executingScriptBaseName.xml" $LogFileFullPath = Join-Path -Path $ScriptLogFilesPath -ChildPath "$executingScriptBaseName.log" # Look for $StatusFileFullPath file as determined above. # If the file is found, load the values into a hash. If not found, initialize an empty hash. if (Test-Path ($StatusFileFullPath)) { # Retrieve the hash table containing the status values $IPChangeStatusHash = Import-Clixml ($StatusFileFullPath) } else { # Status file doesn't exist yet. Create an empty hash. Assume IP not registered. $IPChangeStatusHash = @{"CurrentIPAddress"=""; "CurrentIPAddressChangedAt"=""} } $MainErrorFound = $false # Concatenate output to $Output so we can prepend a one-line $Status $Output = "" ################################################################################ # STEP 3: Get public IP ################################################################################ $Output += "`n======================================================" $Output += "`nRetrieve public IP from a ""What is my IP?"" service" $Output += "`n======================================================" function GetAndDisplayIP([String]$PublicIPSource="", [String]$PublicIPXMLXPath="") { # initialize the three objects we will return $FuncErrorFound = $false $PublicIPContent = "" $PublicIP = "" try { $PublicIPContent = (New-Object System.Net.WebClient).DownloadString($PublicIPSource) } catch { $Output += "`nFailed to download content from """ + $PublicIPSource + """. System message:" $Output += "`n " + $_.Exception.Message.ToString() $FuncErrorFound = $true } # Extract string using XPath: see Karl Prosser's 11/16/2007 post here: # http://www.techtalkz.com/microsoft-windows-powershell/172318-xmldocument-selectsinglenode-doesnt-work.html if ($PublicIPXMLXPath -ne "") { try { # Treat $PublicIPContent as XML - use XPath to extract string [xml]$PublicIPXML = $PublicIPContent $PublicIPContent = $PublicIPXML.SelectSingleNode($PublicIPXMLXPath).InnerText } catch { $Output += "`nFailed to extract data from """ + $PublicIPSource + ` """ using XPath """ + $PublicIPXMLXPath + """. System message:" $Output += "`n " + $_.Exception.Message.ToString() $FuncErrorFound = $true } } # Extract the IP only using a regular expression from this post: # http://stackoverflow.com/questions/2890896/extract-ip-address-from-an-html-string-python $RegEx = '[0-9]+(?:\.[0-9]+){3}' if ($PublicIPContent -match $RegEx) { $PublicIP = $matches[0] # $matches array automatically populated by -match } else { $PublicIP = "" $PublicIPContent = 'No IP address found in content "' + $PublicIPContent + '".' } # Always return three objects $FuncErrorFound $PublicIPContent $PublicIP } # end of GetAndDisplayIP function if ($PublicIPSource -ne "") { $ErrorFound,$PublicIPContent,$PublicIP = GetAndDisplayIP $PublicIPSource $PublicIPXMLXPath $Output += "`n" + $PublicIPSource + ': ' + $PublicIPContent # output results if ($ErrorFound) { $MainErrorFound = $true } } ################################################################################ # STEP 4: Check for and record IP change ################################################################################ $OldCurrentIPAddress = $IPChangeStatusHash.get_Item("CurrentIPAddress") # the _old_ CurrentIPAddress if ($PublicIP -eq "") { $IPComparisonResult = "No public IP found, so IP change not checked" $Output += "`n" + $IPComparisonResult } elseif ($PublicIP -eq $OldCurrentIPAddress) { # compare to the _old_ CurrentIPAddress $IPComparisonResult = "Current public IP $PublicIP matches previous IP, so no change recorded" $Output += "`n" + $IPComparisonResult } else { # new public IP found $IPComparisonResult = "Current IP $PublicIP does not match previous IP $OldCurrentIPAddress" $Output += "`n" + $IPComparisonResult # Call this an "error" to return 1001 so monitoring software can send email alert on IP change $MainErrorFound = $true $Output += "`n" $Output += "`n======================================================" $Output += "`nRecord the IP address change" $Output += "`n======================================================" $LineToAppend = (get-date -format "yyyy/MM/dd HH:mm:ss") + " - " + $PublicIP # Append one line to text file Add-Content -Path $LogFileFullPath -Value ($LineToAppend) $Output += "`nAdded this line to $LogFileFullPath :" $Output += "`n" + $LineToAppend # Update the Current IP and time to the status hash (to be written out at end of script) $IPChangeStatusHash.set_Item("CurrentIPAddress", $PublicIP) $IPChangeStatusHash.set_Item("CurrentIPAddressChangedAt", (Get-Date -Format "G")) # short date + long time with AM/PM } # new public IP found ################################################################################ # STEP 5: Output the IP history from the .log file ################################################################################ $Output += "`n" $Output += "`n======================================================" $Output += "`nIP history from $LogFileFullPath" $Output += "`n======================================================" $Output += "`n" # Get-Content returns a string array. Pipe to Out-String to convert to string and preserve line breaks. $Output += (Get-Content -Path $LogFileFullPath | Out-String) ################################################################################ # STEP 6: Wrap up ################################################################################ # Save the hash table to a file in $ScriptLogFilesPath as determined above $IPChangeStatusHash | Export-Clixml ($StatusFileFullPath) $Status = $IPComparisonResult #Prepare SummaryLine for display in monitoring system. Abbreviate date/time. $SummaryLine = $Status + " [" + (Get-Date -Format "MM/dd HH:mm") + "]" $SummaryLine $Output "======================================================" "Local Machine Time: " + (Get-Date -Format G) # Stop the stopwatch and calculate the elapsed time in seconds, rounded to 0.1 seconds $StopWatch.Stop() $ElapsedSecondsRounded = [Math]::Round($StopWatch.Elapsed.TotalSeconds, 1) $ElapsedString = [string]::Format("{0:0.0} second(s)", $ElapsedSecondsRounded) 'Script execution took ' + $ElapsedString + '.' if ($MainErrorFound) { $ExitCode = 1001 } else { $ExitCode = 0 } 'Exit Code: ' + $ExitCode Exit $ExitCode