Compare commits

12 Commits
master ... main

10 changed files with 2235 additions and 26 deletions

View File

@@ -123,6 +123,16 @@ class Program
case "metrics":
await ShowMetrics(arguments);
break;
case "network":
await HandleNetworkCommandsAsync(arguments);
break;
case "port":
await HandlePortCommandsAsync(arguments);
break;
case "config":
await HandleConfigCommandsAsync(vmService, arguments);
break;
case "exit":
case "quit":
System.Console.WriteLine("Goodbye!");
@@ -159,6 +169,8 @@ class Program
System.Console.WriteLine(" diagnose - Diagnose system and QEMU installation");
System.Console.WriteLine(" monitor <name> [start|stop|status] - Performance monitoring");
System.Console.WriteLine(" metrics <name> [current|history] - Show performance metrics");
System.Console.WriteLine(" network <command> - Network management (bridge, interfaces)");
System.Console.WriteLine(" port <command> - Port forwarding management");
System.Console.WriteLine(" help - Show this help");
System.Console.WriteLine(" exit/quit - Exit the application");
}
@@ -813,4 +825,543 @@ class Program
var input = System.Console.ReadLine()?.Trim();
return string.IsNullOrEmpty(input) ? defaultValue : input;
}
static async Task HandleNetworkCommandsAsync(string[] arguments)
{
if (arguments.Length == 0)
{
System.Console.WriteLine("Network management commands:");
System.Console.WriteLine(" network bridges - List available bridges");
System.Console.WriteLine(" network interfaces - List network interfaces");
System.Console.WriteLine(" network create <name> [interface] - Create a bridge");
System.Console.WriteLine(" network delete <name> - Delete a bridge");
System.Console.WriteLine(" network config <name> <ip> <mask> - Configure bridge IP");
System.Console.WriteLine(" network vm <vm-name> bridge <name> - Configure VM for bridge networking");
System.Console.WriteLine(" network vm <vm-name> user - Configure VM for user networking");
return;
}
var networkManager = new NetworkManager();
var command = arguments[0].ToLower();
try
{
switch (command)
{
case "bridges":
await ShowBridgesAsync(networkManager);
break;
case "interfaces":
await ShowInterfacesAsync(networkManager);
break;
case "create":
if (arguments.Length < 2)
{
System.Console.WriteLine("Usage: network create <bridge-name> [interface-name]");
return;
}
await CreateBridgeAsync(networkManager, arguments[1], arguments.Length > 2 ? arguments[2] : null);
break;
case "delete":
if (arguments.Length < 2)
{
System.Console.WriteLine("Usage: network delete <bridge-name>");
return;
}
await DeleteBridgeAsync(networkManager, arguments[1]);
break;
case "config":
if (arguments.Length < 4)
{
System.Console.WriteLine("Usage: network config <bridge-name> <ip-address> <netmask>");
System.Console.WriteLine("Example: network config virbr0 192.168.122.1 24");
return;
}
await ConfigureBridgeAsync(networkManager, arguments[1], arguments[2], arguments[3]);
break;
case "vm":
if (arguments.Length < 3)
{
System.Console.WriteLine("Usage: network vm <vm-name> <bridge|user> [bridge-name]");
return;
}
await ConfigureVmNetworkAsync(arguments[1], arguments[2], arguments.Length > 3 ? arguments[3] : null);
break;
default:
System.Console.WriteLine($"Unknown network command: {command}");
System.Console.WriteLine("Type 'network' for available network commands");
break;
}
}
catch (Exception ex)
{
System.Console.WriteLine($"Error: {ex.Message}");
}
}
static async Task ShowBridgesAsync(NetworkManager networkManager)
{
System.Console.WriteLine("=== Available Network Bridges ===");
var bridges = await networkManager.GetAvailableBridgesAsync();
if (bridges.Any())
{
foreach (var bridge in bridges)
{
var ipAddress = await networkManager.GetBridgeIpAddressAsync(bridge);
System.Console.WriteLine($" {bridge}: {ipAddress ?? "No IP configured"}");
}
}
else
{
System.Console.WriteLine("No bridges found");
System.Console.WriteLine("Use 'network create <name>' to create a bridge");
}
}
static async Task ShowInterfacesAsync(NetworkManager networkManager)
{
System.Console.WriteLine("=== Available Network Interfaces ===");
var interfaces = await networkManager.GetNetworkInterfacesAsync();
if (interfaces.Any())
{
foreach (var iface in interfaces)
{
System.Console.WriteLine($" {iface}");
}
}
else
{
System.Console.WriteLine("No network interfaces found");
}
}
static async Task CreateBridgeAsync(NetworkManager networkManager, string bridgeName, string? interfaceName)
{
System.Console.WriteLine($"Creating bridge '{bridgeName}'...");
if (!string.IsNullOrEmpty(interfaceName))
{
System.Console.WriteLine($"Adding interface '{interfaceName}' to bridge...");
}
var success = await networkManager.CreateBridgeAsync(bridgeName, interfaceName);
if (success)
{
System.Console.WriteLine($"✅ Bridge '{bridgeName}' created successfully");
if (!string.IsNullOrEmpty(interfaceName))
{
System.Console.WriteLine($"✅ Interface '{interfaceName}' added to bridge");
}
}
else
{
System.Console.WriteLine($"❌ Failed to create bridge '{bridgeName}'");
System.Console.WriteLine("Note: Bridge creation may require root privileges");
}
}
static async Task DeleteBridgeAsync(NetworkManager networkManager, string bridgeName)
{
System.Console.WriteLine($"Deleting bridge '{bridgeName}'...");
var success = await networkManager.DeleteBridgeAsync(bridgeName);
if (success)
{
System.Console.WriteLine($"✅ Bridge '{bridgeName}' deleted successfully");
}
else
{
System.Console.WriteLine($"❌ Failed to delete bridge '{bridgeName}'");
System.Console.WriteLine("Note: Bridge deletion may require root privileges");
}
}
static async Task ConfigureBridgeAsync(NetworkManager networkManager, string bridgeName, string ipAddress, string netmask)
{
System.Console.WriteLine($"Configuring bridge '{bridgeName}' with IP {ipAddress}/{netmask}...");
var success = await networkManager.ConfigureBridgeAsync(bridgeName, ipAddress, netmask);
if (success)
{
System.Console.WriteLine($"✅ Bridge '{bridgeName}' configured successfully");
}
else
{
System.Console.WriteLine($"❌ Failed to configure bridge '{bridgeName}'");
System.Console.WriteLine("Note: Bridge configuration may require root privileges");
}
}
static async Task ConfigureVmNetworkAsync(string vmName, string networkType, string? bridgeName)
{
var vmService = new VmManagementService();
var config = vmService.GetVmConfiguration(vmName);
if (config == null)
{
System.Console.WriteLine($"VM '{vmName}' not found");
return;
}
if (networkType == "bridge")
{
if (string.IsNullOrEmpty(bridgeName))
{
System.Console.WriteLine("Bridge name required for bridge networking");
System.Console.WriteLine("Usage: network vm <vm-name> bridge <bridge-name>");
return;
}
// Configure for bridge networking
config.Network.Interfaces.Clear();
config.Network.Interfaces.Add(new NetworkInterfaceConfiguration
{
Type = "bridge",
Model = "virtio-net-pci",
Bridge = bridgeName
});
System.Console.WriteLine($"✅ VM '{vmName}' configured for bridge networking on '{bridgeName}'");
}
else if (networkType == "user")
{
// Configure for user networking (NAT)
config.Network.Interfaces.Clear();
config.Network.Interfaces.Add(new NetworkInterfaceConfiguration
{
Type = "user",
Model = "virtio-net-pci"
});
System.Console.WriteLine($"✅ VM '{vmName}' configured for user networking (NAT)");
}
else
{
System.Console.WriteLine($"Unknown network type: {networkType}");
System.Console.WriteLine("Supported types: bridge, user");
return;
}
// Save the updated configuration
await vmService.UpdateVmConfigurationAsync(vmName, config);
System.Console.WriteLine($"Configuration saved for VM '{vmName}'");
}
static async Task HandlePortCommandsAsync(string[] arguments)
{
if (arguments.Length == 0)
{
System.Console.WriteLine("Port forwarding commands:");
System.Console.WriteLine(" port forward <vm-name> <host-port> <vm-port> [protocol] - Forward host port to VM");
System.Console.WriteLine(" port list <vm-name> - List active port forwards");
System.Console.WriteLine(" port remove <vm-name> <host-port> - Remove port forward");
System.Console.WriteLine(" port status <vm-name> - Show port forwarding status");
return;
}
var command = arguments[0].ToLower();
try
{
switch (command)
{
case "forward":
if (arguments.Length < 4)
{
System.Console.WriteLine("Usage: port forward <vm-name> <host-port> <vm-port> [protocol]");
System.Console.WriteLine("Example: port forward ubuntu-desktop 8080 80 tcp");
return;
}
await ForwardPortAsync(arguments[1], int.Parse(arguments[2]), int.Parse(arguments[3]),
arguments.Length > 4 ? arguments[4] : "tcp");
break;
case "list":
if (arguments.Length < 2)
{
System.Console.WriteLine("Usage: port list <vm-name>");
return;
}
await ListPortForwardsAsync(arguments[1]);
break;
case "remove":
if (arguments.Length < 3)
{
System.Console.WriteLine("Usage: port remove <vm-name> <host-port>");
return;
}
await RemovePortForwardAsync(arguments[1], int.Parse(arguments[2]));
break;
case "status":
if (arguments.Length < 2)
{
System.Console.WriteLine("Usage: port status <vm-name>");
return;
}
await ShowPortForwardStatusAsync(arguments[1]);
break;
default:
System.Console.WriteLine($"Unknown port command: {command}");
System.Console.WriteLine("Type 'port' for available port commands");
break;
}
}
catch (Exception ex)
{
System.Console.WriteLine($"Error: {ex.Message}");
}
}
static async Task ForwardPortAsync(string vmName, int hostPort, int vmPort, string protocol)
{
var vmService = new VmManagementService();
var config = vmService.GetVmConfiguration(vmName);
if (config == null)
{
System.Console.WriteLine($"VM '{vmName}' not found");
return;
}
try
{
// Check if port is already in use
if (IsPortInUse(hostPort))
{
System.Console.WriteLine($"❌ Port {hostPort} is already in use on the host");
return;
}
// Create port forward entry
var portForward = new QemuVmManager.Models.PortForward
{
VmName = vmName,
HostPort = hostPort,
VmPort = vmPort,
Protocol = protocol.ToUpper(),
Created = DateTime.UtcNow
};
// Save port forward configuration
await SavePortForwardAsync(portForward);
System.Console.WriteLine($"✅ Port forward created: {hostPort}:{vmPort} ({protocol})");
System.Console.WriteLine($" Host: localhost:{hostPort} → VM: {vmName}:{vmPort}");
if (OperatingSystem.IsMacOS())
{
System.Console.WriteLine($" Note: On macOS, VMs use socket networking on ports 10000+");
System.Console.WriteLine($" You may need to configure your application to listen on the socket port");
}
}
catch (Exception ex)
{
System.Console.WriteLine($"❌ Failed to create port forward: {ex.Message}");
}
}
static async Task ListPortForwardsAsync(string vmName)
{
var portForwards = await LoadPortForwardsAsync();
var vmForwards = portForwards.Where(pf => pf.VmName == vmName).ToList();
if (vmForwards.Any())
{
System.Console.WriteLine($"=== Port Forwards for VM '{vmName}' ===");
System.Console.WriteLine($"{"Host Port",-12} {"VM Port",-8} {"Protocol",-8} {"Status",-10}");
System.Console.WriteLine(new string('-', 40));
foreach (var pf in vmForwards)
{
var status = IsPortInUse(pf.HostPort) ? "Active" : "Inactive";
System.Console.WriteLine($"{pf.HostPort,-12} {pf.VmPort,-8} {pf.Protocol,-8} {status,-10}");
}
}
else
{
System.Console.WriteLine($"No port forwards configured for VM '{vmName}'");
}
}
static async Task RemovePortForwardAsync(string vmName, int hostPort)
{
var portForwards = await LoadPortForwardsAsync();
var toRemove = portForwards.FirstOrDefault(pf => pf.VmName == vmName && pf.HostPort == hostPort);
if (toRemove != null)
{
portForwards.Remove(toRemove);
await SavePortForwardsAsync(portForwards);
System.Console.WriteLine($"✅ Port forward {hostPort} removed for VM '{vmName}'");
}
else
{
System.Console.WriteLine($"Port forward {hostPort} not found for VM '{vmName}'");
}
}
static async Task ShowPortForwardStatusAsync(string vmName)
{
var portForwards = await LoadPortForwardsAsync();
var vmForwards = portForwards.Where(pf => pf.VmName == vmName).ToList();
if (vmForwards.Any())
{
System.Console.WriteLine($"=== Port Forward Status for VM '{vmName}' ===");
foreach (var pf in vmForwards)
{
var status = IsPortInUse(pf.HostPort) ? "✅ Active" : "❌ Inactive";
System.Console.WriteLine($"Port {pf.HostPort}:{pf.VmPort} ({pf.Protocol}) - {status}");
}
}
else
{
System.Console.WriteLine($"No port forwards configured for VM '{vmName}'");
}
}
static bool IsPortInUse(int port)
{
try
{
using var client = new System.Net.Sockets.TcpClient();
client.Connect("127.0.0.1", port);
return true;
}
catch
{
return false;
}
}
static async Task<List<QemuVmManager.Models.PortForward>> LoadPortForwardsAsync()
{
var configFile = "port-forwards.json";
if (File.Exists(configFile))
{
try
{
var json = await File.ReadAllTextAsync(configFile);
return System.Text.Json.JsonSerializer.Deserialize<List<QemuVmManager.Models.PortForward>>(json) ?? new List<QemuVmManager.Models.PortForward>();
}
catch
{
return new List<QemuVmManager.Models.PortForward>();
}
}
return new List<QemuVmManager.Models.PortForward>();
}
static async Task SavePortForwardsAsync(List<QemuVmManager.Models.PortForward> portForwards)
{
var configFile = "port-forwards.json";
var json = System.Text.Json.JsonSerializer.Serialize(portForwards, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync(configFile, json);
}
static async Task SavePortForwardAsync(QemuVmManager.Models.PortForward portForward)
{
var portForwards = await LoadPortForwardsAsync();
portForwards.Add(portForward);
await SavePortForwardsAsync(portForwards);
}
static async Task HandleConfigCommandsAsync(VmManagementService vmService, string[] arguments)
{
if (arguments.Length < 3)
{
System.Console.WriteLine("Usage: config <vm-name> <setting> <value>");
System.Console.WriteLine("Settings:");
System.Console.WriteLine(" display <type> [port] - Set display type (gtk, vnc, spice)");
System.Console.WriteLine(" vga <type> - Set VGA type (std, virtio, qxl)");
System.Console.WriteLine(" memory <size> - Set memory size in GB");
System.Console.WriteLine(" cpu <cores> - Set CPU cores");
System.Console.WriteLine("Examples:");
System.Console.WriteLine(" config ubuntu-desktop display vnc 5900");
System.Console.WriteLine(" config ubuntu-desktop display gtk");
System.Console.WriteLine(" config ubuntu-desktop vga std");
return;
}
var vmName = arguments[0];
var setting = arguments[1].ToLower();
var value = arguments[2];
var config = vmService.GetVmConfiguration(vmName);
if (config == null)
{
System.Console.WriteLine($"VM '{vmName}' not found.");
return;
}
try
{
switch (setting)
{
case "display":
config.Display.Type = value;
if (arguments.Length > 3 && value == "vnc")
{
config.Display.SpicePort = int.Parse(arguments[3]);
}
System.Console.WriteLine($"Display type set to '{value}' for VM '{vmName}'");
break;
case "vga":
config.Display.Vga = value;
System.Console.WriteLine($"VGA type set to '{value}' for VM '{vmName}'");
break;
case "memory":
if (int.TryParse(value, out int memorySize))
{
config.Memory.Size = memorySize;
System.Console.WriteLine($"Memory set to {memorySize}GB for VM '{vmName}'");
}
else
{
System.Console.WriteLine("Invalid memory size. Please specify a number.");
}
break;
case "cpu":
if (int.TryParse(value, out int cpuCores))
{
config.Cpu.Cores = cpuCores;
System.Console.WriteLine($"CPU cores set to {cpuCores} for VM '{vmName}'");
}
else
{
System.Console.WriteLine("Invalid CPU cores. Please specify a number.");
}
break;
default:
System.Console.WriteLine($"Unknown setting '{setting}'");
break;
}
// Save the updated configuration
await vmService.UpdateVmConfigurationAsync(vmName, config);
System.Console.WriteLine($"Configuration updated successfully for VM '{vmName}'");
}
catch (Exception ex)
{
System.Console.WriteLine($"Error updating configuration: {ex.Message}");
}
}
}

View File

@@ -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<bool> IsPortAvailableAsync(int port);
Task<bool> ForwardPortAsync(int externalPort, int internalPort, string description);
Task<bool> RemovePortForwardAsync(int externalPort);
Task<List<PortForward>> GetActivePortForwardsAsync();
Task<IPAddress?> GetLocalIpAddressAsync();
Task<IPAddress?> GetExternalIpAddressAsync();
}
public class MacOSNetworkManager : IMacOSNetworkManager
{
private readonly ILogger<MacOSNetworkManager>? _logger;
private readonly Dictionary<int, PortForward> _activeForwards = new();
private readonly object _lock = new();
public MacOSNetworkManager(ILogger<MacOSNetworkManager>? logger = null)
{
_logger = logger;
}
public async Task<bool> 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<bool> 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://<YOUR_EXTERNAL_IP>:{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<bool> 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<List<PortForward>> GetActivePortForwardsAsync()
{
try
{
lock (_lock)
{
return _activeForwards.Values.ToList();
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to get active port forwards");
return new List<PortForward>();
}
}
public async Task<IPAddress?> 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<IPAddress?> 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";
}

View File

@@ -0,0 +1,638 @@
using System.Diagnostics;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
namespace QemuVmManager.Core;
public interface INetworkManager
{
Task<bool> IsBridgeAvailableAsync(string bridgeName);
Task<bool> CreateBridgeAsync(string bridgeName, string? interfaceName = null);
Task<bool> DeleteBridgeAsync(string bridgeName);
Task<List<string>> GetAvailableBridgesAsync();
Task<List<string>> GetNetworkInterfacesAsync();
Task<bool> AddInterfaceToBridgeAsync(string bridgeName, string interfaceName);
Task<bool> RemoveInterfaceFromBridgeAsync(string bridgeName, string interfaceName);
Task<string?> GetBridgeIpAddressAsync(string bridgeName);
Task<bool> ConfigureBridgeAsync(string bridgeName, string ipAddress, string netmask);
}
public class NetworkManager : INetworkManager
{
private readonly ILogger<NetworkManager>? _logger;
public NetworkManager(ILogger<NetworkManager>? logger = null)
{
_logger = logger;
}
public async Task<bool> IsBridgeAvailableAsync(string bridgeName)
{
try
{
if (OperatingSystem.IsLinux())
{
return await CheckLinuxBridgeAsync(bridgeName);
}
else if (OperatingSystem.IsWindows())
{
return await CheckWindowsBridgeAsync(bridgeName);
}
else if (OperatingSystem.IsMacOS())
{
return await CheckMacOSBridgeAsync(bridgeName);
}
return false;
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Failed to check bridge availability for {BridgeName}", bridgeName);
return false;
}
}
public async Task<bool> CreateBridgeAsync(string bridgeName, string? interfaceName = null)
{
try
{
if (OperatingSystem.IsLinux())
{
return await CreateLinuxBridgeAsync(bridgeName, interfaceName);
}
else if (OperatingSystem.IsWindows())
{
return await CreateWindowsBridgeAsync(bridgeName, interfaceName);
}
else if (OperatingSystem.IsMacOS())
{
return await CreateMacOSBridgeAsync(bridgeName, interfaceName);
}
return false;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to create bridge {BridgeName}", bridgeName);
return false;
}
}
public async Task<bool> DeleteBridgeAsync(string bridgeName)
{
try
{
if (OperatingSystem.IsLinux())
{
return await DeleteLinuxBridgeAsync(bridgeName);
}
else if (OperatingSystem.IsWindows())
{
return await DeleteWindowsBridgeAsync(bridgeName);
}
else if (OperatingSystem.IsMacOS())
{
return await DeleteMacOSBridgeAsync(bridgeName);
}
return false;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to delete bridge {BridgeName}", bridgeName);
return false;
}
}
public async Task<List<string>> GetAvailableBridgesAsync()
{
var bridges = new List<string>();
try
{
if (OperatingSystem.IsLinux())
{
bridges.AddRange(await GetLinuxBridgesAsync());
}
else if (OperatingSystem.IsWindows())
{
bridges.AddRange(await GetWindowsBridgesAsync());
}
else if (OperatingSystem.IsMacOS())
{
bridges.AddRange(await GetMacOSBridgesAsync());
}
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Failed to get available bridges");
}
return bridges;
}
public async Task<List<string>> GetNetworkInterfacesAsync()
{
var interfaces = new List<string>();
try
{
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
foreach (var ni in networkInterfaces)
{
// Filter out loopback and virtual interfaces
if (ni.NetworkInterfaceType != NetworkInterfaceType.Loopback &&
ni.OperationalStatus == OperationalStatus.Up &&
!ni.Name.Contains("vEthernet") &&
!ni.Name.Contains("VirtualBox") &&
!ni.Name.Contains("VMware"))
{
interfaces.Add(ni.Name);
}
}
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Failed to get network interfaces");
}
return interfaces;
}
public async Task<bool> AddInterfaceToBridgeAsync(string bridgeName, string interfaceName)
{
try
{
if (OperatingSystem.IsLinux())
{
return await AddInterfaceToLinuxBridgeAsync(bridgeName, interfaceName);
}
else if (OperatingSystem.IsWindows())
{
return await AddInterfaceToWindowsBridgeAsync(bridgeName, interfaceName);
}
else if (OperatingSystem.IsMacOS())
{
return await AddInterfaceToMacOSBridgeAsync(bridgeName, interfaceName);
}
return false;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to add interface {InterfaceName} to bridge {BridgeName}", interfaceName, bridgeName);
return false;
}
}
public async Task<bool> RemoveInterfaceFromBridgeAsync(string bridgeName, string interfaceName)
{
try
{
if (OperatingSystem.IsLinux())
{
return await RemoveInterfaceFromLinuxBridgeAsync(bridgeName, interfaceName);
}
else if (OperatingSystem.IsWindows())
{
return await RemoveInterfaceFromWindowsBridgeAsync(bridgeName, interfaceName);
}
else if (OperatingSystem.IsMacOS())
{
return await RemoveInterfaceFromMacOSBridgeAsync(bridgeName, interfaceName);
}
return false;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to remove interface {InterfaceName} from bridge {BridgeName}", interfaceName, bridgeName);
return false;
}
}
public async Task<string?> GetBridgeIpAddressAsync(string bridgeName)
{
try
{
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
foreach (var ni in networkInterfaces)
{
if (ni.Name.Equals(bridgeName, StringComparison.OrdinalIgnoreCase))
{
var ipProps = ni.GetIPProperties();
var unicastAddresses = ipProps.UnicastAddresses;
foreach (var addr in unicastAddresses)
{
if (addr.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
{
return addr.Address.ToString();
}
}
}
}
return null;
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Failed to get IP address for bridge {BridgeName}", bridgeName);
return null;
}
}
public async Task<bool> ConfigureBridgeAsync(string bridgeName, string ipAddress, string netmask)
{
try
{
if (OperatingSystem.IsLinux())
{
return await ConfigureLinuxBridgeAsync(bridgeName, ipAddress, netmask);
}
else if (OperatingSystem.IsWindows())
{
return await ConfigureWindowsBridgeAsync(bridgeName, ipAddress, netmask);
}
else if (OperatingSystem.IsMacOS())
{
return await ConfigureMacOSBridgeAsync(bridgeName, ipAddress, netmask);
}
return false;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to configure bridge {BridgeName}", bridgeName);
return false;
}
}
// Linux-specific implementations
private async Task<bool> CheckLinuxBridgeAsync(string bridgeName)
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "brctl",
Arguments = $"show {bridgeName}",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
}
};
var started = process.Start();
if (!started) return false;
await process.WaitForExitAsync();
return process.ExitCode == 0;
}
private async Task<bool> CreateLinuxBridgeAsync(string bridgeName, string? interfaceName)
{
// Create bridge
var createProcess = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "sudo",
Arguments = $"brctl addbr {bridgeName}",
UseShellExecute = false,
CreateNoWindow = true
}
};
var started = createProcess.Start();
if (!started) return false;
await createProcess.WaitForExitAsync();
if (createProcess.ExitCode != 0) return false;
// Add interface if specified
if (!string.IsNullOrEmpty(interfaceName))
{
return await AddInterfaceToLinuxBridgeAsync(bridgeName, interfaceName);
}
return true;
}
private async Task<bool> DeleteLinuxBridgeAsync(string bridgeName)
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "sudo",
Arguments = $"brctl delbr {bridgeName}",
UseShellExecute = false,
CreateNoWindow = true
}
};
var started = process.Start();
if (!started) return false;
await process.WaitForExitAsync();
return process.ExitCode == 0;
}
private async Task<List<string>> GetLinuxBridgesAsync()
{
var bridges = new List<string>();
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "brctl",
Arguments = "show",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
}
};
var started = process.Start();
if (!started) return bridges;
var output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
if (process.ExitCode == 0)
{
var lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines.Skip(1)) // Skip header
{
var parts = line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length > 0)
{
bridges.Add(parts[0]);
}
}
}
return bridges;
}
private async Task<bool> AddInterfaceToLinuxBridgeAsync(string bridgeName, string interfaceName)
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "sudo",
Arguments = $"brctl addif {bridgeName} {interfaceName}",
UseShellExecute = false,
CreateNoWindow = true
}
};
var started = process.Start();
if (!started) return false;
await process.WaitForExitAsync();
return process.ExitCode == 0;
}
private async Task<bool> RemoveInterfaceFromLinuxBridgeAsync(string bridgeName, string interfaceName)
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "sudo",
Arguments = $"brctl delif {bridgeName} {interfaceName}",
UseShellExecute = false,
CreateNoWindow = true
}
};
var started = process.Start();
if (!started) return false;
await process.WaitForExitAsync();
return process.ExitCode == 0;
}
private async Task<bool> ConfigureLinuxBridgeAsync(string bridgeName, string ipAddress, string netmask)
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "sudo",
Arguments = $"ip addr add {ipAddress}/{netmask} dev {bridgeName}",
UseShellExecute = false,
CreateNoWindow = true
}
};
var started = process.Start();
if (!started) return false;
await process.WaitForExitAsync();
return process.ExitCode == 0;
}
// Windows-specific implementations (simplified - would need more complex implementation)
private async Task<bool> CheckWindowsBridgeAsync(string bridgeName)
{
// Windows bridge checking would require PowerShell or WMI
// For now, return false as Windows bridge setup is more complex
return false;
}
private async Task<bool> CreateWindowsBridgeAsync(string bridgeName, string? interfaceName)
{
// Windows bridge creation requires PowerShell or Hyper-V
// For now, return false as this requires more complex implementation
return false;
}
private async Task<bool> DeleteWindowsBridgeAsync(string bridgeName)
{
// Windows bridge deletion requires PowerShell or Hyper-V
return false;
}
private async Task<List<string>> GetWindowsBridgesAsync()
{
// Windows bridge enumeration would require PowerShell or WMI
return new List<string>();
}
private async Task<bool> AddInterfaceToWindowsBridgeAsync(string bridgeName, string interfaceName)
{
return false;
}
private async Task<bool> RemoveInterfaceFromWindowsBridgeAsync(string bridgeName, string interfaceName)
{
return false;
}
private async Task<bool> ConfigureWindowsBridgeAsync(string bridgeName, string ipAddress, string netmask)
{
return false;
}
// macOS-specific implementations
private async Task<bool> CheckMacOSBridgeAsync(string bridgeName)
{
try
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "ifconfig",
Arguments = bridgeName,
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
}
};
var started = process.Start();
if (!started) return false;
await process.WaitForExitAsync();
return process.ExitCode == 0;
}
catch
{
return false;
}
}
private async Task<bool> CreateMacOSBridgeAsync(string bridgeName, string? interfaceName)
{
try
{
// On macOS, bridge creation is complex and typically requires system configuration
// For now, we'll check if the bridge already exists and return true if it does
var exists = await CheckMacOSBridgeAsync(bridgeName);
if (exists)
{
_logger?.LogInformation("Bridge {BridgeName} already exists on macOS", bridgeName);
return true;
}
_logger?.LogWarning("Bridge creation on macOS requires manual system configuration");
_logger?.LogInformation("You can use existing bridge0 or create bridges manually");
return false;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to create bridge {BridgeName} on macOS", bridgeName);
return false;
}
}
private async Task<bool> DeleteMacOSBridgeAsync(string bridgeName)
{
// Bridge deletion on macOS requires system configuration
_logger?.LogWarning("Bridge deletion on macOS requires manual system configuration");
return false;
}
private async Task<List<string>> GetMacOSBridgesAsync()
{
var bridges = new List<string>();
try
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "ifconfig",
Arguments = "-a",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
}
};
var started = process.Start();
if (!started) return bridges;
var output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
if (process.ExitCode == 0)
{
var lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.Contains("bridge") && line.Contains(":"))
{
var parts = line.Split(':');
if (parts.Length > 0)
{
var bridgeName = parts[0].Trim();
if (!string.IsNullOrEmpty(bridgeName) && !bridges.Contains(bridgeName))
{
bridges.Add(bridgeName);
}
}
}
}
}
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Failed to get macOS bridges");
}
return bridges;
}
private async Task<bool> AddInterfaceToMacOSBridgeAsync(string bridgeName, string interfaceName)
{
// Interface management on macOS bridges requires system configuration
_logger?.LogWarning("Interface management on macOS bridges requires manual system configuration");
return false;
}
private async Task<bool> RemoveInterfaceFromMacOSBridgeAsync(string bridgeName, string interfaceName)
{
// Interface management on macOS bridges requires system configuration
_logger?.LogWarning("Interface management on macOS bridges requires manual system configuration");
return false;
}
private async Task<bool> ConfigureMacOSBridgeAsync(string bridgeName, string ipAddress, string netmask)
{
try
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "sudo",
Arguments = $"ifconfig {bridgeName} {ipAddress} netmask 255.255.255.0",
UseShellExecute = false,
CreateNoWindow = true
}
};
var started = process.Start();
if (!started) return false;
await process.WaitForExitAsync();
return process.ExitCode == 0;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to configure bridge {BridgeName} on macOS", bridgeName);
return false;
}
}
}

View File

@@ -1,5 +1,8 @@
using QemuVmManager.Models;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Linq;
namespace QemuVmManager.Core;
@@ -30,8 +33,16 @@ public class QemuCommandBuilder
// Storage configuration
AddStorageConfiguration();
// OS Detection Debug
Console.WriteLine($"Debug: OS Detection - IsMacOS: {OperatingSystem.IsMacOS()}, IsLinux: {OperatingSystem.IsLinux()}, IsWindows: {OperatingSystem.IsWindows()}");
// Network configuration
Console.WriteLine("Debug: About to add network configuration");
AddNetworkConfiguration();
Console.WriteLine("Debug: Network configuration added");
// Add port forwarding for bridge networking if needed
AddPortForwardingForBridge();
// Display configuration
AddDisplayConfiguration();
@@ -73,6 +84,425 @@ public class QemuCommandBuilder
return string.Join(" ", _arguments);
}
private void AddPortForwardingForBridge()
{
var customForwards = GetCustomPortForwards();
if (string.IsNullOrEmpty(customForwards))
return;
Console.WriteLine($"Info: Bridge networking detected - port forwarding will be configured via iptables");
Console.WriteLine($"Info: Custom port forwards: {customForwards}");
// For bridge networking, we need to wait for the VM to get an IP
// Port forwarding will be configured after the VM starts and gets an IP
Console.WriteLine($"");
Console.WriteLine($"📝 Bridge Networking Port Forwarding:");
Console.WriteLine($" Port forwarding will be configured automatically once the VM gets an IP address.");
Console.WriteLine($" The system will use iptables to forward traffic from host ports to the VM.");
Console.WriteLine($"");
Console.WriteLine($" Configured forwards:");
foreach (var pf in GetPortForwardList())
{
Console.WriteLine($" • Port {pf.HostPort} → VM:{pf.VmPort} ({pf.Protocol})");
}
Console.WriteLine($"");
Console.WriteLine($" To configure port forwarding manually after VM starts:");
Console.WriteLine($" sudo iptables -t nat -A PREROUTING -p tcp --dport 3300 -j DNAT --to-destination <VM_IP>:80");
Console.WriteLine($" sudo iptables -A FORWARD -p tcp --dport 80 -d <VM_IP> -j ACCEPT");
Console.WriteLine($" (Replace <VM_IP> with the VM's actual IP address from the bridge)");
}
private void ConfigureIptablesPortForwarding()
{
try
{
var configFile = "port-forwards.json";
if (!File.Exists(configFile))
return;
var json = File.ReadAllText(configFile);
var portForwards = System.Text.Json.JsonSerializer.Deserialize<List<QemuVmManager.Models.PortForward>>(json) ?? new List<QemuVmManager.Models.PortForward>();
// Filter port forwards for this VM
var vmForwards = portForwards.Where(pf => pf.VmName == _config.Name).ToList();
if (!vmForwards.Any())
return;
Console.WriteLine($"🔧 Automatically configuring iptables port forwarding for VM '{_config.Name}'...");
foreach (var pf in vmForwards)
{
// Get the VM's IP address from the bridge network
var vmIp = GetVmIpFromBridge();
if (string.IsNullOrEmpty(vmIp))
{
Console.WriteLine($"⚠️ Warning: Could not determine VM IP for port forward {pf.HostPort}:{pf.VmPort}");
Console.WriteLine($" Port forwarding will be configured once the VM gets an IP address");
continue;
}
// Configure iptables rules
ConfigureIptablesRule(pf.HostPort, pf.VmPort, vmIp, pf.Protocol);
}
Console.WriteLine($"✅ Port forwarding configuration completed for VM '{_config.Name}'");
}
catch (Exception ex)
{
Console.WriteLine($"❌ Error configuring iptables port forwarding: {ex.Message}");
}
}
private string? GetVmIpFromBridge()
{
try
{
var bridgeName = _config.Network.Bridge ?? "virbr0";
// Method 1: Check libvirt DHCP leases
var vmIp = GetVmIpFromLibvirtDhcp();
if (!string.IsNullOrEmpty(vmIp))
{
Console.WriteLine($" 📡 Found VM IP {vmIp} from libvirt DHCP leases");
return vmIp;
}
// Method 2: Check system DHCP leases file
vmIp = GetVmIpFromDhcpLeases();
if (!string.IsNullOrEmpty(vmIp))
{
Console.WriteLine($" 📡 Found VM IP {vmIp} from system DHCP leases");
return vmIp;
}
// Method 3: Use ARP to discover VM IP on the bridge
vmIp = GetVmIpFromArp(bridgeName);
if (!string.IsNullOrEmpty(vmIp))
{
Console.WriteLine($" 📡 Found VM IP {vmIp} from ARP table");
return vmIp;
}
Console.WriteLine($" ⚠️ Could not determine VM IP automatically");
Console.WriteLine($" Port forwarding will be configured once the VM gets an IP address");
return null;
}
catch (Exception ex)
{
Console.WriteLine($" ❌ Error getting VM IP: {ex.Message}");
return null;
}
}
private string? GetVmIpFromLibvirtDhcp()
{
try
{
// Use virsh to get network information and DHCP leases
var process = new System.Diagnostics.Process
{
StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = "virsh",
Arguments = "net-dhcp-leases default",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
process.Start();
process.WaitForExit();
if (process.ExitCode != 0)
return null;
var output = process.StandardOutput.ReadToEnd();
// Parse virsh output to find our VM's IP
// Example output format:
// Expiry Time MAC address Protocol IP address Hostname Client ID or DUID
// 2025-09-01 11:00:00 aa:a0:cc:aa:23:b5 ipv4 192.168.122.100 ubuntu-desktop -
var lines = output.Split('\n');
foreach (var line in lines)
{
if (line.Contains("ubuntu-desktop") || line.Contains(_config.Name))
{
var parts = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 4)
{
var ipAddress = parts[3];
if (IsValidIpAddress(ipAddress))
{
return ipAddress;
}
}
}
}
return null;
}
catch
{
return null;
}
}
private string? GetVmIpFromDhcpLeases()
{
try
{
// Check system DHCP leases file (common locations)
var leaseFiles = new[]
{
"/var/lib/libvirt/dnsmasq/virbr0.status",
"/var/lib/dhcp/dhcpd.leases",
"/var/lib/dhcpcd/dhcpcd.leases"
};
foreach (var leaseFile in leaseFiles)
{
if (File.Exists(leaseFile))
{
var content = File.ReadAllText(leaseFile);
var vmIp = ParseDhcpLeaseFile(content);
if (!string.IsNullOrEmpty(vmIp))
return vmIp;
}
}
return null;
}
catch
{
return null;
}
}
private string? GetVmIpFromArp(string bridgeName)
{
try
{
// Use arp command to find devices on the bridge
var process = new System.Diagnostics.Process
{
StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = "arp",
Arguments = $"-n -i {bridgeName}",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
process.Start();
process.WaitForExit();
if (process.ExitCode != 0)
return null;
var output = process.StandardOutput.ReadToEnd();
// Parse arp output to find VM IPs
// Example: Address HWtype HWaddress Flags Mask Iface
// 192.168.122.100 ether aa:a0:cc:aa:23:b5 C virbr0
var lines = output.Split('\n');
foreach (var line in lines)
{
if (line.Contains(bridgeName) && !line.Contains("192.168.122.1"))
{
var parts = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 1)
{
var ipAddress = parts[0];
if (IsValidIpAddress(ipAddress) && ipAddress != "192.168.122.1")
{
return ipAddress;
}
}
}
}
return null;
}
catch
{
return null;
}
}
private string? ParseDhcpLeaseFile(string content)
{
try
{
// Simple parsing of DHCP lease files
// Look for lease entries with our VM's name or MAC
var lines = content.Split('\n');
foreach (var line in lines)
{
if (line.Contains("lease") && line.Contains("192.168.122."))
{
var parts = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts)
{
if (IsValidIpAddress(part) && part != "192.168.122.1")
{
return part;
}
}
}
}
return null;
}
catch
{
return null;
}
}
private bool IsValidIpAddress(string ip)
{
try
{
if (string.IsNullOrWhiteSpace(ip))
return false;
var parts = ip.Split('.');
if (parts.Length != 4)
return false;
foreach (var part in parts)
{
if (!int.TryParse(part, out var num) || num < 0 || num > 255)
return false;
}
return true;
}
catch
{
return false;
}
}
private void ConfigureIptablesRule(int hostPort, int vmPort, string vmIp, string protocol)
{
try
{
var protocolLower = protocol.ToLower();
// Create iptables rules for port forwarding
var natRule = $"-t nat -A PREROUTING -p {protocolLower} --dport {hostPort} -j DNAT --to-destination {vmIp}:{vmPort}";
var forwardRule = $"-A FORWARD -p {protocolLower} --dport {vmPort} -d {vmIp} -j ACCEPT";
// Execute iptables commands
ExecuteIptablesCommand(natRule);
ExecuteIptablesCommand(forwardRule);
Console.WriteLine($" ✅ Port {hostPort} → {vmIp}:{vmPort} ({protocol})");
}
catch (Exception ex)
{
Console.WriteLine($" ❌ Failed to configure port {hostPort}:{vmPort}: {ex.Message}");
}
}
private void ExecuteIptablesCommand(string rule)
{
try
{
// Use Process.Start to execute iptables command
var process = new System.Diagnostics.Process
{
StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = "sudo",
Arguments = $"iptables {rule}",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
process.Start();
process.WaitForExit();
if (process.ExitCode != 0)
{
var error = process.StandardError.ReadToEnd();
throw new Exception($"iptables command failed: {error}");
}
}
catch (Exception ex)
{
throw new Exception($"Failed to execute iptables command: {ex.Message}");
}
}
private string GetCustomPortForwards()
{
try
{
var configFile = "port-forwards.json";
if (!File.Exists(configFile))
return string.Empty;
var json = File.ReadAllText(configFile);
var portForwards = System.Text.Json.JsonSerializer.Deserialize<List<QemuVmManager.Models.PortForward>>(json) ?? new List<QemuVmManager.Models.PortForward>();
// Filter port forwards for this VM
var vmForwards = portForwards.Where(pf => pf.VmName == _config.Name).ToList();
if (!vmForwards.Any())
return string.Empty;
// For bridge networking, we don't use hostfwd - we use iptables instead
// This method is only called for macOS user networking
var forwardArgs = new List<string>();
foreach (var pf in vmForwards)
{
forwardArgs.Add($"hostfwd=tcp::{pf.HostPort}-:{pf.VmPort}");
}
return string.Join(",", forwardArgs);
}
catch (Exception ex)
{
Console.WriteLine($"Warning: Could not load custom port forwards: {ex.Message}");
return string.Empty;
}
}
private List<QemuVmManager.Models.PortForward> GetPortForwardList()
{
try
{
var configFile = "port-forwards.json";
if (!File.Exists(configFile))
return new List<QemuVmManager.Models.PortForward>();
var json = File.ReadAllText(configFile);
var portForwards = System.Text.Json.JsonSerializer.Deserialize<List<QemuVmManager.Models.PortForward>>(json) ?? new List<QemuVmManager.Models.PortForward>();
// Filter port forwards for this VM
return portForwards.Where(pf => pf.VmName == _config.Name).ToList();
}
catch (Exception ex)
{
Console.WriteLine($"Warning: Could not load port forward list: {ex.Message}");
return new List<QemuVmManager.Models.PortForward>();
}
}
private void AddMachineConfiguration()
{
_arguments.Add("-machine");
@@ -194,34 +624,134 @@ public class QemuCommandBuilder
{
var network = _config.Network;
Console.WriteLine($"Debug: Network configuration - Interfaces count: {network.Interfaces.Count}");
Console.WriteLine($"Debug: Network configuration object: {network}");
Console.WriteLine($"Debug: Network interfaces: {string.Join(", ", network.Interfaces.Select(i => $"{i.Type}:{i.Model}:{i.Bridge}"))}");
for (int i = 0; i < network.Interfaces.Count; i++)
{
var nic = network.Interfaces[i];
// Use user network as fallback since bridge might not be available
_arguments.Add("-netdev");
var netdevArgs = $"user,id=net{i}";
_arguments.Add(netdevArgs);
string netdevArgs;
_arguments.Add("-device");
var deviceArgs = "";
// Use different network models based on virtualization type
if (nic.Model == "virtio-net-pci" && _virtualizationType == VirtualizationType.HyperV)
// Configure network backend based on type
if (nic.Type == "bridge" && !string.IsNullOrEmpty(nic.Bridge))
{
// Use e1000 for WHPX compatibility to avoid MSI issues
deviceArgs = $"e1000,netdev=net{i}";
if (OperatingSystem.IsMacOS())
{
// On macOS, use host networking with automatic DHCP (built into QEMU)
// The 'net=' parameter automatically enables DHCP server
var hostPort = 10000 + i; // Use different ports for multiple interfaces
// Build base netdev arguments
var baseArgs = $"user,id=net{i},net=192.168.100.0/24,host=192.168.100.1";
// Add built-in port forwards
var builtInForwards = $"hostfwd=tcp::{hostPort}-:22,hostfwd=tcp::{hostPort + 1}-:80,hostfwd=tcp::{hostPort + 2}-:443";
// Add custom port forwards from configuration
var customForwards = GetCustomPortForwards();
if (!string.IsNullOrEmpty(customForwards))
{
netdevArgs = $"{baseArgs},{builtInForwards},{customForwards}";
}
else
{
netdevArgs = $"{baseArgs},{builtInForwards}";
}
_arguments.Add(netdevArgs);
Console.WriteLine($"Info: Using host-based networking with automatic DHCP on macOS");
Console.WriteLine("Benefits:");
Console.WriteLine("✅ Automatic IP assignment via QEMU's built-in DHCP");
Console.WriteLine("✅ Direct host network access (bypasses QEMU DNS issues)");
Console.WriteLine("✅ DNS requests go directly to host's DNS servers");
Console.WriteLine("✅ Internet access through host network");
Console.WriteLine("✅ Automatic port forwarding for common services");
Console.WriteLine($"✅ Host ports: {hostPort} (SSH), {hostPort + 1} (HTTP), {hostPort + 2} (HTTPS)");
Console.WriteLine("");
Console.WriteLine("📝 Network Configuration (Inside VM):");
Console.WriteLine(" The VM will get an IP automatically via QEMU's DHCP.");
Console.WriteLine(" If DHCP fails, manually configure:");
Console.WriteLine(" sudo ip addr add 192.168.100.x/24 dev enp0s3");
Console.WriteLine(" sudo ip route add default via 192.168.100.1");
Console.WriteLine(" DNS: nameserver 192.168.100.1 or 8.8.8.8");
_arguments.Add("-device");
var deviceArgs = "";
// Use different network models based on virtualization type
if (nic.Model == "virtio-net-pci" && _virtualizationType == VirtualizationType.HyperV)
{
// Use e1000 for WHPX compatibility to avoid MSI issues
deviceArgs = $"e1000,netdev=net{i}";
}
else
{
deviceArgs = $"{nic.Model},netdev=net{i}";
}
if (!string.IsNullOrEmpty(nic.Mac))
{
deviceArgs += $",mac={nic.Mac}";
}
_arguments.Add(deviceArgs);
}
else
{
// Use TAP networking for Linux/Windows
netdevArgs = $"tap,id=net{i},script=no,downscript=no";
_arguments.Add(netdevArgs);
_arguments.Add("-device");
var deviceArgs = "";
// Use different network models based on virtualization type
if (nic.Model == "virtio-net-pci" && _virtualizationType == VirtualizationType.HyperV)
{
// Use e1000 for WHPX compatibility to avoid MSI issues
deviceArgs = $"e1000,netdev=net{i}";
}
else
{
deviceArgs = $"{nic.Model},netdev=net{i}";
}
if (!string.IsNullOrEmpty(nic.Mac))
{
deviceArgs += $",mac={nic.Mac}";
}
_arguments.Add(deviceArgs);
}
}
else
{
deviceArgs = $"{nic.Model},netdev=net{i}";
// Use user network as fallback (NAT only)
netdevArgs = $"user,id=net{i}";
_arguments.Add(netdevArgs);
_arguments.Add("-device");
var deviceArgs = "";
// Use different network models based on virtualization type
if (nic.Model == "virtio-net-pci" && _virtualizationType == VirtualizationType.HyperV)
{
// Use e1000 for WHPX compatibility to avoid MSI issues
deviceArgs = $"e1000,netdev=net{i}";
}
else
{
deviceArgs = $"{nic.Model},netdev=net{i}";
}
if (!string.IsNullOrEmpty(nic.Mac))
{
deviceArgs += $",mac={nic.Mac}";
}
_arguments.Add(deviceArgs);
}
if (!string.IsNullOrEmpty(nic.Mac))
{
deviceArgs += $",mac={nic.Mac}";
}
_arguments.Add(deviceArgs);
}
}
@@ -230,10 +760,53 @@ public class QemuCommandBuilder
var display = _config.Display;
_arguments.Add("-display");
_arguments.Add(display.Type);
_arguments.Add("-vga");
_arguments.Add(display.Vga);
// Use appropriate display backend based on OS
string displayType = display.Type;
if (OperatingSystem.IsMacOS())
{
// On macOS, GTK is not available, use cocoa instead
if (displayType == "gtk")
{
displayType = "cocoa";
}
}
// Handle VNC display type with proper argument
if (displayType == "vnc")
{
var vncPort = display.SpicePort > 0 ? display.SpicePort : 5900;
// QEMU VNC display numbering: vnc=0.0.0.0:0 means port 5900, vnc=0.0.0.0:1 means port 5901, etc.
// So for port 5900, we use display 0, for port 5901, we use display 1, etc.
var displayNumber = vncPort - 5900;
if (displayNumber < 0) displayNumber = 0; // Default to display 0 if port < 5900
displayType = $"vnc=0.0.0.0:{displayNumber}";
}
_arguments.Add(displayType);
// For X11 forwarding compatibility, use std VGA instead of virtio
// This avoids GLX/OpenGL issues when forwarding over X11
if (displayType.StartsWith("gtk") || displayType.StartsWith("x11"))
{
_arguments.Add("-vga");
_arguments.Add("std"); // Use standard VGA for better X11 compatibility
// Add software rendering options to avoid GLX issues
_arguments.Add("-device");
_arguments.Add("virtio-gpu-pci,edid=off"); // Use virtio-gpu without EDID
}
else if (displayType.StartsWith("vnc"))
{
// For VNC, use std VGA for better compatibility
_arguments.Add("-vga");
_arguments.Add("std");
}
else
{
_arguments.Add("-vga");
_arguments.Add(display.Vga);
}
if (display.EnableSpice)
{

View File

@@ -13,6 +13,7 @@ public interface IUPnPManager
Task<bool> AddPortMappingAsync(int externalPort, int internalPort, string description);
Task<bool> RemovePortMappingAsync(int externalPort);
Task<List<PortMapping>> GetPortMappingsAsync();
Task<string> 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<string> GetUPnPDiagnosticsAsync()
{
try
{
var diagnostics = new List<string>();
// 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<IPAddress?> GetExternalIpAddressAsync()
{
try

View File

@@ -135,3 +135,12 @@ public class PortForwardingResponse
public string? ErrorMessage { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
public class PortForward
{
public string VmName { get; set; } = string.Empty;
public int HostPort { get; set; }
public int VmPort { get; set; }
public string Protocol { get; set; } = "TCP";
public DateTime Created { get; set; }
}

View File

@@ -380,17 +380,27 @@ public class VmManagementService
try
{
var json = File.ReadAllText(configFile);
Console.WriteLine($"Loading config from {configFile}");
Console.WriteLine($"JSON content: {json}");
var config = JsonSerializer.Deserialize<VmConfiguration>(json);
if (config != null && !string.IsNullOrWhiteSpace(config.Name))
{
Console.WriteLine($"Successfully loaded VM: {config.Name}");
Console.WriteLine($"Network interfaces count: {config.Network?.Interfaces?.Count ?? 0}");
_vmConfigurations[config.Name] = config;
}
else
{
Console.WriteLine($"Failed to deserialize config from {configFile}: config is null or has no name");
}
}
catch (Exception ex)
{
// Log error but continue loading other configurations
Console.WriteLine($"Failed to load configuration from {configFile}: {ex.Message}");
Console.WriteLine($"Exception details: {ex}");
}
}
}
@@ -472,4 +482,25 @@ public class VmManagementService
{
return await _processManager.GetPerformanceHistoryAsync(vmName, maxSamples);
}
public async Task UpdateVmConfigurationAsync(string vmName, VmConfiguration updatedConfig)
{
if (!_vmConfigurations.ContainsKey(vmName))
{
throw new ArgumentException($"VM configuration '{vmName}' not found");
}
// Ensure name consistency
updatedConfig.Name = vmName;
updatedConfig.LastModified = DateTime.UtcNow;
// Validate configuration
ValidateConfiguration(updatedConfig);
// Save updated configuration
await SaveVmConfigurationAsync(updatedConfig);
// Update in-memory cache
_vmConfigurations[vmName] = updatedConfig;
}
}

66
TestUPnP/Program.cs Normal file
View File

@@ -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!");
}
}

14
TestUPnP/TestUPnP.csproj Normal file
View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../QemuVmManager.Core/QemuVmManager.Core.csproj" />
</ItemGroup>
</Project>

9
port-forwards.json Normal file
View File

@@ -0,0 +1,9 @@
[
{
"VmName": "ubuntu-desktop",
"HostPort": 8080,
"VmPort": 80,
"Protocol": "TCP",
"Created": "2025-08-31T23:55:53.202737Z"
}
]