P2P abilities

This commit is contained in:
2025-08-30 19:34:29 -04:00
parent eb00a5472f
commit 4a9047f31a
7 changed files with 2369 additions and 623 deletions

View File

@@ -0,0 +1,377 @@
using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Logging;
using QemuVmManager.Models;
namespace QemuVmManager.Core;
public interface IUPnPManager
{
Task<bool> IsUPnPAvailableAsync();
Task<IPAddress?> GetExternalIpAddressAsync();
Task<bool> AddPortMappingAsync(int externalPort, int internalPort, string description);
Task<bool> RemovePortMappingAsync(int externalPort);
Task<List<PortMapping>> GetPortMappingsAsync();
}
public class UPnPManager : IUPnPManager
{
private readonly ILogger<UPnPManager>? _logger;
private readonly Dictionary<int, PortMapping> _activeMappings = new();
private readonly object _lock = new();
public UPnPManager(ILogger<UPnPManager>? logger = null)
{
_logger = logger;
}
public async Task<bool> IsUPnPAvailableAsync()
{
try
{
// Try to discover UPnP devices
var devices = await DiscoverUPnPDevicesAsync();
return devices.Any();
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "UPnP discovery failed");
return false;
}
}
public async Task<IPAddress?> GetExternalIpAddressAsync()
{
try
{
// Try multiple methods to get external IP
var externalIp = await GetExternalIpFromUPnPAsync();
if (externalIp != null)
return externalIp;
// Fallback to external service
return await GetExternalIpFromServiceAsync();
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to get external IP address");
return null;
}
}
public async Task<bool> AddPortMappingAsync(int externalPort, int internalPort, string description)
{
try
{
var localIp = GetLocalIpAddress();
if (localIp == null)
{
_logger?.LogError("Could not determine local IP address");
return false;
}
var success = await AddUPnPPortMappingAsync(externalPort, internalPort, localIp, description);
if (success)
{
lock (_lock)
{
_activeMappings[externalPort] = new PortMapping
{
ExternalPort = externalPort,
InternalPort = internalPort,
InternalIp = localIp,
Protocol = "TCP",
Description = description,
CreatedAt = DateTime.UtcNow
};
}
_logger?.LogInformation("Added port mapping: {ExternalPort} -> {InternalPort} ({Description})",
externalPort, internalPort, description);
}
return success;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to add port mapping {ExternalPort} -> {InternalPort}",
externalPort, internalPort);
return false;
}
}
public async Task<bool> RemovePortMappingAsync(int externalPort)
{
try
{
var success = await RemoveUPnPPortMappingAsync(externalPort);
if (success)
{
lock (_lock)
{
_activeMappings.Remove(externalPort);
}
_logger?.LogInformation("Removed port mapping: {ExternalPort}", externalPort);
}
return success;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to remove port mapping {ExternalPort}", externalPort);
return false;
}
}
public async Task<List<PortMapping>> GetPortMappingsAsync()
{
try
{
var mappings = await GetUPnPPortMappingsAsync();
lock (_lock)
{
// Merge with our active mappings
foreach (var mapping in _activeMappings.Values)
{
if (!mappings.Any(m => m.ExternalPort == mapping.ExternalPort))
{
mappings.Add(mapping);
}
}
}
return mappings;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to get port mappings");
lock (_lock)
{
return _activeMappings.Values.ToList();
}
}
}
private async Task<List<UPnPDevice>> DiscoverUPnPDevicesAsync()
{
var devices = new List<UPnPDevice>();
// 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<IPAddress?> 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<IPAddress?> GetExternalIpFromServiceAsync()
{
try
{
using var client = new HttpClient();
client.Timeout = TimeSpan.FromSeconds(10);
// Try multiple external IP services
var services = new[]
{
"https://api.ipify.org",
"https://icanhazip.com",
"https://ifconfig.me/ip"
};
foreach (var service in services)
{
try
{
var response = await client.GetStringAsync(service);
var ipString = response.Trim();
if (IPAddress.TryParse(ipString, out var ip))
return ip;
}
catch
{
continue;
}
}
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Failed to get external IP from services");
}
return null;
}
private IPAddress? GetLocalIpAddress()
{
try
{
var host = Dns.GetHostEntry(Dns.GetHostName());
return host.AddressList.FirstOrDefault(ip =>
ip.AddressFamily == AddressFamily.InterNetwork &&
!IPAddress.IsLoopback(ip));
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Failed to get local IP address");
return null;
}
}
private async Task<bool> 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<bool> 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<List<PortMapping>> GetUPnPPortMappingsAsync()
{
try
{
var devices = await DiscoverUPnPDevicesAsync();
if (!devices.Any())
return new List<PortMapping>();
// This would require implementing SOAP calls to get port mappings
// For now, we'll return empty list
return new List<PortMapping>();
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to get UPnP port mappings");
return new List<PortMapping>();
}
}
}
public class UPnPDevice
{
public string ControlUrl { get; set; } = string.Empty;
public IPAddress IpAddress { get; set; } = IPAddress.Any;
public int Port { get; set; }
}
public class PortMapping
{
public int ExternalPort { get; set; }
public int InternalPort { get; set; }
public IPAddress InternalIp { get; set; } = IPAddress.Any;
public string Protocol { get; set; } = "TCP";
public string Description { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
}