Quellcode durchsuchen

Networking: 1 - Network Manager (#4124)

* NetworkManager

* Config file with additional options.

* Update Jellyfin.Networking/Manager/INetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update Jellyfin.Networking/Manager/INetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update MediaBrowser.Model/Configuration/ServerConfiguration.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update MediaBrowser.Model/Configuration/ServerConfiguration.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update MediaBrowser.Model/Configuration/ServerConfiguration.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Split function.

* Update Jellyfin.Networking/Manager/INetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* fixed iterations

* Update Jellyfin.Networking.csproj

* Update NetworkManager.cs

* Updated to NetCollection 1.03.

* Update ServerConfiguration.cs

* Update StartupController.cs

* Update INetworkManager.cs

Removed public

* Update INetworkManager.cs

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Claus Vium <cvium@users.noreply.github.com>

* Updated comment

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Claus Vium <cvium@users.noreply.github.com>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Claus Vium <cvium@users.noreply.github.com>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Claus Vium <cvium@users.noreply.github.com>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Claus Vium <cvium@users.noreply.github.com>

* Update Jellyfin.Networking/Manager/INetworkManager.cs

Co-authored-by: Claus Vium <cvium@users.noreply.github.com>

* Remove mono code.
Removed forced chromecast option

* Inverted if

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Moved config into a separate container

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Changed sortedList to dictionary.

* Update INetworkManager.cs

Changed UpdateSettings param type

* Update NetworkManager.cs

* Update NetworkManager.cs

* Update NetworkManager.cs

* Update NetworkConfiguration.cs

* Update INetworkManager.cs

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Claus Vium <cvium@users.noreply.github.com>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Claus Vium <cvium@users.noreply.github.com>

* Update MediaBrowser.Model/Configuration/ServerConfiguration.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update MediaBrowser.Model/Configuration/ServerConfiguration.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Updated changes github didn't update.

* Fixed compilation.

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Removed read locking.

* Update NetworkManager.cs

Changed configuration event to NamedConfigurationUpdated

* updated comment

* removed whitespace

* Updated NetCollection to 1.0.6
Updated DXCopAnalyser to 3.3.1

* removed NetCollection

* Update NetworkManager.cs

* Update NetworkExtensions.cs

* Update NetworkExtensions.cs

Removed function.

* Update NetworkExtensions.cs

* Update NetworkManager.cs

Changed ToString() to AsString() as native collection formats incorrectly.

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update NetworkExtensions.cs

* Update Jellyfin.Networking/Configuration/NetworkConfiguration.cs

Co-authored-by: h1dden-da3m0n <33120068+h1dden-da3m0n@users.noreply.github.com>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: h1dden-da3m0n <33120068+h1dden-da3m0n@users.noreply.github.com>

* Update Jellyfin.Networking/Configuration/NetworkConfiguration.cs

Co-authored-by: h1dden-da3m0n <33120068+h1dden-da3m0n@users.noreply.github.com>

* Update Jellyfin.Networking/Configuration/NetworkConfiguration.cs

Co-authored-by: h1dden-da3m0n <33120068+h1dden-da3m0n@users.noreply.github.com>

* Update MediaBrowser.Common/Net/IPObject.cs

Co-authored-by: h1dden-da3m0n <33120068+h1dden-da3m0n@users.noreply.github.com>

* updated

* Replaced NetCollection with Collection<IPObject>

* Update MediaBrowser.Common/Net/NetworkExtensions.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update MediaBrowser.Model/Configuration/PathSubstitution.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update MediaBrowser.Common/Net/NetworkExtensions.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update MediaBrowser.Common/Net/IPObject.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update MediaBrowser.Common/Net/IPObject.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update MediaBrowser.Common/Net/IPObject.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update MediaBrowser.Common/Net/IPHost.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update MediaBrowser.Common/Net/IPHost.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update MediaBrowser.Common/Net/IPHost.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update MediaBrowser.Common/Net/IPHost.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update MediaBrowser.Common/Net/IPHost.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update MediaBrowser.Common/Net/IPHost.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update MediaBrowser.Common/Net/IPHost.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update MediaBrowser.Common/Net/IPObject.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* Update MediaBrowser.Common/Net/IPObject.cs

Co-authored-by: Cody Robibero <cody@robibe.ro>

* updated comments.

* Updated comments / changes as suggested by @crobibero.

* Split function as suggested

* Fixed null ref.

* Updated comment.

* Updated cs to .net5

* Fixed issue with PublishedServerUrl

* Fixes

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Claus Vium <cvium@users.noreply.github.com>

* Restored locking

* optimisation

* Added comment

* updates.

* updated.

* updates

* updated.

* Update IPHost.cs

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Claus Vium <cvium@users.noreply.github.com>

* Update NetworkManager.cs

* Removed whitespace.

* Added debug logging

* Added debug.

* Update Jellyfin.Networking/Manager/NetworkManager.cs

Co-authored-by: Bond-009 <bond.009@outlook.com>

* Replaced GetAddressBytes

Co-authored-by: Cody Robibero <cody@robibe.ro>
Co-authored-by: Claus Vium <cvium@users.noreply.github.com>
Co-authored-by: h1dden-da3m0n <33120068+h1dden-da3m0n@users.noreply.github.com>
Co-authored-by: Bond-009 <bond.009@outlook.com>
BaronGreenback vor 4 Jahren
Ursprung
Commit
124bd4c2c0

+ 20 - 10
Emby.Server.Implementations/Networking/NetworkManager.cs

@@ -18,13 +18,12 @@ namespace Emby.Server.Implementations.Networking
     public class NetworkManager : INetworkManager
     public class NetworkManager : INetworkManager
     {
     {
         private readonly ILogger<NetworkManager> _logger;
         private readonly ILogger<NetworkManager> _logger;
-
-        private IPAddress[] _localIpAddresses;
         private readonly object _localIpAddressSyncLock = new object();
         private readonly object _localIpAddressSyncLock = new object();
-
         private readonly object _subnetLookupLock = new object();
         private readonly object _subnetLookupLock = new object();
         private readonly Dictionary<string, List<string>> _subnetLookup = new Dictionary<string, List<string>>(StringComparer.Ordinal);
         private readonly Dictionary<string, List<string>> _subnetLookup = new Dictionary<string, List<string>>(StringComparer.Ordinal);
 
 
+        private IPAddress[] _localIpAddresses;
+
         private List<PhysicalAddress> _macAddresses;
         private List<PhysicalAddress> _macAddresses;
 
 
         /// <summary>
         /// <summary>
@@ -157,7 +156,9 @@ namespace Emby.Server.Implementations.Networking
                 return false;
                 return false;
             }
             }
 
 
-            byte[] octet = ipAddress.GetAddressBytes();
+            // GetAddressBytes
+            Span<byte> octet = stackalloc byte[ipAddress.AddressFamily == AddressFamily.InterNetwork ? 4 : 16];
+            ipAddress.TryWriteBytes(octet, out _);
 
 
             if ((octet[0] == 10) ||
             if ((octet[0] == 10) ||
                 (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918
                 (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918
@@ -260,7 +261,9 @@ namespace Emby.Server.Implementations.Networking
         /// <inheritdoc/>
         /// <inheritdoc/>
         public bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC)
         public bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC)
         {
         {
-            byte[] octet = address.GetAddressBytes();
+            // GetAddressBytes
+            Span<byte> octet = stackalloc byte[address.AddressFamily == AddressFamily.InterNetwork ? 4 : 16];
+            address.TryWriteBytes(octet, out _);
 
 
             if ((octet[0] == 127) || // RFC1122
             if ((octet[0] == 127) || // RFC1122
                 (octet[0] == 169 && octet[1] == 254)) // RFC3927
                 (octet[0] == 169 && octet[1] == 254)) // RFC3927
@@ -503,18 +506,25 @@ namespace Emby.Server.Implementations.Networking
 
 
         private IPAddress GetNetworkAddress(IPAddress address, IPAddress subnetMask)
         private IPAddress GetNetworkAddress(IPAddress address, IPAddress subnetMask)
         {
         {
-            byte[] ipAdressBytes = address.GetAddressBytes();
-            byte[] subnetMaskBytes = subnetMask.GetAddressBytes();
+            int size = address.AddressFamily == AddressFamily.InterNetwork ? 4 : 16;
+
+            // GetAddressBytes
+            Span<byte> ipAddressBytes = stackalloc byte[size];
+            address.TryWriteBytes(ipAddressBytes, out _);
+
+            // GetAddressBytes
+            Span<byte> subnetMaskBytes = stackalloc byte[size];
+            subnetMask.TryWriteBytes(subnetMaskBytes, out _);
 
 
-            if (ipAdressBytes.Length != subnetMaskBytes.Length)
+            if (ipAddressBytes.Length != subnetMaskBytes.Length)
             {
             {
                 throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
                 throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
             }
             }
 
 
-            byte[] broadcastAddress = new byte[ipAdressBytes.Length];
+            byte[] broadcastAddress = new byte[ipAddressBytes.Length];
             for (int i = 0; i < broadcastAddress.Length; i++)
             for (int i = 0; i < broadcastAddress.Length; i++)
             {
             {
-                broadcastAddress[i] = (byte)(ipAdressBytes[i] & subnetMaskBytes[i]);
+                broadcastAddress[i] = (byte)(ipAddressBytes[i] & subnetMaskBytes[i]);
             }
             }
 
 
             return new IPAddress(broadcastAddress);
             return new IPAddress(broadcastAddress);

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

@@ -72,9 +72,9 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration)
         public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration)
         {
         {
-            _config.Configuration.UICulture = startupConfiguration.UICulture;
-            _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode;
-            _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage;
+            _config.Configuration.UICulture = startupConfiguration.UICulture ?? string.Empty;
+            _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode ?? string.Empty;
+            _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage ?? string.Empty;
             _config.SaveConfiguration();
             _config.SaveConfiguration();
             return NoContent();
             return NoContent();
         }
         }

+ 221 - 0
Jellyfin.Networking/Configuration/NetworkConfiguration.cs

@@ -0,0 +1,221 @@
+#pragma warning disable CA1819 // Properties should not return arrays
+
+using System;
+using MediaBrowser.Model.Configuration;
+
+namespace Jellyfin.Networking.Configuration
+{
+    /// <summary>
+    /// Defines the <see cref="NetworkConfiguration" />.
+    /// </summary>
+    public class NetworkConfiguration
+    {
+        /// <summary>
+        /// The default value for <see cref="HttpServerPortNumber"/>.
+        /// </summary>
+        public const int DefaultHttpPort = 8096;
+
+        /// <summary>
+        /// The default value for <see cref="PublicHttpsPort"/> and <see cref="HttpsPortNumber"/>.
+        /// </summary>
+        public const int DefaultHttpsPort = 8920;
+
+        private string _baseUrl = string.Empty;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the server should force connections over HTTPS.
+        /// </summary>
+        public bool RequireHttps { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at.
+        /// </summary>
+        public string BaseUrl
+        {
+            get => _baseUrl;
+            set
+            {
+                // Normalize the start of the string
+                if (string.IsNullOrWhiteSpace(value))
+                {
+                    // If baseUrl is empty, set an empty prefix string
+                    _baseUrl = string.Empty;
+                    return;
+                }
+
+                if (value[0] != '/')
+                {
+                    // If baseUrl was not configured with a leading slash, append one for consistency
+                    value = "/" + value;
+                }
+
+                // Normalize the end of the string
+                if (value[^1] == '/')
+                {
+                    // If baseUrl was configured with a trailing slash, remove it for consistency
+                    value = value.Remove(value.Length - 1);
+                }
+
+                _baseUrl = value;
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the public HTTPS port.
+        /// </summary>
+        /// <value>The public HTTPS port.</value>
+        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; } = DefaultHttpPort;
+
+        /// <summary>
+        /// Gets or sets the HTTPS server port number.
+        /// </summary>
+        /// <value>The HTTPS server port number.</value>
+        public int HttpsPortNumber { get; set; } = DefaultHttpsPort;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to use HTTPS.
+        /// </summary>
+        /// <remarks>
+        /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be
+        /// provided for <see cref="ServerConfiguration.CertificatePath"/> and <see cref="ServerConfiguration.CertificatePassword"/>.
+        /// </remarks>
+        public bool EnableHttps { get; set; }
+
+        /// <summary>
+        /// Gets or sets the public mapped port.
+        /// </summary>
+        /// <value>The public mapped port.</value>
+        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; }
+
+        /// <summary>
+        /// Gets or sets the UDPPortRange.
+        /// </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; }
+
+        /// <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.
+        /// "Emby.Dlna": "Debug" must be set in logging.default.json for this property to have any effect.
+        /// </summary>
+        public bool EnableSSDPTracing { get; set; }
+
+        /// <summary>
+        /// Gets or sets the SSDPTracingFilter
+        /// 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 a value indicating whether address names that match <see cref="VirtualInterfaceNames"/> should be Ignore for the purposes of binding.
+        /// </summary>
+        public bool IgnoreVirtualInterfaces { get; set; } = true;
+
+        /// <summary>
+        /// Gets or sets a value indicating the interfaces that should be ignored. The list can be comma separated. <seealso cref="IgnoreVirtualInterfaces"/>.
+        /// </summary>
+        public string VirtualInterfaceNames { get; set; } = "vEthernet*";
+
+        /// <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 multi-socket binding is 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; }
+
+        /// <summary>
+        /// Gets or sets the ports that HDHomerun uses.
+        /// </summary>
+        public string HDHomerunPortRange { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Gets or sets the PublishedServerUriBySubnet
+        /// 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 Autodiscovery tracing is enabled.
+        /// </summary>
+        public bool AutoDiscoveryTracing { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether Autodiscovery is enabled.
+        /// </summary>
+        public bool AutoDiscovery { get; set; } = true;
+
+        /// <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>();
+
+        /// <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; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether to enable automatic port forwarding.
+        /// </summary>
+        public bool EnableUPnP { 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 the subnets that are deemed to make up the LAN.
+        /// </summary>
+        public string[] LocalNetworkSubnets { get; set; } = Array.Empty<string>();
+
+        /// <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>();
+
+        /// <summary>
+        /// Gets or sets the known proxies.
+        /// </summary>
+        public string[] KnownProxies { get; set; } = Array.Empty<string>();
+    }
+}

+ 21 - 0
Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs

@@ -0,0 +1,21 @@
+using Jellyfin.Networking.Configuration;
+using MediaBrowser.Common.Configuration;
+
+namespace Jellyfin.Networking.Configuration
+{
+    /// <summary>
+    /// Defines the <see cref="NetworkConfigurationExtensions" />.
+    /// </summary>
+    public static class NetworkConfigurationExtensions
+    {
+        /// <summary>
+        /// Retrieves the network configuration.
+        /// </summary>
+        /// <param name="config">The <see cref="IConfigurationManager"/>.</param>
+        /// <returns>The <see cref="NetworkConfiguration"/>.</returns>
+        public static NetworkConfiguration GetNetworkConfiguration(this IConfigurationManager config)
+        {
+            return config.GetConfiguration<NetworkConfiguration>("network");
+        }
+    }
+}

+ 27 - 0
Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs

@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using MediaBrowser.Common.Configuration;
+
+namespace Jellyfin.Networking.Configuration
+{
+    /// <summary>
+    /// Defines the <see cref="NetworkConfigurationFactory" />.
+    /// </summary>
+    public class NetworkConfigurationFactory : IConfigurationFactory
+    {
+        /// <summary>
+        /// The GetConfigurations.
+        /// </summary>
+        /// <returns>The <see cref="IEnumerable{ConfigurationStore}"/>.</returns>
+        public IEnumerable<ConfigurationStore> GetConfigurations()
+        {
+            return new[]
+            {
+                new ConfigurationStore
+                {
+                    Key = "network",
+                    ConfigurationType = typeof(NetworkConfiguration)
+                }
+            };
+        }
+    }
+}

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

@@ -0,0 +1,30 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFramework>net5.0</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="SerilogAnalyzer" Version="0.15.0" 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>
+    <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
+    <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
+  </ItemGroup>
+</Project>

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

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

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

@@ -0,0 +1,1319 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+using Jellyfin.Networking.Configuration;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Networking.Manager
+{
+    /// <summary>
+    /// Class to take care of network interface management.
+    /// Note: The normal collection methods and properties will not work with Collection{IPObject}. <see cref="MediaBrowser.Common.Net.NetworkExtensions"/>.
+    /// </summary>
+    public class NetworkManager : INetworkManager, IDisposable
+    {
+        /// <summary>
+        /// Contains the description of the interface along with its index.
+        /// </summary>
+        private readonly Dictionary<string, int> _interfaceNames;
+
+        /// <summary>
+        /// Threading lock for network properties.
+        /// </summary>
+        private readonly object _intLock = new object();
+
+        /// <summary>
+        /// List of all interface addresses and masks.
+        /// </summary>
+        private readonly Collection<IPObject> _interfaceAddresses;
+
+        /// <summary>
+        /// List of all interface MAC addresses.
+        /// </summary>
+        private readonly List<PhysicalAddress> _macAddresses;
+
+        private readonly ILogger<NetworkManager> _logger;
+
+        private readonly IConfigurationManager _configurationManager;
+
+        private readonly object _eventFireLock;
+
+        /// <summary>
+        /// Holds the bind address overrides.
+        /// </summary>
+        private readonly Dictionary<IPNetAddress, string> _publishedServerUrls;
+
+        /// <summary>
+        /// Used to stop "event-racing conditions".
+        /// </summary>
+        private bool _eventfire;
+
+        /// <summary>
+        /// Unfiltered user defined LAN subnets. (<see cref="NetworkConfiguration.LocalNetworkSubnets"/>)
+        /// or internal interface network subnets if undefined by user.
+        /// </summary>
+        private Collection<IPObject> _lanSubnets;
+
+        /// <summary>
+        /// User defined list of subnets to excluded from the LAN.
+        /// </summary>
+        private Collection<IPObject> _excludedSubnets;
+
+        /// <summary>
+        /// List of interface addresses to bind the WS.
+        /// </summary>
+        private Collection<IPObject> _bindAddresses;
+
+        /// <summary>
+        /// List of interface addresses to exclude from bind.
+        /// </summary>
+        private Collection<IPObject> _bindExclusions;
+
+        /// <summary>
+        /// Caches list of all internal filtered interface addresses and masks.
+        /// </summary>
+        private Collection<IPObject> _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 UpdateSettings 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 Collection<IPObject>();
+            _macAddresses = new List<PhysicalAddress>();
+            _interfaceNames = new Dictionary<string, int>();
+            _publishedServerUrls = new Dictionary<IPNetAddress, string>();
+            _eventFireLock = new object();
+
+            UpdateSettings(_configurationManager.GetNetworkConfiguration());
+
+            NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged;
+            NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
+
+            _configurationManager.NamedConfigurationUpdated += ConfigurationUpdated;
+        }
+#pragma warning restore CS8618 // Non-nullable field is uninitialized.
+
+        /// <summary>
+        /// Event triggered on network changes.
+        /// </summary>
+        public event EventHandler? NetworkChanged;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether testing is taking place.
+        /// </summary>
+        public static string MockNetworkSettings { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether IP6 is enabled.
+        /// </summary>
+        public bool IsIP6Enabled { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether IP4 is enabled.
+        /// </summary>
+        public bool IsIP4Enabled { get; set; }
+
+        /// <inheritdoc/>
+        public Collection<IPObject> 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> PublishedServerUrls => _publishedServerUrls;
+
+        /// <summary>
+        /// Creates a new network collection.
+        /// </summary>
+        /// <param name="source">Items to assign the collection, or null.</param>
+        /// <returns>The collection created.</returns>
+        public static Collection<IPObject> CreateCollection(IEnumerable<IPObject>? source = null)
+        {
+            var result = new Collection<IPObject>();
+            if (source != null)
+            {
+                foreach (var item in source)
+                {
+                    result.AddItem(item);
+                }
+            }
+
+            return result;
+        }
+
+        /// <inheritdoc/>
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        /// <inheritdoc/>
+        public IReadOnlyCollection<PhysicalAddress> GetMacAddresses()
+        {
+            // Populated in construction - so always has values.
+            return _macAddresses;
+        }
+
+        /// <inheritdoc/>
+        public bool IsGatewayInterface(IPObject? addressObj)
+        {
+            var address = addressObj?.Address ?? IPAddress.None;
+            return _internalInterfaces.Any(i => i.Address.Equals(address) && i.Tag < 0);
+        }
+
+        /// <inheritdoc/>
+        public bool IsGatewayInterface(IPAddress? addressObj)
+        {
+            return _internalInterfaces.Any(i => i.Address.Equals(addressObj ?? IPAddress.None) && i.Tag < 0);
+        }
+
+        /// <inheritdoc/>
+        public Collection<IPObject> GetLoopbacks()
+        {
+            Collection<IPObject> nc = new Collection<IPObject>();
+            if (IsIP4Enabled)
+            {
+                nc.AddItem(IPAddress.Loopback);
+            }
+
+            if (IsIP6Enabled)
+            {
+                nc.AddItem(IPAddress.IPv6Loopback);
+            }
+
+            return nc;
+        }
+
+        /// <inheritdoc/>
+        public bool IsExcluded(IPAddress ip)
+        {
+            return _excludedSubnets.ContainsAddress(ip);
+        }
+
+        /// <inheritdoc/>
+        public bool IsExcluded(EndPoint ip)
+        {
+            return ip != null && IsExcluded(((IPEndPoint)ip).Address);
+        }
+
+        /// <inheritdoc/>
+        public Collection<IPObject> CreateIPCollection(string[] values, bool bracketed = false)
+        {
+            Collection<IPObject> col = new Collection<IPObject>();
+            if (values == null)
+            {
+                return col;
+            }
+
+            for (int a = 0; a < values.Length; a++)
+            {
+                string v = values[a].Trim();
+
+                try
+                {
+                    if (v.StartsWith('[') && v.EndsWith(']'))
+                    {
+                        if (bracketed)
+                        {
+                            AddToCollection(col, v[1..^1]);
+                        }
+                    }
+                    else if (v.StartsWith('!'))
+                    {
+                        if (bracketed)
+                        {
+                            AddToCollection(col, v[1..]);
+                        }
+                    }
+                    else if (!bracketed)
+                    {
+                        AddToCollection(col, v);
+                    }
+                }
+                catch (ArgumentException e)
+                {
+                    _logger.LogWarning(e, "Ignoring LAN value {value}.", v);
+                }
+            }
+
+            return col;
+        }
+
+        /// <inheritdoc/>
+        public Collection<IPObject> GetAllBindInterfaces(bool individualInterfaces = false)
+        {
+            int count = _bindAddresses.Count;
+
+            if (count == 0)
+            {
+                if (_bindExclusions.Count > 0)
+                {
+                    // Return all the interfaces except the ones specifically excluded.
+                    return _interfaceAddresses.Exclude(_bindExclusions);
+                }
+
+                if (individualInterfaces)
+                {
+                    return new Collection<IPObject>(_interfaceAddresses);
+                }
+
+                // No bind address and no exclusions, so listen on all interfaces.
+                Collection<IPObject> result = new Collection<IPObject>();
+
+                if (IsIP4Enabled)
+                {
+                    result.AddItem(IPAddress.Any);
+                }
+
+                if (IsIP6Enabled)
+                {
+                    result.AddItem(IPAddress.IPv6Any);
+                }
+
+                return result;
+            }
+
+            // Remove any excluded bind interfaces.
+            return _bindAddresses.Exclude(_bindExclusions);
+        }
+
+        /// <inheritdoc/>
+        public string GetBindInterface(string source, out int? port)
+        {
+            if (!string.IsNullOrEmpty(source) && IPHost.TryParse(source, out IPHost host))
+            {
+                return GetBindInterface(host, out port);
+            }
+
+            return GetBindInterface(IPHost.None, out port);
+        }
+
+        /// <inheritdoc/>
+        public string GetBindInterface(IPAddress source, out int? port)
+        {
+            return GetBindInterface(new IPNetAddress(source), out port);
+        }
+
+        /// <inheritdoc/>
+        public string GetBindInterface(HttpRequest source, out int? port)
+        {
+            string result;
+
+            if (source != null && IPHost.TryParse(source.Host.Host, out IPHost host))
+            {
+                result = GetBindInterface(host, out port);
+                port ??= source.Host.Port;
+            }
+            else
+            {
+                result = GetBindInterface(IPNetAddress.None, out port);
+                port ??= source?.Host.Port;
+            }
+
+            return result;
+        }
+
+        /// <inheritdoc/>
+        public string GetBindInterface(IPObject source, out int? port)
+        {
+            port = null;
+            if (source == null)
+            {
+                throw new ArgumentNullException(nameof(source));
+            }
+
+            // Do we have a source?
+            bool haveSource = !source.Address.Equals(IPAddress.None);
+            bool isExternal = false;
+
+            if (haveSource)
+            {
+                if (!IsIP6Enabled && source.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 && source.AddressFamily == AddressFamily.InterNetwork)
+                {
+                    _logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
+                }
+
+                isExternal = !IsInLocalNetwork(source);
+
+                if (MatchesPublishedServerUrl(source, isExternal, out string res, out port))
+                {
+                    _logger.LogInformation("{Source}: Using BindAddress {Address}:{Port}", source, res, port);
+                    return res;
+                }
+            }
+
+            _logger.LogDebug("GetBindInterface: Source: {HaveSource}, External: {IsExternal}:", haveSource, isExternal);
+
+            // No preference given, so move on to bind addresses.
+            if (MatchesBindInterface(source, isExternal, out string result))
+            {
+                return result;
+            }
+
+            if (isExternal && MatchesExternalInterface(source, out result))
+            {
+                return result;
+            }
+
+            // Get the first LAN interface address that isn't a loopback.
+            var interfaces = CreateCollection(_interfaceAddresses
+                .Exclude(_bindExclusions)
+                .Where(p => IsInLocalNetwork(p))
+                .OrderBy(p => p.Tag));
+
+            if (interfaces.Count > 0)
+            {
+                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 interfaces)
+                    {
+                        if (intf.Contains(source))
+                        {
+                            result = FormatIP6String(intf.Address);
+                            _logger.LogDebug("{Source}: GetBindInterface: Has source, matched best internal interface on range. {Result}", source, result);
+                            return result;
+                        }
+                    }
+                }
+
+                result = FormatIP6String(interfaces.First().Address);
+                _logger.LogDebug("{Source}: GetBindInterface: Matched first internal interface. {Result}", source, result);
+                return result;
+            }
+
+            // There isn't any others, so we'll use the loopback.
+            result = IsIP6Enabled ? "::" : "127.0.0.1";
+            _logger.LogWarning("{Source}: GetBindInterface: Loopback {Result} returned.", source, result);
+            return result;
+        }
+
+        /// <inheritdoc/>
+        public Collection<IPObject> GetInternalBindAddresses()
+        {
+            int count = _bindAddresses.Count;
+
+            if (count == 0)
+            {
+                if (_bindExclusions.Count > 0)
+                {
+                    // Return all the internal interfaces except the ones excluded.
+                    return CreateCollection(_internalInterfaces.Where(p => !_bindExclusions.ContainsAddress(p)));
+                }
+
+                // No bind address, so return all internal interfaces.
+                return CreateCollection(_internalInterfaces.Where(p => !p.IsLoopback()));
+            }
+
+            return new Collection<IPObject>(_bindAddresses);
+        }
+
+        /// <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;
+            }
+
+            // As private addresses can be redefined by Configuration.LocalNetworkAddresses
+            return _lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address);
+        }
+
+        /// <inheritdoc/>
+        public bool IsInLocalNetwork(string address)
+        {
+            if (IPHost.TryParse(address, out IPHost ep))
+            {
+                return _lanSubnets.ContainsAddress(ep) && !_excludedSubnets.ContainsAddress(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;
+            }
+
+            // As private addresses can be redefined by Configuration.LocalNetworkAddresses
+            return _lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(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)
+        {
+            return _bindExclusions.ContainsAddress(address);
+        }
+
+        /// <inheritdoc/>
+        public Collection<IPObject> GetFilteredLANSubnets(Collection<IPObject>? filter = null)
+        {
+            if (filter == null)
+            {
+                return _lanSubnets.Exclude(_excludedSubnets).AsNetworks();
+            }
+
+            return _lanSubnets.Exclude(filter);
+        }
+
+        /// <inheritdoc/>
+        public bool IsValidInterfaceAddress(IPAddress address)
+        {
+            return _interfaceAddresses.ContainsAddress(address);
+        }
+
+        /// <inheritdoc/>
+        public bool TryParseInterface(string token, out Collection<IPObject>? result)
+        {
+            result = null;
+            if (string.IsNullOrEmpty(token))
+            {
+                return false;
+            }
+
+            if (_interfaceNames != null && _interfaceNames.TryGetValue(token.ToLower(CultureInfo.InvariantCulture), out int index))
+            {
+                result = new Collection<IPObject>();
+
+                _logger.LogInformation("Interface {Token} 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.AddItem(iface);
+                    }
+                }
+
+                return true;
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Reloads all settings and re-initialises the instance.
+        /// </summary>
+        /// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param>
+        public void UpdateSettings(object configuration)
+        {
+            NetworkConfiguration config = (NetworkConfiguration)configuration ?? throw new ArgumentNullException(nameof(configuration));
+
+            IsIP4Enabled = Socket.OSSupportsIPv4 && config.EnableIPV4;
+            IsIP6Enabled = Socket.OSSupportsIPv6 && config.EnableIPV6;
+
+            if (!IsIP6Enabled && !IsIP4Enabled)
+            {
+                _logger.LogError("IPv4 and IPv6 cannot both be disabled.");
+                IsIP4Enabled = true;
+            }
+
+            TrustAllIP6Interfaces = config.TrustAllIP6Interfaces;
+            // UdpHelper.EnableMultiSocketBinding = config.EnableMultiSocketBinding;
+
+            if (string.IsNullOrEmpty(MockNetworkSettings))
+            {
+                InitialiseInterfaces();
+            }
+            else // Used in testing only.
+            {
+                // Format is <IPAddress>,<Index>,<Name>: <next interface>. Set index to -ve to simulate a gateway.
+                var interfaceList = MockNetworkSettings.Split(':');
+                foreach (var details in interfaceList)
+                {
+                    var parts = details.Split(',');
+                    var address = IPNetAddress.Parse(parts[0]);
+                    var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
+                    address.Tag = index;
+                    _interfaceAddresses.AddItem(address);
+                    _interfaceNames.Add(parts[2], Math.Abs(index));
+                }
+            }
+
+            InitialiseLAN(config);
+            InitialiseBind(config);
+            InitialiseRemote(config);
+            InitialiseOverrides(config);
+        }
+
+        /// <summary>
+        /// Protected implementation of Dispose pattern.
+        /// </summary>
+        /// <param name="disposing"><c>True</c> to dispose the managed state.</param>
+        protected virtual void Dispose(bool disposing)
+        {
+            if (!_disposed)
+            {
+                if (disposing)
+                {
+                    _configurationManager.NamedConfigurationUpdated -= ConfigurationUpdated;
+                    NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged;
+                    NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged;
+                }
+
+                _disposed = true;
+            }
+        }
+
+        /// <summary>
+        /// Tries to identify the string and return an object of that class.
+        /// </summary>
+        /// <param name="addr">String to parse.</param>
+        /// <param name="result">IPObject to return.</param>
+        /// <returns><c>true</c> if the value parsed successfully, <c>false</c> otherwise.</returns>
+        private static bool TryParse(string addr, out IPObject result)
+        {
+            if (!string.IsNullOrEmpty(addr))
+            {
+                // Is it an IP address
+                if (IPNetAddress.TryParse(addr, out IPNetAddress nw))
+                {
+                    result = nw;
+                    return true;
+                }
+
+                if (IPHost.TryParse(addr, out IPHost h))
+                {
+                    result = h;
+                    return true;
+                }
+            }
+
+            result = IPNetAddress.None;
+            return false;
+        }
+
+        /// <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 safe conversion of the address.</returns>
+        private static 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;
+        }
+
+        private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt)
+        {
+            if (evt.Key.Equals("network", StringComparison.Ordinal))
+            {
+                UpdateSettings((NetworkConfiguration)evt.NewConfiguration);
+            }
+        }
+
+        /// <summary>
+        /// Checks the string to see if it matches any interface names.
+        /// </summary>
+        /// <param name="token">String to check.</param>
+        /// <param name="index">Interface index number.</param>
+        /// <returns><c>true</c> if an interface name matches the token, <c>False</c> otherwise.</returns>
+        private bool IsInterface(string token, out int index)
+        {
+            index = -1;
+
+            // 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 && token.Length > 1)
+            {
+                bool partial = token[^1] == '*';
+                if (partial)
+                {
+                    token = token[0..^1];
+                }
+
+                foreach ((string interfc, int interfcIndex) in _interfaceNames)
+                {
+                    if ((!partial && string.Equals(interfc, token, StringComparison.OrdinalIgnoreCase))
+                        || (partial && interfc.StartsWith(token, true, CultureInfo.InvariantCulture)))
+                    {
+                        index = interfcIndex;
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Parses a string and adds it into the the collection, replacing any interface references.
+        /// </summary>
+        /// <param name="col"><see cref="Collection{IPObject}"/>Collection.</param>
+        /// <param name="token">String value to parse.</param>
+        private void AddToCollection(Collection<IPObject> 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 (IsInterface(token, out int index))
+            {
+                _logger.LogInformation("Interface {Token} 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.AddItem(iface);
+                    }
+                }
+            }
+            else if (TryParse(token, out IPObject obj))
+            {
+                if (!IsIP6Enabled)
+                {
+                    // Remove IP6 addresses from multi-homed IPHosts.
+                    obj.Remove(AddressFamily.InterNetworkV6);
+                    if (!obj.IsIP6())
+                    {
+                        col.AddItem(obj);
+                    }
+                }
+                else if (!IsIP4Enabled)
+                {
+                    // Remove IP4 addresses from multi-homed IPHosts.
+                    obj.Remove(AddressFamily.InterNetwork);
+                    if (obj.IsIP6())
+                    {
+                        col.AddItem(obj);
+                    }
+                }
+                else
+                {
+                    col.AddItem(obj);
+                }
+            }
+            else
+            {
+                _logger.LogDebug("Invalid or unknown network {Token}.", token);
+            }
+        }
+
+        /// <summary>
+        /// Handler for network change events.
+        /// </summary>
+        /// <param name="sender">Sender.</param>
+        /// <param name="e">A <see cref="NetworkAvailabilityEventArgs"/> containing network availability 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">An <see cref="EventArgs"/>.</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>A <see cref="Task"/> representing the asynchronous operation.</returns>
+        private async Task OnNetworkChangeAsync()
+        {
+            try
+            {
+                await Task.Delay(2000).ConfigureAwait(false);
+                InitialiseInterfaces();
+                // Recalculate LAN caches.
+                InitialiseLAN(_configurationManager.GetNetworkConfiguration());
+
+                NetworkChanged?.Invoke(this, EventArgs.Empty);
+            }
+            finally
+            {
+                _eventfire = false;
+            }
+        }
+
+        /// <summary>
+        /// Triggers our event, and re-loads interface information.
+        /// </summary>
+        private void OnNetworkChanged()
+        {
+            lock (_eventFireLock)
+            {
+                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().GetAwaiter().GetResult();
+                }
+            }
+        }
+
+        /// <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(NetworkConfiguration config)
+        {
+            lock (_intLock)
+            {
+                _publishedServerUrls.Clear();
+                string[] overrides = config.PublishedServerUriBySubnet;
+                if (overrides == null)
+                {
+                    return;
+                }
+
+                foreach (var entry in overrides)
+                {
+                    var parts = entry.Split('=');
+                    if (parts.Length != 2)
+                    {
+                        _logger.LogError("Unable to parse bind override: {Entry}", entry);
+                    }
+                    else
+                    {
+                        var replacement = parts[1].Trim();
+                        if (string.Equals(parts[0], "remaining", StringComparison.OrdinalIgnoreCase))
+                        {
+                            _publishedServerUrls[new IPNetAddress(IPAddress.Broadcast)] = replacement;
+                        }
+                        else if (string.Equals(parts[0], "external", StringComparison.OrdinalIgnoreCase))
+                        {
+                            _publishedServerUrls[new IPNetAddress(IPAddress.Any)] = replacement;
+                        }
+                        else if (TryParseInterface(parts[0], out Collection<IPObject>? addresses) && addresses != null)
+                        {
+                            foreach (IPNetAddress na in addresses)
+                            {
+                                _publishedServerUrls[na] = replacement;
+                            }
+                        }
+                        else if (IPNetAddress.TryParse(parts[0], out IPNetAddress result))
+                        {
+                            _publishedServerUrls[result] = replacement;
+                        }
+                        else
+                        {
+                            _logger.LogError("Unable to parse bind ip address. {Parts}", parts[1]);
+                        }
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Initialises the network bind addresses.
+        /// </summary>
+        private void InitialiseBind(NetworkConfiguration config)
+        {
+            lock (_intLock)
+            {
+                string[] lanAddresses = config.LocalNetworkAddresses;
+
+                // TODO: remove when bug fixed: https://github.com/jellyfin/jellyfin-web/issues/1334
+
+                if (lanAddresses.Length == 1 && lanAddresses[0].IndexOf(',', StringComparison.OrdinalIgnoreCase) != -1)
+                {
+                    lanAddresses = lanAddresses[0].Split(',');
+                }
+
+                // TODO: end fix: https://github.com/jellyfin/jellyfin-web/issues/1334
+
+                // Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded.
+                if (config.IgnoreVirtualInterfaces)
+                {
+                    var virtualInterfaceNames = config.VirtualInterfaceNames.Split(',');
+                    var newList = new string[lanAddresses.Length + virtualInterfaceNames.Length];
+                    Array.Copy(lanAddresses, newList, lanAddresses.Length);
+                    Array.Copy(virtualInterfaceNames, 0, newList, lanAddresses.Length, virtualInterfaceNames.Length);
+                    lanAddresses = newList;
+                }
+
+                // Read and parse bind addresses and exclusions, removing ones that don't exist.
+                _bindAddresses = CreateIPCollection(lanAddresses).Union(_interfaceAddresses);
+                _bindExclusions = CreateIPCollection(lanAddresses, true).Union(_interfaceAddresses);
+                _logger.LogInformation("Using bind addresses: {0}", _bindAddresses.AsString());
+                _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions.AsString());
+            }
+        }
+
+        /// <summary>
+        /// Initialises the remote address values.
+        /// </summary>
+        private void InitialiseRemote(NetworkConfiguration config)
+        {
+            lock (_intLock)
+            {
+                RemoteAddressFilter = CreateIPCollection(config.RemoteIPFilter);
+            }
+        }
+
+        /// <summary>
+        /// Initialises internal LAN cache settings.
+        /// </summary>
+        private void InitialiseLAN(NetworkConfiguration config)
+        {
+            lock (_intLock)
+            {
+                _logger.LogDebug("Refreshing LAN information.");
+
+                // Get config options.
+                string[] subnets = config.LocalNetworkSubnets;
+
+                // Create lists from user settings.
+
+                _lanSubnets = CreateIPCollection(subnets);
+                _excludedSubnets = CreateIPCollection(subnets, true).AsNetworks();
+
+                // 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 generating the collection in this statement matters.
+                // Altering the order will cause the collections to be created incorrectly.
+                if (_usingPrivateAddresses)
+                {
+                    _logger.LogDebug("Using LAN interface addresses as user provided no LAN details.");
+                    // Internal interfaces must be private and not excluded.
+                    _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsPrivateAddressRange(i) && !_excludedSubnets.ContainsAddress(i)));
+
+                    // Subnets are the same as the calculated internal interface.
+                    _lanSubnets = new Collection<IPObject>();
+
+                    // We must listen on loopback for LiveTV to function regardless of the settings.
+                    if (IsIP6Enabled)
+                    {
+                        _lanSubnets.AddItem(IPNetAddress.IP6Loopback);
+                        _lanSubnets.AddItem(IPNetAddress.Parse("fc00::/7")); // ULA
+                        _lanSubnets.AddItem(IPNetAddress.Parse("fe80::/10")); // Site local
+                    }
+
+                    if (IsIP4Enabled)
+                    {
+                        _lanSubnets.AddItem(IPNetAddress.IP4Loopback);
+                        _lanSubnets.AddItem(IPNetAddress.Parse("10.0.0.0/8"));
+                        _lanSubnets.AddItem(IPNetAddress.Parse("172.16.0.0/12"));
+                        _lanSubnets.AddItem(IPNetAddress.Parse("192.168.0.0/16"));
+                    }
+                }
+                else
+                {
+                    // We must listen on loopback for LiveTV to function regardless of the settings.
+                    if (IsIP6Enabled)
+                    {
+                        _lanSubnets.AddItem(IPNetAddress.IP6Loopback);
+                    }
+
+                    if (IsIP4Enabled)
+                    {
+                        _lanSubnets.AddItem(IPNetAddress.IP4Loopback);
+                    }
+
+                    // Internal interfaces must be private, not excluded and part of the LocalNetworkSubnet.
+                    _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsInLocalNetwork(i)));
+                }
+
+                _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets.AsString());
+                _logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets.AsString());
+                _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets).AsNetworks().AsString());
+            }
+        }
+
+        /// <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 addresses.
+        /// </summary>
+        private void InitialiseInterfaces()
+        {
+            lock (_intLock)
+            {
+                _logger.LogDebug("Refreshing interfaces.");
+
+                _interfaceNames.Clear();
+                _interfaceAddresses.Clear();
+                _macAddresses.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, IPObject.MaskToCidr(info.IPv4Mask))
+                                    {
+                                        // Keep the number of gateways on this interface, along with its index.
+                                        Tag = ipProperties.GetIPv4Properties().Index
+                                    };
+
+                                    int tag = nw.Tag;
+                                    if (ipProperties.GatewayAddresses.Count > 0 && !nw.IsLoopback())
+                                    {
+                                        // -ve Tags signify the interface has a gateway.
+                                        nw.Tag *= -1;
+                                    }
+
+                                    _interfaceAddresses.AddItem(nw);
+
+                                    // Store interface name so we can use the name in Collections.
+                                    _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag;
+                                    _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag;
+                                }
+                                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;
+                                    if (ipProperties.GatewayAddresses.Count > 0 && !nw.IsLoopback())
+                                    {
+                                        // -ve Tags signify the interface has a gateway.
+                                        nw.Tag *= -1;
+                                    }
+
+                                    _interfaceAddresses.AddItem(nw);
+
+                                    // Store interface name so we can use the name in Collections.
+                                    _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag;
+                                    _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag;
+                                }
+                            }
+                        }
+#pragma warning disable CA1031 // Do not catch general exception types
+                        catch (Exception ex)
+                        {
+                            // Ignore error, and attempt to continue.
+                            _logger.LogError(ex, "Error encountered parsing interfaces.");
+                        }
+#pragma warning restore CA1031 // Do not catch general exception types
+                    }
+
+                    _logger.LogDebug("Discovered {0} interfaces.", _interfaceAddresses.Count);
+                    _logger.LogDebug("Interfaces addresses : {0}", _interfaceAddresses.AsString());
+
+                    // If for some reason we don't have an interface info, resolve our DNS name.
+                    if (_interfaceAddresses.Count == 0)
+                    {
+                        _logger.LogError("No interfaces information available. Resolving DNS name.");
+                        IPHost host = new IPHost(Dns.GetHostName());
+                        foreach (var a in host.GetAddresses())
+                        {
+                            _interfaceAddresses.AddItem(a);
+                        }
+
+                        if (_interfaceAddresses.Count == 0)
+                        {
+                            _logger.LogWarning("No interfaces information available. Using loopback.");
+                            // Last ditch attempt - use loopback address.
+                            _interfaceAddresses.AddItem(IPNetAddress.IP4Loopback);
+                            if (IsIP6Enabled)
+                            {
+                                _interfaceAddresses.AddItem(IPNetAddress.IP6Loopback);
+                            }
+                        }
+                    }
+                }
+                catch (NetworkInformationException ex)
+                {
+                    _logger.LogError(ex, "Error in InitialiseInterfaces.");
+                }
+            }
+        }
+
+        /// <summary>
+        /// Attempts to match the source against a user defined bind interface.
+        /// </summary>
+        /// <param name="source">IP source address to use.</param>
+        /// <param name="isInExternalSubnet">True if the source is in the external subnet.</param>
+        /// <param name="bindPreference">The published server url that matches the source address.</param>
+        /// <param name="port">The resultant port, if one exists.</param>
+        /// <returns><c>true</c> if a match is found, <c>false</c> otherwise.</returns>
+        private bool MatchesPublishedServerUrl(IPObject source, bool isInExternalSubnet, out string bindPreference, out int? port)
+        {
+            bindPreference = string.Empty;
+            port = null;
+
+            // Check for user override.
+            foreach (var addr in _publishedServerUrls)
+            {
+                // Remaining. Match anything.
+                if (addr.Key.Address.Equals(IPAddress.Broadcast))
+                {
+                    bindPreference = addr.Value;
+                    break;
+                }
+                else if ((addr.Key.Address.Equals(IPAddress.Any) || addr.Key.Address.Equals(IPAddress.IPv6Any)) && isInExternalSubnet)
+                {
+                    // External.
+                    bindPreference = addr.Value;
+                    break;
+                }
+                else if (addr.Key.Contains(source))
+                {
+                    // Match ip address.
+                    bindPreference = addr.Value;
+                    break;
+                }
+            }
+
+            if (string.IsNullOrEmpty(bindPreference))
+            {
+                return false;
+            }
+
+            // 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;
+                }
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Attempts to match the source against a user defined bind interface.
+        /// </summary>
+        /// <param name="source">IP source address to use.</param>
+        /// <param name="isInExternalSubnet">True if the source is in the external subnet.</param>
+        /// <param name="result">The result, if a match is found.</param>
+        /// <returns><c>true</c> if a match is found, <c>false</c> otherwise.</returns>
+        private bool MatchesBindInterface(IPObject source, bool isInExternalSubnet, out string result)
+        {
+            result = string.Empty;
+            var addresses = _bindAddresses.Exclude(_bindExclusions);
+
+            int count = addresses.Count;
+            if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses[0].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.
+
+                IPAddress? defaultGateway = null;
+                IPAddress? bindAddress = null;
+
+                if (isInExternalSubnet)
+                {
+                    // Find all external bind addresses. Store the default gateway, but check to see if there is a better match first.
+                    foreach (var addr in addresses.OrderBy(p => p.Tag))
+                    {
+                        if (defaultGateway == null && !IsInLocalNetwork(addr))
+                        {
+                            defaultGateway = addr.Address;
+                        }
+
+                        if (bindAddress == null && addr.Contains(source))
+                        {
+                            bindAddress = addr.Address;
+                        }
+
+                        if (defaultGateway != null && bindAddress != null)
+                        {
+                            break;
+                        }
+                    }
+                }
+                else
+                {
+                    // Look for the best internal address.
+                    bindAddress = addresses
+                        .Where(p => IsInLocalNetwork(p) && (p.Contains(source) || p.Equals(IPAddress.None)))
+                        .OrderBy(p => p.Tag)
+                        .FirstOrDefault()?.Address;
+                }
+
+                if (bindAddress != null)
+                {
+                    result = FormatIP6String(bindAddress);
+                    _logger.LogDebug("{Source}: GetBindInterface: Has source, found a match bind interface subnets. {Result}", source, result);
+                    return true;
+                }
+
+                if (isInExternalSubnet && defaultGateway != null)
+                {
+                    result = FormatIP6String(defaultGateway);
+                    _logger.LogDebug("{Source}: GetBindInterface: Using first user defined external interface. {Result}", source, result);
+                    return true;
+                }
+
+                result = FormatIP6String(addresses[0].Address);
+                _logger.LogDebug("{Source}: GetBindInterface: Selected first user defined interface. {Result}", source, result);
+
+                if (isInExternalSubnet)
+                {
+                    _logger.LogWarning("{Source}: External request received, however, only an internal interface bind found.", source);
+                }
+
+                return true;
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Attempts to match the source against an external interface.
+        /// </summary>
+        /// <param name="source">IP source address to use.</param>
+        /// <param name="result">The result, if a match is found.</param>
+        /// <returns><c>true</c> if a match is found, <c>false</c> otherwise.</returns>
+        private bool MatchesExternalInterface(IPObject source, out string result)
+        {
+            result = string.Empty;
+            // 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(source))
+                    {
+                        result = FormatIP6String(intf.Address);
+                        _logger.LogDebug("{Source}: GetBindInterface: Selected best external on interface on range. {Result}", source, result);
+                        return true;
+                    }
+                }
+
+                result = FormatIP6String(extResult.First().Address);
+                _logger.LogDebug("{Source}: GetBindInterface: Selected first external interface. {Result}", source, result);
+                return true;
+            }
+
+            // Have to return something, so return an internal address
+
+            _logger.LogWarning("{Source}: External request received, however, no WAN interface found.", source);
+            return false;
+        }
+    }
+}

+ 445 - 0
MediaBrowser.Common/Net/IPHost.cs

@@ -0,0 +1,445 @@
+#nullable enable
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net
+{
+    /// <summary>
+    /// Object that holds a host name.
+    /// </summary>
+    public class IPHost : IPObject
+    {
+        /// <summary>
+        /// Gets or sets timeout value before resolve required, in minutes.
+        /// </summary>
+        public const int Timeout = 30;
+
+        /// <summary>
+        /// Represents an IPHost that has no value.
+        /// </summary>
+        public static readonly IPHost None = new IPHost(string.Empty, IPAddress.None);
+
+        /// <summary>
+        /// Time when last resolved in ticks.
+        /// </summary>
+        private DateTime? _lastResolved = null;
+
+        /// <summary>
+        /// Gets the IP Addresses, attempting to resolve the name, if there are none.
+        /// </summary>
+        private IPAddress[] _addresses;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="IPHost"/> class.
+        /// </summary>
+        /// <param name="name">Host name to assign.</param>
+        public IPHost(string name)
+        {
+            HostName = name ?? throw new ArgumentNullException(nameof(name));
+            _addresses = Array.Empty<IPAddress>();
+            Resolved = false;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="IPHost"/> class.
+        /// </summary>
+        /// <param name="name">Host name to assign.</param>
+        /// <param name="address">Address to assign.</param>
+        private IPHost(string name, IPAddress address)
+        {
+            HostName = name ?? throw new ArgumentNullException(nameof(name));
+            _addresses = new IPAddress[] { address ?? throw new ArgumentNullException(nameof(address)) };
+            Resolved = !address.Equals(IPAddress.None);
+        }
+
+        /// <summary>
+        /// Gets or sets the object's first IP address.
+        /// </summary>
+        public override IPAddress Address
+        {
+            get
+            {
+                return ResolveHost() ? this[0] : IPAddress.None;
+            }
+
+            set
+            {
+                // Not implemented, as a host's address is determined by DNS.
+                throw new NotImplementedException("The address of a host is determined by DNS.");
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the object's first IP's subnet prefix.
+        /// The setter does nothing, but shouldn't raise an exception.
+        /// </summary>
+        public override byte PrefixLength
+        {
+            get
+            {
+                return (byte)(ResolveHost() ? 128 : 32);
+            }
+
+            set
+            {
+                // Not implemented, as a host object can only have a prefix length of 128 (IPv6) or 32 (IPv4) prefix length,
+                // which is automatically determined by it's IP type. Anything else is meaningless.
+            }
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether the address has a value.
+        /// </summary>
+        public bool HasAddress => _addresses.Length != 0;
+
+        /// <summary>
+        /// Gets the host name of this object.
+        /// </summary>
+        public string HostName { get; }
+
+        /// <summary>
+        /// Gets a value indicating whether this host has attempted to be resolved.
+        /// </summary>
+        public bool Resolved { get; private set; }
+
+        /// <summary>
+        /// Gets or sets the IP Addresses associated with this object.
+        /// </summary>
+        /// <param name="index">Index of address.</param>
+        public IPAddress this[int index]
+        {
+            get
+            {
+                ResolveHost();
+                return index >= 0 && index < _addresses.Length ? _addresses[index] : IPAddress.None;
+            }
+        }
+
+        /// <summary>
+        /// Attempts to parse the host string.
+        /// </summary>
+        /// <param name="host">Host name to parse.</param>
+        /// <param name="hostObj">Object representing the string, if it has successfully been parsed.</param>
+        /// <returns><c>true</c> if the parsing is successful, <c>false</c> if not.</returns>
+        public static bool TryParse(string host, out IPHost hostObj)
+        {
+            if (!string.IsNullOrEmpty(host))
+            {
+                // See if it's an IPv6 with port address e.g. [::1]:120.
+                int i = host.IndexOf("]:", StringComparison.OrdinalIgnoreCase);
+                if (i != -1)
+                {
+                    return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj);
+                }
+                else
+                {
+                    // See if it's an IPv6 in [] with no port.
+                    i = host.IndexOf(']', StringComparison.OrdinalIgnoreCase);
+                    if (i != -1)
+                    {
+                        return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj);
+                    }
+
+                    // Is it a host or IPv4 with port?
+                    string[] hosts = host.Split(':');
+
+                    if (hosts.Length > 2)
+                    {
+                        hostObj = new IPHost(string.Empty, IPAddress.None);
+                        return false;
+                    }
+
+                    // Remove port from IPv4 if it exists.
+                    host = hosts[0];
+
+                    if (string.Equals("localhost", host, StringComparison.OrdinalIgnoreCase))
+                    {
+                        hostObj = new IPHost(host, new IPAddress(Ipv4Loopback));
+                        return true;
+                    }
+
+                    if (IPNetAddress.TryParse(host, out IPNetAddress netIP))
+                    {
+                        // Host name is an ip address, so fake resolve.
+                        hostObj = new IPHost(host, netIP.Address);
+                        return true;
+                    }
+                }
+
+                // Only thing left is to see if it's a host string.
+                if (!string.IsNullOrEmpty(host))
+                {
+                    // Use regular expression as CheckHostName isn't RFC5892 compliant.
+                    // Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation
+                    Regex re = new Regex(@"^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)$", RegexOptions.IgnoreCase | RegexOptions.Multiline);
+                    if (re.Match(host).Success)
+                    {
+                        hostObj = new IPHost(host);
+                        return true;
+                    }
+                }
+            }
+
+            hostObj = IPHost.None;
+            return false;
+        }
+
+        /// <summary>
+        /// Attempts to parse the host string.
+        /// </summary>
+        /// <param name="host">Host name to parse.</param>
+        /// <returns>Object representing the string, if it has successfully been parsed.</returns>
+        public static IPHost Parse(string host)
+        {
+            if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res))
+            {
+                return res;
+            }
+
+            throw new InvalidCastException("Host does not contain a valid value. {host}");
+        }
+
+        /// <summary>
+        /// Attempts to parse the host string, ensuring that it resolves only to a specific IP type.
+        /// </summary>
+        /// <param name="host">Host name to parse.</param>
+        /// <param name="family">Addressfamily filter.</param>
+        /// <returns>Object representing the string, if it has successfully been parsed.</returns>
+        public static IPHost Parse(string host, AddressFamily family)
+        {
+            if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res))
+            {
+                if (family == AddressFamily.InterNetwork)
+                {
+                    res.Remove(AddressFamily.InterNetworkV6);
+                }
+                else
+                {
+                    res.Remove(AddressFamily.InterNetwork);
+                }
+
+                return res;
+            }
+
+            throw new InvalidCastException("Host does not contain a valid value. {host}");
+        }
+
+        /// <summary>
+        /// Returns the Addresses that this item resolved to.
+        /// </summary>
+        /// <returns>IPAddress Array.</returns>
+        public IPAddress[] GetAddresses()
+        {
+            ResolveHost();
+            return _addresses;
+        }
+
+        /// <inheritdoc/>
+        public override bool Contains(IPAddress address)
+        {
+            if (address != null && !Address.Equals(IPAddress.None))
+            {
+                if (address.IsIPv4MappedToIPv6)
+                {
+                    address = address.MapToIPv4();
+                }
+
+                foreach (var addr in GetAddresses())
+                {
+                    if (address.Equals(addr))
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        /// <inheritdoc/>
+        public override bool Equals(IPObject? other)
+        {
+            if (other is IPHost otherObj)
+            {
+                // Do we have the name Hostname?
+                if (string.Equals(otherObj.HostName, HostName, StringComparison.OrdinalIgnoreCase))
+                {
+                    return true;
+                }
+
+                if (!ResolveHost() || !otherObj.ResolveHost())
+                {
+                    return false;
+                }
+
+                // Do any of our IP addresses match?
+                foreach (IPAddress addr in _addresses)
+                {
+                    foreach (IPAddress otherAddress in otherObj._addresses)
+                    {
+                        if (addr.Equals(otherAddress))
+                        {
+                            return true;
+                        }
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        /// <inheritdoc/>
+        public override bool IsIP6()
+        {
+            // Returns true if interfaces are only IP6.
+            if (ResolveHost())
+            {
+                foreach (IPAddress i in _addresses)
+                {
+                    if (i.AddressFamily != AddressFamily.InterNetworkV6)
+                    {
+                        return false;
+                    }
+                }
+
+                return true;
+            }
+
+            return false;
+        }
+
+        /// <inheritdoc/>
+        public override string ToString()
+        {
+            // StringBuilder not optimum here.
+            string output = string.Empty;
+            if (_addresses.Length > 0)
+            {
+                bool moreThanOne = _addresses.Length > 1;
+                if (moreThanOne)
+                {
+                    output = "[";
+                }
+
+                foreach (var i in _addresses)
+                {
+                    if (Address.Equals(IPAddress.None) && Address.AddressFamily == AddressFamily.Unspecified)
+                    {
+                        output += HostName + ",";
+                    }
+                    else if (i.Equals(IPAddress.Any))
+                    {
+                        output += "Any IP4 Address,";
+                    }
+                    else if (Address.Equals(IPAddress.IPv6Any))
+                    {
+                        output += "Any IP6 Address,";
+                    }
+                    else if (i.Equals(IPAddress.Broadcast))
+                    {
+                        output += "Any Address,";
+                    }
+                    else
+                    {
+                        output += $"{i}/32,";
+                    }
+                }
+
+                output = output[0..^1];
+
+                if (moreThanOne)
+                {
+                    output += "]";
+                }
+            }
+            else
+            {
+                output = HostName;
+            }
+
+            return output;
+        }
+
+        /// <inheritdoc/>
+        public override void Remove(AddressFamily family)
+        {
+            if (ResolveHost())
+            {
+                _addresses = _addresses.Where(p => p.AddressFamily != family).ToArray();
+            }
+        }
+
+        /// <inheritdoc/>
+        public override bool Contains(IPObject address)
+        {
+            // An IPHost cannot contain another IPObject, it can only be equal.
+            return Equals(address);
+        }
+
+        /// <inheritdoc/>
+        protected override IPObject CalculateNetworkAddress()
+        {
+            var netAddr = NetworkAddressOf(this[0], PrefixLength);
+            return new IPNetAddress(netAddr.Address, netAddr.PrefixLength);
+        }
+
+        /// <summary>
+        /// Attempt to resolve the ip address of a host.
+        /// </summary>
+        /// <returns><c>true</c> if any addresses have been resolved, otherwise <c>false</c>.</returns>
+        private bool ResolveHost()
+        {
+            // When was the last time we resolved?
+            if (_lastResolved == null)
+            {
+                _lastResolved = DateTime.UtcNow;
+            }
+
+            // If we haven't resolved before, or our timer has run out...
+            if ((_addresses.Length == 0 && !Resolved) || (DateTime.UtcNow > _lastResolved?.AddMinutes(Timeout)))
+            {
+                _lastResolved = DateTime.UtcNow;
+                ResolveHostInternal().GetAwaiter().GetResult();
+                Resolved = true;
+            }
+
+            return _addresses.Length > 0;
+        }
+
+        /// <summary>
+        /// Task that looks up a Host name and returns its IP addresses.
+        /// </summary>
+        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+        private async Task ResolveHostInternal()
+        {
+            if (!string.IsNullOrEmpty(HostName))
+            {
+                // Resolves the host name - so save a DNS lookup.
+                if (string.Equals(HostName, "localhost", StringComparison.OrdinalIgnoreCase))
+                {
+                    _addresses = new IPAddress[] { new IPAddress(Ipv4Loopback), new IPAddress(Ipv6Loopback) };
+                    return;
+                }
+
+                if (Uri.CheckHostName(HostName).Equals(UriHostNameType.Dns))
+                {
+                    try
+                    {
+                        IPHostEntry ip = await Dns.GetHostEntryAsync(HostName).ConfigureAwait(false);
+                        _addresses = ip.AddressList;
+                    }
+                    catch (SocketException ex)
+                    {
+                        // Log and then ignore socket errors, as the result value will just be an empty array.
+                        Debug.WriteLine("GetHostEntryAsync failed with {Message}.", ex.Message);
+                    }
+                }
+            }
+        }
+    }
+}

+ 277 - 0
MediaBrowser.Common/Net/IPNetAddress.cs

@@ -0,0 +1,277 @@
+#nullable enable
+using System;
+using System.Net;
+using System.Net.Sockets;
+
+namespace MediaBrowser.Common.Net
+{
+    /// <summary>
+    /// An object that holds and IP address and subnet mask.
+    /// </summary>
+    public class IPNetAddress : IPObject
+    {
+        /// <summary>
+        /// Represents an IPNetAddress that has no value.
+        /// </summary>
+        public static readonly IPNetAddress None = new IPNetAddress(IPAddress.None);
+
+        /// <summary>
+        /// IPv4 multicast address.
+        /// </summary>
+        public static readonly IPAddress SSDPMulticastIPv4 = IPAddress.Parse("239.255.255.250");
+
+        /// <summary>
+        /// IPv6 local link multicast address.
+        /// </summary>
+        public static readonly IPAddress SSDPMulticastIPv6LinkLocal = IPAddress.Parse("ff02::C");
+
+        /// <summary>
+        /// IPv6 site local multicast address.
+        /// </summary>
+        public static readonly IPAddress SSDPMulticastIPv6SiteLocal = IPAddress.Parse("ff05::C");
+
+        /// <summary>
+        /// IP4Loopback address host.
+        /// </summary>
+        public static readonly IPNetAddress IP4Loopback = IPNetAddress.Parse("127.0.0.1/32");
+
+        /// <summary>
+        /// IP6Loopback address host.
+        /// </summary>
+        public static readonly IPNetAddress IP6Loopback = IPNetAddress.Parse("::1");
+
+        /// <summary>
+        /// Object's IP address.
+        /// </summary>
+        private IPAddress _address;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="IPNetAddress"/> class.
+        /// </summary>
+        /// <param name="address">Address to assign.</param>
+        public IPNetAddress(IPAddress address)
+        {
+            _address = address ?? throw new ArgumentNullException(nameof(address));
+            PrefixLength = (byte)(address.AddressFamily == AddressFamily.InterNetwork ? 32 : 128);
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="IPNetAddress"/> class.
+        /// </summary>
+        /// <param name="address">IP Address.</param>
+        /// <param name="prefixLength">Mask as a CIDR.</param>
+        public IPNetAddress(IPAddress address, byte prefixLength)
+        {
+            if (address?.IsIPv4MappedToIPv6 ?? throw new ArgumentNullException(nameof(address)))
+            {
+                _address = address.MapToIPv4();
+            }
+            else
+            {
+                _address = address;
+            }
+
+            PrefixLength = prefixLength;
+        }
+
+        /// <summary>
+        /// Gets or sets the object's IP address.
+        /// </summary>
+        public override IPAddress Address
+        {
+            get
+            {
+                return _address;
+            }
+
+            set
+            {
+                _address = value ?? IPAddress.None;
+            }
+        }
+
+        /// <inheritdoc/>
+        public override byte PrefixLength { get; set; }
+
+        /// <summary>
+        /// Try to parse the address and subnet strings into an IPNetAddress object.
+        /// </summary>
+        /// <param name="addr">IP address to parse. Can be CIDR or X.X.X.X notation.</param>
+        /// <param name="ip">Resultant object.</param>
+        /// <returns>True if the values parsed successfully. False if not, resulting in the IP being null.</returns>
+        public static bool TryParse(string addr, out IPNetAddress ip)
+        {
+            if (!string.IsNullOrEmpty(addr))
+            {
+                addr = addr.Trim();
+
+                // Try to parse it as is.
+                if (IPAddress.TryParse(addr, out IPAddress? res))
+                {
+                    ip = new IPNetAddress(res);
+                    return true;
+                }
+
+                // Is it a network?
+                string[] tokens = addr.Split("/");
+
+                if (tokens.Length == 2)
+                {
+                    tokens[0] = tokens[0].TrimEnd();
+                    tokens[1] = tokens[1].TrimStart();
+
+                    if (IPAddress.TryParse(tokens[0], out res))
+                    {
+                        // Is the subnet part a cidr?
+                        if (byte.TryParse(tokens[1], out byte cidr))
+                        {
+                            ip = new IPNetAddress(res, cidr);
+                            return true;
+                        }
+
+                        // Is the subnet in x.y.a.b form?
+                        if (IPAddress.TryParse(tokens[1], out IPAddress? mask))
+                        {
+                            ip = new IPNetAddress(res, MaskToCidr(mask));
+                            return true;
+                        }
+                    }
+                }
+            }
+
+            ip = None;
+            return false;
+        }
+
+        /// <summary>
+        /// Parses the string provided, throwing an exception if it is badly formed.
+        /// </summary>
+        /// <param name="addr">String to parse.</param>
+        /// <returns>IPNetAddress object.</returns>
+        public static IPNetAddress Parse(string addr)
+        {
+            if (TryParse(addr, out IPNetAddress o))
+            {
+                return o;
+            }
+
+            throw new ArgumentException("Unable to recognise object :" + addr);
+        }
+
+        /// <inheritdoc/>
+        public override bool Contains(IPAddress address)
+        {
+            if (address == null)
+            {
+                throw new ArgumentNullException(nameof(address));
+            }
+
+            if (address.IsIPv4MappedToIPv6)
+            {
+                address = address.MapToIPv4();
+            }
+
+            var altAddress = NetworkAddressOf(address, PrefixLength);
+            return NetworkAddress.Address.Equals(altAddress.Address) && NetworkAddress.PrefixLength >= altAddress.PrefixLength;
+        }
+
+        /// <inheritdoc/>
+        public override bool Contains(IPObject address)
+        {
+            if (address is IPHost addressObj && addressObj.HasAddress)
+            {
+                foreach (IPAddress addr in addressObj.GetAddresses())
+                {
+                    if (Contains(addr))
+                    {
+                        return true;
+                    }
+                }
+            }
+            else if (address is IPNetAddress netaddrObj)
+            {
+                // Have the same network address, but different subnets?
+                if (NetworkAddress.Address.Equals(netaddrObj.NetworkAddress.Address))
+                {
+                    return NetworkAddress.PrefixLength <= netaddrObj.PrefixLength;
+                }
+
+                var altAddress = NetworkAddressOf(netaddrObj.Address, PrefixLength);
+                return NetworkAddress.Address.Equals(altAddress.Address);
+            }
+
+            return false;
+        }
+
+        /// <inheritdoc/>
+        public override bool Equals(IPObject? other)
+        {
+            if (other is IPNetAddress otherObj && !Address.Equals(IPAddress.None) && !otherObj.Address.Equals(IPAddress.None))
+            {
+                return Address.Equals(otherObj.Address) &&
+                    PrefixLength == otherObj.PrefixLength;
+            }
+
+            return false;
+        }
+
+        /// <inheritdoc/>
+        public override bool Equals(IPAddress address)
+        {
+            if (address != null && !address.Equals(IPAddress.None) && !Address.Equals(IPAddress.None))
+            {
+                return address.Equals(Address);
+            }
+
+            return false;
+        }
+
+        /// <inheritdoc/>
+        public override string ToString()
+        {
+            return ToString(false);
+        }
+
+        /// <summary>
+        /// Returns a textual representation of this object.
+        /// </summary>
+        /// <param name="shortVersion">Set to true, if the subnet is to be excluded as part of the address.</param>
+        /// <returns>String representation of this object.</returns>
+        public string ToString(bool shortVersion)
+        {
+            if (!Address.Equals(IPAddress.None))
+            {
+                if (Address.Equals(IPAddress.Any))
+                {
+                    return "Any IP4 Address";
+                }
+
+                if (Address.Equals(IPAddress.IPv6Any))
+                {
+                    return "Any IP6 Address";
+                }
+
+                if (Address.Equals(IPAddress.Broadcast))
+                {
+                    return "Any Address";
+                }
+
+                if (shortVersion)
+                {
+                    return Address.ToString();
+                }
+
+                return $"{Address}/{PrefixLength}";
+            }
+
+            return string.Empty;
+        }
+
+        /// <inheritdoc/>
+        protected override IPObject CalculateNetworkAddress()
+        {
+            var value = NetworkAddressOf(_address, PrefixLength);
+            return new IPNetAddress(value.Address, value.PrefixLength);
+        }
+    }
+}

+ 406 - 0
MediaBrowser.Common/Net/IPObject.cs

@@ -0,0 +1,406 @@
+#nullable enable
+using System;
+using System.Net;
+using System.Net.Sockets;
+
+namespace MediaBrowser.Common.Net
+{
+    /// <summary>
+    /// Base network object class.
+    /// </summary>
+    public abstract class IPObject : IEquatable<IPObject>
+    {
+        /// <summary>
+        /// IPv6 Loopback address.
+        /// </summary>
+        protected static readonly byte[] Ipv6Loopback = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
+
+        /// <summary>
+        /// IPv4 Loopback address.
+        /// </summary>
+        protected static readonly byte[] Ipv4Loopback = { 127, 0, 0, 1 };
+
+        /// <summary>
+        /// The network address of this object.
+        /// </summary>
+        private IPObject? _networkAddress;
+
+        /// <summary>
+        /// Gets or sets a user defined value that is associated with this object.
+        /// </summary>
+        public int Tag { get; set; }
+
+        /// <summary>
+        /// Gets or sets the object's IP address.
+        /// </summary>
+        public abstract IPAddress Address { get; set; }
+
+        /// <summary>
+        /// Gets the object's network address.
+        /// </summary>
+        public IPObject NetworkAddress => _networkAddress ??= CalculateNetworkAddress();
+
+        /// <summary>
+        /// Gets or sets the object's IP address.
+        /// </summary>
+        public abstract byte PrefixLength { get; set; }
+
+        /// <summary>
+        /// Gets the AddressFamily of this object.
+        /// </summary>
+        public AddressFamily AddressFamily
+        {
+            get
+            {
+                // Keep terms separate as Address performs other functions in inherited objects.
+                IPAddress address = Address;
+                return address.Equals(IPAddress.None) ? AddressFamily.Unspecified : address.AddressFamily;
+            }
+        }
+
+        /// <summary>
+        /// Returns the network address of an object.
+        /// </summary>
+        /// <param name="address">IP Address to convert.</param>
+        /// <param name="prefixLength">Subnet prefix.</param>
+        /// <returns>IPAddress.</returns>
+        public static (IPAddress Address, byte PrefixLength) NetworkAddressOf(IPAddress address, byte prefixLength)
+        {
+            if (address == null)
+            {
+                throw new ArgumentNullException(nameof(address));
+            }
+
+            if (address.IsIPv4MappedToIPv6)
+            {
+                address = address.MapToIPv4();
+            }
+
+            if (IsLoopback(address))
+            {
+                return (Address: address, PrefixLength: prefixLength);
+            }
+
+            // An ip address is just a list of bytes, each one representing a segment on the network.
+            // This separates the IP address into octets and calculates how many octets will need to be altered or set to zero dependant upon the
+            // prefix length value. eg. /16 on a 4 octet ip4 address (192.168.2.240) will result in the 2 and the 240 being zeroed out.
+            // Where there is not an exact boundary (eg /23), mod is used to calculate how many bits of this value are to be kept.
+
+            // GetAddressBytes
+            Span<byte> addressBytes = stackalloc byte[address.AddressFamily == AddressFamily.InterNetwork ? 4 : 16];
+            address.TryWriteBytes(addressBytes, out _);
+
+            int div = prefixLength / 8;
+            int mod = prefixLength % 8;
+            if (mod != 0)
+            {
+                // Prefix length is counted right to left, so subtract 8 so we know how many bits to clear.
+                mod = 8 - mod;
+
+                // Shift out the bits from the octet that we don't want, by moving right then back left.
+                addressBytes[div] = (byte)((int)addressBytes[div] >> mod << mod);
+                // Move on the next byte.
+                div++;
+            }
+
+            // Blank out the remaining octets from mod + 1 to the end of the byte array. (192.168.2.240/16 becomes 192.168.0.0)
+            for (int octet = div; octet < addressBytes.Length; octet++)
+            {
+                addressBytes[octet] = 0;
+            }
+
+            // Return the network address for the prefix.
+            return (Address: new IPAddress(addressBytes), PrefixLength: prefixLength);
+        }
+
+        /// <summary>
+        /// Tests to see if the ip address is a Loopback address.
+        /// </summary>
+        /// <param name="address">Value to test.</param>
+        /// <returns>True if it is.</returns>
+        public static bool IsLoopback(IPAddress address)
+        {
+            if (address == null)
+            {
+                throw new ArgumentNullException(nameof(address));
+            }
+
+            if (!address.Equals(IPAddress.None))
+            {
+                if (address.IsIPv4MappedToIPv6)
+                {
+                    address = address.MapToIPv4();
+                }
+
+                return address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback);
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Tests to see if the ip address is an IP6 address.
+        /// </summary>
+        /// <param name="address">Value to test.</param>
+        /// <returns>True if it is.</returns>
+        public static bool IsIP6(IPAddress address)
+        {
+            if (address == null)
+            {
+                throw new ArgumentNullException(nameof(address));
+            }
+
+            if (address.IsIPv4MappedToIPv6)
+            {
+                address = address.MapToIPv4();
+            }
+
+            return !address.Equals(IPAddress.None) && (address.AddressFamily == AddressFamily.InterNetworkV6);
+        }
+
+        /// <summary>
+        /// Tests to see if the address in the private address range.
+        /// </summary>
+        /// <param name="address">Object to test.</param>
+        /// <returns>True if it contains a private address.</returns>
+        public static bool IsPrivateAddressRange(IPAddress address)
+        {
+            if (address == null)
+            {
+                throw new ArgumentNullException(nameof(address));
+            }
+
+            if (!address.Equals(IPAddress.None))
+            {
+                if (address.IsIPv4MappedToIPv6)
+                {
+                    address = address.MapToIPv4();
+                }
+
+                if (address.AddressFamily == AddressFamily.InterNetwork)
+                {
+                    // GetAddressBytes
+                    Span<byte> octet = stackalloc byte[4];
+                    address.TryWriteBytes(octet, out _);
+
+                    return (octet[0] == 10)
+                           || (octet[0] == 172 && octet[1] >= 16 && octet[1] <= 31) // RFC1918
+                           || (octet[0] == 192 && octet[1] == 168) // RFC1918
+                           || (octet[0] == 127); // RFC1122
+                }
+                else
+                {
+                    // GetAddressBytes
+                    Span<byte> octet = stackalloc byte[16];
+                    address.TryWriteBytes(octet, out _);
+
+                    uint word = (uint)(octet[0] << 8) + octet[1];
+
+                    return (word >= 0xfe80 && word <= 0xfebf) // fe80::/10 :Local link.
+                           || (word >= 0xfc00 && word <= 0xfdff); // fc00::/7 :Unique local address.
+                }
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Returns true if the IPAddress contains an IP6 Local link address.
+        /// </summary>
+        /// <param name="address">IPAddress object to check.</param>
+        /// <returns>True if it is a local link address.</returns>
+        /// <remarks>
+        /// See https://stackoverflow.com/questions/6459928/explain-the-instance-properties-of-system-net-ipaddress
+        /// it appears that the IPAddress.IsIPv6LinkLocal is out of date.
+        /// </remarks>
+        public static bool IsIPv6LinkLocal(IPAddress address)
+        {
+            if (address == null)
+            {
+                throw new ArgumentNullException(nameof(address));
+            }
+
+            if (address.IsIPv4MappedToIPv6)
+            {
+                address = address.MapToIPv4();
+            }
+
+            if (address.AddressFamily != AddressFamily.InterNetworkV6)
+            {
+                return false;
+            }
+
+            // GetAddressBytes
+            Span<byte> octet = stackalloc byte[16];
+            address.TryWriteBytes(octet, out _);
+            uint word = (uint)(octet[0] << 8) + octet[1];
+
+            return word >= 0xfe80 && word <= 0xfebf; // fe80::/10 :Local link.
+        }
+
+        /// <summary>
+        /// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only.
+        /// </summary>
+        /// <param name="cidr">Subnet mask in CIDR notation.</param>
+        /// <param name="family">IPv4 or IPv6 family.</param>
+        /// <returns>String value of the subnet mask in dotted decimal notation.</returns>
+        public static IPAddress CidrToMask(byte cidr, AddressFamily family)
+        {
+            uint addr = 0xFFFFFFFF << (family == AddressFamily.InterNetwork ? 32 : 128 - cidr);
+            addr = ((addr & 0xff000000) >> 24)
+                   | ((addr & 0x00ff0000) >> 8)
+                   | ((addr & 0x0000ff00) << 8)
+                   | ((addr & 0x000000ff) << 24);
+            return new IPAddress(addr);
+        }
+
+        /// <summary>
+        /// Convert a mask to a CIDR. IPv4 only.
+        /// https://stackoverflow.com/questions/36954345/get-cidr-from-netmask.
+        /// </summary>
+        /// <param name="mask">Subnet mask.</param>
+        /// <returns>Byte CIDR representing the mask.</returns>
+        public static byte MaskToCidr(IPAddress mask)
+        {
+            if (mask == null)
+            {
+                throw new ArgumentNullException(nameof(mask));
+            }
+
+            byte cidrnet = 0;
+            if (!mask.Equals(IPAddress.Any))
+            {
+                // GetAddressBytes
+                Span<byte> bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? 4 : 16];
+                mask.TryWriteBytes(bytes, out _);
+
+                var zeroed = false;
+                for (var i = 0; i < bytes.Length; i++)
+                {
+                    for (int v = bytes[i]; (v & 0xFF) != 0; v <<= 1)
+                    {
+                        if (zeroed)
+                        {
+                            // Invalid netmask.
+                            return (byte)~cidrnet;
+                        }
+
+                        if ((v & 0x80) == 0)
+                        {
+                            zeroed = true;
+                        }
+                        else
+                        {
+                            cidrnet++;
+                        }
+                    }
+                }
+            }
+
+            return cidrnet;
+        }
+
+        /// <summary>
+        /// Tests to see if this object is a Loopback address.
+        /// </summary>
+        /// <returns>True if it is.</returns>
+        public virtual bool IsLoopback()
+        {
+            return IsLoopback(Address);
+        }
+
+        /// <summary>
+        /// Removes all addresses of a specific type from this object.
+        /// </summary>
+        /// <param name="family">Type of address to remove.</param>
+        public virtual void Remove(AddressFamily family)
+        {
+            // This method only performs a function in the IPHost implementation of IPObject.
+        }
+
+        /// <summary>
+        /// Tests to see if this object is an IPv6 address.
+        /// </summary>
+        /// <returns>True if it is.</returns>
+        public virtual bool IsIP6()
+        {
+            return IsIP6(Address);
+        }
+
+        /// <summary>
+        /// Returns true if this IP address is in the RFC private address range.
+        /// </summary>
+        /// <returns>True this object has a private address.</returns>
+        public virtual bool IsPrivateAddressRange()
+        {
+            return IsPrivateAddressRange(Address);
+        }
+
+        /// <summary>
+        /// Compares this to the object passed as a parameter.
+        /// </summary>
+        /// <param name="ip">Object to compare to.</param>
+        /// <returns>Equality result.</returns>
+        public virtual bool Equals(IPAddress ip)
+        {
+            if (ip != null)
+            {
+                if (ip.IsIPv4MappedToIPv6)
+                {
+                    ip = ip.MapToIPv4();
+                }
+
+                return !Address.Equals(IPAddress.None) && Address.Equals(ip);
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Compares this to the object passed as a parameter.
+        /// </summary>
+        /// <param name="other">Object to compare to.</param>
+        /// <returns>Equality result.</returns>
+        public virtual bool Equals(IPObject? other)
+        {
+            if (other != null)
+            {
+                return !Address.Equals(IPAddress.None) && Address.Equals(other.Address);
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Compares the address in this object and the address in the object passed as a parameter.
+        /// </summary>
+        /// <param name="address">Object's IP address to compare to.</param>
+        /// <returns>Comparison result.</returns>
+        public abstract bool Contains(IPObject address);
+
+        /// <summary>
+        /// Compares the address in this object and the address in the object passed as a parameter.
+        /// </summary>
+        /// <param name="address">Object's IP address to compare to.</param>
+        /// <returns>Comparison result.</returns>
+        public abstract bool Contains(IPAddress address);
+
+        /// <inheritdoc/>
+        public override int GetHashCode()
+        {
+            return Address.GetHashCode();
+        }
+
+        /// <inheritdoc/>
+        public override bool Equals(object? obj)
+        {
+            return Equals(obj as IPObject);
+        }
+
+        /// <summary>
+        /// Calculates the network address of this object.
+        /// </summary>
+        /// <returns>Returns the network address of this object.</returns>
+        protected abstract IPObject CalculateNetworkAddress();
+    }
+}

+ 262 - 0
MediaBrowser.Common/Net/NetworkExtensions.cs

@@ -0,0 +1,262 @@
+#pragma warning disable CA1062 // Validate arguments of public methods
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Net;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace MediaBrowser.Common.Net
+{
+    /// <summary>
+    /// Defines the <see cref="NetworkExtensions" />.
+    /// </summary>
+    public static class NetworkExtensions
+    {
+        /// <summary>
+        /// Add an address to the collection.
+        /// </summary>
+        /// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
+        /// <param name="ip">Item to add.</param>
+        public static void AddItem(this Collection<IPObject> source, IPAddress ip)
+        {
+            if (!source.ContainsAddress(ip))
+            {
+                source.Add(new IPNetAddress(ip, 32));
+            }
+        }
+
+        /// <summary>
+        /// Adds a network to the collection.
+        /// </summary>
+        /// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
+        /// <param name="item">Item to add.</param>
+        public static void AddItem(this Collection<IPObject> source, IPObject item)
+        {
+            if (!source.ContainsAddress(item))
+            {
+                source.Add(item);
+            }
+        }
+
+        /// <summary>
+        /// Converts this object to a string.
+        /// </summary>
+        /// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
+        /// <returns>Returns a string representation of this object.</returns>
+        public static string AsString(this Collection<IPObject> source)
+        {
+            return $"[{string.Join(',', source)}]";
+        }
+
+        /// <summary>
+        /// Returns true if the collection contains an item with the ip address,
+        /// or the ip address falls within any of the collection's network ranges.
+        /// </summary>
+        /// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
+        /// <param name="item">The item to look for.</param>
+        /// <returns>True if the collection contains the item.</returns>
+        public static bool ContainsAddress(this Collection<IPObject> source, IPAddress item)
+        {
+            if (source.Count == 0)
+            {
+                return false;
+            }
+
+            if (item == null)
+            {
+                throw new ArgumentNullException(nameof(item));
+            }
+
+            if (item.IsIPv4MappedToIPv6)
+            {
+                item = item.MapToIPv4();
+            }
+
+            foreach (var i in source)
+            {
+                if (i.Contains(item))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Returns true if the collection contains an item with the ip address,
+        /// or the ip address falls within any of the collection's network ranges.
+        /// </summary>
+        /// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
+        /// <param name="item">The item to look for.</param>
+        /// <returns>True if the collection contains the item.</returns>
+        public static bool ContainsAddress(this Collection<IPObject> source, IPObject item)
+        {
+            if (source.Count == 0)
+            {
+                return false;
+            }
+
+            if (item == null)
+            {
+                throw new ArgumentNullException(nameof(item));
+            }
+
+            foreach (var i in source)
+            {
+                if (i.Contains(item))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Compares two Collection{IPObject} objects. The order is ignored.
+        /// </summary>
+        /// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
+        /// <param name="dest">Item to compare to.</param>
+        /// <returns>True if both are equal.</returns>
+        public static bool Compare(this Collection<IPObject> source, Collection<IPObject> dest)
+        {
+            if (dest == null || source.Count != dest.Count)
+            {
+                return false;
+            }
+
+            foreach (var sourceItem in source)
+            {
+                bool found = false;
+                foreach (var destItem in dest)
+                {
+                    if (sourceItem.Equals(destItem))
+                    {
+                        found = true;
+                        break;
+                    }
+                }
+
+                if (!found)
+                {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Returns a collection containing the subnets of this collection given.
+        /// </summary>
+        /// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
+        /// <returns>Collection{IPObject} object containing the subnets.</returns>
+        public static Collection<IPObject> AsNetworks(this Collection<IPObject> source)
+        {
+            if (source == null)
+            {
+                throw new ArgumentNullException(nameof(source));
+            }
+
+            Collection<IPObject> res = new Collection<IPObject>();
+
+            foreach (IPObject i in source)
+            {
+                if (i is IPNetAddress nw)
+                {
+                    // Add the subnet calculated from the interface address/mask.
+                    var na = nw.NetworkAddress;
+                    na.Tag = i.Tag;
+                    res.AddItem(na);
+                }
+                else if (i is IPHost ipHost)
+                {
+                    // Flatten out IPHost and add all its ip addresses.
+                    foreach (var addr in ipHost.GetAddresses())
+                    {
+                        IPNetAddress host = new IPNetAddress(addr)
+                        {
+                            Tag = i.Tag
+                        };
+
+                        res.AddItem(host);
+                    }
+                }
+            }
+
+            return res;
+        }
+
+        /// <summary>
+        /// Excludes all the items from this list that are found in excludeList.
+        /// </summary>
+        /// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
+        /// <param name="excludeList">Items to exclude.</param>
+        /// <returns>A new collection, with the items excluded.</returns>
+        public static Collection<IPObject> Exclude(this Collection<IPObject> source, Collection<IPObject> excludeList)
+        {
+            if (source.Count == 0 || excludeList == null)
+            {
+                return new Collection<IPObject>(source);
+            }
+
+            Collection<IPObject> results = new Collection<IPObject>();
+
+            bool found;
+            foreach (var outer in source)
+            {
+                found = false;
+
+                foreach (var inner in excludeList)
+                {
+                    if (outer.Equals(inner))
+                    {
+                        found = true;
+                        break;
+                    }
+                }
+
+                if (!found)
+                {
+                    results.AddItem(outer);
+                }
+            }
+
+            return results;
+        }
+
+        /// <summary>
+        /// Returns all items that co-exist in this object and target.
+        /// </summary>
+        /// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
+        /// <param name="target">Collection to compare with.</param>
+        /// <returns>A collection containing all the matches.</returns>
+        public static Collection<IPObject> Union(this Collection<IPObject> source, Collection<IPObject> target)
+        {
+            if (source.Count == 0)
+            {
+                return new Collection<IPObject>();
+            }
+
+            if (target == null)
+            {
+                throw new ArgumentNullException(nameof(target));
+            }
+
+            Collection<IPObject> nc = new Collection<IPObject>();
+
+            foreach (IPObject i in source)
+            {
+                if (target.ContainsAddress(i))
+                {
+                    nc.AddItem(i);
+                }
+            }
+
+            return nc;
+        }
+    }
+}

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

@@ -0,0 +1,20 @@
+#nullable enable
+
+namespace MediaBrowser.Model.Configuration
+{
+    /// <summary>
+    /// Defines the <see cref="PathSubstitution" />.
+    /// </summary>
+    public class PathSubstitution
+    {
+        /// <summary>
+        /// Gets or sets the value to substitute.
+        /// </summary>
+        public string From { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Gets or sets the value to substitution with.
+        /// </summary>
+        public string To { get; set; } = string.Empty;
+    }
+}

+ 227 - 183
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -1,5 +1,5 @@
-#nullable disable
 #pragma warning disable CS1591
 #pragma warning disable CS1591
+#pragma warning disable CA1819
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -13,43 +13,194 @@ namespace MediaBrowser.Model.Configuration
     /// </summary>
     /// </summary>
     public class ServerConfiguration : BaseApplicationConfiguration
     public class ServerConfiguration : BaseApplicationConfiguration
     {
     {
+        /// <summary>
+        /// The default value for <see cref="HttpServerPortNumber"/>.
+        /// </summary>
         public const int DefaultHttpPort = 8096;
         public const int DefaultHttpPort = 8096;
+
+        /// <summary>
+        /// The default value for <see cref="PublicHttpsPort"/> and <see cref="HttpsPortNumber"/>.
+        /// </summary>
         public const int DefaultHttpsPort = 8920;
         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>
         /// <summary>
         /// Gets or sets a value indicating whether to enable automatic port forwarding.
         /// Gets or sets a value indicating whether to enable automatic port forwarding.
         /// </summary>
         /// </summary>
-        public bool EnableUPnP { get; set; }
+        public bool EnableUPnP { get; set; } = false;
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets a value indicating whether to enable prometheus metrics exporting.
         /// Gets or sets a value indicating whether to enable prometheus metrics exporting.
         /// </summary>
         /// </summary>
-        public bool EnableMetrics { get; set; }
+        public bool EnableMetrics { get; set; } = false;
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the public mapped port.
         /// Gets or sets the public mapped port.
         /// </summary>
         /// </summary>
         /// <value>The public mapped port.</value>
         /// <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 IPV6 capability is enabled.
+        /// </summary>
+        public bool EnableIPV6 { get; set; } = false;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether IPV4 capability is enabled.
+        /// </summary>
+        public bool EnableIPV4 { get; set; } = true;
+
+        /// <summary>
+        /// Gets or sets a value indicating whether detailed ssdp logs are sent to the console/log.
+        /// "Emby.Dlna": "Debug" must 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 a value indicating whether address names that match <see cref="VirtualInterfaceNames"/> should be Ignore for the purposes of binding.
+        /// </summary>
+        public bool IgnoreVirtualInterfaces { get; set; } = true;
+
+        /// <summary>
+        /// Gets or sets a value indicating the interfaces that should be ignored. The list can be comma separated. <seealso cref="IgnoreVirtualInterfaces"/>.
+        /// </summary>
+        public string VirtualInterfaceNames { get; set; } = "vEthernet*";
+
+        /// <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 multi-socket binding is 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 Autodiscovery tracing is enabled.
+        /// </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>
         /// <summary>
         /// Gets or sets the public HTTPS port.
         /// Gets or sets the public HTTPS port.
         /// </summary>
         /// </summary>
         /// <value>The public HTTPS port.</value>
         /// <value>The public HTTPS port.</value>
-        public int PublicHttpsPort { get; set; }
+        public int PublicHttpsPort { get; set; } = DefaultHttpsPort;
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the HTTP server port number.
         /// Gets or sets the HTTP server port number.
         /// </summary>
         /// </summary>
         /// <value>The HTTP server port number.</value>
         /// <value>The HTTP server port number.</value>
-        public int HttpServerPortNumber { get; set; }
+        public int HttpServerPortNumber { get; set; } = DefaultHttpPort;
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the HTTPS server port number.
         /// Gets or sets the HTTPS server port number.
         /// </summary>
         /// </summary>
         /// <value>The HTTPS server port number.</value>
         /// <value>The HTTPS server port number.</value>
-        public int HttpsPortNumber { get; set; }
+        public int HttpsPortNumber { get; set; } = DefaultHttpsPort;
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets a value indicating whether to use HTTPS.
         /// Gets or sets a value indicating whether to use HTTPS.
@@ -58,19 +209,19 @@ namespace MediaBrowser.Model.Configuration
         /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be
         /// 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"/>.
         /// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>.
         /// </remarks>
         /// </remarks>
-        public bool EnableHttps { get; set; }
+        public bool EnableHttps { get; set; } = false;
 
 
-        public bool EnableNormalizedItemByNameIds { get; set; }
+        public bool EnableNormalizedItemByNameIds { get; set; } = true;
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the filesystem path of an X.509 certificate to use for SSL.
         /// Gets or sets the filesystem path of an X.509 certificate to use for SSL.
         /// </summary>
         /// </summary>
-        public string CertificatePath { get; set; }
+        public string CertificatePath { get; set; } = string.Empty;
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>.
         /// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>.
         /// </summary>
         /// </summary>
-        public string CertificatePassword { get; set; }
+        public string CertificatePassword { get; set; } = string.Empty;
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets a value indicating whether this instance is port authorized.
         /// Gets or sets a value indicating whether this instance is port authorized.
@@ -79,90 +230,93 @@ namespace MediaBrowser.Model.Configuration
         public bool IsPortAuthorized { get; set; }
         public bool IsPortAuthorized { get; set; }
 
 
         /// <summary>
         /// <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>
         /// </summary>
-        public bool QuickConnectAvailable { get; set; }
-
-        public bool EnableRemoteAccess { get; set; }
+        public bool QuickConnectAvailable { get; set; } = false;
+       
+        /// <summary>
+        /// Gets or sets a value indicating whether access outside of the LAN is permitted.
+        /// </summary>
+        public bool EnableRemoteAccess { get; set; } = true;
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets a value indicating whether [enable case sensitive item ids].
         /// Gets or sets a value indicating whether [enable case sensitive item ids].
         /// </summary>
         /// </summary>
         /// <value><c>true</c> if [enable case sensitive item ids]; otherwise, <c>false</c>.</value>
         /// <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>
         /// <summary>
         /// Gets or sets the metadata path.
         /// Gets or sets the metadata path.
         /// </summary>
         /// </summary>
         /// <value>The metadata path.</value>
         /// <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>
         /// <summary>
         /// Gets or sets the preferred metadata language.
         /// Gets or sets the preferred metadata language.
         /// </summary>
         /// </summary>
         /// <value>The preferred metadata language.</value>
         /// <value>The preferred metadata language.</value>
-        public string PreferredMetadataLanguage { get; set; }
+        public string PreferredMetadataLanguage { get; set; } = string.Empty;
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the metadata country code.
         /// Gets or sets the metadata country code.
         /// </summary>
         /// </summary>
         /// <value>The metadata country code.</value>
         /// <value>The metadata country code.</value>
-        public string MetadataCountryCode { get; set; }
+        public string MetadataCountryCode { get; set; } = "US";
 
 
         /// <summary>
         /// <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>
         /// </summary>
         /// <value>The sort replace characters.</value>
         /// <value>The sort replace characters.</value>
-        public string[] SortReplaceCharacters { get; set; }
+        public string[] SortReplaceCharacters { get; set; } = new[] { ".", "+", "%" };
 
 
         /// <summary>
         /// <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>
         /// </summary>
         /// <value>The sort remove characters.</value>
         /// <value>The sort remove characters.</value>
-        public string[] SortRemoveCharacters { get; set; }
+        public string[] SortRemoveCharacters { get; set; } = new[] { ",", "&", "-", "{", "}", "'" };
 
 
         /// <summary>
         /// <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>
         /// </summary>
         /// <value>The sort remove words.</value>
         /// <value>The sort remove words.</value>
-        public string[] SortRemoveWords { get; set; }
+        public string[] SortRemoveWords { get; set; } = new[] { "the", "a", "an" };
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated.
         /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated.
         /// </summary>
         /// </summary>
         /// <value>The min resume PCT.</value>
         /// <value>The min resume PCT.</value>
-        public int MinResumePct { get; set; }
+        public int MinResumePct { get; set; } = 5;
 
 
         /// <summary>
         /// <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.
         /// 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>
         /// </summary>
         /// <value>The max resume PCT.</value>
         /// <value>The max resume PCT.</value>
-        public int MaxResumePct { get; set; }
+        public int MaxResumePct { get; set; } = 90;
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates..
         /// Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates..
         /// </summary>
         /// </summary>
         /// <value>The min resume duration seconds.</value>
         /// <value>The min resume duration seconds.</value>
-        public int MinResumeDurationSeconds { get; set; }
+        public int MinResumeDurationSeconds { get; set; } = 300;
 
 
         /// <summary>
         /// <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
         /// Some delay is necessary with some items because their creation is not atomic.  It involves the creation of several
         /// different directories and files.
         /// different directories and files.
         /// </summary>
         /// </summary>
         /// <value>The file watcher delay.</value>
         /// <value>The file watcher delay.</value>
-        public int LibraryMonitorDelay { get; set; }
+        public int LibraryMonitorDelay { get; set; } = 60;
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets a value indicating whether [enable dashboard response caching].
         /// 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.
         /// Allows potential contributors without visual studio to modify production dashboard code and test changes.
         /// </summary>
         /// </summary>
         /// <value><c>true</c> if [enable dashboard response caching]; otherwise, <c>false</c>.</value>
         /// <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>
         /// <summary>
         /// Gets or sets the image saving convention.
         /// Gets or sets the image saving convention.
@@ -172,9 +326,9 @@ namespace MediaBrowser.Model.Configuration
 
 
         public MetadataOptions[] MetadataOptions { get; set; }
         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
         public string BaseUrl
         {
         {
@@ -206,194 +360,84 @@ 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>
         /// <summary>
         /// Gets or sets a value indicating whether the server should force connections over HTTPS.
         /// Gets or sets a value indicating whether the server should force connections over HTTPS.
         /// </summary>
         /// </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>
         /// <summary>
         /// Gets or sets a value indicating whether slow server responses should be logged as a warning.
         /// Gets or sets a value indicating whether slow server responses should be logged as a warning.
         /// </summary>
         /// </summary>
-        public bool EnableSlowResponseWarning { get; set; }
+        public bool EnableSlowResponseWarning { get; set; } = true;
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the threshold for the slow response time warning in ms.
         /// Gets or sets the threshold for the slow response time warning in ms.
         /// </summary>
         /// </summary>
-        public long SlowResponseThresholdMs { get; set; }
+        public long SlowResponseThresholdMs { get; set; } = 500;
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the cors hosts.
         /// Gets or sets the cors hosts.
         /// </summary>
         /// </summary>
-        public string[] CorsHosts { get; set; }
+        public string[] CorsHosts { get; set; } = new[] { "*" };
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the known proxies.
         /// Gets or sets the known proxies.
         /// </summary>
         /// </summary>
-        public string[] KnownProxies { get; set; }
+        public string[] KnownProxies { get; set; } = Array.Empty<string>();
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the number of days we should retain activity logs.
         /// Gets or sets the number of days we should retain activity logs.
         /// </summary>
         /// </summary>
-        public int? ActivityLogRetentionDays { 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;
-
-            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>();
-            ActivityLogRetentionDays = 30;
-        }
-    }
-
-    public class PathSubstitution
-    {
-        public string From { get; set; }
-
-        public string To { get; set; }
+        public int? ActivityLogRetentionDays { get; set; } = 30;
     }
     }
 }
 }

+ 20 - 14
MediaBrowser.sln

@@ -1,6 +1,6 @@
 Microsoft Visual Studio Solution File, Format Version 12.00
 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
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}"
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}"
 EndProject
 EndProject
@@ -66,12 +66,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Data", "Jellyfin.D
 EndProject
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}"
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}"
 EndProject
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}"
+EndProject
 Global
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
 		Debug|Any CPU = Debug|Any CPU
 		Release|Any CPU = Release|Any CPU
 		Release|Any CPU = Release|Any CPU
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(ProjectConfigurationPlatforms) = postSolution
 	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.ActiveCfg = Debug|Any CPU
 		{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.Build.0 = 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
 		{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}.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.ActiveCfg = Release|Any CPU
 		{960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
 		{154872D9-6C12-4007-96E3-8F70A58386CE}.Debug|Any CPU.Build.0 = 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
 		{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}.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.ActiveCfg = Release|Any CPU
 		{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.Build.0 = 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
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
 		HideSolutionNode = FALSE
 	EndGlobalSection
 	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
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}
 		SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}
 	EndGlobalSection
 	EndGlobalSection
@@ -201,12 +215,4 @@ Global
 		$0.DotNetNamingPolicy = $2
 		$0.DotNetNamingPolicy = $2
 		$2.DirectoryNamespaceAssociation = PrefixedHierarchical
 		$2.DirectoryNamespaceAssociation = PrefixedHierarchical
 	EndGlobalSection
 	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
 EndGlobal