Skip to content

Commit d158ce0

Browse files
authored
feat: Implement ip filter in emulator (#65)
1 parent fbfe33c commit d158ce0

File tree

5 files changed

+391
-1
lines changed

5 files changed

+391
-1
lines changed

src/Testing/Document/MockIpFilterProvider.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,11 @@ internal Setup(
3535

3636
public void WithCallback(Action<GatewayContext, IpFilterConfig> callback) =>
3737
_handler.CallbackSetup.Add((_predicate, callback).ToTuple());
38+
39+
public void OnIpDeny(Action<GatewayContext, IpFilterConfig> onIpDeny) =>
40+
_handler.OnIpDenied.Add((_predicate, onIpDeny).ToTuple());
41+
42+
public void OnIpAllow(Action<GatewayContext, IpFilterConfig> onIpAllow) =>
43+
_handler.OnIpAllowed.Add((_predicate, onIpAllow).ToTuple());
3844
}
3945
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Net;
5+
using System.Net.Sockets;
6+
7+
namespace Azure.ApiManagement.PolicyToolkit.Testing.Emulator;
8+
9+
public static class IPAddressExtensions
10+
{
11+
public static int CompareTo(this IPAddress? left, IPAddress? right)
12+
{
13+
if (left is null && right is null)
14+
{
15+
return 0;
16+
}
17+
18+
if (left is null)
19+
{
20+
return 1;
21+
}
22+
23+
if (right is null)
24+
{
25+
return -1;
26+
}
27+
28+
var leftBytesSpan = new ReadOnlySpan<byte>(left.GetAddressBytes());
29+
var rightBytesSpan = new ReadOnlySpan<byte>(right.GetAddressBytes());
30+
if (left.AddressFamily == right.AddressFamily)
31+
{
32+
return leftBytesSpan.SequenceCompareTo(rightBytesSpan);
33+
}
34+
35+
return left.AddressFamily == AddressFamily.InterNetwork
36+
? -Compare(rightBytesSpan, leftBytesSpan)
37+
: Compare(leftBytesSpan, rightBytesSpan);
38+
}
39+
40+
// IPv6 need to have the following prefix to be treated as IPv4 (first 12 bytes)
41+
private static byte[] ipv4PrefixInIpv6 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255];
42+
43+
private static int Compare(ReadOnlySpan<byte> ipv6BytesSpan, ReadOnlySpan<byte> ipv4BytesSpan)
44+
{
45+
var prefixCompare = ipv6BytesSpan[..ipv4PrefixInIpv6.Length].SequenceCompareTo(ipv4PrefixInIpv6);
46+
if (prefixCompare != 0)
47+
{
48+
return prefixCompare;
49+
}
50+
51+
var ipv4InIpv6Span = ipv6BytesSpan[ipv4PrefixInIpv6.Length..];
52+
return ipv4InIpv6Span.SequenceCompareTo(ipv4BytesSpan);
53+
}
54+
}
Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,90 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.Net;
5+
46
using Azure.ApiManagement.PolicyToolkit.Authoring;
7+
using Azure.ApiManagement.PolicyToolkit.Testing.Expressions;
58

69
namespace Azure.ApiManagement.PolicyToolkit.Testing.Emulator.Policies;
710

811
[Section(nameof(IInboundContext))]
912
internal class IpFilterHandler : PolicyHandler<IpFilterConfig>
1013
{
14+
public List<Tuple<
15+
Func<GatewayContext, IpFilterConfig, bool>,
16+
Action<GatewayContext, IpFilterConfig>
17+
>> OnIpAllowed { get; } = new();
18+
19+
public List<Tuple<
20+
Func<GatewayContext, IpFilterConfig, bool>,
21+
Action<GatewayContext, IpFilterConfig>
22+
>> OnIpDenied { get; } = new();
23+
1124
public override string PolicyName => nameof(IInboundContext.IpFilter);
1225

1326
protected override void Handle(GatewayContext context, IpFilterConfig config)
1427
{
15-
throw new NotImplementedException();
28+
if (!IPAddress.TryParse(context.Request.IpAddress, out var clientIp))
29+
{
30+
if ("allow".Equals(config.Action, StringComparison.InvariantCultureIgnoreCase))
31+
{
32+
DenyAccess(context, config);
33+
}
34+
35+
OnIpAllowed.Find(tuple => tuple.Item1(context, config))?.Item2(context, config);
36+
return;
37+
}
38+
39+
var directMatch = (config.Addresses ?? [])
40+
.Any(address => clientIp.CompareTo(IPAddress.Parse(address)) == 0);
41+
var rangeMatch = (config.AddressRanges ?? [])
42+
.Any(range => clientIp.CompareTo(IPAddress.Parse(range.From)) >= 0
43+
&& clientIp.CompareTo(IPAddress.Parse(range.To)) <= 0);
44+
var match = directMatch || rangeMatch;
45+
46+
if ("allow".Equals(config.Action, StringComparison.InvariantCultureIgnoreCase))
47+
{
48+
if (!match)
49+
{
50+
DenyAccess(context, config);
51+
}
52+
}
53+
else if ("forbid".Equals(config.Action, StringComparison.InvariantCultureIgnoreCase))
54+
{
55+
if (match)
56+
{
57+
DenyAccess(context, config);
58+
}
59+
}
60+
else
61+
{
62+
throw new NotSupportedException("Specified filter action is not supported.");
63+
}
64+
65+
OnIpAllowed.Find(tuple => tuple.Item1(context, config))?.Item2(context, config);
66+
}
67+
68+
void DenyAccess(GatewayContext context, IpFilterConfig config)
69+
{
70+
context.Response = new MockResponse()
71+
{
72+
StatusCode = 403,
73+
StatusReason = "Forbidden", // TODO use code to reason mapper
74+
Headers = { { "Content-Type", ["application/json"] } },
75+
Body =
76+
{
77+
Content = """
78+
{
79+
"statusCode": 403,
80+
"message": "Forbidden"
81+
}
82+
"""
83+
}
84+
};
85+
86+
OnIpDenied.Find(tuple => tuple.Item1(context, config))?.Item2(context, config);
87+
88+
throw new FinishSectionProcessingException();
1689
}
1790
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Net;
5+
6+
using Azure.ApiManagement.PolicyToolkit.Testing.Emulator;
7+
8+
namespace Test.Emulator.Emulator;
9+
10+
[TestClass]
11+
public class IPAddressExtensionsTests
12+
{
13+
[TestMethod]
14+
[DataRow(null, null, 0)]
15+
[DataRow(null, "1.1.1.1", 1)]
16+
[DataRow("1.1.1.1", null, -1)]
17+
[DataRow("1.1.1.1", "1.1.1.1", 0)]
18+
[DataRow("1.1.1.1", "1.1.1.2", -1)]
19+
[DataRow("1.1.1.2", "1.1.1.1", 1)]
20+
[DataRow("1.1.1.1", "1.1.2.1", -1)]
21+
[DataRow("1.1.2.1", "1.1.1.1", 1)]
22+
[DataRow("1.1.1.1", "1.2.1.1", -1)]
23+
[DataRow("1.2.1.1", "1.1.1.1", 1)]
24+
[DataRow("1.1.1.1", "2.1.1.1", -1)]
25+
[DataRow("2.1.1.1", "1.1.1.1", 1)]
26+
[DataRow(null, "::1", 1)]
27+
[DataRow("::1", null, -1)]
28+
[DataRow("::1", "::1", 0)]
29+
[DataRow("::1", "::2", -1)]
30+
[DataRow("::2", "::1", 1)]
31+
[DataRow("::0201", "::0101", 1)]
32+
[DataRow("::0101", "::0201", -1)]
33+
[DataRow("::0102:0101", "::0101:0101", 1)]
34+
[DataRow("::0101:0101", "::0102:0101", -1)]
35+
[DataRow("::ffff:0101:0101", "1.1.1.1", 0)]
36+
[DataRow("1.1.1.1", "::ffff:0101:0101", 0)]
37+
[DataRow("1.1.1.2", "::ffff:0101:0101", 1)]
38+
[DataRow("::ffff:0101:0101", "1.1.1.2", -1)]
39+
[DataRow("1.1.1.1", "::ffff:0101:0102", -1)]
40+
[DataRow("::ffff:0101:0102", "1.1.1.1", 1)]
41+
[DataRow("192.168.0.1", "10.0.0.1", 192 - 10)]
42+
[DataRow("10.0.0.1", "192.168.0.1", 10 - 192)]
43+
[DataRow("::fffe:0101:0101", "1.1.1.1", -1)] // 1.1.1.1 == ::ffff:0101:0101
44+
[DataRow("1.1.1.1", "::fffe:0101:0101", 1)] // 1.1.1.1 == ::ffff:0101:0101
45+
public void IPAddress_CompareTo(string? left, string? right, int value)
46+
{
47+
IPAddress? lAddress = IPAddress.TryParse(left, out var l) ? l : null;
48+
IPAddress? rAddress = IPAddress.TryParse(right, out var r) ? r : null;
49+
50+
lAddress.CompareTo(rAddress).Should().Be(value);
51+
}
52+
}

0 commit comments

Comments
 (0)