I work in an enterprise with tens of thousands of devices owned by dozens of offices and groups. I’m also one of the people tasked with pushing IPv6 adoption inside of that enterprise! A lot went into planning our IPv6 scheme, but that’s outside of the focus of this blog post. Instead, I’ll be writing about some PowerShell I wrote to make sure all of these disparate system owners use our planned scheme correctly:
- A cmdlet to easily take an IPv4 address and 1:1 convert it to IPv6
- A script to dual-stack an IPv4-only firewall policy
PowerShell was chosen for the original cmdlet because I wanted anyone and everyone in my organization to be able to take an IPv4 address and quickly be able to determine what the “matching” IPv6 address should be, without any outside dependencies. This original cmdlet became the core for a project to speed up the process of replicating IPv4 access to IPv6 on our firewalls.
The Creation of the Calculator
We planned out our IPv6 scheme to include address bits for the physical site, for the VLAN (this is just the VLAN number translated DEC:HEX), for the last octet of the original address, and a few other bits in the same vein – gotta keep this a little vague for security reasons! After the general scheme was drawn up our network team created a master Excel “lookup” workbook with data reference tables for all of those address bits. For example, VLAN 100 could be the San Franciso office, with the site ID arbitrarily set to 0E3.
The Excel sheet was useful – as long as you knew the site and the VLAN it was simple enough to enter the original IPv4 address, the VLAN ID, and the human readable site name and have a cell on the first sheet calculate the full IPv6 address. A little cumbersome if you have to do several addresses, but completely useable.
After a few days of heavy use of the spreadsheet, I got fed up with manually clicking the dropdowns and copying and pasting. I recalled how at a previous job I assisted the accounting team with end-of-month paperwork by using MSWord COM objects to open up their prebills and work some regex magic to extract client names and IDs and how much we were billing them, thereby replacing their manual process. What I can do in Word, I can do in Excel, right? The idea was born: write a PowerShell script that opens up Excel in the background, passes arguments into the correct cells, and then extracts the resulting address and puts it right back into the command line.
The PowerShell to open up and utilize an Office application is actually pretty simple, it looks something like this:
$spreadsheet = New-Object -comObject Excel.Application
$spreadsheet.Visible = $true
$spreadsheet = $spreadsheet.Workbooks.Open('filename')
Now Excel is open! If you want to do this in the background you just keep Visible as $false
. To modify values it’s pretty similar:
$sheet = $spreadsheet.Sheets.Item('sheetname') #open a specific sheet from a workbook
$sheet.Cells.Item($row,$column) = $value #enters $value into cell at $row,$column
This was clumsy, but it worked! However, the more times I ran it, the more likely it was that Excel would hang or not close properly, so another solution was needed. After a bit of brainstorming and cajoling, I got the network team to create a new data reference sheet that had every CIDR we’ve assigned throughout our network. Once that was added, I built a script that scraped the Excel file to make some JSON that described all of our networks! A made-up and sanitized example of one network is below:
{
"CIDR": "10.0.10.0/29",
"VLAN": 100,
"SiteID": "San Francisco",
"SiteHex": "0E3",
"IPs": [
"10.0.10.0",
"10.0.10.1",
"10.0.10.2",
"10.0.10.3",
"10.0.10.4",
"10.0.10.5",
"10.0.10.6",
"10.0.10.7"
]
}
Voila! Instead of opening and closing an instance of Excel in the background, I’d essentially made a big ole JSON lookup table/calculator that could sit in PowerShell’s memory. Astute readers will notice repeated information in each object – why have the readable site name if all we need is the hex, and why expand all of the IPs if we already have the CIDR? Well, the CIDR question has a solid answer: by having all of the information here it makes it so the final script is a quick lookup instead of a calculation – doing it this way uses a little bit more memory up front but greatly increases the speed of the cmdlet, especially when some networks are up to /20. There’s actually a second reason that also covers the SiteID
, but we’ll get to that in the next part!
Convert-IPv4Address.ps1
Honestly, the hard work was already accomplished above. All that’s needed from here is a command line interface to look up an address, cobble together the hex results from the lookup, and format them into a proper IPv6 string.
You can check the repository linked at the bottom for the whole code, but basically cmdlet’s logic flows like this (for IPv4 address $address
):
foreach ($network in $calculatorArray) { #find which network the IPv4 address belongs to
$checker = $network.IPs.Contains($address)
if ($checker = $true) { #if it's a match then save the information from the calculator
$site = $network.SiteHex
$vlan = '{0:x}' -f $network.VLAN #convert to hex
$vlan = $vlan.ToString()
break
}
}
$IPv6Address = $base + ":" + $site + ":" + $vlan #omitting some logic
Throw the whole thing into a process{}
block and hooray, we have a tool that can spit out IPv6 addresses as fast as you can throw them! Since the information was available in the calculator I also created some quick troubleshooting cmdlets on top, like Get-IPv4Location
to figure out the physical location of an IPv4 address, and Get-IPv4VLAN
for the VLAN numbers. Those little guys are truly invaluable in incident/triage situations.
Dualstack-Policy.ps1
This cmdlet helped out a lot, but with tens of thousands of devices, you have thousands of firewall policies! Even replacing the Excel lookup method with a pipeable cmdlet we were still looking a truly Sisyphean task. But if the script was solid and the error rate low, could I automate it even further? A firewall policy is just a miserable pile of IP addresses (and services I guess) and I can automatically convert those addresses, right? How hard could it be?
Pretty hard, it turns out!. The two methods our firewall has for programmatic changes are to feed it a plaintext config file or utilize its API. The API docs are nearly impossible to get to (and even harder to understand), and by time I’ve downloaded the whole config file and parsed it I’m two weeks behind the current config! After playing around a bit I discovered that the web management interface for the firewall actually utilizes the API, so instead of using the official docs I decided to spend some time squinting at Chrome’s dev tools to reverse-engineer this thing.
In order to convert a policy, first I have to look at the whole policy, which was actually pretty simple. The REST endpoint for a firewall policy was just https://firewall/api/v2/firewall/policy/<policynumber>
where <policynumber>
is the ID for the policy. This request returns JSON like this:
{
"policyid": 100,
"service": "HTTPS",
"srcaddr": [
{
"name": "srcObject"
},
{
"name": "srcGroup"
}
],
"dstaddr": [
{
"name": "dstObject"
},
{
"name": "dstGroup"
}
],
"srcaddr6": [
],
"dstaddr6": [
]
}
There’s actually about 100 more objects in there, but these are the ones I’m most interested in. Looking at how previously dual-stacked policies appeared in the firewall, it seemed it would be as easy as adding address objects to srcaddr6
and dstaddr6
. All I have to do is create IPv6 versions of the objects in srcaddr
and dstaddr
, then PUT them to the policy in the right place.
But here’s the inevitable rub: how the firewall handles the objects that make up the source and destination fields. Inside of those fields you can have an address object, which consists of a CIDR and a name, or an address group, consisting of either other groups or individual objects, and the name. Regardless of whether the address is an object or a group, it’s referred to by name. Still not a big problem.
Except that address groups and objects have different API endpoints! And you can’t create a group unless all of its constituent members are already created! And groups can contain other groups as members! It’s recursive! I’ll admit, this was a nightmare of a logic puzzle to figure out. I have to somehow walk down to the bottom level of a tree, create all of its constituent parts, and then walk back up, keeping the relationships intact. I spent a lot of time on this problem (I’m a sysadmin/engineer who likes scripting, not a programmer!). It consumed me for a few weeks. I thought about it in the shower, I saw it in my dreams, it occupied my mind on long bike rides, I asked ChatGPT about it, I talked it over with my wife… and eventually, I solved it!
This really deserves a diagram, but I’ll try to explain it best I can in words. The only way to determine if the name
referenced is an object or a group is to query one of the endpoints: if you get a 404 at the object endpoint then you know that it exists at the group endpoint and vice versa. Armed with this knowledge I created two hashtables:
$convertedObjects
– a table to hold the original IPv4 name of an address or group as the key, and the new IPv6 name as the value- and
$processedGroups
– a list to hold groups to identify if they’ve been processed. If it’s been processed I can grab the name and reference the above table for the new name
The script walks through each address name encountered in the policy
object, if an API call to the address object endpoint gets 200 OK
(and we haven’t already created the object), awesome – create an IPv6 address object with POST and add it to the $convertedObjects
table. If the API call to the address object endpoint fails, call the address group endpoint… and return to the top of this paragraph with each name! Once all objects have been made, POST the group and add the group name to $processedGroups
Once all of the groups have been iterated through and all of the objects created, we look back at the original names from the policy
object. By referencing the $convertedObjects
lookup table we can add the IPv6 version of each address to dstaddr6
or srcaddr6
with no issues! The script then PUTs the policy back and we’re done here!
I’m pretty proud of this solution, and it has saved my organization hundreds, if not thousands, of hours. Due to how different everyone’s IPv6 schemes are bound to be, it’s not a copy and paste solution for anyone else – but I hope it gets some gears turning! All of the final code is available at the repository below:
Repository link: https://github.com/pmalley130/IPv6-Transition-Helpers-Public