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 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;
}
}
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;
}
}