XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

LumaTunnel

【WinUI】LumaTunnel 是一个面向个人多设备的 Windows 代理系统。客户端在本机提供 HTTP/HTTPS CONNECT 与 SOCKS5 TCP 代理,并通过一个受信任 TLS 证书保护的 WSS 会话,将多个 TCP 流复用到自建 Windows Server 节点。

公开
关注 0 Fork 0 Star 0
UTF-8
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);
}