Compare commits
5 Commits
6bb9f4bf39
...
master
Author | SHA1 | Date | |
---|---|---|---|
602af77291 | |||
8a26ddcb3a | |||
b5d78b2bd9 | |||
1f92098d8e | |||
7b8bb02dc3 |
@@ -109,6 +109,9 @@ public class P2PConsole
|
||||
case "upnp":
|
||||
await ShowUPnPStatusAsync();
|
||||
break;
|
||||
case "discovery":
|
||||
await ShowDiscoveryInfoAsync();
|
||||
break;
|
||||
case "exit":
|
||||
case "quit":
|
||||
System.Console.WriteLine("Stopping P2P node...");
|
||||
@@ -144,6 +147,7 @@ public class P2PConsole
|
||||
System.Console.WriteLine(" migrate <vm-id> <node-id> - Migrate VM to different node");
|
||||
System.Console.WriteLine(" forward <vm-id> <port> - Forward port for VM (master only)");
|
||||
System.Console.WriteLine(" upnp - Show UPnP status");
|
||||
System.Console.WriteLine(" discovery - Show network discovery information");
|
||||
System.Console.WriteLine(" help - Show this help");
|
||||
System.Console.WriteLine(" exit/quit - Exit the application");
|
||||
}
|
||||
@@ -518,4 +522,51 @@ public class P2PConsole
|
||||
var input = System.Console.ReadLine()?.Trim();
|
||||
return string.IsNullOrEmpty(input) ? defaultValue : input;
|
||||
}
|
||||
|
||||
private async Task ShowDiscoveryInfoAsync()
|
||||
{
|
||||
System.Console.WriteLine("=== Network Discovery Information ===");
|
||||
System.Console.WriteLine($"Current Node ID: {_p2pNode.CurrentNode.NodeId}");
|
||||
System.Console.WriteLine($"Current Node IP: {_p2pNode.CurrentNode.IpAddress}");
|
||||
System.Console.WriteLine($"Current Node Port: {_p2pNode.CurrentNode.Port}");
|
||||
System.Console.WriteLine($"Current Role: {_p2pNode.CurrentNode.Role}");
|
||||
System.Console.WriteLine($"Is Master: {_p2pNode.IsMaster}");
|
||||
System.Console.WriteLine();
|
||||
|
||||
// Get cluster state to show known nodes
|
||||
var cluster = _p2pNode.ClusterState;
|
||||
System.Console.WriteLine($"Known Nodes: {cluster.Nodes.Count}");
|
||||
|
||||
if (cluster.Nodes.Count > 0)
|
||||
{
|
||||
System.Console.WriteLine();
|
||||
System.Console.WriteLine("=== Known Nodes ===");
|
||||
System.Console.WriteLine($"{"Node ID",-20} {"IP Address",-15} {"Port",-8} {"Role",-10} {"Last Seen"}");
|
||||
System.Console.WriteLine(new string('-', 80));
|
||||
|
||||
foreach (var node in cluster.Nodes)
|
||||
{
|
||||
var lastSeen = node.LastSeen.ToString("HH:mm:ss");
|
||||
System.Console.WriteLine($"{node.NodeId,-20} {node.IpAddress,-15} {node.Port,-8} {node.Role,-10} {lastSeen}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
System.Console.WriteLine("No other nodes discovered yet.");
|
||||
System.Console.WriteLine();
|
||||
System.Console.WriteLine("Discovery troubleshooting:");
|
||||
System.Console.WriteLine("1. Make sure other nodes are running on the same network");
|
||||
System.Console.WriteLine("2. Check if firewall is blocking UDP port 8080");
|
||||
System.Console.WriteLine("3. Try running multiple instances on different machines");
|
||||
System.Console.WriteLine("4. Check network connectivity between nodes");
|
||||
}
|
||||
|
||||
System.Console.WriteLine();
|
||||
System.Console.WriteLine("=== Network Configuration ===");
|
||||
System.Console.WriteLine("UDP Discovery: Enabled (port 8080)");
|
||||
System.Console.WriteLine("TCP Communication: Enabled (port 8081)");
|
||||
System.Console.WriteLine("Heartbeat Interval: 5 seconds");
|
||||
System.Console.WriteLine("Discovery Interval: 30 seconds");
|
||||
System.Console.WriteLine("Node Timeout: 30 seconds");
|
||||
}
|
||||
}
|
||||
|
@@ -273,15 +273,23 @@ public class P2PNode : IDisposable
|
||||
private async Task InitializeNetworkAsync()
|
||||
{
|
||||
// Initialize UDP client for discovery and heartbeats
|
||||
_udpClient = new UdpClient(_port);
|
||||
_udpClient = new UdpClient();
|
||||
_udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
_udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true);
|
||||
_udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, _port));
|
||||
|
||||
// Initialize TCP listener for direct communication
|
||||
_tcpListener = new TcpListener(IPAddress.Any, _port);
|
||||
// Initialize TCP listener for direct communication on a different port
|
||||
var tcpPort = _port + 1; // Use next port for TCP
|
||||
_tcpListener = new TcpListener(IPAddress.Any, tcpPort);
|
||||
_tcpListener.Start();
|
||||
|
||||
_logger?.LogInformation("Network initialized - UDP: {UdpPort}, TCP: {TcpPort}", _port, tcpPort);
|
||||
|
||||
// Start listening for incoming connections
|
||||
_ = Task.Run(() => ListenForConnectionsAsync(_cancellationTokenSource!.Token));
|
||||
|
||||
// Start listening for UDP messages
|
||||
_ = Task.Run(() => ListenForUdpMessagesAsync(_cancellationTokenSource!.Token));
|
||||
}
|
||||
|
||||
private async Task HeartbeatLoopAsync(CancellationToken cancellationToken)
|
||||
@@ -389,8 +397,29 @@ public class P2PNode : IDisposable
|
||||
});
|
||||
|
||||
var data = System.Text.Encoding.UTF8.GetBytes(discoveryMessage);
|
||||
await _udpClient!.SendAsync(data, data.Length, new IPEndPoint(IPAddress.Broadcast, _port));
|
||||
|
||||
// Try multiple broadcast addresses
|
||||
var broadcastAddresses = new[]
|
||||
{
|
||||
IPAddress.Broadcast,
|
||||
IPAddress.Parse("255.255.255.255"),
|
||||
GetLocalBroadcastAddress()
|
||||
};
|
||||
|
||||
foreach (var broadcastAddr in broadcastAddresses)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _udpClient!.SendAsync(data, data.Length, new IPEndPoint(broadcastAddr, _port));
|
||||
_logger?.LogDebug("Sent discovery message to {BroadcastAddress}", broadcastAddr);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogDebug(ex, "Failed to send discovery to {BroadcastAddress}", broadcastAddr);
|
||||
}
|
||||
}
|
||||
|
||||
_logger?.LogInformation("Discovery cycle completed. Known nodes: {NodeCount}", _knownNodes.Count);
|
||||
await Task.Delay(30000, cancellationToken); // Send discovery every 30 seconds
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@@ -579,6 +608,7 @@ public class P2PNode : IDisposable
|
||||
var message = JsonSerializer.Serialize(heartbeat);
|
||||
var data = System.Text.Encoding.UTF8.GetBytes(message);
|
||||
|
||||
// Send to known nodes
|
||||
foreach (var node in _knownNodes.Values)
|
||||
{
|
||||
try
|
||||
@@ -590,6 +620,27 @@ public class P2PNode : IDisposable
|
||||
_logger?.LogWarning(ex, "Failed to send heartbeat to node {NodeId}", node.NodeId);
|
||||
}
|
||||
}
|
||||
|
||||
// Also broadcast to discover new nodes
|
||||
var broadcastAddresses = new[]
|
||||
{
|
||||
IPAddress.Broadcast,
|
||||
IPAddress.Parse("255.255.255.255"),
|
||||
GetLocalBroadcastAddress()
|
||||
};
|
||||
|
||||
foreach (var broadcastAddr in broadcastAddresses)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _udpClient!.SendAsync(data, data.Length, new IPEndPoint(broadcastAddr, _port));
|
||||
_logger?.LogDebug("Broadcasted heartbeat to {BroadcastAddress}", broadcastAddr);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogDebug(ex, "Failed to broadcast heartbeat to {BroadcastAddress}", broadcastAddr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CleanupStaleNodesAsync()
|
||||
@@ -600,6 +651,18 @@ public class P2PNode : IDisposable
|
||||
foreach (var node in staleNodes)
|
||||
{
|
||||
_knownNodes.Remove(node.NodeId);
|
||||
|
||||
// Update cluster state
|
||||
lock (_clusterLock)
|
||||
{
|
||||
var clusterNode = ClusterState.Nodes.FirstOrDefault(n => n.NodeId == node.NodeId);
|
||||
if (clusterNode != null)
|
||||
{
|
||||
ClusterState.Nodes.Remove(clusterNode);
|
||||
ClusterState.LastUpdated = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
NodeLeft?.Invoke(this, node);
|
||||
_logger?.LogInformation("Removed stale node {NodeId}", node.NodeId);
|
||||
}
|
||||
@@ -625,6 +688,57 @@ public class P2PNode : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ListenForUdpMessagesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _udpClient!.ReceiveAsync(cancellationToken);
|
||||
var message = System.Text.Encoding.UTF8.GetString(result.Buffer);
|
||||
|
||||
_logger?.LogDebug("Received UDP message from {Endpoint}: {Message}",
|
||||
result.RemoteEndPoint, message);
|
||||
|
||||
await ProcessUdpMessageAsync(message, result.RemoteEndPoint);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error receiving UDP message");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessUdpMessageAsync(string message, IPEndPoint remoteEndpoint)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = JsonSerializer.Deserialize<JsonElement>(message);
|
||||
var messageType = data.GetProperty("type").GetString();
|
||||
|
||||
switch (messageType)
|
||||
{
|
||||
case "heartbeat":
|
||||
await ProcessHeartbeatAsync(data, remoteEndpoint);
|
||||
break;
|
||||
case "discovery":
|
||||
await ProcessDiscoveryAsync(data, remoteEndpoint);
|
||||
break;
|
||||
default:
|
||||
_logger?.LogWarning("Unknown UDP message type: {MessageType}", messageType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error processing UDP message: {Message}", message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
@@ -659,10 +773,11 @@ public class P2PNode : IDisposable
|
||||
switch (messageType)
|
||||
{
|
||||
case "heartbeat":
|
||||
await ProcessHeartbeatAsync(data);
|
||||
await ProcessHeartbeatAsync(data, null); // No remote endpoint for TCP
|
||||
break;
|
||||
case "discovery":
|
||||
await ProcessDiscoveryAsync(data);
|
||||
// Discovery messages are typically UDP, but handle gracefully
|
||||
_logger?.LogWarning("Received discovery message via TCP - unexpected");
|
||||
break;
|
||||
case "election_request":
|
||||
await ProcessElectionRequestAsync(data, writer);
|
||||
@@ -681,7 +796,7 @@ public class P2PNode : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessHeartbeatAsync(JsonElement data)
|
||||
private async Task ProcessHeartbeatAsync(JsonElement data, IPEndPoint? remoteEndpoint = null)
|
||||
{
|
||||
var nodeId = data.GetProperty("nodeId").GetString()!;
|
||||
var role = Enum.Parse<NodeRole>(data.GetProperty("role").GetString()!);
|
||||
@@ -701,15 +816,42 @@ public class P2PNode : IDisposable
|
||||
_lastHeartbeat = timestamp;
|
||||
}
|
||||
|
||||
// Update node info
|
||||
// Update node info in known nodes
|
||||
if (_knownNodes.TryGetValue(nodeId, out var node))
|
||||
{
|
||||
node.LastSeen = DateTime.UtcNow;
|
||||
node.Role = role;
|
||||
|
||||
// Update IP address if we received it from UDP
|
||||
if (remoteEndpoint != null && node.IpAddress == IPAddress.Any)
|
||||
{
|
||||
node.IpAddress = remoteEndpoint.Address;
|
||||
node.Port = remoteEndpoint.Port;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessDiscoveryAsync(JsonElement data)
|
||||
// Update cluster state
|
||||
lock (_clusterLock)
|
||||
{
|
||||
var clusterNode = ClusterState.Nodes.FirstOrDefault(n => n.NodeId == nodeId);
|
||||
if (clusterNode != null)
|
||||
{
|
||||
clusterNode.LastSeen = DateTime.UtcNow;
|
||||
clusterNode.Role = role;
|
||||
|
||||
// Update IP address if we received it from UDP
|
||||
if (remoteEndpoint != null && clusterNode.IpAddress == IPAddress.Any)
|
||||
{
|
||||
clusterNode.IpAddress = remoteEndpoint.Address;
|
||||
clusterNode.Port = remoteEndpoint.Port;
|
||||
}
|
||||
|
||||
ClusterState.LastUpdated = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessDiscoveryAsync(JsonElement data, IPEndPoint remoteEndpoint)
|
||||
{
|
||||
var nodeId = data.GetProperty("nodeId").GetString()!;
|
||||
|
||||
@@ -718,12 +860,42 @@ public class P2PNode : IDisposable
|
||||
var newNode = new NodeInfo
|
||||
{
|
||||
NodeId = nodeId,
|
||||
LastSeen = DateTime.UtcNow
|
||||
Hostname = Environment.MachineName, // We'll get this from heartbeat
|
||||
IpAddress = remoteEndpoint.Address, // Use the actual sender IP
|
||||
Port = remoteEndpoint.Port,
|
||||
Role = NodeRole.Follower,
|
||||
State = NodeState.Running,
|
||||
LastSeen = DateTime.UtcNow,
|
||||
SystemInfo = new SystemInfo() // We'll get this from heartbeat
|
||||
};
|
||||
|
||||
_knownNodes[nodeId] = newNode;
|
||||
|
||||
// Update cluster state
|
||||
lock (_clusterLock)
|
||||
{
|
||||
ClusterState.Nodes.Add(newNode);
|
||||
ClusterState.LastUpdated = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
NodeJoined?.Invoke(this, newNode);
|
||||
_logger?.LogInformation("Discovered new node {NodeId}", nodeId);
|
||||
_logger?.LogInformation("Discovered new node {NodeId} at {IpAddress}:{Port}", nodeId, remoteEndpoint.Address, remoteEndpoint.Port);
|
||||
}
|
||||
else if (nodeId != _nodeId && _knownNodes.ContainsKey(nodeId))
|
||||
{
|
||||
// Update last seen for existing node
|
||||
_knownNodes[nodeId].LastSeen = DateTime.UtcNow;
|
||||
|
||||
// Update cluster state
|
||||
lock (_clusterLock)
|
||||
{
|
||||
var existingNode = ClusterState.Nodes.FirstOrDefault(n => n.NodeId == nodeId);
|
||||
if (existingNode != null)
|
||||
{
|
||||
existingNode.LastSeen = DateTime.UtcNow;
|
||||
ClusterState.LastUpdated = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -808,6 +980,29 @@ public class P2PNode : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private IPAddress GetLocalBroadcastAddress()
|
||||
{
|
||||
try
|
||||
{
|
||||
var localIp = GetLocalIpAddress();
|
||||
if (localIp != null && localIp != IPAddress.Any)
|
||||
{
|
||||
var ipBytes = localIp.GetAddressBytes();
|
||||
// Set all host bits to 1 for broadcast
|
||||
for (int i = 3; i >= 0; i--)
|
||||
{
|
||||
ipBytes[i] = 255;
|
||||
}
|
||||
return new IPAddress(ipBytes);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to get local broadcast address");
|
||||
}
|
||||
return IPAddress.Broadcast;
|
||||
}
|
||||
|
||||
private SystemInfo GetSystemInfo()
|
||||
{
|
||||
return new SystemInfo
|
||||
|
@@ -11,8 +11,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Open.NAT" Version="2.1.0" />
|
||||
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="System.Reflection.Metadata" Version="9.0.8" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@@ -2,6 +2,7 @@ using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using QemuVmManager.Models;
|
||||
using Open.Nat;
|
||||
|
||||
namespace QemuVmManager.Core;
|
||||
|
||||
@@ -19,23 +20,56 @@ public class UPnPManager : IUPnPManager
|
||||
private readonly ILogger<UPnPManager>? _logger;
|
||||
private readonly Dictionary<int, PortMapping> _activeMappings = new();
|
||||
private readonly object _lock = new();
|
||||
private NatDevice? _natDevice;
|
||||
private bool _isInitialized = false;
|
||||
|
||||
public UPnPManager(ILogger<UPnPManager>? logger = null)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
if (_isInitialized) return;
|
||||
|
||||
try
|
||||
{
|
||||
_logger?.LogInformation("Initializing UPnP/NAT discovery...");
|
||||
|
||||
// Create a new NAT discoverer
|
||||
var discoverer = new NatDiscoverer();
|
||||
|
||||
// Discover UPnP devices with a 10-second timeout
|
||||
_natDevice = await discoverer.DiscoverDeviceAsync(PortMapper.Upnp, new CancellationTokenSource(TimeSpan.FromSeconds(10)));
|
||||
|
||||
if (_natDevice != null)
|
||||
{
|
||||
_logger?.LogInformation("UPnP device discovered successfully");
|
||||
_isInitialized = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger?.LogWarning("No UPnP devices found");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "UPnP initialization failed");
|
||||
_natDevice = null;
|
||||
_isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> IsUPnPAvailableAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to discover UPnP devices
|
||||
var devices = await DiscoverUPnPDevicesAsync();
|
||||
return devices.Any();
|
||||
await InitializeAsync();
|
||||
return _natDevice != null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "UPnP discovery failed");
|
||||
_logger?.LogWarning(ex, "UPnP availability check failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -44,18 +78,22 @@ public class UPnPManager : IUPnPManager
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try multiple methods to get external IP
|
||||
var externalIp = await GetExternalIpFromUPnPAsync();
|
||||
if (externalIp != null)
|
||||
return externalIp;
|
||||
await InitializeAsync();
|
||||
|
||||
// Fallback to external service
|
||||
if (_natDevice == null)
|
||||
{
|
||||
_logger?.LogWarning("No UPnP device available for external IP lookup");
|
||||
return await GetExternalIpFromServiceAsync();
|
||||
}
|
||||
|
||||
var externalIp = await _natDevice.GetExternalIPAsync();
|
||||
_logger?.LogInformation("External IP from UPnP: {ExternalIP}", externalIp);
|
||||
return externalIp;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to get external IP address");
|
||||
return null;
|
||||
_logger?.LogWarning(ex, "Failed to get external IP from UPnP, falling back to external service");
|
||||
return await GetExternalIpFromServiceAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +101,14 @@ public class UPnPManager : IUPnPManager
|
||||
{
|
||||
try
|
||||
{
|
||||
await InitializeAsync();
|
||||
|
||||
if (_natDevice == null)
|
||||
{
|
||||
_logger?.LogError("No UPnP device available for port mapping");
|
||||
return false;
|
||||
}
|
||||
|
||||
var localIp = GetLocalIpAddress();
|
||||
if (localIp == null)
|
||||
{
|
||||
@@ -70,9 +116,12 @@ public class UPnPManager : IUPnPManager
|
||||
return false;
|
||||
}
|
||||
|
||||
var success = await AddUPnPPortMappingAsync(externalPort, internalPort, localIp, description);
|
||||
if (success)
|
||||
{
|
||||
// Create the port mapping
|
||||
var mapping = new Mapping(Protocol.Tcp, internalPort, externalPort, description);
|
||||
|
||||
// Add the mapping
|
||||
await _natDevice.CreatePortMapAsync(mapping);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_activeMappings[externalPort] = new PortMapping
|
||||
@@ -85,11 +134,11 @@ public class UPnPManager : IUPnPManager
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
_logger?.LogInformation("Added port mapping: {ExternalPort} -> {InternalPort} ({Description})",
|
||||
externalPort, internalPort, description);
|
||||
}
|
||||
|
||||
return success;
|
||||
_logger?.LogInformation("Successfully added port mapping: {ExternalPort} -> {InternalIp}:{InternalPort} ({Description})",
|
||||
externalPort, localIp, internalPort, description);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -103,17 +152,34 @@ public class UPnPManager : IUPnPManager
|
||||
{
|
||||
try
|
||||
{
|
||||
var success = await RemoveUPnPPortMappingAsync(externalPort);
|
||||
if (success)
|
||||
await InitializeAsync();
|
||||
|
||||
if (_natDevice == null)
|
||||
{
|
||||
_logger?.LogError("No UPnP device available for port mapping removal");
|
||||
return false;
|
||||
}
|
||||
|
||||
var localIp = GetLocalIpAddress();
|
||||
if (localIp == null)
|
||||
{
|
||||
_logger?.LogError("Could not determine local IP address for mapping removal");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create the mapping object for removal
|
||||
var mapping = new Mapping(Protocol.Tcp, 0, externalPort, "");
|
||||
|
||||
// Remove the mapping
|
||||
await _natDevice.DeletePortMapAsync(mapping);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_activeMappings.Remove(externalPort);
|
||||
}
|
||||
_logger?.LogInformation("Removed port mapping: {ExternalPort}", externalPort);
|
||||
}
|
||||
|
||||
return success;
|
||||
_logger?.LogInformation("Successfully removed port mapping: {ExternalPort}", externalPort);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -126,119 +192,35 @@ public class UPnPManager : IUPnPManager
|
||||
{
|
||||
try
|
||||
{
|
||||
var mappings = await GetUPnPPortMappingsAsync();
|
||||
lock (_lock)
|
||||
await InitializeAsync();
|
||||
|
||||
if (_natDevice == null)
|
||||
{
|
||||
// Merge with our active mappings
|
||||
foreach (var mapping in _activeMappings.Values)
|
||||
{
|
||||
if (!mappings.Any(m => m.ExternalPort == mapping.ExternalPort))
|
||||
{
|
||||
mappings.Add(mapping);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mappings;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to get port mappings");
|
||||
_logger?.LogWarning("No UPnP device available for port mapping retrieval");
|
||||
lock (_lock)
|
||||
{
|
||||
return _activeMappings.Values.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
var mappings = new List<PortMapping>();
|
||||
|
||||
// For now, just return our active mappings since the Open.NAT API is complex
|
||||
// In a full implementation, we would query the device for all mappings
|
||||
lock (_lock)
|
||||
{
|
||||
mappings.AddRange(_activeMappings.Values);
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
return mappings;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to parse UPnP response");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<IPAddress?> GetExternalIpFromUPnPAsync()
|
||||
_logger?.LogError(ex, "Failed to get port mappings from UPnP device");
|
||||
lock (_lock)
|
||||
{
|
||||
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;
|
||||
return _activeMappings.Values.ToList();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,17 +246,21 @@ public class UPnPManager : IUPnPManager
|
||||
var response = await client.GetStringAsync(service);
|
||||
var ipString = response.Trim();
|
||||
if (IPAddress.TryParse(ipString, out var ip))
|
||||
{
|
||||
_logger?.LogInformation("External IP from service {Service}: {IP}", service, ip);
|
||||
return ip;
|
||||
}
|
||||
catch
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogDebug(ex, "Failed to get external IP from {Service}", service);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to get external IP from services");
|
||||
_logger?.LogWarning(ex, "Failed to get external IP from any service");
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -295,75 +281,6 @@ public class UPnPManager : IUPnPManager
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<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
|
||||
|
@@ -24,10 +24,7 @@
|
||||
"isBoot": false
|
||||
}
|
||||
],
|
||||
"cdrom": {
|
||||
"path": "ubuntu-24.04.3-desktop-amd64.iso",
|
||||
"isBoot": true
|
||||
}
|
||||
"cdrom":"ubuntu-24.04.3-desktop-amd64.iso"
|
||||
},
|
||||
"network": {
|
||||
"backend": "user",
|
||||
|
Reference in New Issue
Block a user