Files
anti-syktsu-proxy/Program.cs
2026-04-09 20:41:17 +03:00

654 lines
22 KiB
C#

using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static string USER = "user";
static string PASS = "pass";
static readonly string UPSTREAM_USER = Environment.GetEnvironmentVariable("UPSTREAM_USER");
static readonly string UPSTREAM_PASS = Environment.GetEnvironmentVariable("UPSTREAM_PASS");
static readonly bool UPSTREAM_ALLOW_EMPTY_PASSWORD = string.Equals(
Environment.GetEnvironmentVariable("UPSTREAM_ALLOW_EMPTY_PASSWORD"),
"1",
StringComparison.OrdinalIgnoreCase);
static readonly string UPSTREAM_PROXY_AUTH_HEADER = BuildUpstreamProxyAuthHeader();
static long _connectionSeq;
static async Task Main()
{
int port = 8888;
RegisterGlobalExceptionHandlers();
Log.Info("=== LOCAL PROXY ===");
Log.Info($"http://127.0.0.1:{port}");
Log.Info($"login: {USER}");
Log.Info($"pass : {PASS}");
var listener = new TcpListener(IPAddress.Any, port);
listener.Start();
Log.Info($"Listening on {listener.LocalEndpoint}");
while (true)
{
try
{
var client = await listener.AcceptTcpClientAsync();
var connectionId = Interlocked.Increment(ref _connectionSeq);
Log.Info($"[conn:{connectionId}] accepted from {client.Client.RemoteEndPoint}");
_ = Task.Run(() => HandleClient(client, connectionId));
}
catch (Exception ex)
{
Log.Error("AcceptTcpClientAsync failed", ex);
}
}
}
static void RegisterGlobalExceptionHandlers()
{
AppDomain.CurrentDomain.UnhandledException += (_, args) =>
Log.Error("Unhandled exception", args.ExceptionObject as Exception);
TaskScheduler.UnobservedTaskException += (_, args) =>
{
Log.Error("Unobserved task exception", args.Exception);
args.SetObserved();
};
}
static async Task HandleClient(TcpClient client, long connectionId)
{
using (client)
{
try
{
var stream = client.GetStream();
var request = await ReadHeaders(stream, $"conn:{connectionId} client-request");
if (request == null)
{
Log.Warn($"[conn:{connectionId}] empty or invalid request");
return;
}
Log.Info($"[conn:{connectionId}] {request.Method} {request.Target}");
if (!CheckAuth(request.Raw))
{
Log.Warn($"[conn:{connectionId}] proxy auth failed");
await Write407(stream);
return;
}
if (request.Method == "CONNECT")
await HandleConnect(stream, request.Target, connectionId);
else
await HandleHttp(stream, request, connectionId);
}
catch (Exception ex)
{
Log.Error($"[conn:{connectionId}] client handling failed", ex);
}
finally
{
Log.Info($"[conn:{connectionId}] closed");
}
}
}
// ================= HTTP =================
static async Task HandleHttp(NetworkStream clientStream, HttpRequest req, long connectionId)
{
var proxyUri = WinHttpHelper.GetProxyForUrl(new Uri(req.Url));
if (proxyUri == null)
{
Log.Error($"[conn:{connectionId}] no upstream proxy for {req.Url}");
await Write502(clientStream);
return;
}
Log.Info($"[conn:{connectionId}] HTTP upstream {proxyUri.Host}:{proxyUri.Port} for {req.Url}");
using var upstream = new TcpClient();
await upstream.ConnectAsync(proxyUri.Host, proxyUri.Port);
var upstreamStream = upstream.GetStream();
var outbound = BuildUpstreamHttpRequest(req);
await upstreamStream.WriteAsync(outbound);
await Pump(upstreamStream, clientStream, $"conn:{connectionId} HTTP upstream->client");
}
// ================= CONNECT =================
static async Task HandleConnect(NetworkStream clientStream, string target, long connectionId)
{
var parts = target.Split(':');
if (parts.Length != 2 || !int.TryParse(parts[1], out int port))
{
Log.Error($"[conn:{connectionId}] invalid CONNECT target: {target}");
await clientStream.WriteAsync(Encoding.ASCII.GetBytes("HTTP/1.1 400 Bad Request\r\n\r\n"));
return;
}
string host = parts[0];
var proxyUri = WinHttpHelper.GetProxyForUrl(new Uri($"https://{host}:{port}"));
if (proxyUri == null)
{
Log.Error($"[conn:{connectionId}] no upstream proxy for CONNECT {host}:{port}");
await Write502(clientStream);
return;
}
Log.Info($"[conn:{connectionId}] CONNECT {host}:{port} via {proxyUri.Host}:{proxyUri.Port}");
using var upstream = new TcpClient();
await upstream.ConnectAsync(proxyUri.Host, proxyUri.Port);
var upstreamStream = upstream.GetStream();
string connectReq =
$"CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n{UPSTREAM_PROXY_AUTH_HEADER}\r\n";
await upstreamStream.WriteAsync(Encoding.ASCII.GetBytes(connectReq));
var resp = await ReadHeaders(upstreamStream, $"conn:{connectionId} upstream-connect-response");
if (resp == null || !resp.Raw.Contains("200"))
{
var status = resp?.Raw?.Split("\r\n", StringSplitOptions.None).FirstOrDefault() ?? "<no response>";
Log.Error($"[conn:{connectionId}] upstream CONNECT rejected: {status}");
await clientStream.WriteAsync(Encoding.ASCII.GetBytes("HTTP/1.1 502 Bad Gateway\r\n\r\n"));
return;
}
await clientStream.WriteAsync(Encoding.ASCII.GetBytes("HTTP/1.1 200 Connection Established\r\n\r\n"));
var t1 = Pump(upstreamStream, clientStream, $"conn:{connectionId} tunnel upstream->client");
var t2 = Pump(clientStream, upstreamStream, $"conn:{connectionId} tunnel client->upstream");
await Task.WhenAny(t1, t2);
}
// ================= AUTH =================
static bool CheckAuth(string raw)
{
string expected = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{USER}:{PASS}"));
return raw.Contains($"Proxy-Authorization: Basic {expected}");
}
static async Task Write407(NetworkStream stream)
{
string resp =
"HTTP/1.1 407 Proxy Authentication Required\r\n" +
"Proxy-Authenticate: Basic realm=\"proxy\"\r\n\r\n";
await stream.WriteAsync(Encoding.ASCII.GetBytes(resp));
}
static async Task Write502(NetworkStream stream)
{
const string resp = "HTTP/1.1 502 Bad Gateway\r\n\r\n";
await stream.WriteAsync(Encoding.ASCII.GetBytes(resp));
}
// ================= PARSER =================
class HttpRequest
{
public string Method;
public string Target;
public string Url;
public string Raw;
public byte[] RawBytes;
public int HeaderLength;
}
static async Task<HttpRequest> ReadHeaders(Stream stream, string context)
{
try
{
var buffer = new byte[8192];
int read = await stream.ReadAsync(buffer);
if (read <= 0) return null;
string raw = Encoding.ASCII.GetString(buffer, 0, read);
var lines = raw.Split("\r\n");
var first = lines[0].Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (first.Length < 2)
{
Log.Error($"[{context}] invalid request line: {lines[0]}");
return null;
}
var req = new HttpRequest
{
Method = first[0],
Target = first[1],
Raw = raw,
RawBytes = buffer.Take(read).ToArray(),
HeaderLength = GetHeaderLength(raw)
};
if (req.Method != "CONNECT")
{
string host = lines
.FirstOrDefault(l => l.StartsWith("Host:", StringComparison.OrdinalIgnoreCase))
?.Split(':', 2)[1]
.Trim();
req.Url = req.Target.StartsWith("http", StringComparison.OrdinalIgnoreCase)
? req.Target
: $"http://{host}{req.Target}";
}
return req;
}
catch (Exception ex)
{
Log.Error($"[{context}] failed to read/parse headers", ex);
return null;
}
}
// ================= STREAM =================
static async Task Pump(Stream from, Stream to, string context)
{
var buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
while (true)
{
int read = await from.ReadAsync(buffer);
if (read <= 0) break;
await to.WriteAsync(buffer.AsMemory(0, read));
}
}
catch (Exception ex)
{
if (IsExpectedDisconnect(ex))
Log.Info($"[{context}] stream closed: {OneLine(ex.Message)}");
else
Log.Error($"[{context}] stream pump failed", ex);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
static bool IsExpectedDisconnect(Exception ex)
{
if (ex is ObjectDisposedException)
return true;
if (ex is OperationCanceledException)
return true;
if (ex is IOException io && io.InnerException is SocketException se)
{
return se.SocketErrorCode == SocketError.ConnectionAborted
|| se.SocketErrorCode == SocketError.ConnectionReset
|| se.SocketErrorCode == SocketError.Shutdown
|| se.SocketErrorCode == SocketError.OperationAborted
|| se.SocketErrorCode == SocketError.TimedOut;
}
return false;
}
static string OneLine(string message)
{
if (string.IsNullOrEmpty(message))
return string.Empty;
return message.Replace("\r", " ").Replace("\n", " ").Trim();
}
static int GetHeaderLength(string raw)
{
int idx = raw.IndexOf("\r\n\r\n", StringComparison.Ordinal);
return idx >= 0 ? idx + 4 : raw.Length;
}
static byte[] BuildUpstreamHttpRequest(HttpRequest req)
{
var lines = req.Raw.Split("\r\n", StringSplitOptions.None);
var sb = new StringBuilder();
sb.Append(lines[0]).Append("\r\n");
for (int i = 1; i < lines.Length; i++)
{
var line = lines[i];
if (line.Length == 0)
break;
if (line.StartsWith("Proxy-Authorization:", StringComparison.OrdinalIgnoreCase))
continue; // local proxy auth, must not go upstream
if (line.StartsWith("Proxy-Connection:", StringComparison.OrdinalIgnoreCase))
continue;
sb.Append(line).Append("\r\n");
}
if (!string.IsNullOrEmpty(UPSTREAM_PROXY_AUTH_HEADER))
sb.Append(UPSTREAM_PROXY_AUTH_HEADER);
sb.Append("\r\n");
var headerBytes = Encoding.ASCII.GetBytes(sb.ToString());
if (req.HeaderLength >= req.RawBytes.Length)
return headerBytes;
int bodyLen = req.RawBytes.Length - req.HeaderLength;
var result = new byte[headerBytes.Length + bodyLen];
Buffer.BlockCopy(headerBytes, 0, result, 0, headerBytes.Length);
Buffer.BlockCopy(req.RawBytes, req.HeaderLength, result, headerBytes.Length, bodyLen);
return result;
}
static string BuildUpstreamProxyAuthHeader()
{
if (string.IsNullOrWhiteSpace(UPSTREAM_USER))
return string.Empty;
if (string.IsNullOrEmpty(UPSTREAM_PASS) && !UPSTREAM_ALLOW_EMPTY_PASSWORD)
{
Log.Info("UPSTREAM_PASS is empty; upstream Basic auth header is disabled");
return string.Empty;
}
string pass = UPSTREAM_PASS ?? string.Empty;
string encoded = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{UPSTREAM_USER}:{pass}"));
return $"Proxy-Authorization: Basic {encoded}\r\n";
}
}
// ================= WINHTTP =================
static class WinHttpHelper
{
const int WINHTTP_ACCESS_TYPE_NO_PROXY = 1;
const int WINHTTP_AUTOPROXY_AUTO_DETECT = 0x00000001;
const int WINHTTP_AUTOPROXY_CONFIG_URL = 0x00000002;
const int WINHTTP_AUTO_DETECT_TYPE_DHCP = 0x00000001;
const int WINHTTP_AUTO_DETECT_TYPE_DNS_A = 0x00000002;
[DllImport("winhttp.dll", SetLastError = true)]
static extern IntPtr WinHttpOpen(string agent, int accessType, string proxy, string bypass, int flags);
[DllImport("winhttp.dll", SetLastError = true)]
static extern bool WinHttpGetProxyForUrl(
IntPtr hSession,
string url,
ref WINHTTP_AUTOPROXY_OPTIONS options,
out WINHTTP_PROXY_INFO proxyInfo);
[DllImport("winhttp.dll")]
static extern bool WinHttpCloseHandle(IntPtr handle);
[DllImport("winhttp.dll", SetLastError = true)]
static extern bool WinHttpGetDefaultProxyConfiguration(out WINHTTP_PROXY_INFO proxyInfo);
[DllImport("winhttp.dll", SetLastError = true)]
static extern bool WinHttpGetIEProxyConfigForCurrentUser(out WINHTTP_CURRENT_USER_IE_PROXY_CONFIG proxyConfig);
[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr GlobalFree(IntPtr hMem);
static readonly Uri ForcedUpstreamProxy = ReadForcedUpstreamProxy();
public static Uri GetProxyForUrl(Uri url)
{
if (ForcedUpstreamProxy != null)
{
Log.Info($"Using forced upstream proxy {ForcedUpstreamProxy.Host}:{ForcedUpstreamProxy.Port} for {url}");
return ForcedUpstreamProxy;
}
IntPtr session = WinHttpOpen("proxy", WINHTTP_ACCESS_TYPE_NO_PROXY, null, null, 0);
if (session == IntPtr.Zero)
{
Log.Error($"WinHttpOpen failed (Win32={Marshal.GetLastWin32Error()})");
return null;
}
WINHTTP_PROXY_INFO autoInfo = default;
WINHTTP_PROXY_INFO defaultInfo = default;
WINHTTP_CURRENT_USER_IE_PROXY_CONFIG ieConfig = default;
try
{
if (WinHttpGetIEProxyConfigForCurrentUser(out ieConfig))
{
var autoConfigUrl = PtrToString(ieConfig.lpszAutoConfigUrl);
bool hasSupportedAutoConfigUrl = !string.IsNullOrWhiteSpace(autoConfigUrl)
&& (autoConfigUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|| autoConfigUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(autoConfigUrl) && !hasSupportedAutoConfigUrl)
{
Log.Warn($"IE PAC URL has unsupported scheme for WinHTTP: {autoConfigUrl}");
}
var options = new WINHTTP_AUTOPROXY_OPTIONS
{
dwFlags = 0,
dwAutoDetectFlags = WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A,
lpszAutoConfigUrl = hasSupportedAutoConfigUrl ? ieConfig.lpszAutoConfigUrl : IntPtr.Zero,
fAutoLogonIfChallenged = true
};
if (hasSupportedAutoConfigUrl)
options.dwFlags |= WINHTTP_AUTOPROXY_CONFIG_URL;
if (ieConfig.fAutoDetect)
options.dwFlags |= WINHTTP_AUTOPROXY_AUTO_DETECT;
if (options.dwFlags != 0)
{
if (WinHttpGetProxyForUrl(session, url.ToString(), ref options, out autoInfo))
{
var proxyUri = ParseProxyInfo(autoInfo, url.Scheme);
if (proxyUri != null)
{
return proxyUri;
}
}
else
{
int err = Marshal.GetLastWin32Error();
Log.Warn($"WinHttpGetProxyForUrl failed for {url} (Win32={err})");
}
}
var ieStaticProxyRaw = PtrToString(ieConfig.lpszProxy);
var ieStaticProxy = ParseProxyString(ieStaticProxyRaw, url.Scheme);
if (ieStaticProxy != null)
{
Log.Info($"Using IE static proxy {ieStaticProxy.Host}:{ieStaticProxy.Port} for {url}");
return ieStaticProxy;
}
}
else
{
Log.Warn($"WinHttpGetIEProxyConfigForCurrentUser failed (Win32={Marshal.GetLastWin32Error()})");
}
if (WinHttpGetDefaultProxyConfiguration(out defaultInfo))
{
var fallbackProxy = ParseProxyInfo(defaultInfo, url.Scheme);
if (fallbackProxy != null)
{
Log.Info($"Using default WinHTTP proxy {fallbackProxy.Host}:{fallbackProxy.Port} for {url}");
return fallbackProxy;
}
}
else
{
Log.Warn($"WinHttpGetDefaultProxyConfiguration failed (Win32={Marshal.GetLastWin32Error()})");
}
Log.Error($"No system upstream proxy resolved for {url}");
return null;
}
finally
{
FreeWinHttpProxyInfo(autoInfo);
FreeWinHttpProxyInfo(defaultInfo);
FreeIeConfig(ieConfig);
WinHttpCloseHandle(session);
}
}
static string PtrToString(IntPtr ptr)
{
if (ptr == IntPtr.Zero) return null;
return Marshal.PtrToStringUni(ptr);
}
static Uri ParseProxyInfo(WINHTTP_PROXY_INFO info, string scheme)
{
var raw = PtrToString(info.lpszProxy);
return ParseProxyString(raw, scheme);
}
static Uri ParseProxyString(string raw, string scheme)
{
if (string.IsNullOrWhiteSpace(raw))
return null;
var parts = raw.Split(';', StringSplitOptions.RemoveEmptyEntries)
.Select(p => p.Trim())
.ToArray();
string schemeToken = parts.FirstOrDefault(p =>
p.StartsWith($"{scheme}=", StringComparison.OrdinalIgnoreCase));
string genericToken = parts.FirstOrDefault(p => p.StartsWith("http=", StringComparison.OrdinalIgnoreCase))
?? parts.FirstOrDefault(p => p.StartsWith("https=", StringComparison.OrdinalIgnoreCase));
string first = parts.FirstOrDefault();
string token = schemeToken ?? genericToken ?? first;
if (string.IsNullOrWhiteSpace(token))
return null;
var value = token.Contains('=')
? token.Split('=', 2)[1].Trim()
: token;
if (value.StartsWith("PROXY ", StringComparison.OrdinalIgnoreCase))
value = value.Substring(6).Trim();
if (value.Equals("DIRECT", StringComparison.OrdinalIgnoreCase))
return null;
var normalized = value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|| value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
? value
: "http://" + value;
if (!Uri.TryCreate(normalized, UriKind.Absolute, out var uri) || string.IsNullOrWhiteSpace(uri.Host))
{
Log.Warn($"Cannot parse proxy value '{raw}'");
return null;
}
return uri;
}
static Uri ReadForcedUpstreamProxy()
{
var value = Environment.GetEnvironmentVariable("UPSTREAM_PROXY");
if (string.IsNullOrWhiteSpace(value))
return null;
var normalized = value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|| value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
? value
: "http://" + value;
if (!Uri.TryCreate(normalized, UriKind.Absolute, out var uri) || string.IsNullOrWhiteSpace(uri.Host))
{
Log.Error($"Invalid UPSTREAM_PROXY value: '{value}'");
return null;
}
return uri;
}
static void FreeWinHttpProxyInfo(WINHTTP_PROXY_INFO info)
{
if (info.lpszProxy != IntPtr.Zero)
GlobalFree(info.lpszProxy);
if (info.lpszProxyBypass != IntPtr.Zero)
GlobalFree(info.lpszProxyBypass);
}
static void FreeIeConfig(WINHTTP_CURRENT_USER_IE_PROXY_CONFIG config)
{
if (config.lpszAutoConfigUrl != IntPtr.Zero)
GlobalFree(config.lpszAutoConfigUrl);
if (config.lpszProxy != IntPtr.Zero)
GlobalFree(config.lpszProxy);
if (config.lpszProxyBypass != IntPtr.Zero)
GlobalFree(config.lpszProxyBypass);
}
struct WINHTTP_AUTOPROXY_OPTIONS
{
public int dwFlags;
public int dwAutoDetectFlags;
public IntPtr lpszAutoConfigUrl;
public IntPtr lpvReserved;
public int dwReserved;
public bool fAutoLogonIfChallenged;
}
struct WINHTTP_PROXY_INFO
{
public int dwAccessType;
public IntPtr lpszProxy;
public IntPtr lpszProxyBypass;
}
struct WINHTTP_CURRENT_USER_IE_PROXY_CONFIG
{
[MarshalAs(UnmanagedType.Bool)]
public bool fAutoDetect;
public IntPtr lpszAutoConfigUrl;
public IntPtr lpszProxy;
public IntPtr lpszProxyBypass;
}
}
static class Log
{
static readonly object _sync = new();
public static void Info(string message) => Write("INF", message);
public static void Warn(string message) => Write("WRN", message);
public static void Error(string message, Exception ex = null)
{
Write("ERR", ex == null ? message : $"{message}{Environment.NewLine}{ex}");
}
static void Write(string level, string message)
{
lock (_sync)
{
Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] [{level}] {message}");
}
}
}