using System.Net; using System.Net.Sockets; using Microsoft.Extensions.Logging; using QemuVmManager.Models; using Open.Nat; namespace QemuVmManager.Core; public interface IUPnPManager { Task IsUPnPAvailableAsync(); Task GetExternalIpAddressAsync(); Task AddPortMappingAsync(int externalPort, int internalPort, string description); Task RemovePortMappingAsync(int externalPort); Task> GetPortMappingsAsync(); Task GetUPnPDiagnosticsAsync(); } public class UPnPManager : IUPnPManager { private readonly ILogger? _logger; private readonly Dictionary _activeMappings = new(); private readonly object _lock = new(); private NatDevice? _natDevice; private bool _isInitialized = false; public UPnPManager(ILogger? 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 IsUPnPAvailableAsync() { try { await InitializeAsync(); return _natDevice != null; } catch (Exception ex) { _logger?.LogWarning(ex, "UPnP availability check failed"); return false; } } public async Task GetUPnPDiagnosticsAsync() { try { var diagnostics = new List(); // 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 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 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 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> 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(); // 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 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; } }