LumaTunnel
【WinUI】LumaTunnel 是一个面向个人多设备的 Windows 代理系统。客户端在本机提供 HTTP/HTTPS CONNECT 与 SOCKS5 TCP 代理,并通过一个受信任 TLS 证书保护的 WSS 会话,将多个 TCP 流复用到自建 Windows Server 节点。
关注
0
Fork
0
Star
0
using System.Collections.Concurrent;
using LumaTunnel.Client.Core.Abstractions;
using LumaTunnel.Shared.Models;
namespace LumaTunnel.Client.Core.Nodes;
public sealed class NodeManagerService(
NodeApiClient apiClient,
ITunnelService tunnelService,
Func<NodeProfile, CancellationToken, Task<string?>> getDeviceToken,
string clientVersion) : INodeManagerService
{
private readonly ConcurrentDictionary<Guid, int> _failures = new();
private readonly SemaphoreSlim _switchGate = new(1, 1);
private volatile NodeProfile[] _nodes = [];
private CancellationTokenSource? _monitorCancellation;
private Task? _monitorTask;
public NodeProfile? ActiveNode { get; private set; }
public IReadOnlyList<NodeProfile> Nodes => _nodes;
public event EventHandler<NodeProfile?>? ActiveNodeChanged;
public void SetNodes(IEnumerable<NodeProfile> nodes)
{
ArgumentNullException.ThrowIfNull(nodes);
_nodes = nodes.OrderBy(static node => node.Priority).ToArray();
}
public async Task<NodeProfile> ConnectBestAsync(CancellationToken cancellationToken = default)
{
await _switchGate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
var candidates = _nodes.Where(static node => node.Enabled && node.DeviceId is not null).ToArray();
if (candidates.Length == 0)
throw new InvalidOperationException("No enrolled LumaTunnel node is enabled.");
var checks = await Task.WhenAll(candidates.Select(node => CheckNodeAsync(node, cancellationToken))).ConfigureAwait(false);
var best = checks.Where(static result => result.Success).OrderBy(static result => result.Latency).ThenBy(static result => result.Node.Priority).FirstOrDefault()
?? throw new IOException("None of the configured LumaTunnel nodes is healthy.");
var token = await getDeviceToken(best.Node, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException($"No device token is available for node '{best.Node.Name}'.");
await tunnelService.ConnectAsync(new TunnelCredentials(best.Node.TunnelUri, best.Node.DeviceId!, token, clientVersion), cancellationToken).ConfigureAwait(false);
ActiveNode = best.Node with { LastLatencyMilliseconds = best.Latency.TotalMilliseconds, LastHealthCheckUtc = DateTimeOffset.UtcNow };
_failures[best.Node.Id] = 0;
ActiveNodeChanged?.Invoke(this, ActiveNode);
return ActiveNode;
}
finally
{
_switchGate.Release();
}
}
public Task StartMonitoringAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (_monitorCancellation is not null)
return Task.CompletedTask;
_monitorCancellation = new CancellationTokenSource();
_monitorTask = MonitorLoopAsync(_monitorCancellation.Token);
return Task.CompletedTask;
}
public async Task StopMonitoringAsync(CancellationToken cancellationToken = default)
{
var cancellation = _monitorCancellation;
var task = _monitorTask;
_monitorCancellation = null;
_monitorTask = null;
if (cancellation is null)
return;
cancellation.Cancel();
if (task is not null)
await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(2), cancellationToken)).ConfigureAwait(false);
cancellation.Dispose();
}
public async ValueTask DisposeAsync()
{
await StopMonitoringAsync().ConfigureAwait(false);
_switchGate.Dispose();
apiClient.Dispose();
GC.SuppressFinalize(this);
}
private async Task MonitorLoopAsync(CancellationToken cancellationToken)
{
try
{
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken).ConfigureAwait(false);
var active = ActiveNode;
if (active is null)
continue;
var check = await CheckNodeAsync(active, cancellationToken).ConfigureAwait(false);
if (check.Success && tunnelService.IsConnected)
{
_failures[active.Id] = 0;
continue;
}
var failures = _failures.AddOrUpdate(active.Id, 1, static (_, count) => count + 1);
if (failures >= 3)
{
try
{
await ConnectBestAsync(cancellationToken).ConfigureAwait(false);
}
catch (IOException)
{
}
}
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
}
}
private async Task<HealthResult> CheckNodeAsync(NodeProfile node, CancellationToken cancellationToken)
{
try
{
var (_, latency) = await apiClient.GetHealthAsync(node.ApiUri, cancellationToken).ConfigureAwait(false);
return new HealthResult(node, true, latency);
}
catch (Exception exception) when (exception is HttpRequestException or TaskCanceledException or InvalidDataException)
{
return new HealthResult(node, false, TimeSpan.MaxValue);
}
}
private sealed record HealthResult(NodeProfile Node, bool Success, TimeSpan Latency);
}
using System.Collections.Concurrent;
using LumaTunnel.Client.Core.Abstractions;
using LumaTunnel.Shared.Models;
namespace LumaTunnel.Client.Core.Nodes;
public sealed class NodeManagerService(
NodeApiClient apiClient,
ITunnelService tunnelService,
Func<NodeProfile, CancellationToken, Task<string?>> getDeviceToken,
string clientVersion) : INodeManagerService
{
private readonly ConcurrentDictionary<Guid, int> _failures = new();
private readonly SemaphoreSlim _switchGate = new(1, 1);
private volatile NodeProfile[] _nodes = [];
private CancellationTokenSource? _monitorCancellation;
private Task? _monitorTask;
public NodeProfile? ActiveNode { get; private set; }
public IReadOnlyList<NodeProfile> Nodes => _nodes;
public event EventHandler<NodeProfile?>? ActiveNodeChanged;
public void SetNodes(IEnumerable<NodeProfile> nodes)
{
ArgumentNullException.ThrowIfNull(nodes);
_nodes = nodes.OrderBy(static node => node.Priority).ToArray();
}
public async Task<NodeProfile> ConnectBestAsync(CancellationToken cancellationToken = default)
{
await _switchGate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
var candidates = _nodes.Where(static node => node.Enabled && node.DeviceId is not null).ToArray();
if (candidates.Length == 0)
throw new InvalidOperationException("No enrolled LumaTunnel node is enabled.");
var checks = await Task.WhenAll(candidates.Select(node => CheckNodeAsync(node, cancellationToken))).ConfigureAwait(false);
var best = checks.Where(static result => result.Success).OrderBy(static result => result.Latency).ThenBy(static result => result.Node.Priority).FirstOrDefault()
?? throw new IOException("None of the configured LumaTunnel nodes is healthy.");
var token = await getDeviceToken(best.Node, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException($"No device token is available for node '{best.Node.Name}'.");
await tunnelService.ConnectAsync(new TunnelCredentials(best.Node.TunnelUri, best.Node.DeviceId!, token, clientVersion), cancellationToken).ConfigureAwait(false);
ActiveNode = best.Node with { LastLatencyMilliseconds = best.Latency.TotalMilliseconds, LastHealthCheckUtc = DateTimeOffset.UtcNow };
_failures[best.Node.Id] = 0;
ActiveNodeChanged?.Invoke(this, ActiveNode);
return ActiveNode;
}
finally
{
_switchGate.Release();
}
}
public Task StartMonitoringAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (_monitorCancellation is not null)
return Task.CompletedTask;
_monitorCancellation = new CancellationTokenSource();
_monitorTask = MonitorLoopAsync(_monitorCancellation.Token);
return Task.CompletedTask;
}
public async Task StopMonitoringAsync(CancellationToken cancellationToken = default)
{
var cancellation = _monitorCancellation;
var task = _monitorTask;
_monitorCancellation = null;
_monitorTask = null;
if (cancellation is null)
return;
cancellation.Cancel();
if (task is not null)
await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(2), cancellationToken)).ConfigureAwait(false);
cancellation.Dispose();
}
public async ValueTask DisposeAsync()
{
await StopMonitoringAsync().ConfigureAwait(false);
_switchGate.Dispose();
apiClient.Dispose();
GC.SuppressFinalize(this);
}
private async Task MonitorLoopAsync(CancellationToken cancellationToken)
{
try
{
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken).ConfigureAwait(false);
var active = ActiveNode;
if (active is null)
continue;
var check = await CheckNodeAsync(active, cancellationToken).ConfigureAwait(false);
if (check.Success && tunnelService.IsConnected)
{
_failures[active.Id] = 0;
continue;
}
var failures = _failures.AddOrUpdate(active.Id, 1, static (_, count) => count + 1);
if (failures >= 3)
{
try
{
await ConnectBestAsync(cancellationToken).ConfigureAwait(false);
}
catch (IOException)
{
}
}
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
}
}
private async Task<HealthResult> CheckNodeAsync(NodeProfile node, CancellationToken cancellationToken)
{
try
{
var (_, latency) = await apiClient.GetHealthAsync(node.ApiUri, cancellationToken).ConfigureAwait(false);
return new HealthResult(node, true, latency);
}
catch (Exception exception) when (exception is HttpRequestException or TaskCanceledException or InvalidDataException)
{
return new HealthResult(node, false, TimeSpan.MaxValue);
}
}
private sealed record HealthResult(NodeProfile Node, bool Success, TimeSpan Latency);
}