瀏覽代碼

Large number of files

Jim Cartlidge 4 年之前
父節點
當前提交
9ef79d190b
共有 43 個文件被更改,包括 1836 次插入1204 次删除
  1. 26 16
      Emby.Dlna/Main/DlnaEntryPoint.cs
  2. 1 9
      Emby.Dlna/PlayTo/PlayToManager.cs
  3. 35 183
      Emby.Server.Implementations/ApplicationHost.cs
  4. 1 1
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  5. 1 1
      Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
  6. 1 0
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  7. 2 1
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
  8. 1 0
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  9. 0 556
      Emby.Server.Implementations/Networking/NetworkManager.cs
  10. 1 1
      Emby.Server.Implementations/Udp/UdpServer.cs
  11. 2 1
      Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
  12. 2 1
      Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
  13. 2 1
      Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs
  14. 2 1
      Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs
  15. 1 0
      Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
  16. 1 0
      Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
  17. 2 1
      Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs
  18. 2 1
      Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs
  19. 2 1
      Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
  20. 1 0
      Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs
  21. 6 5
      Jellyfin.Api/Controllers/SystemController.cs
  22. 2 1
      Jellyfin.Api/Controllers/UserController.cs
  23. 2 1
      Jellyfin.Api/Helpers/DynamicHlsHelper.cs
  24. 2 1
      Jellyfin.Api/Helpers/MediaInfoHelper.cs
  25. 39 0
      Jellyfin.Networking/Jellyfin.Networking.csproj
  26. 189 0
      Jellyfin.Networking/Manager/INetworkManager.cs
  27. 1203 0
      Jellyfin.Networking/Manager/NetworkManager.cs
  28. 1 0
      Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
  29. 2 2
      Jellyfin.Server.Implementations/Users/UserManager.cs
  30. 1 3
      Jellyfin.Server/CoreAppHost.cs
  31. 19 18
      Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
  32. 3 21
      Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
  33. 15 52
      Jellyfin.Server/Program.cs
  34. 0 97
      MediaBrowser.Common/Net/INetworkManager.cs
  35. 8 22
      MediaBrowser.Controller/IServerApplicationHost.cs
  36. 19 0
      MediaBrowser.Model/Configuration/PathSubstitution.cs
  37. 208 180
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  38. 20 14
      MediaBrowser.sln
  39. 1 0
      RSSDP/RSSDP.csproj
  40. 3 2
      RSSDP/SsdpCommunicationsServer.cs
  41. 5 6
      RSSDP/SsdpDevicePublisher.cs
  42. 2 1
      tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs
  43. 0 3
      tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs

+ 26 - 16
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -2,12 +2,14 @@
 
 using System;
 using System.Globalization;
+using System.Linq;
 using System.Net.Http;
 using System.Net.Sockets;
 using System.Threading;
 using System.Threading.Tasks;
 using Emby.Dlna.PlayTo;
 using Emby.Dlna.Ssdp;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
@@ -25,6 +27,7 @@ using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.System;
 using Microsoft.Extensions.Logging;
+using NetworkCollection;
 using Rssdp;
 using Rssdp.Infrastructure;
 using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
@@ -134,20 +137,20 @@ namespace Emby.Dlna.Main
         {
             await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
 
-            await ReloadComponents().ConfigureAwait(false);
+            ReloadComponents();
 
             _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))
             {
-                await ReloadComponents().ConfigureAwait(false);
+                ReloadComponents();
             }
         }
 
-        private async Task ReloadComponents()
+        private void ReloadComponents()
         {
             var options = _config.GetDlnaConfiguration();
 
@@ -155,7 +158,7 @@ namespace Emby.Dlna.Main
 
             if (options.EnableServer)
             {
-                await StartDevicePublisher(options).ConfigureAwait(false);
+                StartDevicePublisher(options);
             }
             else
             {
@@ -225,7 +228,7 @@ namespace Emby.Dlna.Main
             }
         }
 
-        public async Task StartDevicePublisher(Configuration.DlnaOptions options)
+        public void StartDevicePublisher(Configuration.DlnaOptions options)
         {
             if (!options.BlastAliveMessages)
             {
@@ -245,7 +248,7 @@ namespace Emby.Dlna.Main
                     SupportPnpRootDevice = false
                 };
 
-                await RegisterServerEndpoints().ConfigureAwait(false);
+                RegisterServerEndpoints();
 
                 _publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
             }
@@ -255,39 +258,46 @@ namespace Emby.Dlna.Main
             }
         }
 
-        private async Task RegisterServerEndpoints()
+        private void RegisterServerEndpoints()
         {
-            var addresses = await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false);
+            var bindAddresses = _networkManager.GetInternalBindAddresses()
+                .Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0));
 
             var udn = CreateUuid(_appHost.SystemId);
 
-            foreach (var address in addresses)
+            if (!bindAddresses.Any())
             {
-                if (address.AddressFamily == AddressFamily.InterNetworkV6)
+                // No interfaces returned, so use loopback.
+                bindAddresses = _networkManager.GetLoopbacks();
+            }
+
+            foreach (var addr in bindAddresses)
+            {
+                if (addr.AddressFamily == AddressFamily.InterNetworkV6)
                 {
                     // Not supporting IPv6 right now
                     continue;
                 }
 
                 // Limit to LAN addresses only
-                if (!_networkManager.IsAddressInSubnets(address, true, true))
+                if (!_networkManager.IsInLocalNetwork(addr))
                 {
                     continue;
                 }
 
                 var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
 
-                _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
+                _logger.LogInformation("Registering publisher for {0} on {1}", fullService, addr);
 
                 var descriptorUri = "/dlna/" + udn + "/description.xml";
-                var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri);
+                var uri = new Uri(_appHost.GetSmartApiUrl(addr.Address) + descriptorUri);
 
                 var device = new SsdpRootDevice
                 {
                     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.
-                    Address = address,
-                    SubnetMask = _networkManager.GetLocalIpSubnetMask(address),
+                    Address = addr.Address,
+                    SubnetMask = ((IPNetAddress)addr).Mask, // MIGRATION: This fields is going.
                     FriendlyName = "Jellyfin",
                     Manufacturer = "Jellyfin",
                     ModelName = "Jellyfin Server",

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

@@ -177,15 +177,7 @@ namespace Emby.Dlna.PlayTo
 
                 _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(
                     sessionInfo,

+ 35 - 183
Emby.Server.Implementations/ApplicationHost.cs

@@ -46,6 +46,7 @@ using Emby.Server.Implementations.SyncPlay;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.Updates;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Events;
@@ -97,6 +98,7 @@ using MediaBrowser.Providers.Manager;
 using MediaBrowser.Providers.Plugins.TheTvdb;
 using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.XbmcMetadata.Providers;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
@@ -117,7 +119,6 @@ namespace Emby.Server.Implementations
         private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
 
         private readonly IFileSystem _fileSystemManager;
-        private readonly INetworkManager _networkManager;
         private readonly IXmlSerializer _xmlSerializer;
         private readonly IStartupOptions _startupOptions;
 
@@ -188,6 +189,11 @@ namespace Emby.Server.Implementations
         /// <value>The plugins.</value>
         public IReadOnlyList<IPlugin> Plugins => _plugins;
 
+        /// <summary>
+        /// Gets the NetworkManager object.
+        /// </summary>
+        private readonly INetworkManager _networkManager;
+
         /// <summary>
         /// Gets the logger factory.
         /// </summary>
@@ -211,7 +217,7 @@ namespace Emby.Server.Implementations
         private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
 
         /// <summary>
-        /// Gets the configuration manager.
+        /// Gets or sets the configuration manager.
         /// </summary>
         /// <value>The configuration manager.</value>
         protected IConfigurationManager ConfigurationManager { get; set; }
@@ -244,28 +250,25 @@ namespace Emby.Server.Implementations
         /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> 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="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
         /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
         public ApplicationHost(
             IServerApplicationPaths applicationPaths,
             ILoggerFactory loggerFactory,
             IStartupOptions options,
             IFileSystem fileSystem,
-            INetworkManager networkManager,
             IServiceCollection serviceCollection)
         {
             _xmlSerializer = new MyXmlSerializer();
             ServiceCollection = serviceCollection;
 
-            _networkManager = networkManager;
-            networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets;
-
             ApplicationPaths = applicationPaths;
             LoggerFactory = loggerFactory;
             _fileSystemManager = fileSystem;
 
             ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
 
+            _networkManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
+
             Logger = LoggerFactory.CreateLogger<ApplicationHost>();
 
             _startupOptions = options;
@@ -278,8 +281,6 @@ namespace Emby.Server.Implementations
 
             fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
 
-            _networkManager.NetworkChanged += OnNetworkChanged;
-
             CertificateInfo = new CertificateInfo
             {
                 Path = ServerConfigurationManager.Configuration.CertificatePath,
@@ -308,16 +309,6 @@ namespace Emby.Server.Implementations
                 .Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
         }
 
-        private string[] GetConfiguredLocalSubnets()
-        {
-            return ServerConfigurationManager.Configuration.LocalNetworkSubnets;
-        }
-
-        private void OnNetworkChanged(object sender, EventArgs e)
-        {
-            _validAddressResults.Clear();
-        }
-
         /// <inheritdoc />
         public Version ApplicationVersion { get; }
 
@@ -398,7 +389,7 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// Resolves this instance.
         /// </summary>
-        /// <typeparam name="T">The type</typeparam>
+        /// <typeparam name="T">The type.</typeparam>
         /// <returns>``0.</returns>
         public T Resolve<T>() => ServiceProvider.GetService<T>();
 
@@ -1091,13 +1082,10 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// Gets the system status.
         /// </summary>
-        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="source">Where this request originated.</param>
         /// <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
             {
                 HasPendingRestart = HasPendingRestart,
@@ -1117,9 +1105,9 @@ namespace Emby.Server.Implementations
                 CanSelfRestart = CanSelfRestart,
                 CanLaunchWebBrowser = CanLaunchWebBrowser,
                 HasUpdateAvailable = HasUpdateAvailable,
-                TranscodingTempPath = transcodingTempPath,
+                TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
                 ServerName = FriendlyName,
-                LocalAddress = localAddress,
+                LocalAddress = GetSmartApiUrl(source),
                 SupportsLibraryMonitor = true,
                 EncoderLocation = _mediaEncoder.EncoderLocation,
                 SystemArchitecture = RuntimeInformation.OSArchitecture,
@@ -1132,10 +1120,8 @@ namespace Emby.Server.Implementations
                 .Select(i => new WakeOnLanInfo(i))
                 .ToList();
 
-        public async Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken)
+        public PublicSystemInfo GetPublicSystemInfo(IPAddress source)
         {
-            var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
-
             return new PublicSystemInfo
             {
                 Version = ApplicationVersionString,
@@ -1143,8 +1129,8 @@ namespace Emby.Server.Implementations
                 Id = SystemId,
                 OperatingSystem = OperatingSystem.Id.ToString(),
                 ServerName = FriendlyName,
-                LocalAddress = localAddress,
-                StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
+                LocalAddress = GetSmartApiUrl(source),
+                StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted              
             };
         }
 
@@ -1152,186 +1138,51 @@ namespace Emby.Server.Implementations
         public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps;
 
         /// <inheritdoc/>
-        public async Task<string> GetLocalApiUrl(CancellationToken cancellationToken)
+        public string GetSmartApiUrl(object source)
         {
-            try
-            {
-                // 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]);
-            }
-            catch (Exception ex)
+            // Published server ends with a /
+            if (_startupOptions.PublishedServerUrl != null)
             {
-                Logger.LogError(ex, "Error getting local Ip address information");
+                // Published server ends with a '/', so we need to remove it.
+                return _startupOptions.PublishedServerUrl.ToString().Trim('/');
             }
 
-            return null;
-        }
+            string smart = _networkManager.GetBindInterface(source, out int? 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)
-        {
-            var index = address.IndexOf('%');
-            if (index == -1)
+            // If the smartAPI doesn't start with http then treat it as a host or ip.
+            if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
             {
-                return address;
+                return smart.Trim('/');
             }
 
-            return address.Slice(0, index);
+            return GetLocalApiUrl(smart.Trim('/'), source is HttpRequest request ? request.Scheme : null, port);
         }
 
-        /// <inheritdoc />
-        public string GetLocalApiUrl(IPAddress ipAddress)
+        /// <inheritdoc/>
+        public string GetLoopbackHttpApiUrl()
         {
-            if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
+            if (NetworkManager.IsIP6Enabled)
             {
-                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 GetLocalApiUrl("::1", Uri.UriSchemeHttp, HttpPort);
             }
 
-            return GetLocalApiUrl(ipAddress.ToString());
-        }
-
-        /// <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)
+        public string GetLocalApiUrl(string 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(),
+                Host = host,
                 Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
                 Path = ServerConfigurationManager.Configuration.BaseUrl
             }.ToString().TrimEnd('/');
         }
 
-        public Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken)
-        {
-            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)
-            {
-                addresses.AddRange(_networkManager.GetLocalIpAddresses());
-            }
-
-            var resultList = new List<IPAddress>();
-
-            foreach (var address in addresses)
-            {
-                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 resultList;
-        }
-
-        public IPAddress NormalizeConfiguredLocalAddress(ReadOnlySpan<char> address)
-        {
-            var index = address.Trim('/').IndexOf('/');
-            if (index != -1)
-            {
-                address = address.Slice(index + 1);
-            }
-
-            if (IPAddress.TryParse(address.Trim('/'), out IPAddress result))
-            {
-                return result;
-            }
-
-            return null;
-        }
-
-        private readonly ConcurrentDictionary<string, bool> _validAddressResults = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
-
-        private async Task<bool> IsLocalIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken)
-        {
-            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().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)
-            {
-                Logger.LogDebug(ex, "Ping test result to {0}. Success: {1}", apiUrl, false);
-
-                _validAddressResults.AddOrUpdate(apiUrl, false, (k, v) => false);
-                return false;
-            }
-        }
-
         public string FriendlyName =>
             string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName)
                 ? Environment.MachineName
@@ -1486,6 +1337,7 @@ namespace Emby.Server.Implementations
 
             _disposed = true;
         }
+
     }
 
     internal class CertificateInfo

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

@@ -36,7 +36,7 @@
     <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.7" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" />
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.7" />
-    <PackageReference Include="Mono.Nat" Version="2.0.2" />
+    <PackageReference Include="Mono.Nat" Version="3.0.0" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
     <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
     <PackageReference Include="sharpcompress" Version="0.26.0" />

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

@@ -76,7 +76,7 @@ namespace Emby.Server.Implementations.LiveTv
             }
 
             var list = sources.ToList();
-            var serverUrl = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
+            var serverUrl = _appHost.GetSmartApiUrl(string.Empty);
 
             foreach (var source in list)
             {

+ 1 - 0
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs

@@ -10,6 +10,7 @@ using System.Net.Http;
 using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;

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

@@ -7,6 +7,7 @@ using System.Net;
 using System.Net.Sockets;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
@@ -57,7 +58,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             var mediaSource = OriginalMediaSource;
 
             var uri = new Uri(mediaSource.Path);
-            var localPort = _networkManager.GetRandomUnusedUdpPort();
+            var localPort = 50000; // Will return to random after next PR.
 
             Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath));
 

+ 1 - 0
Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs

@@ -8,6 +8,7 @@ using System.Linq;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;

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

@@ -1,556 +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 IPAddress[] _localIpAddresses;
-        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 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;
-            }
-
-            byte[] octet = ipAddress.GetAddressBytes();
-
-            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)
-        {
-            byte[] octet = address.GetAddressBytes();
-
-            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)
-        {
-            byte[] ipAdressBytes = address.GetAddressBytes();
-            byte[] subnetMaskBytes = subnetMask.GetAddressBytes();
-
-            if (ipAdressBytes.Length != subnetMaskBytes.Length)
-            {
-                throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
-            }
-
-            byte[] broadcastAddress = new byte[ipAdressBytes.Length];
-            for (int i = 0; i < broadcastAddress.Length; i++)
-            {
-                broadcastAddress[i] = (byte)(ipAdressBytes[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])
                 ? _config[AddressOverrideConfigKey]
-                : await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
+                : _appHost.GetSmartApiUrl(string.Empty); // MIGRATION: Temp value.
 
             if (!string.IsNullOrEmpty(localUrl))
             {

+ 2 - 1
Jellyfin.Api/Auth/BaseAuthorizationHandler.cs

@@ -1,6 +1,7 @@
-using System.Security.Claims;
+using System.Security.Claims;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Data.Enums;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Library;

+ 2 - 1
Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs

@@ -1,4 +1,5 @@
-using System.Threading.Tasks;
+using System.Threading.Tasks;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Library;
 using Microsoft.AspNetCore.Authorization;

+ 2 - 1
Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs

@@ -1,4 +1,5 @@
-using System.Threading.Tasks;
+using System.Threading.Tasks;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Library;
 using Microsoft.AspNetCore.Authorization;

+ 2 - 1
Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs

@@ -1,4 +1,5 @@
-using System.Threading.Tasks;
+using System.Threading.Tasks;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Library;

+ 1 - 0
Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs

@@ -1,4 +1,5 @@
 using System.Threading.Tasks;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Library;

+ 1 - 0
Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs

@@ -1,5 +1,6 @@
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Library;

+ 2 - 1
Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs

@@ -1,4 +1,5 @@
-using System.Threading.Tasks;
+using System.Threading.Tasks;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Library;
 using Microsoft.AspNetCore.Authorization;

+ 2 - 1
Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs

@@ -1,5 +1,6 @@
-using System.Threading.Tasks;
+using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Library;
 using Microsoft.AspNetCore.Authorization;

+ 2 - 1
Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs

@@ -1,4 +1,5 @@
-using System.Threading.Tasks;
+using System.Threading.Tasks;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Library;
 using Microsoft.AspNetCore.Authorization;

+ 1 - 0
Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs

@@ -1,5 +1,6 @@
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Library;
 using Microsoft.AspNetCore.Authorization;

+ 6 - 5
Jellyfin.Api/Controllers/SystemController.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.IO;
@@ -8,6 +8,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Constants;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
@@ -64,9 +65,9 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("Info")]
         [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)]
         [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>
@@ -76,9 +77,9 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="PublicSystemInfo"/> with public info about the system.</returns>
         [HttpGet("Info/Public")]
         [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>

+ 2 - 1
Jellyfin.Api/Controllers/UserController.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.Linq;
@@ -7,6 +7,7 @@ using Jellyfin.Api.Constants;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Models.UserDtos;
 using Jellyfin.Data.Enums;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Authentication;

+ 2 - 1
Jellyfin.Api/Helpers/DynamicHlsHelper.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
@@ -8,6 +8,7 @@ using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;

+ 2 - 1
Jellyfin.Api/Helpers/MediaInfoHelper.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Globalization;
 using System.Linq;
 using System.Text.Json;
@@ -6,6 +6,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;

+ 39 - 0
Jellyfin.Networking/Jellyfin.Networking.csproj

@@ -0,0 +1,39 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <AssemblyName>Jellyfin.Networking</AssemblyName>
+    <OutputType>Library</OutputType>
+    <TargetFramework>netstandard2.1</TargetFramework>
+    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+    <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Compile Include="..\SharedVersion.cs" />
+  </ItemGroup>
+
+  <!-- Code Analyzers-->
+  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
+    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+  </ItemGroup>
+
+  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.7" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.7" />
+    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.7" />
+    <PackageReference Include="Mono.Nat" Version="3.0.0" />
+    <PackageReference Include="NetworkCollection" Version="1.0.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
+    <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
+  </ItemGroup>
+</Project>

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

@@ -0,0 +1,189 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.NetworkInformation;
+using MediaBrowser.Model.Configuration;
+using NetworkCollection;
+
+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 override list.
+        /// </summary>
+        Dictionary<IPNetAddress, string> PublishedServerOverrides { get; }
+
+        /// <summary>
+        /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal.
+        /// </summary>
+        public bool TrustAllIP6Interfaces { get; }
+
+        /// <summary>
+        /// Gets returns the remote address filter.
+        /// </summary>
+        NetCollection RemoteAddressFilter { get; }
+
+        /// <summary>
+        /// Calculates the list of interfaces to use for Kestrel.
+        /// </summary>
+        /// <returns>A NetCollection 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>
+        NetCollection GetAllBindInterfaces();
+
+        /// <summary>
+        /// Returns a collection containing the loopback interfaces.
+        /// </summary>
+        /// <returns>Netcollection.</returns>
+        public NetCollection 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(object? 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>
+        List<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>
+        public bool IsGatewayInterface(object? 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 if successful.</param>
+        /// <returns>Success of the operation.</returns>
+        bool TryParseInterface(string token, out IPNetAddress result);
+
+        /// <summary>
+        /// Parses an array of strings into a NetCollection.
+        /// </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>
+        NetCollection CreateIPCollection(string[] values, bool bracketed = false);
+
+        /// <summary>
+        /// Returns all the internal Bind interface addresses.
+        /// </summary>
+        /// <returns>An internal list of interfaces addresses.</returns>
+        NetCollection 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>
+        NetCollection GetFilteredLANSubnets(NetCollection? filter = null);
+
+        /// <summary>
+        /// Reloads all settings and re-initialises the instance.
+        /// </summary>
+        /// <param name="config"><seealso cref="ServerConfiguration"/> to use.</param>
+        public void UpdateSettings(ServerConfiguration config);
+    }
+}

+ 1203 - 0
Jellyfin.Networking/Manager/NetworkManager.cs

@@ -0,0 +1,1203 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using NetworkCollection;
+
+namespace Jellyfin.Networking.Manager
+{
+    /// <summary>
+    /// Class to take care of network interface management.
+    /// </summary>
+    public class NetworkManager : INetworkManager, IDisposable
+    {
+        private static NetworkManager? _instance;
+
+        /// <summary>
+        /// Contains the description of the interface along with its index.
+        /// </summary>
+        private readonly SortedList<string, int> _interfaceNames;
+
+        /// <summary>
+        /// Threading lock for network interfaces.
+        /// </summary>
+        private readonly object _intLock = new object();
+
+        /// <summary>
+        /// List of all interface addresses and masks.
+        /// </summary>
+        private readonly NetCollection _interfaceAddresses;
+
+        /// <summary>
+        /// List of all interface MAC addresses.
+        /// </summary>
+        private readonly List<PhysicalAddress> _macAddresses;
+
+        private readonly ILogger<NetworkManager> _logger;
+
+        private readonly IConfigurationManager _configurationManager;
+
+        /// <summary>
+        /// Holds the bind address overrides.
+        /// </summary>
+        private readonly Dictionary<IPNetAddress, string> _overrideUrls;
+
+        /// <summary>
+        /// Used to stop "event-racing conditions".
+        /// </summary>
+        private bool _eventfire;
+
+        /// <summary>
+        /// Unfiltered user defined LAN subnets. (Configuration.LocalNetworkSubnets).
+        /// or internal interface network subnets if undefined by user.
+        /// </summary>
+        private NetCollection _lanSubnets;
+
+        /// <summary>
+        /// User defined list of subnets to excluded from the LAN.
+        /// </summary>
+        private NetCollection _excludedSubnets;
+
+        /// <summary>
+        /// List of interface addresses to bind the WS.
+        /// </summary>
+        private NetCollection _bindAddresses;
+
+        /// <summary>
+        /// List of interface addresses to exclude from bind.
+        /// </summary>
+        private NetCollection _bindExclusions;
+
+        /// <summary>
+        /// Caches list of all internal filtered interface addresses and masks.
+        /// </summary>
+        private NetCollection _internalInterfaces;
+
+        /// <summary>
+        /// Flag set when no custom LAN has been defined in the config.
+        /// </summary>
+        private bool _usingPrivateAddresses;
+
+        /// <summary>
+        /// True if this object is disposed.
+        /// </summary>
+        private bool _disposed;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="NetworkManager"/> class.
+        /// </summary>
+        /// <param name="configurationManager">IServerConfigurationManager instance.</param>
+        /// <param name="logger">Logger to use for messages.</param>
+#pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in InitialiseLAN function. Compiler doesn't yet recognise this.
+        public NetworkManager(IConfigurationManager configurationManager, ILogger<NetworkManager> logger)
+        {
+            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+            _configurationManager = configurationManager ?? throw new ArgumentNullException(nameof(configurationManager));
+
+            _interfaceAddresses = new NetCollection(unique: false);
+            _macAddresses = new List<PhysicalAddress>();
+            _interfaceNames = new SortedList<string, int>();
+            _overrideUrls = new Dictionary<IPNetAddress, string>();
+
+            UpdateSettings((ServerConfiguration)_configurationManager.CommonConfiguration);
+            if (!IsIP6Enabled && !IsIP4Enabled)
+            {
+                throw new ApplicationException("IPv4 and IPv6 cannot both be disabled.");
+            }
+
+            NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged;
+            NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
+
+            _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
+
+            Instance = this;
+        }
+#pragma warning restore CS8618 // Non-nullable field is uninitialized.
+
+        /// <summary>
+        /// Event triggered on network changes.
+        /// </summary>
+        public event EventHandler? NetworkChanged;
+
+        /// <summary>
+        /// Gets the singleton of this object.
+        /// </summary>
+        public static NetworkManager Instance
+        {
+            get => GetInstance();
+
+            internal set
+            {
+                _instance = value;
+            }
+        }
+
+        /// <summary>
+        /// Gets the unique network location signature, which is updated on every network change.
+        /// </summary>
+        public static string NetworkLocationSignature { get; internal set; } = Guid.NewGuid().ToString();
+
+        /// <summary>
+        /// Gets a value indicating whether IP6 is enabled.
+        /// </summary>
+        public static bool IsIP6Enabled { get; internal set; }
+
+        /// <summary>
+        /// Gets a value indicating whether IP4 is enabled.
+        /// </summary>
+        public static bool IsIP4Enabled { get; internal set; } = true;
+
+        /// <summary>
+        /// Gets a value indicating whether is multi-socket binding available.
+        /// </summary>
+        public static bool EnableMultiSocketBinding { get; internal set; } = true;
+
+        /// <summary>
+        /// Gets the number of times the network address has changed.
+        /// </summary>
+        public static int NetworkChangeCount { get; internal set; } = 1;
+
+        /// <inheritdoc/>
+        public NetCollection RemoteAddressFilter { get; private set; }
+
+        /// <summary>
+        /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal.
+        /// </summary>
+        public bool TrustAllIP6Interfaces { get; internal set; }
+
+        /// <summary>
+        /// Gets the Published server override list.
+        /// </summary>
+        public Dictionary<IPNetAddress, string> PublishedServerOverrides => _overrideUrls;
+
+        /// <inheritdoc/>
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        /// <inheritdoc/>
+        public List<PhysicalAddress> GetMacAddresses()
+        {
+            // Populated in construction - so always has values.
+            lock (_intLock)
+            {
+                return _macAddresses.ToList();
+            }
+        }
+
+        /// <inheritdoc/>
+        public bool IsGatewayInterface(object? addressObj)
+        {
+            var address = (addressObj is IPAddress addressIP) ?
+                addressIP : (addressObj is IPObject addressIPObj) ?
+                    addressIPObj.Address : IPAddress.None;
+
+            lock (_intLock)
+            {
+                return _internalInterfaces.Where(i => i.Address.Equals(address) && (i.Tag < 0)).Any();
+            }
+        }
+
+        /// <inheritdoc/>
+        public NetCollection GetLoopbacks()
+        {
+            NetCollection nc = new NetCollection();
+            if (IsIP4Enabled)
+            {
+                nc.Add(IPAddress.Loopback);
+            }
+
+            if (IsIP6Enabled)
+            {
+                nc.Add(IPAddress.IPv6Loopback);
+            }
+
+            return nc;
+        }
+
+        /// <inheritdoc/>
+        public bool IsExcluded(IPAddress ip)
+        {
+            return _excludedSubnets.Contains(ip);
+        }
+
+        /// <inheritdoc/>
+        public bool IsExcluded(EndPoint ip)
+        {
+            if (ip != null)
+            {
+                return _excludedSubnets.Contains(((IPEndPoint)ip).Address);
+            }
+
+            return false;
+        }
+
+        /// <inheritdoc/>
+        public NetCollection CreateIPCollection(string[] values, bool bracketed = false)
+        {
+            NetCollection col = new NetCollection();
+            if (values != null)
+            {
+                for (int a = 0; a < values.Length; a++)
+                {
+                    string v = values[a].Trim();
+
+                    try
+                    {
+                        if (v.StartsWith("[", StringComparison.OrdinalIgnoreCase) && v.EndsWith("]", StringComparison.OrdinalIgnoreCase))
+                        {
+                            if (bracketed)
+                            {
+                                AddToCollection(col, v.Remove(v.Length - 1).Substring(1));
+                            }
+                        }
+                        else if (v.StartsWith("!", StringComparison.OrdinalIgnoreCase))
+                        {
+                            if (bracketed)
+                            {
+                                AddToCollection(col, v.Substring(1));
+                            }
+                        }
+                        else if (!bracketed)
+                        {
+                            AddToCollection(col, v);
+                        }
+                    }
+                    catch (ArgumentException e)
+                    {
+                        _logger.LogInformation("Ignoring LAN value {value}. Reason : {reason}", v, e.Message);
+                    }
+                }
+            }
+
+            return col;
+        }
+
+        /// <inheritdoc/>
+        public NetCollection GetAllBindInterfaces()
+        {
+            lock (_intLock)
+            {
+                int count = _bindAddresses.Count;
+
+                if (count == 0)
+                {
+                    if (_bindExclusions.Count > 0)
+                    {
+                        // Return all the interfaces except the ones specifically excluded.
+                        return _interfaceAddresses.Exclude(_bindExclusions);
+                    }
+
+                    // No bind address and no exclusions, so listen on all interfaces.
+                    NetCollection result = new NetCollection();
+
+                    if (IsIP4Enabled)
+                    {
+                        result.Add(IPAddress.Any);
+                    }
+
+                    if (IsIP6Enabled)
+                    {
+                        result.Add(IPAddress.IPv6Any);
+                    }
+
+                    return result;
+                }
+
+                // Remove any excluded bind interfaces.
+                return _bindAddresses.Exclude(_bindExclusions);
+            }
+        }
+
+        /// <inheritdoc/>
+        public string GetBindInterface(object? source, out int? port)
+        {
+            bool chromeCast = false;
+            port = null;
+            // Parse the source object in an attempt to discover where the request originated.
+            IPObject sourceAddr;
+            if (source is HttpRequest sourceReq)
+            {
+                port = sourceReq.Host.Port;
+                if (IPHost.TryParse(sourceReq.Host.Host, out IPHost host))
+                {
+                    sourceAddr = host;
+                }
+                else
+                {
+                    // Assume it's external, as we cannot resolve the host.
+                    sourceAddr = IPHost.None;
+                }
+            }
+            else if (source is string sourceStr && !string.IsNullOrEmpty(sourceStr))
+            {
+                if (string.Equals(sourceStr, "chromecast", StringComparison.OrdinalIgnoreCase))
+                {
+                    chromeCast = true;
+                    // Just assign a variable so has source = true;
+                    sourceAddr = IPNetAddress.IP4Loopback;
+                }
+
+                if (IPHost.TryParse(sourceStr, out IPHost host))
+                {
+                    sourceAddr = host;
+                }
+                else
+                {
+                    // Assume it's external, as we cannot resolve the host.
+                    sourceAddr = IPHost.None;
+                }
+            }
+            else if (source is IPAddress sourceIP)
+            {
+                sourceAddr = new IPNetAddress(sourceIP);
+            }
+            else
+            {
+                // If we have no idea, then assume it came from an external address.
+                sourceAddr = IPHost.None;
+            }
+
+            // Do we have a source?
+            bool haveSource = !sourceAddr.Address.Equals(IPAddress.None);
+
+            if (haveSource)
+            {
+                if (!IsIP6Enabled && sourceAddr.AddressFamily == AddressFamily.InterNetworkV6)
+                {
+                    _logger.LogWarning("IPv6 is disabled in JellyFin, but enabled in the OS. This may affect how the interface is selected.");
+                }
+
+                if (!IsIP4Enabled && sourceAddr.AddressFamily == AddressFamily.InterNetwork)
+                {
+                    _logger.LogWarning("IPv4 is disabled in JellyFin, but enabled in the OS. This may affect how the interface is selected.");
+                }
+            }
+
+            bool isExternal = haveSource && !IsInLocalNetwork(sourceAddr);
+
+            string bindPreference = string.Empty;
+            if (haveSource)
+            {
+                // Check for user override.
+                foreach (var addr in _overrideUrls)
+                {
+                    // Remaining. Match anything.
+                    if (addr.Key.Equals(IPAddress.Broadcast))
+                    {
+                        bindPreference = addr.Value;
+                        break;
+                    }
+                    else if ((addr.Key.Equals(IPAddress.Any) || addr.Key.Equals(IPAddress.IPv6Any)) && (isExternal || chromeCast))
+                    {
+                        // External.
+                        bindPreference = addr.Value;
+                        break;
+                    }
+                    else if (addr.Key.Contains(sourceAddr))
+                    {
+                        // Match ip address.
+                        bindPreference = addr.Value;
+                        break;
+                    }
+                }
+            }
+
+            _logger.LogDebug("GetBindInterface: Souce: {0}, External: {1}:", haveSource, isExternal);
+
+            if (!string.IsNullOrEmpty(bindPreference))
+            {
+                // Has it got a port defined?
+                var parts = bindPreference.Split(':');
+                if (parts.Length > 1)
+                {
+                    if (int.TryParse(parts[1], out int p))
+                    {
+                        bindPreference = parts[0];
+                        port = p;
+                    }
+                }
+
+                _logger.LogInformation("{0}: Using BindAddress {1}:{2}", sourceAddr, bindPreference, port);
+                return bindPreference;
+            }
+
+            string ipresult;
+
+            // No preference given, so move on to bind addresses.
+            lock (_intLock)
+            {
+                var nc = _bindAddresses.Exclude(_bindExclusions).Where(p => !p.IsLoopback());
+
+                int count = nc.Count();
+                if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses.Equals(IPAddress.IPv6Any)))
+                {
+                    // Ignore IPAny addresses.
+                    count = 0;
+                }
+
+                if (count != 0)
+                {
+                    // Check to see if any of the bind interfaces are in the same subnet.
+
+                    IEnumerable<IPObject> bindResult;
+                    IPAddress? defaultGateway = null;
+
+                    if (isExternal)
+                    {
+                        // Find all external bind addresses. Store the default gateway, but check to see if there is a better match first.
+                        bindResult = nc.Where(p => !IsInLocalNetwork(p)).OrderBy(p => p.Tag);
+                        defaultGateway = bindResult.FirstOrDefault()?.Address;
+                        bindResult = bindResult.Where(p => p.Contains(sourceAddr)).OrderBy(p => p.Tag);
+                    }
+                    else
+                    {
+                        // Look for the best internal address.
+                        bindResult = nc.Where(p => IsInLocalNetwork(p) && p.Contains(sourceAddr)).OrderBy(p => p.Tag);
+                    }
+
+                    if (bindResult.Any())
+                    {
+                        ipresult = FormatIP6String(bindResult.First().Address);
+                        _logger.LogDebug("{0}: GetBindInterface: Has source, found a match bind interface subnets. {1}", sourceAddr, ipresult);
+                        return ipresult;
+                    }
+
+                    if (isExternal && defaultGateway != null)
+                    {
+                        ipresult = FormatIP6String(defaultGateway);
+                        _logger.LogDebug("{0}: GetBindInterface: Using first user defined external interface. {1}", sourceAddr, ipresult);
+                        return ipresult;
+                    }
+
+                    ipresult = FormatIP6String(nc.First().Address);
+                    _logger.LogDebug("{0}: GetBindInterface: Selected first user defined interface. {1}", sourceAddr, ipresult);
+
+                    if (isExternal)
+                    {
+                        // TODO: remove this after testing.
+                        _logger.LogWarning("{0}: External request received, however, only an internal interface bind found.", sourceAddr);
+                    }
+
+                    return ipresult;
+                }
+
+                if (isExternal)
+                {
+                    // Get the first WAN interface address that isn't a loopback.
+                    var extResult = _interfaceAddresses
+                        .Exclude(_bindExclusions)
+                        .Where(p => !IsInLocalNetwork(p))
+                        .OrderBy(p => p.Tag);
+
+                    if (extResult.Any())
+                    {
+                        // Does the request originate in one of the interface subnets?
+                        // (For systems with multiple internal network cards, and multiple subnets)
+                        foreach (var intf in extResult)
+                        {
+                            if (!IsInLocalNetwork(intf) && intf.Contains(sourceAddr))
+                            {
+                                ipresult = FormatIP6String(intf.Address);
+                                _logger.LogDebug("{0}: GetBindInterface: Selected best external on interface on range. {1}", sourceAddr, ipresult);
+                                return ipresult;
+                            }
+                        }
+
+                        ipresult = FormatIP6String(extResult.First().Address);
+                        _logger.LogDebug("{0}: GetBindInterface: Selected first external interface. {0}", sourceAddr, ipresult);
+                        return ipresult;
+                    }
+
+                    // Have to return something, so return an internal address
+
+                    // TODO: remove this after testing.
+                    _logger.LogWarning("{0}: External request received, however, no WAN interface found.", sourceAddr);
+                }
+
+                // Get the first LAN interface address that isn't a loopback.
+                var result = _interfaceAddresses
+                    .Exclude(_bindExclusions)
+                    .Where(p => IsInLocalNetwork(p))
+                    .OrderBy(p => p.Tag);
+
+                if (result.Any())
+                {
+                    if (haveSource)
+                    {
+                        // Does the request originate in one of the interface subnets?
+                        // (For systems with multiple internal network cards, and multiple subnets)
+                        foreach (var intf in result)
+                        {
+                            if (intf.Contains(sourceAddr))
+                            {
+                                ipresult = FormatIP6String(intf.Address);
+                                _logger.LogDebug("{0}: GetBindInterface: Has source, matched best internal interface on range. {1}", sourceAddr, ipresult);
+                                return ipresult;
+                            }
+                        }
+                    }
+
+                    ipresult = FormatIP6String(result.First().Address);
+                    _logger.LogDebug("{0}: GetBindInterface: Matched first internal interface. {1}", sourceAddr, ipresult);
+                    return ipresult;
+                }
+
+                // There isn't any others, so we'll use the loopback.
+                ipresult = IsIP6Enabled ? "::" : "127.0.0.1";
+                _logger.LogWarning("{0}: GetBindInterface: Loopback return.", sourceAddr, ipresult);
+                return ipresult;
+            }
+        }
+
+        /// <inheritdoc/>
+        public NetCollection GetInternalBindAddresses()
+        {
+            lock (_intLock)
+            {
+                int count = _bindAddresses.Count;
+
+                if (count == 0)
+                {
+                    if (_bindExclusions.Count > 0)
+                    {
+                        // Return all the internal interfaces except the ones excluded.
+                        return new NetCollection(_internalInterfaces.Where(p => !_bindExclusions.Contains(p)));
+                    }
+
+                    // No bind address, so return all internal interfaces.
+                    return new NetCollection(_internalInterfaces.Where(p => !p.IsLoopback()));
+                }
+
+                return new NetCollection(_bindAddresses.Where(p => !p.IsLoopback()));
+            }
+        }
+
+        /// <inheritdoc/>
+        public bool IsInLocalNetwork(IPObject address)
+        {
+            if (address == null)
+            {
+                throw new ArgumentNullException(nameof(address));
+            }
+
+            if (address.Equals(IPAddress.None))
+            {
+                return false;
+            }
+
+            // See conversation at https://github.com/jellyfin/jellyfin/pull/3515.
+            if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6)
+            {
+                return true;
+            }
+
+            lock (_intLock)
+            {
+                // As private addresses can be redefined by Configuration.LocalNetworkAddresses
+                return _lanSubnets.Contains(address) && !_excludedSubnets.Contains(address);
+            }
+        }
+
+        /// <inheritdoc/>
+        public bool IsInLocalNetwork(string address)
+        {
+            if (IPHost.TryParse(address, out IPHost ep))
+            {
+                lock (_intLock)
+                {
+                    return _lanSubnets.Contains(ep) && !_excludedSubnets.Contains(ep);
+                }
+            }
+
+            return false;
+        }
+
+        /// <inheritdoc/>
+        public bool IsInLocalNetwork(IPAddress address)
+        {
+            if (address == null)
+            {
+                throw new ArgumentNullException(nameof(address));
+            }
+
+            // See conversation at https://github.com/jellyfin/jellyfin/pull/3515.
+            if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6)
+            {
+                return true;
+            }
+
+            lock (_intLock)
+            {
+                // As private addresses can be redefined by Configuration.LocalNetworkAddresses
+                return _lanSubnets.Contains(address) && !_excludedSubnets.Contains(address);
+            }
+        }
+
+        /// <inheritdoc/>
+        public bool IsPrivateAddressRange(IPObject address)
+        {
+            if (address == null)
+            {
+                throw new ArgumentNullException(nameof(address));
+            }
+
+            // See conversation at https://github.com/jellyfin/jellyfin/pull/3515.
+            if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6)
+            {
+                return true;
+            }
+            else
+            {
+                return address.IsPrivateAddressRange();
+            }
+        }
+
+        /// <inheritdoc/>
+        public bool IsExcludedInterface(IPAddress address)
+        {
+            lock (_intLock)
+            {
+                if (_bindExclusions.Count > 0)
+                {
+                    return _bindExclusions.Contains(address);
+                }
+
+                return false;
+            }
+        }
+
+        /// <inheritdoc/>
+        public NetCollection GetFilteredLANSubnets(NetCollection? filter = null)
+        {
+            lock (_intLock)
+            {
+                if (filter == null)
+                {
+                    return NetCollection.AsNetworks(_lanSubnets.Exclude(_excludedSubnets));
+                }
+
+                return _lanSubnets.Exclude(filter);
+            }
+        }
+
+        /// <inheritdoc/>
+        public bool IsValidInterfaceAddress(IPAddress address)
+        {
+            lock (_intLock)
+            {
+                return _interfaceAddresses.Contains(address);
+            }
+        }
+
+        /// <inheritdoc/>
+        public bool TryParseInterface(string token, out IPNetAddress result)
+        {
+            if (string.IsNullOrEmpty(token))
+            {
+                result = IPNetAddress.None;
+                return false;
+            }
+
+            if (_interfaceNames != null && _interfaceNames.TryGetValue(token.ToLower(CultureInfo.InvariantCulture), out int index))
+            {
+                _logger.LogInformation("Interface {0} used in settings. Using its interface addresses.", token);
+
+                // Replace interface tags with the interface IP's.
+                foreach (IPNetAddress iface in _interfaceAddresses)
+                {
+                    if (Math.Abs(iface.Tag) == index &&
+                        ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) ||
+                         (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6)))
+                    {
+                        result = iface;
+                        return true;
+                    }
+                }
+            }
+
+            return IPNetAddress.TryParse(token, out result);
+        }
+
+        /// <summary>
+        /// Reloads all settings and re-initialises the instance.
+        /// </summary>
+        /// <param name="config"><seealso cref="ServerConfiguration"/> to use.</param>
+        public void UpdateSettings(ServerConfiguration config)
+        {
+            if (config == null)
+            {
+                throw new ArgumentNullException(nameof(config));
+            }
+
+            IsIP4Enabled = Socket.OSSupportsIPv6 && config.EnableIPV4;
+            IsIP6Enabled = Socket.OSSupportsIPv6 && config.EnableIPV6;
+            TrustAllIP6Interfaces = config.TrustAllIP6Interfaces;
+            EnableMultiSocketBinding = config.EnableMultiSocketBinding;
+
+            InitialiseInterfaces();
+            InitialiseLAN(config);
+            InitialiseBind(config);
+            InitialiseRemote(config);
+            InitialiseOverrides(config);
+        }
+
+        /// <summary>
+        /// Protected implementation of Dispose pattern.
+        /// </summary>
+        /// <param name="disposing">True to dispose the managed state.</param>
+        protected virtual void Dispose(bool disposing)
+        {
+            if (!_disposed)
+            {
+                if (disposing)
+                {
+                    _configurationManager.ConfigurationUpdated -= ConfigurationUpdated;
+                    NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged;
+                    NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged;
+                }
+
+                _disposed = true;
+            }
+        }
+
+        private static NetworkManager GetInstance()
+        {
+            if (_instance == null)
+            {
+                throw new ApplicationException("NetworkManager is not initialised.");
+            }
+
+            return _instance;
+        }
+
+        private void ConfigurationUpdated(object? sender, EventArgs args)
+        {
+            UpdateSettings((ServerConfiguration)_configurationManager.CommonConfiguration);
+        }
+
+        /// <summary>
+        /// Converts an IPAddress into a string.
+        /// Ipv6 addresses are returned in [ ], with their scope removed.
+        /// </summary>
+        /// <param name="address">Address to convert.</param>
+        /// <returns>URI save conversion of the address.</returns>
+        private string FormatIP6String(IPAddress address)
+        {
+            var str = address.ToString();
+            if (address.AddressFamily == AddressFamily.InterNetworkV6)
+            {
+                int i = str.IndexOf("%", StringComparison.OrdinalIgnoreCase);
+
+                if (i != -1)
+                {
+                    str = str.Substring(0, i);
+                }
+
+                return $"[{str}]";
+            }
+
+            return str;
+        }
+
+        /// <summary>
+        /// Parses strings into the collection, replacing any interface references.
+        /// </summary>
+        /// <param name="col">Collection.</param>
+        /// <param name="token">String to parse.</param>
+        private void AddToCollection(NetCollection col, string token)
+        {
+            // Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1.
+            // Null check required here for automated testing.
+            if (_interfaceNames != null && _interfaceNames.TryGetValue(token.ToLower(CultureInfo.InvariantCulture), out int index))
+            {
+                _logger.LogInformation("Interface {0} used in settings. Using its interface addresses.", token);
+
+                // Replace interface tags with the interface IP's.
+                foreach (IPNetAddress iface in _interfaceAddresses)
+                {
+                    if (Math.Abs(iface.Tag) == index &&
+                        ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) ||
+                         (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6)))
+                    {
+                        col.Add(iface);
+                    }
+                }
+            }
+            else if (NetCollection.TryParse(token, out IPObject obj))
+            {
+                if (!IsIP6Enabled)
+                {
+                    // Remove IP6 addresses from multi-homed IPHosts.
+                    obj.Remove(AddressFamily.InterNetworkV6);
+                    if (!obj.IsIP6())
+                    {
+                        col.Add(obj);
+                    }
+                }
+                else if (!IsIP4Enabled)
+                {
+                    // Remove IP4 addresses from multi-homed IPHosts.
+                    obj.Remove(AddressFamily.InterNetwork);
+                    if (obj.IsIP6())
+                    {
+                        col.Add(obj);
+                    }
+                }
+                else
+                {
+                    col.Add(obj);
+                }
+            }
+            else
+            {
+                _logger.LogDebug("Invalid or unknown network {0}.", token);
+            }
+        }
+
+        /// <summary>
+        /// Handler for network change events.
+        /// </summary>
+        /// <param name="sender">Sender.</param>
+        /// <param name="e">Network availablity information.</param>
+        private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e)
+        {
+            _logger.LogDebug("Network availability changed.");
+            OnNetworkChanged();
+        }
+
+        /// <summary>
+        /// Handler for network change events.
+        /// </summary>
+        /// <param name="sender">Sender.</param>
+        /// <param name="e">Event arguments.</param>
+        private void OnNetworkAddressChanged(object? sender, EventArgs e)
+        {
+            _logger.LogDebug("Network address change detected.");
+            OnNetworkChanged();
+        }
+
+        /// <summary>
+        /// Async task that waits for 2 seconds before re-initialising the settings, as typically these events fire multiple times in succession.
+        /// </summary>
+        /// <returns>The network change async.</returns>
+        private async Task OnNetworkChangeAsync()
+        {
+            try
+            {
+                await Task.Delay(2000).ConfigureAwait(false);
+                InitialiseInterfaces();
+                // Recalculate LAN caches.
+                InitialiseLAN((ServerConfiguration)_configurationManager.CommonConfiguration);
+
+                NetworkChanged?.Invoke(this, EventArgs.Empty);
+            }
+            finally
+            {
+                _eventfire = false;
+            }
+        }
+
+        /// <summary>
+        /// Triggers our event, and re-loads interface information.
+        /// </summary>
+        private void OnNetworkChanged()
+        {
+            // As per UPnP Device Architecture v1.0 Annex A - IPv6 Support.
+            NetworkLocationSignature = Guid.NewGuid().ToString();
+            NetworkChangeCount++;
+            if (NetworkChangeCount > 99)
+            {
+                NetworkChangeCount = 1;
+            }
+
+            if (!_eventfire)
+            {
+                _logger.LogDebug("Network Address Change Event.");
+                // As network events tend to fire one after the other only fire once every second.
+                _eventfire = true;
+                _ = OnNetworkChangeAsync();
+            }
+        }
+
+        /// <summary>
+        /// Parses the user defined overrides into the dictionary object.
+        /// Overrides are the equivalent of localised publishedServerUrl, enabling
+        /// different addresses to be advertised over different subnets.
+        /// format is subnet=ipaddress|host|uri
+        /// when subnet = 0.0.0.0, any external address matches.
+        /// </summary>
+        private void InitialiseOverrides(ServerConfiguration config)
+        {
+            string[] overrides = config.PublishedServerUriBySubnet;
+            if (overrides == null)
+            {
+                lock (_intLock)
+                {
+                    _overrideUrls.Clear();
+                }
+
+                return;
+            }
+
+            lock (_intLock)
+            {
+                _overrideUrls.Clear();
+
+                foreach (var entry in overrides)
+                {
+                    var parts = entry.Split('=');
+                    if (parts.Length != 2)
+                    {
+                        _logger.LogError("Unable to parse bind override. {0}", entry);
+                    }
+                    else
+                    {
+                        var replacement = parts[1].Trim();
+                        if (string.Equals(parts[0], "remaining", StringComparison.OrdinalIgnoreCase))
+                        {
+                            _overrideUrls[new IPNetAddress(IPAddress.Broadcast)] = replacement;
+                        }
+                        else if (string.Equals(parts[0], "external", StringComparison.OrdinalIgnoreCase))
+                        {
+                            _overrideUrls[new IPNetAddress(IPAddress.Any)] = replacement;
+                        }
+                        else if (TryParseInterface(parts[0], out IPNetAddress address))
+                        {
+                            _overrideUrls[address] = replacement;
+                        }
+                        else
+                        {
+                            _logger.LogError("Unable to parse bind ip address. {0}", parts[1]);
+                        }
+                    }
+                }
+            }
+        }
+
+        private void InitialiseBind(ServerConfiguration config)
+        {
+            string[] ba = config.LocalNetworkAddresses;
+
+            // TODO: remove when bug fixed: https://github.com/jellyfin/jellyfin-web/issues/1334
+
+            if (ba.Length == 1 && ba[0].IndexOf(',', StringComparison.OrdinalIgnoreCase) != -1)
+            {
+                ba = ba[0].Split(',');
+            }
+
+            // TODO: end fix.
+
+            // Read and parse bind addresses and exclusions, removing ones that don't exist.
+            _bindAddresses = CreateIPCollection(ba).Union(_interfaceAddresses);
+            _bindExclusions = CreateIPCollection(ba, true).Union(_interfaceAddresses);
+            _logger.LogInformation("Using bind addresses: {0}", _bindAddresses);
+            _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions);
+        }
+
+        private void InitialiseRemote(ServerConfiguration config)
+        {
+            RemoteAddressFilter = CreateIPCollection(config.RemoteIPFilter);
+        }
+
+        /// <summary>
+        /// Initialises internal LAN cache settings.
+        /// </summary>
+        private void InitialiseLAN(ServerConfiguration config)
+        {
+            lock (_intLock)
+            {
+                _logger.LogDebug("Refreshing LAN information.");
+
+                // Get config options.
+                string[] subnets = config.LocalNetworkSubnets;
+
+                // Create lists from user settings.
+
+                _lanSubnets = CreateIPCollection(subnets);
+                _excludedSubnets = NetCollection.AsNetworks(CreateIPCollection(subnets, true));
+
+                // If no LAN addresses are specified - all private subnets are deemed to be the LAN
+                _usingPrivateAddresses = _lanSubnets.Count == 0;
+
+                // NOTE: The order of the commands in this statement matters.
+                if (_usingPrivateAddresses)
+                {
+                    _logger.LogDebug("Using LAN interface addresses as user provided no LAN details.");
+                    // Internal interfaces must be private and not excluded.
+                    _internalInterfaces = new NetCollection(_interfaceAddresses.Where(i => IsPrivateAddressRange(i) && !_excludedSubnets.Contains(i)));
+
+                    // Subnets are the same as the calculated internal interface.
+                    _lanSubnets = new NetCollection();
+
+                    // We must listen on loopback for LiveTV to function regardless of the settings.
+                    if (IsIP6Enabled)
+                    {
+                        _lanSubnets.Add(IPNetAddress.IP6Loopback);
+                        _lanSubnets.Add(IPNetAddress.Parse("fc00::/7")); // ULA
+                        _lanSubnets.Add(IPNetAddress.Parse("fe80::/10")); // Site local
+                    }
+
+                    if (IsIP4Enabled)
+                    {
+                        _lanSubnets.Add(IPNetAddress.IP4Loopback);
+                        _lanSubnets.Add(IPNetAddress.Parse("10.0.0.0/8"));
+                        _lanSubnets.Add(IPNetAddress.Parse("172.16.0.0/12"));
+                        _lanSubnets.Add(IPNetAddress.Parse("192.168.0.0/16"));
+                    }
+                }
+                else
+                {
+                    // We must listen on loopback for LiveTV to function regardless of the settings.
+                    if (IsIP6Enabled)
+                    {
+                        _lanSubnets.Add(IPNetAddress.IP6Loopback);
+                    }
+
+                    if (IsIP4Enabled)
+                    {
+                        _lanSubnets.Add(IPNetAddress.IP4Loopback);
+                    }
+
+                    // Internal interfaces must be private, not excluded and part of the LocalNetworkSubnet.
+                    _internalInterfaces = new NetCollection(_interfaceAddresses.Where(i => IsInLocalNetwork(i) && !_excludedSubnets.Contains(i) && _lanSubnets.Contains(i)));
+                }
+
+                _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets);
+                _logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets);
+                _logger.LogInformation("Using LAN addresses: {0}", NetCollection.AsNetworks(_lanSubnets.Exclude(_excludedSubnets)));
+            }
+        }
+
+        /// <summary>
+        /// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state.
+        /// Generate a list of all active mac addresses that aren't loopback addreses.
+        /// </summary>
+        private void InitialiseInterfaces()
+        {
+            lock (_intLock)
+            {
+                _logger.LogDebug("Refreshing interfaces.");
+
+                _interfaceNames.Clear();
+                _interfaceAddresses.Clear();
+
+                try
+                {
+                    IEnumerable<NetworkInterface> nics = NetworkInterface.GetAllNetworkInterfaces()
+                        .Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up);
+
+                    foreach (NetworkInterface adapter in nics)
+                    {
+                        try
+                        {
+                            IPInterfaceProperties ipProperties = adapter.GetIPProperties();
+                            PhysicalAddress mac = adapter.GetPhysicalAddress();
+
+                            // populate mac list
+                            if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && mac != null && mac != PhysicalAddress.None)
+                            {
+                                _macAddresses.Add(mac);
+                            }
+
+                            // populate interface address list
+                            foreach (UnicastIPAddressInformation info in ipProperties.UnicastAddresses)
+                            {
+                                if (IsIP4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork)
+                                {
+                                    IPNetAddress nw = new IPNetAddress(info.Address, info.IPv4Mask)
+                                    {
+                                        // Keep the number of gateways on this interface, along with its index.
+                                        Tag = ipProperties.GetIPv4Properties().Index
+                                    };
+
+                                    int tag = nw.Tag;
+                                    /* Mono on OSX doesn't give any gateway addresses, so check DNS entries */
+                                    if ((ipProperties.GatewayAddresses.Count > 0 || ipProperties.DnsAddresses.Count > 0) && !nw.IsLoopback())
+                                    {
+                                        // -ve Tags signify the interface has a gateway.
+                                        nw.Tag *= -1;
+                                    }
+
+                                    _interfaceAddresses.Add(nw);
+
+                                    // Store interface name so we can use the name in Collections.
+                                    _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag;
+                                    _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag;
+                                }
+                                else if (IsIP6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6)
+                                {
+                                    IPNetAddress nw = new IPNetAddress(info.Address, (byte)info.PrefixLength)
+                                    {
+                                        // Keep the number of gateways on this interface, along with its index.
+                                        Tag = ipProperties.GetIPv6Properties().Index
+                                    };
+
+                                    int tag = nw.Tag;
+                                    /* Mono on OSX doesn't give any gateway addresses, so check DNS entries */
+                                    if ((ipProperties.GatewayAddresses.Count > 0 || ipProperties.DnsAddresses.Count > 0) && !nw.IsLoopback())
+                                    {
+                                        // -ve Tags signify the interface has a gateway.
+                                        nw.Tag *= -1;
+                                    }
+
+                                    _interfaceAddresses.Add(nw);
+
+                                    // Store interface name so we can use the name in Collections.
+                                    _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag;
+                                    _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag;
+                                }
+                            }
+                        }
+#pragma warning disable CA1031 // Do not catch general exception types
+                        catch
+                        {
+                            // Ignore error, and attempt to continue.
+                        }
+#pragma warning restore CA1031 // Do not catch general exception types
+                    }
+
+                    _logger.LogDebug("Discovered {0} interfaces.", _interfaceAddresses.Count);
+                    _logger.LogDebug("Interfaces addresses : {0}", _interfaceAddresses);
+
+                    // If for some reason we don't have an interface info, resolve our DNS name.
+                    if (_interfaceAddresses.Count == 0)
+                    {
+                        _logger.LogWarning("No interfaces information available. Using loopback.");
+
+                        IPHost host = new IPHost(Dns.GetHostName());
+                        foreach (var a in host.GetAddresses())
+                        {
+                            _interfaceAddresses.Add(a);
+                        }
+
+                        if (_interfaceAddresses.Count == 0)
+                        {
+                            _logger.LogError("No interfaces information available. Resolving DNS name.");
+                            // Last ditch attempt - use loopback address.
+                            _interfaceAddresses.Add(IPNetAddress.IP4Loopback);
+                            if (IsIP6Enabled)
+                            {
+                                _interfaceAddresses.Add(IPNetAddress.IP6Loopback);
+                            }
+                        }
+                    }
+                }
+                catch (NetworkInformationException ex)
+                {
+                    _logger.LogError(ex, "Error in InitialiseInterfaces.");
+                }
+            }
+        }
+    }
+}

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

@@ -36,6 +36,7 @@
 
   <ItemGroup>
     <ProjectReference Include="..\Jellyfin.Data\Jellyfin.Data.csproj" />
+    <ProjectReference Include="..\Jellyfin.Networking\Jellyfin.Networking.csproj" />
     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
     <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
   </ItemGroup>

+ 2 - 2
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -1,4 +1,4 @@
-#nullable enable
+#nullable enable
 #pragma warning disable CA1307
 
 using System;
@@ -12,10 +12,10 @@ using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Events;
 using Jellyfin.Data.Events.Users;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Cryptography;
 using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Events;

+ 1 - 3
Jellyfin.Server/CoreAppHost.cs

@@ -5,6 +5,7 @@ using System.Reflection;
 using Emby.Drawing;
 using Emby.Server.Implementations;
 using Jellyfin.Drawing.Skia;
+using Jellyfin.Networking.Manager;
 using Jellyfin.Server.Implementations;
 using Jellyfin.Server.Implementations.Activity;
 using Jellyfin.Server.Implementations.Events;
@@ -34,21 +35,18 @@ namespace Jellyfin.Server
         /// <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="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>
         public CoreAppHost(
             IServerApplicationPaths applicationPaths,
             ILoggerFactory loggerFactory,
             IStartupOptions options,
             IFileSystem fileSystem,
-            INetworkManager networkManager,
             IServiceCollection collection)
             : base(
                 applicationPaths,
                 loggerFactory,
                 options,
                 fileSystem,
-                networkManager,
                 collection)
         {
         }

+ 19 - 18
Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs

@@ -1,9 +1,11 @@
 using System.Linq;
 using System.Threading.Tasks;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using Microsoft.AspNetCore.Http;
+using NetworkCollection;
 
 namespace Jellyfin.Server.Middleware
 {
@@ -38,36 +40,35 @@ namespace Jellyfin.Server.Middleware
                 return;
             }
 
-            var remoteIp = httpContext.GetNormalizedRemoteIp();
+            var remoteIp = httpContext.Connection.RemoteIpAddress;
 
             if (serverConfigurationManager.Configuration.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.
+                NetCollection 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.Contains(remoteIp);
+                    if (!serverConfigurationManager.Configuration.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);

+ 3 - 21
Jellyfin.Server/Middleware/LanFilteringMiddleware.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Linq;
 using System.Threading.Tasks;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using Microsoft.AspNetCore.Http;
@@ -32,32 +33,13 @@ namespace Jellyfin.Server.Middleware
         /// <returns>The async task.</returns>
         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;
 
-            if (hosts.Count == 0)
+            if (!networkManager.IsInLocalNetwork(host) && !serverConfigurationManager.Configuration.EnableRemoteAccess)
             {
-                await _next(httpContext).ConfigureAwait(false);
                 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);
         }
 

+ 15 - 52
Jellyfin.Server/Program.cs

@@ -12,8 +12,8 @@ using System.Threading.Tasks;
 using CommandLine;
 using Emby.Server.Implementations;
 using Emby.Server.Implementations.IO;
-using Emby.Server.Implementations.Networking;
 using Jellyfin.Api.Controllers;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Extensions;
 using Microsoft.AspNetCore.Hosting;
@@ -24,6 +24,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Abstractions;
+using NetworkCollection;
 using Serilog;
 using Serilog.Extensions.Logging;
 using SQLitePCL;
@@ -161,7 +162,6 @@ namespace Jellyfin.Server
                 _loggerFactory,
                 options,
                 new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
-                new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()),
                 serviceCollection);
 
             try
@@ -272,57 +272,16 @@ namespace Jellyfin.Server
             return builder
                 .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);
-                        }
+                    NetCollection addresses = NetworkManager.Instance.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);
-                                    listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
-                                });
-                            }
-                            else if (builderContext.HostingEnvironment.IsDevelopment())
-                            {
-                                try
-                                {
-                                    options.Listen(address, appHost.HttpsPort, listenOptions =>
-                                    {
-                                        listenOptions.UseHttps();
-                                        listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
-                                    });
-                                }
-                                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)
                         {
-                            options.ListenAnyIP(appHost.HttpsPort, listenOptions =>
+                            options.Listen(netAdd.Address, appHost.HttpsPort, listenOptions =>
                             {
                                 listenOptions.UseHttps(appHost.Certificate);
                                 listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
@@ -332,15 +291,19 @@ namespace Jellyfin.Server
                         {
                             try
                             {
-                                options.ListenAnyIP(appHost.HttpsPort, listenOptions =>
+                                options.Listen(netAdd.Address, appHost.HttpsPort, listenOptions =>
                                 {
                                     listenOptions.UseHttps();
                                     listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
                                 });
                             }
-                            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;
+                                }
                             }
                         }
                     }

+ 0 - 97
MediaBrowser.Common/Net/INetworkManager.cs

@@ -1,97 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Net;
-using System.Net.NetworkInformation;
-
-namespace MediaBrowser.Common.Net
-{
-    public interface INetworkManager
-    {
-        event EventHandler NetworkChanged;
-
-        /// <summary>
-        /// Gets or sets a function to return the list of user defined LAN addresses.
-        /// </summary>
-        Func<string[]> LocalSubnetsFn { get; set; }
-
-        /// <summary>
-        /// Gets a random port TCP number that is currently available.
-        /// </summary>
-        /// <returns>System.Int32.</returns>
-        int GetRandomUnusedTcpPort();
-
-        /// <summary>
-        /// Gets a random port UDP number that is currently available.
-        /// </summary>
-        /// <returns>System.Int32.</returns>
-        int GetRandomUnusedUdpPort();
-
-        /// <summary>
-        /// Returns the MAC Address from first Network Card in Computer.
-        /// </summary>
-        /// <returns>The MAC Address.</returns>
-        List<PhysicalAddress> GetMacAddresses();
-
-        /// <summary>
-        /// Determines whether [is in private address space] [the specified endpoint].
-        /// </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);
-
-        /// <summary>
-        /// Determines whether [is in private address space 10.x.x.x] [the specified endpoint] and exists in the subnets returned by GetSubnets().
-        /// </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);
-
-        /// <summary>
-        /// Determines whether [is in local network] [the specified endpoint].
-        /// </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);
-
-        /// <summary>
-        /// Investigates an caches a list of interface addresses, excluding local link and LAN excluded addresses.
-        /// </summary>
-        /// <returns>The list of ipaddresses.</returns>
-        IPAddress[] GetLocalIpAddresses();
-
-        /// <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.
-        /// </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);
-
-        /// <summary>
-        /// Returns true if address is in the LAN list in the config file.
-        /// </summary>
-        /// <param name="address">The address to check.</param>
-        /// <param name="excludeInterfaces">If true, check against addresses in the LAN settings which have [] arroud 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);
-
-        /// <summary>
-        /// Checks if address is in the LAN list in the config file.
-        /// </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);
-
-        /// <summary>
-        /// Returns the subnet mask of an interface with the given address.
-        /// </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);
-    }
-}

+ 8 - 22
MediaBrowser.Controller/IServerApplicationHost.cs

@@ -56,41 +56,27 @@ namespace MediaBrowser.Controller
         /// <summary>
         /// Gets the system info.
         /// </summary>
+        /// <param name="source">The originator of the request.</param>
         /// <returns>SystemInfo.</returns>
-        Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken);
+        SystemInfo GetSystemInfo(IPAddress source);
 
-        Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken);
-
-        /// <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.
-        /// </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);
+        PublicSystemInfo GetPublicSystemInfo(IPAddress address);
 
         /// <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.
+        /// HTTPS will be preferred when available.
         /// </summary>
-        /// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
+        /// <param name="source">The source of the request.</param>
         /// <returns>The server URL.</returns>
-        Task<string> GetLocalApiUrl(CancellationToken cancellationToken);
+        string GetSmartApiUrl(object source);
 
         /// <summary>
-        /// Gets a localhost URL that can be used to access the API using the loop-back IP address (127.0.0.1)
+        /// Gets a localhost URL that can be used to access the API using the loop-back IP address.
         /// over HTTP (not HTTPS).
         /// </summary>
         /// <returns>The API URL.</returns>
         string GetLoopbackHttpApiUrl();
 
-        /// <summary>
-        /// Gets a local (LAN) URL that can be used to access the API. HTTPS will be preferred when available.
-        /// </summary>
-        /// <param name="address">The IP address to use as the hostname in the URL.</param>
-        /// <returns>The API URL.</returns>
-        string GetLocalApiUrl(IPAddress address);
-
         /// <summary>
         /// Gets a local (LAN) URL that can be used to access the API.
         /// Note: if passing non-null scheme or port it is up to the caller to ensure they form the correct pair.
@@ -105,7 +91,7 @@ namespace MediaBrowser.Controller
         /// preferring the HTTPS port, if available.
         /// </param>
         /// <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>
         /// Open a URL in an external browser window.

+ 19 - 0
MediaBrowser.Model/Configuration/PathSubstitution.cs

@@ -0,0 +1,19 @@
+#nullable enable
+#pragma warning disable CS1591
+#pragma warning disable CA1819
+
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Updates;
+
+namespace MediaBrowser.Model.Configuration
+{
+
+    public class PathSubstitution
+    {
+        public string From { get; set; } = string.Empty;
+
+        public string To { get; set; } = string.Empty;
+    }
+}

+ 208 - 180
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -1,5 +1,6 @@
 #nullable disable
 #pragma warning disable CS1591
+#pragma warning disable CA1819
 
 using System;
 using System.Collections.Generic;
@@ -15,41 +16,174 @@ namespace MediaBrowser.Model.Configuration
     {
         public const int DefaultHttpPort = 8096;
         public const int DefaultHttpsPort = 8920;
-        private string _baseUrl;
+        private string _baseUrl = string.Empty;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
+        /// </summary>
+        public ServerConfiguration()
+        {
+            MetadataOptions = new[]
+            {
+                new MetadataOptions()
+                {
+                    ItemType = "Book"
+                },
+                new MetadataOptions()
+                {
+                    ItemType = "Movie"
+                },
+                new MetadataOptions
+                {
+                    ItemType = "MusicVideo",
+                    DisabledMetadataFetchers = new[] { "The Open Movie Database" },
+                    DisabledImageFetchers = new[] { "The Open Movie Database" }
+                },
+                new MetadataOptions
+                {
+                    ItemType = "Series",
+                    DisabledMetadataFetchers = new[] { "TheMovieDb" },
+                    DisabledImageFetchers = new[] { "TheMovieDb" }
+                },
+                new MetadataOptions
+                {
+                    ItemType = "MusicAlbum",
+                    DisabledMetadataFetchers = new[] { "TheAudioDB" }
+                },
+                new MetadataOptions
+                {
+                    ItemType = "MusicArtist",
+                    DisabledMetadataFetchers = new[] { "TheAudioDB" }
+                },
+                new MetadataOptions
+                {
+                    ItemType = "BoxSet"
+                },
+                new MetadataOptions
+                {
+                    ItemType = "Season",
+                    DisabledMetadataFetchers = new[] { "TheMovieDb" },
+                },
+                new MetadataOptions
+                {
+                    ItemType = "Episode",
+                    DisabledMetadataFetchers = new[] { "The Open Movie Database", "TheMovieDb" },
+                    DisabledImageFetchers = new[] { "The Open Movie Database", "TheMovieDb" }
+                }
+            };
+        }
 
         /// <summary>
         /// Gets or sets a value indicating whether to enable automatic port forwarding.
         /// </summary>
-        public bool EnableUPnP { get; set; }
+        public bool EnableUPnP { get; set; } = false;
 
         /// <summary>
         /// Gets or sets a value indicating whether to enable prometheus metrics exporting.
         /// </summary>
-        public bool EnableMetrics { get; set; }
+        public bool EnableMetrics { get; set; } = false;
 
         /// <summary>
         /// Gets or sets the public mapped port.
         /// </summary>
         /// <value>The public mapped port.</value>
-        public int PublicPort { get; set; }
+        public int PublicPort { get; set; } = DefaultHttpPort;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the http port should be mapped as part of UPnP automatic port forwarding.
+        /// </summary>
+        public bool UPnPCreateHttpPortMap { get; set; } = false;
+
+        /// <summary>
+        /// Gets or sets client udp port range.
+        /// </summary>
+        public string UDPPortRange { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether gets or sets IPV6 capability.
+        /// </summary>
+        public bool EnableIPV6 { get; set; } = false;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether gets or sets IPV4 capability.
+        /// </summary>
+        public bool EnableIPV4 { get; set; } = true;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether detailed ssdp logs are sent to the console/log.
+        /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work.
+        /// </summary>
+        public bool EnableSSDPTracing { get; set; } = false;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether an IP address is to be used to filter the detailed ssdp logs that are being sent to the console/log.
+        /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work.
+        /// </summary>
+        public string SSDPTracingFilter { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Gets or sets the number of times SSDP UDP messages are sent.
+        /// </summary>
+        public int UDPSendCount { get; set; } = 2;
+
+        /// <summary>
+        /// Gets or sets the delay between each groups of SSDP messages (in ms).
+        /// </summary>
+        public int UDPSendDelay { get; set; } = 100;
+
+        /// <summary>
+        /// Gets or sets the time (in seconds) between the pings of SSDP gateway monitor.
+        /// </summary>
+        public int GatewayMonitorPeriod { get; set; } = 60;
+
+        /// <summary>
+        /// Gets a value indicating whether is multi-socket binding available.
+        /// </summary>
+        public bool EnableMultiSocketBinding { get; } = true;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether all IPv6 interfaces should be treated as on the internal network.
+        /// Depending on the address range implemented ULA ranges might not be used.
+        /// </summary>
+        public bool TrustAllIP6Interfaces { get; set; } = false;
+
+        /// <summary>
+        /// Gets or sets the ports that HDHomerun uses.
+        /// </summary>
+        public string HDHomerunPortRange { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Gets or sets PublishedServerUri to advertise for specific subnets.
+        /// </summary>
+        public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>();
+
+        /// <summary>
+        /// Gets or sets a value indicating whether gets or sets Autodiscovery tracing.
+        /// </summary>
+        public bool AutoDiscoveryTracing { get; set; } = false;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether Autodiscovery is enabled.
+        /// </summary>
+        public bool AutoDiscovery { get; set; } = true;
 
         /// <summary>
         /// Gets or sets the public HTTPS port.
         /// </summary>
         /// <value>The public HTTPS port.</value>
-        public int PublicHttpsPort { get; set; }
+        public int PublicHttpsPort { get; set; } = DefaultHttpsPort;
 
         /// <summary>
         /// Gets or sets the HTTP server port number.
         /// </summary>
         /// <value>The HTTP server port number.</value>
-        public int HttpServerPortNumber { get; set; }
+        public int HttpServerPortNumber { get; set; } = DefaultHttpPort;
 
         /// <summary>
         /// Gets or sets the HTTPS server port number.
         /// </summary>
         /// <value>The HTTPS server port number.</value>
-        public int HttpsPortNumber { get; set; }
+        public int HttpsPortNumber { get; set; } = DefaultHttpsPort;
 
         /// <summary>
         /// Gets or sets a value indicating whether to use HTTPS.
@@ -58,19 +192,19 @@ namespace MediaBrowser.Model.Configuration
         /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be
         /// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>.
         /// </remarks>
-        public bool EnableHttps { get; set; }
+        public bool EnableHttps { get; set; } = false;
 
-        public bool EnableNormalizedItemByNameIds { get; set; }
+        public bool EnableNormalizedItemByNameIds { get; set; } = true;
 
         /// <summary>
         /// Gets or sets the filesystem path of an X.509 certificate to use for SSL.
         /// </summary>
-        public string CertificatePath { get; set; }
+        public string CertificatePath { get; set; } = string.Empty;
 
         /// <summary>
         /// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>.
         /// </summary>
-        public string CertificatePassword { get; set; }
+        public string CertificatePassword { get; set; } = string.Empty;
 
         /// <summary>
         /// Gets or sets a value indicating whether this instance is port authorized.
@@ -79,92 +213,95 @@ namespace MediaBrowser.Model.Configuration
         public bool IsPortAuthorized { get; set; }
 
         /// <summary>
-        /// Gets or sets if 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>
-        public bool QuickConnectAvailable { get; set; }
+        public bool QuickConnectAvailable { get; set; } = false;
 
-        public bool AutoRunWebApp { get; set; }
+        public bool AutoRunWebApp { get; set; } = true;
 
-        public bool EnableRemoteAccess { get; set; }
+        /// <summary>
+        /// Gets or sets a value indicating whether access outside of the LAN is permitted.
+        /// </summary>
+        public bool EnableRemoteAccess { get; set; } = true;
 
         /// <summary>
         /// Gets or sets a value indicating whether [enable case sensitive item ids].
         /// </summary>
         /// <value><c>true</c> if [enable case sensitive item ids]; otherwise, <c>false</c>.</value>
-        public bool EnableCaseSensitiveItemIds { get; set; }
+        public bool EnableCaseSensitiveItemIds { get; set; } = true;
 
-        public bool DisableLiveTvChannelUserDataName { get; set; }
+        public bool DisableLiveTvChannelUserDataName { get; set; } = true;
 
         /// <summary>
         /// Gets or sets the metadata path.
         /// </summary>
         /// <value>The metadata path.</value>
-        public string MetadataPath { get; set; }
+        public string MetadataPath { get; set; } = string.Empty;
 
-        public string MetadataNetworkPath { get; set; }
+        public string MetadataNetworkPath { get; set; } = string.Empty;
 
         /// <summary>
         /// Gets or sets the preferred metadata language.
         /// </summary>
         /// <value>The preferred metadata language.</value>
-        public string PreferredMetadataLanguage { get; set; }
+        public string PreferredMetadataLanguage { get; set; } = string.Empty;
 
         /// <summary>
         /// Gets or sets the metadata country code.
         /// </summary>
         /// <value>The metadata country code.</value>
-        public string MetadataCountryCode { get; set; }
+        public string MetadataCountryCode { get; set; } = "US";
 
         /// <summary>
-        /// Characters to be replaced with a ' ' in strings to create a sort name.
+        /// Gets or sets characters to be replaced with a ' ' in strings to create a sort name.
         /// </summary>
         /// <value>The sort replace characters.</value>
-        public string[] SortReplaceCharacters { get; set; }
+        public string[] SortReplaceCharacters { get; set; } = new[] { ".", "+", "%" };
 
         /// <summary>
-        /// Characters to be removed from strings to create a sort name.
+        /// Gets or sets characters to be removed from strings to create a sort name.
         /// </summary>
         /// <value>The sort remove characters.</value>
-        public string[] SortRemoveCharacters { get; set; }
+        public string[] SortRemoveCharacters { get; set; } = new[] { ",", "&", "-", "{", "}", "'" };
 
         /// <summary>
-        /// Words to be removed from strings to create a sort name.
+        /// Gets or sets words to be removed from strings to create a sort name.
         /// </summary>
         /// <value>The sort remove words.</value>
-        public string[] SortRemoveWords { get; set; }
+        public string[] SortRemoveWords { get; set; } = new[] { "the", "a", "an" };
 
         /// <summary>
         /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated.
         /// </summary>
         /// <value>The min resume PCT.</value>
-        public int MinResumePct { get; set; }
+        public int MinResumePct { get; set; } = 5;
 
         /// <summary>
         /// Gets or sets the maximum percentage of an item that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.
         /// </summary>
         /// <value>The max resume PCT.</value>
-        public int MaxResumePct { get; set; }
+        public int MaxResumePct { get; set; } = 90;
 
         /// <summary>
         /// Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates..
         /// </summary>
         /// <value>The min resume duration seconds.</value>
-        public int MinResumeDurationSeconds { get; set; }
+        public int MinResumeDurationSeconds { get; set; } = 300;
 
         /// <summary>
-        /// The delay in seconds that we will wait after a file system change to try and discover what has been added/removed
+        /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed
         /// Some delay is necessary with some items because their creation is not atomic.  It involves the creation of several
         /// different directories and files.
         /// </summary>
         /// <value>The file watcher delay.</value>
-        public int LibraryMonitorDelay { get; set; }
+        public int LibraryMonitorDelay { get; set; } = 60;
 
         /// <summary>
         /// Gets or sets a value indicating whether [enable dashboard response caching].
         /// Allows potential contributors without visual studio to modify production dashboard code and test changes.
         /// </summary>
         /// <value><c>true</c> if [enable dashboard response caching]; otherwise, <c>false</c>.</value>
-        public bool EnableDashboardResponseCaching { get; set; }
+        public bool EnableDashboardResponseCaching { get; set; } = true;
 
         /// <summary>
         /// Gets or sets the image saving convention.
@@ -174,9 +311,9 @@ namespace MediaBrowser.Model.Configuration
 
         public MetadataOptions[] MetadataOptions { get; set; }
 
-        public bool SkipDeserializationForBasicTypes { get; set; }
+        public bool SkipDeserializationForBasicTypes { get; set; } = true;
 
-        public string ServerName { get; set; }
+        public string ServerName { get; set; } = string.Empty;
 
         public string BaseUrl
         {
@@ -208,189 +345,80 @@ namespace MediaBrowser.Model.Configuration
             }
         }
 
-        public string UICulture { get; set; }
-
-        public bool SaveMetadataHidden { get; set; }
+        public string UICulture { get; set; } = "en-US";
 
-        public NameValuePair[] ContentTypes { get; set; }
+        public bool SaveMetadataHidden { get; set; } = false;
 
-        public int RemoteClientBitrateLimit { get; set; }
+        public NameValuePair[] ContentTypes { get; set; } = Array.Empty<NameValuePair>();
 
-        public bool EnableFolderView { get; set; }
+        public int RemoteClientBitrateLimit { get; set; } = 0;
 
-        public bool EnableGroupingIntoCollections { get; set; }
+        public bool EnableFolderView { get; set; } = false;
 
-        public bool DisplaySpecialsWithinSeasons { get; set; }
+        public bool EnableGroupingIntoCollections { get; set; } = false;
 
-        public string[] LocalNetworkSubnets { get; set; }
+        public bool DisplaySpecialsWithinSeasons { get; set; } = true;
 
-        public string[] LocalNetworkAddresses { get; set; }
+        /// <summary>
+        /// Gets or sets the subnets that are deemed to make up the LAN.
+        /// </summary>
+        public string[] LocalNetworkSubnets { get; set; } = Array.Empty<string>();
 
-        public string[] CodecsUsed { get; set; }
+        /// <summary>
+        /// Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used.
+        /// </summary>
+        public string[] LocalNetworkAddresses { get; set; } = Array.Empty<string>();
 
-        public List<RepositoryInfo> PluginRepositories { get; set; }
+        public string[] CodecsUsed { get; set; } = Array.Empty<string>();
 
-        public bool IgnoreVirtualInterfaces { get; set; }
+        public List<RepositoryInfo> PluginRepositories { get; set; } = new List<RepositoryInfo>();
 
-        public bool EnableExternalContentInSuggestions { get; set; }
+        public bool EnableExternalContentInSuggestions { get; set; } = true;
 
         /// <summary>
         /// Gets or sets a value indicating whether the server should force connections over HTTPS.
         /// </summary>
-        public bool RequireHttps { get; set; }
+        public bool RequireHttps { get; set; } = false;
 
-        public bool EnableNewOmdbSupport { get; set; }
+        public bool EnableNewOmdbSupport { get; set; } = true;
 
-        public string[] RemoteIPFilter { get; set; }
+        /// <summary>
+        /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with <seealso cref="IsRemoteIPFilterBlacklist"/>.
+        /// </summary>
+        public string[] RemoteIPFilter { get; set; } = Array.Empty<string>();
 
-        public bool IsRemoteIPFilterBlacklist { get; set; }
+        /// <summary>
+        /// Gets or sets a value indicating whether <seealso cref="RemoteIPFilter"/> contains a blacklist or a whitelist. Default is a whitelist.
+        /// </summary>
+        public bool IsRemoteIPFilterBlacklist { get; set; } = false;
 
-        public int ImageExtractionTimeoutMs { get; set; }
+        public int ImageExtractionTimeoutMs { get; set; } = 0;
 
-        public PathSubstitution[] PathSubstitutions { get; set; }
+        public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty<PathSubstitution>();
 
-        public bool EnableSimpleArtistDetection { get; set; }
+        public bool EnableSimpleArtistDetection { get; set; } = false;
 
-        public string[] UninstalledPlugins { get; set; }
+        public string[] UninstalledPlugins { get; set; } = Array.Empty<string>();
 
         /// <summary>
         /// Gets or sets a value indicating whether slow server responses should be logged as a warning.
         /// </summary>
-        public bool EnableSlowResponseWarning { get; set; }
+        public bool EnableSlowResponseWarning { get; set; } = true;
 
         /// <summary>
         /// Gets or sets the threshold for the slow response time warning in ms.
         /// </summary>
-        public long SlowResponseThresholdMs { get; set; }
+        public long SlowResponseThresholdMs { get; set; } = 500;
 
         /// <summary>
         /// Gets or sets the cors hosts.
         /// </summary>
-        public string[] CorsHosts { get; set; }
+        public string[] CorsHosts { get; set; } = new[] { "*" };
 
         /// <summary>
         /// Gets or sets the known proxies.
         /// </summary>
-        public string[] KnownProxies { get; set; }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
-        /// </summary>
-        public ServerConfiguration()
-        {
-            UninstalledPlugins = Array.Empty<string>();
-            RemoteIPFilter = Array.Empty<string>();
-            LocalNetworkSubnets = Array.Empty<string>();
-            LocalNetworkAddresses = Array.Empty<string>();
-            CodecsUsed = Array.Empty<string>();
-            PathSubstitutions = Array.Empty<PathSubstitution>();
-            IgnoreVirtualInterfaces = false;
-            EnableSimpleArtistDetection = false;
-            SkipDeserializationForBasicTypes = true;
-
-            PluginRepositories = new List<RepositoryInfo>();
-
-            DisplaySpecialsWithinSeasons = true;
-            EnableExternalContentInSuggestions = true;
-
-            ImageSavingConvention = ImageSavingConvention.Compatible;
-            PublicPort = DefaultHttpPort;
-            PublicHttpsPort = DefaultHttpsPort;
-            HttpServerPortNumber = DefaultHttpPort;
-            HttpsPortNumber = DefaultHttpsPort;
-            EnableMetrics = false;
-            EnableHttps = false;
-            EnableDashboardResponseCaching = true;
-            EnableCaseSensitiveItemIds = true;
-            EnableNormalizedItemByNameIds = true;
-            DisableLiveTvChannelUserDataName = true;
-            EnableNewOmdbSupport = true;
-
-            AutoRunWebApp = true;
-            EnableRemoteAccess = true;
-            QuickConnectAvailable = false;
-
-            EnableUPnP = false;
-            MinResumePct = 5;
-            MaxResumePct = 90;
-
-            // 5 minutes
-            MinResumeDurationSeconds = 300;
-
-            LibraryMonitorDelay = 60;
-
-            ContentTypes = Array.Empty<NameValuePair>();
-
-            PreferredMetadataLanguage = "en";
-            MetadataCountryCode = "US";
-
-            SortReplaceCharacters = new[] { ".", "+", "%" };
-            SortRemoveCharacters = new[] { ",", "&", "-", "{", "}", "'" };
-            SortRemoveWords = new[] { "the", "a", "an" };
-
-            BaseUrl = string.Empty;
-            UICulture = "en-US";
-
-            MetadataOptions = new[]
-            {
-                new MetadataOptions()
-                {
-                    ItemType = "Book"
-                },
-                new MetadataOptions()
-                {
-                    ItemType = "Movie"
-                },
-                new MetadataOptions
-                {
-                    ItemType = "MusicVideo",
-                    DisabledMetadataFetchers = new[] { "The Open Movie Database" },
-                    DisabledImageFetchers = new[] { "The Open Movie Database" }
-                },
-                new MetadataOptions
-                {
-                    ItemType = "Series",
-                    DisabledMetadataFetchers = new[] { "TheMovieDb" },
-                    DisabledImageFetchers = new[] { "TheMovieDb" }
-                },
-                new MetadataOptions
-                {
-                    ItemType = "MusicAlbum",
-                    DisabledMetadataFetchers = new[] { "TheAudioDB" }
-                },
-                new MetadataOptions
-                {
-                    ItemType = "MusicArtist",
-                    DisabledMetadataFetchers = new[] { "TheAudioDB" }
-                },
-                new MetadataOptions
-                {
-                    ItemType = "BoxSet"
-                },
-                new MetadataOptions
-                {
-                    ItemType = "Season",
-                    DisabledMetadataFetchers = new[] { "TheMovieDb" },
-                },
-                new MetadataOptions
-                {
-                    ItemType = "Episode",
-                    DisabledMetadataFetchers = new[] { "The Open Movie Database", "TheMovieDb" },
-                    DisabledImageFetchers = new[] { "The Open Movie Database", "TheMovieDb" }
-                }
-            };
-
-            EnableSlowResponseWarning = true;
-            SlowResponseThresholdMs = 500;
-            CorsHosts = new[] { "*" };
-            KnownProxies = Array.Empty<string>();
-        }
-    }
-
-    public class PathSubstitution
-    {
-        public string From { get; set; }
+        public string[] KnownProxies { get; set; } = Array.Empty<string>();
 
-        public string To { get; set; }
     }
 }

+ 20 - 14
MediaBrowser.sln

@@ -1,6 +1,6 @@
 Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.26730.3
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30503.244
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}"
 EndProject
@@ -66,12 +66,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Data", "Jellyfin.D
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
 		Release|Any CPU = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.Build.0 = Release|Any CPU
 		{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -132,10 +138,6 @@ Global
 		{960295EE-4AF4-4440-A525-B4C295B01A61}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.Build.0 = Release|Any CPU
-		{07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.Build.0 = Release|Any CPU
 		{154872D9-6C12-4007-96E3-8F70A58386CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{154872D9-6C12-4007-96E3-8F70A58386CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{154872D9-6C12-4007-96E3-8F70A58386CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -176,10 +178,22 @@ Global
 		{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.Build.0 = Release|Any CPU
+		{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Debug|Any CPU.ActiveCfg = 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.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
 	EndGlobalSection
+	GlobalSection(NestedProjects) = preSolution
+		{DF194677-DFD3-42AF-9F75-D44D5A416478} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+		{28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+		{3998657B-1CCC-49DD-A19F-275DC8495F57} = {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}
+		{462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}
 	EndGlobalSection
@@ -201,12 +215,4 @@ Global
 		$0.DotNetNamingPolicy = $2
 		$2.DirectoryNamespaceAssociation = PrefixedHierarchical
 	EndGlobalSection
-	GlobalSection(NestedProjects) = preSolution
-		{DF194677-DFD3-42AF-9F75-D44D5A416478} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
-		{28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
-		{3998657B-1CCC-49DD-A19F-275DC8495F57} = {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}
-		{462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
-	EndGlobalSection
 EndGlobal

+ 1 - 0
RSSDP/RSSDP.csproj

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

+ 3 - 2
RSSDP/SsdpCommunicationsServer.cs

@@ -7,6 +7,7 @@ using System.Net.Sockets;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Model.Net;
 using Microsoft.Extensions.Logging;
@@ -352,7 +353,7 @@ namespace Rssdp.Infrastructure
 
             if (_enableMultiSocketBinding)
             {
-                foreach (var address in _networkManager.GetLocalIpAddresses())
+                foreach (var address in _networkManager.GetAllBindInterfaces())
                 {
                     if (address.AddressFamily == AddressFamily.InterNetworkV6)
                     {
@@ -362,7 +363,7 @@ namespace Rssdp.Infrastructure
 
                     try
                     {
-                        sockets.Add(_SocketFactory.CreateSsdpUdpSocket(address, _LocalPort));
+                        sockets.Add(_SocketFactory.CreateSsdpUdpSocket(address.Address, _LocalPort));
                     }
                     catch (Exception ex)
                     {

+ 5 - 6
RSSDP/SsdpDevicePublisher.cs

@@ -5,7 +5,9 @@ using System.Linq;
 using System.Net;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Net;
+using NetworkCollection;
 
 namespace Rssdp.Infrastructure
 {
@@ -300,17 +302,14 @@ namespace Rssdp.Infrastructure
 
                     foreach (var device in deviceList)
                     {
-                        if (!_sendOnlyMatchedHost ||
-                            _networkManager.IsInSameSubnet(device.ToRootDevice().Address, remoteEndPoint.Address, device.ToRootDevice().SubnetMask))
+                        var ip1 = new IPNetAddress(device.ToRootDevice().Address, device.ToRootDevice().SubnetMask);
+                        var ip2 = new IPNetAddress(remoteEndPoint.Address, device.ToRootDevice().SubnetMask);
+                        if (!_sendOnlyMatchedHost || ip1.NetworkAddress.Equals(ip2.NetworkAddress))
                         {
                             SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken);
                         }
                     }
                 }
-                else
-                {
-                    // WriteTrace(String.Format("Sending 0 search responses."));
-                }
             });
         }
 

+ 2 - 1
tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs

@@ -1,9 +1,10 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.Threading.Tasks;
 using AutoFixture;
 using AutoFixture.AutoMoq;
 using Jellyfin.Api.Auth.LocalAccessPolicy;
 using Jellyfin.Api.Constants;
+using Jellyfin.Networking.Manager;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Library;

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

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