Files
skystack/QemuVmManager.Core/UPnPManager.cs

396 lines
13 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();
Task<string> GetUPnPDiagnosticsAsync();
}
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 for macOS...");
// Create a new NAT discoverer
var discoverer = new NatDiscoverer();
// Try UPnP first (most common on home routers)
_logger?.LogInformation("Trying UPnP discovery...");
_natDevice = await discoverer.DiscoverDeviceAsync(PortMapper.Upnp, new CancellationTokenSource(TimeSpan.FromSeconds(8)));
if (_natDevice != null)
{
_logger?.LogInformation("UPnP device discovered successfully");
_isInitialized = true;
return;
}
// Try NAT-PMP (Apple's protocol, common on Apple routers)
_logger?.LogInformation("UPnP not found, trying NAT-PMP...");
_natDevice = await discoverer.DiscoverDeviceAsync(PortMapper.Pmp, new CancellationTokenSource(TimeSpan.FromSeconds(5)));
if (_natDevice != null)
{
_logger?.LogInformation("NAT-PMP device discovered successfully");
_isInitialized = true;
return;
}
// Note: PCP (Port Control Protocol) is not supported by Open.NAT library
// We'll stick with UPnP and NAT-PMP which are the most common
_logger?.LogWarning("No UPnP/NAT devices found. This could be due to:");
_logger?.LogWarning("- Router doesn't support UPnP");
_logger?.LogWarning("- macOS firewall blocking discovery");
_logger?.LogWarning("- Network policy blocking UPnP");
_logger?.LogWarning("- Router UPnP is disabled");
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "UPnP/NAT initialization failed");
_logger?.LogInformation("Common solutions:");
_logger?.LogInformation("- Check router UPnP settings");
_logger?.LogInformation("- Temporarily disable macOS firewall");
_logger?.LogInformation("- Try from a different network");
_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<string> GetUPnPDiagnosticsAsync()
{
try
{
var diagnostics = new List<string>();
// Check if we can discover devices
diagnostics.Add("🔍 UPnP Discovery Test:");
var discoverer = new NatDiscoverer();
// Test UPnP
try
{
var upnpDevice = await discoverer.DiscoverDeviceAsync(PortMapper.Upnp, new CancellationTokenSource(TimeSpan.FromSeconds(5)));
if (upnpDevice != null)
{
diagnostics.Add("✅ UPnP: Available");
var externalIp = await upnpDevice.GetExternalIPAsync();
diagnostics.Add($" External IP: {externalIp}");
}
else
{
diagnostics.Add("❌ UPnP: Not available");
}
}
catch (Exception ex)
{
diagnostics.Add($"❌ UPnP: Error - {ex.Message}");
}
// Test NAT-PMP
try
{
var pmpDevice = await discoverer.DiscoverDeviceAsync(PortMapper.Pmp, new CancellationTokenSource(TimeSpan.FromSeconds(5)));
if (pmpDevice != null)
{
diagnostics.Add("✅ NAT-PMP: Available");
var externalIp = await pmpDevice.GetExternalIPAsync();
diagnostics.Add($" External IP: {externalIp}");
}
else
{
diagnostics.Add("❌ NAT-PMP: Not available");
}
}
catch (Exception ex)
{
diagnostics.Add($"❌ NAT-PMP: Error - {ex.Message}");
}
// Note: PCP is not supported by Open.NAT library
diagnostics.Add("❌ PCP: Not supported by Open.NAT library");
// Network information
diagnostics.Add("\n🌐 Network Information:");
var localIp = GetLocalIpAddress();
diagnostics.Add($"Local IP: {localIp?.ToString() ?? "Unknown"}");
// Try external IP service as fallback
try
{
var externalIp = await GetExternalIpFromServiceAsync();
diagnostics.Add($"External IP (Service): {externalIp?.ToString() ?? "Unknown"}");
}
catch
{
diagnostics.Add("External IP (Service): Failed");
}
return string.Join("\n", diagnostics);
}
catch (Exception ex)
{
return $"Diagnostics failed: {ex.Message}";
}
}
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; }
}