Recently I put together a Windows Installer package (MSI file) for our team to use internally.
The app I was installing communicates with other apps on our internal network, and it requires certain network ports be open for ICMP and TCP communication.
My first thought on solving this issue was to figure out where Windows Firewall Manager stores its firewall settings and then tell the firewall to read the new settings. Indeed, I found the location in the registry and I was able to successfully create the new settings. I was also able to kick the firewall using the commands “net stop “windows firewall”” followed immediately by “net start “windows firewall””. Everything seemed great.
But there were issues. For instance, during install, a little message showed up in the taskbar that the firewall was stopping. That’s confusing to the end user, especially because there’s no message that shows up saying the firewall is starting back up. Modifying the registry directly felt dirty. This was not a maintainable solution. The whole approach was a hack.
So I found a better way: the “netsh advfirewall” command. It’s available on Windows 7 and up.
Say you wanted to open ports 8008 and 8009 for inbound TCP traffic on the local network. Here’s the full command to add that rule:
netsh advfirewall firewall add rule name="My Firewall Rule" dir=in action=allow enable=yes protocol=tcp interfacetype=lan localport=8008,8009 remoteport=8008,8009
Even better, the firewall automatically starts applying this rule without a restart. There are many other advfirewall parameters, but you can explore those on your own.
Running these commands in the context of the installer was a little more challenging. I needed to add multiple rules, and I needed to handle removing the rules on uninstall. Let’s start with the variables I used to simplify things:
<?define Quote = """ ?> <?define IpRange = "w.x.y.z-w.x.y.z" ?> <?define OpenPorts = "8008,8009" ?> <?define FirewallRuleName = "$(var.Quote)Open 8008 and 8009 for MyProject$(var.Quote)"?> <?define FirewallAddRule = "advfirewall firewall add rule name=$(var.FirewallRuleName) dir=in action=allow enable=yes" ?> <?define FirewallRuleICMP = "remoteip=$(var.IpRange) protocol=icmpv4 interfacetype=lan" ?> <?define FirewallRuleTCP = "remoteip=$(var.IpRange) protocol=tcp interfacetype=lan localport=$(var.OpenPorts) remoteport=$(var.OpenPorts)" ?> <?define FirewallRemoveRule = "advfirewall firewall delete rule name=$(var.FirewallRuleName)" ?>
The Quote variable handles the cases where we need to embed quotes in the command string. The IpRange variable locks this rule down to a specific set of IPs. Fill in your own values for w.x.y.z. Like many other advfirewall commands, it’s optional. FirewallAddRule has the meat of the command string, and then there are individual suffixes for the ICMP and TCP rules. The ICMP rule doesn’t accept ports. The FirewallRemoveRule ensures that all rules matching the given name will be removed on uninstall.
Here’s the code for executing these rules. First, for install:
<CustomAction Id="SetUpdateFirewallICMPCmd" Property="UpdateFirewallICMP" Execute="immediate" Value=""netsh" $(var.FirewallAddRule) $(var.FirewallRuleICMP)" /> <CustomAction Id="UpdateFirewallICMP" BinaryKey="WixCA" DllEntry="CAQuietExec" Execute="deferred" Return="ignore" Impersonate="no" /> <CustomAction Id="SetUpdateFirewallTCPCmd" Property="UpdateFirewallTCP" Execute="immediate" Value=""netsh" $(var.FirewallAddRule) $(var.FirewallRuleTCP)" /> <CustomAction Id="UpdateFirewallTCP" BinaryKey="WixCA" DllEntry="CAQuietExec" Execute="deferred" Return="ignore" Impersonate="no" /> <InstallExecuteSequence> <Custom Action="SetUpdateFirewallICMPCmd" Before="InstallFinalize"><![CDATA[NOT REMOVE]]></Custom> <Custom Action="UpdateFirewallICMP" After="SetUpdateFirewallICMPCmd"><![CDATA[NOT REMOVE]]></Custom> <Custom Action="SetUpdateFirewallTCPCmd" After="UpdateFirewallICMP"><![CDATA[NOT REMOVE]]></Custom> <Custom Action="UpdateFirewallTCP" After="SetUpdateFirewallTCPCmd"><![CDATA[NOT REMOVE]]><Custom> </InstallExecuteSequence>
There are four custom actions that execute in sequence after the InstallFinalize stage. The first action establishes the command line for adding the ICMP rule using the variables defined above. CAQuietExec requires that the actual app command (netsh in this case) be quoted. The second action executes the command line established in the first command. Using multiple CAQuietExecs requires configuring the command line as a property with a matching ID in the following action. Changing firewall settings requires Windows elevated privilege, so we use deferred execution with no impersonation. The final two commands set the TCP rule in the same manner.
The funky notation ![CDATA[NOT REMOVE]] tells the installer that these commands should run whenever we’re not actually uninstalling the product (e.g. installs, reinstalls, upgrades, repairs).
<CustomAction Id="SetUpdateFirewallRemoveCmd" Property="UpdateFirewallRemove" Execute="immediate" Value=""netsh" $(var.FirewallRemoveRule)" /> <CustomAction Id="UpdateFirewallRemove" BinaryKey="WixCA" DllEntry="CAQuietExec" Execute="deferred" Return="ignore" Impersonate="no" /> <InstallExecuteSequence> <Custom Action="SetUpdateFirewallRemoveCmd" Before="InstallFinalize"> <![CDATA[(NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")]]> </Custom> <Custom Action="UpdateFirewallRemove" After="SetUpdateFirewallRemoveCmd"> <![CDATA[(NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")]]> </Custom> </InstallExecuteSequence>
We set up the command line to remove the firewall settings, then invoke that command line with elevated privileges. We only remove the firewall settings when the product is being uninstalled (not installed, reinstalled, or upgraded)
One additional note. I really wanted to lock down the firewall rule to a specific app, so I was excited that advfirewall has just the option I was looking for:
allowedprogram program=C:\MyApp\MyApp.exe name="My Application"
But unfortunately in my particular case the app I was installing is not actually the app that does all the LAN communication. My app spawns other apps that do the communication, and the locations and names of those apps is not necessarily known at install time. Hence we chose to lock down the firewall rules to specific (small) range of IPs, so that if this install package gets out in the wild it’s not unnecessarily introducing security holes.