Integrate Xray 26.9.9 with the sing-box system tunnel and verify Windows routes during health checks. Improve endpoint selection, network recovery, subscription refresh deadlines, diagnostics, packaging, and regression coverage.
128 lines
6.9 KiB
C#
128 lines
6.9 KiB
C#
using System.Diagnostics;
|
|
using System.Net;
|
|
using QVPN.Core;
|
|
|
|
public static class SubscriptionDownloadTests
|
|
{
|
|
private static readonly string ProviderUrl = "https://test.withblancvpn.online/s/" + new string('a', 32);
|
|
|
|
public static async Task HeadersDoNotEndBodyDeadlineAsync()
|
|
{
|
|
using var caller = new CancellationTokenSource();
|
|
using var body = new StallingStream();
|
|
bool headersReturned = false;
|
|
using var handler = new StubHandler(async (_, token) =>
|
|
{
|
|
await Task.Delay(30, token);
|
|
headersReturned = true;
|
|
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(body) };
|
|
});
|
|
using var http = new HttpClient(handler) { Timeout = Timeout.InfiniteTimeSpan };
|
|
var watch = Stopwatch.StartNew();
|
|
try
|
|
{
|
|
try
|
|
{
|
|
await Subscription.FetchWithClientAsync(ProviderUrl, http, TimeSpan.FromMilliseconds(150), caller.Token)
|
|
.WaitAsync(TimeSpan.FromSeconds(1));
|
|
throw new InvalidOperationException("A stalled subscription body unexpectedly completed.");
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
Check(!caller.IsCancellationRequested, "The download required caller cancellation instead of its own body deadline.");
|
|
}
|
|
Check(headersReturned && body.ReadStarted.Task.IsCompleted, "The regression did not reach the streamed body after receiving headers.");
|
|
Check(body.ReadCancelled && body.Disposed, "The timed-out response did not cancel its body read and dispose the transport.");
|
|
Check(watch.Elapsed < TimeSpan.FromSeconds(1), "The complete request exceeded its body deadline.");
|
|
}
|
|
finally { caller.Cancel(); }
|
|
}
|
|
|
|
public static async Task CallerCancellationWinsBodyDeadlineAsync()
|
|
{
|
|
using var caller = new CancellationTokenSource();
|
|
using var body = new StallingStream();
|
|
using var handler = new StubHandler((_, _) => Task.FromResult(
|
|
new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(body) }));
|
|
using var http = new HttpClient(handler) { Timeout = Timeout.InfiniteTimeSpan };
|
|
var running = Subscription.FetchWithClientAsync(ProviderUrl, http, TimeSpan.FromSeconds(5), caller.Token);
|
|
await body.ReadStarted.Task.WaitAsync(TimeSpan.FromSeconds(1));
|
|
caller.Cancel();
|
|
try
|
|
{
|
|
await running.WaitAsync(TimeSpan.FromSeconds(1));
|
|
throw new InvalidOperationException("Caller cancellation was converted into a successful subscription.");
|
|
}
|
|
catch (OperationCanceledException error)
|
|
{
|
|
Check(error.CancellationToken == caller.Token, "The shorter caller deadline lost its original cancellation token.");
|
|
}
|
|
Check(body.ReadCancelled && body.Disposed, "Caller cancellation did not stop and dispose the streamed response.");
|
|
}
|
|
|
|
public static async Task CompletedBodyPreservesValidationAsync()
|
|
{
|
|
int requests = 0;
|
|
using var handler = new StubHandler((request, _) =>
|
|
{
|
|
Interlocked.Increment(ref requests);
|
|
Check(request.RequestUri!.Scheme == Uri.UriSchemeHttps && request.RequestUri.Host == "test.withblancvpn.online",
|
|
"The bounded download changed the validated HTTPS request.");
|
|
string uuid = new Guid(Enumerable.Range(1, 16).Select(i => (byte)i).ToArray()).ToString();
|
|
string key = Convert.ToBase64String(Enumerable.Range(1, 32).Select(i => (byte)i).ToArray()).TrimEnd('=').Replace('+', '-').Replace('/', '_');
|
|
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
|
{
|
|
Content = new StringContent($"vless://{uuid}@8.8.8.8:443?security=reality&type=tcp&pbk={key}&sid=abcd&sni=example.com&fp=chrome&flow=xtls-rprx-vision-udp443#Synthetic")
|
|
};
|
|
response.Headers.TryAddWithoutValidation("subscription-userinfo", "upload=0; download=1");
|
|
return Task.FromResult(response);
|
|
});
|
|
using var http = new HttpClient(handler) { Timeout = Timeout.InfiniteTimeSpan };
|
|
var result = await Subscription.FetchWithClientAsync(ProviderUrl, http, TimeSpan.FromSeconds(1), CancellationToken.None);
|
|
Check(result.Nodes.Count == 1 && result.Nodes[0].Flow == "xtls-rprx-vision-udp443" && result.Info == "upload=0; download=1",
|
|
"A complete bounded response lost nodes, supported transport fields, or subscription metadata.");
|
|
try
|
|
{
|
|
await Subscription.FetchWithClientAsync("https://example.org/s/" + new string('a', 32), http, TimeSpan.FromSeconds(1), CancellationToken.None);
|
|
throw new InvalidOperationException("Injected transport bypassed provider validation.");
|
|
}
|
|
catch (ArgumentException) { }
|
|
Check(requests == 1, "An untrusted provider URL reached the injected transport.");
|
|
}
|
|
|
|
private static void Check(bool condition, string message)
|
|
{
|
|
if (!condition) throw new InvalidOperationException(message);
|
|
}
|
|
|
|
private sealed class StubHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> send) : HttpMessageHandler
|
|
{
|
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token) => send(request, token);
|
|
}
|
|
|
|
private sealed class StallingStream : Stream
|
|
{
|
|
public TaskCompletionSource ReadStarted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
public bool ReadCancelled { get; private set; }
|
|
public bool Disposed { get; private set; }
|
|
public override bool CanRead => true;
|
|
public override bool CanSeek => false;
|
|
public override bool CanWrite => false;
|
|
public override long Length => throw new NotSupportedException();
|
|
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
|
|
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken token = default)
|
|
{
|
|
ReadStarted.TrySetResult();
|
|
try { await Task.Delay(Timeout.InfiniteTimeSpan, token); }
|
|
catch (OperationCanceledException) { ReadCancelled = true; throw; }
|
|
return 0;
|
|
}
|
|
protected override void Dispose(bool disposing) { Disposed = true; base.Dispose(disposing); }
|
|
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
|
|
public override void Flush() => throw new NotSupportedException();
|
|
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
|
public override void SetLength(long value) => throw new NotSupportedException();
|
|
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
|
|
}
|
|
}
|