Files
skystack/QemuVmManager.Core/UPnPManager.cs
2025-08-31 14:03:58 -04:00

295 lines
8.9 KiB
C#

using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Logging;
using QemuVmManager.Models;
using Open.Nat;
namespace QemuVmManager.Core;
public interface IUPnPManager
{
Task<bool> IsUPnPAvailableAsync();
Task<IPAddress?> GetExternalIpAddressAsync();
Task<bool> AddPortMappingAsync(int externalPort, int internalPort, string description);
Task<bool> RemovePortMappingAsync(int externalPort);
Task<List<PortMapping>> GetPortMappingsAsync();
}
public class UPnPManager : IUPnPManager
{
private readonly ILogger<UPnPManager>? _logger;
private readonly Dictionary<int, PortMapping> _activeMappings = new();
private readonly object _lock = new();
private NatDevice? _natDevice;
private bool _isInitialized = false;
public UPnPManager(ILogger<UPnPManager>? logger = null)
{
_logger = logger;
}
private async Task InitializeAsync()
{
if (_isInitialized) return;
try
{
_logger?.LogInformation("Initializing UPnP/NAT discovery...");
// Create a new NAT discoverer
var discoverer = new NatDiscoverer();
// Discover UPnP devices with a 10-second timeout
_natDevice = await discoverer.DiscoverDeviceAsync(PortMapper.Upnp, new CancellationTokenSource(TimeSpan.FromSeconds(10)));
if (_natDevice != null)
{
_logger?.LogInformation("UPnP device discovered successfully");
_isInitialized = true;
}
else
{
_logger?.LogWarning("No UPnP devices found");
}
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "UPnP initialization failed");
_natDevice = null;
_isInitialized = false;
}
}
public async Task<bool> IsUPnPAvailableAsync()
{
try
{
await InitializeAsync();
return _natDevice != null;
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "UPnP availability check failed");
return false;
}
}
public async Task<IPAddress?> GetExternalIpAddressAsync()
{
try
{
await InitializeAsync();
if (_natDevice == null)
{
_logger?.LogWarning("No UPnP device available for external IP lookup");
return await GetExternalIpFromServiceAsync();
}
var externalIp = await _natDevice.GetExternalIPAsync();
_logger?.LogInformation("External IP from UPnP: {ExternalIP}", externalIp);
return externalIp;
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Failed to get external IP from UPnP, falling back to external service");
return await GetExternalIpFromServiceAsync();
}
}
public async Task<bool> AddPortMappingAsync(int externalPort, int internalPort, string description)
{
try
{
await InitializeAsync();
if (_natDevice == null)
{
_logger?.LogError("No UPnP device available for port mapping");
return false;
}
var localIp = GetLocalIpAddress();
if (localIp == null)
{
_logger?.LogError("Could not determine local IP address");
return false;
}
// Create the port mapping
var mapping = new Mapping(Protocol.Tcp, internalPort, externalPort, description);
// Add the mapping
await _natDevice.CreatePortMapAsync(mapping);
lock (_lock)
{
_activeMappings[externalPort] = new PortMapping
{
ExternalPort = externalPort,
InternalPort = internalPort,
InternalIp = localIp,
Protocol = "TCP",
Description = description,
CreatedAt = DateTime.UtcNow
};
}
_logger?.LogInformation("Successfully added port mapping: {ExternalPort} -> {InternalIp}:{InternalPort} ({Description})",
externalPort, localIp, internalPort, description);
return true;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to add port mapping {ExternalPort} -> {InternalPort}",
externalPort, internalPort);
return false;
}
}
public async Task<bool> RemovePortMappingAsync(int externalPort)
{
try
{
await InitializeAsync();
if (_natDevice == null)
{
_logger?.LogError("No UPnP device available for port mapping removal");
return false;
}
var localIp = GetLocalIpAddress();
if (localIp == null)
{
_logger?.LogError("Could not determine local IP address for mapping removal");
return false;
}
// Create the mapping object for removal
var mapping = new Mapping(Protocol.Tcp, 0, externalPort, "");
// Remove the mapping
await _natDevice.DeletePortMapAsync(mapping);
lock (_lock)
{
_activeMappings.Remove(externalPort);
}
_logger?.LogInformation("Successfully removed port mapping: {ExternalPort}", externalPort);
return true;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to remove port mapping {ExternalPort}", externalPort);
return false;
}
}
public async Task<List<PortMapping>> GetPortMappingsAsync()
{
try
{
await InitializeAsync();
if (_natDevice == null)
{
_logger?.LogWarning("No UPnP device available for port mapping retrieval");
lock (_lock)
{
return _activeMappings.Values.ToList();
}
}
var mappings = new List<PortMapping>();
// For now, just return our active mappings since the Open.NAT API is complex
// In a full implementation, we would query the device for all mappings
lock (_lock)
{
mappings.AddRange(_activeMappings.Values);
}
return mappings;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to get port mappings from UPnP device");
lock (_lock)
{
return _activeMappings.Values.ToList();
}
}
}
private async Task<IPAddress?> GetExternalIpFromServiceAsync()
{
try
{
using var client = new HttpClient();
client.Timeout = TimeSpan.FromSeconds(10);
// Try multiple external IP services
var services = new[]
{
"https://api.ipify.org",
"https://icanhazip.com",
"https://ifconfig.me/ip"
};
foreach (var service in services)
{
try
{
var response = await client.GetStringAsync(service);
var ipString = response.Trim();
if (IPAddress.TryParse(ipString, out var ip))
{
_logger?.LogInformation("External IP from service {Service}: {IP}", service, ip);
return ip;
}
}
catch (Exception ex)
{
_logger?.LogDebug(ex, "Failed to get external IP from {Service}", service);
continue;
}
}
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Failed to get external IP from any service");
}
return null;
}
private IPAddress? GetLocalIpAddress()
{
try
{
var host = Dns.GetHostEntry(Dns.GetHostName());
return host.AddressList.FirstOrDefault(ip =>
ip.AddressFamily == AddressFamily.InterNetwork &&
!IPAddress.IsLoopback(ip));
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Failed to get local IP address");
return null;
}
}
}
public class PortMapping
{
public int ExternalPort { get; set; }
public int InternalPort { get; set; }
public IPAddress InternalIp { get; set; } = IPAddress.Any;
public string Protocol { get; set; } = "TCP";
public string Description { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
}