using System.Net; using System.Net.Sockets; using Microsoft.Extensions.Logging; using QemuVmManager.Models; 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(); public UPnPManager(ILogger? logger = null) { _logger = logger; } public async Task IsUPnPAvailableAsync() { try { // Try to discover UPnP devices var devices = await DiscoverUPnPDevicesAsync(); return devices.Any(); } catch (Exception ex) { _logger?.LogWarning(ex, "UPnP discovery failed"); return false; } } public async Task GetExternalIpAddressAsync() { try { // Try multiple methods to get external IP var externalIp = await GetExternalIpFromUPnPAsync(); if (externalIp != null) return externalIp; // Fallback to external service return await GetExternalIpFromServiceAsync(); } catch (Exception ex) { _logger?.LogError(ex, "Failed to get external IP address"); return null; } } public async Task AddPortMappingAsync(int externalPort, int internalPort, string description) { try { var localIp = GetLocalIpAddress(); if (localIp == null) { _logger?.LogError("Could not determine local IP address"); return false; } var success = await AddUPnPPortMappingAsync(externalPort, internalPort, localIp, description); if (success) { lock (_lock) { _activeMappings[externalPort] = new PortMapping { ExternalPort = externalPort, InternalPort = internalPort, InternalIp = localIp, Protocol = "TCP", Description = description, CreatedAt = DateTime.UtcNow }; } _logger?.LogInformation("Added port mapping: {ExternalPort} -> {InternalPort} ({Description})", externalPort, internalPort, description); } return success; } catch (Exception ex) { _logger?.LogError(ex, "Failed to add port mapping {ExternalPort} -> {InternalPort}", externalPort, internalPort); return false; } } public async Task RemovePortMappingAsync(int externalPort) { try { var success = await RemoveUPnPPortMappingAsync(externalPort); if (success) { lock (_lock) { _activeMappings.Remove(externalPort); } _logger?.LogInformation("Removed port mapping: {ExternalPort}", externalPort); } return success; } catch (Exception ex) { _logger?.LogError(ex, "Failed to remove port mapping {ExternalPort}", externalPort); return false; } } public async Task> GetPortMappingsAsync() { try { var mappings = await GetUPnPPortMappingsAsync(); lock (_lock) { // Merge with our active mappings foreach (var mapping in _activeMappings.Values) { if (!mappings.Any(m => m.ExternalPort == mapping.ExternalPort)) { mappings.Add(mapping); } } } return mappings; } catch (Exception ex) { _logger?.LogError(ex, "Failed to get port mappings"); lock (_lock) { return _activeMappings.Values.ToList(); } } } private async Task> DiscoverUPnPDevicesAsync() { var devices = new List(); // Simple SSDP discovery var ssdpMessage = "M-SEARCH * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "MAN: \"ssdp:discover\"\r\n" + "MX: 3\r\n" + "ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n" + "\r\n"; using var udpClient = new UdpClient(); udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); var endpoint = new IPEndPoint(IPAddress.Parse("239.255.255.250"), 1900); var data = System.Text.Encoding.UTF8.GetBytes(ssdpMessage); await udpClient.SendAsync(data, data.Length, endpoint); // Wait for responses var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); try { while (!cts.Token.IsCancellationRequested) { var result = await udpClient.ReceiveAsync(cts.Token); var response = System.Text.Encoding.UTF8.GetString(result.Buffer); if (response.Contains("InternetGatewayDevice")) { var device = ParseUPnPResponse(response, result.RemoteEndPoint); if (device != null) devices.Add(device); } } } catch (OperationCanceledException) { // Timeout - this is expected } return devices; } private UPnPDevice? ParseUPnPResponse(string response, IPEndPoint endpoint) { try { var lines = response.Split('\n'); var location = lines.FirstOrDefault(l => l.StartsWith("LOCATION:", StringComparison.OrdinalIgnoreCase)); if (location != null) { var url = location.Substring("LOCATION:".Length).Trim(); return new UPnPDevice { ControlUrl = url, IpAddress = endpoint.Address, Port = endpoint.Port }; } } catch (Exception ex) { _logger?.LogWarning(ex, "Failed to parse UPnP response"); } return null; } private async Task GetExternalIpFromUPnPAsync() { try { var devices = await DiscoverUPnPDevicesAsync(); if (!devices.Any()) return null; // Use the first device to get external IP var device = devices.First(); // This would require implementing SOAP calls to the UPnP device // For now, we'll use the fallback method return null; } catch { return null; } } 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)) return ip; } catch { continue; } } } catch (Exception ex) { _logger?.LogWarning(ex, "Failed to get external IP from services"); } 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; } } private async Task AddUPnPPortMappingAsync(int externalPort, int internalPort, IPAddress internalIp, string description) { try { var devices = await DiscoverUPnPDevicesAsync(); if (!devices.Any()) return false; // This would require implementing SOAP calls to add port mapping // For now, we'll simulate success _logger?.LogInformation("Simulating UPnP port mapping: {ExternalPort} -> {InternalIp}:{InternalPort}", externalPort, internalIp, internalPort); return true; } catch (Exception ex) { _logger?.LogError(ex, "Failed to add UPnP port mapping"); return false; } } private async Task RemoveUPnPPortMappingAsync(int externalPort) { try { var devices = await DiscoverUPnPDevicesAsync(); if (!devices.Any()) return false; // This would require implementing SOAP calls to remove port mapping // For now, we'll simulate success _logger?.LogInformation("Simulating UPnP port mapping removal: {ExternalPort}", externalPort); return true; } catch (Exception ex) { _logger?.LogError(ex, "Failed to remove UPnP port mapping"); return false; } } private async Task> GetUPnPPortMappingsAsync() { try { var devices = await DiscoverUPnPDevicesAsync(); if (!devices.Any()) return new List(); // This would require implementing SOAP calls to get port mappings // For now, we'll return empty list return new List(); } catch (Exception ex) { _logger?.LogError(ex, "Failed to get UPnP port mappings"); return new List(); } } } public class UPnPDevice { public string ControlUrl { get; set; } = string.Empty; public IPAddress IpAddress { get; set; } = IPAddress.Any; public int Port { get; set; } } 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; } }