Skip to content

Commit f39e804

Browse files
committed
Implement KnownNetworks dual list
Fixes #63627
1 parent 1cc1447 commit f39e804

File tree

5 files changed

+313
-19
lines changed

5 files changed

+313
-19
lines changed

src/DefaultBuilder/src/ForwardedHeadersOptionsSetup.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@ public void Configure(ForwardedHeadersOptions options)
2727
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
2828
// Only loopback proxies are allowed by default. Clear that restriction because forwarders are
2929
// being enabled by explicit configuration.
30-
#pragma warning disable ASPDEPR005 // KnownNetworks is obsolete
31-
options.KnownNetworks.Clear();
32-
#pragma warning restore ASPDEPR005 // KnownNetworks is obsolete
3330
options.KnownIPNetworks.Clear();
3431
options.KnownProxies.Clear();
3532
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
#pragma warning disable ASPDEPR005 // Type or member is obsolete
5+
6+
using System.Globalization;
7+
using AspNetIPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
8+
using IPAddress = System.Net.IPAddress;
9+
using IPNetwork = System.Net.IPNetwork;
10+
11+
namespace Microsoft.AspNetCore.Builder;
12+
13+
/// <summary>
14+
/// Internal list implementation that keeps <see cref="System.Net.IPNetwork"/> and the obsolete
15+
/// <see cref="Microsoft.AspNetCore.HttpOverrides.IPNetwork"/> collections in sync. Modifications
16+
/// through either interface are reflected in the other.
17+
/// </summary>
18+
internal sealed class DualIPNetworkList : IList<IPNetwork>, IList<AspNetIPNetwork>
19+
{
20+
private readonly List<(IPNetwork system, AspNetIPNetwork aspnet)> _items = new();
21+
22+
public DualIPNetworkList()
23+
{
24+
Add(new IPNetwork(IPAddress.Loopback, 8));
25+
}
26+
27+
int ICollection<IPNetwork>.Count => _items.Count;
28+
int ICollection<AspNetIPNetwork>.Count => _items.Count;
29+
30+
bool ICollection<IPNetwork>.IsReadOnly => false;
31+
bool ICollection<AspNetIPNetwork>.IsReadOnly => false;
32+
33+
IPNetwork IList<IPNetwork>.this[int index]
34+
{
35+
get => _items[index].system;
36+
set => _items[index] = (value, CreateAspNetNetworkFromSystem(value));
37+
}
38+
39+
AspNetIPNetwork IList<AspNetIPNetwork>.this[int index]
40+
{
41+
get => _items[index].aspnet;
42+
set => _items[index] = (new IPNetwork(value.Prefix, value.PrefixLength), value);
43+
}
44+
45+
public void Add(IPNetwork item)
46+
{
47+
_items.Add((item, CreateAspNetNetworkFromSystem(item)));
48+
}
49+
50+
public void Add(AspNetIPNetwork item)
51+
{
52+
_items.Add((new IPNetwork(item.Prefix, item.PrefixLength), item));
53+
}
54+
55+
void ICollection<IPNetwork>.Add(IPNetwork item) => Add(item);
56+
void ICollection<AspNetIPNetwork>.Add(AspNetIPNetwork item) => Add(item);
57+
58+
public void Clear() => _items.Clear();
59+
void ICollection<IPNetwork>.Clear() => Clear();
60+
void ICollection<AspNetIPNetwork>.Clear() => Clear();
61+
62+
public bool Contains(IPNetwork item)
63+
{
64+
var text = item.ToString();
65+
for (var i = 0; i < _items.Count; i++)
66+
{
67+
if (string.Equals(_items[i].system.ToString(), text, StringComparison.Ordinal))
68+
{
69+
return true;
70+
}
71+
}
72+
return false;
73+
}
74+
75+
public bool Contains(AspNetIPNetwork item)
76+
{
77+
for (var i = 0; i < _items.Count; i++)
78+
{
79+
if (_items[i].aspnet.Prefix.Equals(item.Prefix) && _items[i].aspnet.PrefixLength == item.PrefixLength)
80+
{
81+
return true;
82+
}
83+
}
84+
return false;
85+
}
86+
87+
bool ICollection<IPNetwork>.Contains(IPNetwork item) => Contains(item);
88+
bool ICollection<AspNetIPNetwork>.Contains(AspNetIPNetwork item) => Contains(item);
89+
90+
public void CopyTo(IPNetwork[] array, int arrayIndex)
91+
{
92+
for (var i = 0; i < _items.Count; i++)
93+
{
94+
array[arrayIndex + i] = _items[i].system;
95+
}
96+
}
97+
98+
public void CopyTo(AspNetIPNetwork[] array, int arrayIndex)
99+
{
100+
for (var i = 0; i < _items.Count; i++)
101+
{
102+
array[arrayIndex + i] = _items[i].aspnet;
103+
}
104+
}
105+
106+
void ICollection<IPNetwork>.CopyTo(IPNetwork[] array, int arrayIndex) => CopyTo(array, arrayIndex);
107+
void ICollection<AspNetIPNetwork>.CopyTo(AspNetIPNetwork[] array, int arrayIndex) => CopyTo(array, arrayIndex);
108+
109+
public IEnumerator<IPNetwork> GetEnumerator()
110+
{
111+
for (var i = 0; i < _items.Count; i++)
112+
{
113+
yield return _items[i].system;
114+
}
115+
}
116+
117+
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
118+
119+
IEnumerator<AspNetIPNetwork> IEnumerable<AspNetIPNetwork>.GetEnumerator()
120+
{
121+
for (var i = 0; i < _items.Count; i++)
122+
{
123+
yield return _items[i].aspnet;
124+
}
125+
}
126+
127+
public int IndexOf(IPNetwork item)
128+
{
129+
var text = item.ToString();
130+
for (var i = 0; i < _items.Count; i++)
131+
{
132+
if (string.Equals(_items[i].system.ToString(), text, StringComparison.Ordinal))
133+
{
134+
return i;
135+
}
136+
}
137+
return -1;
138+
}
139+
140+
public int IndexOf(AspNetIPNetwork item)
141+
{
142+
for (var i = 0; i < _items.Count; i++)
143+
{
144+
var n = _items[i].aspnet;
145+
if (n.Prefix.Equals(item.Prefix) && n.PrefixLength == item.PrefixLength)
146+
{
147+
return i;
148+
}
149+
}
150+
return -1;
151+
}
152+
153+
int IList<IPNetwork>.IndexOf(IPNetwork item) => IndexOf(item);
154+
int IList<AspNetIPNetwork>.IndexOf(AspNetIPNetwork item) => IndexOf(item);
155+
156+
public void Insert(int index, IPNetwork item)
157+
{
158+
_items.Insert(index, (item, CreateAspNetNetworkFromSystem(item)));
159+
}
160+
161+
public void Insert(int index, AspNetIPNetwork item)
162+
{
163+
_items.Insert(index, (new IPNetwork(item.Prefix, item.PrefixLength), item));
164+
}
165+
166+
void IList<IPNetwork>.Insert(int index, IPNetwork item) => Insert(index, item);
167+
void IList<AspNetIPNetwork>.Insert(int index, AspNetIPNetwork item) => Insert(index, item);
168+
169+
public bool Remove(IPNetwork item)
170+
{
171+
var idx = IndexOf(item);
172+
if (idx >= 0)
173+
{
174+
_items.RemoveAt(idx);
175+
return true;
176+
}
177+
return false;
178+
}
179+
180+
public bool Remove(AspNetIPNetwork item)
181+
{
182+
var idx = IndexOf(item);
183+
if (idx >= 0)
184+
{
185+
_items.RemoveAt(idx);
186+
return true;
187+
}
188+
return false;
189+
}
190+
191+
bool ICollection<IPNetwork>.Remove(IPNetwork item) => Remove(item);
192+
bool ICollection<AspNetIPNetwork>.Remove(AspNetIPNetwork item) => Remove(item);
193+
194+
public void RemoveAt(int index) => _items.RemoveAt(index);
195+
void IList<IPNetwork>.RemoveAt(int index) => RemoveAt(index);
196+
void IList<AspNetIPNetwork>.RemoveAt(int index) => RemoveAt(index);
197+
198+
private static AspNetIPNetwork CreateAspNetNetworkFromSystem(IPNetwork network)
199+
{
200+
var text = network.ToString();
201+
var slash = text.LastIndexOf('/');
202+
if (slash <= 0 || slash == text.Length - 1)
203+
{
204+
return new AspNetIPNetwork(IPAddress.Parse(text), 0);
205+
}
206+
var prefix = IPAddress.Parse(text.AsSpan(0, slash));
207+
var prefixLength = int.Parse(text.AsSpan(slash + 1), CultureInfo.InvariantCulture);
208+
return new AspNetIPNetwork(prefix, prefixLength);
209+
}
210+
}
211+
212+
#pragma warning restore ASPDEPR005 // Type or member is obsolete

src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -213,11 +213,7 @@ public void ApplyForwarders(HttpContext context)
213213
// Host and Scheme initial values are never inspected, no need to set them here.
214214
};
215215

216-
var checkKnownIps = _options.KnownIPNetworks.Count > 0
217-
#pragma warning disable ASPDEPR005 // KnownNetworks is obsolete
218-
|| _options.KnownNetworks.Count > 0
219-
#pragma warning restore ASPDEPR005 // KnownNetworks is obsolete
220-
|| _options.KnownProxies.Count > 0;
216+
var checkKnownIps = _options.KnownIPNetworks.Count > 0 || _options.KnownProxies.Count > 0;
221217
bool applyChanges = false;
222218
int entriesConsumed = 0;
223219

@@ -410,15 +406,6 @@ private bool CheckKnownAddress(IPAddress address)
410406
return true;
411407
}
412408
}
413-
#pragma warning disable ASPDEPR005 // KnownNetworks is obsolete
414-
foreach (var network in _options.KnownNetworks)
415-
{
416-
if (network.Contains(address))
417-
{
418-
return true;
419-
}
420-
}
421-
#pragma warning restore ASPDEPR005 // KnownNetworks is obsolete
422409
return false;
423410
}
424411

src/Middleware/HttpOverrides/src/ForwardedHeadersOptions.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ namespace Microsoft.AspNetCore.Builder;
1313
/// </summary>
1414
public class ForwardedHeadersOptions
1515
{
16+
// Backing dual list that keeps the obsolete and new types in sync.
17+
private readonly DualIPNetworkList _knownNetworks = new();
18+
1619
/// <summary>
1720
/// Gets or sets the header used to retrieve the originating client IP. Defaults to the value specified by
1821
/// <see cref="ForwardedHeadersDefaults.XForwardedForHeaderName"/>.
@@ -87,12 +90,12 @@ public class ForwardedHeadersOptions
8790
/// Obsolete, please use <see cref="KnownIPNetworks"/> instead
8891
/// </summary>
8992
[Obsolete("Please use KnownIPNetworks instead. For more information, visit https://aka.ms/aspnet/deprecate/005.", DiagnosticId = "ASPDEPR005")]
90-
public IList<AspNetIPNetwork> KnownNetworks { get; } = new List<AspNetIPNetwork>() { new(IPAddress.Loopback, 8) };
93+
public IList<AspNetIPNetwork> KnownNetworks => _knownNetworks;
9194

9295
/// <summary>
9396
/// Address ranges of known proxies to accept forwarded headers from.
9497
/// </summary>
95-
public IList<IPNetwork> KnownIPNetworks { get; } = new List<IPNetwork>() { new(IPAddress.Loopback, 8) };
98+
public IList<IPNetwork> KnownIPNetworks => _knownNetworks;
9699

97100
/// <summary>
98101
/// The allowed values from x-forwarded-host. If the list is empty then all hosts are allowed.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Net;
5+
using Microsoft.AspNetCore.Builder;
6+
using Xunit;
7+
using AspNetIPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
8+
9+
namespace Microsoft.AspNetCore.HttpOverrides;
10+
11+
public class DualIPNetworkListTests
12+
{
13+
[Fact]
14+
public void DefaultContainsLoopback()
15+
{
16+
var options = new ForwardedHeadersOptions();
17+
Assert.Single(options.KnownIPNetworks);
18+
#pragma warning disable ASPDEPR005
19+
Assert.Single(options.KnownNetworks);
20+
#pragma warning restore ASPDEPR005
21+
Assert.Equal(IPAddress.Loopback, options.KnownIPNetworks[0].BaseAddress);
22+
Assert.Equal(8, options.KnownIPNetworks[0].PrefixLength);
23+
}
24+
25+
[Fact]
26+
public void AddThroughSystemCollectionVisibleViaObsolete()
27+
{
28+
var options = new ForwardedHeadersOptions();
29+
options.KnownIPNetworks.Add(global::System.Net.IPNetwork.Parse("10.0.0.0/8"));
30+
#pragma warning disable ASPDEPR005
31+
var obsoleteList = options.KnownNetworks;
32+
Assert.Equal(2, obsoleteList.Count);
33+
Assert.Equal(IPAddress.Parse("10.0.0.0"), obsoleteList[1].Prefix);
34+
Assert.Equal(8, obsoleteList[1].PrefixLength);
35+
#pragma warning restore ASPDEPR005
36+
}
37+
38+
[Fact]
39+
public void AddThroughObsoleteCollectionVisibleViaSystem()
40+
{
41+
#pragma warning disable ASPDEPR005
42+
var options = new ForwardedHeadersOptions();
43+
options.KnownNetworks.Add(new AspNetIPNetwork(IPAddress.Parse("192.168.0.0"), 16));
44+
Assert.Equal(2, options.KnownIPNetworks.Count);
45+
Assert.Equal("192.168.0.0/16", options.KnownIPNetworks[1].ToString());
46+
#pragma warning restore ASPDEPR005
47+
}
48+
49+
[Fact]
50+
public void ReplaceViaSystemIndexerUpdatesObsolete()
51+
{
52+
var options = new ForwardedHeadersOptions();
53+
((IList<global::System.Net.IPNetwork>)options.KnownIPNetworks)[0] = global::System.Net.IPNetwork.Parse("172.16.0.0/12");
54+
#pragma warning disable ASPDEPR005
55+
Assert.Equal(IPAddress.Parse("172.16.0.0"), options.KnownNetworks[0].Prefix);
56+
Assert.Equal(12, options.KnownNetworks[0].PrefixLength);
57+
#pragma warning restore ASPDEPR005
58+
}
59+
60+
[Fact]
61+
public void ReplaceViaObsoleteIndexerUpdatesSystem()
62+
{
63+
#pragma warning disable ASPDEPR005
64+
var options = new ForwardedHeadersOptions();
65+
options.KnownNetworks[0] = new AspNetIPNetwork(IPAddress.Parse("172.16.0.0"), 12);
66+
Assert.Equal("172.16.0.0/12", options.KnownIPNetworks[0].ToString());
67+
#pragma warning restore ASPDEPR005
68+
}
69+
70+
[Fact]
71+
public void ClearClearsBoth()
72+
{
73+
var options = new ForwardedHeadersOptions();
74+
options.KnownIPNetworks.Clear();
75+
#pragma warning disable ASPDEPR005
76+
Assert.Empty(options.KnownNetworks);
77+
#pragma warning restore ASPDEPR005
78+
Assert.Empty(options.KnownIPNetworks);
79+
}
80+
81+
[Fact]
82+
public void RemoveThroughEitherCollectionRemovesFromBoth()
83+
{
84+
var options = new ForwardedHeadersOptions();
85+
options.KnownIPNetworks.Add(global::System.Net.IPNetwork.Parse("10.0.0.0/8"));
86+
var first = options.KnownIPNetworks[0];
87+
var removed = options.KnownIPNetworks.Remove(first);
88+
Assert.True(removed);
89+
#pragma warning disable ASPDEPR005
90+
var obsoleteList = options.KnownNetworks;
91+
Assert.DoesNotContain(obsoleteList, n => n.Prefix.Equals(IPAddress.Loopback));
92+
#pragma warning restore ASPDEPR005
93+
Assert.Single(options.KnownIPNetworks); // only the 10.0.0.0/8 entry should remain
94+
}
95+
}

0 commit comments

Comments
 (0)