From 35a30c9d098e7ac5fcaf7d86a4cc616deb8a2cfa Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Fri, 8 Jan 2021 00:05:15 +0000 Subject: [PATCH] Impliments KnownNetworks and KnownProxies --- .../Configuration/NetworkConfiguration.cs | 2 +- Jellyfin.Networking/Manager/NetworkManager.cs | 95 +++++++++----- .../ApiServiceCollectionExtensions.cs | 120 ++++++++++++++++-- Jellyfin.Server/Startup.cs | 8 +- MediaBrowser.Common/Net/INetworkManager.cs | 16 +++ tests/Jellyfin.Api.Tests/ParseNetworkTests.cs | 95 ++++++++++++++ 6 files changed, 285 insertions(+), 51 deletions(-) create mode 100644 tests/Jellyfin.Api.Tests/ParseNetworkTests.cs diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs index 792e57f6ab..91bf0015fd 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -224,7 +224,7 @@ namespace Jellyfin.Networking.Configuration public string[] LocalNetworkAddresses { get; set; } = Array.Empty(); /// - /// Gets or sets the known proxies. + /// Gets or sets the known proxies. If the proxy is a network, it's added to the KnownNetworks. /// public string[] KnownProxies { get; set; } = Array.Empty(); } diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 60b899519e..e2fe440608 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -139,6 +139,16 @@ namespace Jellyfin.Networking.Manager /// public bool IsIP4Enabled { get; set; } + /// + /// Gets or sets a value indicating whether the system has IP4 is enabled. + /// + public bool SystemIP4Enabled { get; set; } + + /// + /// Gets or sets a value indicating whether the system has IP6 is enabled. + /// + public bool SystemIP6Enabled { get; set; } + /// public Collection RemoteAddressFilter { get; private set; } @@ -185,6 +195,15 @@ namespace Jellyfin.Networking.Manager return _macAddresses; } + /// + /// REMOVE after debugging. + /// + /// Message. + public void Log(string msg) + { + _logger.LogInformation(msg); + } + /// public bool IsGatewayInterface(IPObject? addressObj) { @@ -1047,47 +1066,55 @@ namespace Jellyfin.Networking.Manager // populate interface address list foreach (UnicastIPAddressInformation info in ipProperties.UnicastAddresses) { - if (IsIP4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) + if (info.Address.AddressFamily == AddressFamily.InterNetwork) { - IPNetAddress nw = new IPNetAddress(info.Address, IPObject.MaskToCidr(info.IPv4Mask)) - { - // Keep the number of gateways on this interface, along with its index. - Tag = ipProperties.GetIPv4Properties().Index - }; - - int tag = nw.Tag; - if (ipProperties.GatewayAddresses.Count > 0 && !nw.IsLoopback()) + SystemIP4Enabled = true; + if (IsIP4Enabled) { - // -ve Tags signify the interface has a gateway. - nw.Tag *= -1; + IPNetAddress nw = new IPNetAddress(info.Address, IPObject.MaskToCidr(info.IPv4Mask)) + { + // Keep the number of gateways on this interface, along with its index. + Tag = ipProperties.GetIPv4Properties().Index + }; + + int tag = nw.Tag; + if (ipProperties.GatewayAddresses.Count > 0 && !nw.IsLoopback()) + { + // -ve Tags signify the interface has a gateway. + nw.Tag *= -1; + } + + _interfaceAddresses.AddItem(nw); + + // Store interface name so we can use the name in Collections. + _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; + _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag; } - - _interfaceAddresses.AddItem(nw); - - // Store interface name so we can use the name in Collections. - _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; - _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag; } - else if (IsIP6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) + else if (info.Address.AddressFamily == AddressFamily.InterNetworkV6) { - IPNetAddress nw = new IPNetAddress(info.Address, (byte)info.PrefixLength) + SystemIP6Enabled = true; + if (IsIP6Enabled) { - // Keep the number of gateways on this interface, along with its index. - Tag = ipProperties.GetIPv6Properties().Index - }; - - int tag = nw.Tag; - if (ipProperties.GatewayAddresses.Count > 0 && !nw.IsLoopback()) - { - // -ve Tags signify the interface has a gateway. - nw.Tag *= -1; + IPNetAddress nw = new IPNetAddress(info.Address, (byte)info.PrefixLength) + { + // Keep the number of gateways on this interface, along with its index. + Tag = ipProperties.GetIPv6Properties().Index + }; + + int tag = nw.Tag; + if (ipProperties.GatewayAddresses.Count > 0 && !nw.IsLoopback()) + { + // -ve Tags signify the interface has a gateway. + nw.Tag *= -1; + } + + _interfaceAddresses.AddItem(nw); + + // Store interface name so we can use the name in Collections. + _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; + _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag; } - - _interfaceAddresses.AddItem(nw); - - // Store interface name so we can use the name in Collections. - _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; - _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag; } } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index bbfc4fbd4c..4f6329f824 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Net.Sockets; using System.Reflection; +using System.Text; using Emby.Server.Implementations; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; @@ -20,6 +23,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; +using Jellyfin.Networking.Configuration; using Jellyfin.Server.Configuration; using Jellyfin.Server.Filters; using Jellyfin.Server.Formatters; @@ -169,36 +173,124 @@ namespace Jellyfin.Server.Extensions .AddScheme(AuthenticationSchemes.CustomAuthentication, null); } + /// + /// Sets up the proxy configuration based on the addresses in . + /// + /// The instance. + /// The containing the config settings. + /// The string array to parse. + /// The instance. + public static void ParseList(INetworkManager networkManager, NetworkConfiguration config, string[] userList, ForwardedHeadersOptions options) + { + for (var i = 0; i < userList.Length; i++) + { + if (IPNetAddress.TryParse(userList[i], out var addr)) + { + if ((!config.EnableIPV4 && addr.AddressFamily == AddressFamily.InterNetwork) + || (!config.EnableIPV6 && addr.AddressFamily == AddressFamily.InterNetworkV6)) + { + continue; + } + + if (networkManager.SystemIP6Enabled && addr.AddressFamily == AddressFamily.InterNetwork) + { + // If the server is using dual-mode sockets, IPv4 addresses are supplied in an IPv6 format. + // https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-5.0 . + addr.Address = addr.Address.MapToIPv6(); + } + + if (addr.PrefixLength == 32) + { + options.KnownProxies.Add(addr.Address); + } + else + { + options.KnownNetworks.Add(new IPNetwork(addr.Address, addr.PrefixLength)); + } + } + else if (IPHost.TryParse(userList[i], out var host)) + { + foreach (var address in host.GetAddresses()) + { + if ((!config.EnableIPV4 && host.AddressFamily == AddressFamily.InterNetwork) + || (!config.EnableIPV6 && host.AddressFamily == AddressFamily.InterNetworkV6)) + { + continue; + } + + var hostAddr = address; + if (networkManager.SystemIP6Enabled && address.AddressFamily == AddressFamily.InterNetwork) + { + // If the server is using dual-mode sockets, IPv4 addresses are supplied in an IPv6 format. + // https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-5.0 . + hostAddr = address.MapToIPv6(); + } + + options.KnownProxies.Add(hostAddr); + } + } + } + } + + private static string EnumToString(IEnumerable x) + { + var sb = new StringBuilder(); + foreach (var item in x) + { + if (item is IPAddress ipItem) + { + sb.Append(ipItem.ToString()); + } + else if (item is IPNetwork ipNetwork) + { + sb.Append(ipNetwork.Prefix.ToString()); + sb.Append('/'); + sb.Append(ipNetwork.PrefixLength.ToString(CultureInfo.InvariantCulture)); + sb.Append(','); + } + } + + return sb.ToString(); + } + /// /// Extension method for adding the jellyfin API to the service collection. /// /// The service collection. /// An IEnumerable containing all plugin assemblies with API controllers. - /// A list of all known proxies to trust for X-Forwarded-For. + /// The . + /// The instance. /// The MVC builder. - public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable pluginAssemblies, IReadOnlyList knownProxies) + public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable pluginAssemblies, NetworkConfiguration config, INetworkManager networkManager) { IMvcBuilder mvcBuilder = serviceCollection - .AddCors() - .AddTransient() - .Configure(options => + .AddCors() + .AddTransient() + .Configure(options => { + // https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs + // Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues. + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; - if (knownProxies.Count == 0) + if (config.KnownProxies.Length == 0) { - options.KnownNetworks.Clear(); options.KnownProxies.Clear(); + options.KnownNetworks.Clear(); } else { - for (var i = 0; i < knownProxies.Count; i++) - { - if (IPHost.TryParse(knownProxies[i], out var host)) - { - options.KnownProxies.Add(host.Address); - } - } + ParseList(networkManager, config, config.KnownProxies, options); + networkManager.Log("KnownProxies: " + EnumToString(options.KnownProxies)); + networkManager.Log("KnownNetworks: " + EnumToString(options.KnownNetworks)); + } + + // Only set forward limit if we have some known proxies or some known networks. + if (options.KnownProxies.Count != 0 || options.KnownNetworks.Count != 0) + { + options.ForwardLimit = null; } + + networkManager.Log("Forward Limit : " + options.ForwardLimit?.ToString(CultureInfo.CurrentCulture) ?? "None"); }) .AddMvc(opts => { diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 3395d2413f..05228ccaab 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -26,18 +26,22 @@ namespace Jellyfin.Server { private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IServerApplicationHost _serverApplicationHost; + private readonly INetworkManager _networkManager; /// /// Initializes a new instance of the class. /// /// The server configuration manager. /// The server application host. + /// The network manager. public Startup( IServerConfigurationManager serverConfigurationManager, - IServerApplicationHost serverApplicationHost) + IServerApplicationHost serverApplicationHost, + INetworkManager networkManager) { _serverConfigurationManager = serverConfigurationManager; _serverApplicationHost = serverApplicationHost; + _networkManager = networkManager; } /// @@ -52,7 +56,7 @@ namespace Jellyfin.Server { options.HttpsPort = _serverApplicationHost.HttpsPort; }); - services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration().KnownProxies); + services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration(), _networkManager); services.AddJellyfinApiSwagger(); diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index b6c390d239..455fbe27a6 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -44,6 +44,16 @@ namespace MediaBrowser.Common.Net /// bool IsIP4Enabled { get; set; } + /// + /// Gets or sets a value indicating whether the system has IP4 is enabled. + /// + public bool SystemIP4Enabled { get; set; } + + /// + /// Gets or sets a value indicating whether the system has IP6 is enabled. + /// + public bool SystemIP6Enabled { get; set; } + /// /// Calculates the list of interfaces to use for Kestrel. /// @@ -229,5 +239,11 @@ namespace MediaBrowser.Common.Net /// Optional filter for the list. /// Returns a filtered list of LAN addresses. Collection GetFilteredLANSubnets(Collection? filter = null); + + /// + /// REMOVE after debugging. + /// + /// Message. + void Log(string msg); } } diff --git a/tests/Jellyfin.Api.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Api.Tests/ParseNetworkTests.cs new file mode 100644 index 0000000000..3a9f0a0790 --- /dev/null +++ b/tests/Jellyfin.Api.Tests/ParseNetworkTests.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.Manager; +using Jellyfin.Server.Extensions; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Jellyfin.Api.Tests +{ + public class ParseNetworkTests + { + private static IConfigurationManager GetMockConfig(NetworkConfiguration conf) + { + var configManager = new Mock + { + CallBase = true + }; + configManager.Setup(x => x.GetConfiguration(It.IsAny())).Returns(conf); + return (IConfigurationManager)configManager.Object; + } + + private static NetworkManager CreateNetworkManager() + { + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + }; + + return new NetworkManager(GetMockConfig(conf), new NullLogger()); + } + + /// + /// Order of the result has always got to be hosts, then networks. + /// + /// IP4 enabled. + /// IP6 enabled. + /// List to parse. + /// What it should match. + [Theory] + [InlineData(true, true, "192.168.0.0/16,www.yahoo.co.uk", "::ffff:212.82.100.150,::ffff:192.168.0.0/16")] + [InlineData(true, false, "192.168.0.0/16,www.yahoo.co.uk", "212.82.100.150,192.168.0.0/16")] + [InlineData(true, true, "192.168.t,127.0.0.1,1234.1232.12.1234", "::ffff:127.0.0.1")] + [InlineData(true, false, "192.168.x,127.0.0.1,1234.1232.12.1234", "127.0.0.1")] + [InlineData(true, true, "::1", "::1/128")] + public void TestNetworks(bool ip4, bool ip6, string hostList, string match) + { + using var nm = CreateNetworkManager(); + nm.SystemIP6Enabled = ip6; + + var settings = new NetworkConfiguration(); + settings.EnableIPV4 = ip4; + settings.EnableIPV6 = ip6; + + var result = match + ','; + ForwardedHeadersOptions options = new ForwardedHeadersOptions(); + + // Need this here as ::1 and 127.0.0.1 are in them by default. + options.KnownProxies.Clear(); + options.KnownNetworks.Clear(); + + ApiServiceCollectionExtensions.ParseList(nm, settings, hostList.Split(","), options); + + var sb = new StringBuilder(); + foreach (var item in options.KnownProxies) + { + sb.Append(item.ToString()); + sb.Append(','); + } + + foreach (var item in options.KnownNetworks) + { + sb.Append(item.Prefix.ToString()); + sb.Append('/'); + sb.Append(item.PrefixLength.ToString(CultureInfo.InvariantCulture)); + sb.Append(','); + } + + if (!string.Equals(sb.ToString(), result, StringComparison.OrdinalIgnoreCase)) + { + throw new Exception("Not matched: " + sb.ToString() + " does not match " + result); + } + } + } +}