Browse Source

Merge pull request #4125 from BaronGreenback/NetworkPR2

Networking 2 (Cumulative PR) - Swapping over to new NetworkManager
Joshua M. Boniface 4 years ago
parent
commit
a57b99bffd
34 changed files with 1075 additions and 1232 deletions
  1. 1 0
      CONTRIBUTORS.md
  2. 25 15
      Emby.Dlna/Main/DlnaEntryPoint.cs
  3. 1 9
      Emby.Dlna/PlayTo/PlayToManager.cs
  4. 1 17
      Emby.Dlna/Server/DescriptionXmlBuilder.cs
  5. 100 181
      Emby.Server.Implementations/ApplicationHost.cs
  6. 0 1
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  7. 7 4
      Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  8. 1 2
      Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
  9. 24 1
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
  10. 0 566
      Emby.Server.Implementations/Networking/NetworkManager.cs
  11. 1 1
      Emby.Server.Implementations/Udp/UdpServer.cs
  12. 1 1
      Jellyfin.Api/Controllers/DlnaServerController.cs
  13. 5 3
      Jellyfin.Api/Controllers/StartupController.cs
  14. 4 4
      Jellyfin.Api/Controllers/SystemController.cs
  15. 71 0
      Jellyfin.Api/Helpers/ClassMigrationHelper.cs
  16. 0 234
      Jellyfin.Networking/Manager/INetworkManager.cs
  17. 0 3
      Jellyfin.Server/CoreAppHost.cs
  18. 3 2
      Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
  19. 2 1
      Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
  20. 21 20
      Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
  21. 5 33
      Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
  22. 18 48
      Jellyfin.Server/Program.cs
  23. 3 2
      Jellyfin.Server/Startup.cs
  24. 185 49
      MediaBrowser.Common/Net/INetworkManager.cs
  25. 23 21
      MediaBrowser.Controller/IServerApplicationHost.cs
  26. 1 1
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  27. 7 0
      MediaBrowser.sln
  28. 1 0
      RSSDP/RSSDP.csproj
  29. 2 2
      RSSDP/SsdpCommunicationsServer.cs
  30. 4 6
      RSSDP/SsdpDevicePublisher.cs
  31. 2 2
      RSSDP/SsdpRootDevice.cs
  32. 0 3
      tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs
  33. 39 0
      tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj
  34. 517 0
      tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs

+ 1 - 0
CONTRIBUTORS.md

@@ -7,6 +7,7 @@
  - [anthonylavado](https://github.com/anthonylavado)
  - [anthonylavado](https://github.com/anthonylavado)
  - [Artiume](https://github.com/Artiume)
  - [Artiume](https://github.com/Artiume)
  - [AThomsen](https://github.com/AThomsen)
  - [AThomsen](https://github.com/AThomsen)
+ - [barongreenback](https://github.com/BaronGreenback)
  - [barronpm](https://github.com/barronpm)
  - [barronpm](https://github.com/barronpm)
  - [bilde2910](https://github.com/bilde2910)
  - [bilde2910](https://github.com/bilde2910)
  - [bfayers](https://github.com/bfayers)
  - [bfayers](https://github.com/bfayers)

+ 25 - 15
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -2,12 +2,14 @@
 
 
 using System;
 using System;
 using System.Globalization;
 using System.Globalization;
+using System.Linq;
 using System.Net.Http;
 using System.Net.Http;
 using System.Net.Sockets;
 using System.Net.Sockets;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Emby.Dlna.PlayTo;
 using Emby.Dlna.PlayTo;
 using Emby.Dlna.Ssdp;
 using Emby.Dlna.Ssdp;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
@@ -134,20 +136,20 @@ namespace Emby.Dlna.Main
         {
         {
             await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
             await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
 
 
-            await ReloadComponents().ConfigureAwait(false);
+            ReloadComponents();
 
 
             _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
             _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
         }
         }
 
 
-        private async void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
+        private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
         {
         {
             if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
             if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
             {
             {
-                await ReloadComponents().ConfigureAwait(false);
+                ReloadComponents();
             }
             }
         }
         }
 
 
-        private async Task ReloadComponents()
+        private void ReloadComponents()
         {
         {
             var options = _config.GetDlnaConfiguration();
             var options = _config.GetDlnaConfiguration();
 
 
@@ -155,7 +157,7 @@ namespace Emby.Dlna.Main
 
 
             if (options.EnableServer)
             if (options.EnableServer)
             {
             {
-                await StartDevicePublisher(options).ConfigureAwait(false);
+                StartDevicePublisher(options);
             }
             }
             else
             else
             {
             {
@@ -225,7 +227,7 @@ namespace Emby.Dlna.Main
             }
             }
         }
         }
 
 
-        public async Task StartDevicePublisher(Configuration.DlnaOptions options)
+        public void StartDevicePublisher(Configuration.DlnaOptions options)
         {
         {
             if (!options.BlastAliveMessages)
             if (!options.BlastAliveMessages)
             {
             {
@@ -245,7 +247,7 @@ namespace Emby.Dlna.Main
                     SupportPnpRootDevice = false
                     SupportPnpRootDevice = false
                 };
                 };
 
 
-                await RegisterServerEndpoints().ConfigureAwait(false);
+                RegisterServerEndpoints();
 
 
                 _publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
                 _publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
             }
             }
@@ -255,14 +257,22 @@ namespace Emby.Dlna.Main
             }
             }
         }
         }
 
 
-        private async Task RegisterServerEndpoints()
+        private void RegisterServerEndpoints()
         {
         {
-            var addresses = await _appHost.GetLocalIpAddresses().ConfigureAwait(false);
-
             var udn = CreateUuid(_appHost.SystemId);
             var udn = CreateUuid(_appHost.SystemId);
             var descriptorUri = "/dlna/" + udn + "/description.xml";
             var descriptorUri = "/dlna/" + udn + "/description.xml";
 
 
-            foreach (var address in addresses)
+            var bindAddresses = NetworkManager.CreateCollection(
+                _networkManager.GetInternalBindAddresses()
+                .Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)));
+
+            if (bindAddresses.Count == 0)
+            {
+                // No interfaces returned, so use loopback.
+                bindAddresses = _networkManager.GetLoopbacks();
+            }
+
+            foreach (IPNetAddress address in bindAddresses)
             {
             {
                 if (address.AddressFamily == AddressFamily.InterNetworkV6)
                 if (address.AddressFamily == AddressFamily.InterNetworkV6)
                 {
                 {
@@ -271,7 +281,7 @@ namespace Emby.Dlna.Main
                 }
                 }
 
 
                 // Limit to LAN addresses only
                 // Limit to LAN addresses only
-                if (!_networkManager.IsAddressInSubnets(address, true, true))
+                if (!_networkManager.IsInLocalNetwork(address))
                 {
                 {
                     continue;
                     continue;
                 }
                 }
@@ -280,14 +290,14 @@ namespace Emby.Dlna.Main
 
 
                 _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
                 _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
 
 
-                var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri);
+                var uri = new Uri(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
 
 
                 var device = new SsdpRootDevice
                 var device = new SsdpRootDevice
                 {
                 {
                     CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
                     CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
                     Location = uri, // Must point to the URL that serves your devices UPnP description document.
                     Location = uri, // Must point to the URL that serves your devices UPnP description document.
-                    Address = address,
-                    SubnetMask = _networkManager.GetLocalIpSubnetMask(address),
+                    Address = address.Address,
+                    PrefixLength = address.PrefixLength,
                     FriendlyName = "Jellyfin",
                     FriendlyName = "Jellyfin",
                     Manufacturer = "Jellyfin",
                     Manufacturer = "Jellyfin",
                     ModelName = "Jellyfin Server",
                     ModelName = "Jellyfin Server",

+ 1 - 9
Emby.Dlna/PlayTo/PlayToManager.cs

@@ -177,15 +177,7 @@ namespace Emby.Dlna.PlayTo
 
 
                 _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
                 _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
 
 
-                string serverAddress;
-                if (info.LocalIpAddress == null || info.LocalIpAddress.Equals(IPAddress.Any) || info.LocalIpAddress.Equals(IPAddress.IPv6Any))
-                {
-                    serverAddress = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
-                }
-                else
-                {
-                    serverAddress = _appHost.GetLocalApiUrl(info.LocalIpAddress);
-                }
+                string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress);
 
 
                 controller = new PlayToController(
                 controller = new PlayToController(
                     sessionInfo,
                     sessionInfo,

+ 1 - 17
Emby.Dlna/Server/DescriptionXmlBuilder.cs

@@ -40,8 +40,6 @@ namespace Emby.Dlna.Server
             _serverId = serverId;
             _serverId = serverId;
         }
         }
 
 
-        private static bool EnableAbsoluteUrls => false;
-
         public string GetXml()
         public string GetXml()
         {
         {
             var builder = new StringBuilder();
             var builder = new StringBuilder();
@@ -75,13 +73,6 @@ namespace Emby.Dlna.Server
             builder.Append("<minor>0</minor>");
             builder.Append("<minor>0</minor>");
             builder.Append("</specVersion>");
             builder.Append("</specVersion>");
 
 
-            if (!EnableAbsoluteUrls)
-            {
-                builder.Append("<URLBase>")
-                    .Append(SecurityElement.Escape(_serverAddress))
-                    .Append("</URLBase>");
-            }
-
             AppendDeviceInfo(builder);
             AppendDeviceInfo(builder);
 
 
             builder.Append("</root>");
             builder.Append("</root>");
@@ -257,14 +248,7 @@ namespace Emby.Dlna.Server
                 return string.Empty;
                 return string.Empty;
             }
             }
 
 
-            url = url.TrimStart('/');
-
-            url = "/dlna/" + _serverUdn + "/" + url;
-
-            if (EnableAbsoluteUrls)
-            {
-                url = _serverAddress.TrimEnd('/') + url;
-            }
+            url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
 
 
             return SecurityElement.Escape(url);
             return SecurityElement.Escape(url);
         }
         }

+ 100 - 181
Emby.Server.Implementations/ApplicationHost.cs

@@ -15,6 +15,7 @@ using System.Security.Cryptography.X509Certificates;
 using System.Text;
 using System.Text;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using System.Xml.Serialization;
 using Emby.Dlna;
 using Emby.Dlna;
 using Emby.Dlna.Main;
 using Emby.Dlna.Main;
 using Emby.Dlna.Ssdp;
 using Emby.Dlna.Ssdp;
@@ -46,6 +47,8 @@ using Emby.Server.Implementations.SyncPlay;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.Updates;
 using Emby.Server.Implementations.Updates;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Networking.Configuration;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Events;
@@ -97,6 +100,8 @@ using MediaBrowser.Providers.Manager;
 using MediaBrowser.Providers.Plugins.Tmdb;
 using MediaBrowser.Providers.Plugins.Tmdb;
 using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.XbmcMetadata.Providers;
 using MediaBrowser.XbmcMetadata.Providers;
+using Microsoft.AspNetCore.DataProtection.Repositories;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
@@ -117,7 +122,6 @@ namespace Emby.Server.Implementations
         private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
         private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
 
 
         private readonly IFileSystem _fileSystemManager;
         private readonly IFileSystem _fileSystemManager;
-        private readonly INetworkManager _networkManager;
         private readonly IXmlSerializer _xmlSerializer;
         private readonly IXmlSerializer _xmlSerializer;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IStartupOptions _startupOptions;
         private readonly IStartupOptions _startupOptions;
@@ -158,6 +162,11 @@ namespace Emby.Server.Implementations
             }
             }
         }
         }
 
 
+        /// <summary>
+        /// Gets the <see cref="INetworkManager"/> singleton instance.
+        /// </summary>
+        public INetworkManager NetManager { get; internal set; }
+
         /// <summary>
         /// <summary>
         /// Occurs when [has pending restart changed].
         /// Occurs when [has pending restart changed].
         /// </summary>
         /// </summary>
@@ -210,7 +219,7 @@ namespace Emby.Server.Implementations
         private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
         private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
 
 
         /// <summary>
         /// <summary>
-        /// Gets the configuration manager.
+        /// Gets or sets the configuration manager.
         /// </summary>
         /// </summary>
         /// <value>The configuration manager.</value>
         /// <value>The configuration manager.</value>
         protected IConfigurationManager ConfigurationManager { get; set; }
         protected IConfigurationManager ConfigurationManager { get; set; }
@@ -243,14 +252,12 @@ namespace Emby.Server.Implementations
         /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
         /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
         /// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
         /// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
-        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
         /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
         /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
         public ApplicationHost(
         public ApplicationHost(
             IServerApplicationPaths applicationPaths,
             IServerApplicationPaths applicationPaths,
             ILoggerFactory loggerFactory,
             ILoggerFactory loggerFactory,
             IStartupOptions options,
             IStartupOptions options,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
-            INetworkManager networkManager,
             IServiceCollection serviceCollection)
             IServiceCollection serviceCollection)
         {
         {
             _xmlSerializer = new MyXmlSerializer();
             _xmlSerializer = new MyXmlSerializer();
@@ -258,14 +265,17 @@ namespace Emby.Server.Implementations
 
 
             ServiceCollection = serviceCollection;
             ServiceCollection = serviceCollection;
 
 
-            _networkManager = networkManager;
-            networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets;
-
             ApplicationPaths = applicationPaths;
             ApplicationPaths = applicationPaths;
             LoggerFactory = loggerFactory;
             LoggerFactory = loggerFactory;
             _fileSystemManager = fileSystem;
             _fileSystemManager = fileSystem;
 
 
             ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
             ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
+            // Have to migrate settings here as migration subsystem not yet initialised.
+            MigrateNetworkConfiguration();
+
+            // Have to pre-register the NetworkConfigurationFactory, as the configuration sub-system is not yet initialised.
+            ConfigurationManager.RegisterConfiguration<NetworkConfigurationFactory>();
+            NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
 
 
             Logger = LoggerFactory.CreateLogger<ApplicationHost>();
             Logger = LoggerFactory.CreateLogger<ApplicationHost>();
 
 
@@ -279,8 +289,6 @@ namespace Emby.Server.Implementations
 
 
             fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
             fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
 
 
-            _networkManager.NetworkChanged += OnNetworkChanged;
-
             CertificateInfo = new CertificateInfo
             CertificateInfo = new CertificateInfo
             {
             {
                 Path = ServerConfigurationManager.Configuration.CertificatePath,
                 Path = ServerConfigurationManager.Configuration.CertificatePath,
@@ -293,6 +301,22 @@ namespace Emby.Server.Implementations
             ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
             ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
         }
         }
 
 
+        /// <summary>
+        /// Temporary function to migration network settings out of system.xml and into network.xml.
+        /// TODO: remove at the point when a fixed migration path has been decided upon.
+        /// </summary>
+        private void MigrateNetworkConfiguration()
+        {
+            string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml");
+            if (!File.Exists(path))
+            {
+                var networkSettings = new NetworkConfiguration();
+                ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings);
+                _xmlSerializer.SerializeToFile(networkSettings, path);
+                Logger?.LogDebug("Successfully migrated network settings.");
+            }
+        }
+
         public string ExpandVirtualPath(string path)
         public string ExpandVirtualPath(string path)
         {
         {
             var appPaths = ApplicationPaths;
             var appPaths = ApplicationPaths;
@@ -309,16 +333,6 @@ namespace Emby.Server.Implementations
                 .Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
                 .Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
         }
         }
 
 
-        private string[] GetConfiguredLocalSubnets()
-        {
-            return ServerConfigurationManager.Configuration.LocalNetworkSubnets;
-        }
-
-        private void OnNetworkChanged(object sender, EventArgs e)
-        {
-            _validAddressResults.Clear();
-        }
-
         /// <inheritdoc />
         /// <inheritdoc />
         public Version ApplicationVersion { get; }
         public Version ApplicationVersion { get; }
 
 
@@ -485,14 +499,15 @@ namespace Emby.Server.Implementations
         /// <inheritdoc/>
         /// <inheritdoc/>
         public void Init()
         public void Init()
         {
         {
-            HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber;
-            HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber;
+            var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
+            HttpPort = networkConfiguration.HttpServerPortNumber;
+            HttpsPort = networkConfiguration.HttpsPortNumber;
 
 
             // Safeguard against invalid configuration
             // Safeguard against invalid configuration
             if (HttpPort == HttpsPort)
             if (HttpPort == HttpsPort)
             {
             {
-                HttpPort = ServerConfiguration.DefaultHttpPort;
-                HttpsPort = ServerConfiguration.DefaultHttpsPort;
+                HttpPort = NetworkConfiguration.DefaultHttpPort;
+                HttpsPort = NetworkConfiguration.DefaultHttpsPort;
             }
             }
 
 
             DiscoverTypes();
             DiscoverTypes();
@@ -521,7 +536,7 @@ namespace Emby.Server.Implementations
             ServiceCollection.AddSingleton(_fileSystemManager);
             ServiceCollection.AddSingleton(_fileSystemManager);
             ServiceCollection.AddSingleton<TmdbClientManager>();
             ServiceCollection.AddSingleton<TmdbClientManager>();
 
 
-            ServiceCollection.AddSingleton(_networkManager);
+            ServiceCollection.AddSingleton(NetManager);
 
 
             ServiceCollection.AddSingleton<IIsoManager, IsoManager>();
             ServiceCollection.AddSingleton<IIsoManager, IsoManager>();
 
 
@@ -902,9 +917,10 @@ namespace Emby.Server.Implementations
             // Don't do anything if these haven't been set yet
             // Don't do anything if these haven't been set yet
             if (HttpPort != 0 && HttpsPort != 0)
             if (HttpPort != 0 && HttpsPort != 0)
             {
             {
+                var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
                 // Need to restart if ports have changed
                 // Need to restart if ports have changed
-                if (ServerConfigurationManager.Configuration.HttpServerPortNumber != HttpPort ||
-                    ServerConfigurationManager.Configuration.HttpsPortNumber != HttpsPort)
+                if (networkConfiguration.HttpServerPortNumber != HttpPort ||
+                    networkConfiguration.HttpsPortNumber != HttpsPort)
                 {
                 {
                     if (ServerConfigurationManager.Configuration.IsPortAuthorized)
                     if (ServerConfigurationManager.Configuration.IsPortAuthorized)
                     {
                     {
@@ -1147,6 +1163,9 @@ namespace Emby.Server.Implementations
             // Xbmc
             // Xbmc
             yield return typeof(ArtistNfoProvider).Assembly;
             yield return typeof(ArtistNfoProvider).Assembly;
 
 
+            // Network
+            yield return typeof(NetworkManager).Assembly;
+
             foreach (var i in GetAssembliesWithPartsInternal())
             foreach (var i in GetAssembliesWithPartsInternal())
             {
             {
                 yield return i;
                 yield return i;
@@ -1158,13 +1177,10 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// <summary>
         /// Gets the system status.
         /// Gets the system status.
         /// </summary>
         /// </summary>
-        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="source">Where this request originated.</param>
         /// <returns>SystemInfo.</returns>
         /// <returns>SystemInfo.</returns>
-        public async Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken)
+        public SystemInfo GetSystemInfo(IPAddress source)
         {
         {
-            var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
-            var transcodingTempPath = ConfigurationManager.GetTranscodePath();
-
             return new SystemInfo
             return new SystemInfo
             {
             {
                 HasPendingRestart = HasPendingRestart,
                 HasPendingRestart = HasPendingRestart,
@@ -1184,9 +1200,9 @@ namespace Emby.Server.Implementations
                 CanSelfRestart = CanSelfRestart,
                 CanSelfRestart = CanSelfRestart,
                 CanLaunchWebBrowser = CanLaunchWebBrowser,
                 CanLaunchWebBrowser = CanLaunchWebBrowser,
                 HasUpdateAvailable = HasUpdateAvailable,
                 HasUpdateAvailable = HasUpdateAvailable,
-                TranscodingTempPath = transcodingTempPath,
+                TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
                 ServerName = FriendlyName,
                 ServerName = FriendlyName,
-                LocalAddress = localAddress,
+                LocalAddress = GetSmartApiUrl(source),
                 SupportsLibraryMonitor = true,
                 SupportsLibraryMonitor = true,
                 EncoderLocation = _mediaEncoder.EncoderLocation,
                 EncoderLocation = _mediaEncoder.EncoderLocation,
                 SystemArchitecture = RuntimeInformation.OSArchitecture,
                 SystemArchitecture = RuntimeInformation.OSArchitecture,
@@ -1195,14 +1211,12 @@ namespace Emby.Server.Implementations
         }
         }
 
 
         public IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo()
         public IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo()
-            => _networkManager.GetMacAddresses()
+            => NetManager.GetMacAddresses()
                 .Select(i => new WakeOnLanInfo(i))
                 .Select(i => new WakeOnLanInfo(i))
                 .ToList();
                 .ToList();
 
 
-        public async Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken)
+        public PublicSystemInfo GetPublicSystemInfo(IPAddress source)
         {
         {
-            var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
-
             return new PublicSystemInfo
             return new PublicSystemInfo
             {
             {
                 Version = ApplicationVersionString,
                 Version = ApplicationVersionString,
@@ -1210,193 +1224,98 @@ namespace Emby.Server.Implementations
                 Id = SystemId,
                 Id = SystemId,
                 OperatingSystem = OperatingSystem.Id.ToString(),
                 OperatingSystem = OperatingSystem.Id.ToString(),
                 ServerName = FriendlyName,
                 ServerName = FriendlyName,
-                LocalAddress = localAddress,
+                LocalAddress = GetSmartApiUrl(source),
                 StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
                 StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
             };
             };
         }
         }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
-        public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps;
+        public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.GetNetworkConfiguration().EnableHttps;
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
-        public async Task<string> GetLocalApiUrl(CancellationToken cancellationToken)
+        public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
         {
         {
-            try
+            // Published server ends with a /
+            if (_startupOptions.PublishedServerUrl != null)
             {
             {
-                // Return the first matched address, if found, or the first known local address
-                var addresses = await GetLocalIpAddressesInternal(false, 1, cancellationToken).ConfigureAwait(false);
-                if (addresses.Count == 0)
-                {
-                    return null;
-                }
-
-                return GetLocalApiUrl(addresses[0]);
+                // Published server ends with a '/', so we need to remove it.
+                return _startupOptions.PublishedServerUrl.ToString().Trim('/');
             }
             }
-            catch (Exception ex)
+
+            string smart = NetManager.GetBindInterface(ipAddress, out port);
+            // If the smartAPI doesn't start with http then treat it as a host or ip.
+            if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
             {
             {
-                Logger.LogError(ex, "Error getting local Ip address information");
+                return smart.Trim('/');
             }
             }
 
 
-            return null;
+            return GetLocalApiUrl(smart.Trim('/'), null, port);
         }
         }
 
 
-        /// <summary>
-        /// Removes the scope id from IPv6 addresses.
-        /// </summary>
-        /// <param name="address">The IPv6 address.</param>
-        /// <returns>The IPv6 address without the scope id.</returns>
-        private ReadOnlySpan<char> RemoveScopeId(ReadOnlySpan<char> address)
+        /// <inheritdoc/>
+        public string GetSmartApiUrl(HttpRequest request, int? port = null)
         {
         {
-            var index = address.IndexOf('%');
-            if (index == -1)
+            // Published server ends with a /
+            if (_startupOptions.PublishedServerUrl != null)
             {
             {
-                return address;
+                // Published server ends with a '/', so we need to remove it.
+                return _startupOptions.PublishedServerUrl.ToString().Trim('/');
             }
             }
 
 
-            return address.Slice(0, index);
-        }
-
-        /// <inheritdoc />
-        public string GetLocalApiUrl(IPAddress ipAddress)
-        {
-            if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
+            string smart = NetManager.GetBindInterface(request, out port);
+            // If the smartAPI doesn't start with http then treat it as a host or ip.
+            if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
             {
             {
-                var str = RemoveScopeId(ipAddress.ToString());
-                Span<char> span = new char[str.Length + 2];
-                span[0] = '[';
-                str.CopyTo(span.Slice(1));
-                span[^1] = ']';
-
-                return GetLocalApiUrl(span);
+                return smart.Trim('/');
             }
             }
 
 
-            return GetLocalApiUrl(ipAddress.ToString());
+            return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port);
         }
         }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
-        public string GetLoopbackHttpApiUrl()
-        {
-            return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort);
-        }
-
-        /// <inheritdoc/>
-        public string GetLocalApiUrl(ReadOnlySpan<char> host, string scheme = null, int? port = null)
-        {
-            // NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
-            // not. For consistency, always trim the trailing slash.
-            return new UriBuilder
-            {
-                Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
-                Host = host.ToString(),
-                Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
-                Path = ServerConfigurationManager.Configuration.BaseUrl
-            }.ToString().TrimEnd('/');
-        }
-
-        public Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken)
+        public string GetSmartApiUrl(string hostname, int? port = null)
         {
         {
-            return GetLocalIpAddressesInternal(true, 0, cancellationToken);
-        }
-
-        private async Task<List<IPAddress>> GetLocalIpAddressesInternal(bool allowLoopback, int limit, CancellationToken cancellationToken)
-        {
-            var addresses = ServerConfigurationManager
-                .Configuration
-                .LocalNetworkAddresses
-                .Select(x => NormalizeConfiguredLocalAddress(x))
-                .Where(i => i != null)
-                .ToList();
-
-            if (addresses.Count == 0)
+            // Published server ends with a /
+            if (_startupOptions.PublishedServerUrl != null)
             {
             {
-                addresses.AddRange(_networkManager.GetLocalIpAddresses());
+                // Published server ends with a '/', so we need to remove it.
+                return _startupOptions.PublishedServerUrl.ToString().Trim('/');
             }
             }
 
 
-            var resultList = new List<IPAddress>();
+            string smart = NetManager.GetBindInterface(hostname, out port);
 
 
-            foreach (var address in addresses)
+            // If the smartAPI doesn't start with http then treat it as a host or ip.
+            if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
             {
             {
-                if (!allowLoopback)
-                {
-                    if (address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback))
-                    {
-                        continue;
-                    }
-                }
-
-                if (await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false))
-                {
-                    resultList.Add(address);
-
-                    if (limit > 0 && resultList.Count >= limit)
-                    {
-                        return resultList;
-                    }
-                }
+                return smart.Trim('/');
             }
             }
 
 
-            return resultList;
+            return GetLocalApiUrl(smart.Trim('/'), null, port);
         }
         }
 
 
-        public IPAddress NormalizeConfiguredLocalAddress(ReadOnlySpan<char> address)
+        /// <inheritdoc/>
+        public string GetLoopbackHttpApiUrl()
         {
         {
-            var index = address.Trim('/').IndexOf('/');
-            if (index != -1)
+            if (NetManager.IsIP6Enabled)
             {
             {
-                address = address.Slice(index + 1);
+                return GetLocalApiUrl("::1", Uri.UriSchemeHttp, HttpPort);
             }
             }
 
 
-            if (IPAddress.TryParse(address.Trim('/'), out IPAddress result))
-            {
-                return result;
-            }
-
-            return null;
+            return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort);
         }
         }
 
 
-        private readonly ConcurrentDictionary<string, bool> _validAddressResults = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
-
-        private async Task<bool> IsLocalIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken)
+        /// <inheritdoc/>
+        public string GetLocalApiUrl(string host, string scheme = null, int? port = null)
         {
         {
-            if (address.Equals(IPAddress.Loopback)
-                || address.Equals(IPAddress.IPv6Loopback))
-            {
-                return true;
-            }
-
-            var apiUrl = GetLocalApiUrl(address) + "/system/ping";
-
-            if (_validAddressResults.TryGetValue(apiUrl, out var cachedResult))
-            {
-                return cachedResult;
-            }
-
-            try
-            {
-                using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl);
-                using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
-                    .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
-
-                await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-                var result = await System.Text.Json.JsonSerializer.DeserializeAsync<string>(stream, JsonDefaults.GetOptions(), cancellationToken).ConfigureAwait(false);
-                var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase);
-
-                _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid);
-                Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid);
-                return valid;
-            }
-            catch (OperationCanceledException)
-            {
-                Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, "Cancelled");
-                throw;
-            }
-            catch (Exception ex)
+            // NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
+            // not. For consistency, always trim the trailing slash.
+            return new UriBuilder
             {
             {
-                Logger.LogDebug(ex, "Ping test result to {0}. Success: {1}", apiUrl, false);
-
-                _validAddressResults.AddOrUpdate(apiUrl, false, (k, v) => false);
-                return false;
-            }
+                Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
+                Host = host,
+                Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
+                Path = ServerConfigurationManager.GetNetworkConfiguration().BaseUrl
+            }.ToString().TrimEnd('/');
         }
         }
 
 
         public string FriendlyName =>
         public string FriendlyName =>

+ 0 - 1
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -22,7 +22,6 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="IPNetwork2" Version="2.5.226" />
     <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
     <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />

+ 7 - 4
Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs

@@ -8,6 +8,7 @@ using System.Text;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Data.Events;
 using Jellyfin.Data.Events;
+using Jellyfin.Networking.Configuration;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Plugins;
@@ -56,7 +57,7 @@ namespace Emby.Server.Implementations.EntryPoints
         private string GetConfigIdentifier()
         private string GetConfigIdentifier()
         {
         {
             const char Separator = '|';
             const char Separator = '|';
-            var config = _config.Configuration;
+            var config = _config.GetNetworkConfiguration();
 
 
             return new StringBuilder(32)
             return new StringBuilder(32)
                 .Append(config.EnableUPnP).Append(Separator)
                 .Append(config.EnableUPnP).Append(Separator)
@@ -93,7 +94,8 @@ namespace Emby.Server.Implementations.EntryPoints
 
 
         private void Start()
         private void Start()
         {
         {
-            if (!_config.Configuration.EnableUPnP || !_config.Configuration.EnableRemoteAccess)
+            var config = _config.GetNetworkConfiguration();
+            if (!config.EnableUPnP || !config.EnableRemoteAccess)
             {
             {
                 return;
                 return;
             }
             }
@@ -156,11 +158,12 @@ namespace Emby.Server.Implementations.EntryPoints
 
 
         private IEnumerable<Task> CreatePortMaps(INatDevice device)
         private IEnumerable<Task> CreatePortMaps(INatDevice device)
         {
         {
-            yield return CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort);
+            var config = _config.GetNetworkConfiguration();
+            yield return CreatePortMap(device, _appHost.HttpPort, config.PublicPort);
 
 
             if (_appHost.ListenWithHttps)
             if (_appHost.ListenWithHttps)
             {
             {
-                yield return CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort);
+                yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicHttpsPort);
             }
             }
         }
         }
 
 

+ 1 - 2
Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs

@@ -76,7 +76,6 @@ namespace Emby.Server.Implementations.LiveTv
             }
             }
 
 
             var list = sources.ToList();
             var list = sources.ToList();
-            var serverUrl = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
 
 
             foreach (var source in list)
             foreach (var source in list)
             {
             {
@@ -103,7 +102,7 @@ namespace Emby.Server.Implementations.LiveTv
                 // Dummy this up so that direct play checks can still run
                 // Dummy this up so that direct play checks can still run
                 if (string.IsNullOrEmpty(source.Path) && source.Protocol == MediaProtocol.Http)
                 if (string.IsNullOrEmpty(source.Path) && source.Protocol == MediaProtocol.Http)
                 {
                 {
-                    source.Path = serverUrl;
+                    source.Path = _appHost.GetSmartApiUrl(string.Empty);
                 }
                 }
             }
             }
 
 

+ 24 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs

@@ -3,7 +3,9 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
+using System.Linq;
 using System.Net;
 using System.Net;
+using System.Net.NetworkInformation;
 using System.Net.Sockets;
 using System.Net.Sockets;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -50,6 +52,26 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             EnableStreamSharing = true;
             EnableStreamSharing = true;
         }
         }
 
 
+        /// <summary>
+        /// Returns an unused UDP port number in the range specified.
+        /// Temporarily placed here until future network PR merged.
+        /// </summary>
+        /// <param name="range">Upper and Lower boundary of ports to select.</param>
+        /// <returns>System.Int32.</returns>
+        private static int GetUdpPortFromRange((int Min, int Max) range)
+        {
+            var properties = IPGlobalProperties.GetIPGlobalProperties();
+
+            // Get active udp listeners.
+            var udpListenerPorts = properties.GetActiveUdpListeners()
+                        .Where(n => n.Port >= range.Min && n.Port <= range.Max)
+                        .Select(n => n.Port);
+
+            return Enumerable
+                .Range(range.Min, range.Max)
+                .FirstOrDefault(i => !udpListenerPorts.Contains(i));
+        }
+
         public override async Task Open(CancellationToken openCancellationToken)
         public override async Task Open(CancellationToken openCancellationToken)
         {
         {
             LiveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested();
             LiveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested();
@@ -57,7 +79,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             var mediaSource = OriginalMediaSource;
             var mediaSource = OriginalMediaSource;
 
 
             var uri = new Uri(mediaSource.Path);
             var uri = new Uri(mediaSource.Path);
-            var localPort = _networkManager.GetRandomUnusedUdpPort();
+            // Temporary code to reduce PR size. This will be updated by a future network pr.
+            var localPort = GetUdpPortFromRange((49152, 65535));
 
 
             Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath));
             Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath));
 
 

+ 0 - 566
Emby.Server.Implementations/Networking/NetworkManager.cs

@@ -1,566 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using System.Net.NetworkInformation;
-using System.Net.Sockets;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Networking
-{
-    /// <summary>
-    /// Class to take care of network interface management.
-    /// </summary>
-    public class NetworkManager : INetworkManager
-    {
-        private readonly ILogger<NetworkManager> _logger;
-        private readonly object _localIpAddressSyncLock = new object();
-        private readonly object _subnetLookupLock = new object();
-        private readonly Dictionary<string, List<string>> _subnetLookup = new Dictionary<string, List<string>>(StringComparer.Ordinal);
-
-        private IPAddress[] _localIpAddresses;
-
-        private List<PhysicalAddress> _macAddresses;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="NetworkManager"/> class.
-        /// </summary>
-        /// <param name="logger">Logger to use for messages.</param>
-        public NetworkManager(ILogger<NetworkManager> logger)
-        {
-            _logger = logger;
-
-            NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged;
-            NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
-        }
-
-        /// <inheritdoc/>
-        public event EventHandler NetworkChanged;
-
-        /// <inheritdoc/>
-        public Func<string[]> LocalSubnetsFn { get; set; }
-
-        private void OnNetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
-        {
-            _logger.LogDebug("NetworkAvailabilityChanged");
-            OnNetworkChanged();
-        }
-
-        private void OnNetworkAddressChanged(object sender, EventArgs e)
-        {
-            _logger.LogDebug("NetworkAddressChanged");
-            OnNetworkChanged();
-        }
-
-        private void OnNetworkChanged()
-        {
-            lock (_localIpAddressSyncLock)
-            {
-                _localIpAddresses = null;
-                _macAddresses = null;
-            }
-
-            NetworkChanged?.Invoke(this, EventArgs.Empty);
-        }
-
-        /// <inheritdoc/>
-        public IPAddress[] GetLocalIpAddresses()
-        {
-            lock (_localIpAddressSyncLock)
-            {
-                if (_localIpAddresses == null)
-                {
-                    var addresses = GetLocalIpAddressesInternal().ToArray();
-
-                    _localIpAddresses = addresses;
-                }
-
-                return _localIpAddresses;
-            }
-        }
-
-        private List<IPAddress> GetLocalIpAddressesInternal()
-        {
-            var list = GetIPsDefault().ToList();
-
-            if (list.Count == 0)
-            {
-                list = GetLocalIpAddressesFallback().GetAwaiter().GetResult().ToList();
-            }
-
-            var listClone = new List<IPAddress>();
-
-            var subnets = LocalSubnetsFn();
-
-            foreach (var i in list)
-            {
-                if (i.IsIPv6LinkLocal || i.ToString().StartsWith("169.254.", StringComparison.OrdinalIgnoreCase))
-                {
-                    continue;
-                }
-
-                if (Array.IndexOf(subnets, $"[{i}]") == -1)
-                {
-                    listClone.Add(i);
-                }
-            }
-
-            return listClone
-                .OrderBy(i => i.AddressFamily == AddressFamily.InterNetwork ? 0 : 1)
-                // .ThenBy(i => listClone.IndexOf(i))
-                .GroupBy(i => i.ToString())
-                .Select(x => x.First())
-                .ToList();
-        }
-
-        /// <inheritdoc/>
-        public bool IsInPrivateAddressSpace(string endpoint)
-        {
-            return IsInPrivateAddressSpace(endpoint, true);
-        }
-
-        // Checks if the address in endpoint is an RFC1918, RFC1122, or RFC3927 address
-        private bool IsInPrivateAddressSpace(string endpoint, bool checkSubnets)
-        {
-            if (string.Equals(endpoint, "::1", StringComparison.OrdinalIgnoreCase))
-            {
-                return true;
-            }
-
-            // IPV6
-            if (endpoint.Split('.').Length > 4)
-            {
-                // Handle ipv4 mapped to ipv6
-                var originalEndpoint = endpoint;
-                endpoint = endpoint.Replace("::ffff:", string.Empty, StringComparison.OrdinalIgnoreCase);
-
-                if (string.Equals(endpoint, originalEndpoint, StringComparison.OrdinalIgnoreCase))
-                {
-                    return false;
-                }
-            }
-
-            // Private address space:
-
-            if (string.Equals(endpoint, "localhost", StringComparison.OrdinalIgnoreCase))
-            {
-                return true;
-            }
-
-            if (!IPAddress.TryParse(endpoint, out var ipAddress))
-            {
-                return false;
-            }
-
-            // GetAddressBytes
-            Span<byte> octet = stackalloc byte[ipAddress.AddressFamily == AddressFamily.InterNetwork ? 4 : 16];
-            ipAddress.TryWriteBytes(octet, out _);
-
-            if ((octet[0] == 10) ||
-                (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918
-                (octet[0] == 192 && octet[1] == 168) || // RFC1918
-                (octet[0] == 127) || // RFC1122
-                (octet[0] == 169 && octet[1] == 254)) // RFC3927
-            {
-                return true;
-            }
-
-            if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint))
-            {
-                return true;
-            }
-
-            return false;
-        }
-
-        /// <inheritdoc/>
-        public bool IsInPrivateAddressSpaceAndLocalSubnet(string endpoint)
-        {
-            if (endpoint.StartsWith("10.", StringComparison.OrdinalIgnoreCase))
-            {
-                var endpointFirstPart = endpoint.Split('.')[0];
-
-                var subnets = GetSubnets(endpointFirstPart);
-
-                foreach (var subnet_Match in subnets)
-                {
-                    // logger.LogDebug("subnet_Match:" + subnet_Match);
-
-                    if (endpoint.StartsWith(subnet_Match + ".", StringComparison.OrdinalIgnoreCase))
-                    {
-                        return true;
-                    }
-                }
-            }
-
-            return false;
-        }
-
-        // Gives a list of possible subnets from the system whose interface ip starts with endpointFirstPart
-        private List<string> GetSubnets(string endpointFirstPart)
-        {
-            lock (_subnetLookupLock)
-            {
-                if (_subnetLookup.TryGetValue(endpointFirstPart, out var subnets))
-                {
-                    return subnets;
-                }
-
-                subnets = new List<string>();
-
-                foreach (var adapter in NetworkInterface.GetAllNetworkInterfaces())
-                {
-                    foreach (var unicastIPAddressInformation in adapter.GetIPProperties().UnicastAddresses)
-                    {
-                        if (unicastIPAddressInformation.Address.AddressFamily == AddressFamily.InterNetwork && endpointFirstPart == unicastIPAddressInformation.Address.ToString().Split('.')[0])
-                        {
-                            int subnet_Test = 0;
-                            foreach (string part in unicastIPAddressInformation.IPv4Mask.ToString().Split('.'))
-                            {
-                                if (part.Equals("0", StringComparison.Ordinal))
-                                {
-                                    break;
-                                }
-
-                                subnet_Test++;
-                            }
-
-                            var subnet_Match = string.Join(".", unicastIPAddressInformation.Address.ToString().Split('.').Take(subnet_Test).ToArray());
-
-                            // TODO: Is this check necessary?
-                            if (adapter.OperationalStatus == OperationalStatus.Up)
-                            {
-                                subnets.Add(subnet_Match);
-                            }
-                        }
-                    }
-                }
-
-                _subnetLookup[endpointFirstPart] = subnets;
-
-                return subnets;
-            }
-        }
-
-        /// <inheritdoc/>
-        public bool IsInLocalNetwork(string endpoint)
-        {
-            return IsInLocalNetworkInternal(endpoint, true);
-        }
-
-        /// <inheritdoc/>
-        public bool IsAddressInSubnets(string addressString, string[] subnets)
-        {
-            return IsAddressInSubnets(IPAddress.Parse(addressString), addressString, subnets);
-        }
-
-        /// <inheritdoc/>
-        public bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC)
-        {
-            // GetAddressBytes
-            Span<byte> octet = stackalloc byte[address.AddressFamily == AddressFamily.InterNetwork ? 4 : 16];
-            address.TryWriteBytes(octet, out _);
-
-            if ((octet[0] == 127) || // RFC1122
-                (octet[0] == 169 && octet[1] == 254)) // RFC3927
-            {
-                // don't use on loopback or 169 interfaces
-                return false;
-            }
-
-            string addressString = address.ToString();
-            string excludeAddress = "[" + addressString + "]";
-            var subnets = LocalSubnetsFn();
-
-            // Include any address if LAN subnets aren't specified
-            if (subnets.Length == 0)
-            {
-                return true;
-            }
-
-            // Exclude any addresses if they appear in the LAN list in [ ]
-            if (Array.IndexOf(subnets, excludeAddress) != -1)
-            {
-                return false;
-            }
-
-            return IsAddressInSubnets(address, addressString, subnets);
-        }
-
-        /// <summary>
-        /// Checks if the give address falls within the ranges given in [subnets]. The addresses in subnets can be hosts or subnets in the CIDR format.
-        /// </summary>
-        /// <param name="address">IPAddress version of the address.</param>
-        /// <param name="addressString">The address to check.</param>
-        /// <param name="subnets">If true, check against addresses in the LAN settings which have [] arroud and return true if it matches the address give in address.</param>
-        /// <returns><c>false</c>if the address isn't in the subnets, <c>true</c> otherwise.</returns>
-        private static bool IsAddressInSubnets(IPAddress address, string addressString, string[] subnets)
-        {
-            foreach (var subnet in subnets)
-            {
-                var normalizedSubnet = subnet.Trim();
-                // Is the subnet a host address and does it match the address being passes?
-                if (string.Equals(normalizedSubnet, addressString, StringComparison.OrdinalIgnoreCase))
-                {
-                    return true;
-                }
-
-                // Parse CIDR subnets and see if address falls within it.
-                if (normalizedSubnet.Contains('/', StringComparison.Ordinal))
-                {
-                    try
-                    {
-                        var ipNetwork = IPNetwork.Parse(normalizedSubnet);
-                        if (ipNetwork.Contains(address))
-                        {
-                            return true;
-                        }
-                    }
-                    catch
-                    {
-                        // Ignoring - invalid subnet passed encountered.
-                    }
-                }
-            }
-
-            return false;
-        }
-
-        private bool IsInLocalNetworkInternal(string endpoint, bool resolveHost)
-        {
-            if (string.IsNullOrEmpty(endpoint))
-            {
-                throw new ArgumentNullException(nameof(endpoint));
-            }
-
-            if (IPAddress.TryParse(endpoint, out var address))
-            {
-                var addressString = address.ToString();
-
-                var localSubnetsFn = LocalSubnetsFn;
-                if (localSubnetsFn != null)
-                {
-                    var localSubnets = localSubnetsFn();
-                    foreach (var subnet in localSubnets)
-                    {
-                        // Only validate if there's at least one valid entry.
-                        if (!string.IsNullOrWhiteSpace(subnet))
-                        {
-                            return IsAddressInSubnets(address, addressString, localSubnets) || IsInPrivateAddressSpace(addressString, false);
-                        }
-                    }
-                }
-
-                int lengthMatch = 100;
-                if (address.AddressFamily == AddressFamily.InterNetwork)
-                {
-                    lengthMatch = 4;
-                    if (IsInPrivateAddressSpace(addressString, true))
-                    {
-                        return true;
-                    }
-                }
-                else if (address.AddressFamily == AddressFamily.InterNetworkV6)
-                {
-                    lengthMatch = 9;
-                    if (IsInPrivateAddressSpace(endpoint, true))
-                    {
-                        return true;
-                    }
-                }
-
-                // Should be even be doing this with ipv6?
-                if (addressString.Length >= lengthMatch)
-                {
-                    var prefix = addressString.Substring(0, lengthMatch);
-
-                    if (GetLocalIpAddresses().Any(i => i.ToString().StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
-                    {
-                        return true;
-                    }
-                }
-            }
-            else if (resolveHost)
-            {
-                if (Uri.TryCreate(endpoint, UriKind.RelativeOrAbsolute, out var uri))
-                {
-                    try
-                    {
-                        var host = uri.DnsSafeHost;
-                        _logger.LogDebug("Resolving host {0}", host);
-
-                        address = GetIpAddresses(host).GetAwaiter().GetResult().FirstOrDefault();
-
-                        if (address != null)
-                        {
-                            _logger.LogDebug("{0} resolved to {1}", host, address);
-
-                            return IsInLocalNetworkInternal(address.ToString(), false);
-                        }
-                    }
-                    catch (InvalidOperationException)
-                    {
-                        // Can happen with reverse proxy or IIS url rewriting?
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.LogError(ex, "Error resolving hostname");
-                    }
-                }
-            }
-
-            return false;
-        }
-
-        private static Task<IPAddress[]> GetIpAddresses(string hostName)
-        {
-            return Dns.GetHostAddressesAsync(hostName);
-        }
-
-        private IEnumerable<IPAddress> GetIPsDefault()
-        {
-            IEnumerable<NetworkInterface> interfaces;
-
-            try
-            {
-                interfaces = NetworkInterface.GetAllNetworkInterfaces()
-                    .Where(x => x.OperationalStatus == OperationalStatus.Up
-                        || x.OperationalStatus == OperationalStatus.Unknown);
-            }
-            catch (NetworkInformationException ex)
-            {
-                _logger.LogError(ex, "Error in GetAllNetworkInterfaces");
-                return Enumerable.Empty<IPAddress>();
-            }
-
-            return interfaces.SelectMany(network =>
-            {
-                var ipProperties = network.GetIPProperties();
-
-                // Exclude any addresses if they appear in the LAN list in [ ]
-
-                return ipProperties.UnicastAddresses
-                    .Select(i => i.Address)
-                    .Where(i => i.AddressFamily == AddressFamily.InterNetwork || i.AddressFamily == AddressFamily.InterNetworkV6);
-            }).GroupBy(i => i.ToString())
-                .Select(x => x.First());
-        }
-
-        private static async Task<IEnumerable<IPAddress>> GetLocalIpAddressesFallback()
-        {
-            var host = await Dns.GetHostEntryAsync(Dns.GetHostName()).ConfigureAwait(false);
-
-            // Reverse them because the last one is usually the correct one
-            // It's not fool-proof so ultimately the consumer will have to examine them and decide
-            return host.AddressList
-                .Where(i => i.AddressFamily == AddressFamily.InterNetwork || i.AddressFamily == AddressFamily.InterNetworkV6)
-                .Reverse();
-        }
-
-        /// <summary>
-        /// Gets a random port number that is currently available.
-        /// </summary>
-        /// <returns>System.Int32.</returns>
-        public int GetRandomUnusedTcpPort()
-        {
-            var listener = new TcpListener(IPAddress.Any, 0);
-            listener.Start();
-            var port = ((IPEndPoint)listener.LocalEndpoint).Port;
-            listener.Stop();
-            return port;
-        }
-
-        /// <inheritdoc/>
-        public int GetRandomUnusedUdpPort()
-        {
-            var localEndPoint = new IPEndPoint(IPAddress.Any, 0);
-            using (var udpClient = new UdpClient(localEndPoint))
-            {
-                return ((IPEndPoint)udpClient.Client.LocalEndPoint).Port;
-            }
-        }
-
-        /// <inheritdoc/>
-        public List<PhysicalAddress> GetMacAddresses()
-        {
-            return _macAddresses ??= GetMacAddressesInternal().ToList();
-        }
-
-        private static IEnumerable<PhysicalAddress> GetMacAddressesInternal()
-            => NetworkInterface.GetAllNetworkInterfaces()
-                .Where(i => i.NetworkInterfaceType != NetworkInterfaceType.Loopback)
-                .Select(x => x.GetPhysicalAddress())
-                .Where(x => !x.Equals(PhysicalAddress.None));
-
-        /// <inheritdoc/>
-        public bool IsInSameSubnet(IPAddress address1, IPAddress address2, IPAddress subnetMask)
-        {
-            IPAddress network1 = GetNetworkAddress(address1, subnetMask);
-            IPAddress network2 = GetNetworkAddress(address2, subnetMask);
-            return network1.Equals(network2);
-        }
-
-        private IPAddress GetNetworkAddress(IPAddress address, IPAddress subnetMask)
-        {
-            int size = address.AddressFamily == AddressFamily.InterNetwork ? 4 : 16;
-
-            // GetAddressBytes
-            Span<byte> ipAddressBytes = stackalloc byte[size];
-            address.TryWriteBytes(ipAddressBytes, out _);
-
-            // GetAddressBytes
-            Span<byte> subnetMaskBytes = stackalloc byte[size];
-            subnetMask.TryWriteBytes(subnetMaskBytes, out _);
-
-            if (ipAddressBytes.Length != subnetMaskBytes.Length)
-            {
-                throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
-            }
-
-            byte[] broadcastAddress = new byte[ipAddressBytes.Length];
-            for (int i = 0; i < broadcastAddress.Length; i++)
-            {
-                broadcastAddress[i] = (byte)(ipAddressBytes[i] & subnetMaskBytes[i]);
-            }
-
-            return new IPAddress(broadcastAddress);
-        }
-
-         /// <inheritdoc/>
-        public IPAddress GetLocalIpSubnetMask(IPAddress address)
-        {
-            NetworkInterface[] interfaces;
-
-            try
-            {
-                var validStatuses = new[] { OperationalStatus.Up, OperationalStatus.Unknown };
-
-                interfaces = NetworkInterface.GetAllNetworkInterfaces()
-                    .Where(i => validStatuses.Contains(i.OperationalStatus))
-                    .ToArray();
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error in GetAllNetworkInterfaces");
-                return null;
-            }
-
-            foreach (NetworkInterface ni in interfaces)
-            {
-                foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses)
-                {
-                    if (ip.Address.Equals(address) && ip.IPv4Mask != null)
-                    {
-                        return ip.IPv4Mask;
-                    }
-                }
-            }
-
-            return null;
-        }
-    }
-}

+ 1 - 1
Emby.Server.Implementations/Udp/UdpServer.cs

@@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Udp
         {
         {
             string localUrl = !string.IsNullOrEmpty(_config[AddressOverrideConfigKey])
             string localUrl = !string.IsNullOrEmpty(_config[AddressOverrideConfigKey])
                 ? _config[AddressOverrideConfigKey]
                 ? _config[AddressOverrideConfigKey]
-                : await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
+                : _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address);
 
 
             if (!string.IsNullOrEmpty(localUrl))
             if (!string.IsNullOrEmpty(localUrl))
             {
             {

+ 1 - 1
Jellyfin.Api/Controllers/DlnaServerController.cs

@@ -252,7 +252,7 @@ namespace Jellyfin.Api.Controllers
 
 
         private string GetAbsoluteUri()
         private string GetAbsoluteUri()
         {
         {
-            return $"{Request.Scheme}://{Request.Host}{Request.Path}";
+            return $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}";
         }
         }
 
 
         private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)
         private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)

+ 5 - 3
Jellyfin.Api/Controllers/StartupController.cs

@@ -3,6 +3,7 @@ using System.Linq;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Models.StartupDtos;
 using Jellyfin.Api.Models.StartupDtos;
+using Jellyfin.Networking.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Authorization;
@@ -89,9 +90,10 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult SetRemoteAccess([FromBody, Required] StartupRemoteAccessDto startupRemoteAccessDto)
         public ActionResult SetRemoteAccess([FromBody, Required] StartupRemoteAccessDto startupRemoteAccessDto)
         {
         {
-            _config.Configuration.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess;
-            _config.Configuration.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping;
-            _config.SaveConfiguration();
+            NetworkConfiguration settings = _config.GetNetworkConfiguration();
+            settings.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess;
+            settings.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping;
+            _config.SaveConfiguration("network", settings);
             return NoContent();
             return NoContent();
         }
         }
 
 

+ 4 - 4
Jellyfin.Api/Controllers/SystemController.cs

@@ -64,9 +64,9 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("Info")]
         [HttpGet("Info")]
         [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)]
         [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public async Task<ActionResult<SystemInfo>> GetSystemInfo()
+        public ActionResult<SystemInfo> GetSystemInfo()
         {
         {
-            return await _appHost.GetSystemInfo(CancellationToken.None).ConfigureAwait(false);
+            return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -76,9 +76,9 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="PublicSystemInfo"/> with public info about the system.</returns>
         /// <returns>A <see cref="PublicSystemInfo"/> with public info about the system.</returns>
         [HttpGet("Info/Public")]
         [HttpGet("Info/Public")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public async Task<ActionResult<PublicSystemInfo>> GetPublicSystemInfo()
+        public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
         {
         {
-            return await _appHost.GetPublicSystemInfo(CancellationToken.None).ConfigureAwait(false);
+            return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress);
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 71 - 0
Jellyfin.Api/Helpers/ClassMigrationHelper.cs

@@ -0,0 +1,71 @@
+using System;
+using System.Reflection;
+
+namespace Jellyfin.Api.Helpers
+{
+    /// <summary>
+    /// A static class for copying matching properties from one object to another.
+    /// TODO: remove at the point when a fixed migration path has been decided upon.
+    /// </summary>
+    public static class ClassMigrationHelper
+    {
+        /// <summary>
+        /// Extension for 'Object' that copies the properties to a destination object.
+        /// </summary>
+        /// <param name="source">The source.</param>
+        /// <param name="destination">The destination.</param>
+        public static void CopyProperties(this object source, object destination)
+        {
+            // If any this null throw an exception.
+            if (source == null || destination == null)
+            {
+                throw new Exception("Source or/and Destination Objects are null");
+            }
+
+            // Getting the Types of the objects.
+            Type typeDest = destination.GetType();
+            Type typeSrc = source.GetType();
+
+            // Iterate the Properties of the source instance and populate them from their destination counterparts.
+            PropertyInfo[] srcProps = typeSrc.GetProperties();
+            foreach (PropertyInfo srcProp in srcProps)
+            {
+                if (!srcProp.CanRead)
+                {
+                    continue;
+                }
+
+                var targetProperty = typeDest.GetProperty(srcProp.Name);
+                if (targetProperty == null)
+                {
+                    continue;
+                }
+
+                if (!targetProperty.CanWrite)
+                {
+                    continue;
+                }
+
+                var obj = targetProperty.GetSetMethod(true);
+                if (obj != null && obj.IsPrivate)
+                {
+                    continue;
+                }
+
+                var target = targetProperty.GetSetMethod();
+                if (target != null && (target.Attributes & MethodAttributes.Static) != 0)
+                {
+                    continue;
+                }
+
+                if (!targetProperty.PropertyType.IsAssignableFrom(srcProp.PropertyType))
+                {
+                    continue;
+                }
+
+                // Passed all tests, lets set the value.
+                targetProperty.SetValue(destination, srcProp.GetValue(source, null), null);
+            }
+        }
+    }
+}

+ 0 - 234
Jellyfin.Networking/Manager/INetworkManager.cs

@@ -1,234 +0,0 @@
-#nullable enable
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Net;
-using System.Net.NetworkInformation;
-using Jellyfin.Networking.Configuration;
-using MediaBrowser.Common.Net;
-using Microsoft.AspNetCore.Http;
-
-namespace Jellyfin.Networking.Manager
-{
-    /// <summary>
-    /// Interface for the NetworkManager class.
-    /// </summary>
-    public interface INetworkManager
-    {
-        /// <summary>
-        /// Event triggered on network changes.
-        /// </summary>
-        event EventHandler NetworkChanged;
-
-        /// <summary>
-        /// Gets the published server urls list.
-        /// </summary>
-        Dictionary<IPNetAddress, string> PublishedServerUrls { get; }
-
-        /// <summary>
-        /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal.
-        /// </summary>
-        bool TrustAllIP6Interfaces { get; }
-
-        /// <summary>
-        /// Gets the remote address filter.
-        /// </summary>
-        Collection<IPObject> RemoteAddressFilter { get; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether iP6 is enabled.
-        /// </summary>
-        bool IsIP6Enabled { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether iP4 is enabled.
-        /// </summary>
-        bool IsIP4Enabled { get; set; }
-
-        /// <summary>
-        /// Calculates the list of interfaces to use for Kestrel.
-        /// </summary>
-        /// <returns>A Collection{IPObject} object containing all the interfaces to bind.
-        /// If all the interfaces are specified, and none are excluded, it returns zero items
-        /// to represent any address.</returns>
-        /// <param name="individualInterfaces">When false, return <see cref="IPAddress.Any"/> or <see cref="IPAddress.IPv6Any"/> for all interfaces.</param>
-        Collection<IPObject> GetAllBindInterfaces(bool individualInterfaces = false);
-
-        /// <summary>
-        /// Returns a collection containing the loopback interfaces.
-        /// </summary>
-        /// <returns>Collection{IPObject}.</returns>
-        Collection<IPObject> GetLoopbacks();
-
-        /// <summary>
-        /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo)
-        /// If no bind addresses are specified, an internal interface address is selected.
-        /// The priority of selection is as follows:-
-        ///
-        /// The value contained in the startup parameter --published-server-url.
-        ///
-        /// If the user specified custom subnet overrides, the correct subnet for the source address.
-        ///
-        /// If the user specified bind interfaces to use:-
-        ///  The bind interface that contains the source subnet.
-        ///  The first bind interface specified that suits best first the source's endpoint. eg. external or internal.
-        ///
-        /// If the source is from a public subnet address range and the user hasn't specified any bind addresses:-
-        ///  The first public interface that isn't a loopback and contains the source subnet.
-        ///  The first public interface that isn't a loopback. Priority is given to interfaces with gateways.
-        ///  An internal interface if there are no public ip addresses.
-        ///
-        /// If the source is from a private subnet address range and the user hasn't specified any bind addresses:-
-        ///  The first private interface that contains the source subnet.
-        ///  The first private interface that isn't a loopback. Priority is given to interfaces with gateways.
-        ///
-        /// If no interfaces meet any of these criteria, then a loopback address is returned.
-        ///
-        /// Interface that have been specifically excluded from binding are not used in any of the calculations.
-        /// </summary>
-        /// <param name="source">Source of the request.</param>
-        /// <param name="port">Optional port returned, if it's part of an override.</param>
-        /// <returns>IP Address to use, or loopback address if all else fails.</returns>
-        string GetBindInterface(IPObject source, out int? port);
-
-        /// <summary>
-        /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo)
-        /// If no bind addresses are specified, an internal interface address is selected.
-        /// (See <see cref="GetBindInterface(IPObject, out int?)"/>.
-        /// </summary>
-        /// <param name="source">Source of the request.</param>
-        /// <param name="port">Optional port returned, if it's part of an override.</param>
-        /// <returns>IP Address to use, or loopback address if all else fails.</returns>
-        string GetBindInterface(HttpRequest source, out int? port);
-
-        /// <summary>
-        /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo)
-        /// If no bind addresses are specified, an internal interface address is selected.
-        /// (See <see cref="GetBindInterface(IPObject, out int?)"/>.
-        /// </summary>
-        /// <param name="source">IP address of the request.</param>
-        /// <param name="port">Optional port returned, if it's part of an override.</param>
-        /// <returns>IP Address to use, or loopback address if all else fails.</returns>
-        string GetBindInterface(IPAddress source, out int? port);
-
-        /// <summary>
-        /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo)
-        /// If no bind addresses are specified, an internal interface address is selected.
-        /// (See <see cref="GetBindInterface(IPObject, out int?)"/>.
-        /// </summary>
-        /// <param name="source">Source of the request.</param>
-        /// <param name="port">Optional port returned, if it's part of an override.</param>
-        /// <returns>IP Address to use, or loopback address if all else fails.</returns>
-        string GetBindInterface(string source, out int? port);
-
-        /// <summary>
-        /// Checks to see if the ip address is specifically excluded in LocalNetworkAddresses.
-        /// </summary>
-        /// <param name="address">IP address to check.</param>
-        /// <returns>True if it is.</returns>
-        bool IsExcludedInterface(IPAddress address);
-
-        /// <summary>
-        /// Get a list of all the MAC addresses associated with active interfaces.
-        /// </summary>
-        /// <returns>List of MAC addresses.</returns>
-        IReadOnlyCollection<PhysicalAddress> GetMacAddresses();
-
-        /// <summary>
-        /// Checks to see if the IP Address provided matches an interface that has a gateway.
-        /// </summary>
-        /// <param name="addressObj">IP to check. Can be an IPAddress or an IPObject.</param>
-        /// <returns>Result of the check.</returns>
-        bool IsGatewayInterface(IPObject? addressObj);
-
-        /// <summary>
-        /// Checks to see if the IP Address provided matches an interface that has a gateway.
-        /// </summary>
-        /// <param name="addressObj">IP to check. Can be an IPAddress or an IPObject.</param>
-        /// <returns>Result of the check.</returns>
-        bool IsGatewayInterface(IPAddress? addressObj);
-
-        /// <summary>
-        /// Returns true if the address is a private address.
-        /// The config option TrustIP6Interfaces overrides this functions behaviour.
-        /// </summary>
-        /// <param name="address">Address to check.</param>
-        /// <returns>True or False.</returns>
-        bool IsPrivateAddressRange(IPObject address);
-
-        /// <summary>
-        /// Returns true if the address is part of the user defined LAN.
-        /// The config option TrustIP6Interfaces overrides this functions behaviour.
-        /// </summary>
-        /// <param name="address">IP to check.</param>
-        /// <returns>True if endpoint is within the LAN range.</returns>
-        bool IsInLocalNetwork(string address);
-
-        /// <summary>
-        /// Returns true if the address is part of the user defined LAN.
-        /// The config option TrustIP6Interfaces overrides this functions behaviour.
-        /// </summary>
-        /// <param name="address">IP to check.</param>
-        /// <returns>True if endpoint is within the LAN range.</returns>
-        bool IsInLocalNetwork(IPObject address);
-
-        /// <summary>
-        /// Returns true if the address is part of the user defined LAN.
-        /// The config option TrustIP6Interfaces overrides this functions behaviour.
-        /// </summary>
-        /// <param name="address">IP to check.</param>
-        /// <returns>True if endpoint is within the LAN range.</returns>
-        bool IsInLocalNetwork(IPAddress address);
-
-        /// <summary>
-        /// Attempts to convert the token to an IP address, permitting for interface descriptions and indexes.
-        /// eg. "eth1", or "TP-LINK Wireless USB Adapter".
-        /// </summary>
-        /// <param name="token">Token to parse.</param>
-        /// <param name="result">Resultant object's ip addresses, if successful.</param>
-        /// <returns>Success of the operation.</returns>
-        bool TryParseInterface(string token, out Collection<IPObject>? result);
-
-        /// <summary>
-        /// Parses an array of strings into a Collection{IPObject}.
-        /// </summary>
-        /// <param name="values">Values to parse.</param>
-        /// <param name="bracketed">When true, only include values in []. When false, ignore bracketed values.</param>
-        /// <returns>IPCollection object containing the value strings.</returns>
-        Collection<IPObject> CreateIPCollection(string[] values, bool bracketed = false);
-
-        /// <summary>
-        /// Returns all the internal Bind interface addresses.
-        /// </summary>
-        /// <returns>An internal list of interfaces addresses.</returns>
-        Collection<IPObject> GetInternalBindAddresses();
-
-        /// <summary>
-        /// Checks to see if an IP address is still a valid interface address.
-        /// </summary>
-        /// <param name="address">IP address to check.</param>
-        /// <returns>True if it is.</returns>
-        bool IsValidInterfaceAddress(IPAddress address);
-
-        /// <summary>
-        /// Returns true if the IP address is in the excluded list.
-        /// </summary>
-        /// <param name="ip">IP to check.</param>
-        /// <returns>True if excluded.</returns>
-        bool IsExcluded(IPAddress ip);
-
-        /// <summary>
-        /// Returns true if the IP address is in the excluded list.
-        /// </summary>
-        /// <param name="ip">IP to check.</param>
-        /// <returns>True if excluded.</returns>
-        bool IsExcluded(EndPoint ip);
-
-        /// <summary>
-        /// Gets the filtered LAN ip addresses.
-        /// </summary>
-        /// <param name="filter">Optional filter for the list.</param>
-        /// <returns>Returns a filtered list of LAN addresses.</returns>
-        Collection<IPObject> GetFilteredLANSubnets(Collection<IPObject>? filter = null);
-    }
-}

+ 0 - 3
Jellyfin.Server/CoreAppHost.cs

@@ -38,21 +38,18 @@ namespace Jellyfin.Server
         /// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
-        /// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param>
         /// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param>
         /// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param>
         public CoreAppHost(
         public CoreAppHost(
             IServerApplicationPaths applicationPaths,
             IServerApplicationPaths applicationPaths,
             ILoggerFactory loggerFactory,
             ILoggerFactory loggerFactory,
             IStartupOptions options,
             IStartupOptions options,
             IFileSystem fileSystem,
             IFileSystem fileSystem,
-            INetworkManager networkManager,
             IServiceCollection collection)
             IServiceCollection collection)
             : base(
             : base(
                 applicationPaths,
                 applicationPaths,
                 loggerFactory,
                 loggerFactory,
                 options,
                 options,
                 fileSystem,
                 fileSystem,
-                networkManager,
                 collection)
                 collection)
         {
         {
         }
         }

+ 3 - 2
Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs

@@ -1,4 +1,5 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
+using Jellyfin.Networking.Configuration;
 using Jellyfin.Server.Middleware;
 using Jellyfin.Server.Middleware;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Builder;
@@ -24,8 +25,8 @@ namespace Jellyfin.Server.Extensions
             // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
             // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
             // specifying the Swagger JSON endpoint.
             // specifying the Swagger JSON endpoint.
 
 
-            var baseUrl = serverConfigurationManager.Configuration.BaseUrl.Trim('/');
-            var apiDocBaseUrl = serverConfigurationManager.Configuration.BaseUrl;
+            var baseUrl = serverConfigurationManager.GetNetworkConfiguration().BaseUrl.Trim('/');
+            var apiDocBaseUrl = serverConfigurationManager.GetNetworkConfiguration().BaseUrl;
             if (!string.IsNullOrEmpty(baseUrl))
             if (!string.IsNullOrEmpty(baseUrl))
             {
             {
                 baseUrl += '/';
                 baseUrl += '/';

+ 2 - 1
Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Jellyfin.Networking.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Configuration;
@@ -42,7 +43,7 @@ namespace Jellyfin.Server.Middleware
         public async Task Invoke(HttpContext httpContext, IServerConfigurationManager serverConfigurationManager)
         public async Task Invoke(HttpContext httpContext, IServerConfigurationManager serverConfigurationManager)
         {
         {
             var localPath = httpContext.Request.Path.ToString();
             var localPath = httpContext.Request.Path.ToString();
-            var baseUrlPrefix = serverConfigurationManager.Configuration.BaseUrl;
+            var baseUrlPrefix = serverConfigurationManager.GetNetworkConfiguration().BaseUrl;
 
 
             if (string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
             if (string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
                 || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
                 || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase)

+ 21 - 20
Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs

@@ -1,5 +1,6 @@
-using System.Linq;
+using System.Net;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Jellyfin.Networking.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
@@ -34,40 +35,40 @@ namespace Jellyfin.Server.Middleware
         {
         {
             if (httpContext.IsLocal())
             if (httpContext.IsLocal())
             {
             {
+                // Running locally.
                 await _next(httpContext).ConfigureAwait(false);
                 await _next(httpContext).ConfigureAwait(false);
                 return;
                 return;
             }
             }
 
 
-            var remoteIp = httpContext.GetNormalizedRemoteIp();
+            var remoteIp = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback;
 
 
-            if (serverConfigurationManager.Configuration.EnableRemoteAccess)
+            if (serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess)
             {
             {
-                var addressFilter = serverConfigurationManager.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
+                // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
+                // If left blank, all remote addresses will be allowed.
+                var remoteAddressFilter = networkManager.RemoteAddressFilter;
 
 
-                if (addressFilter.Length > 0 && !networkManager.IsInLocalNetwork(remoteIp))
+                if (remoteAddressFilter.Count > 0 && !networkManager.IsInLocalNetwork(remoteIp))
                 {
                 {
-                    if (serverConfigurationManager.Configuration.IsRemoteIPFilterBlacklist)
+                    // remoteAddressFilter is a whitelist or blacklist.
+                    bool isListed = remoteAddressFilter.ContainsAddress(remoteIp);
+                    if (!serverConfigurationManager.GetNetworkConfiguration().IsRemoteIPFilterBlacklist)
                     {
                     {
-                        if (networkManager.IsAddressInSubnets(remoteIp, addressFilter))
-                        {
-                            return;
-                        }
+                        // Black list, so flip over.
+                        isListed = !isListed;
                     }
                     }
-                    else
+
+                    if (!isListed)
                     {
                     {
-                        if (!networkManager.IsAddressInSubnets(remoteIp, addressFilter))
-                        {
-                            return;
-                        }
+                        // If your name isn't on the list, you arn't coming in.
+                        return;
                     }
                     }
                 }
                 }
             }
             }
-            else
+            else if (!networkManager.IsInLocalNetwork(remoteIp))
             {
             {
-                if (!networkManager.IsInLocalNetwork(remoteIp))
-                {
-                    return;
-                }
+                // Remote not enabled. So everyone should be LAN.
+                return;
             }
             }
 
 
             await _next(httpContext).ConfigureAwait(false);
             await _next(httpContext).ConfigureAwait(false);

+ 5 - 33
Jellyfin.Server/Middleware/LanFilteringMiddleware.cs

@@ -1,6 +1,9 @@
 using System;
 using System;
 using System.Linq;
 using System.Linq;
+using System.Net;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Jellyfin.Networking.Configuration;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
@@ -32,45 +35,14 @@ namespace Jellyfin.Server.Middleware
         /// <returns>The async task.</returns>
         /// <returns>The async task.</returns>
         public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager)
         public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager)
         {
         {
-            var currentHost = httpContext.Request.Host.ToString();
-            var hosts = serverConfigurationManager
-                .Configuration
-                .LocalNetworkAddresses
-                .Select(NormalizeConfiguredLocalAddress)
-                .ToList();
+            var host = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback;
 
 
-            if (hosts.Count == 0)
+            if (!networkManager.IsInLocalNetwork(host) && !serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess)
             {
             {
-                await _next(httpContext).ConfigureAwait(false);
                 return;
                 return;
             }
             }
 
 
-            currentHost ??= string.Empty;
-
-            if (networkManager.IsInPrivateAddressSpace(currentHost))
-            {
-                hosts.Add("localhost");
-                hosts.Add("127.0.0.1");
-
-                if (hosts.All(i => currentHost.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1))
-                {
-                    return;
-                }
-            }
-
             await _next(httpContext).ConfigureAwait(false);
             await _next(httpContext).ConfigureAwait(false);
         }
         }
-
-        private static string NormalizeConfiguredLocalAddress(string address)
-        {
-            var add = address.AsSpan().Trim('/');
-            int index = add.IndexOf('/');
-            if (index != -1)
-            {
-                add = add.Slice(index + 1);
-            }
-
-            return add.TrimStart('/').ToString();
-        }
     }
     }
 }
 }

+ 18 - 48
Jellyfin.Server/Program.cs

@@ -12,9 +12,9 @@ using System.Threading.Tasks;
 using CommandLine;
 using CommandLine;
 using Emby.Server.Implementations;
 using Emby.Server.Implementations;
 using Emby.Server.Implementations.IO;
 using Emby.Server.Implementations.IO;
-using Emby.Server.Implementations.Networking;
 using Jellyfin.Api.Controllers;
 using Jellyfin.Api.Controllers;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Extensions;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Server.Kestrel.Core;
 using Microsoft.AspNetCore.Server.Kestrel.Core;
@@ -161,7 +161,6 @@ namespace Jellyfin.Server
                 _loggerFactory,
                 _loggerFactory,
                 options,
                 options,
                 new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
                 new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
-                new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()),
                 serviceCollection);
                 serviceCollection);
 
 
             try
             try
@@ -272,53 +271,17 @@ namespace Jellyfin.Server
             return builder
             return builder
                 .UseKestrel((builderContext, options) =>
                 .UseKestrel((builderContext, options) =>
                 {
                 {
-                    var addresses = appHost.ServerConfigurationManager
-                        .Configuration
-                        .LocalNetworkAddresses
-                        .Select(x => appHost.NormalizeConfiguredLocalAddress(x))
-                        .Where(i => i != null)
-                        .ToHashSet();
-                    if (addresses.Count > 0 && !addresses.Contains(IPAddress.Any))
-                    {
-                        if (!addresses.Contains(IPAddress.Loopback))
-                        {
-                            // we must listen on loopback for LiveTV to function regardless of the settings
-                            addresses.Add(IPAddress.Loopback);
-                        }
+                    var addresses = appHost.NetManager.GetAllBindInterfaces();
 
 
-                        foreach (var address in addresses)
-                        {
-                            _logger.LogInformation("Kestrel listening on {IpAddress}", address);
-                            options.Listen(address, appHost.HttpPort);
-
-                            if (appHost.ListenWithHttps)
-                            {
-                                options.Listen(
-                                    address,
-                                    appHost.HttpsPort,
-                                    listenOptions => listenOptions.UseHttps(appHost.Certificate));
-                            }
-                            else if (builderContext.HostingEnvironment.IsDevelopment())
-                            {
-                                try
-                                {
-                                    options.Listen(address, appHost.HttpsPort, listenOptions => listenOptions.UseHttps());
-                                }
-                                catch (InvalidOperationException ex)
-                                {
-                                    _logger.LogError(ex, "Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted.");
-                                }
-                            }
-                        }
-                    }
-                    else
+                    bool flagged = false;
+                    foreach (IPObject netAdd in addresses)
                     {
                     {
-                        _logger.LogInformation("Kestrel listening on all interfaces");
-                        options.ListenAnyIP(appHost.HttpPort);
-
+                        _logger.LogInformation("Kestrel listening on {0}", netAdd);
+                        options.Listen(netAdd.Address, appHost.HttpPort);
                         if (appHost.ListenWithHttps)
                         if (appHost.ListenWithHttps)
                         {
                         {
-                            options.ListenAnyIP(
+                            options.Listen(
+                                netAdd.Address,
                                 appHost.HttpsPort,
                                 appHost.HttpsPort,
                                 listenOptions => listenOptions.UseHttps(appHost.Certificate));
                                 listenOptions => listenOptions.UseHttps(appHost.Certificate));
                         }
                         }
@@ -326,11 +289,18 @@ namespace Jellyfin.Server
                         {
                         {
                             try
                             try
                             {
                             {
-                                options.ListenAnyIP(appHost.HttpsPort, listenOptions => listenOptions.UseHttps());
+                                options.Listen(
+                                    netAdd.Address,
+                                    appHost.HttpsPort,
+                                    listenOptions => listenOptions.UseHttps());
                             }
                             }
-                            catch (InvalidOperationException ex)
+                            catch (InvalidOperationException)
                             {
                             {
-                                _logger.LogError(ex, "Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted.");
+                                if (!flagged)
+                                {
+                                    _logger.LogWarning("Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted.");
+                                    flagged = true;
+                                }
                             }
                             }
                         }
                         }
                     }
                     }

+ 3 - 2
Jellyfin.Server/Startup.cs

@@ -1,5 +1,6 @@
 using System.Net.Http.Headers;
 using System.Net.Http.Headers;
 using System.Net.Mime;
 using System.Net.Mime;
+using Jellyfin.Networking.Configuration;
 using Jellyfin.Server.Extensions;
 using Jellyfin.Server.Extensions;
 using Jellyfin.Server.Implementations;
 using Jellyfin.Server.Implementations;
 using Jellyfin.Server.Middleware;
 using Jellyfin.Server.Middleware;
@@ -51,7 +52,7 @@ namespace Jellyfin.Server
             {
             {
                 options.HttpsPort = _serverApplicationHost.HttpsPort;
                 options.HttpsPort = _serverApplicationHost.HttpsPort;
             });
             });
-            services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.Configuration.KnownProxies);
+            services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration().KnownProxies);
 
 
             services.AddJellyfinApiSwagger();
             services.AddJellyfinApiSwagger();
 
 
@@ -103,7 +104,7 @@ namespace Jellyfin.Server
             app.UseBaseUrlRedirection();
             app.UseBaseUrlRedirection();
 
 
             // Wrap rest of configuration so everything only listens on BaseUrl.
             // Wrap rest of configuration so everything only listens on BaseUrl.
-            app.Map(_serverConfigurationManager.Configuration.BaseUrl, mainApp =>
+            app.Map(_serverConfigurationManager.GetNetworkConfiguration().BaseUrl, mainApp =>
             {
             {
                 if (env.IsDevelopment())
                 if (env.IsDevelopment())
                 {
                 {

+ 185 - 49
MediaBrowser.Common/Net/INetworkManager.cs

@@ -1,97 +1,233 @@
-#pragma warning disable CS1591
-
+#nullable enable
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.Net;
 using System.Net;
 using System.Net.NetworkInformation;
 using System.Net.NetworkInformation;
+using MediaBrowser.Common.Net;
+using Microsoft.AspNetCore.Http;
 
 
 namespace MediaBrowser.Common.Net
 namespace MediaBrowser.Common.Net
 {
 {
+    /// <summary>
+    /// Interface for the NetworkManager class.
+    /// </summary>
     public interface INetworkManager
     public interface INetworkManager
     {
     {
+        /// <summary>
+        /// Event triggered on network changes.
+        /// </summary>
         event EventHandler NetworkChanged;
         event EventHandler NetworkChanged;
 
 
         /// <summary>
         /// <summary>
-        /// Gets or sets a function to return the list of user defined LAN addresses.
+        /// Gets the published server urls list.
+        /// </summary>
+        Dictionary<IPNetAddress, string> PublishedServerUrls { get; }
+
+        /// <summary>
+        /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal.
+        /// </summary>
+        bool TrustAllIP6Interfaces { get; }
+
+        /// <summary>
+        /// Gets the remote address filter.
+        /// </summary>
+        Collection<IPObject> RemoteAddressFilter { get; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether iP6 is enabled.
+        /// </summary>
+        bool IsIP6Enabled { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether iP4 is enabled.
+        /// </summary>
+        bool IsIP4Enabled { get; set; }
+
+        /// <summary>
+        /// Calculates the list of interfaces to use for Kestrel.
+        /// </summary>
+        /// <returns>A Collection{IPObject} object containing all the interfaces to bind.
+        /// If all the interfaces are specified, and none are excluded, it returns zero items
+        /// to represent any address.</returns>
+        /// <param name="individualInterfaces">When false, return <see cref="IPAddress.Any"/> or <see cref="IPAddress.IPv6Any"/> for all interfaces.</param>
+        Collection<IPObject> GetAllBindInterfaces(bool individualInterfaces = false);
+
+        /// <summary>
+        /// Returns a collection containing the loopback interfaces.
+        /// </summary>
+        /// <returns>Collection{IPObject}.</returns>
+        Collection<IPObject> GetLoopbacks();
+
+        /// <summary>
+        /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo)
+        /// If no bind addresses are specified, an internal interface address is selected.
+        /// The priority of selection is as follows:-
+        ///
+        /// The value contained in the startup parameter --published-server-url.
+        ///
+        /// If the user specified custom subnet overrides, the correct subnet for the source address.
+        ///
+        /// If the user specified bind interfaces to use:-
+        ///  The bind interface that contains the source subnet.
+        ///  The first bind interface specified that suits best first the source's endpoint. eg. external or internal.
+        ///
+        /// If the source is from a public subnet address range and the user hasn't specified any bind addresses:-
+        ///  The first public interface that isn't a loopback and contains the source subnet.
+        ///  The first public interface that isn't a loopback. Priority is given to interfaces with gateways.
+        ///  An internal interface if there are no public ip addresses.
+        ///
+        /// If the source is from a private subnet address range and the user hasn't specified any bind addresses:-
+        ///  The first private interface that contains the source subnet.
+        ///  The first private interface that isn't a loopback. Priority is given to interfaces with gateways.
+        ///
+        /// If no interfaces meet any of these criteria, then a loopback address is returned.
+        ///
+        /// Interface that have been specifically excluded from binding are not used in any of the calculations.
+        /// </summary>
+        /// <param name="source">Source of the request.</param>
+        /// <param name="port">Optional port returned, if it's part of an override.</param>
+        /// <returns>IP Address to use, or loopback address if all else fails.</returns>
+        string GetBindInterface(IPObject source, out int? port);
+
+        /// <summary>
+        /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo)
+        /// If no bind addresses are specified, an internal interface address is selected.
+        /// (See <see cref="GetBindInterface(IPObject, out int?)"/>.
+        /// </summary>
+        /// <param name="source">Source of the request.</param>
+        /// <param name="port">Optional port returned, if it's part of an override.</param>
+        /// <returns>IP Address to use, or loopback address if all else fails.</returns>
+        string GetBindInterface(HttpRequest source, out int? port);
+
+        /// <summary>
+        /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo)
+        /// If no bind addresses are specified, an internal interface address is selected.
+        /// (See <see cref="GetBindInterface(IPObject, out int?)"/>.
+        /// </summary>
+        /// <param name="source">IP address of the request.</param>
+        /// <param name="port">Optional port returned, if it's part of an override.</param>
+        /// <returns>IP Address to use, or loopback address if all else fails.</returns>
+        string GetBindInterface(IPAddress source, out int? port);
+
+        /// <summary>
+        /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo)
+        /// If no bind addresses are specified, an internal interface address is selected.
+        /// (See <see cref="GetBindInterface(IPObject, out int?)"/>.
+        /// </summary>
+        /// <param name="source">Source of the request.</param>
+        /// <param name="port">Optional port returned, if it's part of an override.</param>
+        /// <returns>IP Address to use, or loopback address if all else fails.</returns>
+        string GetBindInterface(string source, out int? port);
+
+        /// <summary>
+        /// Checks to see if the ip address is specifically excluded in LocalNetworkAddresses.
+        /// </summary>
+        /// <param name="address">IP address to check.</param>
+        /// <returns>True if it is.</returns>
+        bool IsExcludedInterface(IPAddress address);
+
+        /// <summary>
+        /// Get a list of all the MAC addresses associated with active interfaces.
+        /// </summary>
+        /// <returns>List of MAC addresses.</returns>
+        IReadOnlyCollection<PhysicalAddress> GetMacAddresses();
+
+        /// <summary>
+        /// Checks to see if the IP Address provided matches an interface that has a gateway.
+        /// </summary>
+        /// <param name="addressObj">IP to check. Can be an IPAddress or an IPObject.</param>
+        /// <returns>Result of the check.</returns>
+        bool IsGatewayInterface(IPObject? addressObj);
+
+        /// <summary>
+        /// Checks to see if the IP Address provided matches an interface that has a gateway.
         /// </summary>
         /// </summary>
-        Func<string[]> LocalSubnetsFn { get; set; }
+        /// <param name="addressObj">IP to check. Can be an IPAddress or an IPObject.</param>
+        /// <returns>Result of the check.</returns>
+        bool IsGatewayInterface(IPAddress? addressObj);
 
 
         /// <summary>
         /// <summary>
-        /// Gets a random port TCP number that is currently available.
+        /// Returns true if the address is a private address.
+        /// The config option TrustIP6Interfaces overrides this functions behaviour.
         /// </summary>
         /// </summary>
-        /// <returns>System.Int32.</returns>
-        int GetRandomUnusedTcpPort();
+        /// <param name="address">Address to check.</param>
+        /// <returns>True or False.</returns>
+        bool IsPrivateAddressRange(IPObject address);
 
 
         /// <summary>
         /// <summary>
-        /// Gets a random port UDP number that is currently available.
+        /// Returns true if the address is part of the user defined LAN.
+        /// The config option TrustIP6Interfaces overrides this functions behaviour.
         /// </summary>
         /// </summary>
-        /// <returns>System.Int32.</returns>
-        int GetRandomUnusedUdpPort();
+        /// <param name="address">IP to check.</param>
+        /// <returns>True if endpoint is within the LAN range.</returns>
+        bool IsInLocalNetwork(string address);
 
 
         /// <summary>
         /// <summary>
-        /// Returns the MAC Address from first Network Card in Computer.
+        /// Returns true if the address is part of the user defined LAN.
+        /// The config option TrustIP6Interfaces overrides this functions behaviour.
         /// </summary>
         /// </summary>
-        /// <returns>The MAC Address.</returns>
-        List<PhysicalAddress> GetMacAddresses();
+        /// <param name="address">IP to check.</param>
+        /// <returns>True if endpoint is within the LAN range.</returns>
+        bool IsInLocalNetwork(IPObject address);
 
 
         /// <summary>
         /// <summary>
-        /// Determines whether [is in private address space] [the specified endpoint].
+        /// Returns true if the address is part of the user defined LAN.
+        /// The config option TrustIP6Interfaces overrides this functions behaviour.
         /// </summary>
         /// </summary>
-        /// <param name="endpoint">The endpoint.</param>
-        /// <returns><c>true</c> if [is in private address space] [the specified endpoint]; otherwise, <c>false</c>.</returns>
-        bool IsInPrivateAddressSpace(string endpoint);
+        /// <param name="address">IP to check.</param>
+        /// <returns>True if endpoint is within the LAN range.</returns>
+        bool IsInLocalNetwork(IPAddress address);
 
 
         /// <summary>
         /// <summary>
-        /// Determines whether [is in private address space 10.x.x.x] [the specified endpoint] and exists in the subnets returned by GetSubnets().
+        /// Attempts to convert the token to an IP address, permitting for interface descriptions and indexes.
+        /// eg. "eth1", or "TP-LINK Wireless USB Adapter".
         /// </summary>
         /// </summary>
-        /// <param name="endpoint">The endpoint.</param>
-        /// <returns><c>true</c> if [is in private address space 10.x.x.x] [the specified endpoint]; otherwise, <c>false</c>.</returns>
-        bool IsInPrivateAddressSpaceAndLocalSubnet(string endpoint);
+        /// <param name="token">Token to parse.</param>
+        /// <param name="result">Resultant object's ip addresses, if successful.</param>
+        /// <returns>Success of the operation.</returns>
+        bool TryParseInterface(string token, out Collection<IPObject>? result);
 
 
         /// <summary>
         /// <summary>
-        /// Determines whether [is in local network] [the specified endpoint].
+        /// Parses an array of strings into a Collection{IPObject}.
         /// </summary>
         /// </summary>
-        /// <param name="endpoint">The endpoint.</param>
-        /// <returns><c>true</c> if [is in local network] [the specified endpoint]; otherwise, <c>false</c>.</returns>
-        bool IsInLocalNetwork(string endpoint);
+        /// <param name="values">Values to parse.</param>
+        /// <param name="bracketed">When true, only include values in []. When false, ignore bracketed values.</param>
+        /// <returns>IPCollection object containing the value strings.</returns>
+        Collection<IPObject> CreateIPCollection(string[] values, bool bracketed = false);
 
 
         /// <summary>
         /// <summary>
-        /// Investigates an caches a list of interface addresses, excluding local link and LAN excluded addresses.
+        /// Returns all the internal Bind interface addresses.
         /// </summary>
         /// </summary>
-        /// <returns>The list of ip addresses.</returns>
-        IPAddress[] GetLocalIpAddresses();
+        /// <returns>An internal list of interfaces addresses.</returns>
+        Collection<IPObject> GetInternalBindAddresses();
 
 
         /// <summary>
         /// <summary>
-        /// Checks if the given address falls within the ranges given in [subnets]. The addresses in subnets can be hosts or subnets in the CIDR format.
+        /// Checks to see if an IP address is still a valid interface address.
         /// </summary>
         /// </summary>
-        /// <param name="addressString">The address to check.</param>
-        /// <param name="subnets">If true, check against addresses in the LAN settings surrounded by brackets ([]).</param>
-        /// <returns><c>true</c>if the address is in at least one of the given subnets, <c>false</c> otherwise.</returns>
-        bool IsAddressInSubnets(string addressString, string[] subnets);
+        /// <param name="address">IP address to check.</param>
+        /// <returns>True if it is.</returns>
+        bool IsValidInterfaceAddress(IPAddress address);
 
 
         /// <summary>
         /// <summary>
-        /// Returns true if address is in the LAN list in the config file.
+        /// Returns true if the IP address is in the excluded list.
         /// </summary>
         /// </summary>
-        /// <param name="address">The address to check.</param>
-        /// <param name="excludeInterfaces">If true, check against addresses in the LAN settings which have [] around and return true if it matches the address give in address.</param>
-        /// <param name="excludeRFC">If true, returns false if address is in the 127.x.x.x or 169.128.x.x range.</param>
-        /// <returns><c>false</c>if the address isn't in the LAN list, <c>true</c> if the address has been defined as a LAN address.</returns>
-        bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC);
+        /// <param name="ip">IP to check.</param>
+        /// <returns>True if excluded.</returns>
+        bool IsExcluded(IPAddress ip);
 
 
         /// <summary>
         /// <summary>
-        /// Checks if address is in the LAN list in the config file.
+        /// Returns true if the IP address is in the excluded list.
         /// </summary>
         /// </summary>
-        /// <param name="address1">Source address to check.</param>
-        /// <param name="address2">Destination address to check against.</param>
-        /// <param name="subnetMask">Destination subnet to check against.</param>
-        /// <returns><c>true/false</c>depending on whether address1 is in the same subnet as IPAddress2 with subnetMask.</returns>
-        bool IsInSameSubnet(IPAddress address1, IPAddress address2, IPAddress subnetMask);
+        /// <param name="ip">IP to check.</param>
+        /// <returns>True if excluded.</returns>
+        bool IsExcluded(EndPoint ip);
 
 
         /// <summary>
         /// <summary>
-        /// Returns the subnet mask of an interface with the given address.
+        /// Gets the filtered LAN ip addresses.
         /// </summary>
         /// </summary>
-        /// <param name="address">The address to check.</param>
-        /// <returns>Returns the subnet mask of an interface with the given address, or null if an interface match cannot be found.</returns>
-        IPAddress GetLocalIpSubnetMask(IPAddress address);
+        /// <param name="filter">Optional filter for the list.</param>
+        /// <returns>Returns a filtered list of LAN addresses.</returns>
+        Collection<IPObject> GetFilteredLANSubnets(Collection<IPObject>? filter = null);
     }
     }
 }
 }

+ 23 - 21
MediaBrowser.Controller/IServerApplicationHost.cs

@@ -8,6 +8,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Model.System;
 using MediaBrowser.Model.System;
+using Microsoft.AspNetCore.Http;
 
 
 namespace MediaBrowser.Controller
 namespace MediaBrowser.Controller
 {
 {
@@ -56,41 +57,42 @@ namespace MediaBrowser.Controller
         /// <summary>
         /// <summary>
         /// Gets the system info.
         /// Gets the system info.
         /// </summary>
         /// </summary>
-        /// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
+        /// <param name="source">The originator of the request.</param>
         /// <returns>SystemInfo.</returns>
         /// <returns>SystemInfo.</returns>
-        Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken = default);
+        SystemInfo GetSystemInfo(IPAddress source);
 
 
-        Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken = default);
+        PublicSystemInfo GetPublicSystemInfo(IPAddress address);
 
 
         /// <summary>
         /// <summary>
-        /// Gets all the local IP addresses of this API instance. Each address is validated by sending a 'ping' request
-        /// to the API that should exist at the address.
+        /// Gets a URL specific for the request.
         /// </summary>
         /// </summary>
-        /// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
-        /// <returns>A list containing all the local IP addresses of the server.</returns>
-        Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken = default);
+        /// <param name="request">The <see cref="HttpRequest"/> instance.</param>
+        /// <param name="port">Optional port number.</param>
+        /// <returns>An accessible URL.</returns>
+        string GetSmartApiUrl(HttpRequest request, int? port = null);
 
 
         /// <summary>
         /// <summary>
-        /// Gets a local (LAN) URL that can be used to access the API. The hostname used is the first valid configured
-        /// IP address that can be found via <see cref="GetLocalIpAddresses"/>. HTTPS will be preferred when available.
+        /// Gets a URL specific for the request.
         /// </summary>
         /// </summary>
-        /// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
-        /// <returns>The server URL.</returns>
-        Task<string> GetLocalApiUrl(CancellationToken cancellationToken = default);
+        /// <param name="remoteAddr">The remote <see cref="IPAddress"/> of the connection.</param>
+        /// <param name="port">Optional port number.</param>
+        /// <returns>An accessible URL.</returns>
+        string GetSmartApiUrl(IPAddress remoteAddr, int? port = null);
 
 
         /// <summary>
         /// <summary>
-        /// Gets a localhost URL that can be used to access the API using the loop-back IP address (127.0.0.1)
-        /// over HTTP (not HTTPS).
+        /// Gets a URL specific for the request.
         /// </summary>
         /// </summary>
-        /// <returns>The API URL.</returns>
-        string GetLoopbackHttpApiUrl();
+        /// <param name="hostname">The hostname used in the connection.</param>
+        /// <param name="port">Optional port number.</param>
+        /// <returns>An accessible URL.</returns>
+        string GetSmartApiUrl(string hostname, int? port = null);
 
 
         /// <summary>
         /// <summary>
-        /// Gets a local (LAN) URL that can be used to access the API. HTTPS will be preferred when available.
+        /// Gets a localhost URL that can be used to access the API using the loop-back IP address.
+        /// over HTTP (not HTTPS).
         /// </summary>
         /// </summary>
-        /// <param name="address">The IP address to use as the hostname in the URL.</param>
         /// <returns>The API URL.</returns>
         /// <returns>The API URL.</returns>
-        string GetLocalApiUrl(IPAddress address);
+        string GetLoopbackHttpApiUrl();
 
 
         /// <summary>
         /// <summary>
         /// Gets a local (LAN) URL that can be used to access the API.
         /// Gets a local (LAN) URL that can be used to access the API.
@@ -106,7 +108,7 @@ namespace MediaBrowser.Controller
         /// preferring the HTTPS port, if available.
         /// preferring the HTTPS port, if available.
         /// </param>
         /// </param>
         /// <returns>The API URL.</returns>
         /// <returns>The API URL.</returns>
-        string GetLocalApiUrl(ReadOnlySpan<char> hostname, string scheme = null, int? port = null);
+        string GetLocalApiUrl(string hostname, string scheme = null, int? port = null);
 
 
         /// <summary>
         /// <summary>
         /// Open a URL in an external browser window.
         /// Open a URL in an external browser window.

+ 1 - 1
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -233,7 +233,7 @@ namespace MediaBrowser.Model.Configuration
         /// Gets or sets a value indicating whether quick connect is available for use on this server.
         /// Gets or sets a value indicating whether quick connect is available for use on this server.
         /// </summary>
         /// </summary>
         public bool QuickConnectAvailable { get; set; } = false;
         public bool QuickConnectAvailable { get; set; } = false;
-       
+
         /// <summary>
         /// <summary>
         /// Gets or sets a value indicating whether access outside of the LAN is permitted.
         /// Gets or sets a value indicating whether access outside of the LAN is permitted.
         /// </summary>
         /// </summary>

+ 7 - 0
MediaBrowser.sln

@@ -68,6 +68,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementat
 EndProject
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}"
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}"
 EndProject
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking.Tests", "tests\Jellyfin.Networking.Tests\NetworkTesting\Jellyfin.Networking.Tests.csproj", "{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}"
+EndProject
 Global
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
 		Debug|Any CPU = Debug|Any CPU
@@ -182,6 +184,10 @@ Global
 		{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.Build.0 = Release|Any CPU
 		{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.Build.0 = Release|Any CPU
+		{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
 		HideSolutionNode = FALSE
@@ -193,6 +199,7 @@ Global
 		{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+		{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}
 		SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}

+ 1 - 0
RSSDP/RSSDP.csproj

@@ -6,6 +6,7 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
+    <ProjectReference Include="..\Jellyfin.Networking\Jellyfin.Networking.csproj" />
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
   </ItemGroup>
   </ItemGroup>
 
 

+ 2 - 2
RSSDP/SsdpCommunicationsServer.cs

@@ -352,7 +352,7 @@ namespace Rssdp.Infrastructure
 
 
             if (_enableMultiSocketBinding)
             if (_enableMultiSocketBinding)
             {
             {
-                foreach (var address in _networkManager.GetLocalIpAddresses())
+                foreach (var address in _networkManager.GetInternalBindAddresses())
                 {
                 {
                     if (address.AddressFamily == AddressFamily.InterNetworkV6)
                     if (address.AddressFamily == AddressFamily.InterNetworkV6)
                     {
                     {
@@ -362,7 +362,7 @@ namespace Rssdp.Infrastructure
 
 
                     try
                     try
                     {
                     {
-                        sockets.Add(_SocketFactory.CreateSsdpUdpSocket(address, _LocalPort));
+                        sockets.Add(_SocketFactory.CreateSsdpUdpSocket(address.Address, _LocalPort));
                     }
                     }
                     catch (Exception ex)
                     catch (Exception ex)
                     {
                     {

+ 4 - 6
RSSDP/SsdpDevicePublisher.cs

@@ -300,17 +300,15 @@ namespace Rssdp.Infrastructure
 
 
                     foreach (var device in deviceList)
                     foreach (var device in deviceList)
                     {
                     {
-                        if (!_sendOnlyMatchedHost ||
-                            _networkManager.IsInSameSubnet(device.ToRootDevice().Address, remoteEndPoint.Address, device.ToRootDevice().SubnetMask))
+                        var root = device.ToRootDevice();
+                        var source = new IPNetAddress(root.Address, root.PrefixLength);
+                        var destination = new IPNetAddress(remoteEndPoint.Address, root.PrefixLength);
+                        if (!_sendOnlyMatchedHost || source.NetworkAddress.Equals(destination.NetworkAddress))
                         {
                         {
                             SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken);
                             SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken);
                         }
                         }
                     }
                     }
                 }
                 }
-                else
-                {
-                    // WriteTrace(String.Format("Sending 0 search responses."));
-                }
             });
             });
         }
         }
 
 

+ 2 - 2
RSSDP/SsdpRootDevice.cs

@@ -45,9 +45,9 @@ namespace Rssdp
         public IPAddress Address { get; set; }
         public IPAddress Address { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets or sets the SubnetMask used to check if the received message from same interface with this device/tree. Required.
+        /// Gets or sets the prefix length used to check if the received message from same interface with this device/tree. Required.
         /// </summary>
         /// </summary>
-        public IPAddress SubnetMask { get; set; }
+        public byte PrefixLength { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// The base URL to use for all relative url's provided in other properties (and those of child devices). Optional.
         /// The base URL to use for all relative url's provided in other properties (and those of child devices). Optional.

+ 0 - 3
tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs

@@ -3,8 +3,6 @@ using System.Collections.Concurrent;
 using System.IO;
 using System.IO;
 using Emby.Server.Implementations;
 using Emby.Server.Implementations;
 using Emby.Server.Implementations.IO;
 using Emby.Server.Implementations.IO;
-using Emby.Server.Implementations.Networking;
-using Jellyfin.Drawing.Skia;
 using Jellyfin.Server;
 using Jellyfin.Server;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Hosting;
@@ -80,7 +78,6 @@ namespace Jellyfin.Api.Tests
                 loggerFactory,
                 loggerFactory,
                 commandLineOpts,
                 commandLineOpts,
                 new ManagedFileSystem(loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
                 new ManagedFileSystem(loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
-                new NetworkManager(loggerFactory.CreateLogger<NetworkManager>()),
                 serviceCollection);
                 serviceCollection);
             _disposableComponents.Add(appHost);
             _disposableComponents.Add(appHost);
             appHost.Init();
             appHost.Init();

+ 39 - 0
tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj

@@ -0,0 +1,39 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
+  <PropertyGroup>
+    <ProjectGuid>{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}</ProjectGuid>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <TargetFramework>net5.0</TargetFramework>
+    <IsPackable>false</IsPackable>
+    <Nullable>enable</Nullable>
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
+    <PackageReference Include="xunit" Version="2.4.1" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
+    <PackageReference Include="coverlet.collector" Version="1.3.0" />
+    <PackageReference Include="Moq" Version="4.14.5" />
+  </ItemGroup>
+
+  <!-- Code Analyzers-->
+  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
+    <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
+    <ProjectReference Include="..\..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
+  </ItemGroup>
+  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <CodeAnalysisRuleSet>../../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+    <DefineConstants>DEBUG</DefineConstants>
+  </PropertyGroup>
+</Project>

+ 517 - 0
tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs

@@ -0,0 +1,517 @@
+using System;
+using System.Net;
+using Jellyfin.Networking.Configuration;
+using Jellyfin.Networking.Manager;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using Moq;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+using System.Collections.ObjectModel;
+
+namespace Jellyfin.Networking.Tests
+{
+    public class NetworkParseTests
+    {
+        /// <summary>
+        /// Tries to identify the string and return an object of that class.
+        /// </summary>
+        /// <param name="addr">String to parse.</param>
+        /// <param name="result">IPObject to return.</param>
+        /// <returns>True if the value parsed successfully.</returns>
+        private static bool TryParse(string addr, out IPObject result)
+        {
+            if (!string.IsNullOrEmpty(addr))
+            {
+                // Is it an IP address
+                if (IPNetAddress.TryParse(addr, out IPNetAddress nw))
+                {
+                    result = nw;
+                    return true;
+                }
+
+                if (IPHost.TryParse(addr, out IPHost h))
+                {
+                    result = h;
+                    return true;
+                }
+            }
+
+            result = IPNetAddress.None;
+            return false;
+        }
+
+        private static IConfigurationManager GetMockConfig(NetworkConfiguration conf)
+        {
+            var configManager = new Mock<IConfigurationManager>
+            {
+                CallBase = true
+            };
+            configManager.Setup(x => x.GetConfiguration(It.IsAny<string>())).Returns(conf);
+            return (IConfigurationManager)configManager.Object;
+        }
+
+        /// <summary>
+        /// Checks the ability to ignore interfaces
+        /// </summary>
+        /// <param name="interfaces">Mock network setup, in the format (IP address, interface index, interface name) : .... </param>
+        /// <param name="lan">LAN addresses.</param>
+        /// <param name="value">Bind addresses that are excluded.</param>
+        [Theory]
+        [InlineData("192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.0/24", "[192.168.1.208/24,200.200.200.200/24]")]
+        [InlineData("192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")]
+        [InlineData("192.168.1.208/24,-16,vEthernet1:192.168.1.208/24,-16,vEthernet212;200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")]
+        public void IgnoreVirtualInterfaces(string interfaces, string lan, string value)
+        {
+            var conf = new NetworkConfiguration()
+            {
+                EnableIPV6 = true,
+                EnableIPV4 = true,
+                LocalNetworkSubnets = lan?.Split(';') ?? throw new ArgumentNullException(nameof(lan))
+            };
+
+            NetworkManager.MockNetworkSettings = interfaces;
+            using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+            NetworkManager.MockNetworkSettings = string.Empty;
+
+            Assert.Equal(nm.GetInternalBindAddresses().AsString(), value);
+        }
+
+        /// <summary>
+        /// Check that the value given is in the network provided.
+        /// </summary>
+        /// <param name="network">Network address.</param>
+        /// <param name="value">Value to check.</param>
+        [Theory]
+        [InlineData("192.168.10.0/24, !192.168.10.60/32", "192.168.10.60")]
+        public void IsInNetwork(string network, string value)
+        {
+            if (network == null)
+            {
+                throw new ArgumentNullException(nameof(network));
+            }
+
+            var conf = new NetworkConfiguration()
+            {
+                EnableIPV6 = true,
+                EnableIPV4 = true,
+                LocalNetworkSubnets = network.Split(',')
+            };
+
+            using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+
+            Assert.False(nm.IsInLocalNetwork(value));
+        }
+
+        /// <summary>
+        /// Checks IP address formats.
+        /// </summary>
+        /// <param name="address"></param>
+        [Theory]
+        [InlineData("127.0.0.1")]
+        [InlineData("127.0.0.1:123")]
+        [InlineData("localhost")]
+        [InlineData("localhost:1345")]
+        [InlineData("www.google.co.uk")]
+        [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
+        [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
+        [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")]
+        [InlineData("fe80::7add:12ff:febb:c67b%16")]
+        [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")]
+        [InlineData("192.168.1.2/255.255.255.0")]
+        [InlineData("192.168.1.2/24")]
+        public void ValidIPStrings(string address)
+        {
+            Assert.True(TryParse(address, out _));
+        }
+
+
+        /// <summary>
+        /// All should be invalid address strings.
+        /// </summary>
+        /// <param name="address">Invalid address strings.</param>
+        [Theory]
+        [InlineData("256.128.0.0.0.1")]
+        [InlineData("127.0.0.1#")]
+        [InlineData("localhost!")]
+        [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")]
+        public void InvalidAddressString(string address)
+        {
+            Assert.False(TryParse(address, out _));
+        }
+
+
+        /// <summary>
+        /// Test collection parsing.
+        /// </summary>
+        /// <param name="settings">Collection to parse.</param>
+        /// <param name="result1">Included addresses from the collection.</param>
+        /// <param name="result2">Included IP4 addresses from the collection.</param>
+        /// <param name="result3">Excluded addresses from the collection.</param>
+        /// <param name="result4">Excluded IP4 addresses from the collection.</param>
+        /// <param name="result5">Network addresses of the collection.</param>
+        [Theory]
+        [InlineData("127.0.0.1#",
+            "[]",
+            "[]",
+            "[]",
+            "[]",
+            "[]")]
+        [InlineData("[127.0.0.1]",
+            "[]",
+            "[]",
+            "[127.0.0.1/32]",
+            "[127.0.0.1/32]",
+            "[]")]
+        [InlineData("",
+            "[]",
+            "[]",
+            "[]",
+            "[]",
+            "[]")]
+        [InlineData("192.158.1.2/255.255.0.0,192.169.1.2/8",
+            "[192.158.1.2/16,192.169.1.2/8]",
+            "[192.158.1.2/16,192.169.1.2/8]",
+            "[]",
+            "[]",
+            "[192.158.0.0/16,192.0.0.0/8]")]
+        [InlineData("192.158.1.2/16, localhost, fd23:184f:2029:0:3139:7386:67d7:d517,    [10.10.10.10]",
+            "[192.158.1.2/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]",
+            "[192.158.1.2/16,127.0.0.1/32]",
+            "[10.10.10.10/32]",
+            "[10.10.10.10/32]",
+            "[192.158.0.0/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]")]
+        public void TestCollections(string settings, string result1, string result2, string result3, string result4, string result5)
+        {
+            if (settings == null)
+            {
+                throw new ArgumentNullException(nameof(settings));
+            }
+
+            var conf = new NetworkConfiguration()
+            {
+                EnableIPV6 = true,
+                EnableIPV4 = true,
+            };           
+
+            using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+
+            // Test included.
+            Collection<IPObject> nc = nm.CreateIPCollection(settings.Split(","), false); 
+            Assert.Equal(nc.AsString(), result1);
+
+            // Test excluded.
+            nc = nm.CreateIPCollection(settings.Split(","), true);
+            Assert.Equal(nc.AsString(), result3);
+
+            conf.EnableIPV6 = false;
+            nm.UpdateSettings(conf);
+            
+            // Test IP4 included.
+            nc = nm.CreateIPCollection(settings.Split(","), false);
+            Assert.Equal(nc.AsString(), result2);
+
+            // Test IP4 excluded.
+            nc = nm.CreateIPCollection(settings.Split(","), true);
+            Assert.Equal(nc.AsString(), result4);
+
+            conf.EnableIPV6 = true;
+            nm.UpdateSettings(conf);
+
+            // Test network addresses of collection.
+            nc = nm.CreateIPCollection(settings.Split(","), false);
+            nc = nc.AsNetworks();
+            Assert.Equal(nc.AsString(), result5);
+        }
+
+        /// <summary>
+        /// Union two collections.
+        /// </summary>
+        /// <param name="settings">Source.</param>
+        /// <param name="compare">Destination.</param>
+        /// <param name="result">Result.</param>
+        [Theory]
+        [InlineData("127.0.0.1", "fd23:184f:2029:0:3139:7386:67d7:d517/64,fd23:184f:2029:0:c0f0:8a8a:7605:fffa/128,fe80::3139:7386:67d7:d517%16/64,192.168.1.208/24,::1/128,127.0.0.1/8", "[127.0.0.1/32]")]
+        [InlineData("127.0.0.1", "127.0.0.1/8", "[127.0.0.1/32]")]
+        public void UnionCheck(string settings, string compare, string result)
+        {
+            if (settings == null)
+            {
+                throw new ArgumentNullException(nameof(settings));
+            }
+
+            if (compare == null)
+            {
+                throw new ArgumentNullException(nameof(compare));
+            }
+
+            if (result == null)
+            {
+                throw new ArgumentNullException(nameof(result));
+            }
+
+
+            var conf = new NetworkConfiguration()
+            {
+                EnableIPV6 = true,
+                EnableIPV4 = true,
+            };
+
+            using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+
+            Collection<IPObject> nc1 = nm.CreateIPCollection(settings.Split(","), false);
+            Collection<IPObject> nc2 = nm.CreateIPCollection(compare.Split(","), false);
+
+            Assert.Equal(nc1.Union(nc2).AsString(), result);
+        }
+
+        [Theory]
+        [InlineData("192.168.5.85/24", "192.168.5.1")]
+        [InlineData("192.168.5.85/24", "192.168.5.254")]
+        [InlineData("10.128.240.50/30", "10.128.240.48")]
+        [InlineData("10.128.240.50/30", "10.128.240.49")]
+        [InlineData("10.128.240.50/30", "10.128.240.50")]
+        [InlineData("10.128.240.50/30", "10.128.240.51")]
+        [InlineData("127.0.0.1/8", "127.0.0.1")]
+        public void IpV4SubnetMaskMatchesValidIpAddress(string netMask, string ipAddress)
+        {
+            var ipAddressObj = IPNetAddress.Parse(netMask);
+            Assert.True(ipAddressObj.Contains(IPAddress.Parse(ipAddress)));
+        }
+
+        [Theory]
+        [InlineData("192.168.5.85/24", "192.168.4.254")]
+        [InlineData("192.168.5.85/24", "191.168.5.254")]
+        [InlineData("10.128.240.50/30", "10.128.240.47")]
+        [InlineData("10.128.240.50/30", "10.128.240.52")]
+        [InlineData("10.128.240.50/30", "10.128.239.50")]
+        [InlineData("10.128.240.50/30", "10.127.240.51")]
+        public void IpV4SubnetMaskDoesNotMatchInvalidIpAddress(string netMask, string ipAddress)
+        {
+            var ipAddressObj = IPNetAddress.Parse(netMask);
+            Assert.False(ipAddressObj.Contains(IPAddress.Parse(ipAddress)));
+        }
+
+        [Theory]
+        [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")]
+        [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:FFFF:FFFF:FFFF:FFFF")]
+        [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:0001:0000:0000:0000")]
+        [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:FFFF:FFFF:FFFF:FFF0")]
+        [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")]
+        public void IpV6SubnetMaskMatchesValidIpAddress(string netMask, string ipAddress)
+        {
+            var ipAddressObj = IPNetAddress.Parse(netMask);
+            Assert.True(ipAddressObj.Contains(IPAddress.Parse(ipAddress)));
+        }
+
+        [Theory]
+        [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0011:FFFF:FFFF:FFFF:FFFF")]
+        [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0013:0000:0000:0000:0000")]
+        [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0013:0001:0000:0000:0000")]
+        [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0011:FFFF:FFFF:FFFF:FFF0")]
+        [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0001")]
+        public void IpV6SubnetMaskDoesNotMatchInvalidIpAddress(string netMask, string ipAddress)
+        {
+            var ipAddressObj = IPNetAddress.Parse(netMask);
+            Assert.False(ipAddressObj.Contains(IPAddress.Parse(ipAddress)));
+        }
+
+        [Theory]
+        [InlineData("10.0.0.0/255.0.0.0", "10.10.10.1/32")]
+        [InlineData("10.0.0.0/8", "10.10.10.1/32")]
+        [InlineData("10.0.0.0/255.0.0.0", "10.10.10.1")]
+
+        [InlineData("10.10.0.0/255.255.0.0", "10.10.10.1/32")]
+        [InlineData("10.10.0.0/16", "10.10.10.1/32")]
+        [InlineData("10.10.0.0/255.255.0.0", "10.10.10.1")]
+
+        [InlineData("10.10.10.0/255.255.255.0", "10.10.10.1/32")]
+        [InlineData("10.10.10.0/24", "10.10.10.1/32")]
+        [InlineData("10.10.10.0/255.255.255.0", "10.10.10.1")]
+
+        public void TestSubnetContains(string network, string ip)
+        {
+            Assert.True(TryParse(network, out IPObject? networkObj));
+            Assert.True(TryParse(ip, out IPObject? ipObj));
+            Assert.True(networkObj.Contains(ipObj));
+        }
+
+        [Theory]
+        [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "172.168.1.2/24", "172.168.1.2/24")]
+        [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "172.168.1.2/24, 10.10.10.1", "172.168.1.2/24,10.10.10.1/24")]
+        [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "192.168.1.2/255.255.255.0, 10.10.10.1", "192.168.1.2/24,10.10.10.1/24")]
+        [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "192.168.1.2/24, 100.10.10.1", "192.168.1.2/24")]
+        [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "194.168.1.2/24, 100.10.10.1", "")]
+
+        public void TestCollectionEquality(string source, string dest, string result)
+        {
+            if (source == null)
+            {
+                throw new ArgumentNullException(nameof(source));
+            }
+
+            if (dest == null)
+            {
+                throw new ArgumentNullException(nameof(dest));
+            }
+
+            if (result == null)
+            {
+                throw new ArgumentNullException(nameof(result));
+            }
+
+            var conf = new NetworkConfiguration()
+            {
+                EnableIPV6 = true,
+                EnableIPV4 = true
+            };
+
+            using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+
+            // Test included, IP6.
+            Collection<IPObject> ncSource = nm.CreateIPCollection(source.Split(","));
+            Collection<IPObject> ncDest = nm.CreateIPCollection(dest.Split(","));
+            Collection<IPObject> ncResult = ncSource.Union(ncDest);
+            Collection<IPObject> resultCollection = nm.CreateIPCollection(result.Split(","));
+            Assert.True(ncResult.Compare(resultCollection));
+        }
+
+
+        [Theory]
+        [InlineData("10.1.1.1/32", "10.1.1.1")]
+        [InlineData("192.168.1.254/32", "192.168.1.254/255.255.255.255")]
+
+        public void TestEquals(string source, string dest)
+        {
+            Assert.True(IPNetAddress.Parse(source).Equals(IPNetAddress.Parse(dest)));
+            Assert.True(IPNetAddress.Parse(dest).Equals(IPNetAddress.Parse(source)));
+        }
+
+        [Theory]
+
+        // Testing bind interfaces.
+        // On my system eth16 is internal, eth11 external (Windows defines the indexes).
+        //
+        // This test is to replicate how DNLA requests work throughout the system.
+
+        // User on internal network, we're bound internal and external - so result is internal.
+        [InlineData("192.168.1.1", "eth16,eth11", false, "eth16")]
+        // User on external network, we're bound internal and external - so result is external.
+        [InlineData("8.8.8.8", "eth16,eth11", false, "eth11")]
+        // User on internal network, we're bound internal only - so result is internal.
+        [InlineData("10.10.10.10", "eth16", false, "eth16")]
+        // User on internal network, no binding specified - so result is the 1st internal.
+        [InlineData("192.168.1.1", "", false, "eth16")]
+        // User on external network, internal binding only - so result is the 1st internal.
+        [InlineData("jellyfin.org", "eth16", false, "eth16")]
+        // User on external network, no binding - so result is the 1st external.
+        [InlineData("jellyfin.org", "", false, "eth11")]
+        // User assumed to be internal, no binding - so result is the 1st internal.
+        [InlineData("", "", false, "eth16")]
+        public void TestBindInterfaces(string source, string bindAddresses, bool ipv6enabled, string result)
+        {
+            if (source == null)
+            {
+                throw new ArgumentNullException(nameof(source));
+            }
+
+            if (bindAddresses == null)
+            {
+                throw new ArgumentNullException(nameof(bindAddresses));
+            }
+
+            if (result == null)
+            {
+                throw new ArgumentNullException(nameof(result));
+            }
+
+            var conf = new NetworkConfiguration()
+            {
+                LocalNetworkAddresses = bindAddresses.Split(','),
+                EnableIPV6 = ipv6enabled,
+                EnableIPV4 = true
+            };
+
+            NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11";
+            using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+            NetworkManager.MockNetworkSettings = string.Empty;
+
+            _ = nm.TryParseInterface(result, out Collection<IPObject>? resultObj);
+
+            if (resultObj != null)
+            {
+                result = ((IPNetAddress)resultObj[0]).ToString(true);
+                var intf = nm.GetBindInterface(source, out int? _);
+
+                Assert.Equal(intf, result);
+            }
+        }
+
+        [Theory]
+
+        // Testing bind interfaces. These are set for my system so won't work elsewhere.
+        // On my system eth16 is internal, eth11 external (Windows defines the indexes).
+        //
+        // This test is to replicate how subnet bound ServerPublisherUri work throughout the system.
+        
+        // User on internal network, we're bound internal and external - so result is internal override.
+        [InlineData("192.168.1.1", "192.168.1.0/24", "eth16,eth11", false, "192.168.1.0/24=internal.jellyfin", "internal.jellyfin")]
+
+        // User on external network, we're bound internal and external - so result is override.
+        [InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")]
+
+        // User on internal network, we're bound internal only, but the address isn't in the LAN - so return the override.
+        [InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://internalButNotDefinedAsLan.com", "http://internalButNotDefinedAsLan.com")]
+
+        // User on internal network, no binding specified - so result is the 1st internal.
+        [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")]
+
+        // User on external network, internal binding only - so asumption is a proxy forward, return external override.
+        [InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")]
+
+        // User on external network, no binding - so result is the 1st external which is overriden.
+        [InlineData("jellyfin.org", "192.168.1.0/24", "", false, "0.0.0.0 = http://helloworld.com", "http://helloworld.com")]
+
+        // User assumed to be internal, no binding - so result is the 1st internal.
+        [InlineData("", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")]
+
+        // User is internal, no binding - so result is the 1st internal, which is then overridden.
+        [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "eth16=http://helloworld.com", "http://helloworld.com")]
+
+        public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result)
+        {
+            if (lan == null)
+            {
+                throw new ArgumentNullException(nameof(lan));
+            }
+
+            if (bindAddresses == null)
+            {
+                throw new ArgumentNullException(nameof(bindAddresses));
+            }
+
+            var conf = new NetworkConfiguration()
+            {
+                LocalNetworkSubnets = lan.Split(','),
+                LocalNetworkAddresses = bindAddresses.Split(','),
+                EnableIPV6 = ipv6enabled,
+                EnableIPV4 = true,
+                PublishedServerUriBySubnet = new string[] { publishedServers }
+            };
+
+            NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11";
+            using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+            NetworkManager.MockNetworkSettings = string.Empty;
+
+            if (nm.TryParseInterface(result, out Collection<IPObject>? resultObj) && resultObj != null)
+            {
+                // Parse out IPAddresses so we can do a string comparison. (Ignore subnet masks).
+                result = ((IPNetAddress)resultObj[0]).ToString(true);
+            }
+
+            var intf = nm.GetBindInterface(source, out int? _);
+
+            Assert.Equal(intf, result);
+        }
+    }
+}