Refactor URI overrides (#10051)

pull/10387/head
Tim Eisele 8 months ago committed by GitHub
parent a88e13a677
commit dc27d8f9cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -201,9 +201,10 @@ namespace Emby.Dlna.Main
{ {
if (_communicationsServer is null) if (_communicationsServer is null)
{ {
var enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); _communicationsServer = new SsdpCommunicationsServer(
_socketFactory,
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding) _networkManager,
_logger)
{ {
IsShared = true IsShared = true
}; };
@ -213,7 +214,7 @@ namespace Emby.Dlna.Main
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error starting ssdp handlers"); _logger.LogError(ex, "Error starting SSDP handlers");
} }
} }

@ -450,7 +450,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>()); ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>()); NetManager = new NetworkManager(ConfigurationManager, _startupConfig, LoggerFactory.CreateLogger<NetworkManager>());
// Initialize runtime stat collection // Initialize runtime stat collection
if (ConfigurationManager.Configuration.EnableMetrics) if (ConfigurationManager.Configuration.EnableMetrics)
@ -913,7 +913,7 @@ namespace Emby.Server.Implementations
/// <inheritdoc/> /// <inheritdoc/>
public string GetSmartApiUrl(HttpRequest request) public string GetSmartApiUrl(HttpRequest request)
{ {
// Return the host in the HTTP request as the API url // Return the host in the HTTP request as the API URL if not configured otherwise
if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest) if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
{ {
int? requestPort = request.Host.Port; int? requestPort = request.Host.Port;
@ -948,7 +948,7 @@ namespace Emby.Server.Implementations
public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true) public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true)
{ {
// With an empty source, the port will be null // With an empty source, the port will be null
var smart = NetManager.GetBindAddress(ipAddress, out _, true); var smart = NetManager.GetBindAddress(ipAddress, out _, false);
var scheme = !allowHttps ? Uri.UriSchemeHttp : null; var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
int? port = !allowHttps ? HttpPort : null; int? port = !allowHttps ? HttpPort : null;
return GetLocalApiUrl(smart, scheme, port); return GetLocalApiUrl(smart, scheme, port);

@ -18,7 +18,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints namespace Emby.Server.Implementations.EntryPoints
{ {
/// <summary> /// <summary>
/// Class UdpServerEntryPoint. /// Class responsible for registering all UDP broadcast endpoints and their handlers.
/// </summary> /// </summary>
public sealed class UdpServerEntryPoint : IServerEntryPoint public sealed class UdpServerEntryPoint : IServerEntryPoint
{ {
@ -35,7 +35,6 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly IConfiguration _config; private readonly IConfiguration _config;
private readonly IConfigurationManager _configurationManager; private readonly IConfigurationManager _configurationManager;
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
private readonly bool _enableMultiSocketBinding;
/// <summary> /// <summary>
/// The UDP server. /// The UDP server.
@ -65,7 +64,6 @@ namespace Emby.Server.Implementations.EntryPoints
_configurationManager = configurationManager; _configurationManager = configurationManager;
_networkManager = networkManager; _networkManager = networkManager;
_udpServers = new List<UdpServer>(); _udpServers = new List<UdpServer>();
_enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux();
} }
/// <inheritdoc /> /// <inheritdoc />
@ -80,14 +78,16 @@ namespace Emby.Server.Implementations.EntryPoints
try try
{ {
if (_enableMultiSocketBinding) // 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 socket // Add global broadcast listener
var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber); var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber);
server.Start(_cancellationTokenSource.Token); server.Start(_cancellationTokenSource.Token);
_udpServers.Add(server); _udpServers.Add(server);
// Add bind address specific broadcast sockets // Add bind address specific broadcast listeners
// IPv6 is currently unsupported // IPv6 is currently unsupported
var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork); var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
foreach (var intf in validInterfaces) foreach (var intf in validInterfaces)
@ -102,9 +102,18 @@ namespace Emby.Server.Implementations.EntryPoints
} }
else else
{ {
var server = new UdpServer(_logger, _appHost, _config, IPAddress.Any, PortNumber); // Add bind address specific broadcast listeners
server.Start(_cancellationTokenSource.Token); // IPv6 is currently unsupported
_udpServers.Add(server); 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) catch (SocketException ex)
@ -119,7 +128,7 @@ namespace Emby.Server.Implementations.EntryPoints
{ {
if (_disposed) if (_disposed)
{ {
throw new ObjectDisposedException(this.GetType().Name); throw new ObjectDisposedException(GetType().Name);
} }
} }

@ -1,12 +1,15 @@
#pragma warning disable CS1591
using System; using System;
using System.Linq;
using System.Net; using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets; using System.Net.Sockets;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
namespace Emby.Server.Implementations.Net namespace Emby.Server.Implementations.Net
{ {
/// <summary>
/// Factory class to create different kinds of sockets.
/// </summary>
public class SocketFactory : ISocketFactory public class SocketFactory : ISocketFactory
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -38,7 +41,8 @@ namespace Emby.Server.Implementations.Net
/// <inheritdoc /> /// <inheritdoc />
public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort) public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort)
{ {
ArgumentNullException.ThrowIfNull(bindInterface.Address); var interfaceAddress = bindInterface.Address;
ArgumentNullException.ThrowIfNull(interfaceAddress);
if (localPort < 0) if (localPort < 0)
{ {
@ -49,7 +53,7 @@ namespace Emby.Server.Implementations.Net
try try
{ {
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
socket.Bind(new IPEndPoint(bindInterface.Address, localPort)); socket.Bind(new IPEndPoint(interfaceAddress, localPort));
return socket; return socket;
} }
@ -82,16 +86,25 @@ namespace Emby.Server.Implementations.Net
try try
{ {
var interfaceIndex = bindInterface.Index;
var interfaceIndexSwapped = (int)IPAddress.HostToNetworkOrder(interfaceIndex);
socket.MulticastLoopback = false; socket.MulticastLoopback = false;
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true);
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, interfaceIndexSwapped);
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex)); if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
socket.Bind(new IPEndPoint(multicastAddress, localPort)); {
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress));
socket.Bind(new IPEndPoint(multicastAddress, localPort));
}
else
{
// Only create socket if interface supports multicast
var interfaceIndex = bindInterface.Index;
var interfaceIndexSwapped = IPAddress.HostToNetworkOrder(interfaceIndex);
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex));
socket.Bind(new IPEndPoint(bindIPAddress, localPort));
}
return socket; return socket;
} }

@ -52,7 +52,10 @@ namespace Emby.Server.Implementations.Udp
_endpoint = new IPEndPoint(bindAddress, port); _endpoint = new IPEndPoint(bindAddress, port);
_udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
{
MulticastLoopback = false,
};
_udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
} }
@ -74,6 +77,7 @@ namespace Emby.Server.Implementations.Udp
try try
{ {
_logger.LogDebug("Sending AutoDiscovery response");
await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false); await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false);
} }
catch (SocketException ex) catch (SocketException ex)
@ -99,7 +103,8 @@ namespace Emby.Server.Implementations.Udp
{ {
try try
{ {
var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint, cancellationToken).ConfigureAwait(false); 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); var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase)) if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
{ {
@ -112,7 +117,7 @@ namespace Emby.Server.Implementations.Udp
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// Don't throw _logger.LogDebug("Broadcast socket operation cancelled");
} }
} }
} }

@ -231,12 +231,12 @@ public static partial class NetworkExtensions
} }
else if (address.AddressFamily == AddressFamily.InterNetwork) else if (address.AddressFamily == AddressFamily.InterNetwork)
{ {
result = new IPNetwork(address, Network.MinimumIPv4PrefixSize); result = address.Equals(IPAddress.Any) ? Network.IPv4Any : new IPNetwork(address, Network.MinimumIPv4PrefixSize);
return true; return true;
} }
else if (address.AddressFamily == AddressFamily.InterNetworkV6) else if (address.AddressFamily == AddressFamily.InterNetworkV6)
{ {
result = new IPNetwork(address, Network.MinimumIPv6PrefixSize); result = address.Equals(IPAddress.IPv6Any) ? Network.IPv6Any : new IPNetwork(address, Network.MinimumIPv6PrefixSize);
return true; return true;
} }
} }
@ -284,12 +284,15 @@ public static partial class NetworkExtensions
if (hosts.Count <= 2) if (hosts.Count <= 2)
{ {
var firstPart = hosts[0];
// Is hostname or hostname:port // Is hostname or hostname:port
if (FqdnGeneratedRegex().IsMatch(hosts[0])) if (FqdnGeneratedRegex().IsMatch(firstPart))
{ {
try try
{ {
addresses = Dns.GetHostAddresses(hosts[0]); // .NET automatically filters only supported returned addresses based on OS support.
addresses = Dns.GetHostAddresses(firstPart);
return true; return true;
} }
catch (SocketException) catch (SocketException)
@ -299,7 +302,7 @@ public static partial class NetworkExtensions
} }
// Is an IPv4 or IPv4:port // Is an IPv4 or IPv4:port
if (IPAddress.TryParse(hosts[0].AsSpan().LeftPart('/'), out var address)) if (IPAddress.TryParse(firstPart.AsSpan().LeftPart('/'), out var address))
{ {
if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled)) if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled))
|| ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled))) || ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled)))

@ -15,7 +15,9 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Jellyfin.Networking.Manager namespace Jellyfin.Networking.Manager
{ {
@ -33,12 +35,14 @@ namespace Jellyfin.Networking.Manager
private readonly IConfigurationManager _configurationManager; private readonly IConfigurationManager _configurationManager;
private readonly IConfiguration _startupConfig;
private readonly object _networkEventLock; private readonly object _networkEventLock;
/// <summary> /// <summary>
/// Holds the published server URLs and the IPs to use them on. /// Holds the published server URLs and the IPs to use them on.
/// </summary> /// </summary>
private IReadOnlyDictionary<IPData, string> _publishedServerUrls; private IReadOnlyList<PublishedServerUriOverride> _publishedServerUrls;
private IReadOnlyList<IPNetwork> _remoteAddressFilter; private IReadOnlyList<IPNetwork> _remoteAddressFilter;
@ -76,20 +80,22 @@ namespace Jellyfin.Networking.Manager
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="NetworkManager"/> class. /// Initializes a new instance of the <see cref="NetworkManager"/> class.
/// </summary> /// </summary>
/// <param name="configurationManager">IServerConfigurationManager instance.</param> /// <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> /// <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. #pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in UpdateSettings function. Compiler doesn't yet recognise this.
public NetworkManager(IConfigurationManager configurationManager, ILogger<NetworkManager> logger) public NetworkManager(IConfigurationManager configurationManager, IConfiguration startupConfig, ILogger<NetworkManager> logger)
{ {
ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(configurationManager); ArgumentNullException.ThrowIfNull(configurationManager);
_logger = logger; _logger = logger;
_configurationManager = configurationManager; _configurationManager = configurationManager;
_startupConfig = startupConfig;
_initLock = new(); _initLock = new();
_interfaces = new List<IPData>(); _interfaces = new List<IPData>();
_macAddresses = new List<PhysicalAddress>(); _macAddresses = new List<PhysicalAddress>();
_publishedServerUrls = new Dictionary<IPData, string>(); _publishedServerUrls = new List<PublishedServerUriOverride>();
_networkEventLock = new object(); _networkEventLock = new object();
_remoteAddressFilter = new List<IPNetwork>(); _remoteAddressFilter = new List<IPNetwork>();
@ -130,7 +136,7 @@ namespace Jellyfin.Networking.Manager
/// <summary> /// <summary>
/// Gets the Published server override list. /// Gets the Published server override list.
/// </summary> /// </summary>
public IReadOnlyDictionary<IPData, string> PublishedServerUrls => _publishedServerUrls; public IReadOnlyList<PublishedServerUriOverride> PublishedServerUrls => _publishedServerUrls;
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() public void Dispose()
@ -170,7 +176,6 @@ namespace Jellyfin.Networking.Manager
{ {
if (!_eventfire) if (!_eventfire)
{ {
_logger.LogDebug("Network Address Change Event.");
// As network events tend to fire one after the other only fire once every second. // As network events tend to fire one after the other only fire once every second.
_eventfire = true; _eventfire = true;
OnNetworkChange(); OnNetworkChange();
@ -193,11 +198,12 @@ namespace Jellyfin.Networking.Manager
} }
else else
{ {
InitialiseInterfaces(); InitializeInterfaces();
InitialiseLan(networkConfig); InitializeLan(networkConfig);
EnforceBindSettings(networkConfig); EnforceBindSettings(networkConfig);
} }
PrintNetworkInformation(networkConfig);
NetworkChanged?.Invoke(this, EventArgs.Empty); NetworkChanged?.Invoke(this, EventArgs.Empty);
} }
finally finally
@ -210,7 +216,7 @@ namespace Jellyfin.Networking.Manager
/// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state. /// 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. /// Generate a list of all active mac addresses that aren't loopback addresses.
/// </summary> /// </summary>
private void InitialiseInterfaces() private void InitializeInterfaces()
{ {
lock (_initLock) lock (_initLock)
{ {
@ -222,7 +228,7 @@ namespace Jellyfin.Networking.Manager
try try
{ {
var nics = NetworkInterface.GetAllNetworkInterfaces() var nics = NetworkInterface.GetAllNetworkInterfaces()
.Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up); .Where(i => i.OperationalStatus == OperationalStatus.Up);
foreach (NetworkInterface adapter in nics) foreach (NetworkInterface adapter in nics)
{ {
@ -242,34 +248,36 @@ namespace Jellyfin.Networking.Manager
{ {
if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork)
{ {
var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
interfaceObject.Index = ipProperties.GetIPv4Properties().Index; {
interfaceObject.Name = adapter.Name; Index = ipProperties.GetIPv4Properties().Index,
Name = adapter.Name,
SupportsMulticast = adapter.SupportsMulticast
};
interfaces.Add(interfaceObject); interfaces.Add(interfaceObject);
} }
else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6)
{ {
var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
interfaceObject.Index = ipProperties.GetIPv6Properties().Index; {
interfaceObject.Name = adapter.Name; Index = ipProperties.GetIPv6Properties().Index,
Name = adapter.Name,
SupportsMulticast = adapter.SupportsMulticast
};
interfaces.Add(interfaceObject); interfaces.Add(interfaceObject);
} }
} }
} }
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex) catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{ {
// Ignore error, and attempt to continue. // Ignore error, and attempt to continue.
_logger.LogError(ex, "Error encountered parsing interfaces."); _logger.LogError(ex, "Error encountered parsing interfaces.");
} }
} }
} }
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex) catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{ {
_logger.LogError(ex, "Error obtaining interfaces."); _logger.LogError(ex, "Error obtaining interfaces.");
} }
@ -279,14 +287,14 @@ namespace Jellyfin.Networking.Manager
{ {
_logger.LogWarning("No interface information available. Using loopback interface(s)."); _logger.LogWarning("No interface information available. Using loopback interface(s).");
if (IsIPv4Enabled && !IsIPv6Enabled) if (IsIPv4Enabled)
{ {
interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo")); interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
} }
if (!IsIPv4Enabled && IsIPv6Enabled) if (IsIPv6Enabled)
{ {
interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo")); interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
} }
} }
@ -299,9 +307,9 @@ namespace Jellyfin.Networking.Manager
} }
/// <summary> /// <summary>
/// Initialises internal LAN cache. /// Initializes internal LAN cache.
/// </summary> /// </summary>
private void InitialiseLan(NetworkConfiguration config) private void InitializeLan(NetworkConfiguration config)
{ {
lock (_initLock) lock (_initLock)
{ {
@ -341,10 +349,6 @@ namespace Jellyfin.Networking.Manager
_excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true) _excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true)
? excludedSubnets ? excludedSubnets
: new List<IPNetwork>(); : new List<IPNetwork>();
_logger.LogInformation("Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
_logger.LogInformation("Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
_logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength));
} }
} }
@ -369,12 +373,12 @@ namespace Jellyfin.Networking.Manager
.ToHashSet(); .ToHashSet();
interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList(); interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList();
if (bindAddresses.Contains(IPAddress.Loopback)) if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback)))
{ {
interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo")); interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
} }
if (bindAddresses.Contains(IPAddress.IPv6Loopback)) if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback)))
{ {
interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo")); interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
} }
@ -409,15 +413,14 @@ namespace Jellyfin.Networking.Manager
interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6); interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6);
} }
_logger.LogInformation("Using bind addresses: {0}", interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
_interfaces = interfaces; _interfaces = interfaces;
} }
} }
/// <summary> /// <summary>
/// Initialises the remote address values. /// Initializes the remote address values.
/// </summary> /// </summary>
private void InitialiseRemote(NetworkConfiguration config) private void InitializeRemote(NetworkConfiguration config)
{ {
lock (_initLock) lock (_initLock)
{ {
@ -455,13 +458,33 @@ namespace Jellyfin.Networking.Manager
/// format is subnet=ipaddress|host|uri /// format is subnet=ipaddress|host|uri
/// when subnet = 0.0.0.0, any external address matches. /// when subnet = 0.0.0.0, any external address matches.
/// </summary> /// </summary>
private void InitialiseOverrides(NetworkConfiguration config) private void InitializeOverrides(NetworkConfiguration config)
{ {
lock (_initLock) lock (_initLock)
{ {
var publishedServerUrls = new Dictionary<IPData, string>(); var publishedServerUrls = new List<PublishedServerUriOverride>();
var overrides = config.PublishedServerUriBySubnet;
// Prefer startup configuration.
var startupOverrideKey = _startupConfig[AddressOverrideKey];
if (!string.IsNullOrEmpty(startupOverrideKey))
{
publishedServerUrls.Add(
new PublishedServerUriOverride(
new IPData(IPAddress.Any, Network.IPv4Any),
startupOverrideKey,
true,
true));
publishedServerUrls.Add(
new PublishedServerUriOverride(
new IPData(IPAddress.IPv6Any, Network.IPv6Any),
startupOverrideKey,
true,
true));
_publishedServerUrls = publishedServerUrls;
return;
}
var overrides = config.PublishedServerUriBySubnet;
foreach (var entry in overrides) foreach (var entry in overrides)
{ {
var parts = entry.Split('='); var parts = entry.Split('=');
@ -475,31 +498,70 @@ namespace Jellyfin.Networking.Manager
var identifier = parts[0]; var identifier = parts[0];
if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase)) if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase))
{ {
publishedServerUrls[new IPData(IPAddress.Broadcast, null)] = replacement; // Drop any other overrides in case an "all" override exists
publishedServerUrls.Clear();
publishedServerUrls.Add(
new PublishedServerUriOverride(
new IPData(IPAddress.Any, Network.IPv4Any),
replacement,
true,
true));
publishedServerUrls.Add(
new PublishedServerUriOverride(
new IPData(IPAddress.IPv6Any, Network.IPv6Any),
replacement,
true,
true));
break;
} }
else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase))
{ {
publishedServerUrls[new IPData(IPAddress.Any, Network.IPv4Any)] = replacement; publishedServerUrls.Add(
publishedServerUrls[new IPData(IPAddress.IPv6Any, Network.IPv6Any)] = replacement; new PublishedServerUriOverride(
new IPData(IPAddress.Any, Network.IPv4Any),
replacement,
false,
true));
publishedServerUrls.Add(
new PublishedServerUriOverride(
new IPData(IPAddress.IPv6Any, Network.IPv6Any),
replacement,
false,
true));
} }
else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase))
{ {
foreach (var lan in _lanSubnets) foreach (var lan in _lanSubnets)
{ {
var lanPrefix = lan.Prefix; var lanPrefix = lan.Prefix;
publishedServerUrls[new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength))] = replacement; publishedServerUrls.Add(
new PublishedServerUriOverride(
new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)),
replacement,
true,
false));
} }
} }
else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null) else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null)
{ {
var data = new IPData(result.Prefix, result); var data = new IPData(result.Prefix, result);
publishedServerUrls[data] = replacement; publishedServerUrls.Add(
new PublishedServerUriOverride(
data,
replacement,
true,
true));
} }
else if (TryParseInterface(identifier, out var ifaces)) else if (TryParseInterface(identifier, out var ifaces))
{ {
foreach (var iface in ifaces) foreach (var iface in ifaces)
{ {
publishedServerUrls[iface] = replacement; publishedServerUrls.Add(
new PublishedServerUriOverride(
iface,
replacement,
true,
true));
} }
} }
else else
@ -521,7 +583,7 @@ namespace Jellyfin.Networking.Manager
} }
/// <summary> /// <summary>
/// Reloads all settings and re-initialises the instance. /// Reloads all settings and re-Initializes the instance.
/// </summary> /// </summary>
/// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param> /// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param>
public void UpdateSettings(object configuration) public void UpdateSettings(object configuration)
@ -531,12 +593,12 @@ namespace Jellyfin.Networking.Manager
var config = (NetworkConfiguration)configuration; var config = (NetworkConfiguration)configuration;
HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6; HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6;
InitialiseLan(config); InitializeLan(config);
InitialiseRemote(config); InitializeRemote(config);
if (string.IsNullOrEmpty(MockNetworkSettings)) if (string.IsNullOrEmpty(MockNetworkSettings))
{ {
InitialiseInterfaces(); InitializeInterfaces();
} }
else // Used in testing only. else // Used in testing only.
{ {
@ -552,8 +614,10 @@ namespace Jellyfin.Networking.Manager
var index = int.Parse(parts[1], CultureInfo.InvariantCulture); var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6)
{ {
var data = new IPData(address, subnet, parts[2]); var data = new IPData(address, subnet, parts[2])
data.Index = index; {
Index = index
};
interfaces.Add(data); interfaces.Add(data);
} }
} }
@ -567,7 +631,9 @@ namespace Jellyfin.Networking.Manager
} }
EnforceBindSettings(config); EnforceBindSettings(config);
InitialiseOverrides(config); InitializeOverrides(config);
PrintNetworkInformation(config, false);
} }
/// <summary> /// <summary>
@ -672,20 +738,13 @@ namespace Jellyfin.Networking.Manager
/// <inheritdoc/> /// <inheritdoc/>
public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false) public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false)
{ {
if (_interfaces.Count != 0) if (_interfaces.Count > 0 || individualInterfaces)
{ {
return _interfaces; return _interfaces;
} }
// No bind address and no exclusions, so listen on all interfaces. // No bind address and no exclusions, so listen on all interfaces.
var result = new List<IPData>(); var result = new List<IPData>();
if (individualInterfaces)
{
result.AddRange(_interfaces);
return result;
}
if (IsIPv4Enabled && IsIPv6Enabled) if (IsIPv4Enabled && IsIPv6Enabled)
{ {
// Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default
@ -892,31 +951,34 @@ namespace Jellyfin.Networking.Manager
bindPreference = string.Empty; bindPreference = string.Empty;
int? port = null; int? port = null;
var validPublishedServerUrls = _publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.Any) // Only consider subnets including the source IP, prefering specific overrides
|| x.Key.Address.Equals(IPAddress.IPv6Any) List<PublishedServerUriOverride> validPublishedServerUrls;
|| x.Key.Subnet.Contains(source)) if (!isInExternalSubnet)
.DistinctBy(x => x.Key) {
.OrderBy(x => x.Key.Address.Equals(IPAddress.Any) // Only use matching internal subnets
|| x.Key.Address.Equals(IPAddress.IPv6Any)) // 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(); .ToList();
}
// Check for user override.
foreach (var data in validPublishedServerUrls) foreach (var data in validPublishedServerUrls)
{ {
if (isInExternalSubnet && (data.Key.Address.Equals(IPAddress.Any) || data.Key.Address.Equals(IPAddress.IPv6Any))) // Get interface matching override subnet
{ var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
// External.
bindPreference = data.Value;
break;
}
// Get address interface.
var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Key.Subnet.Contains(x.Address));
if (intf?.Address is not null) if (intf?.Address is not null)
{ {
// Match IP address. // If matching interface is found, use override
bindPreference = data.Value; bindPreference = data.OverrideUri;
break; break;
} }
} }
@ -927,7 +989,7 @@ namespace Jellyfin.Networking.Manager
return false; return false;
} }
// Has it got a port defined? // Handle override specifying port
var parts = bindPreference.Split(':'); var parts = bindPreference.Split(':');
if (parts.Length > 1) if (parts.Length > 1)
{ {
@ -935,18 +997,12 @@ namespace Jellyfin.Networking.Manager
{ {
bindPreference = parts[0]; bindPreference = parts[0];
port = p; port = p;
_logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
return true;
} }
} }
if (port is not null) _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
{
_logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
}
else
{
_logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
}
return true; return true;
} }
@ -1053,5 +1109,19 @@ namespace Jellyfin.Networking.Manager
_logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result); _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result);
return true; 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));
}
}
} }
} }

@ -282,7 +282,7 @@ namespace Jellyfin.Server.Extensions
AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength); AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength);
} }
} }
else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses)) else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6))
{ {
foreach (var address in addresses) foreach (var address in addresses)
{ {

@ -3,7 +3,6 @@ using System.IO;
using System.Net; using System.Net;
using Jellyfin.Server.Helpers; using Jellyfin.Server.Helpers;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Extensions;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -36,7 +35,7 @@ public static class WebHostBuilderExtensions
return builder return builder
.UseKestrel((builderContext, options) => .UseKestrel((builderContext, options) =>
{ {
var addresses = appHost.NetManager.GetAllBindInterfaces(); var addresses = appHost.NetManager.GetAllBindInterfaces(true);
bool flagged = false; bool flagged = false;
foreach (var netAdd in addresses) foreach (var netAdd in addresses)

@ -47,6 +47,11 @@ public class IPData
/// </summary> /// </summary>
public int Index { get; set; } public int Index { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the network supports multicast.
/// </summary>
public bool SupportsMulticast { get; set; } = false;
/// <summary> /// <summary>
/// Gets or sets the interface name. /// Gets or sets the interface name.
/// </summary> /// </summary>

@ -0,0 +1,42 @@
namespace MediaBrowser.Model.Net;
/// <summary>
/// Class holding information for a published server URI override.
/// </summary>
public class PublishedServerUriOverride
{
/// <summary>
/// Initializes a new instance of the <see cref="PublishedServerUriOverride"/> class.
/// </summary>
/// <param name="data">The <see cref="IPData"/>.</param>
/// <param name="overrideUri">The override.</param>
/// <param name="internalOverride">A value indicating whether the override is for internal requests.</param>
/// <param name="externalOverride">A value indicating whether the override is for external requests.</param>
public PublishedServerUriOverride(IPData data, string overrideUri, bool internalOverride, bool externalOverride)
{
Data = data;
OverrideUri = overrideUri;
IsInternalOverride = internalOverride;
IsExternalOverride = externalOverride;
}
/// <summary>
/// Gets or sets the object's IP address.
/// </summary>
public IPData Data { get; set; }
/// <summary>
/// Gets or sets the override URI.
/// </summary>
public string OverrideUri { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the override should be applied to internal requests.
/// </summary>
public bool IsInternalOverride { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the override should be applied to external requests.
/// </summary>
public bool IsExternalOverride { get; set; }
}

@ -32,10 +32,10 @@ namespace Rssdp.Infrastructure
* port to use, we will default to 0 which allows the underlying system to auto-assign a free port. * port to use, we will default to 0 which allows the underlying system to auto-assign a free port.
*/ */
private object _BroadcastListenSocketSynchroniser = new object(); private object _BroadcastListenSocketSynchroniser = new();
private List<Socket> _MulticastListenSockets; private List<Socket> _MulticastListenSockets;
private object _SendSocketSynchroniser = new object(); private object _SendSocketSynchroniser = new();
private List<Socket> _sendSockets; private List<Socket> _sendSockets;
private HttpRequestParser _RequestParser; private HttpRequestParser _RequestParser;
@ -48,7 +48,6 @@ namespace Rssdp.Infrastructure
private int _MulticastTtl; private int _MulticastTtl;
private bool _IsShared; private bool _IsShared;
private readonly bool _enableMultiSocketBinding;
/// <summary> /// <summary>
/// Raised when a HTTPU request message is received by a socket (unicast or multicast). /// Raised when a HTTPU request message is received by a socket (unicast or multicast).
@ -64,9 +63,11 @@ namespace Rssdp.Infrastructure
/// Minimum constructor. /// Minimum constructor.
/// </summary> /// </summary>
/// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception> /// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception>
public SsdpCommunicationsServer(ISocketFactory socketFactory, public SsdpCommunicationsServer(
INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding) ISocketFactory socketFactory,
: this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding) INetworkManager networkManager,
ILogger logger)
: this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger)
{ {
} }
@ -76,7 +77,12 @@ namespace Rssdp.Infrastructure
/// </summary> /// </summary>
/// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception> /// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">The <paramref name="multicastTimeToLive"/> argument is less than or equal to zero.</exception> /// <exception cref="ArgumentOutOfRangeException">The <paramref name="multicastTimeToLive"/> argument is less than or equal to zero.</exception>
public SsdpCommunicationsServer(ISocketFactory socketFactory, int localPort, int multicastTimeToLive, INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding) public SsdpCommunicationsServer(
ISocketFactory socketFactory,
int localPort,
int multicastTimeToLive,
INetworkManager networkManager,
ILogger logger)
{ {
if (socketFactory is null) if (socketFactory is null)
{ {
@ -88,19 +94,18 @@ namespace Rssdp.Infrastructure
throw new ArgumentOutOfRangeException(nameof(multicastTimeToLive), "multicastTimeToLive must be greater than zero."); throw new ArgumentOutOfRangeException(nameof(multicastTimeToLive), "multicastTimeToLive must be greater than zero.");
} }
_BroadcastListenSocketSynchroniser = new object(); _BroadcastListenSocketSynchroniser = new();
_SendSocketSynchroniser = new object(); _SendSocketSynchroniser = new();
_LocalPort = localPort; _LocalPort = localPort;
_SocketFactory = socketFactory; _SocketFactory = socketFactory;
_RequestParser = new HttpRequestParser(); _RequestParser = new();
_ResponseParser = new HttpResponseParser(); _ResponseParser = new();
_MulticastTtl = multicastTimeToLive; _MulticastTtl = multicastTimeToLive;
_networkManager = networkManager; _networkManager = networkManager;
_logger = logger; _logger = logger;
_enableMultiSocketBinding = enableMultiSocketBinding;
} }
/// <summary> /// <summary>
@ -335,7 +340,7 @@ namespace Rssdp.Infrastructure
{ {
sockets = sockets.ToList(); sockets = sockets.ToList();
var tasks = sockets.Where(s => (fromlocalIPAddress is null || fromlocalIPAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address))) var tasks = sockets.Where(s => fromlocalIPAddress is null || fromlocalIPAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address))
.Select(s => SendFromSocket(s, messageData, destination, cancellationToken)); .Select(s => SendFromSocket(s, messageData, destination, cancellationToken));
return Task.WhenAll(tasks); return Task.WhenAll(tasks);
} }
@ -347,33 +352,26 @@ namespace Rssdp.Infrastructure
{ {
var sockets = new List<Socket>(); var sockets = new List<Socket>();
var multicastGroupAddress = IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress); var multicastGroupAddress = IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress);
if (_enableMultiSocketBinding)
{
// IPv6 is currently unsupported
var validInterfaces = _networkManager.GetInternalBindAddresses()
.Where(x => x.Address is not null)
.Where(x => x.AddressFamily == AddressFamily.InterNetwork)
.DistinctBy(x => x.Index);
foreach (var intf in validInterfaces) // IPv6 is currently unsupported
var validInterfaces = _networkManager.GetInternalBindAddresses()
.Where(x => x.Address is not null)
.Where(x => x.SupportsMulticast)
.Where(x => x.AddressFamily == AddressFamily.InterNetwork)
.DistinctBy(x => x.Index);
foreach (var intf in validInterfaces)
{
try
{ {
try var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, intf, _MulticastTtl, SsdpConstants.MulticastPort);
{ _ = ListenToSocketInternal(socket);
var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, intf, _MulticastTtl, SsdpConstants.MulticastPort); sockets.Add(socket);
_ = ListenToSocketInternal(socket); }
sockets.Add(socket); catch (Exception ex)
} {
catch (Exception ex) _logger.LogError(ex, "Failed to create SSDP UDP multicast socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index);
{
_logger.LogError(ex, "Error in CreateMulticastSocketsAndListen. IP address: {0}", intf.Address);
}
} }
}
else
{
var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, new IPData(IPAddress.Any, null), _MulticastTtl, SsdpConstants.MulticastPort);
_ = ListenToSocketInternal(socket);
sockets.Add(socket);
} }
return sockets; return sockets;
@ -382,34 +380,32 @@ namespace Rssdp.Infrastructure
private List<Socket> CreateSendSockets() private List<Socket> CreateSendSockets()
{ {
var sockets = new List<Socket>(); var sockets = new List<Socket>();
if (_enableMultiSocketBinding)
// IPv6 is currently unsupported
var validInterfaces = _networkManager.GetInternalBindAddresses()
.Where(x => x.Address is not null)
.Where(x => x.SupportsMulticast)
.Where(x => x.AddressFamily == AddressFamily.InterNetwork);
if (OperatingSystem.IsMacOS())
{ {
// IPv6 is currently unsupported // Manually remove loopback on macOS due to https://github.com/dotnet/runtime/issues/24340
var validInterfaces = _networkManager.GetInternalBindAddresses() validInterfaces = validInterfaces.Where(x => !x.Address.Equals(IPAddress.Loopback));
.Where(x => x.Address is not null) }
.Where(x => x.AddressFamily == AddressFamily.InterNetwork);
foreach (var intf in validInterfaces) foreach (var intf in validInterfaces)
{
try
{ {
try var socket = _SocketFactory.CreateSsdpUdpSocket(intf, _LocalPort);
{ _ = ListenToSocketInternal(socket);
var socket = _SocketFactory.CreateSsdpUdpSocket(intf, _LocalPort); sockets.Add(socket);
_ = ListenToSocketInternal(socket); }
sockets.Add(socket); catch (Exception ex)
} {
catch (Exception ex) _logger.LogError(ex, "Failed to create SSDP UDP sender socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index);
{
_logger.LogError(ex, "Error in CreateSsdpUdpSocket. IPAddress: {0}", intf.Address);
}
} }
} }
else
{
var socket = _SocketFactory.CreateSsdpUdpSocket(new IPData(IPAddress.Any, null), _LocalPort);
_ = ListenToSocketInternal(socket);
sockets.Add(socket);
}
return sockets; return sockets;
} }
@ -423,7 +419,7 @@ namespace Rssdp.Infrastructure
{ {
try try
{ {
var result = await socket.ReceiveMessageFromAsync(receiveBuffer, SocketFlags.None, new IPEndPoint(IPAddress.Any, 0), CancellationToken.None).ConfigureAwait(false);; var result = await socket.ReceiveMessageFromAsync(receiveBuffer, new IPEndPoint(IPAddress.Any, _LocalPort), CancellationToken.None).ConfigureAwait(false);;
if (result.ReceivedBytes > 0) if (result.ReceivedBytes > 0)
{ {
@ -431,7 +427,7 @@ namespace Rssdp.Infrastructure
var localEndpointAdapter = _networkManager.GetAllBindInterfaces().First(a => a.Index == result.PacketInformation.Interface); var localEndpointAdapter = _networkManager.GetAllBindInterfaces().First(a => a.Index == result.PacketInformation.Interface);
ProcessMessage( ProcessMessage(
UTF8Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes), Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes),
remoteEndpoint, remoteEndpoint,
localEndpointAdapter.Address); localEndpointAdapter.Address);
} }
@ -511,7 +507,7 @@ namespace Rssdp.Infrastructure
return; return;
} }
var handlers = this.RequestReceived; var handlers = RequestReceived;
if (handlers is not null) if (handlers is not null)
{ {
handlers(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnlocalIPAddress)); handlers(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnlocalIPAddress));
@ -520,7 +516,7 @@ namespace Rssdp.Infrastructure
private void OnResponseReceived(HttpResponseMessage data, IPEndPoint endPoint, IPAddress localIPAddress) private void OnResponseReceived(HttpResponseMessage data, IPEndPoint endPoint, IPAddress localIPAddress)
{ {
var handlers = this.ResponseReceived; var handlers = ResponseReceived;
if (handlers is not null) if (handlers is not null)
{ {
handlers(this, new ResponseReceivedEventArgs(data, endPoint) handlers(this, new ResponseReceivedEventArgs(data, endPoint)

@ -17,7 +17,7 @@ namespace Rssdp.Infrastructure
private ISsdpCommunicationsServer _CommunicationsServer; private ISsdpCommunicationsServer _CommunicationsServer;
private Timer _BroadcastTimer; private Timer _BroadcastTimer;
private object _timerLock = new object(); private object _timerLock = new();
private string _OSName; private string _OSName;
@ -221,12 +221,12 @@ namespace Rssdp.Infrastructure
/// <seealso cref="DeviceAvailable"/> /// <seealso cref="DeviceAvailable"/>
protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IPAddress) protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IPAddress)
{ {
if (this.IsDisposed) if (IsDisposed)
{ {
return; return;
} }
var handlers = this.DeviceAvailable; var handlers = DeviceAvailable;
if (handlers is not null) if (handlers is not null)
{ {
handlers(this, new DeviceAvailableEventArgs(device, isNewDevice) handlers(this, new DeviceAvailableEventArgs(device, isNewDevice)
@ -244,12 +244,12 @@ namespace Rssdp.Infrastructure
/// <seealso cref="DeviceUnavailable"/> /// <seealso cref="DeviceUnavailable"/>
protected virtual void OnDeviceUnavailable(DiscoveredSsdpDevice device, bool expired) protected virtual void OnDeviceUnavailable(DiscoveredSsdpDevice device, bool expired)
{ {
if (this.IsDisposed) if (IsDisposed)
{ {
return; return;
} }
var handlers = this.DeviceUnavailable; var handlers = DeviceUnavailable;
if (handlers is not null) if (handlers is not null)
{ {
handlers(this, new DeviceUnavailableEventArgs(device, expired)); handlers(this, new DeviceUnavailableEventArgs(device, expired));
@ -291,8 +291,8 @@ namespace Rssdp.Infrastructure
_CommunicationsServer = null; _CommunicationsServer = null;
if (commsServer is not null) if (commsServer is not null)
{ {
commsServer.ResponseReceived -= this.CommsServer_ResponseReceived; commsServer.ResponseReceived -= CommsServer_ResponseReceived;
commsServer.RequestReceived -= this.CommsServer_RequestReceived; commsServer.RequestReceived -= CommsServer_RequestReceived;
} }
} }
} }
@ -341,7 +341,7 @@ namespace Rssdp.Infrastructure
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
values["HOST"] = "239.255.255.250:1900"; values["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort);
values["USER-AGENT"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); values["USER-AGENT"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion);
values["MAN"] = "\"ssdp:discover\""; values["MAN"] = "\"ssdp:discover\"";
@ -382,17 +382,17 @@ namespace Rssdp.Infrastructure
private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress IPAddress) private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress IPAddress)
{ {
if (String.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0) if (string.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0)
{ {
return; return;
} }
var notificationType = GetFirstHeaderStringValue("NTS", message); var notificationType = GetFirstHeaderStringValue("NTS", message);
if (String.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0) if (string.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0)
{ {
ProcessAliveNotification(message, IPAddress); ProcessAliveNotification(message, IPAddress);
} }
else if (String.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0) else if (string.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0)
{ {
ProcessByeByeNotification(message); ProcessByeByeNotification(message);
} }
@ -420,7 +420,7 @@ namespace Rssdp.Infrastructure
private void ProcessByeByeNotification(HttpRequestMessage message) private void ProcessByeByeNotification(HttpRequestMessage message)
{ {
var notficationType = GetFirstHeaderStringValue("NT", message); var notficationType = GetFirstHeaderStringValue("NT", message);
if (!String.IsNullOrEmpty(notficationType)) if (!string.IsNullOrEmpty(notficationType))
{ {
var usn = GetFirstHeaderStringValue("USN", message); var usn = GetFirstHeaderStringValue("USN", message);
@ -447,10 +447,9 @@ namespace Rssdp.Infrastructure
private string GetFirstHeaderStringValue(string headerName, HttpResponseMessage message) private string GetFirstHeaderStringValue(string headerName, HttpResponseMessage message)
{ {
string retVal = null; string retVal = null;
IEnumerable<string> values;
if (message.Headers.Contains(headerName)) if (message.Headers.Contains(headerName))
{ {
message.Headers.TryGetValues(headerName, out values); message.Headers.TryGetValues(headerName, out var values);
if (values is not null) if (values is not null)
{ {
retVal = values.FirstOrDefault(); retVal = values.FirstOrDefault();
@ -463,10 +462,9 @@ namespace Rssdp.Infrastructure
private string GetFirstHeaderStringValue(string headerName, HttpRequestMessage message) private string GetFirstHeaderStringValue(string headerName, HttpRequestMessage message)
{ {
string retVal = null; string retVal = null;
IEnumerable<string> values;
if (message.Headers.Contains(headerName)) if (message.Headers.Contains(headerName))
{ {
message.Headers.TryGetValues(headerName, out values); message.Headers.TryGetValues(headerName, out var values);
if (values is not null) if (values is not null)
{ {
retVal = values.FirstOrDefault(); retVal = values.FirstOrDefault();
@ -479,10 +477,9 @@ namespace Rssdp.Infrastructure
private Uri GetFirstHeaderUriValue(string headerName, HttpRequestMessage request) private Uri GetFirstHeaderUriValue(string headerName, HttpRequestMessage request)
{ {
string value = null; string value = null;
IEnumerable<string> values;
if (request.Headers.Contains(headerName)) if (request.Headers.Contains(headerName))
{ {
request.Headers.TryGetValues(headerName, out values); request.Headers.TryGetValues(headerName, out var values);
if (values is not null) if (values is not null)
{ {
value = values.FirstOrDefault(); value = values.FirstOrDefault();
@ -496,10 +493,9 @@ namespace Rssdp.Infrastructure
private Uri GetFirstHeaderUriValue(string headerName, HttpResponseMessage response) private Uri GetFirstHeaderUriValue(string headerName, HttpResponseMessage response)
{ {
string value = null; string value = null;
IEnumerable<string> values;
if (response.Headers.Contains(headerName)) if (response.Headers.Contains(headerName))
{ {
response.Headers.TryGetValues(headerName, out values); response.Headers.TryGetValues(headerName, out var values);
if (values is not null) if (values is not null)
{ {
value = values.FirstOrDefault(); value = values.FirstOrDefault();
@ -529,7 +525,7 @@ namespace Rssdp.Infrastructure
foreach (var device in expiredDevices) foreach (var device in expiredDevices)
{ {
if (this.IsDisposed) if (IsDisposed)
{ {
return; return;
} }
@ -543,7 +539,7 @@ namespace Rssdp.Infrastructure
// problems. // problems.
foreach (var expiredUsn in (from expiredDevice in expiredDevices select expiredDevice.Usn).Distinct()) foreach (var expiredUsn in (from expiredDevice in expiredDevices select expiredDevice.Usn).Distinct())
{ {
if (this.IsDisposed) if (IsDisposed)
{ {
return; return;
} }
@ -560,7 +556,7 @@ namespace Rssdp.Infrastructure
existingDevices = FindExistingDeviceNotifications(_Devices, deviceUsn); existingDevices = FindExistingDeviceNotifications(_Devices, deviceUsn);
foreach (var existingDevice in existingDevices) foreach (var existingDevice in existingDevices)
{ {
if (this.IsDisposed) if (IsDisposed)
{ {
return true; return true;
} }

@ -206,9 +206,9 @@ namespace Rssdp.Infrastructure
IPAddress receivedOnlocalIPAddress, IPAddress receivedOnlocalIPAddress,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (String.IsNullOrEmpty(searchTarget)) if (string.IsNullOrEmpty(searchTarget))
{ {
WriteTrace(String.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString())); WriteTrace(string.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString()));
return; return;
} }
@ -232,7 +232,7 @@ namespace Rssdp.Infrastructure
// return; // return;
} }
if (!Int32.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0) if (!int.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0)
{ {
return; return;
} }
@ -243,27 +243,27 @@ namespace Rssdp.Infrastructure
} }
// Do not block synchronously as that may tie up a threadpool thread for several seconds. // Do not block synchronously as that may tie up a threadpool thread for several seconds.
Task.Delay(_Random.Next(16, (maxWaitInterval * 1000))).ContinueWith((parentTask) => Task.Delay(_Random.Next(16, maxWaitInterval * 1000)).ContinueWith((parentTask) =>
{ {
// Copying devices to local array here to avoid threading issues/enumerator exceptions. // Copying devices to local array here to avoid threading issues/enumerator exceptions.
IEnumerable<SsdpDevice> devices = null; IEnumerable<SsdpDevice> devices = null;
lock (_Devices) lock (_Devices)
{ {
if (String.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0) if (string.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0)
{ {
devices = GetAllDevicesAsFlatEnumerable().ToArray(); devices = GetAllDevicesAsFlatEnumerable().ToArray();
} }
else if (String.Compare(SsdpConstants.UpnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 || (this.SupportPnpRootDevice && String.Compare(SsdpConstants.PnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0)) else if (string.Compare(SsdpConstants.UpnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 || (SupportPnpRootDevice && String.Compare(SsdpConstants.PnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0))
{ {
devices = _Devices.ToArray(); devices = _Devices.ToArray();
} }
else if (searchTarget.Trim().StartsWith("uuid:", StringComparison.OrdinalIgnoreCase)) else if (searchTarget.Trim().StartsWith("uuid:", StringComparison.OrdinalIgnoreCase))
{ {
devices = GetAllDevicesAsFlatEnumerable().Where(d => String.Compare(d.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0).ToArray(); devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0).ToArray();
} }
else if (searchTarget.StartsWith("urn:", StringComparison.OrdinalIgnoreCase)) else if (searchTarget.StartsWith("urn:", StringComparison.OrdinalIgnoreCase))
{ {
devices = GetAllDevicesAsFlatEnumerable().Where(d => String.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray(); devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray();
} }
} }
@ -299,7 +299,7 @@ namespace Rssdp.Infrastructure
if (isRootDevice) if (isRootDevice)
{ {
SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken); SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken);
if (this.SupportPnpRootDevice) if (SupportPnpRootDevice)
{ {
SendSearchResponse(SsdpConstants.PnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken); SendSearchResponse(SsdpConstants.PnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken);
} }
@ -312,7 +312,7 @@ namespace Rssdp.Infrastructure
private string GetUsn(string udn, string fullDeviceType) private string GetUsn(string udn, string fullDeviceType)
{ {
return String.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType); return string.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType);
} }
private async void SendSearchResponse( private async void SendSearchResponse(
@ -326,16 +326,17 @@ namespace Rssdp.Infrastructure
const string header = "HTTP/1.1 200 OK"; const string header = "HTTP/1.1 200 OK";
var rootDevice = device.ToRootDevice(); var rootDevice = device.ToRootDevice();
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
values["EXT"] = ""; ["EXT"] = "",
values["DATE"] = DateTime.UtcNow.ToString("r"); ["DATE"] = DateTime.UtcNow.ToString("r"),
values["HOST"] = "239.255.255.250:1900"; ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort),
values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds; ["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds,
values["ST"] = searchTarget; ["ST"] = searchTarget,
values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion),
values["USN"] = uniqueServiceName; ["USN"] = uniqueServiceName,
values["LOCATION"] = rootDevice.Location.ToString(); ["LOCATION"] = rootDevice.Location.ToString()
};
var message = BuildMessage(header, values); var message = BuildMessage(header, values);
@ -439,7 +440,7 @@ namespace Rssdp.Infrastructure
if (isRoot) if (isRoot)
{ {
SendAliveNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken); SendAliveNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken);
if (this.SupportPnpRootDevice) if (SupportPnpRootDevice)
{ {
SendAliveNotification(device, SsdpConstants.PnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), cancellationToken); SendAliveNotification(device, SsdpConstants.PnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), cancellationToken);
} }
@ -460,17 +461,18 @@ namespace Rssdp.Infrastructure
const string header = "NOTIFY * HTTP/1.1"; const string header = "NOTIFY * HTTP/1.1";
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
// If needed later for non-server devices, these headers will need to be dynamic // If needed later for non-server devices, these headers will need to be dynamic
values["HOST"] = "239.255.255.250:1900"; ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort),
values["DATE"] = DateTime.UtcNow.ToString("r"); ["DATE"] = DateTime.UtcNow.ToString("r"),
values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds; ["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds,
values["LOCATION"] = rootDevice.Location.ToString(); ["LOCATION"] = rootDevice.Location.ToString(),
values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion),
values["NTS"] = "ssdp:alive"; ["NTS"] = "ssdp:alive",
values["NT"] = notificationType; ["NT"] = notificationType,
values["USN"] = uniqueServiceName; ["USN"] = uniqueServiceName
};
var message = BuildMessage(header, values); var message = BuildMessage(header, values);
@ -485,7 +487,7 @@ namespace Rssdp.Infrastructure
if (isRoot) if (isRoot)
{ {
tasks.Add(SendByeByeNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken)); tasks.Add(SendByeByeNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken));
if (this.SupportPnpRootDevice) if (SupportPnpRootDevice)
{ {
tasks.Add(SendByeByeNotification(device, "pnp:rootdevice", GetUsn(device.Udn, "pnp:rootdevice"), cancellationToken)); tasks.Add(SendByeByeNotification(device, "pnp:rootdevice", GetUsn(device.Udn, "pnp:rootdevice"), cancellationToken));
} }
@ -506,20 +508,21 @@ namespace Rssdp.Infrastructure
{ {
const string header = "NOTIFY * HTTP/1.1"; const string header = "NOTIFY * HTTP/1.1";
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
// If needed later for non-server devices, these headers will need to be dynamic // If needed later for non-server devices, these headers will need to be dynamic
values["HOST"] = "239.255.255.250:1900"; ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort),
values["DATE"] = DateTime.UtcNow.ToString("r"); ["DATE"] = DateTime.UtcNow.ToString("r"),
values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion),
values["NTS"] = "ssdp:byebye"; ["NTS"] = "ssdp:byebye",
values["NT"] = notificationType; ["NT"] = notificationType,
values["USN"] = uniqueServiceName; ["USN"] = uniqueServiceName
};
var message = BuildMessage(header, values); var message = BuildMessage(header, values);
var sendCount = IsDisposed ? 1 : 3; var sendCount = IsDisposed ? 1 : 3;
WriteTrace(String.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device); WriteTrace(string.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device);
return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken); return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken);
} }

@ -16,7 +16,6 @@ namespace Jellyfin.Networking.Tests
[InlineData("127.0.0.1:123")] [InlineData("127.0.0.1:123")]
[InlineData("localhost")] [InlineData("localhost")]
[InlineData("localhost:1345")] [InlineData("localhost:1345")]
[InlineData("www.google.co.uk")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")] [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")] [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")] [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")]

@ -1,7 +1,9 @@
using System.Net; using System.Net;
using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager; using Jellyfin.Networking.Manager;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit; using Xunit;
namespace Jellyfin.Networking.Tests namespace Jellyfin.Networking.Tests
@ -28,7 +30,8 @@ namespace Jellyfin.Networking.Tests
LocalNetworkSubnets = network.Split(',') LocalNetworkSubnets = network.Split(',')
}; };
using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger<NetworkManager>()); var startupConf = new Mock<IConfiguration>();
using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
Assert.True(networkManager.IsInLocalNetwork(ip)); Assert.True(networkManager.IsInLocalNetwork(ip));
} }
@ -56,9 +59,10 @@ namespace Jellyfin.Networking.Tests
LocalNetworkSubnets = network.Split(',') LocalNetworkSubnets = network.Split(',')
}; };
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger<NetworkManager>()); var startupConf = new Mock<IConfiguration>();
using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
Assert.False(nm.IsInLocalNetwork(ip)); Assert.False(networkManager.IsInLocalNetwork(ip));
} }
} }
} }

@ -7,6 +7,7 @@ using Jellyfin.Networking.Extensions;
using Jellyfin.Networking.Manager; using Jellyfin.Networking.Manager;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Moq; using Moq;
using Xunit; using Xunit;
@ -54,7 +55,8 @@ namespace Jellyfin.Networking.Tests
}; };
NetworkManager.MockNetworkSettings = interfaces; NetworkManager.MockNetworkSettings = interfaces;
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); var startupConf = new Mock<IConfiguration>();
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
NetworkManager.MockNetworkSettings = string.Empty; NetworkManager.MockNetworkSettings = string.Empty;
Assert.Equal(value, "[" + string.Join(",", nm.GetInternalBindAddresses().Select(x => x.Address + "/" + x.Subnet.PrefixLength)) + "]"); Assert.Equal(value, "[" + string.Join(",", nm.GetInternalBindAddresses().Select(x => x.Address + "/" + x.Subnet.PrefixLength)) + "]");
@ -200,7 +202,8 @@ namespace Jellyfin.Networking.Tests
}; };
NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11"; NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11";
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); var startupConf = new Mock<IConfiguration>();
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
NetworkManager.MockNetworkSettings = string.Empty; NetworkManager.MockNetworkSettings = string.Empty;
// Check to see if DNS resolution is working. If not, skip test. // Check to see if DNS resolution is working. If not, skip test.
@ -229,24 +232,24 @@ namespace Jellyfin.Networking.Tests
[InlineData("192.168.1.1", "192.168.1.0/24", "eth16,eth11", false, "192.168.1.0/24=internal.jellyfin", "internal.jellyfin")] [InlineData("192.168.1.1", "192.168.1.0/24", "eth16,eth11", false, "192.168.1.0/24=internal.jellyfin", "internal.jellyfin")]
// User on external network, we're bound internal and external - so result is override. // User on external network, we're bound internal and external - so result is override.
[InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")] [InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "all=http://helloworld.com", "http://helloworld.com")]
// User on internal network, we're bound internal only, but the address isn't in the LAN - so return the override. // User on internal network, we're bound internal only, but the address isn't in the LAN - so return the override.
[InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://internalButNotDefinedAsLan.com", "http://internalButNotDefinedAsLan.com")] [InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "external=http://internalButNotDefinedAsLan.com", "http://internalButNotDefinedAsLan.com")]
// User on internal network, no binding specified - so result is the 1st internal. // User on internal network, no binding specified - so result is the 1st internal.
[InlineData("192.168.1.1", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")] [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "external=http://helloworld.com", "eth16")]
// User on external network, internal binding only - so assumption is a proxy forward, return external override. // User on external network, internal binding only - so assumption is a proxy forward, return external override.
[InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")] [InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "external=http://helloworld.com", "http://helloworld.com")]
// User on external network, no binding - so result is the 1st external which is overriden. // User on external network, no binding - so result is the 1st external which is overriden.
[InlineData("jellyfin.org", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")] [InlineData("jellyfin.org", "192.168.1.0/24", "", false, "external=http://helloworld.com", "http://helloworld.com")]
// User assumed to be internal, no binding - so result is the 1st internal. // User assumed to be internal, no binding - so result is the 1st matching interface.
[InlineData("", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")] [InlineData("", "192.168.1.0/24", "", false, "all=http://helloworld.com", "eth16")]
// User is internal, no binding - so result is the 1st internal, which is then overridden. // User is internal, no binding - so result is the 1st internal interface, which is then overridden.
[InlineData("192.168.1.1", "192.168.1.0/24", "", false, "eth16=http://helloworld.com", "http://helloworld.com")] [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "eth16=http://helloworld.com", "http://helloworld.com")]
public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result) public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result)
{ {
@ -264,7 +267,8 @@ namespace Jellyfin.Networking.Tests
}; };
NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11"; NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11";
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); var startupConf = new Mock<IConfiguration>();
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
NetworkManager.MockNetworkSettings = string.Empty; NetworkManager.MockNetworkSettings = string.Empty;
if (nm.TryParseInterface(result, out IReadOnlyList<IPData>? resultObj) && resultObj is not null) if (nm.TryParseInterface(result, out IReadOnlyList<IPData>? resultObj) && resultObj is not null)
@ -293,7 +297,9 @@ namespace Jellyfin.Networking.Tests
RemoteIPFilter = addresses.Split(','), RemoteIPFilter = addresses.Split(','),
IsRemoteIPFilterBlacklist = false IsRemoteIPFilterBlacklist = false
}; };
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
var startupConf = new Mock<IConfiguration>();
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied); Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied);
} }
@ -314,7 +320,8 @@ namespace Jellyfin.Networking.Tests
IsRemoteIPFilterBlacklist = true IsRemoteIPFilterBlacklist = true
}; };
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); var startupConf = new Mock<IConfiguration>();
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied); Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied);
} }
@ -334,7 +341,8 @@ namespace Jellyfin.Networking.Tests
}; };
NetworkManager.MockNetworkSettings = interfaces; NetworkManager.MockNetworkSettings = interfaces;
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); var startupConf = new Mock<IConfiguration>();
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
var interfaceToUse = nm.GetBindAddress(string.Empty, out _); var interfaceToUse = nm.GetBindAddress(string.Empty, out _);
@ -358,7 +366,8 @@ namespace Jellyfin.Networking.Tests
}; };
NetworkManager.MockNetworkSettings = interfaces; NetworkManager.MockNetworkSettings = interfaces;
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); var startupConf = new Mock<IConfiguration>();
using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
var interfaceToUse = nm.GetBindAddress(source, out _); var interfaceToUse = nm.GetBindAddress(source, out _);

@ -7,6 +7,7 @@ using Jellyfin.Server.Extensions;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Moq; using Moq;
using Xunit; using Xunit;
@ -119,8 +120,8 @@ namespace Jellyfin.Server.Tests
EnableIPv6 = true, EnableIPv6 = true,
EnableIPv4 = true, EnableIPv4 = true,
}; };
var startupConf = new Mock<IConfiguration>();
return new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); return new NetworkManager(GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>());
} }
} }
} }

Loading…
Cancel
Save