Selaa lähdekoodia

Auto Discovery Cleanup (#10793)

* Call GetSmartApiUrl directly in UdpServer.RespondToV2Message

GetSmartApiUrl already returns PublishedServerUrl if set.

* Rewrite auto discovery using UdpClient and BackgroundService

* Respect network address settings in AutoDiscoveryHost

* Always listen on broadcast address in Linux for auto-discovery

* Await udp server tasks in AutoDiscoveryHost

* Only bind to broadcast addresses for IPv4

* Only bind to broadcast if IPv4 is enabled
Patrick Barron 1 vuosi sitten
vanhempi
sitoutus
43b32b0d94

+ 3 - 1
Jellyfin.Server/Startup.cs

@@ -6,6 +6,7 @@ using System.Net.Mime;
 using System.Text;
 using Jellyfin.Api.Middleware;
 using Jellyfin.MediaEncoding.Hls.Extensions;
+using Jellyfin.Networking;
 using Jellyfin.Networking.HappyEyeballs;
 using Jellyfin.Server.Extensions;
 using Jellyfin.Server.HealthChecks;
@@ -13,7 +14,6 @@ using Jellyfin.Server.Implementations;
 using Jellyfin.Server.Implementations.Extensions;
 using Jellyfin.Server.Infrastructure;
 using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Extensions;
 using Microsoft.AspNetCore.Builder;
@@ -121,6 +121,8 @@ namespace Jellyfin.Server
                 .AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext));
 
             services.AddHlsPlaylistGenerator();
+
+            services.AddHostedService<AutoDiscoveryHost>();
         }
 
         /// <summary>

+ 129 - 0
src/Jellyfin.Networking/AutoDiscoveryHost.cs

@@ -0,0 +1,129 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.ApiClient;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Networking;
+
+/// <summary>
+/// <see cref="BackgroundService"/> responsible for responding to auto-discovery messages.
+/// </summary>
+public sealed class AutoDiscoveryHost : BackgroundService
+{
+    /// <summary>
+    /// The port to listen on for auto-discovery messages.
+    /// </summary>
+    private const int PortNumber = 7359;
+
+    private readonly ILogger<AutoDiscoveryHost> _logger;
+    private readonly IServerApplicationHost _appHost;
+    private readonly IConfigurationManager _configurationManager;
+    private readonly INetworkManager _networkManager;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="AutoDiscoveryHost" /> class.
+    /// </summary>
+    /// <param name="logger">The <see cref="ILogger{AutoDiscoveryHost}"/>.</param>
+    /// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param>
+    /// <param name="configurationManager">The <see cref="IConfigurationManager"/>.</param>
+    /// <param name="networkManager">The <see cref="INetworkManager"/>.</param>
+    public AutoDiscoveryHost(
+        ILogger<AutoDiscoveryHost> logger,
+        IServerApplicationHost appHost,
+        IConfigurationManager configurationManager,
+        INetworkManager networkManager)
+    {
+        _logger = logger;
+        _appHost = appHost;
+        _configurationManager = configurationManager;
+        _networkManager = networkManager;
+    }
+
+    /// <inheritdoc />
+    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+    {
+        var networkConfig = _configurationManager.GetNetworkConfiguration();
+        if (!networkConfig.AutoDiscovery)
+        {
+            return;
+        }
+
+        var udpServers = new List<Task>();
+        // Linux needs to bind to the broadcast addresses to receive broadcast traffic
+        if (OperatingSystem.IsLinux() && networkConfig.EnableIPv4)
+        {
+            udpServers.Add(ListenForAutoDiscoveryMessage(IPAddress.Broadcast, stoppingToken));
+        }
+
+        udpServers.AddRange(_networkManager.GetInternalBindAddresses()
+            .Select(intf => ListenForAutoDiscoveryMessage(
+                OperatingSystem.IsLinux() && intf.AddressFamily == AddressFamily.InterNetwork
+                    ? NetworkUtils.GetBroadcastAddress(intf.Subnet)
+                    : intf.Address,
+                stoppingToken)));
+
+        await Task.WhenAll(udpServers).ConfigureAwait(false);
+    }
+
+    private async Task ListenForAutoDiscoveryMessage(IPAddress address, CancellationToken cancellationToken)
+    {
+        using var udpClient = new UdpClient(new IPEndPoint(address, PortNumber));
+        udpClient.MulticastLoopback = false;
+
+        while (!cancellationToken.IsCancellationRequested)
+        {
+            try
+            {
+                var result = await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false);
+                var text = Encoding.UTF8.GetString(result.Buffer);
+                if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
+                {
+                    await RespondToV2Message(udpClient, 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");
+            }
+        }
+    }
+
+    private async Task RespondToV2Message(UdpClient udpClient, IPEndPoint endpoint, CancellationToken cancellationToken)
+    {
+        var localUrl = _appHost.GetSmartApiUrl(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 udpClient
+                .SendAsync(JsonSerializer.SerializeToUtf8Bytes(response).AsMemory(), endpoint, cancellationToken)
+                .ConfigureAwait(false);
+        }
+        catch (SocketException ex)
+        {
+            _logger.LogError(ex, "Error sending response message");
+        }
+    }
+}

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

@@ -1,136 +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 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;
-    }
-}

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

@@ -1,143 +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 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;
-    }
-}