Skip to content

Commit cfd5934

Browse files
authored
Fixed a PrivateRange bug for classic firewall when trying to use /32 (#25027)
* Fixed a PrivateRange bug for classic firewall when trying to use /32 in a private range. Modified the firewall/policy validation on PrivateRange to give a recommended masked address if the supplied one is improperly masked. * Updated the Network change log. Minor formatting fix for whitespace in Utils. * Fixed tab width formatting. * Fixed tab width and formatting in PSAzureFirewallPolicy.cs * Fixed formatting in Utils.cs * Changed the added utility code so the class and namespace structure is similar to the existing utility code. * Removed unused function. * Removed a few more unused methods. * Cleaned up small redundant string usage. * Added the static keyword to clarify the intent of the utility class.
1 parent 786d474 commit cfd5934

File tree

8 files changed

+2292
-944
lines changed

8 files changed

+2292
-944
lines changed

src/Network/Network.Test/ScenarioTests/AzureFirewallPolicyTests.ps1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1517,6 +1517,7 @@ function Test-AzureFirewallPolicyPrivateRangeCRUD {
15171517
$privateRange2 = @("IANAPrivateRanges", "0.0.0.0/0", "66.92.0.0/16")
15181518
$privateRange1 = @("3.3.0.0/24", "98.0.0.0/8","10.227.16.0/20")
15191519
$privateRange2Translated = @("0.0.0.0/0", "66.92.0.0/16", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "100.64.0.0/10")
1520+
$privateRange3 = @("255.255.255.255/32")
15201521

15211522
try {
15221523

@@ -1541,6 +1542,12 @@ function Test-AzureFirewallPolicyPrivateRangeCRUD {
15411542
Set-AzFirewallPolicy -InputObject $azureFirewallPolicy
15421543
$getAzureFirewallPolicy = Get-AzFirewallPolicy -Name $azureFirewallPolicyName -ResourceGroupName $rgname
15431544
Assert-AreEqualArray $privateRange2Translated $getAzureFirewallPolicy.PrivateRange
1545+
1546+
# Test Always SNAT
1547+
$azureFirewallPolicy.PrivateRange = $privateRange3
1548+
Set-AzFirewallPolicy -InputObject $azureFirewallPolicy
1549+
$getAzureFirewallPolicy = Get-AzFirewallPolicy -Name $azureFirewallPolicyName -ResourceGroupName $rgname
1550+
Assert-AreEqualArray $privateRange3 $getAzureFirewallPolicy.PrivateRange
15441551
}
15451552
finally {
15461553
# Cleanup

src/Network/Network.Test/ScenarioTests/AzureFirewallTests.ps1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,6 +1351,7 @@ function Test-AzureFirewallPrivateRangeCRUD {
13511351

13521352
$privateRange1 = @("IANAPrivateRanges", "0.0.0.0/0", "66.92.0.0/16")
13531353
$privateRange2 = @("3.3.0.0/24", "98.0.0.0/8","10.227.16.0/20","10.226.0.0/16")
1354+
$privateRange3 = @("255.255.255.255/32")
13541355

13551356
try {
13561357
# Create the resource group
@@ -1375,6 +1376,12 @@ function Test-AzureFirewallPrivateRangeCRUD {
13751376
Set-AzFirewall -AzureFirewall $azureFirewall
13761377
$getAzureFirewall = Get-AzFirewall -Name $azureFirewallName -ResourceGroupName $rgname
13771378
Assert-AreEqualArray $privateRange2 $getAzureFirewall.PrivateRange
1379+
1380+
# Test Always SNAT
1381+
$azureFirewall.PrivateRange = $privateRange3
1382+
Set-AzFirewall -AzureFirewall $azureFirewall
1383+
$getAzureFirewall = Get-AzFirewall -Name $azureFirewallName -ResourceGroupName $rgname
1384+
Assert-AreEqualArray $privateRange3 $getAzureFirewall.PrivateRange
13781385
}
13791386
finally {
13801387
# Cleanup

src/Network/Network.Test/SessionRecords/Commands.Network.Test.ScenarioTests.AzureFirewallPolicyTests/TestAzureFirewallPolicyPrivateRangeCRUD.json

Lines changed: 846 additions & 268 deletions
Large diffs are not rendered by default.

src/Network/Network.Test/SessionRecords/Commands.Network.Test.ScenarioTests.AzureFirewallTests/TestAzureFirewallPrivateRangeCRUD.json

Lines changed: 1289 additions & 606 deletions
Large diffs are not rendered by default.

src/Network/Network/ChangeLog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
--->
2020

2121
## Upcoming Release
22+
* Updated the Azure Firewall and Azure Firewall Policy setter for their respective Private Range properties
23+
- Fixed a bug that prevented using /32 in private ranges on classic Azure Firewalls
24+
- Updated the error message to provide a suggested private range when the supplied range is not correctly masked by the host identifier
2225

2326
## Version 7.6.0
2427
* Added cmdlet `New-AzVirtualApplianceNetworkProfile` to build network profile for network virtual appliance and pass as a parameter.

src/Network/Network/Common/Utils.cs

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
using Microsoft.Azure.Commands.Network.Models;
1616
using System;
1717
using System.Globalization;
18+
using System.Management.Automation;
19+
using System.Net;
1820

1921
namespace Microsoft.Azure.Commands.Network
2022
{
@@ -72,9 +74,9 @@ public static bool IsRegionRestrictedForBasicFirewall(string region)
7274
{
7375
if (!string.IsNullOrEmpty(region))
7476
{
75-
foreach(var reg in RestrictedBasicSkuFirewallRegions)
77+
foreach (var reg in RestrictedBasicSkuFirewallRegions)
7678
{
77-
if(String.Compare(reg, region, CultureInfo.CurrentCulture, CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols) == 0)
79+
if (String.Compare(reg, region, CultureInfo.CurrentCulture, CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols) == 0)
7880
{
7981
return true;
8082
}
@@ -83,4 +85,134 @@ public static bool IsRegionRestrictedForBasicFirewall(string region)
8385
return false;
8486
}
8587
}
88+
89+
public static class NetworkValidationUtils
90+
{
91+
public static void ValidateIpAddress(string ipAddress)
92+
{
93+
IPAddress ipVal;
94+
95+
if (!IPAddress.TryParse(ipAddress, out ipVal) || ipVal.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
96+
{
97+
throw new PSArgumentException(String.Format("\'{0}\' is not a valid IPv4 address", ipAddress));
98+
}
99+
}
100+
101+
public static void ValidateSubnet(string ipAddress)
102+
{
103+
ValidateIPv4CidrNotation(ipAddress);
104+
}
105+
106+
private static void ValidateIPv4CidrNotation(string ipv4Cidr)
107+
{
108+
var subnet = ipv4Cidr.Split('/');
109+
if (subnet.Length != 2)
110+
throw new PSArgumentException(String.Format("\'{0}\' is not a valid IPv4 subnet", ipv4Cidr)); // Recommend proper format? e.g. 192.168.1.0/24
111+
112+
uint address;
113+
if (!TryParseIPv4Address(subnet[0], out address))
114+
{
115+
throw new PSArgumentException(String.Format("The network prefix for \'{0}\' is not a valid IPv4 address", ipv4Cidr));
116+
}
117+
118+
int host;
119+
if (!TryParseIPv4HostIdentifier(subnet[1], out host))
120+
{
121+
throw new PSArgumentException(String.Format("\'{0}\' is not a valid IPv4 subnet. The network mask should be an integer from 0 to 32", ipv4Cidr));
122+
}
123+
124+
125+
if (!isIpAddressCorrectlyMaskByNetworkPrefixLength(address, host))
126+
{
127+
uint recommendedAddressBits;
128+
string explanation = $"The network prefix for the CIDR string '{ipv4Cidr}' should be masked according to the suffix '{host}'.";
129+
if (TryApplyMask(address, host, out recommendedAddressBits))
130+
{
131+
string recommendation = $"Try using '{uintToIPv4AddressString(recommendedAddressBits)}' instead.";
132+
throw new PSArgumentException($"{explanation} {recommendation}");
133+
}
134+
else
135+
{
136+
throw new PSArgumentException($"{explanation}");
137+
}
138+
}
139+
}
140+
141+
private static string uintToIPv4AddressString(uint address)
142+
{
143+
var bytes = new byte[4];
144+
for (int i = 0; i < 4; i++)
145+
{
146+
bytes[i] = (byte)(address >> (8 * (3 - i)));
147+
}
148+
149+
return String.Join(".", bytes);
150+
}
151+
152+
private static bool isValidHostIdentifier(int host)
153+
{
154+
return host >= 0 && host <= 32;
155+
}
156+
157+
/// <summary>
158+
/// Checks to see if an IP address (192.168.10.0) is correctly masked by the host identifier in IPv4 CIDR notation.
159+
/// ex. 192.168.1.0/24 is correctly masked, but 192.168.1.0/23 is not correctly masked
160+
/// </summary>
161+
/// <param name="ipAddress"></param>
162+
/// <param name="networkPrefixLength"></param>
163+
/// <returns></returns>
164+
private static bool isIpAddressCorrectlyMaskByNetworkPrefixLength(uint ipAddress, int networkPrefixLength)
165+
{
166+
const uint fullMask = UInt32.MaxValue;
167+
if (!isValidHostIdentifier(networkPrefixLength)) return false;
168+
if (networkPrefixLength == 32 && ipAddress != fullMask) return false;
169+
if (networkPrefixLength == 32 && ipAddress == fullMask) return true;
170+
171+
return (ipAddress << networkPrefixLength) == 0;
172+
}
173+
174+
/// <summary>
175+
/// Tries to mask the non-network bits with 0, according to the prefixLength (if the length is valid).
176+
/// ex. 192.168.111.234/16 -> 192.168.0.0
177+
/// </summary>
178+
/// <param name="address"></param>
179+
/// <param name="prefixLength"></param>
180+
/// <param name="maskedAddress"></param>
181+
/// <returns></returns>
182+
private static bool TryApplyMask(uint address, int prefixLength, out uint maskedAddress)
183+
{
184+
maskedAddress = 0;
185+
if (!isValidHostIdentifier(prefixLength)) return false;
186+
int shift = (32 - prefixLength);
187+
maskedAddress = (address >> shift) << shift;
188+
189+
return true;
190+
}
191+
192+
/// <summary>
193+
/// Try to parse an IPv4 address string into a UInt32
194+
/// </summary>
195+
/// <param name="address"></param>
196+
/// <param name="parsed"></param>
197+
/// <returns></returns>
198+
private static bool TryParseIPv4Address(string address, out uint parsed)
199+
{
200+
parsed = 0;
201+
if (!IPAddress.TryParse(address, out var addr)) return false;
202+
byte[] bytes = addr.GetAddressBytes();
203+
204+
for (int i = 0; i < 4; i++)
205+
{
206+
int amountToShift = (8 * (3 - i)); // 24, 16, 8, 0
207+
parsed += ((uint)bytes[i] << amountToShift);
208+
}
209+
210+
return true;
211+
}
212+
213+
private static bool TryParseIPv4HostIdentifier(string host, out int parsed)
214+
{
215+
return Int32.TryParse(host, out parsed) && isValidHostIdentifier(parsed);
216+
}
217+
}
86218
}

src/Network/Network/Models/AzureFirewall/PSAzureFirewall.cs

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,8 @@ public void RemovePublicIpAddress(PSPublicIpAddress publicIpAddress)
358358
throw new ArgumentException($"Public IP Address {publicIpAddress.Id} is not attached to firewall {this.Name}");
359359
}
360360

361-
if (this.ManagementIpConfiguration != null) {
361+
if (this.ManagementIpConfiguration != null)
362+
{
362363
if (ipConfigToRemove.Subnet != null)
363364
{
364365
ipConfigToRemove.PublicIpAddress = null;
@@ -508,43 +509,12 @@ private void ValidatePrivateRange(string[] privateRange)
508509
continue;
509510

510511
if (ip.Contains("/"))
511-
ValidateMaskedIpAddress(ip);
512+
NetworkValidationUtils.ValidateSubnet(ip);
512513
else
513-
ValidateSingleIpAddress(ip);
514-
}
515-
}
516-
517-
private void ValidateSingleIpAddress(string ipAddress)
518-
{
519-
IPAddress ipVal;
520-
if (!IPAddress.TryParse(ipAddress, out ipVal) || ipVal.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
521-
{
522-
throw new PSArgumentException(String.Format("\'{0}\' is not a valid private range ip address", ipAddress));
514+
NetworkValidationUtils.ValidateIpAddress(ip);
523515
}
524516
}
525517

526-
private void ValidateMaskedIpAddress(string ipAddress)
527-
{
528-
var split = ipAddress.Split('/');
529-
if (split.Length != 2)
530-
throw new PSArgumentException(String.Format("\'{0}\' is not a valid private range ip address", ipAddress));
531-
532-
// validate the ip
533-
ValidateSingleIpAddress(split[0]);
534-
535-
// validate mask
536-
var bit = 0;
537-
if (!Int32.TryParse(split[1], out bit) || bit < 0 || bit > 32)
538-
throw new PSArgumentException(String.Format("\'{0}\' is not a valid private range ip address, subnet mask should between 0 and 32", ipAddress));
539-
540-
// validated that unmasked bits are 0
541-
var splittedIp = split[0].Split('.');
542-
var ip = Int32.Parse(splittedIp[0]) << 24;
543-
ip += (Int32.Parse(splittedIp[1]) << 16) + (Int32.Parse(splittedIp[2]) << 8) + Int32.Parse(splittedIp[3]);
544-
if (ip << bit != 0)
545-
throw new PSArgumentException(String.Format("\'{0}\' is not a valid private range ip address, bits not covered by subnet mask should be all 0", ipAddress));
546-
}
547-
548518
#endregion
549519

550520
#region DNS Proxy Validation

src/Network/Network/Models/AzureFirewallPolicy/PSAzureFirewallPolicy.cs

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -91,43 +91,11 @@ private void ValidatePrivateRange(string[] privateRange)
9191
continue;
9292

9393
if (ip.Contains("/"))
94-
ValidateMaskedIpAddress(ip);
94+
NetworkValidationUtils.ValidateSubnet(ip);
9595
else
96-
ValidateSingleIpAddress(ip);
96+
NetworkValidationUtils.ValidateIpAddress(ip);
9797
}
9898
}
99-
100-
private void ValidateSingleIpAddress(string ipAddress)
101-
{
102-
IPAddress ipVal;
103-
if (!IPAddress.TryParse(ipAddress, out ipVal) || ipVal.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
104-
{
105-
throw new AzPSArgumentException(String.Format(Resources.InvalidPrivateIPRange, ipAddress), nameof(ipAddress), ErrorKind.UserError);
106-
}
107-
}
108-
109-
private void ValidateMaskedIpAddress(string ipAddress)
110-
{
111-
var split = ipAddress.Split('/');
112-
if (split.Length != 2)
113-
throw new AzPSArgumentException(String.Format(Resources.InvalidPrivateIPRange, ipAddress), nameof(ipAddress), ErrorKind.UserError);
114-
115-
// validate the ip
116-
ValidateSingleIpAddress(split[0]);
117-
118-
// validate mask
119-
var bit = 0;
120-
if (!Int32.TryParse(split[1], out bit) || bit < 0 || bit > 32)
121-
throw new AzPSArgumentException(String.Format(Resources.InvalidPrivateIPRangeMask, ipAddress), nameof(ipAddress), ErrorKind.UserError);
122-
123-
// validated that unmasked bits are 0
124-
var splittedIp = split[0].Split('.');
125-
var ip = Int32.Parse(splittedIp[0]) << 24;
126-
ip += (Int32.Parse(splittedIp[1]) << 16) + (Int32.Parse(splittedIp[2]) << 8) + Int32.Parse(splittedIp[3]);
127-
if ((ip << bit != 0) && (ip << bit != -1))
128-
throw new AzPSArgumentException(String.Format(Resources.InvalidPrivateIPRangeUnmaskedBits, ipAddress), nameof(ipAddress), ErrorKind.UserError);
129-
}
130-
13199
#endregion
132100
}
133101
}

0 commit comments

Comments
 (0)