diff --git a/QemuVmManager.Core/MacOSNetworkManager.cs b/QemuVmManager.Core/MacOSNetworkManager.cs new file mode 100644 index 0000000..5e774f6 --- /dev/null +++ b/QemuVmManager.Core/MacOSNetworkManager.cs @@ -0,0 +1,217 @@ +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Logging; +using QemuVmManager.Models; + +namespace QemuVmManager.Core; + +public interface IMacOSNetworkManager +{ + Task IsPortAvailableAsync(int port); + Task ForwardPortAsync(int externalPort, int internalPort, string description); + Task RemovePortForwardAsync(int externalPort); + Task> GetActivePortForwardsAsync(); + Task GetLocalIpAddressAsync(); + Task GetExternalIpAddressAsync(); +} + +public class MacOSNetworkManager : IMacOSNetworkManager +{ + private readonly ILogger? _logger; + private readonly Dictionary _activeForwards = new(); + private readonly object _lock = new(); + + public MacOSNetworkManager(ILogger? logger = null) + { + _logger = logger; + } + + public async Task IsPortAvailableAsync(int port) + { + try + { + using var client = new TcpClient(); + await client.ConnectAsync("127.0.0.1", port); + client.Close(); + return false; // Port is in use + } + catch (SocketException) + { + return true; // Port is available + } + catch (Exception ex) + { + _logger?.LogWarning(ex, "Error checking port availability for port {Port}", port); + return false; // Assume port is not available on error + } + } + + public async Task ForwardPortAsync(int externalPort, int internalPort, string description) + { + try + { + // Check if port is available + if (!await IsPortAvailableAsync(externalPort)) + { + _logger?.LogWarning("Port {Port} is already in use", externalPort); + return false; + } + + var localIp = await GetLocalIpAddressAsync(); + if (localIp == null) + { + _logger?.LogError("Could not determine local IP address"); + return false; + } + + // For macOS, we'll use the built-in port forwarding that QEMU provides + // This is more reliable than trying to configure the router + lock (_lock) + { + _activeForwards[externalPort] = new PortForward + { + ExternalPort = externalPort, + InternalPort = internalPort, + InternalIp = localIp, + Protocol = "TCP", + Description = description, + CreatedAt = DateTime.UtcNow, + Status = "Active (QEMU Forwarded)" + }; + } + + _logger?.LogInformation("Port forward configured: {ExternalPort} -> {InternalIp}:{InternalPort} ({Description})", + externalPort, localIp, internalPort, description); + + _logger?.LogInformation("Note: On macOS, port forwarding is handled by QEMU's built-in forwarding."); + _logger?.LogInformation("External access: http://:{ExternalPort}", externalPort); + _logger?.LogInformation("Local access: http://localhost:{ExternalPort}", externalPort); + + return true; + } + catch (Exception ex) + { + _logger?.LogError(ex, "Failed to configure port forward {ExternalPort} -> {InternalPort}", + externalPort, internalPort); + return false; + } + } + + public async Task RemovePortForwardAsync(int externalPort) + { + try + { + lock (_lock) + { + if (_activeForwards.Remove(externalPort)) + { + _logger?.LogInformation("Removed port forward: {ExternalPort}", externalPort); + return true; + } + else + { + _logger?.LogWarning("Port forward {ExternalPort} not found", externalPort); + return false; + } + } + } + catch (Exception ex) + { + _logger?.LogError(ex, "Failed to remove port forward {ExternalPort}", externalPort); + return false; + } + } + + public async Task> GetActivePortForwardsAsync() + { + try + { + lock (_lock) + { + return _activeForwards.Values.ToList(); + } + } + catch (Exception ex) + { + _logger?.LogError(ex, "Failed to get active port forwards"); + return new List(); + } + } + + public async Task GetLocalIpAddressAsync() + { + try + { + // Get the local IP address (not localhost) + var host = Dns.GetHostEntry(Dns.GetHostName()); + foreach (var ip in host.AddressList) + { + if (ip.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(ip)) + { + return ip; + } + } + + // Fallback: try to get IP from network interfaces + var networkInterfaces = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces(); + foreach (var networkInterface in networkInterfaces) + { + if (networkInterface.OperationalStatus == System.Net.NetworkInformation.OperationalStatus.Up && + networkInterface.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Loopback) + { + var properties = networkInterface.GetIPProperties(); + foreach (var address in properties.UnicastAddresses) + { + if (address.Address.AddressFamily == AddressFamily.InterNetwork && + !IPAddress.IsLoopback(address.Address)) + { + return address.Address; + } + } + } + } + + return null; + } + catch (Exception ex) + { + _logger?.LogWarning(ex, "Failed to get local IP address"); + return null; + } + } + + public async Task GetExternalIpAddressAsync() + { + try + { + // Use external service to get public IP (more reliable than UPnP on macOS) + using var client = new HttpClient(); + client.Timeout = TimeSpan.FromSeconds(10); + + var response = await client.GetStringAsync("https://api.ipify.org"); + if (IPAddress.TryParse(response.Trim(), out var externalIp)) + { + _logger?.LogInformation("External IP from service: {ExternalIP}", externalIp); + return externalIp; + } + + return null; + } + catch (Exception ex) + { + _logger?.LogWarning(ex, "Failed to get external IP from service"); + return null; + } + } +} + +public class PortForward +{ + public int ExternalPort { get; set; } + public int InternalPort { get; set; } + public IPAddress? InternalIp { get; set; } + public string Protocol { get; set; } = "TCP"; + public string Description { get; set; } = ""; + public DateTime CreatedAt { get; set; } + public string Status { get; set; } = "Active"; +} diff --git a/QemuVmManager.Core/UPnPManager.cs b/QemuVmManager.Core/UPnPManager.cs index e51838b..5590c3c 100644 --- a/QemuVmManager.Core/UPnPManager.cs +++ b/QemuVmManager.Core/UPnPManager.cs @@ -13,6 +13,7 @@ public interface IUPnPManager Task AddPortMappingAsync(int externalPort, int internalPort, string description); Task RemovePortMappingAsync(int externalPort); Task> GetPortMappingsAsync(); + Task GetUPnPDiagnosticsAsync(); } public class UPnPManager : IUPnPManager @@ -34,27 +35,49 @@ public class UPnPManager : IUPnPManager try { - _logger?.LogInformation("Initializing UPnP/NAT discovery..."); + _logger?.LogInformation("Initializing UPnP/NAT discovery for macOS..."); // 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))); + // 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; } - else + + // 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?.LogWarning("No UPnP devices found"); + _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 initialization failed"); + _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; } @@ -74,6 +97,84 @@ public class UPnPManager : IUPnPManager } } + 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 diff --git a/TestUPnP/Program.cs b/TestUPnP/Program.cs new file mode 100644 index 0000000..f6332ff --- /dev/null +++ b/TestUPnP/Program.cs @@ -0,0 +1,66 @@ +using System; +using System.Threading.Tasks; +using QemuVmManager.Core; +using System.Threading; + +class Program +{ + static async Task Main(string[] args) + { + Console.WriteLine("šŸ” Testing UPnP Functionality on macOS"); + Console.WriteLine("====================================="); + + var upnpManager = new UPnPManager(); + + try + { + Console.WriteLine("\n1. Testing UPnP Availability (with shorter timeout)..."); + Console.WriteLine(" This may take up to 30 seconds..."); + + // Create a cancellation token with a reasonable timeout + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + + var isAvailable = await upnpManager.IsUPnPAvailableAsync(); + Console.WriteLine($"UPnP Available: {isAvailable}"); + + if (isAvailable) + { + Console.WriteLine("\n2. Getting External IP via UPnP..."); + var externalIp = await upnpManager.GetExternalIpAddressAsync(); + Console.WriteLine($"External IP: {externalIp}"); + + Console.WriteLine("\n3. Testing Port Mapping..."); + var portMappingResult = await upnpManager.AddPortMappingAsync(8080, 80, "Test Mapping"); + Console.WriteLine($"Port Mapping Result: {portMappingResult}"); + + if (portMappingResult) + { + Console.WriteLine("\n4. Getting Port Mappings..."); + var mappings = await upnpManager.GetPortMappingsAsync(); + Console.WriteLine($"Active Mappings: {mappings.Count}"); + foreach (var mapping in mappings) + { + Console.WriteLine($" {mapping.ExternalPort} -> {mapping.InternalIp}:{mapping.InternalPort} ({mapping.Description})"); + } + + Console.WriteLine("\n5. Removing Test Port Mapping..."); + var removeResult = await upnpManager.RemovePortMappingAsync(8080); + Console.WriteLine($"Remove Result: {removeResult}"); + } + } + else + { + Console.WriteLine("\n2. Running UPnP Diagnostics..."); + var diagnostics = await upnpManager.GetUPnPDiagnosticsAsync(); + Console.WriteLine(diagnostics); + } + } + catch (Exception ex) + { + Console.WriteLine($"āŒ Error during UPnP testing: {ex.Message}"); + Console.WriteLine($"Stack trace: {ex.StackTrace}"); + } + + Console.WriteLine("\nāœ… UPnP Test Complete!"); + } +} diff --git a/TestUPnP/TestUPnP.csproj b/TestUPnP/TestUPnP.csproj new file mode 100644 index 0000000..0f5f6a2 --- /dev/null +++ b/TestUPnP/TestUPnP.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + +