using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading;
using J2N.Collections.Generic.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager;
namespace Jellyfin.Networking.Manager;
///
/// Class to take care of network interface management.
///
public class NetworkManager : INetworkManager, IDisposable
{
///
/// Threading lock for network properties.
///
private readonly Lock _initLock;
private readonly ILogger _logger;
private readonly IConfigurationManager _configurationManager;
private readonly IConfiguration _startupConfig;
private readonly Lock _networkEventLock;
///
/// Holds the published server URLs and the IPs to use them on.
///
private IReadOnlyList _publishedServerUrls;
private IReadOnlyList _remoteAddressFilter;
///
/// Used to stop "event-racing conditions".
///
private bool _eventfire;
///
/// Dictionary containing interface addresses and their subnets.
///
private List _interfaces;
///
/// Unfiltered user defined LAN subnets ()
/// or internal interface network subnets if undefined by user.
///
private IReadOnlyList _lanSubnets;
///
/// User defined list of subnets to excluded from the LAN.
///
private IReadOnlyList _excludedSubnets;
///
/// True if this object is disposed.
///
private bool _disposed;
///
/// Initializes a new instance of the class.
///
/// The instance.
/// The instance holding startup parameters.
/// Logger to use for messages.
public NetworkManager(IConfigurationManager configurationManager, IConfiguration startupConfig, ILogger logger)
{
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(configurationManager);
_logger = logger;
_configurationManager = configurationManager;
_startupConfig = startupConfig;
_initLock = new();
_interfaces = new List();
_publishedServerUrls = new List();
_networkEventLock = new();
_remoteAddressFilter = new List();
_ = bool.TryParse(startupConfig[DetectNetworkChangeKey], out var detectNetworkChange);
UpdateSettings(_configurationManager.GetNetworkConfiguration());
if (detectNetworkChange)
{
NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged;
NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
}
_configurationManager.NamedConfigurationUpdated += ConfigurationUpdated;
}
///
/// Event triggered on network changes.
///
public event EventHandler? NetworkChanged;
///
/// Gets or sets a value indicating whether testing is taking place.
///
public static string MockNetworkSettings { get; set; } = string.Empty;
///
/// Gets a value indicating whether IPv4 is enabled.
///
public bool IsIPv4Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv4;
///
/// Gets a value indicating whether IP6 is enabled.
///
public bool IsIPv6Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv6;
///
/// Gets a value indicating whether is all IPv6 interfaces are trusted as internal.
///
public bool TrustAllIPv6Interfaces { get; private set; }
///
/// Gets the Published server override list.
///
public IReadOnlyList PublishedServerUrls => _publishedServerUrls;
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Handler for network change events.
///
/// Sender.
/// A containing network availability information.
private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e)
{
_logger.LogDebug("Network availability changed.");
HandleNetworkChange();
}
///
/// Handler for network change events.
///
/// Sender.
/// An .
private void OnNetworkAddressChanged(object? sender, EventArgs e)
{
_logger.LogDebug("Network address change detected.");
HandleNetworkChange();
}
///
/// Triggers our event, and re-loads interface information.
///
private void HandleNetworkChange()
{
lock (_networkEventLock)
{
if (!_eventfire)
{
// As network events tend to fire one after the other only fire once every second.
_eventfire = true;
OnNetworkChange();
}
}
}
///
/// Waits for 2 seconds before re-initialising the settings, as typically these events fire multiple times in succession.
///
private void OnNetworkChange()
{
try
{
Thread.Sleep(2000);
var networkConfig = _configurationManager.GetNetworkConfiguration();
if (IsIPv6Enabled && !Socket.OSSupportsIPv6)
{
UpdateSettings(networkConfig);
}
else
{
InitializeInterfaces();
InitializeLan(networkConfig);
EnforceBindSettings(networkConfig);
}
PrintNetworkInformation(networkConfig);
NetworkChanged?.Invoke(this, EventArgs.Empty);
}
finally
{
_eventfire = false;
}
}
///
/// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state.
///
private void InitializeInterfaces()
{
lock (_initLock)
{
_interfaces = GetInterfacesCore(_logger, IsIPv4Enabled, IsIPv6Enabled).ToList();
}
}
///
/// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state.
///
/// The logger.
/// If true evaluates IPV4 type ip addresses.
/// If true evaluates IPV6 type ip addresses.
/// A list of all locally known up addresses and submasks that are to be considered usable.
public static IReadOnlyList GetInterfacesCore(ILogger logger, bool isIPv4Enabled, bool isIPv6Enabled)
{
logger.LogDebug("Refreshing interfaces.");
var interfaces = new List();
try
{
var nics = NetworkInterface.GetAllNetworkInterfaces()
.Where(i => i.OperationalStatus == OperationalStatus.Up);
foreach (NetworkInterface adapter in nics)
{
try
{
var ipProperties = adapter.GetIPProperties();
// Populate interface list
foreach (var info in ipProperties.UnicastAddresses)
{
if (isIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork)
{
var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
{
Index = ipProperties.GetIPv4Properties().Index,
Name = adapter.Name,
SupportsMulticast = adapter.SupportsMulticast
};
interfaces.Add(interfaceObject);
}
else if (isIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6)
{
var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
{
Index = ipProperties.GetIPv6Properties().Index,
Name = adapter.Name,
SupportsMulticast = adapter.SupportsMulticast
};
interfaces.Add(interfaceObject);
}
}
}
catch (Exception ex)
{
// Ignore error, and attempt to continue.
logger.LogError(ex, "Error encountered parsing interfaces.");
}
}
}
catch (Exception ex)
{
logger.LogError(ex, "Error obtaining interfaces.");
}
// If no interfaces are found, fallback to loopback interfaces.
if (interfaces.Count == 0)
{
logger.LogWarning("No interface information available. Using loopback interface(s).");
if (isIPv4Enabled)
{
interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo"));
}
if (isIPv6Enabled)
{
interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo"));
}
}
logger.LogDebug("Discovered {NumberOfInterfaces} interfaces.", interfaces.Count);
logger.LogDebug("Interfaces addresses: {Addresses}", interfaces.OrderByDescending(s => s.AddressFamily == AddressFamily.InterNetwork).Select(s => s.Address.ToString()));
return interfaces;
}
///
/// Initializes internal LAN cache.
///
[MemberNotNull(nameof(_lanSubnets), nameof(_excludedSubnets))]
private void InitializeLan(NetworkConfiguration config)
{
lock (_initLock)
{
_logger.LogDebug("Refreshing LAN information.");
// Get configuration options
var subnets = config.LocalNetworkSubnets;
// If no LAN addresses are specified, all private subnets and Loopback are deemed to be the LAN
if (!NetworkUtils.TryParseToSubnets(subnets, out var lanSubnets, false, _logger) || lanSubnets.Count == 0)
{
_logger.LogDebug("Using LAN interface addresses as user provided no LAN details.");
var fallbackLanSubnets = new List();
if (IsIPv6Enabled)
{
fallbackLanSubnets.Add(NetworkConstants.IPv6RFC4291Loopback); // RFC 4291 (Loopback)
fallbackLanSubnets.Add(NetworkConstants.IPv6RFC4291SiteLocal); // RFC 4291 (Site local)
fallbackLanSubnets.Add(NetworkConstants.IPv6RFC4193UniqueLocal); // RFC 4193 (Unique local)
}
if (IsIPv4Enabled)
{
fallbackLanSubnets.Add(NetworkConstants.IPv4RFC5735Loopback); // RFC 5735 (Loopback)
fallbackLanSubnets.Add(NetworkConstants.IPv4RFC1918PrivateClassA); // RFC 1918 (private Class A)
fallbackLanSubnets.Add(NetworkConstants.IPv4RFC1918PrivateClassB); // RFC 1918 (private Class B)
fallbackLanSubnets.Add(NetworkConstants.IPv4RFC1918PrivateClassC); // RFC 1918 (private Class C)
}
_lanSubnets = fallbackLanSubnets;
}
else
{
_lanSubnets = lanSubnets.Select(x => x.Subnet).ToArray();
}
_excludedSubnets = NetworkUtils.TryParseToSubnets(subnets, out var excludedSubnets, true, _logger)
? excludedSubnets.Select(x => x.Subnet).ToArray()
: Array.Empty();
}
}
///
/// Enforce bind addresses and exclusions on available interfaces.
///
private void EnforceBindSettings(NetworkConfiguration config)
{
lock (_initLock)
{
_interfaces = FilterBindSettings(config, _interfaces, IsIPv4Enabled, IsIPv6Enabled).ToList();
}
}
///
/// Filters a list of bind addresses and exclusions on available interfaces.
///
/// The network config to be filtered by.
/// A list of possible interfaces to be filtered.
/// If true evaluates IPV4 type ip addresses.
/// If true evaluates IPV6 type ip addresses.
/// A list of all locally known up addresses and submasks that are to be considered usable.
public static IReadOnlyList FilterBindSettings(NetworkConfiguration config, IList interfaces, bool isIPv4Enabled, bool isIPv6Enabled)
{
// Respect explicit bind addresses
var localNetworkAddresses = config.LocalNetworkAddresses;
if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0]))
{
var bindAddresses = localNetworkAddresses.Select(p => NetworkUtils.TryParseToSubnet(p, out var network)
? network.Address
: (interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase))
.Select(x => x.Address)
.FirstOrDefault() ?? IPAddress.None))
.Where(x => x != IPAddress.None)
.ToHashSet();
interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList();
if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback)))
{
interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo"));
}
if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback)))
{
interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo"));
}
}
// Remove all interfaces matching any virtual machine interface prefix
if (config.IgnoreVirtualInterfaces)
{
// Remove potentially existing * and split config string into prefixes
var virtualInterfacePrefixes = config.VirtualInterfaceNames
.Select(i => i.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase));
// Check all interfaces for matches against the prefixes and remove them
if (interfaces.Count > 0)
{
foreach (var virtualInterfacePrefix in virtualInterfacePrefixes)
{
interfaces.RemoveAll(x => x.Name.StartsWith(virtualInterfacePrefix, StringComparison.OrdinalIgnoreCase));
}
}
}
// Remove all IPv4 interfaces if IPv4 is disabled
if (!isIPv4Enabled)
{
interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetwork);
}
// Remove all IPv6 interfaces if IPv6 is disabled
if (!isIPv6Enabled)
{
interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6);
}
// Users may have complex networking configuration that multiple interfaces sharing the same IP address
// Only return one IP for binding, and let the OS handle the rest
return interfaces.DistinctBy(iface => iface.Address).ToList();
}
///
/// Initializes the remote address values.
///
private void InitializeRemote(NetworkConfiguration config)
{
lock (_initLock)
{
// Parse config values into filter collection
var remoteIPFilter = config.RemoteIPFilter;
if (remoteIPFilter.Length != 0 && !string.IsNullOrWhiteSpace(remoteIPFilter[0]))
{
// Parse all IPs with netmask to a subnet
var remoteAddressFilter = new List();
var remoteFilteredSubnets = remoteIPFilter.Where(x => x.Contains('/', StringComparison.OrdinalIgnoreCase)).ToArray();
if (NetworkUtils.TryParseToSubnets(remoteFilteredSubnets, out var remoteAddressFilterResult, false))
{
remoteAddressFilter = remoteAddressFilterResult.Select(x => x.Subnet).ToList();
}
// Parse everything else as an IP and construct subnet with a single IP
var remoteFilteredIPs = remoteIPFilter.Where(x => !x.Contains('/', StringComparison.OrdinalIgnoreCase));
foreach (var ip in remoteFilteredIPs)
{
if (IPAddress.TryParse(ip, out var ipp))
{
remoteAddressFilter.Add(new IPNetwork(ipp, ipp.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize));
}
}
_remoteAddressFilter = remoteAddressFilter;
}
}
}
///
/// Parses the user defined overrides into the dictionary object.
/// Overrides are the equivalent of localised publishedServerUrl, enabling
/// different addresses to be advertised over different subnets.
/// format is subnet=ipaddress|host|uri
/// when subnet = 0.0.0.0, any external address matches.
///
private void InitializeOverrides(NetworkConfiguration config)
{
lock (_initLock)
{
var publishedServerUrls = new List();
// Prefer startup configuration.
var startupOverrideKey = _startupConfig[AddressOverrideKey];
if (!string.IsNullOrEmpty(startupOverrideKey))
{
publishedServerUrls.Add(
new PublishedServerUriOverride(
new IPData(IPAddress.Any, NetworkConstants.IPv4Any),
startupOverrideKey,
true,
true));
publishedServerUrls.Add(
new PublishedServerUriOverride(
new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any),
startupOverrideKey,
true,
true));
WarnIfPublishedUrlBasePathDiffers(publishedServerUrls, config.BaseUrl);
_publishedServerUrls = publishedServerUrls;
return;
}
var overrides = config.PublishedServerUriBySubnet;
foreach (var entry in overrides)
{
var parts = entry.Split('=');
if (parts.Length != 2)
{
_logger.LogError("Unable to parse bind override: {Entry}", entry);
return;
}
var replacement = parts[1].Trim();
var identifier = parts[0];
if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase))
{
// Drop any other overrides in case an "all" override exists
publishedServerUrls.Clear();
publishedServerUrls.Add(
new PublishedServerUriOverride(
new IPData(IPAddress.Any, NetworkConstants.IPv4Any),
replacement,
true,
true));
publishedServerUrls.Add(
new PublishedServerUriOverride(
new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any),
replacement,
true,
true));
break;
}
else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase))
{
publishedServerUrls.Add(
new PublishedServerUriOverride(
new IPData(IPAddress.Any, NetworkConstants.IPv4Any),
replacement,
false,
true));
publishedServerUrls.Add(
new PublishedServerUriOverride(
new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any),
replacement,
false,
true));
}
else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase))
{
foreach (var lan in _lanSubnets)
{
var lanPrefix = lan.BaseAddress;
publishedServerUrls.Add(
new PublishedServerUriOverride(
new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)),
replacement,
true,
false));
}
}
else if (NetworkUtils.TryParseToSubnet(identifier, out var result))
{
publishedServerUrls.Add(
new PublishedServerUriOverride(
result,
replacement,
true,
true));
}
else if (TryParseInterface(identifier, out var ifaces))
{
foreach (var iface in ifaces)
{
publishedServerUrls.Add(
new PublishedServerUriOverride(
iface,
replacement,
true,
true));
}
}
else
{
_logger.LogError("Unable to parse bind override: {Entry}", entry);
}
}
WarnIfPublishedUrlBasePathDiffers(publishedServerUrls, config.BaseUrl);
_publishedServerUrls = publishedServerUrls;
}
}
///
/// Warns when a full-URL published server override uses a public path that differs from the configured base
/// URL. Jellyfin appends the base URL to generated Live TV client URLs in this case, which can conflict with
/// reverse proxies that translate public request paths. Bare host/IP overrides are exempt because the base URL
/// is appended when the API URL is built from them.
///
/// The parsed published server URL overrides.
/// The configured base URL, if any.
private void WarnIfPublishedUrlBasePathDiffers(List publishedServerUrls, string baseUrl)
{
if (string.IsNullOrEmpty(baseUrl))
{
return;
}
foreach (var overrideUri in publishedServerUrls.Select(x => x.OverrideUri).Distinct(StringComparer.OrdinalIgnoreCase))
{
if (!overrideUri.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
&& !overrideUri.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (!Uri.TryCreate(overrideUri, UriKind.Absolute, out var uri))
{
continue;
}
var path = Uri.UnescapeDataString(uri.AbsolutePath).TrimEnd('/');
if (path.EndsWith(baseUrl, StringComparison.OrdinalIgnoreCase))
{
continue;
}
var publishedServerHost = uri.GetComponents(UriComponents.HostAndPort, UriFormat.Unescaped);
_logger.LogWarning(
"The published server URL for host '{PublishedServerHost}' does not end with the configured base URL '{BaseUrl}'. Jellyfin will append this base URL when generating Live TV client URLs. If your reverse proxy translates public paths, this may cause Live TV playback to fail. Update the Published Server URIs setting on the Networking page of the admin dashboard, the JELLYFIN_PublishedServerUrl environment variable / --published-server-url option, or the reverse proxy path mapping accordingly.",
publishedServerHost,
baseUrl);
}
}
private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt)
{
if (evt.Key.Equals(NetworkConfigurationStore.StoreKey, StringComparison.Ordinal))
{
UpdateSettings((NetworkConfiguration)evt.NewConfiguration);
}
}
///
/// Reloads all settings and re-Initializes the instance.
///
/// The to use.
[MemberNotNull(nameof(_lanSubnets), nameof(_excludedSubnets))]
public void UpdateSettings(object configuration)
{
ArgumentNullException.ThrowIfNull(configuration);
var config = (NetworkConfiguration)configuration;
HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6;
InitializeLan(config);
InitializeRemote(config);
if (string.IsNullOrEmpty(MockNetworkSettings))
{
InitializeInterfaces();
}
else // Used in testing only.
{
// Format is ,,: . Set index to -ve to simulate a gateway.
var interfaceList = MockNetworkSettings.Split('|');
var interfaces = new List();
foreach (var details in interfaceList)
{
var parts = details.Split(',');
if (NetworkUtils.TryParseToSubnet(parts[0], out var data))
{
data.Index = int.Parse(parts[1], CultureInfo.InvariantCulture);
if (data.AddressFamily == AddressFamily.InterNetwork || data.AddressFamily == AddressFamily.InterNetworkV6)
{
data.Name = parts[2];
interfaces.Add(data);
}
}
else
{
_logger.LogWarning("Could not parse mock interface settings: {Part}", details);
}
}
_interfaces = interfaces;
}
EnforceBindSettings(config);
InitializeOverrides(config);
PrintNetworkInformation(config, false);
}
///
/// Protected implementation of Dispose pattern.
///
/// True to dispose the managed state.
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_configurationManager.NamedConfigurationUpdated -= ConfigurationUpdated;
NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged;
NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged;
}
_disposed = true;
}
}
///
public bool TryParseInterface(string intf, [NotNullWhen(true)] out IReadOnlyList? result)
{
if (string.IsNullOrEmpty(intf)
|| _interfaces is null
|| _interfaces.Count == 0)
{
result = null;
return false;
}
// Match all interfaces starting with names starting with token
result = _interfaces
.Where(i => i.Name.Equals(intf, StringComparison.OrdinalIgnoreCase)
&& ((IsIPv4Enabled && i.Address.AddressFamily == AddressFamily.InterNetwork)
|| (IsIPv6Enabled && i.Address.AddressFamily == AddressFamily.InterNetworkV6)))
.OrderBy(x => x.Index)
.ToArray();
return result.Count > 0;
}
///
public RemoteAccessPolicyResult ShouldAllowServerAccess(IPAddress remoteIP)
{
var config = _configurationManager.GetNetworkConfiguration();
if (IsInLocalNetwork(remoteIP))
{
return RemoteAccessPolicyResult.Allow;
}
if (!config.EnableRemoteAccess)
{
// Remote not enabled. So everyone should be LAN.
return RemoteAccessPolicyResult.RejectDueToRemoteAccessDisabled;
}
if (!_remoteAddressFilter.Any())
{
// No filter on remote addresses, allow any of them.
return RemoteAccessPolicyResult.Allow;
}
// Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
// If left blank, all remote addresses will be allowed.
// remoteAddressFilter is a whitelist or blacklist.
var anyMatches = _remoteAddressFilter.Any(remoteNetwork => NetworkUtils.SubnetContainsAddress(remoteNetwork, remoteIP));
if (config.IsRemoteIPFilterBlacklist)
{
return anyMatches
? RemoteAccessPolicyResult.RejectDueToIPBlocklist
: RemoteAccessPolicyResult.Allow;
}
// Allow-list
return anyMatches
? RemoteAccessPolicyResult.Allow
: RemoteAccessPolicyResult.RejectDueToNotAllowlistedRemoteIP;
}
///
public IReadOnlyList GetLoopbacks()
{
if (!IsIPv4Enabled && !IsIPv6Enabled)
{
return Array.Empty();
}
var loopbackNetworks = new List();
if (IsIPv4Enabled)
{
loopbackNetworks.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo"));
}
if (IsIPv6Enabled)
{
loopbackNetworks.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo"));
}
return loopbackNetworks;
}
///
public IReadOnlyList GetAllBindInterfaces(bool individualInterfaces = false)
{
return NetworkManager.GetAllBindInterfaces(_logger, individualInterfaces, _configurationManager, _interfaces, IsIPv4Enabled, IsIPv6Enabled);
}
///
/// Reads the jellyfin configuration of the configuration manager and produces a list of interfaces that should be bound.
///
/// Logger to use for messages.
/// Defines that only known interfaces should be used.
/// The ConfigurationManager.
/// The known interfaces that gets returned if possible or instructed.
/// Include IPV4 type interfaces.
/// Include IPV6 type interfaces.
/// A list of ip address of which jellyfin should bind to.
public static IReadOnlyList GetAllBindInterfaces(
ILogger logger,
bool individualInterfaces,
IConfigurationManager configurationManager,
IReadOnlyList knownInterfaces,
bool readIpv4,
bool readIpv6)
{
var config = configurationManager.GetNetworkConfiguration();
var localNetworkAddresses = config.LocalNetworkAddresses;
if ((localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0]) && knownInterfaces.Count > 0) || individualInterfaces)
{
return knownInterfaces;
}
// TODO: remove when upgrade to dotnet 11 is done
if (readIpv6 && !Socket.OSSupportsIPv6)
{
logger.LogWarning("IPv6 Unsupported by OS, not listening on IPv6");
readIpv6 = false;
}
// No bind address and no exclusions, so listen on all interfaces.
var result = new List();
if (readIpv4 && readIpv6)
{
// Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default
result.Add(new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any));
}
else if (readIpv4)
{
result.Add(new IPData(IPAddress.Any, NetworkConstants.IPv4Any));
}
else if (readIpv6)
{
// Cannot use IPv6Any as Kestrel will bind to IPv4 addresses too.
foreach (var iface in knownInterfaces)
{
if (iface.AddressFamily == AddressFamily.InterNetworkV6)
{
result.Add(iface);
}
}
}
return result;
}
///
public string GetBindAddress(string source, out int? port)
{
if (!NetworkUtils.TryParseHost(source, out var addresses, IsIPv4Enabled, IsIPv6Enabled))
{
addresses = Array.Empty();
}
var result = GetBindAddress(addresses.FirstOrDefault(), out port);
return result;
}
///
public string GetBindAddress(HttpRequest source, out int? port)
{
var result = GetBindAddress(source.Host.Host, out port);
port ??= source.Host.Port;
return result;
}
///
public string GetBindAddress(IPAddress? source, out int? port, bool skipOverrides = false)
{
port = null;
string result;
if (source is not null)
{
if (IsIPv4Enabled && !IsIPv6Enabled && source.AddressFamily == AddressFamily.InterNetworkV6)
{
_logger.LogWarning("IPv6 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
}
if (!IsIPv4Enabled && IsIPv6Enabled && source.AddressFamily == AddressFamily.InterNetwork)
{
_logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
}
bool isExternal = !IsInLocalNetwork(source);
_logger.LogDebug("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal);
if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result, out port))
{
return result;
}
// No preference given, so move on to bind addresses.
if (MatchesBindInterface(source, isExternal, out result))
{
return result;
}
if (isExternal && MatchesExternalInterface(source, out result))
{
return result;
}
}
// Get the first LAN interface address that's not excluded and not a loopback address.
// Get all available interfaces, prefer local interfaces
var availableInterfaces = _interfaces.Where(x => !IPAddress.IsLoopback(x.Address))
.OrderByDescending(x => IsInLocalNetwork(x.Address))
.ThenBy(x => x.Index)
.ToList();
if (availableInterfaces.Count == 0)
{
// There isn't any others, so we'll use the loopback.
// Prefer loopback address matching the source's address family
if (source is not null && source.AddressFamily == AddressFamily.InterNetwork && IsIPv4Enabled)
{
result = "127.0.0.1";
}
else if (source is not null && source.AddressFamily == AddressFamily.InterNetworkV6 && IsIPv6Enabled)
{
result = "::1";
}
else
{
result = IsIPv4Enabled ? "127.0.0.1" : "::1";
}
_logger.LogWarning("{Source}: Only loopback {Result} returned, using that as bind address.", source, result);
return result;
}
// If no source address is given, use the preferred (first) interface
if (source is null)
{
result = NetworkUtils.FormatIPString(availableInterfaces.First().Address);
_logger.LogDebug("{Source}: Using first internal interface as bind address: {Result}", source, result);
return result;
}
// Does the request originate in one of the interface subnets?
// (For systems with multiple internal network cards, and multiple subnets)
foreach (var intf in availableInterfaces)
{
if (NetworkUtils.SubnetContainsAddress(intf.Subnet, source))
{
result = NetworkUtils.FormatIPString(intf.Address);
_logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result);
return result;
}
}
// Fallback to an interface matching the source's address family, or first available
var preferredInterface = availableInterfaces
.FirstOrDefault(x => x.Address.AddressFamily == source.AddressFamily);
if (preferredInterface is not null)
{
result = NetworkUtils.FormatIPString(preferredInterface.Address);
_logger.LogDebug("{Source}: No matching subnet found, using interface with matching address family: {Result}", source, result);
return result;
}
result = NetworkUtils.FormatIPString(availableInterfaces[0].Address);
_logger.LogDebug("{Source}: No matching interfaces found, using first available interface as bind address: {Result}", source, result);
return result;
}
///
public IReadOnlyList GetInternalBindAddresses()
{
// Select all local bind addresses
return _interfaces.Where(x => IsInLocalNetwork(x.Address))
.OrderBy(x => x.Index)
.ToList();
}
///
public bool IsInLocalNetwork(string address)
{
if (NetworkUtils.TryParseToSubnet(address, out var subnet))
{
return IsInLocalNetwork(subnet.Address);
}
return NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)
&& addresses.Any(IsInLocalNetwork);
}
///
/// Get if the IPAddress is Link-local.
///
/// The IP Address.
/// Bool indicates if the address is link-local.
public bool IsLinkLocalAddress(IPAddress address)
{
ArgumentNullException.ThrowIfNull(address);
return NetworkConstants.IPv4RFC3927LinkLocal.Contains(address) || address.IsIPv6LinkLocal;
}
///
public bool IsInLocalNetwork(IPAddress address)
{
ArgumentNullException.ThrowIfNull(address);
// Map IPv6 mapped IPv4 back to IPv4 (happens if Kestrel runs in dual-socket mode)
if (address.IsIPv4MappedToIPv6)
{
address = address.MapToIPv4();
}
if ((TrustAllIPv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6)
|| IPAddress.IsLoopback(address))
{
return true;
}
// As private addresses can be redefined by Configuration.LocalNetworkAddresses
return CheckIfLanAndNotExcluded(address);
}
///
/// Check if the address is in the LAN and not excluded.
///
/// The IP address to check. The caller should make sure this is not an IPv4MappedToIPv6 address.
/// Boolean indicates whether the address is in LAN.
private bool CheckIfLanAndNotExcluded(IPAddress address)
{
foreach (var lanSubnet in _lanSubnets)
{
if (lanSubnet.Contains(address))
{
foreach (var excludedSubnet in _excludedSubnets)
{
if (excludedSubnet.Contains(address))
{
return false;
}
}
return true;
}
}
return false;
}
///
/// Attempts to match the source against the published server URL overrides.
///
/// IP source address to use.
/// True if the source is in an external subnet.
/// The published server URL that matches the source address.
/// The explicit port parsed from the override, if any.
/// true if a match is found, false otherwise.
private bool MatchesPublishedServerUrl(IPAddress source, bool isInExternalSubnet, out string bindPreference, out int? port)
{
bindPreference = string.Empty;
port = null;
// Only consider subnets including the source IP, preferring specific overrides
List validPublishedServerUrls;
if (!isInExternalSubnet)
{
// Only use matching internal subnets
// Prefer more specific (bigger subnet prefix) overrides
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && NetworkUtils.SubnetContainsAddress(x.Data.Subnet, 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 && NetworkUtils.SubnetContainsAddress(x.Data.Subnet, source))
.OrderByDescending(x => x.Data.Subnet.PrefixLength)
.ToList();
}
foreach (var data in validPublishedServerUrls)
{
// Get interface matching override subnet
var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => NetworkUtils.SubnetContainsAddress(data.Data.Subnet, x.Address));
if (intf?.Address is not null
|| (data.Data.AddressFamily == AddressFamily.InterNetwork && data.Data.Address.Equals(IPAddress.Any))
|| (data.Data.AddressFamily == AddressFamily.InterNetworkV6 && data.Data.Address.Equals(IPAddress.IPv6Any)))
{
// If matching interface is found, use override
bindPreference = data.OverrideUri;
break;
}
}
if (string.IsNullOrEmpty(bindPreference))
{
_logger.LogDebug("{Source}: No matching bind address override found", source);
return false;
}
// Handle override specifying an explicit port.
(bindPreference, port) = ParseHostAndPort(bindPreference);
if (port.HasValue)
{
_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;
}
///
/// Splits a published server URL override into its host and explicit port, if any.
/// Full URLs (containing "://") are returned whole, with any port left embedded.
///
/// The override value, e.g. "host:port", "[::1]:port", or a full URL.
/// The parsed host (or the original value if not split) and the explicit port, if any.
private static (string Host, int? Port) ParseHostAndPort(string value)
{
if (value.Contains("://", StringComparison.Ordinal))
{
return (value, null);
}
if (Uri.TryCreate("any://" + value, UriKind.Absolute, out var parsed) && parsed.Port != -1)
{
return (parsed.DnsSafeHost, parsed.Port);
}
return (value, null);
}
///
/// Attempts to match the source against the user defined bind interfaces.
///
/// IP source address to use.
/// True if the source is in the external subnet.
/// The result, if a match is found.
/// true if a match is found, false otherwise.
private bool MatchesBindInterface(IPAddress source, bool isInExternalSubnet, out string result)
{
result = string.Empty;
int count = _interfaces.Count;
if (count == 1 && (_interfaces[0].Address.Equals(IPAddress.Any) || _interfaces[0].Address.Equals(IPAddress.IPv6Any)))
{
// Ignore IPAny addresses.
count = 0;
}
if (count == 0)
{
return false;
}
IPAddress? bindAddress = null;
if (isInExternalSubnet)
{
var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address))
.Where(x => !IsLinkLocalAddress(x.Address))
.OrderBy(x => x.Index)
.ToList();
if (externalInterfaces.Count > 0)
{
// Check to see if any of the external bind interfaces are in the same subnet as the source.
// If none exists, this will select the first external interface if there is one.
bindAddress = externalInterfaces
.OrderByDescending(x => NetworkUtils.SubnetContainsAddress(x.Subnet, source))
.ThenByDescending(x => x.Subnet.PrefixLength)
.ThenBy(x => x.Index)
.Select(x => x.Address)
.First();
result = NetworkUtils.FormatIPString(bindAddress);
_logger.LogDebug("{Source}: External request received, matching external bind address found: {Result}", source, result);
return true;
}
_logger.LogDebug("{Source}: External request received, no matching external bind address found, trying internal addresses", source);
}
else
{
// Check to see if any of the internal bind interfaces are in the same subnet as the source.
// If none exists, this will select the first internal interface if there is one.
bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address))
.OrderByDescending(x => NetworkUtils.SubnetContainsAddress(x.Subnet, source))
.ThenByDescending(x => x.Subnet.PrefixLength)
.ThenBy(x => x.Index)
.Select(x => x.Address)
.FirstOrDefault();
if (bindAddress is not null)
{
result = NetworkUtils.FormatIPString(bindAddress);
_logger.LogDebug("{Source}: Internal request received, matching internal bind address found: {Result}", source, result);
return true;
}
}
return false;
}
///
/// Attempts to match the source against external interfaces.
///
/// IP source address to use.
/// The result, if a match is found.
/// true if a match is found, false otherwise.
private bool MatchesExternalInterface(IPAddress source, out string result)
{
// Get the first external interface address that isn't a loopback.
var extResult = _interfaces
.Where(p => !IsInLocalNetwork(p.Address))
.Where(p => p.Address.AddressFamily.Equals(source.AddressFamily))
.Where(p => !IsLinkLocalAddress(p.Address))
.OrderBy(x => x.Index).ToArray();
// No external interface found
if (extResult.Length == 0)
{
result = string.Empty;
_logger.LogDebug("{Source}: External request received, but no external interface found. Need to route through internal network", source);
return false;
}
// Does the request originate in one of the interface subnets?
// (For systems with multiple network cards and/or multiple subnets)
foreach (var intf in extResult)
{
if (NetworkUtils.SubnetContainsAddress(intf.Subnet, source))
{
result = NetworkUtils.FormatIPString(intf.Address);
_logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result);
return true;
}
}
// Fallback to first external interface.
result = NetworkUtils.FormatIPString(extResult[0].Address);
_logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result);
return true;
}
private void PrintNetworkInformation(NetworkConfiguration config, bool debug = true)
{
var logLevel = debug ? LogLevel.Debug : LogLevel.Information;
if (_logger.IsEnabled(logLevel))
{
_logger.Log(logLevel, "Defined LAN subnets: {Subnets}", _lanSubnets.Select(s => s.BaseAddress + "/" + s.PrefixLength));
_logger.Log(logLevel, "Defined LAN exclusions: {Subnets}", _excludedSubnets.Select(s => s.BaseAddress + "/" + s.PrefixLength));
_logger.Log(logLevel, "Used LAN subnets: {Subnets}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.BaseAddress + "/" + s.PrefixLength));
_logger.Log(logLevel, "Filtered interface addresses: {Addresses}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
_logger.Log(logLevel, "Bind Addresses {Addresses}", GetAllBindInterfaces(false).OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
_logger.Log(logLevel, "Remote IP filter is {Type}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist");
_logger.Log(logLevel, "Filtered subnets: {Subnets}", _remoteAddressFilter.Select(s => s.BaseAddress + "/" + s.PrefixLength));
}
}
}