Update based on PR1 changes.

This commit is contained in:
Jim Cartlidge
2020-09-14 15:46:38 +01:00
parent 288d89493e
commit b44455ad0d
31 changed files with 388 additions and 263 deletions

View File

@@ -7,6 +7,7 @@ using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Model.Configuration;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -19,8 +20,6 @@ namespace Jellyfin.Networking.Manager
/// </summary>
public class NetworkManager : INetworkManager, IDisposable
{
private static NetworkManager? _instance;
/// <summary>
/// Contains the description of the interface along with its index.
/// </summary>
@@ -48,7 +47,7 @@ namespace Jellyfin.Networking.Manager
/// <summary>
/// Holds the bind address overrides.
/// </summary>
private readonly Dictionary<IPNetAddress, string> _overrideUrls;
private readonly Dictionary<IPNetAddress, string> _publishedServerUrls;
/// <summary>
/// Used to stop "event-racing conditions".
@@ -105,7 +104,7 @@ namespace Jellyfin.Networking.Manager
_interfaceAddresses = new NetCollection(unique: false);
_macAddresses = new List<PhysicalAddress>();
_interfaceNames = new SortedList<string, int>();
_overrideUrls = new Dictionary<IPNetAddress, string>();
_publishedServerUrls = new Dictionary<IPNetAddress, string>();
UpdateSettings((ServerConfiguration)_configurationManager.CommonConfiguration);
if (!IsIP6Enabled && !IsIP4Enabled)
@@ -117,8 +116,6 @@ namespace Jellyfin.Networking.Manager
NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
Instance = this;
}
#pragma warning restore CS8618 // Non-nullable field is uninitialized.
@@ -127,19 +124,6 @@ namespace Jellyfin.Networking.Manager
/// </summary>
public event EventHandler? NetworkChanged;
/// <summary>
/// Gets the singleton of this object.
/// </summary>
public static NetworkManager Instance
{
get => GetInstance();
internal set
{
_instance = value;
}
}
/// <summary>
/// Gets the unique network location signature, which is updated on every network change.
/// </summary>
@@ -176,7 +160,7 @@ namespace Jellyfin.Networking.Manager
/// <summary>
/// Gets the Published server override list.
/// </summary>
public Dictionary<IPNetAddress, string> PublishedServerOverrides => _overrideUrls;
public Dictionary<IPNetAddress, string> PublishedServerUrls => _publishedServerUrls;
/// <inheritdoc/>
public void Dispose()
@@ -198,9 +182,12 @@ namespace Jellyfin.Networking.Manager
/// <inheritdoc/>
public bool IsGatewayInterface(object? addressObj)
{
var address = (addressObj is IPAddress addressIP) ?
addressIP : (addressObj is IPObject addressIPObj) ?
addressIPObj.Address : IPAddress.None;
var address = addressObj switch
{
IPAddress addressIp => addressIp,
IPObject addressIpObj => addressIpObj.Address,
_ => IPAddress.None
};
lock (_intLock)
{
@@ -320,243 +307,127 @@ namespace Jellyfin.Networking.Manager
}
/// <inheritdoc/>
public string GetBindInterface(object? source, out int? port)
public string GetBindInterface(string source, out int? port)
{
bool chromeCast = false;
port = null;
// Parse the source object in an attempt to discover where the request originated.
IPObject sourceAddr;
if (source is HttpRequest sourceReq)
if (!string.IsNullOrEmpty(source))
{
port = sourceReq.Host.Port;
if (IPHost.TryParse(sourceReq.Host.Host, out IPHost host))
if (string.Equals(source, "chromecast", StringComparison.OrdinalIgnoreCase))
{
sourceAddr = host;
}
else
{
// Assume it's external, as we cannot resolve the host.
sourceAddr = IPHost.None;
}
}
else if (source is string sourceStr && !string.IsNullOrEmpty(sourceStr))
{
if (string.Equals(sourceStr, "chromecast", StringComparison.OrdinalIgnoreCase))
{
chromeCast = true;
// Just assign a variable so has source = true;
sourceAddr = IPNetAddress.IP4Loopback;
return GetBindInterface(IPNetAddress.IP4Loopback, out port);
}
if (IPHost.TryParse(sourceStr, out IPHost host))
if (IPHost.TryParse(source, out IPHost host))
{
sourceAddr = host;
}
else
{
// Assume it's external, as we cannot resolve the host.
sourceAddr = IPHost.None;
return GetBindInterface(host, out port);
}
}
else if (source is IPAddress sourceIP)
return GetBindInterface(IPHost.None, out port);
}
/// <inheritdoc/>
public string GetBindInterface(IPAddress source, out int? port)
{
return GetBindInterface(new IPNetAddress(source), out port);
}
/// <inheritdoc/>
public string GetBindInterface(HttpRequest source, out int? port)
{
string result;
if (source != null && IPHost.TryParse(source.Host.Host, out IPHost host))
{
sourceAddr = new IPNetAddress(sourceIP);
result = GetBindInterface(host, out port);
port ??= source.Host.Port;
}
else
{
// If we have no idea, then assume it came from an external address.
sourceAddr = IPHost.None;
result = GetBindInterface(IPNetAddress.None, out port);
port ??= source?.Host.Port;
}
return result;
}
/// <inheritdoc/>
public string GetBindInterface(IPObject source, out int? port)
{
port = null;
bool isChromeCast = source == IPNetAddress.IP4Loopback;
// Do we have a source?
bool haveSource = !sourceAddr.Address.Equals(IPAddress.None);
bool haveSource = !source.Address.Equals(IPAddress.None);
bool isExternal = false;
if (haveSource)
{
if (!IsIP6Enabled && sourceAddr.AddressFamily == AddressFamily.InterNetworkV6)
if (!IsIP6Enabled && source.AddressFamily == AddressFamily.InterNetworkV6)
{
_logger.LogWarning("IPv6 is disabled in JellyFin, but enabled in the OS. This may affect how the interface is selected.");
}
if (!IsIP4Enabled && sourceAddr.AddressFamily == AddressFamily.InterNetwork)
if (!IsIP4Enabled && source.AddressFamily == AddressFamily.InterNetwork)
{
_logger.LogWarning("IPv4 is disabled in JellyFin, but enabled in the OS. This may affect how the interface is selected.");
}
}
bool isExternal = haveSource && !IsInLocalNetwork(sourceAddr);
isExternal = !IsInLocalNetwork(source);
string bindPreference = string.Empty;
if (haveSource)
{
// Check for user override.
foreach (var addr in _overrideUrls)
if (MatchesPublishedServerUrl(source, isExternal, isChromeCast, out string result, out port))
{
// Remaining. Match anything.
if (addr.Key.Equals(IPAddress.Broadcast))
{
bindPreference = addr.Value;
break;
}
else if ((addr.Key.Equals(IPAddress.Any) || addr.Key.Equals(IPAddress.IPv6Any)) && (isExternal || chromeCast))
{
// External.
bindPreference = addr.Value;
break;
}
else if (addr.Key.Contains(sourceAddr))
{
// Match ip address.
bindPreference = addr.Value;
break;
}
_logger.LogInformation("{0}: Using BindAddress {1}:{2}", source, result, port);
return result;
}
}
_logger.LogDebug("GetBindInterface: Souce: {0}, External: {1}:", haveSource, isExternal);
if (!string.IsNullOrEmpty(bindPreference))
{
// Has it got a port defined?
var parts = bindPreference.Split(':');
if (parts.Length > 1)
{
if (int.TryParse(parts[1], out int p))
{
bindPreference = parts[0];
port = p;
}
}
_logger.LogInformation("{0}: Using BindAddress {1}:{2}", sourceAddr, bindPreference, port);
return bindPreference;
}
string ipresult;
// No preference given, so move on to bind addresses.
lock (_intLock)
{
var nc = _bindAddresses.Exclude(_bindExclusions).Where(p => !p.IsLoopback());
int count = nc.Count();
if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses.Equals(IPAddress.IPv6Any)))
if (MatchesBindInterface(source, isExternal, out string result))
{
// Ignore IPAny addresses.
count = 0;
return result;
}
if (count != 0)
if (isExternal && MatchesExternalInterface(source, out result))
{
// Check to see if any of the bind interfaces are in the same subnet.
IEnumerable<IPObject> bindResult;
IPAddress? defaultGateway = null;
if (isExternal)
{
// Find all external bind addresses. Store the default gateway, but check to see if there is a better match first.
bindResult = nc.Where(p => !IsInLocalNetwork(p)).OrderBy(p => p.Tag);
defaultGateway = bindResult.FirstOrDefault()?.Address;
bindResult = bindResult.Where(p => p.Contains(sourceAddr)).OrderBy(p => p.Tag);
}
else
{
// Look for the best internal address.
bindResult = nc.Where(p => IsInLocalNetwork(p) && p.Contains(sourceAddr)).OrderBy(p => p.Tag);
}
if (bindResult.Any())
{
ipresult = FormatIP6String(bindResult.First().Address);
_logger.LogDebug("{0}: GetBindInterface: Has source, found a match bind interface subnets. {1}", sourceAddr, ipresult);
return ipresult;
}
if (isExternal && defaultGateway != null)
{
ipresult = FormatIP6String(defaultGateway);
_logger.LogDebug("{0}: GetBindInterface: Using first user defined external interface. {1}", sourceAddr, ipresult);
return ipresult;
}
ipresult = FormatIP6String(nc.First().Address);
_logger.LogDebug("{0}: GetBindInterface: Selected first user defined interface. {1}", sourceAddr, ipresult);
if (isExternal)
{
// TODO: remove this after testing.
_logger.LogWarning("{0}: External request received, however, only an internal interface bind found.", sourceAddr);
}
return ipresult;
}
if (isExternal)
{
// Get the first WAN interface address that isn't a loopback.
var extResult = _interfaceAddresses
.Exclude(_bindExclusions)
.Where(p => !IsInLocalNetwork(p))
.OrderBy(p => p.Tag);
if (extResult.Any())
{
// Does the request originate in one of the interface subnets?
// (For systems with multiple internal network cards, and multiple subnets)
foreach (var intf in extResult)
{
if (!IsInLocalNetwork(intf) && intf.Contains(sourceAddr))
{
ipresult = FormatIP6String(intf.Address);
_logger.LogDebug("{0}: GetBindInterface: Selected best external on interface on range. {1}", sourceAddr, ipresult);
return ipresult;
}
}
ipresult = FormatIP6String(extResult.First().Address);
_logger.LogDebug("{0}: GetBindInterface: Selected first external interface. {0}", sourceAddr, ipresult);
return ipresult;
}
// Have to return something, so return an internal address
// TODO: remove this after testing.
_logger.LogWarning("{0}: External request received, however, no WAN interface found.", sourceAddr);
return result;
}
// Get the first LAN interface address that isn't a loopback.
var result = _interfaceAddresses
var interfaces = new NetCollection(_interfaceAddresses
.Exclude(_bindExclusions)
.Where(p => IsInLocalNetwork(p))
.OrderBy(p => p.Tag);
.OrderBy(p => p.Tag));
if (result.Any())
if (interfaces.Count > 0)
{
if (haveSource)
{
// Does the request originate in one of the interface subnets?
// (For systems with multiple internal network cards, and multiple subnets)
foreach (var intf in result)
foreach (var intf in interfaces)
{
if (intf.Contains(sourceAddr))
if (intf.Contains(source))
{
ipresult = FormatIP6String(intf.Address);
_logger.LogDebug("{0}: GetBindInterface: Has source, matched best internal interface on range. {1}", sourceAddr, ipresult);
return ipresult;
result = FormatIP6String(intf.Address);
_logger.LogDebug("{0}: GetBindInterface: Has source, matched best internal interface on range. {1}", source, result);
return result;
}
}
}
ipresult = FormatIP6String(result.First().Address);
_logger.LogDebug("{0}: GetBindInterface: Matched first internal interface. {1}", sourceAddr, ipresult);
return ipresult;
result = FormatIP6String(interfaces.First().Address);
_logger.LogDebug("{0}: GetBindInterface: Matched first internal interface. {1}", source, result);
return result;
}
// There isn't any others, so we'll use the loopback.
ipresult = IsIP6Enabled ? "::" : "127.0.0.1";
_logger.LogWarning("{0}: GetBindInterface: Loopback return.", sourceAddr, ipresult);
return ipresult;
result = IsIP6Enabled ? "::" : "127.0.0.1";
_logger.LogWarning("{0}: GetBindInterface: Loopback return.", source, result);
return result;
}
}
@@ -771,16 +642,6 @@ namespace Jellyfin.Networking.Manager
}
}
private static NetworkManager GetInstance()
{
if (_instance == null)
{
throw new ApplicationException("NetworkManager is not initialised.");
}
return _instance;
}
private void ConfigurationUpdated(object? sender, EventArgs args)
{
UpdateSettings((ServerConfiguration)_configurationManager.CommonConfiguration);
@@ -944,7 +805,7 @@ namespace Jellyfin.Networking.Manager
{
lock (_intLock)
{
_overrideUrls.Clear();
_publishedServerUrls.Clear();
}
return;
@@ -952,7 +813,7 @@ namespace Jellyfin.Networking.Manager
lock (_intLock)
{
_overrideUrls.Clear();
_publishedServerUrls.Clear();
foreach (var entry in overrides)
{
@@ -966,15 +827,15 @@ namespace Jellyfin.Networking.Manager
var replacement = parts[1].Trim();
if (string.Equals(parts[0], "remaining", StringComparison.OrdinalIgnoreCase))
{
_overrideUrls[new IPNetAddress(IPAddress.Broadcast)] = replacement;
_publishedServerUrls[new IPNetAddress(IPAddress.Broadcast)] = replacement;
}
else if (string.Equals(parts[0], "external", StringComparison.OrdinalIgnoreCase))
{
_overrideUrls[new IPNetAddress(IPAddress.Any)] = replacement;
_publishedServerUrls[new IPNetAddress(IPAddress.Any)] = replacement;
}
else if (TryParseInterface(parts[0], out IPNetAddress address))
{
_overrideUrls[address] = replacement;
_publishedServerUrls[address] = replacement;
}
else
{
@@ -1199,5 +1060,179 @@ namespace Jellyfin.Networking.Manager
}
}
}
/// <summary>
/// Attempts to match the source against a user defined bind interface.
/// </summary>
/// <param name="source">IP source address to use.</param>
/// <param name="isExternal">True if the source is in the external subnet.</param>
/// <param name="isChromeCast">True if the request is for a chromecast device.</param>
/// <param name="bindPreference">The published server url that matches the source address.</param>
/// <param name="port">The resultant port, if one exists.</param>
/// <returns>True if a match is found.</returns>
private bool MatchesPublishedServerUrl(IPObject source, bool isExternal, bool isChromeCast, out string bindPreference, out int? port)
{
bindPreference = string.Empty;
port = null;
// Check for user override.
foreach (var addr in _publishedServerUrls)
{
// Remaining. Match anything.
if (addr.Key.Equals(IPAddress.Broadcast))
{
bindPreference = addr.Value;
break;
}
else if ((addr.Key.Equals(IPAddress.Any) || addr.Key.Equals(IPAddress.IPv6Any)) && (isExternal || isChromeCast))
{
// External.
bindPreference = addr.Value;
break;
}
else if (addr.Key.Contains(source))
{
// Match ip address.
bindPreference = addr.Value;
break;
}
}
if (!string.IsNullOrEmpty(bindPreference))
{
// Has it got a port defined?
var parts = bindPreference.Split(':');
if (parts.Length > 1)
{
if (int.TryParse(parts[1], out int p))
{
bindPreference = parts[0];
port = p;
}
}
return true;
}
return false;
}
/// <summary>
/// Attempts to match the source against a user defined bind interface.
/// </summary>
/// <param name="source">IP source address to use.</param>
/// <param name="isExternal">True if the source is in the external subnet.</param>
/// <param name="result">The result, if a match is found.</param>
/// <returns>True if a match is found.</returns>
private bool MatchesBindInterface(IPObject source, bool isExternal, out string result)
{
result = string.Empty;
var nc = new NetCollection(_bindAddresses.Exclude(_bindExclusions).Where(p => !p.IsLoopback()));
int count = nc.Count;
if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses[0].Equals(IPAddress.IPv6Any)))
{
// Ignore IPAny addresses.
count = 0;
}
if (count != 0)
{
// Check to see if any of the bind interfaces are in the same subnet.
NetCollection bindResult;
IPAddress? defaultGateway = null;
IPAddress? bindAddress;
if (isExternal)
{
// Find all external bind addresses. Store the default gateway, but check to see if there is a better match first.
bindResult = new NetCollection(nc
.Where(p => !IsInLocalNetwork(p))
.OrderBy(p => p.Tag));
defaultGateway = bindResult.FirstOrDefault()?.Address;
bindAddress = bindResult
.Where(p => p.Contains(source))
.OrderBy(p => p.Tag)
.FirstOrDefault()?.Address;
}
else
{
// Look for the best internal address.
bindAddress = nc
.Where(p => IsInLocalNetwork(p) && (p.Contains(source) || p.Equals(IPAddress.None)))
.OrderBy(p => p.Tag)
.FirstOrDefault()?.Address;
}
if (bindAddress != null)
{
result = FormatIP6String(bindAddress);
_logger.LogDebug("{0}: GetBindInterface: Has source, found a match bind interface subnets. {1}", source, result);
return true;
}
if (isExternal && defaultGateway != null)
{
result = FormatIP6String(defaultGateway);
_logger.LogDebug("{0}: GetBindInterface: Using first user defined external interface. {1}", source, result);
return true;
}
result = FormatIP6String(nc.First().Address);
_logger.LogDebug("{0}: GetBindInterface: Selected first user defined interface. {1}", source, result);
if (isExternal)
{
// TODO: remove this after testing.
_logger.LogWarning("{0}: External request received, however, only an internal interface bind found.", source);
}
return true;
}
return false;
}
/// <summary>
/// Attempts to match the source against am external interface.
/// </summary>
/// <param name="source">IP source address to use.</param>
/// <param name="result">The result, if a match is found.</param>
/// <returns>True if a match is found.</returns>
private bool MatchesExternalInterface(IPObject source, out string result)
{
result = string.Empty;
// Get the first WAN interface address that isn't a loopback.
var extResult = new NetCollection(_interfaceAddresses
.Exclude(_bindExclusions)
.Where(p => !IsInLocalNetwork(p))
.OrderBy(p => p.Tag));
if (extResult.Count > 0)
{
// Does the request originate in one of the interface subnets?
// (For systems with multiple internal network cards, and multiple subnets)
foreach (var intf in extResult)
{
if (!IsInLocalNetwork(intf) && intf.Contains(source))
{
result = FormatIP6String(intf.Address);
_logger.LogDebug("{0}: GetBindInterface: Selected best external on interface on range. {1}", source, result);
return true;
}
}
result = FormatIP6String(extResult.First().Address);
_logger.LogDebug("{0}: GetBindInterface: Selected first external interface. {0}", source, result);
return true;
}
// Have to return something, so return an internal address
// TODO: remove this after testing.
_logger.LogWarning("{0}: External request received, however, no WAN interface found.", source);
return false;
}
}
}