LumaTunnel
【WinUI】LumaTunnel 是一个面向个人多设备的 Windows 代理系统。客户端在本机提供 HTTP/HTTPS CONNECT 与 SOCKS5 TCP 代理,并通过一个受信任 TLS 证书保护的 WSS 会话,将多个 TCP 流复用到自建 Windows Server 节点。
关注
0
Fork
0
Star
0
using System.Net;
using System.Net.Sockets;
using LumaTunnel.Client.Core.Abstractions;
using LumaTunnel.Shared.Models;
namespace LumaTunnel.Client.Core.Routing;
public sealed class RoutingService : IRoutingService
{
private volatile CompiledRule[] _rules = [];
public ProxyMode Mode { get; set; } = ProxyMode.Rule;
public IReadOnlyList<RoutingRule> Rules => _rules.Select(static item => item.Source).ToArray();
public void SetRules(IEnumerable<RoutingRule> rules)
{
ArgumentNullException.ThrowIfNull(rules);
_rules = rules
.Where(static rule => rule.Enabled)
.OrderBy(static rule => rule.Priority)
.Select(Compile)
.ToArray();
}
public RouteAction Resolve(ProxyTarget target)
{
ArgumentNullException.ThrowIfNull(target);
if (IsForcedDirect(target))
return RouteAction.Direct;
if (Mode == ProxyMode.Direct)
return RouteAction.Direct;
if (Mode == ProxyMode.Global)
return RouteAction.Tunnel;
foreach (var rule in _rules)
{
if (rule.Matches(target))
return rule.Source.Action;
}
return RouteAction.Tunnel;
}
public static bool IsForcedDirect(ProxyTarget target)
{
var host = target.Host.TrimEnd('.');
if (host.Equals("localhost", StringComparison.OrdinalIgnoreCase)
|| host.EndsWith(".local", StringComparison.OrdinalIgnoreCase)
|| host.EndsWith(".lan", StringComparison.OrdinalIgnoreCase))
return true;
return (target.Address ?? (IPAddress.TryParse(host, out var parsed) ? parsed : null)) is { } address
&& IsPrivateOrLocal(address);
}
public static bool IsPrivateOrLocal(IPAddress address)
{
if (IPAddress.IsLoopback(address) || address.IsIPv6LinkLocal || address.IsIPv6SiteLocal)
return true;
var bytes = address.GetAddressBytes();
if (address.AddressFamily == AddressFamily.InterNetwork)
{
return bytes[0] == 10
|| bytes[0] == 127
|| bytes[0] == 169 && bytes[1] == 254
|| bytes[0] == 172 && bytes[1] is >= 16 and <= 31
|| bytes[0] == 192 && bytes[1] == 168;
}
return address.Equals(IPAddress.IPv6Loopback)
|| (bytes[0] & 0xFE) == 0xFC;
}
private static CompiledRule Compile(RoutingRule source)
{
var value = source.Value.Trim();
return source.Kind switch
{
RuleKind.Domain => new CompiledRule(source, target => target.Host.TrimEnd('.').Equals(value.TrimEnd('.'), StringComparison.OrdinalIgnoreCase)),
RuleKind.DomainSuffix => new CompiledRule(source, target => DomainMatchesSuffix(target.Host, value)),
RuleKind.Port when int.TryParse(value, out var port) && port is > 0 and <= 65535 => new CompiledRule(source, target => target.Port == port),
RuleKind.IpCidr when IpCidr.TryParse(value, out var cidr) => new CompiledRule(source, target => GetAddress(target) is { } address && cidr.Contains(address)),
_ => throw new ArgumentException($"Invalid {source.Kind} routing rule value: '{source.Value}'.", nameof(source))
};
}
private static bool DomainMatchesSuffix(string host, string suffix)
{
host = host.TrimEnd('.');
suffix = suffix.Trim().TrimStart('.').TrimEnd('.');
return host.Equals(suffix, StringComparison.OrdinalIgnoreCase)
|| host.EndsWith('.' + suffix, StringComparison.OrdinalIgnoreCase);
}
private static IPAddress? GetAddress(ProxyTarget target) =>
target.Address ?? (IPAddress.TryParse(target.Host, out var parsed) ? parsed : null);
private sealed record CompiledRule(RoutingRule Source, Func<ProxyTarget, bool> Matches);
}
using System.Net;
using System.Net.Sockets;
using LumaTunnel.Client.Core.Abstractions;
using LumaTunnel.Shared.Models;
namespace LumaTunnel.Client.Core.Routing;
public sealed class RoutingService : IRoutingService
{
private volatile CompiledRule[] _rules = [];
public ProxyMode Mode { get; set; } = ProxyMode.Rule;
public IReadOnlyList<RoutingRule> Rules => _rules.Select(static item => item.Source).ToArray();
public void SetRules(IEnumerable<RoutingRule> rules)
{
ArgumentNullException.ThrowIfNull(rules);
_rules = rules
.Where(static rule => rule.Enabled)
.OrderBy(static rule => rule.Priority)
.Select(Compile)
.ToArray();
}
public RouteAction Resolve(ProxyTarget target)
{
ArgumentNullException.ThrowIfNull(target);
if (IsForcedDirect(target))
return RouteAction.Direct;
if (Mode == ProxyMode.Direct)
return RouteAction.Direct;
if (Mode == ProxyMode.Global)
return RouteAction.Tunnel;
foreach (var rule in _rules)
{
if (rule.Matches(target))
return rule.Source.Action;
}
return RouteAction.Tunnel;
}
public static bool IsForcedDirect(ProxyTarget target)
{
var host = target.Host.TrimEnd('.');
if (host.Equals("localhost", StringComparison.OrdinalIgnoreCase)
|| host.EndsWith(".local", StringComparison.OrdinalIgnoreCase)
|| host.EndsWith(".lan", StringComparison.OrdinalIgnoreCase))
return true;
return (target.Address ?? (IPAddress.TryParse(host, out var parsed) ? parsed : null)) is { } address
&& IsPrivateOrLocal(address);
}
public static bool IsPrivateOrLocal(IPAddress address)
{
if (IPAddress.IsLoopback(address) || address.IsIPv6LinkLocal || address.IsIPv6SiteLocal)
return true;
var bytes = address.GetAddressBytes();
if (address.AddressFamily == AddressFamily.InterNetwork)
{
return bytes[0] == 10
|| bytes[0] == 127
|| bytes[0] == 169 && bytes[1] == 254
|| bytes[0] == 172 && bytes[1] is >= 16 and <= 31
|| bytes[0] == 192 && bytes[1] == 168;
}
return address.Equals(IPAddress.IPv6Loopback)
|| (bytes[0] & 0xFE) == 0xFC;
}
private static CompiledRule Compile(RoutingRule source)
{
var value = source.Value.Trim();
return source.Kind switch
{
RuleKind.Domain => new CompiledRule(source, target => target.Host.TrimEnd('.').Equals(value.TrimEnd('.'), StringComparison.OrdinalIgnoreCase)),
RuleKind.DomainSuffix => new CompiledRule(source, target => DomainMatchesSuffix(target.Host, value)),
RuleKind.Port when int.TryParse(value, out var port) && port is > 0 and <= 65535 => new CompiledRule(source, target => target.Port == port),
RuleKind.IpCidr when IpCidr.TryParse(value, out var cidr) => new CompiledRule(source, target => GetAddress(target) is { } address && cidr.Contains(address)),
_ => throw new ArgumentException($"Invalid {source.Kind} routing rule value: '{source.Value}'.", nameof(source))
};
}
private static bool DomainMatchesSuffix(string host, string suffix)
{
host = host.TrimEnd('.');
suffix = suffix.Trim().TrimStart('.').TrimEnd('.');
return host.Equals(suffix, StringComparison.OrdinalIgnoreCase)
|| host.EndsWith('.' + suffix, StringComparison.OrdinalIgnoreCase);
}
private static IPAddress? GetAddress(ProxyTarget target) =>
target.Address ?? (IPAddress.TryParse(target.Host, out var parsed) ? parsed : null);
private sealed record CompiledRule(RoutingRule Source, Func<ProxyTarget, bool> Matches);
}