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(); } 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..."); // 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 IsUPnPAvailableAsync() { try { await InitializeAsync(); return _natDevice != null; } catch (Exception ex) { _logger?.LogWarning(ex, "UPnP availability check failed"); return false; } } 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; } }