Compare commits
3 Commits
307d256ddf
...
main
Author | SHA1 | Date | |
---|---|---|---|
b5fa671d8a | |||
19abfb919e | |||
c8a48e61e7 |
@@ -33,11 +33,17 @@ public class QemuCommandBuilder
|
|||||||
// Storage configuration
|
// Storage configuration
|
||||||
AddStorageConfiguration();
|
AddStorageConfiguration();
|
||||||
|
|
||||||
|
// OS Detection Debug
|
||||||
|
Console.WriteLine($"Debug: OS Detection - IsMacOS: {OperatingSystem.IsMacOS()}, IsLinux: {OperatingSystem.IsLinux()}, IsWindows: {OperatingSystem.IsWindows()}");
|
||||||
|
|
||||||
// Network configuration
|
// Network configuration
|
||||||
Console.WriteLine("Debug: About to add network configuration");
|
Console.WriteLine("Debug: About to add network configuration");
|
||||||
AddNetworkConfiguration();
|
AddNetworkConfiguration();
|
||||||
Console.WriteLine("Debug: Network configuration added");
|
Console.WriteLine("Debug: Network configuration added");
|
||||||
|
|
||||||
|
// Add port forwarding for bridge networking if needed
|
||||||
|
AddPortForwardingForBridge();
|
||||||
|
|
||||||
// Display configuration
|
// Display configuration
|
||||||
AddDisplayConfiguration();
|
AddDisplayConfiguration();
|
||||||
|
|
||||||
@@ -78,6 +84,370 @@ public class QemuCommandBuilder
|
|||||||
return string.Join(" ", _arguments);
|
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()
|
private string GetCustomPortForwards()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -95,7 +465,8 @@ public class QemuCommandBuilder
|
|||||||
if (!vmForwards.Any())
|
if (!vmForwards.Any())
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
|
||||||
// Build hostfwd arguments for custom port forwards
|
// 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>();
|
var forwardArgs = new List<string>();
|
||||||
foreach (var pf in vmForwards)
|
foreach (var pf in vmForwards)
|
||||||
{
|
{
|
||||||
@@ -111,6 +482,27 @@ public class QemuCommandBuilder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
private void AddMachineConfiguration()
|
||||||
{
|
{
|
||||||
_arguments.Add("-machine");
|
_arguments.Add("-machine");
|
||||||
|
@@ -380,17 +380,27 @@ public class VmManagementService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var json = File.ReadAllText(configFile);
|
var json = File.ReadAllText(configFile);
|
||||||
|
Console.WriteLine($"Loading config from {configFile}");
|
||||||
|
Console.WriteLine($"JSON content: {json}");
|
||||||
|
|
||||||
var config = JsonSerializer.Deserialize<VmConfiguration>(json);
|
var config = JsonSerializer.Deserialize<VmConfiguration>(json);
|
||||||
|
|
||||||
if (config != null && !string.IsNullOrWhiteSpace(config.Name))
|
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;
|
_vmConfigurations[config.Name] = config;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to deserialize config from {configFile}: config is null or has no name");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Log error but continue loading other configurations
|
// Log error but continue loading other configurations
|
||||||
Console.WriteLine($"Failed to load configuration from {configFile}: {ex.Message}");
|
Console.WriteLine($"Failed to load configuration from {configFile}: {ex.Message}");
|
||||||
|
Console.WriteLine($"Exception details: {ex}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
9
port-forwards.json
Normal file
9
port-forwards.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"VmName": "ubuntu-desktop",
|
||||||
|
"HostPort": 8080,
|
||||||
|
"VmPort": 80,
|
||||||
|
"Protocol": "TCP",
|
||||||
|
"Created": "2025-08-31T23:55:53.202737Z"
|
||||||
|
}
|
||||||
|
]
|
Reference in New Issue
Block a user