Kaynağa Gözat

Merge pull request #10660 from barronpm/move-networking

Move Jellyfin.Networking
Bond-009 1 yıl önce
ebeveyn
işleme
000ccaa6db

+ 1 - 1
Emby.Server.Implementations/ApplicationHost.cs

@@ -27,7 +27,6 @@ using Emby.Server.Implementations.IO;
 using Emby.Server.Implementations.Library;
 using Emby.Server.Implementations.Library;
 using Emby.Server.Implementations.LiveTv;
 using Emby.Server.Implementations.LiveTv;
 using Emby.Server.Implementations.Localization;
 using Emby.Server.Implementations.Localization;
-using Emby.Server.Implementations.Net;
 using Emby.Server.Implementations.Playlists;
 using Emby.Server.Implementations.Playlists;
 using Emby.Server.Implementations.Plugins;
 using Emby.Server.Implementations.Plugins;
 using Emby.Server.Implementations.QuickConnect;
 using Emby.Server.Implementations.QuickConnect;
@@ -41,6 +40,7 @@ using Jellyfin.Api.Helpers;
 using Jellyfin.Drawing;
 using Jellyfin.Drawing;
 using Jellyfin.MediaEncoding.Hls.Playlist;
 using Jellyfin.MediaEncoding.Hls.Playlist;
 using Jellyfin.Networking.Manager;
 using Jellyfin.Networking.Manager;
+using Jellyfin.Networking.Udp;
 using Jellyfin.Server.Implementations;
 using Jellyfin.Server.Implementations;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;

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

@@ -29,7 +29,6 @@
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
-    <PackageReference Include="Mono.Nat" />
     <PackageReference Include="prometheus-net.DotNetRuntime" />
     <PackageReference Include="prometheus-net.DotNetRuntime" />
     <PackageReference Include="DotNet.Glob" />
     <PackageReference Include="DotNet.Glob" />
   </ItemGroup>
   </ItemGroup>

+ 0 - 208
Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs

@@ -1,208 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Net;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Plugins;
-using Microsoft.Extensions.Logging;
-using Mono.Nat;
-
-namespace Emby.Server.Implementations.EntryPoints
-{
-    /// <summary>
-    /// Server entrypoint handling external port forwarding.
-    /// </summary>
-    public class ExternalPortForwarding : IServerEntryPoint
-    {
-        private readonly IServerApplicationHost _appHost;
-        private readonly ILogger<ExternalPortForwarding> _logger;
-        private readonly IServerConfigurationManager _config;
-
-        private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
-
-        private Timer _timer;
-        private string _configIdentifier;
-
-        private bool _disposed = false;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ExternalPortForwarding"/> class.
-        /// </summary>
-        /// <param name="logger">The logger.</param>
-        /// <param name="appHost">The application host.</param>
-        /// <param name="config">The configuration manager.</param>
-        public ExternalPortForwarding(
-            ILogger<ExternalPortForwarding> logger,
-            IServerApplicationHost appHost,
-            IServerConfigurationManager config)
-        {
-            _logger = logger;
-            _appHost = appHost;
-            _config = config;
-        }
-
-        private string GetConfigIdentifier()
-        {
-            const char Separator = '|';
-            var config = _config.GetNetworkConfiguration();
-
-            return new StringBuilder(32)
-                .Append(config.EnableUPnP).Append(Separator)
-                .Append(config.PublicHttpPort).Append(Separator)
-                .Append(config.PublicHttpsPort).Append(Separator)
-                .Append(_appHost.HttpPort).Append(Separator)
-                .Append(_appHost.HttpsPort).Append(Separator)
-                .Append(_appHost.ListenWithHttps).Append(Separator)
-                .Append(config.EnableRemoteAccess).Append(Separator)
-                .ToString();
-        }
-
-        private void OnConfigurationUpdated(object sender, EventArgs e)
-        {
-            var oldConfigIdentifier = _configIdentifier;
-            _configIdentifier = GetConfigIdentifier();
-
-            if (!string.Equals(_configIdentifier, oldConfigIdentifier, StringComparison.OrdinalIgnoreCase))
-            {
-                Stop();
-                Start();
-            }
-        }
-
-        /// <inheritdoc />
-        public Task RunAsync()
-        {
-            Start();
-
-            _config.ConfigurationUpdated += OnConfigurationUpdated;
-
-            return Task.CompletedTask;
-        }
-
-        private void Start()
-        {
-            var config = _config.GetNetworkConfiguration();
-            if (!config.EnableUPnP || !config.EnableRemoteAccess)
-            {
-                return;
-            }
-
-            _logger.LogInformation("Starting NAT discovery");
-
-            NatUtility.DeviceFound += OnNatUtilityDeviceFound;
-            NatUtility.StartDiscovery();
-
-            _timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
-        }
-
-        private void Stop()
-        {
-            _logger.LogInformation("Stopping NAT discovery");
-
-            NatUtility.StopDiscovery();
-            NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
-
-            _timer?.Dispose();
-        }
-
-        private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
-        {
-            try
-            {
-                await CreateRules(e.Device).ConfigureAwait(false);
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error creating port forwarding rules");
-            }
-        }
-
-        private Task CreateRules(INatDevice device)
-        {
-            if (_disposed)
-            {
-                throw new ObjectDisposedException(GetType().Name);
-            }
-
-            // On some systems the device discovered event seems to fire repeatedly
-            // This check will help ensure we're not trying to port map the same device over and over
-            if (!_createdRules.TryAdd(device.DeviceEndpoint, 0))
-            {
-                return Task.CompletedTask;
-            }
-
-            return Task.WhenAll(CreatePortMaps(device));
-        }
-
-        private IEnumerable<Task> CreatePortMaps(INatDevice device)
-        {
-            var config = _config.GetNetworkConfiguration();
-            yield return CreatePortMap(device, _appHost.HttpPort, config.PublicHttpPort);
-
-            if (_appHost.ListenWithHttps)
-            {
-                yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicHttpsPort);
-            }
-        }
-
-        private async Task CreatePortMap(INatDevice device, int privatePort, int publicPort)
-        {
-            _logger.LogDebug(
-                "Creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}",
-                privatePort,
-                publicPort,
-                device.DeviceEndpoint);
-
-            try
-            {
-                var mapping = new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name);
-                await device.CreatePortMapAsync(mapping).ConfigureAwait(false);
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(
-                    ex,
-                    "Error creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}.",
-                    privatePort,
-                    publicPort,
-                    device.DeviceEndpoint);
-            }
-        }
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        /// <summary>
-        /// Releases unmanaged and - optionally - managed resources.
-        /// </summary>
-        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
-        protected virtual void Dispose(bool dispose)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            _config.ConfigurationUpdated -= OnConfigurationUpdated;
-
-            Stop();
-
-            _timer = null;
-
-            _disposed = true;
-        }
-    }
-}

+ 0 - 153
Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs

@@ -1,153 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using System.Net.Sockets;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.Udp;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Plugins;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager;
-
-namespace Emby.Server.Implementations.EntryPoints
-{
-    /// <summary>
-    /// Class responsible for registering all UDP broadcast endpoints and their handlers.
-    /// </summary>
-    public sealed class UdpServerEntryPoint : IServerEntryPoint
-    {
-        /// <summary>
-        /// The port of the UDP server.
-        /// </summary>
-        public const int PortNumber = 7359;
-
-        /// <summary>
-        /// The logger.
-        /// </summary>
-        private readonly ILogger<UdpServerEntryPoint> _logger;
-        private readonly IServerApplicationHost _appHost;
-        private readonly IConfiguration _config;
-        private readonly IConfigurationManager _configurationManager;
-        private readonly INetworkManager _networkManager;
-
-        /// <summary>
-        /// The UDP server.
-        /// </summary>
-        private readonly List<UdpServer> _udpServers;
-        private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
-        private bool _disposed;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
-        /// </summary>
-        /// <param name="logger">Instance of the <see cref="ILogger{UdpServerEntryPoint}"/> interface.</param>
-        /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
-        /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
-        /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
-        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
-        public UdpServerEntryPoint(
-            ILogger<UdpServerEntryPoint> logger,
-            IServerApplicationHost appHost,
-            IConfiguration configuration,
-            IConfigurationManager configurationManager,
-            INetworkManager networkManager)
-        {
-            _logger = logger;
-            _appHost = appHost;
-            _config = configuration;
-            _configurationManager = configurationManager;
-            _networkManager = networkManager;
-            _udpServers = new List<UdpServer>();
-        }
-
-        /// <inheritdoc />
-        public Task RunAsync()
-        {
-            CheckDisposed();
-
-            if (!_configurationManager.GetNetworkConfiguration().AutoDiscovery)
-            {
-                return Task.CompletedTask;
-            }
-
-            try
-            {
-                // Linux needs to bind to the broadcast addresses to get broadcast traffic
-                // Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses
-                if (OperatingSystem.IsLinux())
-                {
-                    // Add global broadcast listener
-                    var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber);
-                    server.Start(_cancellationTokenSource.Token);
-                    _udpServers.Add(server);
-
-                    // Add bind address specific broadcast listeners
-                    // IPv6 is currently unsupported
-                    var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
-                    foreach (var intf in validInterfaces)
-                    {
-                        var broadcastAddress = NetworkUtils.GetBroadcastAddress(intf.Subnet);
-                        _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", broadcastAddress, PortNumber);
-
-                        server = new UdpServer(_logger, _appHost, _config, broadcastAddress, PortNumber);
-                        server.Start(_cancellationTokenSource.Token);
-                        _udpServers.Add(server);
-                    }
-                }
-                else
-                {
-                    // Add bind address specific broadcast listeners
-                    // IPv6 is currently unsupported
-                    var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
-                    foreach (var intf in validInterfaces)
-                    {
-                        var intfAddress = intf.Address;
-                        _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber);
-
-                        var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber);
-                        server.Start(_cancellationTokenSource.Token);
-                        _udpServers.Add(server);
-                    }
-                }
-            }
-            catch (SocketException ex)
-            {
-                _logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber);
-            }
-
-            return Task.CompletedTask;
-        }
-
-        private void CheckDisposed()
-        {
-            if (_disposed)
-            {
-                throw new ObjectDisposedException(GetType().Name);
-            }
-        }
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            _cancellationTokenSource.Cancel();
-            _cancellationTokenSource.Dispose();
-            foreach (var server in _udpServers)
-            {
-                server.Dispose();
-            }
-
-            _udpServers.Clear();
-            _disposed = true;
-        }
-    }
-}

+ 0 - 39
Emby.Server.Implementations/Net/SocketFactory.cs

@@ -1,39 +0,0 @@
-using System;
-using System.Net;
-using System.Net.Sockets;
-using MediaBrowser.Model.Net;
-
-namespace Emby.Server.Implementations.Net
-{
-    /// <summary>
-    /// Factory class to create different kinds of sockets.
-    /// </summary>
-    public class SocketFactory : ISocketFactory
-    {
-        /// <inheritdoc />
-        public Socket CreateUdpBroadcastSocket(int localPort)
-        {
-            if (localPort < 0)
-            {
-                throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort));
-            }
-
-            var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
-            try
-            {
-                socket.EnableBroadcast = true;
-                socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
-                socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
-                socket.Bind(new IPEndPoint(IPAddress.Any, localPort));
-
-                return socket;
-            }
-            catch
-            {
-                socket.Dispose();
-
-                throw;
-            }
-        }
-    }
-}

+ 0 - 137
Emby.Server.Implementations/Udp/UdpServer.cs

@@ -1,137 +0,0 @@
-using System;
-using System.Net;
-using System.Net.Sockets;
-using System.Text;
-using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller;
-using MediaBrowser.Model.ApiClient;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
-
-namespace Emby.Server.Implementations.Udp
-{
-    /// <summary>
-    /// Provides a Udp Server.
-    /// </summary>
-    public sealed class UdpServer : IDisposable
-    {
-        /// <summary>
-        /// The _logger.
-        /// </summary>
-        private readonly ILogger _logger;
-        private readonly IServerApplicationHost _appHost;
-        private readonly IConfiguration _config;
-
-        private readonly byte[] _receiveBuffer = new byte[8192];
-
-        private readonly Socket _udpSocket;
-        private readonly IPEndPoint _endpoint;
-        private bool _disposed;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="UdpServer" /> class.
-        /// </summary>
-        /// <param name="logger">The logger.</param>
-        /// <param name="appHost">The application host.</param>
-        /// <param name="configuration">The configuration manager.</param>
-        /// <param name="bindAddress"> The bind address.</param>
-        /// <param name="port">The port.</param>
-        public UdpServer(
-            ILogger logger,
-            IServerApplicationHost appHost,
-            IConfiguration configuration,
-            IPAddress bindAddress,
-            int port)
-        {
-            _logger = logger;
-            _appHost = appHost;
-            _config = configuration;
-
-            _endpoint = new IPEndPoint(bindAddress, port);
-
-            _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
-            {
-                MulticastLoopback = false,
-            };
-            _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
-        }
-
-        private async Task RespondToV2Message(EndPoint endpoint, CancellationToken cancellationToken)
-        {
-            string? localUrl = _config[AddressOverrideKey];
-            if (string.IsNullOrEmpty(localUrl))
-            {
-                localUrl = _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address);
-            }
-
-            if (string.IsNullOrEmpty(localUrl))
-            {
-                _logger.LogWarning("Unable to respond to server discovery request because the local ip address could not be determined.");
-                return;
-            }
-
-            var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName);
-
-            try
-            {
-                _logger.LogDebug("Sending AutoDiscovery response");
-                await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false);
-            }
-            catch (SocketException ex)
-            {
-                _logger.LogError(ex, "Error sending response message");
-            }
-        }
-
-        /// <summary>
-        /// Starts the specified port.
-        /// </summary>
-        /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
-        public void Start(CancellationToken cancellationToken)
-        {
-            _udpSocket.Bind(_endpoint);
-
-            _ = Task.Run(async () => await BeginReceiveAsync(cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false);
-        }
-
-        private async Task BeginReceiveAsync(CancellationToken cancellationToken)
-        {
-            while (!cancellationToken.IsCancellationRequested)
-            {
-                try
-                {
-                    var endpoint = (EndPoint)new IPEndPoint(IPAddress.Any, 0);
-                    var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, endpoint, cancellationToken).ConfigureAwait(false);
-                    var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
-                    if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
-                    {
-                        await RespondToV2Message(result.RemoteEndPoint, cancellationToken).ConfigureAwait(false);
-                    }
-                }
-                catch (SocketException ex)
-                {
-                    _logger.LogError(ex, "Failed to receive data from socket");
-                }
-                catch (OperationCanceledException)
-                {
-                    _logger.LogDebug("Broadcast socket operation cancelled");
-                }
-            }
-        }
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            _udpSocket.Dispose();
-            _disposed = true;
-        }
-    }
-}

+ 1 - 1
Jellyfin.Api/Jellyfin.Api.csproj

@@ -18,10 +18,10 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <ProjectReference Include="..\Jellyfin.Networking\Jellyfin.Networking.csproj" />
     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
     <ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" />
     <ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" />
     <ProjectReference Include="..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" />
     <ProjectReference Include="..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" />
+    <ProjectReference Include="..\src\Jellyfin.Networking\Jellyfin.Networking.csproj" />
   </ItemGroup>
   </ItemGroup>
 
 
   <!-- Code Analyzers -->
   <!-- Code Analyzers -->

+ 0 - 120
Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs

@@ -1,120 +0,0 @@
-/*
-The MIT License (MIT)
-
-Copyright (c) .NET Foundation and Contributors
-
-All rights reserved.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-*/
-
-using System.IO;
-using System.Net.Http;
-using System.Net.Sockets;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Jellyfin.Networking.HappyEyeballs
-{
-    /// <summary>
-    /// Defines the <see cref="HttpClientExtension"/> class.
-    ///
-    /// Implementation taken from https://github.com/ppy/osu-framework/pull/4191 .
-    /// </summary>
-    public static class HttpClientExtension
-    {
-        /// <summary>
-        /// Gets or sets a value indicating whether the client should use IPv6.
-        /// </summary>
-        public static bool UseIPv6 { get; set; } = true;
-
-        /// <summary>
-        /// Implements the httpclient callback method.
-        /// </summary>
-        /// <param name="context">The <see cref="SocketsHttpConnectionContext"/> instance.</param>
-        /// <param name="cancellationToken">The <see cref="CancellationToken"/> instance.</param>
-        /// <returns>The http steam.</returns>
-        public static async ValueTask<Stream> OnConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
-        {
-            if (!UseIPv6)
-            {
-                return await AttemptConnection(AddressFamily.InterNetwork, context, cancellationToken).ConfigureAwait(false);
-            }
-
-            using var cancelIPv6 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
-            var tryConnectAsyncIPv6 = AttemptConnection(AddressFamily.InterNetworkV6, context, cancelIPv6.Token);
-
-            // GetAwaiter().GetResult() is used instead of .Result as this results in improved exception handling.
-            // The tasks have already been completed.
-            // See https://github.com/dotnet/corefx/pull/29792/files#r189415885 for more details.
-            if (await Task.WhenAny(tryConnectAsyncIPv6, Task.Delay(200, cancelIPv6.Token)).ConfigureAwait(false) == tryConnectAsyncIPv6 && tryConnectAsyncIPv6.IsCompletedSuccessfully)
-            {
-                await cancelIPv6.CancelAsync().ConfigureAwait(false);
-                return tryConnectAsyncIPv6.GetAwaiter().GetResult();
-            }
-
-            using var cancelIPv4 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
-            var tryConnectAsyncIPv4 = AttemptConnection(AddressFamily.InterNetwork, context, cancelIPv4.Token);
-
-            if (await Task.WhenAny(tryConnectAsyncIPv6, tryConnectAsyncIPv4).ConfigureAwait(false) == tryConnectAsyncIPv6)
-            {
-                if (tryConnectAsyncIPv6.IsCompletedSuccessfully)
-                {
-                    await cancelIPv4.CancelAsync().ConfigureAwait(false);
-                    return tryConnectAsyncIPv6.GetAwaiter().GetResult();
-                }
-
-                return tryConnectAsyncIPv4.GetAwaiter().GetResult();
-            }
-            else
-            {
-                if (tryConnectAsyncIPv4.IsCompletedSuccessfully)
-                {
-                    await cancelIPv6.CancelAsync().ConfigureAwait(false);
-                    return tryConnectAsyncIPv4.GetAwaiter().GetResult();
-                }
-
-                return tryConnectAsyncIPv6.GetAwaiter().GetResult();
-            }
-        }
-
-        private static async Task<Stream> AttemptConnection(AddressFamily addressFamily, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
-        {
-            // The following socket constructor will create a dual-mode socket on systems where IPV6 is available.
-            var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp)
-            {
-                // Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios.
-                NoDelay = true
-            };
-
-            try
-            {
-                await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);
-                // The stream should take the ownership of the underlying socket,
-                // closing it when it's disposed.
-                return new NetworkStream(socket, ownsSocket: true);
-            }
-            catch
-            {
-                socket.Dispose();
-                throw;
-            }
-        }
-    }
-}

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

@@ -1,31 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-  <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
-    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
-    <GenerateDocumentationFile>true</GenerateDocumentationFile>
-  </PropertyGroup>
-
-  <ItemGroup>
-    <Compile Include="..\SharedVersion.cs" />
-  </ItemGroup>
-
-  <!-- Code Analyzers -->
-  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="IDisposableAnalyzers">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
-    <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
-  </ItemGroup>
-</Project>

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

@@ -1,1126 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
-using System.Linq;
-using System.Net;
-using System.Net.NetworkInformation;
-using System.Net.Sockets;
-using System.Threading;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Model.Net;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.HttpOverrides;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
-using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager;
-using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
-
-namespace Jellyfin.Networking.Manager
-{
-    /// <summary>
-    /// Class to take care of network interface management.
-    /// </summary>
-    public class NetworkManager : INetworkManager, IDisposable
-    {
-        /// <summary>
-        /// Threading lock for network properties.
-        /// </summary>
-        private readonly object _initLock;
-
-        private readonly ILogger<NetworkManager> _logger;
-
-        private readonly IConfigurationManager _configurationManager;
-
-        private readonly IConfiguration _startupConfig;
-
-        private readonly object _networkEventLock;
-
-        /// <summary>
-        /// Holds the published server URLs and the IPs to use them on.
-        /// </summary>
-        private IReadOnlyList<PublishedServerUriOverride> _publishedServerUrls;
-
-        private IReadOnlyList<IPNetwork> _remoteAddressFilter;
-
-        /// <summary>
-        /// Used to stop "event-racing conditions".
-        /// </summary>
-        private bool _eventfire;
-
-        /// <summary>
-        /// List of all interface MAC addresses.
-        /// </summary>
-        private IReadOnlyList<PhysicalAddress> _macAddresses;
-
-        /// <summary>
-        /// Dictionary containing interface addresses and their subnets.
-        /// </summary>
-        private IReadOnlyList<IPData> _interfaces;
-
-        /// <summary>
-        /// Unfiltered user defined LAN subnets (<see cref="NetworkConfiguration.LocalNetworkSubnets"/>)
-        /// or internal interface network subnets if undefined by user.
-        /// </summary>
-        private IReadOnlyList<IPNetwork> _lanSubnets;
-
-        /// <summary>
-        /// User defined list of subnets to excluded from the LAN.
-        /// </summary>
-        private IReadOnlyList<IPNetwork> _excludedSubnets;
-
-        /// <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">The <see cref="IConfigurationManager"/> instance.</param>
-        /// <param name="startupConfig">The <see cref="IConfiguration"/> instance holding startup parameters.</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, IConfiguration startupConfig, ILogger<NetworkManager> logger)
-        {
-            ArgumentNullException.ThrowIfNull(logger);
-            ArgumentNullException.ThrowIfNull(configurationManager);
-
-            _logger = logger;
-            _configurationManager = configurationManager;
-            _startupConfig = startupConfig;
-            _initLock = new();
-            _interfaces = new List<IPData>();
-            _macAddresses = new List<PhysicalAddress>();
-            _publishedServerUrls = new List<PublishedServerUriOverride>();
-            _networkEventLock = new object();
-            _remoteAddressFilter = new List<IPNetwork>();
-
-            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 a value indicating whether IP4 is enabled.
-        /// </summary>
-        public bool IsIPv4Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv4;
-
-        /// <summary>
-        /// Gets a value indicating whether IP6 is enabled.
-        /// </summary>
-        public bool IsIPv6Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv6;
-
-        /// <summary>
-        /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal.
-        /// </summary>
-        public bool TrustAllIPv6Interfaces { get; private set; }
-
-        /// <summary>
-        /// Gets the Published server override list.
-        /// </summary>
-        public IReadOnlyList<PublishedServerUriOverride> PublishedServerUrls => _publishedServerUrls;
-
-        /// <inheritdoc/>
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        /// <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.");
-            HandleNetworkChange();
-        }
-
-        /// <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.");
-            HandleNetworkChange();
-        }
-
-        /// <summary>
-        /// Triggers our event, and re-loads interface information.
-        /// </summary>
-        private void HandleNetworkChange()
-        {
-            lock (_networkEventLock)
-            {
-                if (!_eventfire)
-                {
-                    // As network events tend to fire one after the other only fire once every second.
-                    _eventfire = true;
-                    OnNetworkChange();
-                }
-            }
-        }
-
-        /// <summary>
-        /// Waits for 2 seconds before re-initialising the settings, as typically these events fire multiple times in succession.
-        /// </summary>
-        private void OnNetworkChange()
-        {
-            try
-            {
-                Thread.Sleep(2000);
-                var networkConfig = _configurationManager.GetNetworkConfiguration();
-                if (IsIPv6Enabled && !Socket.OSSupportsIPv6)
-                {
-                    UpdateSettings(networkConfig);
-                }
-                else
-                {
-                    InitializeInterfaces();
-                    InitializeLan(networkConfig);
-                    EnforceBindSettings(networkConfig);
-                }
-
-                PrintNetworkInformation(networkConfig);
-                NetworkChanged?.Invoke(this, EventArgs.Empty);
-            }
-            finally
-            {
-                _eventfire = false;
-            }
-        }
-
-        /// <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 InitializeInterfaces()
-        {
-            lock (_initLock)
-            {
-                _logger.LogDebug("Refreshing interfaces.");
-
-                var interfaces = new List<IPData>();
-                var macAddresses = new List<PhysicalAddress>();
-
-                try
-                {
-                    var nics = NetworkInterface.GetAllNetworkInterfaces()
-                        .Where(i => i.OperationalStatus == OperationalStatus.Up);
-
-                    foreach (NetworkInterface adapter in nics)
-                    {
-                        try
-                        {
-                            var ipProperties = adapter.GetIPProperties();
-                            var mac = adapter.GetPhysicalAddress();
-
-                            // Populate MAC list
-                            if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && PhysicalAddress.None.Equals(mac))
-                            {
-                                macAddresses.Add(mac);
-                            }
-
-                            // Populate interface list
-                            foreach (var info in ipProperties.UnicastAddresses)
-                            {
-                                if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork)
-                                {
-                                    var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
-                                    {
-                                        Index = ipProperties.GetIPv4Properties().Index,
-                                        Name = adapter.Name,
-                                        SupportsMulticast = adapter.SupportsMulticast
-                                    };
-
-                                    interfaces.Add(interfaceObject);
-                                }
-                                else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6)
-                                {
-                                    var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
-                                    {
-                                        Index = ipProperties.GetIPv6Properties().Index,
-                                        Name = adapter.Name,
-                                        SupportsMulticast = adapter.SupportsMulticast
-                                    };
-
-                                    interfaces.Add(interfaceObject);
-                                }
-                            }
-                        }
-                        catch (Exception ex)
-                        {
-                            // Ignore error, and attempt to continue.
-                            _logger.LogError(ex, "Error encountered parsing interfaces.");
-                        }
-                    }
-                }
-                catch (Exception ex)
-                {
-                    _logger.LogError(ex, "Error obtaining interfaces.");
-                }
-
-                // If no interfaces are found, fallback to loopback interfaces.
-                if (interfaces.Count == 0)
-                {
-                    _logger.LogWarning("No interface information available. Using loopback interface(s).");
-
-                    if (IsIPv4Enabled)
-                    {
-                        interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo"));
-                    }
-
-                    if (IsIPv6Enabled)
-                    {
-                        interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo"));
-                    }
-                }
-
-                _logger.LogDebug("Discovered {NumberOfInterfaces} interfaces.", interfaces.Count);
-                _logger.LogDebug("Interfaces addresses: {Addresses}", interfaces.OrderByDescending(s => s.AddressFamily == AddressFamily.InterNetwork).Select(s => s.Address.ToString()));
-
-                _macAddresses = macAddresses;
-                _interfaces = interfaces;
-            }
-        }
-
-        /// <summary>
-        /// Initializes internal LAN cache.
-        /// </summary>
-        private void InitializeLan(NetworkConfiguration config)
-        {
-            lock (_initLock)
-            {
-                _logger.LogDebug("Refreshing LAN information.");
-
-                // Get configuration options
-                var subnets = config.LocalNetworkSubnets;
-
-                // If no LAN addresses are specified, all private subnets and Loopback are deemed to be the LAN
-                if (!NetworkUtils.TryParseToSubnets(subnets, out var lanSubnets, false) || lanSubnets.Count == 0)
-                {
-                    _logger.LogDebug("Using LAN interface addresses as user provided no LAN details.");
-
-                    var fallbackLanSubnets = new List<IPNetwork>();
-                    if (IsIPv6Enabled)
-                    {
-                        fallbackLanSubnets.Add(NetworkConstants.IPv6RFC4291Loopback); // RFC 4291 (Loopback)
-                        fallbackLanSubnets.Add(NetworkConstants.IPv6RFC4291SiteLocal); // RFC 4291 (Site local)
-                        fallbackLanSubnets.Add(NetworkConstants.IPv6RFC4193UniqueLocal); // RFC 4193 (Unique local)
-                    }
-
-                    if (IsIPv4Enabled)
-                    {
-                        fallbackLanSubnets.Add(NetworkConstants.IPv4RFC5735Loopback); // RFC 5735 (Loopback)
-                        fallbackLanSubnets.Add(NetworkConstants.IPv4RFC1918PrivateClassA); // RFC 1918 (private Class A)
-                        fallbackLanSubnets.Add(NetworkConstants.IPv4RFC1918PrivateClassB); // RFC 1918 (private Class B)
-                        fallbackLanSubnets.Add(NetworkConstants.IPv4RFC1918PrivateClassC); // RFC 1918 (private Class C)
-                    }
-
-                    _lanSubnets = fallbackLanSubnets;
-                }
-                else
-                {
-                    _lanSubnets = lanSubnets;
-                }
-
-                _excludedSubnets = NetworkUtils.TryParseToSubnets(subnets, out var excludedSubnets, true)
-                    ? excludedSubnets
-                    : new List<IPNetwork>();
-            }
-        }
-
-        /// <summary>
-        /// Enforce bind addresses and exclusions on available interfaces.
-        /// </summary>
-        private void EnforceBindSettings(NetworkConfiguration config)
-        {
-            lock (_initLock)
-            {
-                // Respect explicit bind addresses
-                var interfaces = _interfaces.ToList();
-                var localNetworkAddresses = config.LocalNetworkAddresses;
-                if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0]))
-                {
-                    var bindAddresses = localNetworkAddresses.Select(p => NetworkUtils.TryParseToSubnet(p, out var network)
-                        ? network.Prefix
-                        : (interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase))
-                            .Select(x => x.Address)
-                            .FirstOrDefault() ?? IPAddress.None))
-                        .Where(x => x != IPAddress.None)
-                        .ToHashSet();
-                    interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList();
-
-                    if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback)))
-                    {
-                        interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo"));
-                    }
-
-                    if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback)))
-                    {
-                        interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo"));
-                    }
-                }
-
-                // Remove all interfaces matching any virtual machine interface prefix
-                if (config.IgnoreVirtualInterfaces)
-                {
-                    // Remove potentially existing * and split config string into prefixes
-                    var virtualInterfacePrefixes = config.VirtualInterfaceNames
-                        .Select(i => i.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase));
-
-                    // Check all interfaces for matches against the prefixes and remove them
-                    if (_interfaces.Count > 0)
-                    {
-                        foreach (var virtualInterfacePrefix in virtualInterfacePrefixes)
-                        {
-                            interfaces.RemoveAll(x => x.Name.StartsWith(virtualInterfacePrefix, StringComparison.OrdinalIgnoreCase));
-                        }
-                    }
-                }
-
-                // Remove all IPv4 interfaces if IPv4 is disabled
-                if (!IsIPv4Enabled)
-                {
-                    interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetwork);
-                }
-
-                // Remove all IPv6 interfaces if IPv6 is disabled
-                if (!IsIPv6Enabled)
-                {
-                    interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6);
-                }
-
-                _interfaces = interfaces;
-            }
-        }
-
-        /// <summary>
-        /// Initializes the remote address values.
-        /// </summary>
-        private void InitializeRemote(NetworkConfiguration config)
-        {
-            lock (_initLock)
-            {
-                // Parse config values into filter collection
-                var remoteIPFilter = config.RemoteIPFilter;
-                if (remoteIPFilter.Length != 0 && !string.IsNullOrWhiteSpace(remoteIPFilter[0]))
-                {
-                    // Parse all IPs with netmask to a subnet
-                    var remoteAddressFilter = new List<IPNetwork>();
-                    var remoteFilteredSubnets = remoteIPFilter.Where(x => x.Contains('/', StringComparison.OrdinalIgnoreCase)).ToArray();
-                    if (NetworkUtils.TryParseToSubnets(remoteFilteredSubnets, out var remoteAddressFilterResult, false))
-                    {
-                        remoteAddressFilter = remoteAddressFilterResult.ToList();
-                    }
-
-                    // Parse everything else as an IP and construct subnet with a single IP
-                    var remoteFilteredIPs = remoteIPFilter.Where(x => !x.Contains('/', StringComparison.OrdinalIgnoreCase));
-                    foreach (var ip in remoteFilteredIPs)
-                    {
-                        if (IPAddress.TryParse(ip, out var ipp))
-                        {
-                            remoteAddressFilter.Add(new IPNetwork(ipp, ipp.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize));
-                        }
-                    }
-
-                    _remoteAddressFilter = remoteAddressFilter;
-                }
-            }
-        }
-
-        /// <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 InitializeOverrides(NetworkConfiguration config)
-        {
-            lock (_initLock)
-            {
-                var publishedServerUrls = new List<PublishedServerUriOverride>();
-
-                // Prefer startup configuration.
-                var startupOverrideKey = _startupConfig[AddressOverrideKey];
-                if (!string.IsNullOrEmpty(startupOverrideKey))
-                {
-                    publishedServerUrls.Add(
-                        new PublishedServerUriOverride(
-                            new IPData(IPAddress.Any, NetworkConstants.IPv4Any),
-                            startupOverrideKey,
-                            true,
-                            true));
-                    publishedServerUrls.Add(
-                        new PublishedServerUriOverride(
-                            new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any),
-                            startupOverrideKey,
-                            true,
-                            true));
-                    _publishedServerUrls = publishedServerUrls;
-                    return;
-                }
-
-                var overrides = config.PublishedServerUriBySubnet;
-                foreach (var entry in overrides)
-                {
-                    var parts = entry.Split('=');
-                    if (parts.Length != 2)
-                    {
-                        _logger.LogError("Unable to parse bind override: {Entry}", entry);
-                        return;
-                    }
-
-                    var replacement = parts[1].Trim();
-                    var identifier = parts[0];
-                    if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase))
-                    {
-                        // Drop any other overrides in case an "all" override exists
-                        publishedServerUrls.Clear();
-                        publishedServerUrls.Add(
-                            new PublishedServerUriOverride(
-                                new IPData(IPAddress.Any, NetworkConstants.IPv4Any),
-                                replacement,
-                                true,
-                                true));
-                        publishedServerUrls.Add(
-                            new PublishedServerUriOverride(
-                                new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any),
-                                replacement,
-                                true,
-                                true));
-                        break;
-                    }
-                    else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase))
-                    {
-                        publishedServerUrls.Add(
-                            new PublishedServerUriOverride(
-                                new IPData(IPAddress.Any, NetworkConstants.IPv4Any),
-                                replacement,
-                                false,
-                                true));
-                        publishedServerUrls.Add(
-                            new PublishedServerUriOverride(
-                                new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any),
-                                replacement,
-                                false,
-                                true));
-                    }
-                    else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase))
-                    {
-                        foreach (var lan in _lanSubnets)
-                        {
-                            var lanPrefix = lan.Prefix;
-                            publishedServerUrls.Add(
-                                new PublishedServerUriOverride(
-                                    new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)),
-                                    replacement,
-                                    true,
-                                    false));
-                        }
-                    }
-                    else if (NetworkUtils.TryParseToSubnet(identifier, out var result) && result is not null)
-                    {
-                        var data = new IPData(result.Prefix, result);
-                        publishedServerUrls.Add(
-                            new PublishedServerUriOverride(
-                                data,
-                                replacement,
-                                true,
-                                true));
-                    }
-                    else if (TryParseInterface(identifier, out var ifaces))
-                    {
-                        foreach (var iface in ifaces)
-                        {
-                            publishedServerUrls.Add(
-                            new PublishedServerUriOverride(
-                                iface,
-                                replacement,
-                                true,
-                                true));
-                        }
-                    }
-                    else
-                    {
-                        _logger.LogError("Unable to parse bind override: {Entry}", entry);
-                    }
-                }
-
-                _publishedServerUrls = publishedServerUrls;
-            }
-        }
-
-        private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt)
-        {
-            if (evt.Key.Equals(NetworkConfigurationStore.StoreKey, StringComparison.Ordinal))
-            {
-                UpdateSettings((NetworkConfiguration)evt.NewConfiguration);
-            }
-        }
-
-        /// <summary>
-        /// Reloads all settings and re-Initializes the instance.
-        /// </summary>
-        /// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param>
-        public void UpdateSettings(object configuration)
-        {
-            ArgumentNullException.ThrowIfNull(configuration);
-
-            var config = (NetworkConfiguration)configuration;
-            HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6;
-
-            InitializeLan(config);
-            InitializeRemote(config);
-
-            if (string.IsNullOrEmpty(MockNetworkSettings))
-            {
-                InitializeInterfaces();
-            }
-            else // Used in testing only.
-            {
-                // Format is <IPAddress>,<Index>,<Name>: <next interface>. Set index to -ve to simulate a gateway.
-                var interfaceList = MockNetworkSettings.Split('|');
-                var interfaces = new List<IPData>();
-                foreach (var details in interfaceList)
-                {
-                    var parts = details.Split(',');
-                    if (NetworkUtils.TryParseToSubnet(parts[0], out var subnet))
-                    {
-                        var address = subnet.Prefix;
-                        var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
-                        if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6)
-                        {
-                            var data = new IPData(address, subnet, parts[2])
-                            {
-                                Index = index
-                            };
-                            interfaces.Add(data);
-                        }
-                    }
-                    else
-                    {
-                        _logger.LogWarning("Could not parse mock interface settings: {Part}", details);
-                    }
-                }
-
-                _interfaces = interfaces;
-            }
-
-            EnforceBindSettings(config);
-            InitializeOverrides(config);
-
-            PrintNetworkInformation(config, false);
-        }
-
-        /// <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;
-            }
-        }
-
-        /// <inheritdoc/>
-        public bool TryParseInterface(string intf, [NotNullWhen(true)] out IReadOnlyList<IPData>? result)
-        {
-            if (string.IsNullOrEmpty(intf)
-                || _interfaces is null
-                || _interfaces.Count == 0)
-            {
-                result = null;
-                return false;
-            }
-
-            // Match all interfaces starting with names starting with token
-            result = _interfaces
-                .Where(i => i.Name.Equals(intf, StringComparison.OrdinalIgnoreCase)
-                    && ((IsIPv4Enabled && i.Address.AddressFamily == AddressFamily.InterNetwork)
-                        || (IsIPv6Enabled && i.Address.AddressFamily == AddressFamily.InterNetworkV6)))
-                .OrderBy(x => x.Index)
-                .ToArray();
-            return result.Count > 0;
-        }
-
-        /// <inheritdoc/>
-        public bool HasRemoteAccess(IPAddress remoteIP)
-        {
-            var config = _configurationManager.GetNetworkConfiguration();
-            if (config.EnableRemoteAccess)
-            {
-                // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
-                // If left blank, all remote addresses will be allowed.
-                if (_remoteAddressFilter.Any() && !_lanSubnets.Any(x => x.Contains(remoteIP)))
-                {
-                    // remoteAddressFilter is a whitelist or blacklist.
-                    var matches = _remoteAddressFilter.Count(remoteNetwork => remoteNetwork.Contains(remoteIP));
-                    if ((!config.IsRemoteIPFilterBlacklist && matches > 0)
-                        || (config.IsRemoteIPFilterBlacklist && matches == 0))
-                    {
-                        return true;
-                    }
-
-                    return false;
-                }
-            }
-            else if (!_lanSubnets.Any(x => x.Contains(remoteIP)))
-            {
-                // Remote not enabled. So everyone should be LAN.
-                return false;
-            }
-
-            return true;
-        }
-
-        /// <inheritdoc/>
-        public IReadOnlyList<PhysicalAddress> GetMacAddresses()
-        {
-            // Populated in construction - so always has values.
-            return _macAddresses;
-        }
-
-        /// <inheritdoc/>
-        public IReadOnlyList<IPData> GetLoopbacks()
-        {
-            if (!IsIPv4Enabled && !IsIPv6Enabled)
-            {
-                return Array.Empty<IPData>();
-            }
-
-            var loopbackNetworks = new List<IPData>();
-            if (IsIPv4Enabled)
-            {
-                loopbackNetworks.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo"));
-            }
-
-            if (IsIPv6Enabled)
-            {
-                loopbackNetworks.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo"));
-            }
-
-            return loopbackNetworks;
-        }
-
-        /// <inheritdoc/>
-        public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false)
-        {
-            if (_interfaces.Count > 0 || individualInterfaces)
-            {
-                return _interfaces;
-            }
-
-            // No bind address and no exclusions, so listen on all interfaces.
-            var result = new List<IPData>();
-            if (IsIPv4Enabled && IsIPv6Enabled)
-            {
-                // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default
-                result.Add(new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any));
-            }
-            else if (IsIPv4Enabled)
-            {
-                result.Add(new IPData(IPAddress.Any, NetworkConstants.IPv4Any));
-            }
-            else if (IsIPv6Enabled)
-            {
-                // Cannot use IPv6Any as Kestrel will bind to IPv4 addresses too.
-                foreach (var iface in _interfaces)
-                {
-                    if (iface.AddressFamily == AddressFamily.InterNetworkV6)
-                    {
-                        result.Add(iface);
-                    }
-                }
-            }
-
-            return result;
-        }
-
-        /// <inheritdoc/>
-        public string GetBindAddress(string source, out int? port)
-        {
-            if (!NetworkUtils.TryParseHost(source, out var addresses, IsIPv4Enabled, IsIPv6Enabled))
-            {
-                addresses = Array.Empty<IPAddress>();
-            }
-
-            var result = GetBindAddress(addresses.FirstOrDefault(), out port);
-            return result;
-        }
-
-        /// <inheritdoc/>
-        public string GetBindAddress(HttpRequest source, out int? port)
-        {
-            var result = GetBindAddress(source.Host.Host, out port);
-            port ??= source.Host.Port;
-
-            return result;
-        }
-
-        /// <inheritdoc/>
-        public string GetBindAddress(IPAddress? source, out int? port, bool skipOverrides = false)
-        {
-            port = null;
-
-            string result;
-
-            if (source is not null)
-            {
-                if (IsIPv4Enabled && !IsIPv6Enabled && 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 (!IsIPv4Enabled && IsIPv6Enabled && source.AddressFamily == AddressFamily.InterNetwork)
-                {
-                    _logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
-                }
-
-                bool isExternal = !_lanSubnets.Any(network => network.Contains(source));
-                _logger.LogDebug("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal);
-
-                if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result))
-                {
-                    return result;
-                }
-
-                // No preference given, so move on to bind addresses.
-                if (MatchesBindInterface(source, isExternal, out result))
-                {
-                    return result;
-                }
-
-                if (isExternal && MatchesExternalInterface(source, out result))
-                {
-                    return result;
-                }
-            }
-
-            // Get the first LAN interface address that's not excluded and not a loopback address.
-            // Get all available interfaces, prefer local interfaces
-            var availableInterfaces = _interfaces.Where(x => !IPAddress.IsLoopback(x.Address))
-                .OrderByDescending(x => IsInLocalNetwork(x.Address))
-                .ThenBy(x => x.Index)
-                .ToList();
-
-            if (availableInterfaces.Count == 0)
-            {
-                // There isn't any others, so we'll use the loopback.
-                result = IsIPv4Enabled && !IsIPv6Enabled ? "127.0.0.1" : "::1";
-                _logger.LogWarning("{Source}: Only loopback {Result} returned, using that as bind address.", source, result);
-                return result;
-            }
-
-            // If no source address is given, use the preferred (first) interface
-            if (source is null)
-            {
-                result = NetworkUtils.FormatIPString(availableInterfaces.First().Address);
-                _logger.LogDebug("{Source}: Using first internal interface as bind address: {Result}", source, result);
-                return result;
-            }
-
-            // Does the request originate in one of the interface subnets?
-            // (For systems with multiple internal network cards, and multiple subnets)
-            foreach (var intf in availableInterfaces)
-            {
-                if (intf.Subnet.Contains(source))
-                {
-                    result = NetworkUtils.FormatIPString(intf.Address);
-                    _logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result);
-                    return result;
-                }
-            }
-
-            // Fallback to first available interface
-            result = NetworkUtils.FormatIPString(availableInterfaces[0].Address);
-            _logger.LogDebug("{Source}: No matching interfaces found, using preferred interface as bind address: {Result}", source, result);
-            return result;
-        }
-
-        /// <inheritdoc/>
-        public IReadOnlyList<IPData> GetInternalBindAddresses()
-        {
-            // Select all local bind addresses
-            return _interfaces.Where(x => IsInLocalNetwork(x.Address))
-                .OrderBy(x => x.Index)
-                .ToList();
-        }
-
-        /// <inheritdoc/>
-        public bool IsInLocalNetwork(string address)
-        {
-            if (NetworkUtils.TryParseToSubnet(address, out var subnet))
-            {
-                return IPAddress.IsLoopback(subnet.Prefix) || (_lanSubnets.Any(x => x.Contains(subnet.Prefix)) && !_excludedSubnets.Any(x => x.Contains(subnet.Prefix)));
-            }
-
-            if (NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled))
-            {
-                foreach (var ept in addresses)
-                {
-                    if (IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept))))
-                    {
-                        return true;
-                    }
-                }
-            }
-
-            return false;
-        }
-
-        /// <inheritdoc/>
-        public bool IsInLocalNetwork(IPAddress address)
-        {
-            ArgumentNullException.ThrowIfNull(address);
-
-            // See conversation at https://github.com/jellyfin/jellyfin/pull/3515.
-            if ((TrustAllIPv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6)
-                || address.Equals(IPAddress.Loopback)
-                || address.Equals(IPAddress.IPv6Loopback))
-            {
-                return true;
-            }
-
-            // As private addresses can be redefined by Configuration.LocalNetworkAddresses
-            return CheckIfLanAndNotExcluded(address);
-        }
-
-        private bool CheckIfLanAndNotExcluded(IPAddress address)
-        {
-            foreach (var lanSubnet in _lanSubnets)
-            {
-                if (lanSubnet.Contains(address))
-                {
-                    foreach (var excludedSubnet in _excludedSubnets)
-                    {
-                        if (excludedSubnet.Contains(address))
-                        {
-                            return false;
-                        }
-                    }
-
-                    return true;
-                }
-            }
-
-            return false;
-        }
-
-        /// <summary>
-        /// Attempts to match the source against the published server URL overrides.
-        /// </summary>
-        /// <param name="source">IP source address to use.</param>
-        /// <param name="isInExternalSubnet">True if the source is in an external subnet.</param>
-        /// <param name="bindPreference">The published server URL that matches the source address.</param>
-        /// <returns><c>true</c> if a match is found, <c>false</c> otherwise.</returns>
-        private bool MatchesPublishedServerUrl(IPAddress source, bool isInExternalSubnet, out string bindPreference)
-        {
-            bindPreference = string.Empty;
-            int? port = null;
-
-            // Only consider subnets including the source IP, prefering specific overrides
-            List<PublishedServerUriOverride> validPublishedServerUrls;
-            if (!isInExternalSubnet)
-            {
-                // Only use matching internal subnets
-                // Prefer more specific (bigger subnet prefix) overrides
-                validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source))
-                                            .OrderByDescending(x => x.Data.Subnet.PrefixLength)
-                                            .ToList();
-            }
-            else
-            {
-                // Only use matching external subnets
-                // Prefer more specific (bigger subnet prefix) overrides
-                validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source))
-                                            .OrderByDescending(x => x.Data.Subnet.PrefixLength)
-                                            .ToList();
-            }
-
-            foreach (var data in validPublishedServerUrls)
-            {
-                // Get interface matching override subnet
-                var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
-
-                if (intf?.Address is not null)
-                {
-                    // If matching interface is found, use override
-                    bindPreference = data.OverrideUri;
-                    break;
-                }
-            }
-
-            if (string.IsNullOrEmpty(bindPreference))
-            {
-                _logger.LogDebug("{Source}: No matching bind address override found", source);
-                return false;
-            }
-
-            // Handle override specifying port
-            var parts = bindPreference.Split(':');
-            if (parts.Length > 1)
-            {
-                if (int.TryParse(parts[1], out int p))
-                {
-                    bindPreference = parts[0];
-                    port = p;
-                    _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
-                    return true;
-                }
-            }
-
-            _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
-            return true;
-        }
-
-        /// <summary>
-        /// Attempts to match the source against the user defined bind interfaces.
-        /// </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(IPAddress source, bool isInExternalSubnet, out string result)
-        {
-            result = string.Empty;
-
-            int count = _interfaces.Count;
-            if (count == 1 && (_interfaces[0].Equals(IPAddress.Any) || _interfaces[0].Equals(IPAddress.IPv6Any)))
-            {
-                // Ignore IPAny addresses.
-                count = 0;
-            }
-
-            if (count == 0)
-            {
-                return false;
-            }
-
-            IPAddress? bindAddress = null;
-            if (isInExternalSubnet)
-            {
-                var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address))
-                    .OrderBy(x => x.Index)
-                    .ToList();
-                if (externalInterfaces.Count > 0)
-                {
-                    // Check to see if any of the external bind interfaces are in the same subnet as the source.
-                    // If none exists, this will select the first external interface if there is one.
-                    bindAddress = externalInterfaces
-                        .OrderByDescending(x => x.Subnet.Contains(source))
-                        .ThenBy(x => x.Index)
-                        .Select(x => x.Address)
-                        .First();
-
-                    result = NetworkUtils.FormatIPString(bindAddress);
-                    _logger.LogDebug("{Source}: External request received, matching external bind address found: {Result}", source, result);
-                    return true;
-                }
-
-                _logger.LogWarning("{Source}: External request received, no matching external bind address found, trying internal addresses.", source);
-            }
-            else
-            {
-                // Check to see if any of the internal bind interfaces are in the same subnet as the source.
-                // If none exists, this will select the first internal interface if there is one.
-                bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address))
-                    .OrderByDescending(x => x.Subnet.Contains(source))
-                    .ThenBy(x => x.Index)
-                    .Select(x => x.Address)
-                    .FirstOrDefault();
-
-                if (bindAddress is not null)
-                {
-                    result = NetworkUtils.FormatIPString(bindAddress);
-                    _logger.LogDebug("{Source}: Internal request received, matching internal bind address found: {Result}", source, result);
-                    return true;
-                }
-            }
-
-            return false;
-        }
-
-        /// <summary>
-        /// Attempts to match the source against external interfaces.
-        /// </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(IPAddress source, out string result)
-        {
-            // Get the first external interface address that isn't a loopback.
-            var extResult = _interfaces.Where(p => !IsInLocalNetwork(p.Address)).OrderBy(x => x.Index).ToArray();
-
-            // No external interface found
-            if (extResult.Length == 0)
-            {
-                result = string.Empty;
-                _logger.LogWarning("{Source}: External request received, but no external interface found. Need to route through internal network.", source);
-                return false;
-            }
-
-            // Does the request originate in one of the interface subnets?
-            // (For systems with multiple network cards and/or multiple subnets)
-            foreach (var intf in extResult)
-            {
-                if (intf.Subnet.Contains(source))
-                {
-                    result = NetworkUtils.FormatIPString(intf.Address);
-                    _logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result);
-                    return true;
-                }
-            }
-
-            // Fallback to first external interface.
-            result = NetworkUtils.FormatIPString(extResult[0].Address);
-            _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result);
-            return true;
-        }
-
-        private void PrintNetworkInformation(NetworkConfiguration config, bool debug = true)
-        {
-            var logLevel = debug ? LogLevel.Debug : LogLevel.Information;
-            if (_logger.IsEnabled(logLevel))
-            {
-                _logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
-                _logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
-                _logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength));
-                _logger.Log(logLevel, "Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
-                _logger.Log(logLevel, "Remote IP filter is {0}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist");
-                _logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength));
-            }
-        }
-    }
-}

+ 2 - 1
Jellyfin.sln

@@ -59,7 +59,7 @@ 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}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "src\Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}"
 EndProject
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.XbmcMetadata.Tests", "tests\Jellyfin.XbmcMetadata.Tests\Jellyfin.XbmcMetadata.Tests.csproj", "{30922383-D513-4F4D-B890-A940B57FA353}"
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.XbmcMetadata.Tests", "tests\Jellyfin.XbmcMetadata.Tests\Jellyfin.XbmcMetadata.Tests.csproj", "{30922383-D513-4F4D-B890-A940B57FA353}"
 EndProject
 EndProject
@@ -258,6 +258,7 @@ Global
 		{24960660-DE6C-47BF-AEEF-CEE8F19FE6C2} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{24960660-DE6C-47BF-AEEF-CEE8F19FE6C2} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
 		{08FFF49B-F175-4807-A2B5-73B0EBD9F716} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
 		{08FFF49B-F175-4807-A2B5-73B0EBD9F716} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
 		{154872D9-6C12-4007-96E3-8F70A58386CE} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
 		{154872D9-6C12-4007-96E3-8F70A58386CE} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
+		{0A3FCC4D-C714-4072-B90F-E374A15F9FF9} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}
 		SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}

+ 195 - 0
src/Jellyfin.Networking/ExternalPortForwarding.cs

@@ -0,0 +1,195 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Net;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Plugins;
+using Microsoft.Extensions.Logging;
+using Mono.Nat;
+
+namespace Jellyfin.Networking;
+
+/// <summary>
+/// Server entrypoint handling external port forwarding.
+/// </summary>
+public sealed class ExternalPortForwarding : IServerEntryPoint
+{
+    private readonly IServerApplicationHost _appHost;
+    private readonly ILogger<ExternalPortForwarding> _logger;
+    private readonly IServerConfigurationManager _config;
+
+    private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
+
+    private Timer _timer;
+    private string _configIdentifier;
+
+    private bool _disposed;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="ExternalPortForwarding"/> class.
+    /// </summary>
+    /// <param name="logger">The logger.</param>
+    /// <param name="appHost">The application host.</param>
+    /// <param name="config">The configuration manager.</param>
+    public ExternalPortForwarding(
+        ILogger<ExternalPortForwarding> logger,
+        IServerApplicationHost appHost,
+        IServerConfigurationManager config)
+    {
+        _logger = logger;
+        _appHost = appHost;
+        _config = config;
+    }
+
+    private string GetConfigIdentifier()
+    {
+        const char Separator = '|';
+        var config = _config.GetNetworkConfiguration();
+
+        return new StringBuilder(32)
+            .Append(config.EnableUPnP).Append(Separator)
+            .Append(config.PublicHttpPort).Append(Separator)
+            .Append(config.PublicHttpsPort).Append(Separator)
+            .Append(_appHost.HttpPort).Append(Separator)
+            .Append(_appHost.HttpsPort).Append(Separator)
+            .Append(_appHost.ListenWithHttps).Append(Separator)
+            .Append(config.EnableRemoteAccess).Append(Separator)
+            .ToString();
+    }
+
+    private void OnConfigurationUpdated(object sender, EventArgs e)
+    {
+        var oldConfigIdentifier = _configIdentifier;
+        _configIdentifier = GetConfigIdentifier();
+
+        if (!string.Equals(_configIdentifier, oldConfigIdentifier, StringComparison.OrdinalIgnoreCase))
+        {
+            Stop();
+            Start();
+        }
+    }
+
+    /// <inheritdoc />
+    public Task RunAsync()
+    {
+        Start();
+
+        _config.ConfigurationUpdated += OnConfigurationUpdated;
+
+        return Task.CompletedTask;
+    }
+
+    private void Start()
+    {
+        var config = _config.GetNetworkConfiguration();
+        if (!config.EnableUPnP || !config.EnableRemoteAccess)
+        {
+            return;
+        }
+
+        _logger.LogInformation("Starting NAT discovery");
+
+        NatUtility.DeviceFound += OnNatUtilityDeviceFound;
+        NatUtility.StartDiscovery();
+
+        _timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
+    }
+
+    private void Stop()
+    {
+        _logger.LogInformation("Stopping NAT discovery");
+
+        NatUtility.StopDiscovery();
+        NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
+
+        _timer?.Dispose();
+    }
+
+    private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
+    {
+        try
+        {
+            await CreateRules(e.Device).ConfigureAwait(false);
+        }
+        catch (Exception ex)
+        {
+            _logger.LogError(ex, "Error creating port forwarding rules");
+        }
+    }
+
+    private Task CreateRules(INatDevice device)
+    {
+        ObjectDisposedException.ThrowIf(_disposed, this);
+
+        // On some systems the device discovered event seems to fire repeatedly
+        // This check will help ensure we're not trying to port map the same device over and over
+        if (!_createdRules.TryAdd(device.DeviceEndpoint, 0))
+        {
+            return Task.CompletedTask;
+        }
+
+        return Task.WhenAll(CreatePortMaps(device));
+    }
+
+    private IEnumerable<Task> CreatePortMaps(INatDevice device)
+    {
+        var config = _config.GetNetworkConfiguration();
+        yield return CreatePortMap(device, _appHost.HttpPort, config.PublicHttpPort);
+
+        if (_appHost.ListenWithHttps)
+        {
+            yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicHttpsPort);
+        }
+    }
+
+    private async Task CreatePortMap(INatDevice device, int privatePort, int publicPort)
+    {
+        _logger.LogDebug(
+            "Creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}",
+            privatePort,
+            publicPort,
+            device.DeviceEndpoint);
+
+        try
+        {
+            var mapping = new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name);
+            await device.CreatePortMapAsync(mapping).ConfigureAwait(false);
+        }
+        catch (Exception ex)
+        {
+            _logger.LogError(
+                ex,
+                "Error creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}.",
+                privatePort,
+                publicPort,
+                device.DeviceEndpoint);
+        }
+    }
+
+    /// <inheritdoc />
+    public void Dispose()
+    {
+        if (_disposed)
+        {
+            return;
+        }
+
+        _config.ConfigurationUpdated -= OnConfigurationUpdated;
+
+        Stop();
+
+        _timer?.Dispose();
+        _timer = null;
+
+        _disposed = true;
+    }
+}

+ 119 - 0
src/Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs

@@ -0,0 +1,119 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) .NET Foundation and Contributors
+
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+using System.IO;
+using System.Net.Http;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Jellyfin.Networking.HappyEyeballs;
+
+/// <summary>
+/// Defines the <see cref="HttpClientExtension"/> class.
+///
+/// Implementation taken from https://github.com/ppy/osu-framework/pull/4191 .
+/// </summary>
+public static class HttpClientExtension
+{
+    /// <summary>
+    /// Gets or sets a value indicating whether the client should use IPv6.
+    /// </summary>
+    public static bool UseIPv6 { get; set; } = true;
+
+    /// <summary>
+    /// Implements the httpclient callback method.
+    /// </summary>
+    /// <param name="context">The <see cref="SocketsHttpConnectionContext"/> instance.</param>
+    /// <param name="cancellationToken">The <see cref="CancellationToken"/> instance.</param>
+    /// <returns>The http steam.</returns>
+    public static async ValueTask<Stream> OnConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
+    {
+        if (!UseIPv6)
+        {
+            return await AttemptConnection(AddressFamily.InterNetwork, context, cancellationToken).ConfigureAwait(false);
+        }
+
+        using var cancelIPv6 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+        var tryConnectAsyncIPv6 = AttemptConnection(AddressFamily.InterNetworkV6, context, cancelIPv6.Token);
+
+        // GetAwaiter().GetResult() is used instead of .Result as this results in improved exception handling.
+        // The tasks have already been completed.
+        // See https://github.com/dotnet/corefx/pull/29792/files#r189415885 for more details.
+        if (await Task.WhenAny(tryConnectAsyncIPv6, Task.Delay(200, cancelIPv6.Token)).ConfigureAwait(false) == tryConnectAsyncIPv6 && tryConnectAsyncIPv6.IsCompletedSuccessfully)
+        {
+            await cancelIPv6.CancelAsync().ConfigureAwait(false);
+            return tryConnectAsyncIPv6.GetAwaiter().GetResult();
+        }
+
+        using var cancelIPv4 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+        var tryConnectAsyncIPv4 = AttemptConnection(AddressFamily.InterNetwork, context, cancelIPv4.Token);
+
+        if (await Task.WhenAny(tryConnectAsyncIPv6, tryConnectAsyncIPv4).ConfigureAwait(false) == tryConnectAsyncIPv6)
+        {
+            if (tryConnectAsyncIPv6.IsCompletedSuccessfully)
+            {
+                await cancelIPv4.CancelAsync().ConfigureAwait(false);
+                return tryConnectAsyncIPv6.GetAwaiter().GetResult();
+            }
+
+            return tryConnectAsyncIPv4.GetAwaiter().GetResult();
+        }
+        else
+        {
+            if (tryConnectAsyncIPv4.IsCompletedSuccessfully)
+            {
+                await cancelIPv6.CancelAsync().ConfigureAwait(false);
+                return tryConnectAsyncIPv4.GetAwaiter().GetResult();
+            }
+
+            return tryConnectAsyncIPv6.GetAwaiter().GetResult();
+        }
+    }
+
+    private static async Task<Stream> AttemptConnection(AddressFamily addressFamily, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
+    {
+        // The following socket constructor will create a dual-mode socket on systems where IPV6 is available.
+        var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp)
+        {
+            // Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios.
+            NoDelay = true
+        };
+
+        try
+        {
+            await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);
+            // The stream should take the ownership of the underlying socket,
+            // closing it when it's disposed.
+            return new NetworkStream(socket, ownsSocket: true);
+        }
+        catch
+        {
+            socket.Dispose();
+            throw;
+        }
+    }
+}

+ 20 - 0
src/Jellyfin.Networking/Jellyfin.Networking.csproj

@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFramework>net8.0</TargetFramework>
+    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+    <GenerateDocumentationFile>true</GenerateDocumentationFile>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Compile Include="..\..\SharedVersion.cs" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
+    <ProjectReference Include="..\..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Mono.Nat" />
+  </ItemGroup>
+</Project>

+ 1125 - 0
src/Jellyfin.Networking/Manager/NetworkManager.cs

@@ -0,0 +1,1125 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+using System.Threading;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
+using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager;
+using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
+
+namespace Jellyfin.Networking.Manager;
+
+/// <summary>
+/// Class to take care of network interface management.
+/// </summary>
+public class NetworkManager : INetworkManager, IDisposable
+{
+    /// <summary>
+    /// Threading lock for network properties.
+    /// </summary>
+    private readonly object _initLock;
+
+    private readonly ILogger<NetworkManager> _logger;
+
+    private readonly IConfigurationManager _configurationManager;
+
+    private readonly IConfiguration _startupConfig;
+
+    private readonly object _networkEventLock;
+
+    /// <summary>
+    /// Holds the published server URLs and the IPs to use them on.
+    /// </summary>
+    private IReadOnlyList<PublishedServerUriOverride> _publishedServerUrls;
+
+    private IReadOnlyList<IPNetwork> _remoteAddressFilter;
+
+    /// <summary>
+    /// Used to stop "event-racing conditions".
+    /// </summary>
+    private bool _eventfire;
+
+    /// <summary>
+    /// List of all interface MAC addresses.
+    /// </summary>
+    private IReadOnlyList<PhysicalAddress> _macAddresses;
+
+    /// <summary>
+    /// Dictionary containing interface addresses and their subnets.
+    /// </summary>
+    private IReadOnlyList<IPData> _interfaces;
+
+    /// <summary>
+    /// Unfiltered user defined LAN subnets (<see cref="NetworkConfiguration.LocalNetworkSubnets"/>)
+    /// or internal interface network subnets if undefined by user.
+    /// </summary>
+    private IReadOnlyList<IPNetwork> _lanSubnets;
+
+    /// <summary>
+    /// User defined list of subnets to excluded from the LAN.
+    /// </summary>
+    private IReadOnlyList<IPNetwork> _excludedSubnets;
+
+    /// <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">The <see cref="IConfigurationManager"/> instance.</param>
+    /// <param name="startupConfig">The <see cref="IConfiguration"/> instance holding startup parameters.</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, IConfiguration startupConfig, ILogger<NetworkManager> logger)
+    {
+        ArgumentNullException.ThrowIfNull(logger);
+        ArgumentNullException.ThrowIfNull(configurationManager);
+
+        _logger = logger;
+        _configurationManager = configurationManager;
+        _startupConfig = startupConfig;
+        _initLock = new();
+        _interfaces = new List<IPData>();
+        _macAddresses = new List<PhysicalAddress>();
+        _publishedServerUrls = new List<PublishedServerUriOverride>();
+        _networkEventLock = new object();
+        _remoteAddressFilter = new List<IPNetwork>();
+
+        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 a value indicating whether IP4 is enabled.
+    /// </summary>
+    public bool IsIPv4Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv4;
+
+    /// <summary>
+    /// Gets a value indicating whether IP6 is enabled.
+    /// </summary>
+    public bool IsIPv6Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv6;
+
+    /// <summary>
+    /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal.
+    /// </summary>
+    public bool TrustAllIPv6Interfaces { get; private set; }
+
+    /// <summary>
+    /// Gets the Published server override list.
+    /// </summary>
+    public IReadOnlyList<PublishedServerUriOverride> PublishedServerUrls => _publishedServerUrls;
+
+    /// <inheritdoc/>
+    public void Dispose()
+    {
+        Dispose(true);
+        GC.SuppressFinalize(this);
+    }
+
+    /// <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.");
+        HandleNetworkChange();
+    }
+
+    /// <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.");
+        HandleNetworkChange();
+    }
+
+    /// <summary>
+    /// Triggers our event, and re-loads interface information.
+    /// </summary>
+    private void HandleNetworkChange()
+    {
+        lock (_networkEventLock)
+        {
+            if (!_eventfire)
+            {
+                // As network events tend to fire one after the other only fire once every second.
+                _eventfire = true;
+                OnNetworkChange();
+            }
+        }
+    }
+
+    /// <summary>
+    /// Waits for 2 seconds before re-initialising the settings, as typically these events fire multiple times in succession.
+    /// </summary>
+    private void OnNetworkChange()
+    {
+        try
+        {
+            Thread.Sleep(2000);
+            var networkConfig = _configurationManager.GetNetworkConfiguration();
+            if (IsIPv6Enabled && !Socket.OSSupportsIPv6)
+            {
+                UpdateSettings(networkConfig);
+            }
+            else
+            {
+                InitializeInterfaces();
+                InitializeLan(networkConfig);
+                EnforceBindSettings(networkConfig);
+            }
+
+            PrintNetworkInformation(networkConfig);
+            NetworkChanged?.Invoke(this, EventArgs.Empty);
+        }
+        finally
+        {
+            _eventfire = false;
+        }
+    }
+
+    /// <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 InitializeInterfaces()
+    {
+        lock (_initLock)
+        {
+            _logger.LogDebug("Refreshing interfaces.");
+
+            var interfaces = new List<IPData>();
+            var macAddresses = new List<PhysicalAddress>();
+
+            try
+            {
+                var nics = NetworkInterface.GetAllNetworkInterfaces()
+                    .Where(i => i.OperationalStatus == OperationalStatus.Up);
+
+                foreach (NetworkInterface adapter in nics)
+                {
+                    try
+                    {
+                        var ipProperties = adapter.GetIPProperties();
+                        var mac = adapter.GetPhysicalAddress();
+
+                        // Populate MAC list
+                        if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && PhysicalAddress.None.Equals(mac))
+                        {
+                            macAddresses.Add(mac);
+                        }
+
+                        // Populate interface list
+                        foreach (var info in ipProperties.UnicastAddresses)
+                        {
+                            if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork)
+                            {
+                                var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
+                                {
+                                    Index = ipProperties.GetIPv4Properties().Index,
+                                    Name = adapter.Name,
+                                    SupportsMulticast = adapter.SupportsMulticast
+                                };
+
+                                interfaces.Add(interfaceObject);
+                            }
+                            else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6)
+                            {
+                                var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
+                                {
+                                    Index = ipProperties.GetIPv6Properties().Index,
+                                    Name = adapter.Name,
+                                    SupportsMulticast = adapter.SupportsMulticast
+                                };
+
+                                interfaces.Add(interfaceObject);
+                            }
+                        }
+                    }
+                    catch (Exception ex)
+                    {
+                        // Ignore error, and attempt to continue.
+                        _logger.LogError(ex, "Error encountered parsing interfaces.");
+                    }
+                }
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error obtaining interfaces.");
+            }
+
+            // If no interfaces are found, fallback to loopback interfaces.
+            if (interfaces.Count == 0)
+            {
+                _logger.LogWarning("No interface information available. Using loopback interface(s).");
+
+                if (IsIPv4Enabled)
+                {
+                    interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo"));
+                }
+
+                if (IsIPv6Enabled)
+                {
+                    interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo"));
+                }
+            }
+
+            _logger.LogDebug("Discovered {NumberOfInterfaces} interfaces.", interfaces.Count);
+            _logger.LogDebug("Interfaces addresses: {Addresses}", interfaces.OrderByDescending(s => s.AddressFamily == AddressFamily.InterNetwork).Select(s => s.Address.ToString()));
+
+            _macAddresses = macAddresses;
+            _interfaces = interfaces;
+        }
+    }
+
+    /// <summary>
+    /// Initializes internal LAN cache.
+    /// </summary>
+    private void InitializeLan(NetworkConfiguration config)
+    {
+        lock (_initLock)
+        {
+            _logger.LogDebug("Refreshing LAN information.");
+
+            // Get configuration options
+            var subnets = config.LocalNetworkSubnets;
+
+            // If no LAN addresses are specified, all private subnets and Loopback are deemed to be the LAN
+            if (!NetworkUtils.TryParseToSubnets(subnets, out var lanSubnets, false) || lanSubnets.Count == 0)
+            {
+                _logger.LogDebug("Using LAN interface addresses as user provided no LAN details.");
+
+                var fallbackLanSubnets = new List<IPNetwork>();
+                if (IsIPv6Enabled)
+                {
+                    fallbackLanSubnets.Add(NetworkConstants.IPv6RFC4291Loopback); // RFC 4291 (Loopback)
+                    fallbackLanSubnets.Add(NetworkConstants.IPv6RFC4291SiteLocal); // RFC 4291 (Site local)
+                    fallbackLanSubnets.Add(NetworkConstants.IPv6RFC4193UniqueLocal); // RFC 4193 (Unique local)
+                }
+
+                if (IsIPv4Enabled)
+                {
+                    fallbackLanSubnets.Add(NetworkConstants.IPv4RFC5735Loopback); // RFC 5735 (Loopback)
+                    fallbackLanSubnets.Add(NetworkConstants.IPv4RFC1918PrivateClassA); // RFC 1918 (private Class A)
+                    fallbackLanSubnets.Add(NetworkConstants.IPv4RFC1918PrivateClassB); // RFC 1918 (private Class B)
+                    fallbackLanSubnets.Add(NetworkConstants.IPv4RFC1918PrivateClassC); // RFC 1918 (private Class C)
+                }
+
+                _lanSubnets = fallbackLanSubnets;
+            }
+            else
+            {
+                _lanSubnets = lanSubnets;
+            }
+
+            _excludedSubnets = NetworkUtils.TryParseToSubnets(subnets, out var excludedSubnets, true)
+                ? excludedSubnets
+                : new List<IPNetwork>();
+        }
+    }
+
+    /// <summary>
+    /// Enforce bind addresses and exclusions on available interfaces.
+    /// </summary>
+    private void EnforceBindSettings(NetworkConfiguration config)
+    {
+        lock (_initLock)
+        {
+            // Respect explicit bind addresses
+            var interfaces = _interfaces.ToList();
+            var localNetworkAddresses = config.LocalNetworkAddresses;
+            if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0]))
+            {
+                var bindAddresses = localNetworkAddresses.Select(p => NetworkUtils.TryParseToSubnet(p, out var network)
+                        ? network.Prefix
+                        : (interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase))
+                            .Select(x => x.Address)
+                            .FirstOrDefault() ?? IPAddress.None))
+                    .Where(x => x != IPAddress.None)
+                    .ToHashSet();
+                interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList();
+
+                if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback)))
+                {
+                    interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo"));
+                }
+
+                if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback)))
+                {
+                    interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo"));
+                }
+            }
+
+            // Remove all interfaces matching any virtual machine interface prefix
+            if (config.IgnoreVirtualInterfaces)
+            {
+                // Remove potentially existing * and split config string into prefixes
+                var virtualInterfacePrefixes = config.VirtualInterfaceNames
+                    .Select(i => i.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase));
+
+                // Check all interfaces for matches against the prefixes and remove them
+                if (_interfaces.Count > 0)
+                {
+                    foreach (var virtualInterfacePrefix in virtualInterfacePrefixes)
+                    {
+                        interfaces.RemoveAll(x => x.Name.StartsWith(virtualInterfacePrefix, StringComparison.OrdinalIgnoreCase));
+                    }
+                }
+            }
+
+            // Remove all IPv4 interfaces if IPv4 is disabled
+            if (!IsIPv4Enabled)
+            {
+                interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetwork);
+            }
+
+            // Remove all IPv6 interfaces if IPv6 is disabled
+            if (!IsIPv6Enabled)
+            {
+                interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6);
+            }
+
+            _interfaces = interfaces;
+        }
+    }
+
+    /// <summary>
+    /// Initializes the remote address values.
+    /// </summary>
+    private void InitializeRemote(NetworkConfiguration config)
+    {
+        lock (_initLock)
+        {
+            // Parse config values into filter collection
+            var remoteIPFilter = config.RemoteIPFilter;
+            if (remoteIPFilter.Length != 0 && !string.IsNullOrWhiteSpace(remoteIPFilter[0]))
+            {
+                // Parse all IPs with netmask to a subnet
+                var remoteAddressFilter = new List<IPNetwork>();
+                var remoteFilteredSubnets = remoteIPFilter.Where(x => x.Contains('/', StringComparison.OrdinalIgnoreCase)).ToArray();
+                if (NetworkUtils.TryParseToSubnets(remoteFilteredSubnets, out var remoteAddressFilterResult, false))
+                {
+                    remoteAddressFilter = remoteAddressFilterResult.ToList();
+                }
+
+                // Parse everything else as an IP and construct subnet with a single IP
+                var remoteFilteredIPs = remoteIPFilter.Where(x => !x.Contains('/', StringComparison.OrdinalIgnoreCase));
+                foreach (var ip in remoteFilteredIPs)
+                {
+                    if (IPAddress.TryParse(ip, out var ipp))
+                    {
+                        remoteAddressFilter.Add(new IPNetwork(ipp, ipp.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize));
+                    }
+                }
+
+                _remoteAddressFilter = remoteAddressFilter;
+            }
+        }
+    }
+
+    /// <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 InitializeOverrides(NetworkConfiguration config)
+    {
+        lock (_initLock)
+        {
+            var publishedServerUrls = new List<PublishedServerUriOverride>();
+
+            // Prefer startup configuration.
+            var startupOverrideKey = _startupConfig[AddressOverrideKey];
+            if (!string.IsNullOrEmpty(startupOverrideKey))
+            {
+                publishedServerUrls.Add(
+                    new PublishedServerUriOverride(
+                        new IPData(IPAddress.Any, NetworkConstants.IPv4Any),
+                        startupOverrideKey,
+                        true,
+                        true));
+                publishedServerUrls.Add(
+                    new PublishedServerUriOverride(
+                        new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any),
+                        startupOverrideKey,
+                        true,
+                        true));
+                _publishedServerUrls = publishedServerUrls;
+                return;
+            }
+
+            var overrides = config.PublishedServerUriBySubnet;
+            foreach (var entry in overrides)
+            {
+                var parts = entry.Split('=');
+                if (parts.Length != 2)
+                {
+                    _logger.LogError("Unable to parse bind override: {Entry}", entry);
+                    return;
+                }
+
+                var replacement = parts[1].Trim();
+                var identifier = parts[0];
+                if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase))
+                {
+                    // Drop any other overrides in case an "all" override exists
+                    publishedServerUrls.Clear();
+                    publishedServerUrls.Add(
+                        new PublishedServerUriOverride(
+                            new IPData(IPAddress.Any, NetworkConstants.IPv4Any),
+                            replacement,
+                            true,
+                            true));
+                    publishedServerUrls.Add(
+                        new PublishedServerUriOverride(
+                            new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any),
+                            replacement,
+                            true,
+                            true));
+                    break;
+                }
+                else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase))
+                {
+                    publishedServerUrls.Add(
+                        new PublishedServerUriOverride(
+                            new IPData(IPAddress.Any, NetworkConstants.IPv4Any),
+                            replacement,
+                            false,
+                            true));
+                    publishedServerUrls.Add(
+                        new PublishedServerUriOverride(
+                            new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any),
+                            replacement,
+                            false,
+                            true));
+                }
+                else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase))
+                {
+                    foreach (var lan in _lanSubnets)
+                    {
+                        var lanPrefix = lan.Prefix;
+                        publishedServerUrls.Add(
+                            new PublishedServerUriOverride(
+                                new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)),
+                                replacement,
+                                true,
+                                false));
+                    }
+                }
+                else if (NetworkUtils.TryParseToSubnet(identifier, out var result) && result is not null)
+                {
+                    var data = new IPData(result.Prefix, result);
+                    publishedServerUrls.Add(
+                        new PublishedServerUriOverride(
+                            data,
+                            replacement,
+                            true,
+                            true));
+                }
+                else if (TryParseInterface(identifier, out var ifaces))
+                {
+                    foreach (var iface in ifaces)
+                    {
+                        publishedServerUrls.Add(
+                            new PublishedServerUriOverride(
+                                iface,
+                                replacement,
+                                true,
+                                true));
+                    }
+                }
+                else
+                {
+                    _logger.LogError("Unable to parse bind override: {Entry}", entry);
+                }
+            }
+
+            _publishedServerUrls = publishedServerUrls;
+        }
+    }
+
+    private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt)
+    {
+        if (evt.Key.Equals(NetworkConfigurationStore.StoreKey, StringComparison.Ordinal))
+        {
+            UpdateSettings((NetworkConfiguration)evt.NewConfiguration);
+        }
+    }
+
+    /// <summary>
+    /// Reloads all settings and re-Initializes the instance.
+    /// </summary>
+    /// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param>
+    public void UpdateSettings(object configuration)
+    {
+        ArgumentNullException.ThrowIfNull(configuration);
+
+        var config = (NetworkConfiguration)configuration;
+        HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6;
+
+        InitializeLan(config);
+        InitializeRemote(config);
+
+        if (string.IsNullOrEmpty(MockNetworkSettings))
+        {
+            InitializeInterfaces();
+        }
+        else // Used in testing only.
+        {
+            // Format is <IPAddress>,<Index>,<Name>: <next interface>. Set index to -ve to simulate a gateway.
+            var interfaceList = MockNetworkSettings.Split('|');
+            var interfaces = new List<IPData>();
+            foreach (var details in interfaceList)
+            {
+                var parts = details.Split(',');
+                if (NetworkUtils.TryParseToSubnet(parts[0], out var subnet))
+                {
+                    var address = subnet.Prefix;
+                    var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
+                    if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6)
+                    {
+                        var data = new IPData(address, subnet, parts[2])
+                        {
+                            Index = index
+                        };
+                        interfaces.Add(data);
+                    }
+                }
+                else
+                {
+                    _logger.LogWarning("Could not parse mock interface settings: {Part}", details);
+                }
+            }
+
+            _interfaces = interfaces;
+        }
+
+        EnforceBindSettings(config);
+        InitializeOverrides(config);
+
+        PrintNetworkInformation(config, false);
+    }
+
+    /// <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;
+        }
+    }
+
+    /// <inheritdoc/>
+    public bool TryParseInterface(string intf, [NotNullWhen(true)] out IReadOnlyList<IPData>? result)
+    {
+        if (string.IsNullOrEmpty(intf)
+            || _interfaces is null
+            || _interfaces.Count == 0)
+        {
+            result = null;
+            return false;
+        }
+
+        // Match all interfaces starting with names starting with token
+        result = _interfaces
+            .Where(i => i.Name.Equals(intf, StringComparison.OrdinalIgnoreCase)
+                        && ((IsIPv4Enabled && i.Address.AddressFamily == AddressFamily.InterNetwork)
+                            || (IsIPv6Enabled && i.Address.AddressFamily == AddressFamily.InterNetworkV6)))
+            .OrderBy(x => x.Index)
+            .ToArray();
+        return result.Count > 0;
+    }
+
+    /// <inheritdoc/>
+    public bool HasRemoteAccess(IPAddress remoteIP)
+    {
+        var config = _configurationManager.GetNetworkConfiguration();
+        if (config.EnableRemoteAccess)
+        {
+            // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
+            // If left blank, all remote addresses will be allowed.
+            if (_remoteAddressFilter.Any() && !_lanSubnets.Any(x => x.Contains(remoteIP)))
+            {
+                // remoteAddressFilter is a whitelist or blacklist.
+                var matches = _remoteAddressFilter.Count(remoteNetwork => remoteNetwork.Contains(remoteIP));
+                if ((!config.IsRemoteIPFilterBlacklist && matches > 0)
+                    || (config.IsRemoteIPFilterBlacklist && matches == 0))
+                {
+                    return true;
+                }
+
+                return false;
+            }
+        }
+        else if (!_lanSubnets.Any(x => x.Contains(remoteIP)))
+        {
+            // Remote not enabled. So everyone should be LAN.
+            return false;
+        }
+
+        return true;
+    }
+
+    /// <inheritdoc/>
+    public IReadOnlyList<PhysicalAddress> GetMacAddresses()
+    {
+        // Populated in construction - so always has values.
+        return _macAddresses;
+    }
+
+    /// <inheritdoc/>
+    public IReadOnlyList<IPData> GetLoopbacks()
+    {
+        if (!IsIPv4Enabled && !IsIPv6Enabled)
+        {
+            return Array.Empty<IPData>();
+        }
+
+        var loopbackNetworks = new List<IPData>();
+        if (IsIPv4Enabled)
+        {
+            loopbackNetworks.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo"));
+        }
+
+        if (IsIPv6Enabled)
+        {
+            loopbackNetworks.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo"));
+        }
+
+        return loopbackNetworks;
+    }
+
+    /// <inheritdoc/>
+    public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false)
+    {
+        if (_interfaces.Count > 0 || individualInterfaces)
+        {
+            return _interfaces;
+        }
+
+        // No bind address and no exclusions, so listen on all interfaces.
+        var result = new List<IPData>();
+        if (IsIPv4Enabled && IsIPv6Enabled)
+        {
+            // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default
+            result.Add(new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any));
+        }
+        else if (IsIPv4Enabled)
+        {
+            result.Add(new IPData(IPAddress.Any, NetworkConstants.IPv4Any));
+        }
+        else if (IsIPv6Enabled)
+        {
+            // Cannot use IPv6Any as Kestrel will bind to IPv4 addresses too.
+            foreach (var iface in _interfaces)
+            {
+                if (iface.AddressFamily == AddressFamily.InterNetworkV6)
+                {
+                    result.Add(iface);
+                }
+            }
+        }
+
+        return result;
+    }
+
+    /// <inheritdoc/>
+    public string GetBindAddress(string source, out int? port)
+    {
+        if (!NetworkUtils.TryParseHost(source, out var addresses, IsIPv4Enabled, IsIPv6Enabled))
+        {
+            addresses = Array.Empty<IPAddress>();
+        }
+
+        var result = GetBindAddress(addresses.FirstOrDefault(), out port);
+        return result;
+    }
+
+    /// <inheritdoc/>
+    public string GetBindAddress(HttpRequest source, out int? port)
+    {
+        var result = GetBindAddress(source.Host.Host, out port);
+        port ??= source.Host.Port;
+
+        return result;
+    }
+
+    /// <inheritdoc/>
+    public string GetBindAddress(IPAddress? source, out int? port, bool skipOverrides = false)
+    {
+        port = null;
+
+        string result;
+
+        if (source is not null)
+        {
+            if (IsIPv4Enabled && !IsIPv6Enabled && 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 (!IsIPv4Enabled && IsIPv6Enabled && source.AddressFamily == AddressFamily.InterNetwork)
+            {
+                _logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
+            }
+
+            bool isExternal = !_lanSubnets.Any(network => network.Contains(source));
+            _logger.LogDebug("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal);
+
+            if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result))
+            {
+                return result;
+            }
+
+            // No preference given, so move on to bind addresses.
+            if (MatchesBindInterface(source, isExternal, out result))
+            {
+                return result;
+            }
+
+            if (isExternal && MatchesExternalInterface(source, out result))
+            {
+                return result;
+            }
+        }
+
+        // Get the first LAN interface address that's not excluded and not a loopback address.
+        // Get all available interfaces, prefer local interfaces
+        var availableInterfaces = _interfaces.Where(x => !IPAddress.IsLoopback(x.Address))
+            .OrderByDescending(x => IsInLocalNetwork(x.Address))
+            .ThenBy(x => x.Index)
+            .ToList();
+
+        if (availableInterfaces.Count == 0)
+        {
+            // There isn't any others, so we'll use the loopback.
+            result = IsIPv4Enabled && !IsIPv6Enabled ? "127.0.0.1" : "::1";
+            _logger.LogWarning("{Source}: Only loopback {Result} returned, using that as bind address.", source, result);
+            return result;
+        }
+
+        // If no source address is given, use the preferred (first) interface
+        if (source is null)
+        {
+            result = NetworkUtils.FormatIPString(availableInterfaces.First().Address);
+            _logger.LogDebug("{Source}: Using first internal interface as bind address: {Result}", source, result);
+            return result;
+        }
+
+        // Does the request originate in one of the interface subnets?
+        // (For systems with multiple internal network cards, and multiple subnets)
+        foreach (var intf in availableInterfaces)
+        {
+            if (intf.Subnet.Contains(source))
+            {
+                result = NetworkUtils.FormatIPString(intf.Address);
+                _logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result);
+                return result;
+            }
+        }
+
+        // Fallback to first available interface
+        result = NetworkUtils.FormatIPString(availableInterfaces[0].Address);
+        _logger.LogDebug("{Source}: No matching interfaces found, using preferred interface as bind address: {Result}", source, result);
+        return result;
+    }
+
+    /// <inheritdoc/>
+    public IReadOnlyList<IPData> GetInternalBindAddresses()
+    {
+        // Select all local bind addresses
+        return _interfaces.Where(x => IsInLocalNetwork(x.Address))
+            .OrderBy(x => x.Index)
+            .ToList();
+    }
+
+    /// <inheritdoc/>
+    public bool IsInLocalNetwork(string address)
+    {
+        if (NetworkUtils.TryParseToSubnet(address, out var subnet))
+        {
+            return IPAddress.IsLoopback(subnet.Prefix) || (_lanSubnets.Any(x => x.Contains(subnet.Prefix)) && !_excludedSubnets.Any(x => x.Contains(subnet.Prefix)));
+        }
+
+        if (NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled))
+        {
+            foreach (var ept in addresses)
+            {
+                if (IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept))))
+                {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /// <inheritdoc/>
+    public bool IsInLocalNetwork(IPAddress address)
+    {
+        ArgumentNullException.ThrowIfNull(address);
+
+        // See conversation at https://github.com/jellyfin/jellyfin/pull/3515.
+        if ((TrustAllIPv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6)
+            || address.Equals(IPAddress.Loopback)
+            || address.Equals(IPAddress.IPv6Loopback))
+        {
+            return true;
+        }
+
+        // As private addresses can be redefined by Configuration.LocalNetworkAddresses
+        return CheckIfLanAndNotExcluded(address);
+    }
+
+    private bool CheckIfLanAndNotExcluded(IPAddress address)
+    {
+        foreach (var lanSubnet in _lanSubnets)
+        {
+            if (lanSubnet.Contains(address))
+            {
+                foreach (var excludedSubnet in _excludedSubnets)
+                {
+                    if (excludedSubnet.Contains(address))
+                    {
+                        return false;
+                    }
+                }
+
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /// <summary>
+    /// Attempts to match the source against the published server URL overrides.
+    /// </summary>
+    /// <param name="source">IP source address to use.</param>
+    /// <param name="isInExternalSubnet">True if the source is in an external subnet.</param>
+    /// <param name="bindPreference">The published server URL that matches the source address.</param>
+    /// <returns><c>true</c> if a match is found, <c>false</c> otherwise.</returns>
+    private bool MatchesPublishedServerUrl(IPAddress source, bool isInExternalSubnet, out string bindPreference)
+    {
+        bindPreference = string.Empty;
+        int? port = null;
+
+        // Only consider subnets including the source IP, prefering specific overrides
+        List<PublishedServerUriOverride> validPublishedServerUrls;
+        if (!isInExternalSubnet)
+        {
+            // Only use matching internal subnets
+            // Prefer more specific (bigger subnet prefix) overrides
+            validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source))
+                .OrderByDescending(x => x.Data.Subnet.PrefixLength)
+                .ToList();
+        }
+        else
+        {
+            // Only use matching external subnets
+            // Prefer more specific (bigger subnet prefix) overrides
+            validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source))
+                .OrderByDescending(x => x.Data.Subnet.PrefixLength)
+                .ToList();
+        }
+
+        foreach (var data in validPublishedServerUrls)
+        {
+            // Get interface matching override subnet
+            var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
+
+            if (intf?.Address is not null)
+            {
+                // If matching interface is found, use override
+                bindPreference = data.OverrideUri;
+                break;
+            }
+        }
+
+        if (string.IsNullOrEmpty(bindPreference))
+        {
+            _logger.LogDebug("{Source}: No matching bind address override found", source);
+            return false;
+        }
+
+        // Handle override specifying port
+        var parts = bindPreference.Split(':');
+        if (parts.Length > 1)
+        {
+            if (int.TryParse(parts[1], out int p))
+            {
+                bindPreference = parts[0];
+                port = p;
+                _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
+                return true;
+            }
+        }
+
+        _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
+        return true;
+    }
+
+    /// <summary>
+    /// Attempts to match the source against the user defined bind interfaces.
+    /// </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(IPAddress source, bool isInExternalSubnet, out string result)
+    {
+        result = string.Empty;
+
+        int count = _interfaces.Count;
+        if (count == 1 && (_interfaces[0].Equals(IPAddress.Any) || _interfaces[0].Equals(IPAddress.IPv6Any)))
+        {
+            // Ignore IPAny addresses.
+            count = 0;
+        }
+
+        if (count == 0)
+        {
+            return false;
+        }
+
+        IPAddress? bindAddress = null;
+        if (isInExternalSubnet)
+        {
+            var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address))
+                .OrderBy(x => x.Index)
+                .ToList();
+            if (externalInterfaces.Count > 0)
+            {
+                // Check to see if any of the external bind interfaces are in the same subnet as the source.
+                // If none exists, this will select the first external interface if there is one.
+                bindAddress = externalInterfaces
+                    .OrderByDescending(x => x.Subnet.Contains(source))
+                    .ThenBy(x => x.Index)
+                    .Select(x => x.Address)
+                    .First();
+
+                result = NetworkUtils.FormatIPString(bindAddress);
+                _logger.LogDebug("{Source}: External request received, matching external bind address found: {Result}", source, result);
+                return true;
+            }
+
+            _logger.LogWarning("{Source}: External request received, no matching external bind address found, trying internal addresses.", source);
+        }
+        else
+        {
+            // Check to see if any of the internal bind interfaces are in the same subnet as the source.
+            // If none exists, this will select the first internal interface if there is one.
+            bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address))
+                .OrderByDescending(x => x.Subnet.Contains(source))
+                .ThenBy(x => x.Index)
+                .Select(x => x.Address)
+                .FirstOrDefault();
+
+            if (bindAddress is not null)
+            {
+                result = NetworkUtils.FormatIPString(bindAddress);
+                _logger.LogDebug("{Source}: Internal request received, matching internal bind address found: {Result}", source, result);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /// <summary>
+    /// Attempts to match the source against external interfaces.
+    /// </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(IPAddress source, out string result)
+    {
+        // Get the first external interface address that isn't a loopback.
+        var extResult = _interfaces.Where(p => !IsInLocalNetwork(p.Address)).OrderBy(x => x.Index).ToArray();
+
+        // No external interface found
+        if (extResult.Length == 0)
+        {
+            result = string.Empty;
+            _logger.LogWarning("{Source}: External request received, but no external interface found. Need to route through internal network.", source);
+            return false;
+        }
+
+        // Does the request originate in one of the interface subnets?
+        // (For systems with multiple network cards and/or multiple subnets)
+        foreach (var intf in extResult)
+        {
+            if (intf.Subnet.Contains(source))
+            {
+                result = NetworkUtils.FormatIPString(intf.Address);
+                _logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result);
+                return true;
+            }
+        }
+
+        // Fallback to first external interface.
+        result = NetworkUtils.FormatIPString(extResult[0].Address);
+        _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result);
+        return true;
+    }
+
+    private void PrintNetworkInformation(NetworkConfiguration config, bool debug = true)
+    {
+        var logLevel = debug ? LogLevel.Debug : LogLevel.Information;
+        if (_logger.IsEnabled(logLevel))
+        {
+            _logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
+            _logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
+            _logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength));
+            _logger.Log(logLevel, "Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
+            _logger.Log(logLevel, "Remote IP filter is {0}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist");
+            _logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength));
+        }
+    }
+}

+ 38 - 0
src/Jellyfin.Networking/Udp/SocketFactory.cs

@@ -0,0 +1,38 @@
+using System;
+using System.Net;
+using System.Net.Sockets;
+using MediaBrowser.Model.Net;
+
+namespace Jellyfin.Networking.Udp;
+
+/// <summary>
+/// Factory class to create different kinds of sockets.
+/// </summary>
+public class SocketFactory : ISocketFactory
+{
+    /// <inheritdoc />
+    public Socket CreateUdpBroadcastSocket(int localPort)
+    {
+        if (localPort < 0)
+        {
+            throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort));
+        }
+
+        var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+        try
+        {
+            socket.EnableBroadcast = true;
+            socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
+            socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
+            socket.Bind(new IPEndPoint(IPAddress.Any, localPort));
+
+            return socket;
+        }
+        catch
+        {
+            socket.Dispose();
+
+            throw;
+        }
+    }
+}

+ 136 - 0
src/Jellyfin.Networking/Udp/UdpServer.cs

@@ -0,0 +1,136 @@
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.ApiClient;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
+
+namespace Jellyfin.Networking.Udp;
+
+/// <summary>
+/// Provides a Udp Server.
+/// </summary>
+public sealed class UdpServer : IDisposable
+{
+    /// <summary>
+    /// The _logger.
+    /// </summary>
+    private readonly ILogger _logger;
+    private readonly IServerApplicationHost _appHost;
+    private readonly IConfiguration _config;
+
+    private readonly byte[] _receiveBuffer = new byte[8192];
+
+    private readonly Socket _udpSocket;
+    private readonly IPEndPoint _endpoint;
+    private bool _disposed;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="UdpServer" /> class.
+    /// </summary>
+    /// <param name="logger">The logger.</param>
+    /// <param name="appHost">The application host.</param>
+    /// <param name="configuration">The configuration manager.</param>
+    /// <param name="bindAddress"> The bind address.</param>
+    /// <param name="port">The port.</param>
+    public UdpServer(
+        ILogger logger,
+        IServerApplicationHost appHost,
+        IConfiguration configuration,
+        IPAddress bindAddress,
+        int port)
+    {
+        _logger = logger;
+        _appHost = appHost;
+        _config = configuration;
+
+        _endpoint = new IPEndPoint(bindAddress, port);
+
+        _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
+        {
+            MulticastLoopback = false,
+        };
+        _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
+    }
+
+    private async Task RespondToV2Message(EndPoint endpoint, CancellationToken cancellationToken)
+    {
+        string? localUrl = _config[AddressOverrideKey];
+        if (string.IsNullOrEmpty(localUrl))
+        {
+            localUrl = _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address);
+        }
+
+        if (string.IsNullOrEmpty(localUrl))
+        {
+            _logger.LogWarning("Unable to respond to server discovery request because the local ip address could not be determined.");
+            return;
+        }
+
+        var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName);
+
+        try
+        {
+            _logger.LogDebug("Sending AutoDiscovery response");
+            await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false);
+        }
+        catch (SocketException ex)
+        {
+            _logger.LogError(ex, "Error sending response message");
+        }
+    }
+
+    /// <summary>
+    /// Starts the specified port.
+    /// </summary>
+    /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
+    public void Start(CancellationToken cancellationToken)
+    {
+        _udpSocket.Bind(_endpoint);
+
+        _ = Task.Run(async () => await BeginReceiveAsync(cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false);
+    }
+
+    private async Task BeginReceiveAsync(CancellationToken cancellationToken)
+    {
+        while (!cancellationToken.IsCancellationRequested)
+        {
+            try
+            {
+                var endpoint = (EndPoint)new IPEndPoint(IPAddress.Any, 0);
+                var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, endpoint, cancellationToken).ConfigureAwait(false);
+                var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
+                if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
+                {
+                    await RespondToV2Message(result.RemoteEndPoint, cancellationToken).ConfigureAwait(false);
+                }
+            }
+            catch (SocketException ex)
+            {
+                _logger.LogError(ex, "Failed to receive data from socket");
+            }
+            catch (OperationCanceledException)
+            {
+                _logger.LogDebug("Broadcast socket operation cancelled");
+            }
+        }
+    }
+
+    /// <inheritdoc />
+    public void Dispose()
+    {
+        if (_disposed)
+        {
+            return;
+        }
+
+        _udpSocket.Dispose();
+        _disposed = true;
+    }
+}

+ 143 - 0
src/Jellyfin.Networking/UdpServerEntryPoint.cs

@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Networking.Udp;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Plugins;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager;
+
+namespace Jellyfin.Networking;
+
+/// <summary>
+/// Class responsible for registering all UDP broadcast endpoints and their handlers.
+/// </summary>
+public sealed class UdpServerEntryPoint : IServerEntryPoint
+{
+    /// <summary>
+    /// The port of the UDP server.
+    /// </summary>
+    public const int PortNumber = 7359;
+
+    /// <summary>
+    /// The logger.
+    /// </summary>
+    private readonly ILogger<UdpServerEntryPoint> _logger;
+    private readonly IServerApplicationHost _appHost;
+    private readonly IConfiguration _config;
+    private readonly IConfigurationManager _configurationManager;
+    private readonly INetworkManager _networkManager;
+
+    /// <summary>
+    /// The UDP server.
+    /// </summary>
+    private readonly List<UdpServer> _udpServers;
+    private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+    private bool _disposed;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
+    /// </summary>
+    /// <param name="logger">Instance of the <see cref="ILogger{UdpServerEntryPoint}"/> interface.</param>
+    /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
+    /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
+    /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+    /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+    public UdpServerEntryPoint(
+        ILogger<UdpServerEntryPoint> logger,
+        IServerApplicationHost appHost,
+        IConfiguration configuration,
+        IConfigurationManager configurationManager,
+        INetworkManager networkManager)
+    {
+        _logger = logger;
+        _appHost = appHost;
+        _config = configuration;
+        _configurationManager = configurationManager;
+        _networkManager = networkManager;
+        _udpServers = new List<UdpServer>();
+    }
+
+    /// <inheritdoc />
+    public Task RunAsync()
+    {
+        ObjectDisposedException.ThrowIf(_disposed, this);
+
+        if (!_configurationManager.GetNetworkConfiguration().AutoDiscovery)
+        {
+            return Task.CompletedTask;
+        }
+
+        try
+        {
+            // Linux needs to bind to the broadcast addresses to get broadcast traffic
+            // Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses
+            if (OperatingSystem.IsLinux())
+            {
+                // Add global broadcast listener
+                var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber);
+                server.Start(_cancellationTokenSource.Token);
+                _udpServers.Add(server);
+
+                // Add bind address specific broadcast listeners
+                // IPv6 is currently unsupported
+                var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
+                foreach (var intf in validInterfaces)
+                {
+                    var broadcastAddress = NetworkUtils.GetBroadcastAddress(intf.Subnet);
+                    _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", broadcastAddress, PortNumber);
+
+                    server = new UdpServer(_logger, _appHost, _config, broadcastAddress, PortNumber);
+                    server.Start(_cancellationTokenSource.Token);
+                    _udpServers.Add(server);
+                }
+            }
+            else
+            {
+                // Add bind address specific broadcast listeners
+                // IPv6 is currently unsupported
+                var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
+                foreach (var intf in validInterfaces)
+                {
+                    var intfAddress = intf.Address;
+                    _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber);
+
+                    var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber);
+                    server.Start(_cancellationTokenSource.Token);
+                    _udpServers.Add(server);
+                }
+            }
+        }
+        catch (SocketException ex)
+        {
+            _logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber);
+        }
+
+        return Task.CompletedTask;
+    }
+
+    /// <inheritdoc />
+    public void Dispose()
+    {
+        if (_disposed)
+        {
+            return;
+        }
+
+        _cancellationTokenSource.Cancel();
+        _cancellationTokenSource.Dispose();
+        foreach (var server in _udpServers)
+        {
+            server.Dispose();
+        }
+
+        _udpServers.Clear();
+        _disposed = true;
+    }
+}

+ 1 - 1
tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj

@@ -18,7 +18,7 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <ProjectReference Include="../../Jellyfin.Networking/Jellyfin.Networking.csproj" />
+    <ProjectReference Include="../../src/Jellyfin.Networking/Jellyfin.Networking.csproj" />
   </ItemGroup>
   </ItemGroup>
 
 
 </Project>
 </Project>