Windows Firewall traffic audit

Windows Firewall is a Windows built-in security feature that helps protect your system by filtering incoming and outgoing traffic based on predefined rules. While the firewall does a great job of blocking unwanted traffic, there are times when you need to dig deeper. For instance, you might want to:

  • Verify that specific traffic is being allowed or blocked as expected.
  • Troubleshoot connectivity issues by identifying which firewall rule is being triggered.
  • Audit your firewall rules to ensure they align with your security policies.

One of the particularities of Windows Firewall is the order in which it evaluates rules. All rules are divided into four groups. Within each category, rules are evaluated from the most specific to the least specific. For example, a rule that specifies four criteria is selected over a rule that specifies only three criteria. The evaluation is satisfied and stops with the first rule that matches within the group.

The four groups are as follows:

  1. Authenticated bypass
    These are rules in which the "Override block rules" option is selected together with "Allow if secure", to allow network traffic that would otherwise be blocked. They are intended for allowing through highly authorized network administrators and maintenance.
  2. Block connection
    The Block rules have a higher priority than all other rules.
  3. Allow connection
    Rules that allow inbound network traffic. This rules are required since the default behavior is to block unsolicited inbound network traffic.
  4. Default profile behavior
    The default behavior is to block unsolicited inbound network traffic, but to allow all outbound network traffic. This default behavior can be changed via the Domain Profile, Private Profile, and Public Profile tabs of the Firewall rule details.

So, if you have a rule that allows all outbound traffic to 1.1.1.0/24 and one that blocks all outbound traffic to 1.1.1.1/32, traffic to 1.1.1.1 will always be blocked.

This, and other particularities such as rules applying only to certain apps, makes visual inspection of the ruleset a chore prone to error. I know because I've been there.

In this blog post I am sharing a PowerShell script that helps sysadmins, security professionals and general network security enthusiasts audit the behaviour of Windows Firewall by matching traffic parameters to the active rule set.

Check-traffic.ps1

The script provided, check-traffic.ps1, is a robust tool designed to check specific traffic against Windows Firewall rules and determine which rule and action are matched. It offers flexibility by allowing you to specify various traffic details such as source IP, destination IP, ports, protocol, direction, and even the program associated with the traffic.

Key Features of the Script

  1. Parameterized Input: The script accepts several parameters, including source and destination IPs, ports, protocol, direction, and program. This allows you to tailor the traffic check to your specific needs.
  2. Validation Functions: The script includes helper functions to validate inputs such as IP addresses, ports, protocols, and directions. This ensures that the inputs are correct before proceeding with the rule checks.
  3. Rule Matching Logic: The script uses advanced logic to match traffic against firewall rules. It checks for IP matches (including subnets), port ranges, protocol compatibility, and program associations.
  4. Specificity Scoring: When multiple rules match the traffic, the script calculates a specificity score for each rule based on how narrowly defined the rule is. The rule with the highest score (most specific) is considered the best match.
  5. Debug Mode: The script includes a debug mode that provides detailed logs of the rule processing, making it easier to troubleshoot and understand how the script is making its decisions.
  6. Performance: The script leverages netsh commands instead of built-in PowerShell functions for faster performance. In my local tests, this makes the script at least 50x faster. Your results may vary depending on your system, your ruleset, etc.
  7. Final Decision: The script outputs the most specific rule that matches the traffic, along with its action (allow or block). If no rules match, it defaults to blocking inbound traffic and allowing outbound traffic.

How the Script Works

  1. Input Validation: The script starts by validating the input parameters to ensure they are correct and within acceptable ranges.
  2. Retrieve Firewall Rules: Instead of using PowerShell cmdlets, the script uses netsh to retrieve firewall rules. This approach is faster and provides detailed information about each rule.
  3. Rule Processing: The script processes each rule, checking if it matches the specified traffic details. It uses helper functions to match IPs, ports, protocols, and programs.
  4. Specificity Scoring: For each matched rule, the script calculates a specificity score based on how narrowly defined the rule is. Rules with more specific criteria (e.g., specific IPs, ports, or programs) receive higher scores.
  5. Output Results: The script outputs the most specific rule that matches the traffic, along with its action and detailed criteria. If no rules match, it applies a default action based on the traffic direction.

Example Usage

Here’s an example of how you might run the script:

.\check-traffic.ps1 -srcPort 12345 -dstPort 443 -srcIP "192.168.1.100" -dstIP "80.58.21.66" -protocol "TCP" -direction "Outbound" -debug

This command checks for outbound TCP traffic from 192.168.1.100 on port 12345 to 80.58.21.66 on port 443. The -debug flag enables detailed logging, which can help you understand how the script is processing the rules.

Output (debug trimmed out):

[2025-01-24 13:03:07] [INFO] Final decision: Applying Block from most specific rule:
[2025-01-24 13:03:07] [INFO] Rule Name: MyRuleTest
[2025-01-24 13:03:07] [INFO]   Action    : Block
[2025-01-24 13:03:07] [INFO]   Score     : 4
[2025-01-24 13:03:07] [INFO]   Direction : Outbound
[2025-01-24 13:03:07] [INFO]   Criteria  : RemoteIP: 80.58.21.0/24, LocalPort: 12345, RemotePort: 443, Protocol: TCP
[2025-01-24 13:03:07] [INFO]   Full Match:
[2025-01-24 13:03:07] [INFO]     LocalIP    : Any
[2025-01-24 13:03:07] [INFO]     RemoteIP   : 80.58.21.0/24
[2025-01-24 13:03:07] [INFO]     LocalPort  : 12345
[2025-01-24 13:03:07] [INFO]     RemotePort : 443
[2025-01-24 13:03:07] [INFO]     Protocol   : TCP
[2025-01-24 13:03:07] [INFO]     Program    : Any
[2025-01-24 13:03:07] [INFO] ----------------------------------------
[2025-01-24 13:03:07] [INFO] Firewall check completed

Using Measure-Command we can see the script took less than 2 seconds to produce results:

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 1
Milliseconds      : 70
Ticks             : 10702873
TotalDays         : 1.23875844907407E-05
TotalHours        : 0.000297302027777778
TotalMinutes      : 0.0178381216666667
TotalSeconds      : 1.0702873
TotalMilliseconds : 1070.2873

Practical Applications

Troubleshooting Connectivity Issues

Imagine you’re experiencing connectivity issues with a specific application. You suspect that the Windows Firewall might be blocking the traffic. By running this script, you can quickly identify which firewall rule is being triggered and whether it’s allowing or blocking the traffic. This can save you hours of manual troubleshooting.

Auditing Firewall Rules

Regularly auditing your firewall rules is a best practice for maintaining a secure environment. This script can help you verify that your rules are configured correctly and that they’re effectively enforcing your security policies.

Automating Security Checks

If you’re responsible for managing multiple systems, manually checking firewall rules on each one can be time-consuming. By automating this process with PowerShell, you can perform security checks across all your systems efficiently.

Conclusion

PowerShell is an incredibly powerful tool for managing and monitoring Windows Firewall. The check-traffic.ps1 script we’ve explored in this post provides a simple yet effective way to check specific traffic against your firewall rules, giving you valuable insights into your network security. Whether you’re troubleshooting connectivity issues, auditing your firewall rules, or automating security checks, this script is a great addition to your toolkit.

Feel free to modify and expand the script to suit your needs. And if you have any questions or suggestions, don’t hesitate to leave a comment below. Happy scripting!

Disclaimer: This script is provided as-is, without any warranties or guarantees. Use it at your own risk, and always test in a non-production environment before deploying in a live setting.

Code

param (
    [int]$srcPort,
    [int]$dstPort,
    [string]$srcIP,
    [string]$dstIP,
    [string]$protocol = "Any",
    [string]$direction = "Any",
    [string]$program = $null,
    [switch]$debug
)

# Helper function to format logs
function Write-Log {
    param (
        [string]$message,
        [string]$level = "INFO"
    )
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    Write-Output "[$timestamp] [$level] $message"
}

# Helper functions for various validations
function Test-ValidIPOrSubnet {
    param ([string]$ip)
    try {
        if ($ip -eq "Any") { return $true }
        if ($ip -match '^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$') {
            if ($ip -match '/') {
                $ipPart, $prefix = $ip -split '/'
                [IPAddress]$ipPart | Out-Null
                if ($prefix -lt 0 -or $prefix -gt 32) { return $false }
            } else {
                [IPAddress]$ip | Out-Null
            }
            return $true
        }
        return $false
    } catch { return $false }
}

function Test-ValidPort {
    param ([int]$port)
    return ($port -ge 0 -and $port -le 65535)
}

function Test-ValidProtocol {
    param ([string]$protocol)
    return $protocol -in @("Any", "TCP", "UDP", "ICMP")
}

function Test-ValidDirection {
    param ([string]$direction)
    return $direction -in @("Any", "Inbound", "Outbound")
}

# Helper functions for IP/Subnet, port and protocol matching
function Test-IPMatch {
    param ([string]$ruleIP, [string]$checkIP)
    try {
        if ($ruleIP -eq "Any") { return $true }
        if ($ruleIP -like "*/*") {
            $networkPart, $cidr = $ruleIP -split '/'
            $cidr = [int]$cidr
            $network = [IPAddress]$networkPart
            $checkIPAddr = [IPAddress]$checkIP

            # Convert IPs to uint32 in network order (big-endian)
            $networkBytes = $network.GetAddressBytes()
            [Array]::Reverse($networkBytes)
            $networkUint = [BitConverter]::ToUInt32($networkBytes, 0)

            $checkBytes = $checkIPAddr.GetAddressBytes()
            [Array]::Reverse($checkBytes)
            $checkUint = [BitConverter]::ToUInt32($checkBytes, 0)

            $mask = if ($cidr -eq 0) { 0 } else { [uint32]::MaxValue -shl (32 - $cidr) }

            return ($networkUint -band $mask) -eq ($checkUint -band $mask)
        }
        return ($ruleIP -eq $checkIP)
    } catch { return $false }
}

function Test-PortMatch {
    param ([string]$rulePort, [int]$checkPort)
    try {
        if ($rulePort -eq "Any") { return $true }
        if ($rulePort -like "*-*") {
            $start, $end = $rulePort -split '-'
            return ($checkPort -ge $start) -and ($checkPort -le $end)
        }
        return ($checkPort -eq $rulePort)
    } catch { return $false }
}

function Test-ProgramMatch {
    param ([string]$ruleProgram, [string]$checkProgram)
    try {
        if ($ruleProgram -eq "Any" -or -not $checkProgram) { return $true }
        return ($ruleProgram -eq $checkProgram)
    } catch { return $false }
}


# Pull and parse all rules. We use netsh instead of Powershell because it is much faster.
function Get-FirewallRules {
    $rules = @()
    $output = netsh advfirewall firewall show rule name=all verbose | Out-String
    
    # Split rules by "Rule Name:" and process each block
    $blocks = $output -split "(?ms)^Rule Name:" | Where-Object { $_ -match "\S" }
    
    foreach ($block in $blocks) {
        $rule = @{}
        $lines = $block -split "`r`n"
        
        # Extract rule name (first non-empty line after splitting)
        $ruleNameLine = $lines | Where-Object { $_ -match "\S" } | Select-Object -First 1
        if ($ruleNameLine -match "-+") {
            $ruleName = ($ruleNameLine -split "-+")[0].Trim()
        } else {
            $ruleName = $ruleNameLine.Trim()
        }
        if (-not $ruleName) { continue }
        $rule["Rule Name"] = $ruleName
        
        # Process remaining lines for key-value pairs
        foreach ($line in $lines) {
            if ($line -match "^\s*([^:]+?)\s*:\s*(.+?)\s*$") {
                $key = $matches[1].Trim()
                $value = $matches[2].Trim()
                $rule[$key] = $value
            }
        }
        
        # Map direction and set defaults
        $rule["Direction"] = switch ($rule["Direction"]) {
            "In" { "Inbound" }
            "Out" { "Outbound" }
            default { $_ }
        }
        foreach ($field in @("LocalIP", "RemoteIP", "LocalPort", "RemotePort", "Protocol", "Program")) {
            if (-not $rule.ContainsKey($field)) { $rule[$field] = "Any" }
        }
        
        # Only process enabled rules
        if ($rule["Enabled"] -eq "Yes") {
            $rules += $rule
        }
    }
    return $rules
}


# Validate inputs
if (-not (Test-ValidIPOrSubnet $srcIP)) { Write-Log "Invalid source IP: $srcIP" -level "ERROR"; exit 1 }
if (-not (Test-ValidIPOrSubnet $dstIP)) { Write-Log "Invalid destination IP: $dstIP" -level "ERROR"; exit 1 }
if (-not (Test-ValidPort $srcPort)) { Write-Log "Invalid source port: $srcPort" -level "ERROR"; exit 1 }
if (-not (Test-ValidPort $dstPort)) { Write-Log "Invalid destination port: $dstPort" -level "ERROR"; exit 1 }
if (-not (Test-ValidProtocol $protocol)) { Write-Log "Invalid protocol: $protocol - Valid protocols are: ICMP, TCP, UDP, ANY" -level "ERROR"; exit 1 }
if (-not (Test-ValidDirection $direction)) { Write-Log "Invalid direction: $direction" -level "ERROR"; exit 1 }

# Process rules
$firewallRules = Get-FirewallRules
$matchedRules = @()

foreach ($rule in $firewallRules) {
    try {
        if ($debug) {
            Write-Log "Processing rule: $($rule['Rule Name'])" -level "DEBUG"
            Write-Log "Direction: $($rule['Direction']), Protocol: $($rule['Protocol'])" -level "DEBUG"
            Write-Log "Source IP: $($rule['LocalIP']), Destination IP: $($rule['RemoteIP'])" -level "DEBUG"
            Write-Log "Source Port: $($rule['LocalPort']), Destination Port: $($rule['RemotePort'])" -level "DEBUG"
            Write-Log "Program: $($rule['Program'])" -level "DEBUG"
        }

        # Skip if direction/protocol don't match
        if ($direction -ne "Any" -and $rule["Direction"] -ne $direction) { continue }
        if ($protocol -ne "Any" -and $rule["Protocol"] -ne "Any" -and $rule["Protocol"] -ne $protocol) { continue }

        # Check IPs/Ports/Program
        $ipMatch = (Test-IPMatch $rule["LocalIP"] $srcIP) -and (Test-IPMatch $rule["RemoteIP"] $dstIP)
        $portMatch = ($protocol -eq "ICMP") -or (
            (Test-PortMatch $rule["LocalPort"] $srcPort) -and 
            (Test-PortMatch $rule["RemotePort"] $dstPort)
        )
        $programMatch = Test-ProgramMatch $rule["Program"] $program

        if ($ipMatch -and $portMatch -and $programMatch) {
            $matchedRules += $rule
        }
    } catch {
        Write-Log "Error processing rule: $_" -level "ERROR"
    }
}

if ($matchedRules.Count -gt 0) {
    # Calculate specificity score and details for each matched rule
    $scoredRules = $matchedRules | ForEach-Object {
        $score = 0
        $criteria = @()
        
        if ($_.LocalIP -ne "Any") { 
            $score++
            $criteria += "LocalIP: $($_.LocalIP)"
        }
        if ($_.RemoteIP -ne "Any") { 
            $score++
            $criteria += "RemoteIP: $($_.RemoteIP)"
        }
        if ($_.LocalPort -ne "Any") { 
            $score++
            $criteria += "LocalPort: $($_.LocalPort)"
        }
        if ($_.RemotePort -ne "Any") { 
            $score++
            $criteria += "RemotePort: $($_.RemotePort)"
        }
        if ($_.Protocol -ne "Any") { 
            $score++
            $criteria += "Protocol: $($_.Protocol)"
        }
        if ($_.Program -ne "Any") { 
            $score++
            $criteria += "Program: $($_.Program)"
        }

        [PSCustomObject]@{
            Rule = $_
            Score = $score
            Criteria = $criteria -join ", "
            Direction = $_.Direction
            Action = $_.Action
        }
    } | Sort-Object -Property Score -Descending

    # Display all matched rules
    if ($debug) {
        Write-Log "`nMatched Rules (ordered by specificity):" -level "DEBUG"
        $scoredRules | ForEach-Object {
            Write-Log "Rule Name: $($_.Rule['Rule Name'])" -level "DEBUG"
            Write-Log "  Action    : $($_.Action)" -level "DEBUG"
            Write-Log "  Score     : $($_.Score)" -level "DEBUG"
            Write-Log "  Direction : $($_.Direction)" -level "DEBUG"
            Write-Log "  Criteria  : $($_.Criteria)" -level "DEBUG"
            Write-Log "  Full Match:" -level "DEBUG"
            Write-Log "    LocalIP    : $($_.Rule.LocalIP)" -level "DEBUG"
            Write-Log "    RemoteIP   : $($_.Rule.RemoteIP)" -level "DEBUG"
            Write-Log "    LocalPort  : $($_.Rule.LocalPort)" -level "DEBUG"
            Write-Log "    RemotePort : $($_.Rule.RemotePort)" -level "DEBUG"
            Write-Log "    Protocol   : $($_.Rule.Protocol)" -level "DEBUG"
            Write-Log "    Program    : $($_.Rule.Program)" -level "DEBUG"
            Write-Log "----------------------------------------" -level "DEBUG"
        }
    }

    # Final decision
    $mostSpecificRule = $scoredRules[0]
    Write-Log "Final decision: Applying $($mostSpecificRule.Action) from most specific rule: $($mostSpecificRule.RuleName)" -level "INFO"
    Write-Log "Rule Name: $($mostSpecificRule.Rule['Rule Name'])" -level "INFO"
    Write-Log "  Action    : $($mostSpecificRule.Action)" -level "INFO"
    Write-Log "  Score     : $($mostSpecificRule.Score)" -level "INFO"
    Write-Log "  Direction : $($mostSpecificRule.Direction)" -level "INFO"
    Write-Log "  Criteria  : $($mostSpecificRule.Criteria)" -level "INFO"
    Write-Log "  Full Match:" -level "INFO"
    Write-Log "    LocalIP    : $($mostSpecificRule.Rule.LocalIP)" -level "INFO"
    Write-Log "    RemoteIP   : $($mostSpecificRule.Rule.RemoteIP)" -level "INFO"
    Write-Log "    LocalPort  : $($mostSpecificRule.Rule.LocalPort)" -level "INFO"
    Write-Log "    RemotePort : $($mostSpecificRule.Rule.RemotePort)" -level "INFO"
    Write-Log "    Protocol   : $($mostSpecificRule.Rule.Protocol)" -level "INFO"
    Write-Log "    Program    : $($mostSpecificRule.Rule.Program)" -level "INFO"
    Write-Log "----------------------------------------" -level "INFO"
} else {
    $defaultAction = if ($direction -eq "Inbound") { "Block" } else { "Allow" }
    Write-Log "No matching rules found. Default action for $direction traffic: $defaultAction" -level "INFO"
}

Write-Log "Firewall check completed" -level "INFO"

Subscribe to Cloud Networking Pro

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe