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