diff --git a/QemuVmManager.Core/QemuVmManager.Core.csproj b/QemuVmManager.Core/QemuVmManager.Core.csproj index fc954a4..0068913 100644 --- a/QemuVmManager.Core/QemuVmManager.Core.csproj +++ b/QemuVmManager.Core/QemuVmManager.Core.csproj @@ -11,8 +11,10 @@ + + diff --git a/QemuVmManager.Core/UPnPManager.cs b/QemuVmManager.Core/UPnPManager.cs index d167657..e51838b 100644 --- a/QemuVmManager.Core/UPnPManager.cs +++ b/QemuVmManager.Core/UPnPManager.cs @@ -2,6 +2,7 @@ using System.Net; using System.Net.Sockets; using Microsoft.Extensions.Logging; using QemuVmManager.Models; +using Open.Nat; namespace QemuVmManager.Core; @@ -19,23 +20,56 @@ 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 { - // Try to discover UPnP devices - var devices = await DiscoverUPnPDevicesAsync(); - return devices.Any(); + await InitializeAsync(); + return _natDevice != null; } catch (Exception ex) { - _logger?.LogWarning(ex, "UPnP discovery failed"); + _logger?.LogWarning(ex, "UPnP availability check failed"); return false; } } @@ -44,18 +78,22 @@ public class UPnPManager : IUPnPManager { try { - // Try multiple methods to get external IP - var externalIp = await GetExternalIpFromUPnPAsync(); - if (externalIp != null) - return externalIp; + await InitializeAsync(); + + if (_natDevice == null) + { + _logger?.LogWarning("No UPnP device available for external IP lookup"); + return await GetExternalIpFromServiceAsync(); + } - // Fallback to external service - return await GetExternalIpFromServiceAsync(); + var externalIp = await _natDevice.GetExternalIPAsync(); + _logger?.LogInformation("External IP from UPnP: {ExternalIP}", externalIp); + return externalIp; } catch (Exception ex) { - _logger?.LogError(ex, "Failed to get external IP address"); - return null; + _logger?.LogWarning(ex, "Failed to get external IP from UPnP, falling back to external service"); + return await GetExternalIpFromServiceAsync(); } } @@ -63,6 +101,14 @@ public class UPnPManager : IUPnPManager { try { + await InitializeAsync(); + + if (_natDevice == null) + { + _logger?.LogError("No UPnP device available for port mapping"); + return false; + } + var localIp = GetLocalIpAddress(); if (localIp == null) { @@ -70,26 +116,29 @@ public class UPnPManager : IUPnPManager return false; } - var success = await AddUPnPPortMappingAsync(externalPort, internalPort, localIp, description); - if (success) + // Create the port mapping + var mapping = new Mapping(Protocol.Tcp, internalPort, externalPort, description); + + // Add the mapping + await _natDevice.CreatePortMapAsync(mapping); + + lock (_lock) { - lock (_lock) + _activeMappings[externalPort] = new PortMapping { - _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); + ExternalPort = externalPort, + InternalPort = internalPort, + InternalIp = localIp, + Protocol = "TCP", + Description = description, + CreatedAt = DateTime.UtcNow + }; } - - return success; + + _logger?.LogInformation("Successfully added port mapping: {ExternalPort} -> {InternalIp}:{InternalPort} ({Description})", + externalPort, localIp, internalPort, description); + + return true; } catch (Exception ex) { @@ -103,17 +152,34 @@ public class UPnPManager : IUPnPManager { try { - var success = await RemoveUPnPPortMappingAsync(externalPort); - if (success) + await InitializeAsync(); + + if (_natDevice == null) { - lock (_lock) - { - _activeMappings.Remove(externalPort); - } - _logger?.LogInformation("Removed port mapping: {ExternalPort}", externalPort); + _logger?.LogError("No UPnP device available for port mapping removal"); + return false; } - return success; + 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) { @@ -126,23 +192,31 @@ public class UPnPManager : IUPnPManager { try { - var mappings = await GetUPnPPortMappingsAsync(); - lock (_lock) + await InitializeAsync(); + + if (_natDevice == null) { - // Merge with our active mappings - foreach (var mapping in _activeMappings.Values) + _logger?.LogWarning("No UPnP device available for port mapping retrieval"); + lock (_lock) { - if (!mappings.Any(m => m.ExternalPort == mapping.ExternalPort)) - { - mappings.Add(mapping); - } + 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"); + _logger?.LogError(ex, "Failed to get port mappings from UPnP device"); lock (_lock) { return _activeMappings.Values.ToList(); @@ -150,98 +224,6 @@ public class UPnPManager : IUPnPManager } } - 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 @@ -264,17 +246,21 @@ public class UPnPManager : IUPnPManager 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 + 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 services"); + _logger?.LogWarning(ex, "Failed to get external IP from any service"); } return null; @@ -295,75 +281,6 @@ public class UPnPManager : IUPnPManager 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