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 System.Text;
using LumaTunnel.Client.Core.Abstractions;
using LumaTunnel.Client.Core.Networking;
using LumaTunnel.Client.Core.Proxy;
using LumaTunnel.Client.Core.Routing;
using LumaTunnel.Shared.Models;

namespace LumaTunnel.EndToEnd.Tests;

public sealed class ProxySmokeTests
{
    [Fact]
    public async Task AbsoluteHttpRequestIsForwardedThroughLocalProxy()
    {
        var origin = new TcpListener(IPAddress.Loopback, 0);
        origin.Start();
        var originPort = ((IPEndPoint)origin.LocalEndpoint).Port;
        var originTask = Task.Run(async () =>
        {
            using var accepted = await origin.AcceptTcpClientAsync();
            var buffer = new byte[4096];
            var count = await accepted.GetStream().ReadAsync(buffer);
            var header = Encoding.ASCII.GetString(buffer, 0, count);
            Assert.StartsWith("GET /hello?value=1 HTTP/1.1", header, StringComparison.Ordinal);
            await accepted.GetStream().WriteAsync("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nOK"u8.ToArray());
        });

        var httpPort = GetFreePort();
        var socksPort = GetFreePort();
        var routing = new RoutingService { Mode = ProxyMode.Direct };
        await using var tunnel = new UnusedTunnelService();
        await using var engine = new ProxyEngineService(routing, new DirectConnector(), tunnel);
        await engine.StartAsync(new ProxyEngineOptions { HttpPort = httpPort, SocksPort = socksPort, Mode = ProxyMode.Direct });

        using var client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, httpPort);
        var request = Encoding.ASCII.GetBytes($"GET http://127.0.0.1:{originPort}/hello?value=1 HTTP/1.1\r\nHost: 127.0.0.1:{originPort}\r\nConnection: close\r\n\r\n");
        await client.GetStream().WriteAsync(request);
        var response = new byte[4096];
        var responseLength = await client.GetStream().ReadAsync(response);
        Assert.Contains("200 OK", Encoding.ASCII.GetString(response, 0, responseLength), StringComparison.Ordinal);

        await originTask.WaitAsync(TimeSpan.FromSeconds(5));
        await engine.StopAsync();
        origin.Stop();
    }

    [Fact]
    public async Task Socks5DomainConnectRelaysTcpBytes()
    {
        var origin = new TcpListener(IPAddress.Loopback, 0);
        origin.Start();
        var originPort = ((IPEndPoint)origin.LocalEndpoint).Port;
        var originTask = Task.Run(async () =>
        {
            using var accepted = await origin.AcceptTcpClientAsync();
            var request = new byte[4];
            await accepted.GetStream().ReadExactlyAsync(request);
            Assert.Equal("ping", Encoding.ASCII.GetString(request));
            await accepted.GetStream().WriteAsync("pong"u8.ToArray());
        });

        var httpPort = GetFreePort();
        var socksPort = GetFreePort();
        var routing = new RoutingService { Mode = ProxyMode.Direct };
        await using var tunnel = new UnusedTunnelService();
        await using var engine = new ProxyEngineService(routing, new DirectConnector(), tunnel);
        await engine.StartAsync(new ProxyEngineOptions { HttpPort = httpPort, SocksPort = socksPort, Mode = ProxyMode.Direct });

        using var client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, socksPort);
        var stream = client.GetStream();
        await stream.WriteAsync(new byte[] { 5, 1, 0 });
        var methodReply = new byte[2];
        await stream.ReadExactlyAsync(methodReply);
        Assert.Equal(new byte[] { 5, 0 }, methodReply);

        var domain = Encoding.ASCII.GetBytes("localhost");
        var connect = new byte[5 + domain.Length + 2];
        connect[0] = 5; connect[1] = 1; connect[2] = 0; connect[3] = 3; connect[4] = checked((byte)domain.Length);
        domain.CopyTo(connect, 5);
        System.Buffers.Binary.BinaryPrimitives.WriteUInt16BigEndian(connect.AsSpan(5 + domain.Length), checked((ushort)originPort));
        await stream.WriteAsync(connect);
        var replyHeader = new byte[4];
        await stream.ReadExactlyAsync(replyHeader);
        Assert.Equal(0, replyHeader[1]);
        var addressLength = replyHeader[3] == 1 ? 4 : 16;
        var replyTail = new byte[addressLength + 2];
        await stream.ReadExactlyAsync(replyTail);

        await stream.WriteAsync("ping"u8.ToArray());
        var response = new byte[4];
        await stream.ReadExactlyAsync(response);
        Assert.Equal("pong", Encoding.ASCII.GetString(response));

        await originTask.WaitAsync(TimeSpan.FromSeconds(5));
        await engine.StopAsync();
        origin.Stop();
    }

    private static int GetFreePort()
    {
        var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;
        listener.Stop();
        return port;
    }

    private sealed class UnusedTunnelService : ITunnelService, IProxyConnector
    {
        public bool IsConnected => false;
        public event EventHandler<bool>? ConnectionStateChanged { add { } remove { } }
        public Task ConnectAsync(TunnelCredentials credentials, CancellationToken cancellationToken = default) => throw new NotSupportedException();
        public ValueTask<Stream> OpenStreamAsync(ProxyTarget target, CancellationToken cancellationToken = default) => throw new NotSupportedException();
        public ValueTask<Stream> ConnectAsync(ProxyTarget target, CancellationToken cancellationToken = default) => throw new NotSupportedException();
        public Task DisconnectAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
        public ValueTask DisposeAsync() => ValueTask.CompletedTask;
    }
}