PortForwardingHost.cs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.Net;
  5. using System.Text;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. using MediaBrowser.Common.Net;
  9. using MediaBrowser.Controller;
  10. using MediaBrowser.Controller.Configuration;
  11. using Microsoft.Extensions.Hosting;
  12. using Microsoft.Extensions.Logging;
  13. using Mono.Nat;
  14. namespace Jellyfin.Networking;
  15. /// <summary>
  16. /// <see cref="IHostedService"/> responsible for UPnP port forwarding.
  17. /// </summary>
  18. public sealed class PortForwardingHost : IHostedService, IDisposable
  19. {
  20. private readonly IServerApplicationHost _appHost;
  21. private readonly ILogger<PortForwardingHost> _logger;
  22. private readonly IServerConfigurationManager _config;
  23. private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new();
  24. private Timer? _timer;
  25. private string? _configIdentifier;
  26. private bool _disposed;
  27. /// <summary>
  28. /// Initializes a new instance of the <see cref="PortForwardingHost"/> class.
  29. /// </summary>
  30. /// <param name="logger">The logger.</param>
  31. /// <param name="appHost">The application host.</param>
  32. /// <param name="config">The configuration manager.</param>
  33. public PortForwardingHost(
  34. ILogger<PortForwardingHost> logger,
  35. IServerApplicationHost appHost,
  36. IServerConfigurationManager config)
  37. {
  38. _logger = logger;
  39. _appHost = appHost;
  40. _config = config;
  41. }
  42. private string GetConfigIdentifier()
  43. {
  44. const char Separator = '|';
  45. var config = _config.GetNetworkConfiguration();
  46. return new StringBuilder(32)
  47. .Append(config.EnableUPnP).Append(Separator)
  48. .Append(config.PublicHttpPort).Append(Separator)
  49. .Append(config.PublicHttpsPort).Append(Separator)
  50. .Append(_appHost.HttpPort).Append(Separator)
  51. .Append(_appHost.HttpsPort).Append(Separator)
  52. .Append(_appHost.ListenWithHttps).Append(Separator)
  53. .Append(config.EnableRemoteAccess).Append(Separator)
  54. .ToString();
  55. }
  56. private void OnConfigurationUpdated(object? sender, EventArgs e)
  57. {
  58. var oldConfigIdentifier = _configIdentifier;
  59. _configIdentifier = GetConfigIdentifier();
  60. if (!string.Equals(_configIdentifier, oldConfigIdentifier, StringComparison.OrdinalIgnoreCase))
  61. {
  62. Stop();
  63. Start();
  64. }
  65. }
  66. /// <inheritdoc />
  67. public Task StartAsync(CancellationToken cancellationToken)
  68. {
  69. Start();
  70. _config.ConfigurationUpdated += OnConfigurationUpdated;
  71. return Task.CompletedTask;
  72. }
  73. /// <inheritdoc />
  74. public Task StopAsync(CancellationToken cancellationToken)
  75. {
  76. Stop();
  77. return Task.CompletedTask;
  78. }
  79. private void Start()
  80. {
  81. var config = _config.GetNetworkConfiguration();
  82. if (!config.EnableUPnP || !config.EnableRemoteAccess)
  83. {
  84. return;
  85. }
  86. _logger.LogInformation("Starting NAT discovery");
  87. NatUtility.DeviceFound += OnNatUtilityDeviceFound;
  88. NatUtility.StartDiscovery();
  89. _timer?.Dispose();
  90. _timer = new Timer(_ => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
  91. }
  92. private void Stop()
  93. {
  94. _logger.LogInformation("Stopping NAT discovery");
  95. NatUtility.StopDiscovery();
  96. NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
  97. _timer?.Dispose();
  98. _timer = null;
  99. }
  100. private async void OnNatUtilityDeviceFound(object? sender, DeviceEventArgs e)
  101. {
  102. ObjectDisposedException.ThrowIf(_disposed, this);
  103. try
  104. {
  105. // On some systems the device discovered event seems to fire repeatedly
  106. // This check will help ensure we're not trying to port map the same device over and over
  107. if (!_createdRules.TryAdd(e.Device.DeviceEndpoint, 0))
  108. {
  109. return;
  110. }
  111. await Task.WhenAll(CreatePortMaps(e.Device)).ConfigureAwait(false);
  112. }
  113. catch (Exception ex)
  114. {
  115. _logger.LogError(ex, "Error creating port forwarding rules");
  116. }
  117. }
  118. private IEnumerable<Task> CreatePortMaps(INatDevice device)
  119. {
  120. var config = _config.GetNetworkConfiguration();
  121. yield return CreatePortMap(device, _appHost.HttpPort, config.PublicHttpPort);
  122. if (_appHost.ListenWithHttps)
  123. {
  124. yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicHttpsPort);
  125. }
  126. }
  127. private async Task CreatePortMap(INatDevice device, int privatePort, int publicPort)
  128. {
  129. _logger.LogDebug(
  130. "Creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}",
  131. privatePort,
  132. publicPort,
  133. device.DeviceEndpoint);
  134. try
  135. {
  136. var mapping = new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name);
  137. await device.CreatePortMapAsync(mapping).ConfigureAwait(false);
  138. }
  139. catch (Exception ex)
  140. {
  141. _logger.LogError(
  142. ex,
  143. "Error creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}.",
  144. privatePort,
  145. publicPort,
  146. device.DeviceEndpoint);
  147. }
  148. }
  149. /// <inheritdoc />
  150. public void Dispose()
  151. {
  152. if (_disposed)
  153. {
  154. return;
  155. }
  156. _config.ConfigurationUpdated -= OnConfigurationUpdated;
  157. _timer?.Dispose();
  158. _timer = null;
  159. _disposed = true;
  160. }
  161. }