• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

loresoft / HashGate / 22023434986

14 Feb 2026 08:02PM UTC coverage: 79.039% (+3.4%) from 75.643%
22023434986

push

github

pwelter34
Add IP address whitelist utility with CIDR and tests

178 of 248 branches covered (71.77%)

Branch coverage included in aggregate %.

42 of 44 new or added lines in 1 file covered. (95.45%)

365 of 439 relevant lines covered (83.14%)

14.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.08
/src/HashGate.AspNetCore/IpAddressWhitelist.cs
1
using System.Net;
2

3
namespace HashGate.AspNetCore;
4

5
/// <summary>
6
/// Provides methods to check if an IP address is allowed based on a whitelist of addresses and networks.
7
/// </summary>
8
/// <remarks>
9
/// This class provides IP address filtering capabilities for security purposes, supporting both individual
10
/// IP addresses and CIDR network ranges for IPv4 and IPv6 addresses.
11
/// </remarks>
12
public static class IpAddressWhitelist
13
{
14
    /// <summary>
15
    /// Determines whether the specified IP address string is allowed based on the provided allowed addresses and networks.
16
    /// </summary>
17
    /// <param name="ipAddress">The IP address string to check. Can be null or empty.</param>
18
    /// <param name="allowedAddresses">A list of allowed IP addresses in string format. Can be null or empty.</param>
19
    /// <param name="allowedNetworks">A list of allowed networks in CIDR notation (e.g., "192.168.1.0/24"). Can be null or empty.</param>
20
    /// <returns>
21
    /// <c>true</c> if the IP address is allowed; otherwise, <c>false</c>.
22
    /// Returns <c>true</c> if no restrictions are configured (both lists are null or empty).
23
    /// Returns <c>false</c> if the IP address is null, empty, or cannot be parsed as a valid IP address.
24
    /// </returns>
25
    /// <remarks>
26
    /// <para>
27
    /// This is a convenience method that parses the IP address string and delegates to the
28
    /// <see cref="IsIpAllowed(IPAddress?, IReadOnlyList{string}?, IReadOnlyList{string}?)"/> overload.
29
    /// </para>
30
    /// <para>
31
    /// If no IP restrictions are configured (both <paramref name="allowedAddresses"/> and
32
    /// <paramref name="allowedNetworks"/> are null or empty), all IP addresses are allowed.
33
    /// </para>
34
    /// <para>
35
    /// If the IP address string cannot be parsed as a valid IPv4 or IPv6 address, the method returns <c>false</c>.
36
    /// </para>
37
    /// </remarks>
38
    /// <example>
39
    /// <code>
40
    /// var allowedIPs = new[] { "192.168.1.100", "10.0.0.1" };
41
    /// var allowedNetworks = new[] { "192.168.0.0/16", "10.0.0.0/8" };
42
    ///
43
    /// bool isAllowed1 = SecurityKeyWhitelist.IsIpAllowed("192.168.1.50", allowedIPs, allowedNetworks);
44
    /// // Returns true because 192.168.1.50 is in the 192.168.0.0/16 network
45
    ///
46
    /// bool isAllowed2 = SecurityKeyWhitelist.IsIpAllowed("invalid-ip", allowedIPs, allowedNetworks);
47
    /// // Returns false because "invalid-ip" cannot be parsed as an IP address
48
    ///
49
    /// bool isAllowed3 = SecurityKeyWhitelist.IsIpAllowed("203.0.113.1", null, null);
50
    /// // Returns true because no restrictions are configured
51
    /// </code>
52
    /// </example>
53
    public static bool IsIpAllowed(string? ipAddress, IReadOnlyList<string>? allowedAddresses, IReadOnlyList<string>? allowedNetworks)
54
    {
55
        // If no IP restrictions are configured, allow all
56
        if (!(allowedAddresses?.Count > 0 || allowedNetworks?.Count > 0))
33✔
57
            return true;
2✔
58

59
        if (ipAddress == null)
31✔
60
            return false;
1✔
61

62
        if (IPAddress.TryParse(ipAddress, out var parsedIp))
30✔
63
            return IsIpAllowed(parsedIp, allowedAddresses, allowedNetworks);
26✔
64

65
        // If parsing fails, treat as not allowed
66
        return false;
4✔
67
    }
68

69
    /// <summary>
70
    /// Determines whether the specified IP address is allowed based on the provided allowed addresses and networks.
71
    /// </summary>
72
    /// <param name="ipAddress">The IP address to check. Can be null.</param>
73
    /// <param name="allowedAddresses">A list of allowed IP addresses in string format. Can be null or empty.</param>
74
    /// <param name="allowedNetworks">A list of allowed networks in CIDR notation (e.g., "192.168.1.0/24"). Can be null or empty.</param>
75
    /// <returns>
76
    /// <c>true</c> if the IP address is allowed; otherwise, <c>false</c>.
77
    /// Returns <c>true</c> if no restrictions are configured (both lists are null or empty).
78
    /// Returns <c>false</c> if the IP address is null, or represents any address (0.0.0.0 or ::).
79
    /// </returns>
80
    /// <remarks>
81
    /// <para>
82
    /// If no IP restrictions are configured (both <paramref name="allowedAddresses"/> and
83
    /// <paramref name="allowedNetworks"/> are null or empty), all IP addresses are allowed.
84
    /// </para>
85
    /// <para>
86
    /// The method first checks individual addresses, then network ranges. If the IP address
87
    /// matches any allowed address or falls within any allowed network, it is considered allowed.
88
    /// </para>
89
    /// <para>
90
    /// IPv4 and IPv6 addresses are supported. Network comparisons ensure address family compatibility.
91
    /// </para>
92
    /// </remarks>
93
    /// <example>
94
    /// <code>
95
    /// var allowedIPs = new[] { "192.168.1.100", "10.0.0.1" };
96
    /// var allowedNetworks = new[] { "192.168.0.0/16", "10.0.0.0/8" };
97
    /// var clientIP = IPAddress.Parse("192.168.1.50");
98
    ///
99
    /// bool isAllowed = SecurityKeyWhitelist.IsIpAllowed(clientIP, allowedIPs, allowedNetworks);
100
    /// // Returns true because 192.168.1.50 is in the 192.168.0.0/16 network
101
    /// </code>
102
    /// </example>
103
    public static bool IsIpAllowed(IPAddress? ipAddress, IReadOnlyList<string>? allowedAddresses, IReadOnlyList<string>? allowedNetworks)
104
    {
105
        // If no IP restrictions are configured, allow all
106
        if (!(allowedAddresses?.Count > 0 || allowedNetworks?.Count > 0))
39✔
107
            return true;
2✔
108

109
        if (ipAddress == null)
37✔
110
            return false;
1✔
111

112
        if (ipAddress.Equals(IPAddress.Any) || ipAddress.Equals(IPAddress.IPv6Any))
36✔
113
            return false;
2✔
114

115
        // Check AllowedAddresses
116
        if (allowedAddresses?.Count > 0 && allowedAddresses.Any(addr => IPAddress.TryParse(addr, out var ip) && ip.Equals(ipAddress)))
86✔
117
            return true;
12✔
118

119
        // Check AllowedNetworks (CIDR)
120
        if (allowedNetworks?.Count > 0 && allowedNetworks.Any(network => IsIpInNetwork(ipAddress, network)))
55✔
121
            return true;
9✔
122

123
        return false;
13✔
124
    }
125

126
    /// <summary>
127
    /// Determines whether the specified IP address is within the given network range.
128
    /// </summary>
129
    /// <param name="ipAddress">The IP address to check. Can be null.</param>
130
    /// <param name="network">
131
    /// The network in CIDR notation (e.g., "192.168.1.0/24" for IPv4 or "2001:db8::/32" for IPv6).
132
    /// Can be null or empty.
133
    /// </param>
134
    /// <returns>
135
    /// <c>true</c> if the IP address is within the specified network range; otherwise, <c>false</c>.
136
    /// Returns <c>false</c> if either parameter is null, empty, or if parsing fails.
137
    /// </returns>
138
    /// <remarks>
139
    /// <para>
140
    /// This method supports both IPv4 and IPv6 CIDR notation. The network parameter must be in the
141
    /// format "baseIP/prefixLength" where prefixLength represents the number of leading bits that
142
    /// define the network portion.
143
    /// </para>
144
    /// <para>
145
    /// Both the IP address and network base address must be of the same address family (IPv4 or IPv6)
146
    /// for comparison to succeed.
147
    /// </para>
148
    /// <para>
149
    /// The method performs bitwise comparison of the network portion of the addresses based on the
150
    /// prefix length specified in the CIDR notation.
151
    /// </para>
152
    /// </remarks>
153
    /// <example>
154
    /// <code>
155
    /// var ipAddress = IPAddress.Parse("192.168.1.100");
156
    /// bool result1 = SecurityKeyWhitelist.IsIpInNetwork(ipAddress, "192.168.1.0/24");
157
    /// // Returns true - IP is in the 192.168.1.0/24 network
158
    ///
159
    /// bool result2 = SecurityKeyWhitelist.IsIpInNetwork(ipAddress, "10.0.0.0/8");
160
    /// // Returns false - IP is not in the 10.0.0.0/8 network
161
    ///
162
    /// var ipv6Address = IPAddress.Parse("2001:db8::1");
163
    /// bool result3 = SecurityKeyWhitelist.IsIpInNetwork(ipv6Address, "2001:db8::/32");
164
    /// // Returns true - IPv6 address is in the specified network
165
    /// </code>
166
    /// </example>
167
    /// <exception cref="ArgumentException">
168
    /// Not thrown directly, but malformed CIDR notation will result in <c>false</c> being returned.
169
    /// </exception>
170
    public static bool IsIpInNetwork(IPAddress? ipAddress, string? network)
171
    {
172
        if (string.IsNullOrWhiteSpace(network) || ipAddress == null)
73✔
173
            return false;
4✔
174

175
        try
176
        {
177
            // Split the network string into base IP and prefix length (e.g., "192.168.1.0/24")
178
            var parts = network.Split('/');
69✔
179
            if (parts.Length != 2)
69✔
180
                return false;
2✔
181

182
            // Try to parse the base IP address
183
            if (!IPAddress.TryParse(parts[0], out var baseIp))
67✔
184
                return false;
2✔
185

186
            // Try to parse the prefix length (number of leading bits in the mask)
187
            if (!int.TryParse(parts[1], out int prefixLength))
65✔
188
                return false;
2✔
189

190
            var baseBytes = baseIp.GetAddressBytes();
63✔
191
            var remoteBytes = ipAddress.GetAddressBytes();
63✔
192

193
            // Ensure both IPs are of the same address family (IPv4/IPv6)
194
            if (baseBytes.Length != remoteBytes.Length)
63✔
195
                return false;
5✔
196

197
            // Compare the bytes covered by the prefix
198
            int bytes = prefixLength / 8;
58✔
199
            int bits = prefixLength % 8;
58✔
200

201
            for (int i = 0; i < bytes; i++)
346✔
202
            {
203
                if (baseBytes[i] != remoteBytes[i])
142✔
204
                    return false;
27✔
205
            }
206

207
            // If there are remaining bits, compare them using a mask
208
            if (bits > 0)
31✔
209
            {
210
                int mask = (byte)~(0xFF >> bits);
12✔
211
                if ((baseBytes[bytes] & mask) != (remoteBytes[bytes] & mask))
12✔
212
                    return false;
5✔
213
            }
214

215
            // All relevant bits match, IP is in network
216
            return true;
26✔
217
        }
NEW
218
        catch
×
219
        {
220
            // If parsing fails, treat as not in network
NEW
221
            return false;
×
222
        }
223
    }
69✔
224
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc