Enforce upstream-only proxy chaining via system proxy config

This commit is contained in:
2026-04-09 19:57:36 +03:00
parent a9f1a2c84e
commit 8e51bd45fc

View File

@@ -103,6 +103,13 @@ class Program
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();
@@ -130,6 +137,13 @@ class Program
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();
@@ -176,6 +190,12 @@ class Program
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
@@ -287,28 +307,50 @@ static class WinHttpHelper
[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);
public static Uri GetProxyForUrl(Uri url)
{
IntPtr session = WinHttpOpen("proxy", WINHTTP_ACCESS_TYPE_NO_PROXY, null, null, 0);
if (session == IntPtr.Zero)
{
Log.Error($"WinHttpOpen failed (Win32={Marshal.GetLastWin32Error()})");
return url;
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);
var options = new WINHTTP_AUTOPROXY_OPTIONS
{
dwFlags = WINHTTP_AUTOPROXY_AUTO_DETECT | WINHTTP_AUTOPROXY_CONFIG_URL,
dwFlags = 0,
dwAutoDetectFlags = WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A,
lpszAutoConfigUrl = ieConfig.lpszAutoConfigUrl,
fAutoLogonIfChallenged = true
};
if (WinHttpGetProxyForUrl(session, url.ToString(), ref options, out var autoInfo))
if (!string.IsNullOrWhiteSpace(autoConfigUrl))
options.dwFlags |= WINHTTP_AUTOPROXY_CONFIG_URL;
if (ieConfig.fAutoDetect)
options.dwFlags |= WINHTTP_AUTOPROXY_AUTO_DETECT;
if (options.dwFlags != 0)
{
var proxyUri = ParseProxyInfo(autoInfo);
if (WinHttpGetProxyForUrl(session, url.ToString(), ref options, out autoInfo))
{
var proxyUri = ParseProxyInfo(autoInfo, url.Scheme);
if (proxyUri != null)
{
WinHttpCloseHandle(session);
return proxyUri;
}
}
@@ -317,14 +359,27 @@ static class WinHttpHelper
int err = Marshal.GetLastWin32Error();
Log.Warn($"WinHttpGetProxyForUrl failed for {url} (Win32={err})");
}
}
if (WinHttpGetDefaultProxyConfiguration(out var defaultInfo))
var ieStaticProxyRaw = PtrToString(ieConfig.lpszProxy);
var ieStaticProxy = ParseProxyString(ieStaticProxyRaw, url.Scheme);
if (ieStaticProxy != null)
{
var fallbackProxy = ParseProxyInfo(defaultInfo);
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}");
WinHttpCloseHandle(session);
return fallbackProxy;
}
}
@@ -333,9 +388,16 @@ static class WinHttpHelper
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);
Log.Warn($"No upstream proxy resolved for {url}, fallback to direct");
return url;
}
}
static string PtrToString(IntPtr ptr)
@@ -344,37 +406,45 @@ static class WinHttpHelper
return Marshal.PtrToStringUni(ptr);
}
static Uri ParseProxyInfo(WINHTTP_PROXY_INFO info)
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;
// Common WinHTTP formats:
// "proxy.local:8080"
// "http=proxy.local:8080;https=proxy.local:8443"
var token = raw.Split(';', StringSplitOptions.RemoveEmptyEntries)
var parts = raw.Split(';', StringSplitOptions.RemoveEmptyEntries)
.Select(p => p.Trim())
.FirstOrDefault(p => p.StartsWith("https=", StringComparison.OrdinalIgnoreCase))
?? raw.Split(';', StringSplitOptions.RemoveEmptyEntries)
.Select(p => p.Trim())
.FirstOrDefault(p => p.StartsWith("http=", StringComparison.OrdinalIgnoreCase))
?? raw.Split(';', StringSplitOptions.RemoveEmptyEntries)
.Select(p => p.Trim())
.FirstOrDefault();
.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 hostPort = token.Contains('=')
var value = token.Contains('=')
? token.Split('=', 2)[1].Trim()
: token;
var normalized = hostPort.StartsWith("http", StringComparison.OrdinalIgnoreCase)
? hostPort
: "http://" + hostPort;
if (value.StartsWith("PROXY ", StringComparison.OrdinalIgnoreCase))
value = value.Substring(6).Trim();
if (value.Equals("DIRECT", StringComparison.OrdinalIgnoreCase))
return null;
if (!Uri.TryCreate(normalized, UriKind.Absolute, out var uri))
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;
@@ -383,6 +453,24 @@ static class WinHttpHelper
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;
@@ -399,6 +487,15 @@ static class WinHttpHelper
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