{"id":73,"date":"2025-07-14T15:45:44","date_gmt":"2025-07-14T19:45:44","guid":{"rendered":"https:\/\/learnwithpatrick.casa\/?p=73"},"modified":"2025-07-14T15:45:44","modified_gmt":"2025-07-14T19:45:44","slug":"tools-to-ease-an-ipv6-transition","status":"publish","type":"post","link":"https:\/\/learnwithpatrick.casa\/?p=73","title":{"rendered":"Tools to ease an IPv6 Transition"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">I work in an enterprise with tens of thousands of devices owned by dozens of offices and groups. I&#8217;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&#8217;s outside of the focus of this blog post. Instead, I&#8217;ll be writing about some PowerShell I wrote to make sure all of these disparate system owners use our planned scheme correctly:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A cmdlet to easily take an IPv4 address and 1:1 convert it to IPv6<\/li>\n\n\n\n<li>A script to dual-stack an IPv4-only firewall policy<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">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 &#8220;matching&#8221; 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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Creation of the Calculator<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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 &#8211; gotta keep this a little vague for security reasons! After the general scheme was drawn up our network team created a master Excel &#8220;lookup&#8221; 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The Excel sheet was useful &#8211; 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The PowerShell to open up and utilize an Office application is actually pretty simple, it looks something like this:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span role=\"button\" tabindex=\"0\" style=\"color:#e1e4e8;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>$spreadsheet = New-Object -comObject Excel.Application\n$spreadsheet.Visible = $true\n$spreadsheet = $spreadsheet.Workbooks.Open('filename')<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki github-dark\" style=\"background-color: #24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #E1E4E8\">$spreadsheet <\/span><span style=\"color: #F97583\">=<\/span><span style=\"color: #E1E4E8\"> <\/span><span style=\"color: #79B8FF\">New-Object<\/span><span style=\"color: #E1E4E8\"> <\/span><span style=\"color: #F97583\">-<\/span><span style=\"color: #E1E4E8\">comObject Excel.Application<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">$spreadsheet.Visible <\/span><span style=\"color: #F97583\">=<\/span><span style=\"color: #E1E4E8\"> <\/span><span style=\"color: #79B8FF\">$true<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">$spreadsheet <\/span><span style=\"color: #F97583\">=<\/span><span style=\"color: #E1E4E8\"> $spreadsheet.Workbooks.Open(<\/span><span style=\"color: #9ECBFF\">&#39;filename&#39;<\/span><span style=\"color: #E1E4E8\">)<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Now Excel is open! If you want to do this in the background you just keep Visible as <code>$false<\/code>. To modify values it&#8217;s pretty similar:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span role=\"button\" tabindex=\"0\" style=\"color:#e1e4e8;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>$sheet = $spreadsheet.Sheets.Item('sheetname') #open a specific sheet from a workbook\n$sheet.Cells.Item($row,$column) = $value #enters $value into cell at $row,$column<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki github-dark\" style=\"background-color: #24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #E1E4E8\">$sheet <\/span><span style=\"color: #F97583\">=<\/span><span style=\"color: #E1E4E8\"> $spreadsheet.Sheets.Item(<\/span><span style=\"color: #9ECBFF\">&#39;sheetname&#39;<\/span><span style=\"color: #E1E4E8\">) <\/span><span style=\"color: #6A737D\">#open a specific sheet from a workbook<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">$sheet.Cells.Item($row<\/span><span style=\"color: #F97583\">,<\/span><span style=\"color: #E1E4E8\">$column) <\/span><span style=\"color: #F97583\">=<\/span><span style=\"color: #E1E4E8\"> $value <\/span><span style=\"color: #6A737D\">#enters $value into cell at $row,$column<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">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&#8217;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:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span role=\"button\" tabindex=\"0\" style=\"color:#e1e4e8;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>  {\n    \"CIDR\": \"10.0.10.0\/29\",\n    \"VLAN\": 100,\n    \"SiteID\": \"San Francisco\",\n    \"SiteHex\": \"0E3\",\n    \"IPs\": &#91;\n      \"10.0.10.0\",\n      \"10.0.10.1\",\n      \"10.0.10.2\",\n      \"10.0.10.3\",\n      \"10.0.10.4\",\n      \"10.0.10.5\",\n      \"10.0.10.6\",\n      \"10.0.10.7\"\n      &#93;\n  }<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki github-dark\" style=\"background-color: #24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #E1E4E8\">  {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">    <\/span><span style=\"color: #79B8FF\">&quot;CIDR&quot;<\/span><span style=\"color: #E1E4E8\">: <\/span><span style=\"color: #9ECBFF\">&quot;10.0.10.0\/29&quot;<\/span><span style=\"color: #E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">    <\/span><span style=\"color: #79B8FF\">&quot;VLAN&quot;<\/span><span style=\"color: #E1E4E8\">: <\/span><span style=\"color: #79B8FF\">100<\/span><span style=\"color: #E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">    <\/span><span style=\"color: #79B8FF\">&quot;SiteID&quot;<\/span><span style=\"color: #E1E4E8\">: <\/span><span style=\"color: #9ECBFF\">&quot;San Francisco&quot;<\/span><span style=\"color: #E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">    <\/span><span style=\"color: #79B8FF\">&quot;SiteHex&quot;<\/span><span style=\"color: #E1E4E8\">: <\/span><span style=\"color: #9ECBFF\">&quot;0E3&quot;<\/span><span style=\"color: #E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">    <\/span><span style=\"color: #79B8FF\">&quot;IPs&quot;<\/span><span style=\"color: #E1E4E8\">: &#91;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">      <\/span><span style=\"color: #9ECBFF\">&quot;10.0.10.0&quot;<\/span><span style=\"color: #E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">      <\/span><span style=\"color: #9ECBFF\">&quot;10.0.10.1&quot;<\/span><span style=\"color: #E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">      <\/span><span style=\"color: #9ECBFF\">&quot;10.0.10.2&quot;<\/span><span style=\"color: #E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">      <\/span><span style=\"color: #9ECBFF\">&quot;10.0.10.3&quot;<\/span><span style=\"color: #E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">      <\/span><span style=\"color: #9ECBFF\">&quot;10.0.10.4&quot;<\/span><span style=\"color: #E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">      <\/span><span style=\"color: #9ECBFF\">&quot;10.0.10.5&quot;<\/span><span style=\"color: #E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">      <\/span><span style=\"color: #9ECBFF\">&quot;10.0.10.6&quot;<\/span><span style=\"color: #E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">      <\/span><span style=\"color: #9ECBFF\">&quot;10.0.10.7&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">      &#93;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">  }<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Voila! Instead of opening and closing an instance of Excel in the background, I&#8217;d essentially made a big ole JSON lookup table\/calculator that could sit in PowerShell&#8217;s memory. Astute readers will notice repeated information in each object &#8211; 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 &#8211; 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&#8217;s actually a second reason that also covers the <code>SiteID<\/code>, but we&#8217;ll get to that in the next part!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Convert-IPv4Address.ps1<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Honestly, the hard work was already accomplished above. All that&#8217;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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You can check the repository linked at the bottom for the whole code, but basically cmdlet&#8217;s logic flows like this (for IPv4 address <code>$address<\/code>):<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span role=\"button\" tabindex=\"0\" style=\"color:#e1e4e8;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>foreach ($network in $calculatorArray) { #find which network the IPv4 address belongs to\n  $checker = $network.IPs.Contains($address)\n  if ($checker = $true) { #if it's a match then save the information from the calculator\n    $site = $network.SiteHex\n    $vlan = '{0:x}' -f $network.VLAN #convert to hex\n    $vlan = $vlan.ToString()\n    break\n    }\n  }\n  \n$IPv6Address = $base + \":\" + $site + \":\" + $vlan #omitting some logic<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki github-dark\" style=\"background-color: #24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #F97583\">foreach<\/span><span style=\"color: #E1E4E8\"> ($network <\/span><span style=\"color: #F97583\">in<\/span><span style=\"color: #E1E4E8\"> $calculatorArray) { <\/span><span style=\"color: #6A737D\">#find which network the IPv4 address belongs to<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">  $checker <\/span><span style=\"color: #F97583\">=<\/span><span style=\"color: #E1E4E8\"> $network.IPs.Contains($address)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">  <\/span><span style=\"color: #F97583\">if<\/span><span style=\"color: #E1E4E8\"> ($checker <\/span><span style=\"color: #F97583\">=<\/span><span style=\"color: #E1E4E8\"> <\/span><span style=\"color: #79B8FF\">$true<\/span><span style=\"color: #E1E4E8\">) { <\/span><span style=\"color: #6A737D\">#if it&#39;s a match then save the information from the calculator<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">    $site <\/span><span style=\"color: #F97583\">=<\/span><span style=\"color: #E1E4E8\"> $network.SiteHex<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">    $vlan <\/span><span style=\"color: #F97583\">=<\/span><span style=\"color: #E1E4E8\"> <\/span><span style=\"color: #9ECBFF\">&#39;{0:x}&#39;<\/span><span style=\"color: #E1E4E8\"> <\/span><span style=\"color: #F97583\">-f<\/span><span style=\"color: #E1E4E8\"> $network.VLAN <\/span><span style=\"color: #6A737D\">#convert to hex<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">    $vlan <\/span><span style=\"color: #F97583\">=<\/span><span style=\"color: #E1E4E8\"> $vlan.ToString()<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">    <\/span><span style=\"color: #F97583\">break<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">    }<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">  }<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">  <\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">$IPv6Address <\/span><span style=\"color: #F97583\">=<\/span><span style=\"color: #E1E4E8\"> $base <\/span><span style=\"color: #F97583\">+<\/span><span style=\"color: #E1E4E8\"> <\/span><span style=\"color: #9ECBFF\">&quot;:&quot;<\/span><span style=\"color: #E1E4E8\"> <\/span><span style=\"color: #F97583\">+<\/span><span style=\"color: #E1E4E8\"> $site <\/span><span style=\"color: #F97583\">+<\/span><span style=\"color: #E1E4E8\"> <\/span><span style=\"color: #9ECBFF\">&quot;:&quot;<\/span><span style=\"color: #E1E4E8\"> <\/span><span style=\"color: #F97583\">+<\/span><span style=\"color: #E1E4E8\"> $vlan <\/span><span style=\"color: #6A737D\">#omitting some logic<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Throw the whole thing into a <code>process{}<\/code> 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 <code>Get-IPv4Location<\/code> to figure out the physical location of an IPv4 address, and <code>Get-IPv4VLAN<\/code> for the VLAN numbers. Those little guys are truly invaluable in incident\/triage situations.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Dualstack-Policy.ps1<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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&#8217;ve downloaded the whole config file and parsed it I&#8217;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&#8217;s dev tools to reverse-engineer this thing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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 <code>https:\/\/firewall\/api\/v2\/firewall\/policy\/&lt;policynumber><\/code> where <code>&lt;policynumber><\/code> is the ID for the policy. This request returns JSON like this:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span role=\"button\" tabindex=\"0\" style=\"color:#e1e4e8;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>{\n  \"policyid\": 100,\n  \"service\": \"HTTPS\",\n  \"srcaddr\": &#91;\n    {\n      \"name\": \"srcObject\"\n    },\n    { \n      \"name\": \"srcGroup\"\n    }\n  &#93;,\n  \"dstaddr\": &#91;\n    {\n      \"name\": \"dstObject\"\n    },\n    { \n      \"name\": \"dstGroup\"\n    }\n  &#93;,\n  \"srcaddr6\": &#91;\n  &#93;,\n  \"dstaddr6\": &#91;\n  &#93;\n}<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki github-dark\" style=\"background-color: #24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #E1E4E8\">{<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">  <\/span><span style=\"color: #79B8FF\">&quot;policyid&quot;<\/span><span style=\"color: #E1E4E8\">: <\/span><span style=\"color: #79B8FF\">100<\/span><span style=\"color: #E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">  <\/span><span style=\"color: #79B8FF\">&quot;service&quot;<\/span><span style=\"color: #E1E4E8\">: <\/span><span style=\"color: #9ECBFF\">&quot;HTTPS&quot;<\/span><span style=\"color: #E1E4E8\">,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">  <\/span><span style=\"color: #79B8FF\">&quot;srcaddr&quot;<\/span><span style=\"color: #E1E4E8\">: &#91;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">      <\/span><span style=\"color: #79B8FF\">&quot;name&quot;<\/span><span style=\"color: #E1E4E8\">: <\/span><span style=\"color: #9ECBFF\">&quot;srcObject&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">    },<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">    { <\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">      <\/span><span style=\"color: #79B8FF\">&quot;name&quot;<\/span><span style=\"color: #E1E4E8\">: <\/span><span style=\"color: #9ECBFF\">&quot;srcGroup&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">    }<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">  &#93;,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">  <\/span><span style=\"color: #79B8FF\">&quot;dstaddr&quot;<\/span><span style=\"color: #E1E4E8\">: &#91;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">    {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">      <\/span><span style=\"color: #79B8FF\">&quot;name&quot;<\/span><span style=\"color: #E1E4E8\">: <\/span><span style=\"color: #9ECBFF\">&quot;dstObject&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">    },<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">    { <\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">      <\/span><span style=\"color: #79B8FF\">&quot;name&quot;<\/span><span style=\"color: #E1E4E8\">: <\/span><span style=\"color: #9ECBFF\">&quot;dstGroup&quot;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">    }<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">  &#93;,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">  <\/span><span style=\"color: #79B8FF\">&quot;srcaddr6&quot;<\/span><span style=\"color: #E1E4E8\">: &#91;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">  &#93;,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">  <\/span><span style=\"color: #79B8FF\">&quot;dstaddr6&quot;<\/span><span style=\"color: #E1E4E8\">: &#91;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">  &#93;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #E1E4E8\">}<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">There&#8217;s actually about 100 more objects in there, but these are the ones I&#8217;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 <code>srcaddr6<\/code> and <code>dstaddr6<\/code>. All I have to do is create IPv6 versions of the objects in <code>srcaddr<\/code> and <code>dstaddr<\/code>, then PUT them to the policy in the right place.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But here&#8217;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 <em>object<\/em>, which consists of a CIDR and a name, or an address <em>group<\/em>, consisting of either other groups or individual objects, and the name. Regardless of whether the address is an object or a group, it&#8217;s referred to by <em>name.<\/em> Still not a big problem.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Except that address groups and objects have different API endpoints! And you can&#8217;t create a group unless all of its constituent members are already created! And groups can contain other groups as members! It&#8217;s recursive! I&#8217;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 <em>a lot<\/em> of time on this problem (I&#8217;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&#8230; and eventually, I solved it!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This really deserves a diagram, but I&#8217;ll try to explain it best I can in words. The only way to determine if the <code>name<\/code> 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:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>$convertedObjects<\/code> &#8211; a table to hold the original IPv4 name of an address or group as the key, and the new IPv6 name as the value<\/li>\n\n\n\n<li>and <code>$processedGroups<\/code> &#8211; a list to hold groups to identify if they&#8217;ve been processed. If it&#8217;s been processed I can grab the name and reference the above table for the new name<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The script walks through each address name encountered in the <code>policy<\/code> object, if an API call to the address object endpoint gets <code>200 OK<\/code> (and we haven&#8217;t already created the object), awesome &#8211; create an IPv6 address object with POST and add it to the <code>$convertedObjects<\/code> table. If the API call to the address object endpoint <em>fails<\/em>, call the address group endpoint&#8230; 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 <code>$processedGroups<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Once all of the groups have been iterated through and all of the objects created, we look back at the original names from the <code>policy<\/code> object. By referencing the <code>$convertedObjects<\/code> lookup table we can add the IPv6 version of each address to <code>dstaddr6<\/code> or <code>srcaddr6<\/code> with no issues! The script then PUTs the policy back and we&#8217;re done here!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I&#8217;m pretty proud of this solution, and it has saved my organization hundreds, if not thousands, of hours. Due to how different everyone&#8217;s IPv6 schemes are bound to be, it&#8217;s not a copy and paste solution for anyone else &#8211; but I hope it gets some gears turning! All of the final code is available at the repository below:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Repository link: <a href=\"https:\/\/github.com\/pmalley130\/IPv6-Transition-Helpers-Public\">https:\/\/github.com\/pmalley130\/IPv6-Transition-Helpers-Public<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>I work in an enterprise with tens of thousands of devices owned by dozens of offices and groups. I&#8217;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&#8217;s outside of the focus of this blog post. Instead, I&#8217;ll be writing [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"site-container-style":"default","site-container-layout":"default","site-sidebar-layout":"default","disable-article-header":"default","disable-site-header":"default","disable-site-footer":"default","disable-content-area-spacing":"default","footnotes":""},"categories":[2],"tags":[7,8,15,16,17],"class_list":["post-73","post","type-post","status-publish","format-standard","hentry","category-projects","tag-api","tag-automation","tag-firewalls","tag-ipv6","tag-powershell"],"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/learnwithpatrick.casa\/index.php?rest_route=\/wp\/v2\/posts\/73","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/learnwithpatrick.casa\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/learnwithpatrick.casa\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/learnwithpatrick.casa\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/learnwithpatrick.casa\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=73"}],"version-history":[{"count":0,"href":"https:\/\/learnwithpatrick.casa\/index.php?rest_route=\/wp\/v2\/posts\/73\/revisions"}],"wp:attachment":[{"href":"https:\/\/learnwithpatrick.casa\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=73"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/learnwithpatrick.casa\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=73"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/learnwithpatrick.casa\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=73"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}