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.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index bbfc4fbd4c..4f65a31e07 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -1,9 +1,13 @@ 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.Runtime.CompilerServices; +using System.Text; using Emby.Server.Implementations; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; @@ -20,6 +24,8 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; +using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.Manager; using Jellyfin.Server.Configuration; using Jellyfin.Server.Filters; using Jellyfin.Server.Formatters; @@ -174,30 +180,33 @@ namespace Jellyfin.Server.Extensions /// /// 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 MVC builder. - public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable pluginAssemblies, IReadOnlyList knownProxies) + public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable pluginAssemblies, NetworkConfiguration config) { IMvcBuilder mvcBuilder = serviceCollection .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(); } else { - for (var i = 0; i < knownProxies.Count; i++) - { - if (IPHost.TryParse(knownProxies[i], out var host)) - { - options.KnownProxies.Add(host.Address); - } - } + AddProxyAddresses(config, config.KnownProxies, options); + } + + // 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; } }) .AddMvc(opts => @@ -312,6 +321,55 @@ namespace Jellyfin.Server.Extensions }); } + /// + /// Sets up the proxy configuration based on the addresses in . + /// + /// The containing the config settings. + /// The string array to parse. + /// The instance. + internal static void AddProxyAddresses(NetworkConfiguration config, string[] allowedProxies, ForwardedHeadersOptions options) + { + for (var i = 0; i < allowedProxies.Length; i++) + { + if (IPNetAddress.TryParse(allowedProxies[i], out var addr)) + { + AddIpAddress(config, options, addr.Address, addr.PrefixLength); + } + else if (IPHost.TryParse(allowedProxies[i], out var host)) + { + foreach (var address in host.GetAddresses()) + { + AddIpAddress(config, options, addr.Address, addr.PrefixLength); + } + } + } + } + + private static void AddIpAddress(NetworkConfiguration config, ForwardedHeadersOptions options, IPAddress addr, int prefixLength) + { + if ((!config.EnableIPV4 && addr.AddressFamily == AddressFamily.InterNetwork) || (!config.EnableIPV6 && addr.AddressFamily == AddressFamily.InterNetworkV6)) + { + return; + } + + // In order for dual-mode sockets to be used, IP6 has to be enabled in JF and an interface has to have an IP6 address. + if (addr.AddressFamily == AddressFamily.InterNetwork && config.EnableIPV6) + { + // 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 = addr.MapToIPv6(); + } + + if (prefixLength == 32) + { + options.KnownProxies.Add(addr); + } + else + { + options.KnownNetworks.Add(new IPNetwork(addr, prefixLength)); + } + } + private static void AddSwaggerTypeMappings(this SwaggerGenOptions options) { /* diff --git a/Jellyfin.Server/Properties/AssemblyInfo.cs b/Jellyfin.Server/Properties/AssemblyInfo.cs index 5de1e653d9..7abf298b12 100644 --- a/Jellyfin.Server/Properties/AssemblyInfo.cs +++ b/Jellyfin.Server/Properties/AssemblyInfo.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Resources; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -19,3 +20,5 @@ using System.Runtime.InteropServices; // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] + +[assembly: InternalsVisibleTo("Jellyfin.Api.Tests")] diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 3395d2413f..e56e61092b 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -52,7 +52,7 @@ namespace Jellyfin.Server { options.HttpsPort = _serverApplicationHost.HttpsPort; }); - services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration().KnownProxies); + services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration()); services.AddJellyfinApiSwagger(); diff --git a/tests/Jellyfin.Api.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Api.Tests/ParseNetworkTests.cs new file mode 100644 index 0000000000..6c3fd0ee10 --- /dev/null +++ b/tests/Jellyfin.Api.Tests/ParseNetworkTests.cs @@ -0,0 +1,88 @@ +using System; +using System.Globalization; +using System.Text; +using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.Manager; +using Jellyfin.Server.Extensions; +using MediaBrowser.Common.Configuration; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Jellyfin.Api.Tests +{ + public class ParseNetworkTests + { + /// + /// 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")] <- fails on Max. www.yahoo.co.uk resolves to a different ip address. + // [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(); + + var settings = new NetworkConfiguration + { + EnableIPV4 = ip4, + 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.AddProxyAddresses(settings, hostList.Split(","), options); + + var sb = new StringBuilder(); + foreach (var item in options.KnownProxies) + { + sb.Append(item); + sb.Append(','); + } + + foreach (var item in options.KnownNetworks) + { + sb.Append(item.Prefix); + sb.Append('/'); + sb.Append(item.PrefixLength.ToString(CultureInfo.InvariantCulture)); + sb.Append(','); + } + + Assert.Equal(sb.ToString(), result); + } + + private static IConfigurationManager GetMockConfig(NetworkConfiguration conf) + { + var configManager = new Mock + { + CallBase = true + }; + configManager.Setup(x => x.GetConfiguration(It.IsAny())).Returns(conf); + return configManager.Object; + } + + private static NetworkManager CreateNetworkManager() + { + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + }; + + return new NetworkManager(GetMockConfig(conf), new NullLogger()); + } + } +}