using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading.Tasks; using Jellyfin.Networking.Configuration; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace Jellyfin.Networking.Manager { /// /// Class to take care of network interface management. /// Note: The normal collection methods and properties will not work with Collection{IPObject}. . /// public class NetworkManager : INetworkManager, IDisposable { /// /// Contains the description of the interface along with its index. /// private readonly Dictionary _interfaceNames; /// /// Threading lock for network properties. /// private readonly object _intLock = new object(); /// /// List of all interface addresses and masks. /// private readonly Collection _interfaceAddresses; /// /// List of all interface MAC addresses. /// private readonly List _macAddresses; private readonly ILogger _logger; private readonly IConfigurationManager _configurationManager; private readonly object _eventFireLock; /// /// Holds the bind address overrides. /// private readonly Dictionary _publishedServerUrls; /// /// Used to stop "event-racing conditions". /// private bool _eventfire; /// /// Unfiltered user defined LAN subnets. () /// or internal interface network subnets if undefined by user. /// private Collection _lanSubnets; /// /// User defined list of subnets to excluded from the LAN. /// private Collection _excludedSubnets; /// /// List of interface addresses to bind the WS. /// private Collection _bindAddresses; /// /// List of interface addresses to exclude from bind. /// private Collection _bindExclusions; /// /// Caches list of all internal filtered interface addresses and masks. /// private Collection _internalInterfaces; /// /// Flag set when no custom LAN has been defined in the configuration. /// private bool _usingPrivateAddresses; /// /// True if this object is disposed. /// private bool _disposed; /// /// Initializes a new instance of the class. /// /// IServerConfigurationManager instance. /// Logger to use for messages. #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 logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _configurationManager = configurationManager ?? throw new ArgumentNullException(nameof(configurationManager)); _interfaceAddresses = new Collection(); _macAddresses = new List(); _interfaceNames = new Dictionary(); _publishedServerUrls = new Dictionary(); _eventFireLock = new object(); UpdateSettings(_configurationManager.GetNetworkConfiguration()); NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged; NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; _configurationManager.NamedConfigurationUpdated += ConfigurationUpdated; } #pragma warning restore CS8618 // Non-nullable field is uninitialized. /// /// 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 or sets a value indicating whether IP6 is enabled. /// public bool IsIP6Enabled { get; set; } /// /// Gets or sets a value indicating whether IP4 is enabled. /// public bool IsIP4Enabled { get; set; } /// public Collection RemoteAddressFilter { get; private set; } /// /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. /// public bool TrustAllIP6Interfaces { get; internal set; } /// /// Gets the Published server override list. /// public Dictionary PublishedServerUrls => _publishedServerUrls; /// /// Creates a new network collection. /// /// Items to assign the collection, or null. /// The collection created. public static Collection CreateCollection(IEnumerable? source = null) { var result = new Collection(); if (source != null) { foreach (var item in source) { result.AddItem(item, false); } } return result; } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// public IReadOnlyCollection GetMacAddresses() { // Populated in construction - so always has values. return _macAddresses; } /// public bool IsGatewayInterface(IPObject? addressObj) { var address = addressObj?.Address ?? IPAddress.None; return _internalInterfaces.Any(i => i.Address.Equals(address) && i.Tag < 0); } /// public bool IsGatewayInterface(IPAddress? addressObj) { return _internalInterfaces.Any(i => i.Address.Equals(addressObj ?? IPAddress.None) && i.Tag < 0); } /// public Collection GetLoopbacks() { Collection nc = new Collection(); if (IsIP4Enabled) { nc.AddItem(IPAddress.Loopback); } if (IsIP6Enabled) { nc.AddItem(IPAddress.IPv6Loopback); } return nc; } /// public bool IsExcluded(IPAddress ip) { return _excludedSubnets.ContainsAddress(ip); } /// public bool IsExcluded(EndPoint ip) { return ip != null && IsExcluded(((IPEndPoint)ip).Address); } /// public Collection CreateIPCollection(string[] values, bool negated = false) { Collection col = new Collection(); if (values == null) { return col; } for (int a = 0; a < values.Length; a++) { string v = values[a].Trim(); try { if (v.StartsWith('!')) { if (negated) { AddToCollection(col, v[1..]); } } else if (!negated) { AddToCollection(col, v); } } catch (ArgumentException e) { _logger.LogWarning(e, "Ignoring LAN value {Value}.", v); } } return col; } /// public Collection GetAllBindInterfaces(bool individualInterfaces = false) { int count = _bindAddresses.Count; if (count == 0) { if (_bindExclusions.Count > 0) { // Return all the interfaces except the ones specifically excluded. return _interfaceAddresses.Exclude(_bindExclusions, false); } if (individualInterfaces) { return new Collection(_interfaceAddresses); } // No bind address and no exclusions, so listen on all interfaces. Collection result = new Collection(); if (IsIP6Enabled && IsIP4Enabled) { // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any result.AddItem(IPAddress.IPv6Any); } else if (IsIP4Enabled) { result.AddItem(IPAddress.Any); } else if (IsIP6Enabled) { // Cannot use IPv6Any as Kestrel will bind to IPv4 addresses. foreach (var iface in _interfaceAddresses) { if (iface.AddressFamily == AddressFamily.InterNetworkV6) { result.AddItem(iface.Address); } } } return result; } // Remove any excluded bind interfaces. return _bindAddresses.Exclude(_bindExclusions, false); } /// public string GetBindInterface(string source, out int? port) { if (!string.IsNullOrEmpty(source) && IPHost.TryParse(source, out IPHost host)) { return GetBindInterface(host, out port); } return GetBindInterface(IPHost.None, out port); } /// public string GetBindInterface(IPAddress source, out int? port) { return GetBindInterface(new IPNetAddress(source), out port); } /// public string GetBindInterface(HttpRequest source, out int? port) { string result; if (source != null && IPHost.TryParse(source.Host.Host, out IPHost host)) { result = GetBindInterface(host, out port); port ??= source.Host.Port; } else { result = GetBindInterface(IPNetAddress.None, out port); port ??= source?.Host.Port; } return result; } /// public string GetBindInterface(IPObject source, out int? port) { port = null; if (source == null) { throw new ArgumentNullException(nameof(source)); } // Do we have a source? bool haveSource = !source.Address.Equals(IPAddress.None); bool isExternal = false; if (haveSource) { 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 && source.AddressFamily == AddressFamily.InterNetwork) { _logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected."); } isExternal = !IsInLocalNetwork(source); if (MatchesPublishedServerUrl(source, isExternal, out string res, out port)) { _logger.LogInformation("{Source}: Using BindAddress {Address}:{Port}", source, res, port); return res; } } _logger.LogDebug("GetBindInterface: Source: {HaveSource}, External: {IsExternal}:", haveSource, isExternal); // No preference given, so move on to bind addresses. if (MatchesBindInterface(source, isExternal, out string result)) { return result; } if (isExternal && MatchesExternalInterface(source, out result)) { return result; } // Get the first LAN interface address that isn't a loopback. var interfaces = CreateCollection( _interfaceAddresses .Exclude(_bindExclusions, false) .Where(IsInLocalNetwork) .OrderBy(p => p.Tag)); if (interfaces.Count > 0) { if (haveSource) { foreach (var intf in interfaces) { if (intf.Address.Equals(source.Address)) { result = FormatIP6String(intf.Address); _logger.LogDebug("{Source}: GetBindInterface: Has found matching interface. {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 interfaces) { if (intf.Contains(source)) { result = FormatIP6String(intf.Address); _logger.LogDebug("{Source}: GetBindInterface: Has source, matched best internal interface on range. {Result}", source, result); return result; } } } result = FormatIP6String(interfaces.First().Address); _logger.LogDebug("{Source}: GetBindInterface: Matched first internal interface. {Result}", source, result); return result; } // There isn't any others, so we'll use the loopback. result = IsIP6Enabled ? "::1" : "127.0.0.1"; _logger.LogWarning("{Source}: GetBindInterface: Loopback {Result} returned.", source, result); return result; } /// public Collection GetInternalBindAddresses() { int count = _bindAddresses.Count; if (count == 0) { if (_bindExclusions.Count > 0) { // Return all the internal interfaces except the ones excluded. return CreateCollection(_internalInterfaces.Where(p => !_bindExclusions.ContainsAddress(p))); } // No bind address, so return all internal interfaces. return CreateCollection(_internalInterfaces.Where(p => !p.IsLoopback())); } return new Collection(_bindAddresses); } /// public bool IsInLocalNetwork(IPObject address) { if (address == null) { throw new ArgumentNullException(nameof(address)); } if (address.Equals(IPAddress.None)) { return false; } // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) { return true; } // As private addresses can be redefined by Configuration.LocalNetworkAddresses return _lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address); } /// public bool IsInLocalNetwork(string address) { if (IPHost.TryParse(address, out IPHost ep)) { return _lanSubnets.ContainsAddress(ep) && !_excludedSubnets.ContainsAddress(ep); } return false; } /// public bool IsInLocalNetwork(IPAddress address) { if (address == null) { throw new ArgumentNullException(nameof(address)); } // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) { return true; } // As private addresses can be redefined by Configuration.LocalNetworkAddresses return _lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address); } /// public bool IsPrivateAddressRange(IPObject address) { if (address == null) { throw new ArgumentNullException(nameof(address)); } // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) { return true; } else { return address.IsPrivateAddressRange(); } } /// public bool IsExcludedInterface(IPAddress address) { return _bindExclusions.ContainsAddress(address); } /// public Collection GetFilteredLANSubnets(Collection? filter = null) { if (filter == null) { return _lanSubnets.Exclude(_excludedSubnets, true).AsNetworks(); } return _lanSubnets.Exclude(filter, true); } /// public bool IsValidInterfaceAddress(IPAddress address) { return _interfaceAddresses.ContainsAddress(address); } /// public bool TryParseInterface(string token, out Collection? result) { result = null; if (string.IsNullOrEmpty(token)) { return false; } if (_interfaceNames != null && _interfaceNames.TryGetValue(token.ToLower(CultureInfo.InvariantCulture), out int index)) { result = new Collection(); _logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", token); // Replace interface tags with the interface IP's. foreach (IPNetAddress iface in _interfaceAddresses) { if (Math.Abs(iface.Tag) == index && ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) || (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) { result.AddItem(iface, false); } } return true; } return false; } /// public bool HasRemoteAccess(IPAddress remoteIp) { var config = _configurationManager.GetNetworkConfiguration(); if (config.EnableRemoteAccess) { // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely. // If left blank, all remote addresses will be allowed. if (RemoteAddressFilter.Count > 0 && !IsInLocalNetwork(remoteIp)) { // remoteAddressFilter is a whitelist or blacklist. return RemoteAddressFilter.ContainsAddress(remoteIp) == !config.IsRemoteIPFilterBlacklist; } } else if (!IsInLocalNetwork(remoteIp)) { // Remote not enabled. So everyone should be LAN. return false; } return true; } /// /// Reloads all settings and re-initialises the instance. /// /// The to use. public void UpdateSettings(object configuration) { NetworkConfiguration config = (NetworkConfiguration)configuration ?? throw new ArgumentNullException(nameof(configuration)); IsIP4Enabled = Socket.OSSupportsIPv4 && config.EnableIPV4; IsIP6Enabled = Socket.OSSupportsIPv6 && config.EnableIPV6; if (!IsIP6Enabled && !IsIP4Enabled) { _logger.LogError("IPv4 and IPv6 cannot both be disabled."); IsIP4Enabled = true; } TrustAllIP6Interfaces = config.TrustAllIP6Interfaces; // UdpHelper.EnableMultiSocketBinding = config.EnableMultiSocketBinding; if (string.IsNullOrEmpty(MockNetworkSettings)) { InitialiseInterfaces(); } else // Used in testing only. { // Format is ,,: . Set index to -ve to simulate a gateway. var interfaceList = MockNetworkSettings.Split('|'); foreach (var details in interfaceList) { var parts = details.Split(','); var address = IPNetAddress.Parse(parts[0]); var index = int.Parse(parts[1], CultureInfo.InvariantCulture); address.Tag = index; _interfaceAddresses.AddItem(address, false); _interfaceNames[parts[2]] = Math.Abs(index); } if (IsIP4Enabled) { _interfaceAddresses.AddItem(IPNetAddress.IP4Loopback); } if (IsIP6Enabled) { _interfaceAddresses.AddItem(IPNetAddress.IP6Loopback); } } InitialiseLAN(config); InitialiseBind(config); InitialiseRemote(config); InitialiseOverrides(config); } /// /// 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; } } /// /// Tries to identify the string and return an object of that class. /// /// String to parse. /// IPObject to return. /// true if the value parsed successfully, false otherwise. private static bool TryParse(string addr, out IPObject result) { if (!string.IsNullOrEmpty(addr)) { // Is it an IP address if (IPNetAddress.TryParse(addr, out IPNetAddress nw)) { result = nw; return true; } if (IPHost.TryParse(addr, out IPHost h)) { result = h; return true; } } result = IPNetAddress.None; return false; } /// /// Converts an IPAddress into a string. /// Ipv6 addresses are returned in [ ], with their scope removed. /// /// Address to convert. /// URI safe conversion of the address. private static string FormatIP6String(IPAddress address) { var str = address.ToString(); if (address.AddressFamily == AddressFamily.InterNetworkV6) { int i = str.IndexOf("%", StringComparison.OrdinalIgnoreCase); if (i != -1) { str = str.Substring(0, i); } return $"[{str}]"; } return str; } private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt) { if (evt.Key.Equals("network", StringComparison.Ordinal)) { UpdateSettings((NetworkConfiguration)evt.NewConfiguration); } } /// /// Checks the string to see if it matches any interface names. /// /// String to check. /// Interface index numbers that match. /// true if an interface name matches the token, False otherwise. private bool TryGetInterfaces(string token, [NotNullWhen(true)] out List? index) { index = null; // Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1. // Null check required here for automated testing. if (_interfaceNames != null && token.Length > 1) { bool partial = token[^1] == '*'; if (partial) { token = token[0..^1]; } foreach ((string interfc, int interfcIndex) in _interfaceNames) { if ((!partial && string.Equals(interfc, token, StringComparison.OrdinalIgnoreCase)) || (partial && interfc.StartsWith(token, true, CultureInfo.InvariantCulture))) { index ??= new List(); index.Add(interfcIndex); } } } return index != null; } /// /// Parses a string and adds it into the collection, replacing any interface references. /// /// Collection. /// String value to parse. private void AddToCollection(Collection col, string token) { // Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1. // Null check required here for automated testing. if (TryGetInterfaces(token, out var indices)) { _logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", token); // Replace all the interface tags with the interface IP's. foreach (IPNetAddress iface in _interfaceAddresses) { if (indices.Contains(Math.Abs(iface.Tag)) && ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) || (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) { col.AddItem(iface); } } } else if (TryParse(token, out IPObject obj)) { // Expand if the ip address is "any". if ((obj.Address.Equals(IPAddress.Any) && IsIP4Enabled) || (obj.Address.Equals(IPAddress.IPv6Any) && IsIP6Enabled)) { foreach (IPNetAddress iface in _interfaceAddresses) { if (obj.AddressFamily == iface.AddressFamily) { col.AddItem(iface); } } } else if (!IsIP6Enabled) { // Remove IP6 addresses from multi-homed IPHosts. obj.Remove(AddressFamily.InterNetworkV6); if (!obj.IsIP6()) { col.AddItem(obj); } } else if (!IsIP4Enabled) { // Remove IP4 addresses from multi-homed IPHosts. obj.Remove(AddressFamily.InterNetwork); if (obj.IsIP6()) { col.AddItem(obj); } } else { col.AddItem(obj); } } else { _logger.LogDebug("Invalid or unknown object {Token}.", token); } } /// /// Handler for network change events. /// /// Sender. /// A containing network availability information. private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e) { _logger.LogDebug("Network availability changed."); OnNetworkChanged(); } /// /// Handler for network change events. /// /// Sender. /// An . private void OnNetworkAddressChanged(object? sender, EventArgs e) { _logger.LogDebug("Network address change detected."); OnNetworkChanged(); } /// /// Async task that waits for 2 seconds before re-initialising the settings, as typically these events fire multiple times in succession. /// /// A representing the asynchronous operation. private async Task OnNetworkChangeAsync() { try { await Task.Delay(2000).ConfigureAwait(false); InitialiseInterfaces(); // Recalculate LAN caches. InitialiseLAN(_configurationManager.GetNetworkConfiguration()); NetworkChanged?.Invoke(this, EventArgs.Empty); } finally { _eventfire = false; } } /// /// Triggers our event, and re-loads interface information. /// private void OnNetworkChanged() { lock (_eventFireLock) { if (!_eventfire) { _logger.LogDebug("Network Address Change Event."); // As network events tend to fire one after the other only fire once every second. _eventfire = true; OnNetworkChangeAsync().GetAwaiter().GetResult(); } } } /// /// 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 InitialiseOverrides(NetworkConfiguration config) { lock (_intLock) { _publishedServerUrls.Clear(); string[] overrides = config.PublishedServerUriBySubnet; if (overrides == null) { return; } foreach (var entry in overrides) { var parts = entry.Split('='); if (parts.Length != 2) { _logger.LogError("Unable to parse bind override: {Entry}", entry); } else { var replacement = parts[1].Trim(); if (string.Equals(parts[0], "all", StringComparison.OrdinalIgnoreCase)) { _publishedServerUrls[new IPNetAddress(IPAddress.Broadcast)] = replacement; } else if (string.Equals(parts[0], "external", StringComparison.OrdinalIgnoreCase)) { _publishedServerUrls[new IPNetAddress(IPAddress.Any)] = replacement; } else if (TryParseInterface(parts[0], out Collection? addresses) && addresses != null) { foreach (IPNetAddress na in addresses) { _publishedServerUrls[na] = replacement; } } else if (IPNetAddress.TryParse(parts[0], out IPNetAddress result)) { _publishedServerUrls[result] = replacement; } else { _logger.LogError("Unable to parse bind ip address. {Parts}", parts[1]); } } } } } /// /// Initialises the network bind addresses. /// private void InitialiseBind(NetworkConfiguration config) { lock (_intLock) { string[] lanAddresses = config.LocalNetworkAddresses; // Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded. if (config.IgnoreVirtualInterfaces) { // each virtual interface name must be pre-pended with the exclusion symbol ! var virtualInterfaceNames = config.VirtualInterfaceNames.Split(',').Select(p => "!" + p).ToArray(); if (lanAddresses.Length > 0) { var newList = new string[lanAddresses.Length + virtualInterfaceNames.Length]; Array.Copy(lanAddresses, newList, lanAddresses.Length); Array.Copy(virtualInterfaceNames, 0, newList, lanAddresses.Length, virtualInterfaceNames.Length); lanAddresses = newList; } else { lanAddresses = virtualInterfaceNames; } } // Read and parse bind addresses and exclusions, removing ones that don't exist. _bindAddresses = CreateIPCollection(lanAddresses).ThatAreContainedInNetworks(_interfaceAddresses); _bindExclusions = CreateIPCollection(lanAddresses, true).ThatAreContainedInNetworks(_interfaceAddresses); _logger.LogInformation("Using bind addresses: {0}", _bindAddresses.AsString()); _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions.AsString()); } } /// /// Initialises the remote address values. /// private void InitialiseRemote(NetworkConfiguration config) { lock (_intLock) { RemoteAddressFilter = CreateIPCollection(config.RemoteIPFilter); } } /// /// Initialises internal LAN cache settings. /// private void InitialiseLAN(NetworkConfiguration config) { lock (_intLock) { _logger.LogDebug("Refreshing LAN information."); // Get configuration options. string[] subnets = config.LocalNetworkSubnets; // Create lists from user settings. _lanSubnets = CreateIPCollection(subnets); _excludedSubnets = CreateIPCollection(subnets, true).AsNetworks(); // If no LAN addresses are specified - all private subnets are deemed to be the LAN _usingPrivateAddresses = _lanSubnets.Count == 0; // NOTE: The order of the commands generating the collection in this statement matters. // Altering the order will cause the collections to be created incorrectly. if (_usingPrivateAddresses) { _logger.LogDebug("Using LAN interface addresses as user provided no LAN details."); // Internal interfaces must be private and not excluded. _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsPrivateAddressRange(i) && !_excludedSubnets.ContainsAddress(i))); // Subnets are the same as the calculated internal interface. _lanSubnets = new Collection(); // We must listen on loopback for LiveTV to function regardless of the settings. if (IsIP6Enabled) { _lanSubnets.AddItem(IPNetAddress.IP6Loopback); _lanSubnets.AddItem(IPNetAddress.Parse("fc00::/7")); // ULA _lanSubnets.AddItem(IPNetAddress.Parse("fe80::/10")); // Site local } if (IsIP4Enabled) { _lanSubnets.AddItem(IPNetAddress.IP4Loopback); _lanSubnets.AddItem(IPNetAddress.Parse("10.0.0.0/8")); _lanSubnets.AddItem(IPNetAddress.Parse("172.16.0.0/12")); _lanSubnets.AddItem(IPNetAddress.Parse("192.168.0.0/16")); } } else { // We must listen on loopback for LiveTV to function regardless of the settings. if (IsIP6Enabled) { _lanSubnets.AddItem(IPNetAddress.IP6Loopback); } if (IsIP4Enabled) { _lanSubnets.AddItem(IPNetAddress.IP4Loopback); } // Internal interfaces must be private, not excluded and part of the LocalNetworkSubnet. _internalInterfaces = CreateCollection(_interfaceAddresses.Where(IsInLocalNetwork)); } _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets.AsString()); _logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets.AsString()); _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets, true).AsNetworks().AsString()); } } /// /// 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. /// private void InitialiseInterfaces() { lock (_intLock) { _logger.LogDebug("Refreshing interfaces."); _interfaceNames.Clear(); _interfaceAddresses.Clear(); _macAddresses.Clear(); try { IEnumerable nics = NetworkInterface.GetAllNetworkInterfaces() .Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up); foreach (NetworkInterface adapter in nics) { try { IPInterfaceProperties ipProperties = adapter.GetIPProperties(); PhysicalAddress mac = adapter.GetPhysicalAddress(); // populate mac list if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && mac != null && mac != PhysicalAddress.None) { _macAddresses.Add(mac); } // populate interface address list foreach (UnicastIPAddressInformation info in ipProperties.UnicastAddresses) { if (IsIP4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) { IPNetAddress nw = new IPNetAddress(info.Address, IPObject.MaskToCidr(info.IPv4Mask)) { // Keep the number of gateways on this interface, along with its index. Tag = ipProperties.GetIPv4Properties().Index }; int tag = nw.Tag; if (ipProperties.GatewayAddresses.Count > 0 && !nw.IsLoopback()) { // -ve Tags signify the interface has a gateway. nw.Tag *= -1; } _interfaceAddresses.AddItem(nw, false); // Store interface name so we can use the name in Collections. _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag; } else if (IsIP6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) { IPNetAddress nw = new IPNetAddress(info.Address, (byte)info.PrefixLength) { // Keep the number of gateways on this interface, along with its index. Tag = ipProperties.GetIPv6Properties().Index }; int tag = nw.Tag; if (ipProperties.GatewayAddresses.Count > 0 && !nw.IsLoopback()) { // -ve Tags signify the interface has a gateway. nw.Tag *= -1; } _interfaceAddresses.AddItem(nw, false); // Store interface name so we can use the name in Collections. _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag; } } } #pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) { // Ignore error, and attempt to continue. _logger.LogError(ex, "Error encountered parsing interfaces."); } #pragma warning restore CA1031 // Do not catch general exception types } } catch (Exception ex) { _logger.LogError(ex, "Error in InitialiseInterfaces."); } // If for some reason we don't have an interface info, resolve our DNS name. if (_interfaceAddresses.Count == 0) { _logger.LogError("No interfaces information available. Resolving DNS name."); IPHost host = new IPHost(Dns.GetHostName()); foreach (var a in host.GetAddresses()) { _interfaceAddresses.AddItem(a); } if (_interfaceAddresses.Count == 0) { _logger.LogWarning("No interfaces information available. Using loopback."); } } if (IsIP4Enabled) { _interfaceAddresses.AddItem(IPNetAddress.IP4Loopback); } if (IsIP6Enabled) { _interfaceAddresses.AddItem(IPNetAddress.IP6Loopback); } _logger.LogDebug("Discovered {0} interfaces.", _interfaceAddresses.Count); _logger.LogDebug("Interfaces addresses : {0}", _interfaceAddresses.AsString()); } } /// /// Attempts to match the source against a user defined bind interface. /// /// IP source address to use. /// True if the source is in the external subnet. /// The published server url that matches the source address. /// The resultant port, if one exists. /// true if a match is found, false otherwise. private bool MatchesPublishedServerUrl(IPObject source, bool isInExternalSubnet, 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.Address.Equals(IPAddress.Broadcast)) { bindPreference = addr.Value; break; } else if ((addr.Key.Address.Equals(IPAddress.Any) || addr.Key.Address.Equals(IPAddress.IPv6Any)) && isInExternalSubnet) { // External. bindPreference = addr.Value; break; } else if (addr.Key.Contains(source)) { // Match ip address. bindPreference = addr.Value; break; } } if (string.IsNullOrEmpty(bindPreference)) { return false; } // 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; } /// /// Attempts to match the source against a user defined bind interface. /// /// 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(IPObject source, bool isInExternalSubnet, out string result) { result = string.Empty; var addresses = _bindAddresses.Exclude(_bindExclusions, false); int count = addresses.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. IPAddress? defaultGateway = null; IPAddress? bindAddress = null; if (isInExternalSubnet) { // Find all external bind addresses. Store the default gateway, but check to see if there is a better match first. foreach (var addr in addresses.OrderBy(p => p.Tag)) { if (defaultGateway == null && !IsInLocalNetwork(addr)) { defaultGateway = addr.Address; } if (bindAddress == null && addr.Contains(source)) { bindAddress = addr.Address; } if (defaultGateway != null && bindAddress != null) { break; } } } else { // Look for the best internal address. bindAddress = addresses .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("{Source}: GetBindInterface: Has source, found a match bind interface subnets. {Result}", source, result); return true; } if (isInExternalSubnet && defaultGateway != null) { result = FormatIP6String(defaultGateway); _logger.LogDebug("{Source}: GetBindInterface: Using first user defined external interface. {Result}", source, result); return true; } result = FormatIP6String(addresses[0].Address); _logger.LogDebug("{Source}: GetBindInterface: Selected first user defined interface. {Result}", source, result); if (isInExternalSubnet) { _logger.LogWarning("{Source}: External request received, however, only an internal interface bind found.", source); } return true; } return false; } /// /// Attempts to match the source against an external interface. /// /// IP source address to use. /// The result, if a match is found. /// true if a match is found, false otherwise. private bool MatchesExternalInterface(IPObject source, out string result) { result = string.Empty; // Get the first WAN interface address that isn't a loopback. var extResult = _interfaceAddresses .Exclude(_bindExclusions, false) .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(source)) { result = FormatIP6String(intf.Address); _logger.LogDebug("{Source}: GetBindInterface: Selected best external on interface on range. {Result}", source, result); return true; } } result = FormatIP6String(extResult.First().Address); _logger.LogDebug("{Source}: GetBindInterface: Selected first external interface. {Result}", source, result); return true; } _logger.LogDebug("{Source}: External request received, but no WAN interface found. Need to route through internal network.", source); return false; } } }