diff --git a/.cursor/rules/re-measure-wording.mdc b/.cursor/rules/re-measure-wording.mdc new file mode 100644 index 000000000..75334321f --- /dev/null +++ b/.cursor/rules/re-measure-wording.mdc @@ -0,0 +1,10 @@ +--- +description: Use hyphenated re-measure; never remasure or remeasure +alwaysApply: true +--- + +# Wording: re-measure + +When writing about measuring again (RPS, memory, UI layout), use the hyphenated form **re-measure** (and **re-measurement**). + +Never write `remasure` or `remeasure` in code, comments, docs, wiki, the website, commit messages, or plans. diff --git a/.github/workflows/rps-saturation.yml b/.github/workflows/rps-saturation.yml index f3574d23a..daaa2f0f0 100644 --- a/.github/workflows/rps-saturation.yml +++ b/.github/workflows/rps-saturation.yml @@ -97,6 +97,8 @@ on: - compare-tls-cost - compare-arch - compare-grpc + - compare-ws-h1tls + - compare-ws-h2 - compare-saturation - compare-spot - origin-direct diff --git a/.gitignore b/.gitignore index f59d4ca55..747b0dd3e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. -# Local Cursor agent rules / config (not shared) -.cursor/ +# Local Cursor config (plans, caches). Shared project rules are tracked. +.cursor/* +!.cursor/rules/ +!.cursor/rules/** # User-specific files *.suo diff --git a/docs/api/Titanium.Web.Proxy.EventArguments.SessionEventArgs.html b/docs/api/Titanium.Web.Proxy.EventArguments.SessionEventArgs.html index c97ab8c36..b33271033 100644 --- a/docs/api/Titanium.Web.Proxy.EventArguments.SessionEventArgs.html +++ b/docs/api/Titanium.Web.Proxy.EventArguments.SessionEventArgs.html @@ -231,7 +231,7 @@

Properties Edit this page - View Source + View Source

IdleReadTimeout

@@ -263,7 +263,7 @@
Property Value
Edit this page - View Source + View Source

IdleWriteTimeout

@@ -295,7 +295,7 @@
Property Value
Edit this page - View Source + View Source

IsPromise

@@ -326,7 +326,7 @@
Property Value
Edit this page - View Source + View Source

MaxBufferedBodyBytes

@@ -359,7 +359,7 @@
Property Value
Edit this page - View Source + View Source

MaxWebSocketFramePayloadBytes

@@ -392,7 +392,7 @@
Property Value
Edit this page - View Source + View Source

NetworkFailureRetryAttempts

@@ -425,7 +425,7 @@
Property Value
Edit this page - View Source + View Source

OriginHttpVersionPolicy

@@ -458,7 +458,7 @@
Property Value
Edit this page - View Source + View Source

ReRequest

@@ -489,7 +489,7 @@
Property Value
Edit this page - View Source + View Source

RequestTimeout

@@ -521,7 +521,7 @@
Property Value
Edit this page - View Source + View Source

ResponseHeaderTimeout

@@ -553,7 +553,7 @@
Property Value
Edit this page - View Source + View Source

UpstreamHttpProtocol

@@ -593,7 +593,7 @@
Property Value
Edit this page - View Source + View Source

WebSocketClientWriter

@@ -625,7 +625,7 @@
Property Value
Edit this page - View Source + View Source

WebSocketDecoder

@@ -656,7 +656,7 @@
Property Value
Edit this page - View Source + View Source

WebSocketDecoderReceive

@@ -686,7 +686,7 @@
Property Value
Edit this page - View Source + View Source

WebSocketDecoderSend

@@ -716,7 +716,7 @@
Property Value
Edit this page - View Source + View Source

WebSocketServerWriter

@@ -750,7 +750,7 @@

Methods Edit this page - View Source + View Source

Dispose(bool)

@@ -784,7 +784,7 @@
Overrides
Edit this page - View Source + View Source

DrainClientBodyAsync(CancellationToken)

@@ -843,7 +843,7 @@
Edit this page - View Source + View Source

DrainServerBodyAsync(CancellationToken)

@@ -899,7 +899,7 @@
Edit this page - View Source + View Source

GenericResponse(byte[], HttpStatusCode, IDictionary<string, HttpHeader>, bool)

@@ -952,7 +952,7 @@
Parameters
Edit this page - View Source + View Source

GenericResponse(byte[], HttpStatusCode, IEnumerable<HttpHeader>?, bool)

@@ -1005,7 +1005,7 @@
Parameters
Edit this page - View Source + View Source

GenericResponse(string, HttpStatusCode, IDictionary<string, HttpHeader>?, bool)

@@ -1059,7 +1059,7 @@
Parameters
Edit this page - View Source + View Source

GenericResponse(string, HttpStatusCode, IEnumerable<HttpHeader>?, bool)

@@ -1113,7 +1113,7 @@
Parameters
Edit this page - View Source + View Source

GetRequestBody(CancellationToken)

@@ -1163,7 +1163,7 @@
Returns
Edit this page - View Source + View Source

GetRequestBodyAsString(CancellationToken)

@@ -1213,7 +1213,7 @@
Returns
Edit this page - View Source + View Source

GetResponseBody(CancellationToken)

@@ -1263,7 +1263,7 @@
Returns
Edit this page - View Source + View Source

GetResponseBodyAsString(CancellationToken)

@@ -1313,7 +1313,7 @@
Returns
Edit this page - View Source + View Source

Ok(byte[], IDictionary<string, HttpHeader>?, bool)

@@ -1360,7 +1360,7 @@
Parameters
Edit this page - View Source + View Source

Ok(byte[], IEnumerable<HttpHeader>?, bool)

@@ -1407,7 +1407,7 @@
Parameters
Edit this page - View Source + View Source

Ok(string, IDictionary<string, HttpHeader>?, bool)

@@ -1454,7 +1454,7 @@
Parameters
Edit this page - View Source + View Source

Ok(string, IEnumerable<HttpHeader>?, bool)

@@ -1501,7 +1501,7 @@
Parameters
Edit this page - View Source + View Source

Redirect(string, bool)

@@ -1541,7 +1541,7 @@
Parameters
Edit this page - View Source + View Source

Respond(Response, bool)

@@ -1588,7 +1588,7 @@
Edit this page - View Source + View Source

RespondStreaming(Response, Func<Stream, CancellationToken, Task>, bool)

@@ -1644,7 +1644,7 @@
Edit this page - View Source + View Source

RespondStreaming(StreamingProxyResult, bool)

@@ -1685,7 +1685,7 @@
Parameters
Edit this page - View Source + View Source

SetRequestBody(byte[])

@@ -1719,7 +1719,7 @@
Parameters
Edit this page - View Source + View Source

SetRequestBodyString(string)

@@ -1753,7 +1753,7 @@
Parameters
Edit this page - View Source + View Source

SetResponseBody(byte[])

@@ -1787,7 +1787,7 @@
Parameters
Edit this page - View Source + View Source

SetResponseBodyString(string)

@@ -1821,7 +1821,7 @@
Parameters
Edit this page - View Source + View Source

TerminateServerConnection()

@@ -1839,7 +1839,7 @@

Events Edit this page - View Source + View Source

BeforeWebSocketFrame

Fired for each WebSocket frame after upgrade when at least one handler is subscribed. @@ -1875,7 +1875,7 @@

Event Type
Edit this page - View Source + View Source

MultipartRequestPartSent

Occurs when multipart request part sent.

diff --git a/docs/api/Titanium.Web.Proxy.Http.RequestResponseBase.html b/docs/api/Titanium.Web.Proxy.Http.RequestResponseBase.html index fe35a8c3c..6964c26c9 100644 --- a/docs/api/Titanium.Web.Proxy.Http.RequestResponseBase.html +++ b/docs/api/Titanium.Web.Proxy.Http.RequestResponseBase.html @@ -612,7 +612,7 @@

Methods Edit this page - View Source + View Source

ToString()

diff --git a/docs/api/Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html b/docs/api/Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html new file mode 100644 index 000000000..7427fcfd7 --- /dev/null +++ b/docs/api/Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html @@ -0,0 +1,374 @@ + + + + + + + + Class DecryptFailureBypassEntry | Titanium Web Proxy + + + + + + + + + + + + + + + +
+
+ +
+
+
+ + + + + +
+
+
+
+ +
+
+
+
+
+ +
+
+
    +
  • +
+
+
+
+
+ +
+
Search Results for
+
+

+
+
    +
    +
    +
    + +
    + Show / Hide Table of Contents +
    +
    +
    +
    +
    +
    +
    + + + +

    Class DecryptFailureBypassEntry

    +

    A host that the proxy learned to tunnel without decrypt after repeated origin TLS +handshake failures under MITM (e.g. bot / TLS-fingerprint rejection).

    +
    +
    +
    +
    Inheritance
    +
    object
    +
    DecryptFailureBypassEntry
    +
    +
    +
    Inherited Members
    +
    + object.Equals(object) +
    +
    + object.Equals(object, object) +
    +
    + object.GetHashCode() +
    +
    + object.GetType() +
    +
    + object.ReferenceEquals(object, object) +
    +
    + object.ToString() +
    +
    +
    Namespace: Titanium.Web.Proxy.Models
    +
    Assembly: Titanium.Web.Proxy.dll
    +
    Syntax
    +
    +
    public sealed class DecryptFailureBypassEntry
    +
    +

    Constructors +

    + + | + Edit this page + + + View Source + + +

    DecryptFailureBypassEntry(string, int, DateTime, DateTime, bool)

    +
    +
    +
    Declaration
    +
    +
    public DecryptFailureBypassEntry(string host, int strikes, DateTime learnedAtUtc, DateTime expiresAtUtc, bool bypassActive)
    +
    +
    Parameters
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    TypeNameDescription
    stringhost
    intstrikes
    DateTimelearnedAtUtc
    DateTimeexpiresAtUtc
    boolbypassActive
    +

    Properties +

    + + | + Edit this page + + + View Source + + +

    BypassActive

    +

    True when subsequent CONNECTs skip decrypt for this host.

    +
    +
    +
    Declaration
    +
    +
    public bool BypassActive { get; }
    +
    +
    Property Value
    + + + + + + + + + + + + + +
    TypeDescription
    bool
    + + | + Edit this page + + + View Source + + +

    ExpiresAtUtc

    +

    When this entry expires if not refreshed.

    +
    +
    +
    Declaration
    +
    +
    public DateTime ExpiresAtUtc { get; }
    +
    +
    Property Value
    + + + + + + + + + + + + + +
    TypeDescription
    DateTime
    + + | + Edit this page + + + View Source + + +

    Host

    +

    Normalized hostname (no port).

    +
    +
    +
    Declaration
    +
    +
    public string Host { get; }
    +
    +
    Property Value
    + + + + + + + + + + + + + +
    TypeDescription
    string
    + + | + Edit this page + + + View Source + + +

    LearnedAtUtc

    +

    When the first strike (or forced bypass) was recorded.

    +
    +
    +
    Declaration
    +
    +
    public DateTime LearnedAtUtc { get; }
    +
    +
    Property Value
    + + + + + + + + + + + + + +
    TypeDescription
    DateTime
    + + | + Edit this page + + + View Source + + +

    Strikes

    +

    Origin TLS failure strikes accumulated toward the bypass threshold.

    +
    +
    +
    Declaration
    +
    +
    public int Strikes { get; }
    +
    +
    Property Value
    + + + + + + + + + + + + + +
    TypeDescription
    int
    + +
    +
    + +
    +
    +
    +
      +
    • + Edit this page +
    • +
    • + View Source +
    • +
    +
    +
    +
    In this article
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + + Back to top + + + Generated by DocFX +
    +
    +
    +
    + + + + + + diff --git a/docs/api/Titanium.Web.Proxy.Models.html b/docs/api/Titanium.Web.Proxy.Models.html index d276cb529..21d454d3f 100644 --- a/docs/api/Titanium.Web.Proxy.Models.html +++ b/docs/api/Titanium.Web.Proxy.Models.html @@ -85,6 +85,10 @@

    Classes

    +

    DecryptFailureBypassEntry

    +

    A host that the proxy learned to tunnel without decrypt after repeated origin TLS +handshake failures under MITM (e.g. bot / TLS-fingerprint rejection).

    +

    ExplicitProxyEndPoint

    A proxy endpoint that the client is aware of. So client application know that it is communicating with a proxy server.

    diff --git a/docs/api/Titanium.Web.Proxy.ProxyServer.html b/docs/api/Titanium.Web.Proxy.ProxyServer.html index 54ffb4268..5d791f4a5 100644 --- a/docs/api/Titanium.Web.Proxy.ProxyServer.html +++ b/docs/api/Titanium.Web.Proxy.ProxyServer.html @@ -259,7 +259,7 @@

    Fields Edit this page - View Source + View Source

    DefaultViaHeaderPseudonym

    Default Via header pseudonym (RFC 9110 §7.6.3). Used by ViaHeaderPseudonym @@ -292,7 +292,7 @@

    Properties Edit this page - View Source + View Source

    AdmittedClientConnectionCount

    @@ -326,7 +326,7 @@
    Property Value
    Edit this page - View Source + View Source

    BlockPrivateNetworkDestinations

    @@ -378,7 +378,7 @@
    Property Value
    Edit this page - View Source + View Source

    BufferPool

    @@ -412,7 +412,7 @@
    Property Value
    Edit this page - View Source + View Source

    CertificateManager

    @@ -443,7 +443,7 @@
    Property Value
    Edit this page - View Source + View Source

    CheckCertificateRevocation

    @@ -475,7 +475,7 @@
    Property Value
    Edit this page - View Source + View Source

    ClientConnectionCount

    @@ -507,7 +507,7 @@
    Property Value
    Edit this page - View Source + View Source

    ClientHeaderTimeoutSeconds

    @@ -547,7 +547,7 @@
    Property Value
    Edit this page - View Source + View Source

    CompatibilityMode100Continue

    @@ -585,7 +585,7 @@
    Property Value
    Edit this page - View Source + View Source

    ConnectTimeOutSeconds

    @@ -617,7 +617,7 @@
    Property Value
    Edit this page - View Source + View Source

    ConnectionTimeOutSeconds

    @@ -650,7 +650,7 @@
    Property Value
    Edit this page - View Source + View Source

    CustomUpStreamProxyFailureFunc

    @@ -677,12 +677,107 @@
    Property Value
    + + | + Edit this page + + + View Source + + +

    DecryptFailureBypassMaxEntries

    +

    Maximum learned hosts retained (approximate LRU eviction). Default 256.

    +
    +
    +
    Declaration
    +
    +
    public int DecryptFailureBypassMaxEntries { get; set; }
    +
    +
    Property Value
    + + + + + + + + + + + + + +
    TypeDescription
    int
    + + | + Edit this page + + + View Source + + +

    DecryptFailureBypassThreshold

    +

    Origin TLS failure strikes required before a host is bypassed on later CONNECTs. +Same-CONNECT opaque fallback after an awaited H2 probe failure marks bypass immediately. +Default 2.

    +
    +
    +
    Declaration
    +
    +
    public int DecryptFailureBypassThreshold { get; set; }
    +
    +
    Property Value
    + + + + + + + + + + + + + +
    TypeDescription
    int
    + + | + Edit this page + + + View Source + + +

    DecryptFailureBypassTtl

    +

    How long a learned decrypt-bypass entry remains valid. Default 30 minutes.

    +
    +
    +
    Declaration
    +
    +
    public TimeSpan DecryptFailureBypassTtl { get; set; }
    +
    +
    Property Value
    + + + + + + + + + + + + + +
    TypeDescription
    TimeSpan
    | Edit this page - View Source + View Source

    DnsServerEndPoint

    @@ -722,7 +817,7 @@
    Property Value
    Edit this page - View Source + View Source

    Enable100ContinueBehaviour

    @@ -755,7 +850,7 @@
    Property Value
    Edit this page - View Source + View Source

    EnableConnectionPool

    @@ -792,12 +887,47 @@
    Property Value
    + + | + Edit this page + + + View Source + + +

    EnableDecryptFailureBypass

    +

    When true, the proxy learns hosts whose origin TLS handshake fails under +MITM (non-ALPN AuthenticationException, typically +bot / TLS-fingerprint rejection) and tunnels subsequent CONNECTs without decrypt. +Default false so library and RPS baselines are unchanged. Inspector enables +this by default. Success-path cost when on is one dictionary lookup per CONNECT.

    +
    +
    +
    Declaration
    +
    +
    public bool EnableDecryptFailureBypass { get; set; }
    +
    +
    Property Value
    + + + + + + + + + + + + + +
    TypeDescription
    bool
    | Edit this page - View Source + View Source

    EnableHttp2

    @@ -841,7 +971,7 @@
    Property Value
    Edit this page - View Source + View Source

    EnableHttp3

    @@ -897,7 +1027,7 @@
    Property Value
    Edit this page - View Source + View Source

    EnableHttpInterception

    @@ -930,7 +1060,7 @@
    Property Value
    Edit this page - View Source + View Source

    EnableHttpsSvcbDnsDiscovery

    @@ -972,7 +1102,7 @@
    Property Value
    Edit this page - View Source + View Source

    EnableIpv6UnreachableSoftSkip

    @@ -1007,7 +1137,7 @@
    Property Value
    Edit this page - View Source + View Source

    EnableQpackDynamicTable

    @@ -1042,7 +1172,7 @@
    Property Value
    Edit this page - View Source + View Source

    EnableRequestTimingCapture

    @@ -1089,7 +1219,7 @@
    Property Value
    Edit this page - View Source + View Source

    EnableRfc8441

    @@ -1135,7 +1265,7 @@
    Property Value
    Edit this page - View Source + View Source

    EnableTcpKeepAlive

    @@ -1167,7 +1297,7 @@
    Property Value
    Edit this page - View Source + View Source

    EnableTcpServerConnectionPrefetch

    @@ -1205,7 +1335,7 @@
    Property Value
    Edit this page - View Source + View Source

    EnableWinAuth

    @@ -1238,7 +1368,7 @@
    Property Value
    Edit this page - View Source + View Source

    EndpointAdmissionRejectionCount

    @@ -1270,7 +1400,7 @@
    Property Value
    Edit this page - View Source + View Source

    ForwardToUpstreamGateway

    @@ -1302,7 +1432,7 @@
    Property Value
    Edit this page - View Source + View Source

    GetCustomUpStreamProxyFunc

    @@ -1334,7 +1464,7 @@
    Property Value
    Edit this page - View Source + View Source

    GlobalAdmissionRejectionCount

    @@ -1366,7 +1496,7 @@
    Property Value
    Edit this page - View Source + View Source

    Http3ClientConnectionCount

    @@ -1397,7 +1527,7 @@
    Property Value
    Edit this page - View Source + View Source

    Http3ServerConnectionCount

    @@ -1429,7 +1559,7 @@
    Property Value
    Edit this page - View Source + View Source

    IdleReadTimeoutSeconds

    @@ -1462,7 +1592,7 @@
    Property Value
    Edit this page - View Source + View Source

    IdleWriteTimeoutSeconds

    @@ -1495,7 +1625,7 @@
    Property Value
    Edit this page - View Source + View Source

    IgnoreServerCertificateErrors

    @@ -1529,7 +1659,7 @@
    Property Value
    Edit this page - View Source + View Source

    ListenerBackLog

    @@ -1560,7 +1690,7 @@
    Property Value
    Edit this page - View Source + View Source

    Logger

    @@ -1592,7 +1722,7 @@
    Property Value
    Edit this page - View Source + View Source

    Logging

    @@ -1633,7 +1763,7 @@
    Property Value
    Edit this page - View Source + View Source

    MaxBufferedBodyBytes

    @@ -1668,7 +1798,7 @@
    Property Value
    Edit this page - View Source + View Source

    MaxCachedConnections

    @@ -1722,7 +1852,7 @@
    Exceptions
    Edit this page - View Source + View Source

    MaxConcurrentClientConnections

    @@ -1762,7 +1892,7 @@
    Property Value
    Edit this page - View Source + View Source

    MaxConcurrentHttp11HttpsOriginCreates

    @@ -1813,7 +1943,7 @@
    Exceptions
    Edit this page - View Source + View Source

    MaxDecodedHeaderListBytes

    @@ -1848,7 +1978,7 @@
    Property Value
    Edit this page - View Source + View Source

    MaxWebSocketFramePayloadBytes

    @@ -1885,7 +2015,7 @@
    Property Value
    Edit this page - View Source + View Source

    NetworkFailureRetryAttempts

    @@ -1916,7 +2046,7 @@
    Property Value
    Edit this page - View Source + View Source

    NoDelay

    @@ -1948,7 +2078,7 @@
    Property Value
    Edit this page - View Source + View Source

    OriginHttpVersionPolicy

    @@ -1987,7 +2117,7 @@
    Property Value
    Edit this page - View Source + View Source

    PolicyModes

    @@ -2030,7 +2160,7 @@
    Property Value
    Edit this page - View Source + View Source

    Profile

    @@ -2076,7 +2206,7 @@
    Property Value
    Edit this page - View Source + View Source

    ProxyAuthenticationRealm

    @@ -2107,7 +2237,7 @@
    Property Value
    Edit this page - View Source + View Source

    ProxyAuthenticationSchemes

    @@ -2140,7 +2270,7 @@
    Property Value
    Edit this page - View Source + View Source

    ProxyBasicAuthenticateFunc

    @@ -2173,7 +2303,7 @@
    Property Value
    Edit this page - View Source + View Source

    ProxyEndPoints

    @@ -2204,7 +2334,7 @@
    Property Value
    Edit this page - View Source + View Source

    ProxyRunning

    @@ -2235,7 +2365,7 @@
    Property Value
    Edit this page - View Source + View Source

    ProxySchemeAuthenticateFunc

    @@ -2269,7 +2399,7 @@
    Property Value
    Edit this page - View Source + View Source

    RequestTimeoutSeconds

    @@ -2303,7 +2433,7 @@
    Property Value
    Edit this page - View Source + View Source

    ResourceLimits

    @@ -2344,7 +2474,7 @@
    Property Value
    Edit this page - View Source + View Source

    ResponseHeaderTimeoutSeconds

    @@ -2384,7 +2514,7 @@
    Property Value
    Edit this page - View Source + View Source

    ReuseSocket

    @@ -2417,7 +2547,7 @@
    Property Value
    Edit this page - View Source + View Source

    ReverseProxy

    @@ -2449,7 +2579,7 @@
    Property Value
    Edit this page - View Source + View Source

    ServerConnectionCount

    @@ -2481,7 +2611,7 @@
    Property Value
    Edit this page - View Source + View Source

    ShouldInterceptHttp

    @@ -2515,7 +2645,7 @@
    Property Value
    Edit this page - View Source + View Source

    SupportedServerSslProtocols

    @@ -2556,7 +2686,7 @@
    Property Value
    Edit this page - View Source + View Source

    SupportedSslProtocols

    @@ -2594,7 +2724,7 @@
    Property Value
    Edit this page - View Source + View Source

    TcpTimeWaitSeconds

    @@ -2630,7 +2760,7 @@
    Property Value
    Edit this page - View Source + View Source

    ThreadPoolWorkerThread

    @@ -2663,7 +2793,7 @@
    Property Value
    Edit this page - View Source + View Source

    UpStreamEndPoint

    @@ -2698,7 +2828,7 @@
    Property Value
    Edit this page - View Source + View Source

    UpStreamEndPointIPv4

    @@ -2730,7 +2860,7 @@
    Property Value
    Edit this page - View Source + View Source

    UpStreamEndPointIPv6

    @@ -2762,7 +2892,7 @@
    Property Value
    Edit this page - View Source + View Source

    UpStreamHttpProxy

    @@ -2793,7 +2923,7 @@
    Property Value
    Edit this page - View Source + View Source

    UpStreamHttpsProxy

    @@ -2824,7 +2954,7 @@
    Property Value
    Edit this page - View Source + View Source

    UpstreamProxyConfigurationScript

    @@ -2855,7 +2985,7 @@
    Property Value
    Edit this page - View Source + View Source

    ViaHeaderPseudonym

    @@ -2889,7 +3019,7 @@
    Property Value
    Edit this page - View Source + View Source

    WinAuthCredentialsProvider

    @@ -2959,7 +3089,7 @@
    Parameters
    Edit this page - View Source + View Source

    ApplyLoggingConfiguration()

    @@ -2975,6 +3105,22 @@
    Declaration
    public void ApplyLoggingConfiguration()
    + + | + Edit this page + + + View Source + + +

    ClearDecryptFailureBypass()

    +

    Clears all learned decrypt-bypass entries.

    +
    +
    +
    Declaration
    +
    +
    public void ClearDecryptFailureBypass()
    +
    | Edit this page @@ -3105,6 +3251,85 @@
    Parameters
    + + | + Edit this page + + + View Source + + +

    GetDecryptFailureBypassEntries()

    +

    Snapshot of current learned decrypt-bypass entries (may include non-active strikes).

    +
    +
    +
    Declaration
    +
    +
    public IReadOnlyList<DecryptFailureBypassEntry> GetDecryptFailureBypassEntries()
    +
    +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    IReadOnlyList<DecryptFailureBypassEntry>
    + + | + Edit this page + + + View Source + + +

    RemoveDecryptFailureBypass(string)

    +

    Removes one host from the learned decrypt-bypass cache.

    +
    +
    +
    Declaration
    +
    +
    public bool RemoveDecryptFailureBypass(string host)
    +
    +
    Parameters
    + + + + + + + + + + + + + + + +
    TypeNameDescription
    stringhost
    +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    bool
    | Edit this page @@ -3395,7 +3620,7 @@
    Parameters
    Edit this page
    - View Source + View Source

    SetHttp3Enabled(bool)

    @@ -3442,6 +3667,55 @@
    Returns
    + + | + Edit this page + + + View Source + + +

    ShouldBypassDecryptForLearnedHost(string?)

    +

    When EnableDecryptFailureBypass is on and host is actively +bypassed, returns true (decrypt should be skipped).

    +
    +
    +
    Declaration
    +
    +
    public bool ShouldBypassDecryptForLearnedHost(string? host)
    +
    +
    Parameters
    + + + + + + + + + + + + + + + +
    TypeNameDescription
    stringhost
    +
    Returns
    + + + + + + + + + + + + + +
    TypeDescription
    bool
    | Edit this page @@ -3639,7 +3913,7 @@
    Returns
    Edit this page
    - View Source + View Source

    TryEnableHttp3IfSupported()

    @@ -3765,7 +4039,7 @@

    Events Edit this page - View Source + View Source

    AfterResponse

    Intercept after response event from server.

    @@ -3795,7 +4069,7 @@
    Event Type
    Edit this page - View Source + View Source

    BeforeRequest

    Intercept request event to server.

    @@ -3825,7 +4099,7 @@
    Event Type
    Edit this page - View Source + View Source

    BeforeResponse

    Intercept response event from server.

    @@ -3855,7 +4129,7 @@
    Event Type
    Edit this page - View Source + View Source

    BeforeUpStreamConnectRequest

    Intercept connect request sent to upstream proxy.

    @@ -3885,7 +4159,7 @@
    Event Type
    Edit this page - View Source + View Source

    ClientCertificateSelectionCallback

    Event to override client certificate selection during mutual SSL authentication.

    @@ -3915,7 +4189,7 @@
    Event Type
    Edit this page - View Source + View Source

    ClientConnectionCountChanged

    Event occurs when client connection count changed.

    @@ -3940,12 +4214,43 @@
    Event Type
    + + | + Edit this page + + + View Source + +

    DecryptFailureBypassChanged

    +

    Raised when a host becomes actively bypassed (threshold reached or same-CONNECT mark). +Handlers must not block; Inspector marshals to the UI thread.

    +
    +
    +
    Declaration
    +
    +
    public event EventHandler<DecryptFailureBypassEntry>? DecryptFailureBypassChanged
    +
    +
    Event Type
    + + + + + + + + + + + + + +
    TypeDescription
    EventHandler<DecryptFailureBypassEntry>
    | Edit this page - View Source + View Source

    Http3ClientConnectionCountChanged

    Event occurs when inbound HTTP/3 client connection count changed.

    @@ -3975,7 +4280,7 @@
    Event Type
    Edit this page - View Source + View Source

    Http3ServerConnectionCountChanged

    Event occurs when upstream HTTP/3 server connection count changed.

    @@ -4005,7 +4310,7 @@
    Event Type
    Edit this page - View Source + View Source

    OnClientConnectionCreate

    Customize TcpClient used for client connection upon create.

    @@ -4035,7 +4340,7 @@
    Event Type
    Edit this page - View Source + View Source

    OnRequestBodyWrite

    Intercept request body send event to server. @@ -4067,7 +4372,7 @@

    Event Type
    Edit this page - View Source + View Source

    OnResponseBodyWrite

    Intercept response body send event to client. @@ -4099,7 +4404,7 @@

    Event Type
    Edit this page - View Source + View Source

    OnServerConnectionCreate

    Customize TcpClient used for server connection upon create.

    @@ -4129,7 +4434,7 @@
    Event Type
    Edit this page - View Source + View Source

    ServerCertificateValidationCallback

    Event to override the default verification logic of remote SSL certificate received during authentication.

    @@ -4159,7 +4464,7 @@
    Event Type
    Edit this page - View Source + View Source

    ServerConnectionCountChanged

    Event occurs when server connection count changed.

    diff --git a/docs/api/toc.html b/docs/api/toc.html index bab939497..1f47c9fb2 100644 --- a/docs/api/toc.html +++ b/docs/api/toc.html @@ -311,6 +311,9 @@ Titanium.Web.Proxy.Models
      +
    • + DecryptFailureBypassEntry +
    • ExplicitProxyEndPoint
    • diff --git a/docs/api/toc.json b/docs/api/toc.json index bc6b32a06..4deac3aaa 100644 --- a/docs/api/toc.json +++ b/docs/api/toc.json @@ -1,2 +1,2 @@ -{"items":[{"name":"Titanium.Web.Proxy","href":"Titanium.Web.Proxy.html","topicHref":"Titanium.Web.Proxy.html","topicUid":"Titanium.Web.Proxy","type":"Namespace","items":[{"name":"ClientProcessId","href":"Titanium.Web.Proxy.ClientProcessId.html","topicHref":"Titanium.Web.Proxy.ClientProcessId.html","topicUid":"Titanium.Web.Proxy.ClientProcessId","type":"Class"},{"name":"MitmExclusionDefaults","href":"Titanium.Web.Proxy.MitmExclusionDefaults.html","topicHref":"Titanium.Web.Proxy.MitmExclusionDefaults.html","topicUid":"Titanium.Web.Proxy.MitmExclusionDefaults","type":"Class"},{"name":"MitmExclusionMode","href":"Titanium.Web.Proxy.MitmExclusionMode.html","topicHref":"Titanium.Web.Proxy.MitmExclusionMode.html","topicUid":"Titanium.Web.Proxy.MitmExclusionMode","type":"Enum"},{"name":"ProxyLimits","href":"Titanium.Web.Proxy.ProxyLimits.html","topicHref":"Titanium.Web.Proxy.ProxyLimits.html","topicUid":"Titanium.Web.Proxy.ProxyLimits","type":"Class"},{"name":"ProxyServer","href":"Titanium.Web.Proxy.ProxyServer.html","topicHref":"Titanium.Web.Proxy.ProxyServer.html","topicUid":"Titanium.Web.Proxy.ProxyServer","type":"Class"},{"name":"SystemProxyBypassRuleMode","href":"Titanium.Web.Proxy.SystemProxyBypassRuleMode.html","topicHref":"Titanium.Web.Proxy.SystemProxyBypassRuleMode.html","topicUid":"Titanium.Web.Proxy.SystemProxyBypassRuleMode","type":"Enum"},{"name":"SystemProxyChangeResult","href":"Titanium.Web.Proxy.SystemProxyChangeResult.html","topicHref":"Titanium.Web.Proxy.SystemProxyChangeResult.html","topicUid":"Titanium.Web.Proxy.SystemProxyChangeResult","type":"Struct"},{"name":"SystemProxyLoopbackPlacement","href":"Titanium.Web.Proxy.SystemProxyLoopbackPlacement.html","topicHref":"Titanium.Web.Proxy.SystemProxyLoopbackPlacement.html","topicUid":"Titanium.Web.Proxy.SystemProxyLoopbackPlacement","type":"Enum"},{"name":"SystemProxySettings","href":"Titanium.Web.Proxy.SystemProxySettings.html","topicHref":"Titanium.Web.Proxy.SystemProxySettings.html","topicUid":"Titanium.Web.Proxy.SystemProxySettings","type":"Class"},{"name":"WebSocketDecoder","href":"Titanium.Web.Proxy.WebSocketDecoder.html","topicHref":"Titanium.Web.Proxy.WebSocketDecoder.html","topicUid":"Titanium.Web.Proxy.WebSocketDecoder","type":"Class"},{"name":"WebSocketFrame","href":"Titanium.Web.Proxy.WebSocketFrame.html","topicHref":"Titanium.Web.Proxy.WebSocketFrame.html","topicUid":"Titanium.Web.Proxy.WebSocketFrame","type":"Class"},{"name":"WebSocketFrameEncoder","href":"Titanium.Web.Proxy.WebSocketFrameEncoder.html","topicHref":"Titanium.Web.Proxy.WebSocketFrameEncoder.html","topicUid":"Titanium.Web.Proxy.WebSocketFrameEncoder","type":"Class"},{"name":"WebSocketFrameWriter","href":"Titanium.Web.Proxy.WebSocketFrameWriter.html","topicHref":"Titanium.Web.Proxy.WebSocketFrameWriter.html","topicUid":"Titanium.Web.Proxy.WebSocketFrameWriter","type":"Class"},{"name":"WebSocketProtocolException","href":"Titanium.Web.Proxy.WebSocketProtocolException.html","topicHref":"Titanium.Web.Proxy.WebSocketProtocolException.html","topicUid":"Titanium.Web.Proxy.WebSocketProtocolException","type":"Class"},{"name":"WebsocketOpCode","href":"Titanium.Web.Proxy.WebsocketOpCode.html","topicHref":"Titanium.Web.Proxy.WebsocketOpCode.html","topicUid":"Titanium.Web.Proxy.WebsocketOpCode","type":"Enum"}]},{"name":"Titanium.Web.Proxy.Caching","href":"Titanium.Web.Proxy.Caching.html","topicHref":"Titanium.Web.Proxy.Caching.html","topicUid":"Titanium.Web.Proxy.Caching","type":"Namespace","items":[{"name":"HttpResponseCacheMiddleware","href":"Titanium.Web.Proxy.Caching.HttpResponseCacheMiddleware.html","topicHref":"Titanium.Web.Proxy.Caching.HttpResponseCacheMiddleware.html","topicUid":"Titanium.Web.Proxy.Caching.HttpResponseCacheMiddleware","type":"Class"},{"name":"MemoryHttpResponseCache","href":"Titanium.Web.Proxy.Caching.MemoryHttpResponseCache.html","topicHref":"Titanium.Web.Proxy.Caching.MemoryHttpResponseCache.html","topicUid":"Titanium.Web.Proxy.Caching.MemoryHttpResponseCache","type":"Class"}]},{"name":"Titanium.Web.Proxy.Clusters","href":"Titanium.Web.Proxy.Clusters.html","topicHref":"Titanium.Web.Proxy.Clusters.html","topicUid":"Titanium.Web.Proxy.Clusters","type":"Namespace","items":[{"name":"ClusterManager","href":"Titanium.Web.Proxy.Clusters.ClusterManager.html","topicHref":"Titanium.Web.Proxy.Clusters.ClusterManager.html","topicUid":"Titanium.Web.Proxy.Clusters.ClusterManager","type":"Class"},{"name":"DestinationHealthTracker","href":"Titanium.Web.Proxy.Clusters.DestinationHealthTracker.html","topicHref":"Titanium.Web.Proxy.Clusters.DestinationHealthTracker.html","topicUid":"Titanium.Web.Proxy.Clusters.DestinationHealthTracker","type":"Class"},{"name":"LoadBalancer","href":"Titanium.Web.Proxy.Clusters.LoadBalancer.html","topicHref":"Titanium.Web.Proxy.Clusters.LoadBalancer.html","topicUid":"Titanium.Web.Proxy.Clusters.LoadBalancer","type":"Class"}]},{"name":"Titanium.Web.Proxy.Diagnostics","href":"Titanium.Web.Proxy.Diagnostics.html","topicHref":"Titanium.Web.Proxy.Diagnostics.html","topicUid":"Titanium.Web.Proxy.Diagnostics","type":"Namespace","items":[{"name":"ClientTlsTiming","href":"Titanium.Web.Proxy.Diagnostics.ClientTlsTiming.html","topicHref":"Titanium.Web.Proxy.Diagnostics.ClientTlsTiming.html","topicUid":"Titanium.Web.Proxy.Diagnostics.ClientTlsTiming","type":"Class"},{"name":"HttpRequestTiming","href":"Titanium.Web.Proxy.Diagnostics.HttpRequestTiming.html","topicHref":"Titanium.Web.Proxy.Diagnostics.HttpRequestTiming.html","topicUid":"Titanium.Web.Proxy.Diagnostics.HttpRequestTiming","type":"Class"},{"name":"TunnelConnectTiming","href":"Titanium.Web.Proxy.Diagnostics.TunnelConnectTiming.html","topicHref":"Titanium.Web.Proxy.Diagnostics.TunnelConnectTiming.html","topicUid":"Titanium.Web.Proxy.Diagnostics.TunnelConnectTiming","type":"Class"},{"name":"UpstreamConnectionTiming","href":"Titanium.Web.Proxy.Diagnostics.UpstreamConnectionTiming.html","topicHref":"Titanium.Web.Proxy.Diagnostics.UpstreamConnectionTiming.html","topicUid":"Titanium.Web.Proxy.Diagnostics.UpstreamConnectionTiming","type":"Class"}]},{"name":"Titanium.Web.Proxy.EventArguments","href":"Titanium.Web.Proxy.EventArguments.html","topicHref":"Titanium.Web.Proxy.EventArguments.html","topicUid":"Titanium.Web.Proxy.EventArguments","type":"Namespace","items":[{"name":"AsyncEventHandler","href":"Titanium.Web.Proxy.EventArguments.AsyncEventHandler-1.html","topicHref":"Titanium.Web.Proxy.EventArguments.AsyncEventHandler-1.html","topicUid":"Titanium.Web.Proxy.EventArguments.AsyncEventHandler`1","type":"Delegate"},{"name":"BeforeBodyWriteEventArgs","href":"Titanium.Web.Proxy.EventArguments.BeforeBodyWriteEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.BeforeBodyWriteEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.BeforeBodyWriteEventArgs","type":"Class"},{"name":"BeforeHttpAuthenticateEventArgs","href":"Titanium.Web.Proxy.EventArguments.BeforeHttpAuthenticateEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.BeforeHttpAuthenticateEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.BeforeHttpAuthenticateEventArgs","type":"Class"},{"name":"BeforeQuicAuthenticateEventArgs","href":"Titanium.Web.Proxy.EventArguments.BeforeQuicAuthenticateEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.BeforeQuicAuthenticateEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.BeforeQuicAuthenticateEventArgs","type":"Class"},{"name":"BeforeSslAuthenticateEventArgs","href":"Titanium.Web.Proxy.EventArguments.BeforeSslAuthenticateEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.BeforeSslAuthenticateEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.BeforeSslAuthenticateEventArgs","type":"Class"},{"name":"CertificateSelectionEventArgs","href":"Titanium.Web.Proxy.EventArguments.CertificateSelectionEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.CertificateSelectionEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.CertificateSelectionEventArgs","type":"Class"},{"name":"CertificateValidationEventArgs","href":"Titanium.Web.Proxy.EventArguments.CertificateValidationEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.CertificateValidationEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.CertificateValidationEventArgs","type":"Class"},{"name":"MultipartRequestPartSentEventArgs","href":"Titanium.Web.Proxy.EventArguments.MultipartRequestPartSentEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.MultipartRequestPartSentEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.MultipartRequestPartSentEventArgs","type":"Class"},{"name":"ProxyEventArgsBase","href":"Titanium.Web.Proxy.EventArguments.ProxyEventArgsBase.html","topicHref":"Titanium.Web.Proxy.EventArguments.ProxyEventArgsBase.html","topicUid":"Titanium.Web.Proxy.EventArguments.ProxyEventArgsBase","type":"Class"},{"name":"SessionEventArgs","href":"Titanium.Web.Proxy.EventArguments.SessionEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.SessionEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.SessionEventArgs","type":"Class"},{"name":"SessionEventArgsBase","href":"Titanium.Web.Proxy.EventArguments.SessionEventArgsBase.html","topicHref":"Titanium.Web.Proxy.EventArguments.SessionEventArgsBase.html","topicUid":"Titanium.Web.Proxy.EventArguments.SessionEventArgsBase","type":"Class"},{"name":"SocksAuthenticateEventArgs","href":"Titanium.Web.Proxy.EventArguments.SocksAuthenticateEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.SocksAuthenticateEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.SocksAuthenticateEventArgs","type":"Class"},{"name":"TunnelConnectFailureEventArgs","href":"Titanium.Web.Proxy.EventArguments.TunnelConnectFailureEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.TunnelConnectFailureEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.TunnelConnectFailureEventArgs","type":"Class"},{"name":"TunnelConnectSessionEventArgs","href":"Titanium.Web.Proxy.EventArguments.TunnelConnectSessionEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.TunnelConnectSessionEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.TunnelConnectSessionEventArgs","type":"Class"},{"name":"WebSocketFrameDirection","href":"Titanium.Web.Proxy.EventArguments.WebSocketFrameDirection.html","topicHref":"Titanium.Web.Proxy.EventArguments.WebSocketFrameDirection.html","topicUid":"Titanium.Web.Proxy.EventArguments.WebSocketFrameDirection","type":"Enum"},{"name":"WebSocketFrameInterceptAction","href":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptAction.html","topicHref":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptAction.html","topicUid":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptAction","type":"Enum"},{"name":"WebSocketFrameInterceptEventArgs","href":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptEventArgs","type":"Class"}]},{"name":"Titanium.Web.Proxy.Exceptions","href":"Titanium.Web.Proxy.Exceptions.html","topicHref":"Titanium.Web.Proxy.Exceptions.html","topicUid":"Titanium.Web.Proxy.Exceptions","type":"Namespace","items":[{"name":"BodyNotFoundException","href":"Titanium.Web.Proxy.Exceptions.BodyNotFoundException.html","topicHref":"Titanium.Web.Proxy.Exceptions.BodyNotFoundException.html","topicUid":"Titanium.Web.Proxy.Exceptions.BodyNotFoundException","type":"Class"},{"name":"OutboundDestinationBlockedException","href":"Titanium.Web.Proxy.Exceptions.OutboundDestinationBlockedException.html","topicHref":"Titanium.Web.Proxy.Exceptions.OutboundDestinationBlockedException.html","topicUid":"Titanium.Web.Proxy.Exceptions.OutboundDestinationBlockedException","type":"Class"},{"name":"ProxyAuthorizationException","href":"Titanium.Web.Proxy.Exceptions.ProxyAuthorizationException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyAuthorizationException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyAuthorizationException","type":"Class"},{"name":"ProxyConnectException","href":"Titanium.Web.Proxy.Exceptions.ProxyConnectException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyConnectException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyConnectException","type":"Class"},{"name":"ProxyException","href":"Titanium.Web.Proxy.Exceptions.ProxyException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyException","type":"Class"},{"name":"ProxyHttpException","href":"Titanium.Web.Proxy.Exceptions.ProxyHttpException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyHttpException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyHttpException","type":"Class"},{"name":"ProxyTimeoutException","href":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutException","type":"Class"},{"name":"ProxyTimeoutKind","href":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutKind.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutKind.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutKind","type":"Enum"},{"name":"UpstreamProxyConnectException","href":"Titanium.Web.Proxy.Exceptions.UpstreamProxyConnectException.html","topicHref":"Titanium.Web.Proxy.Exceptions.UpstreamProxyConnectException.html","topicUid":"Titanium.Web.Proxy.Exceptions.UpstreamProxyConnectException","type":"Class"}]},{"name":"Titanium.Web.Proxy.Helpers","href":"Titanium.Web.Proxy.Helpers.html","topicHref":"Titanium.Web.Proxy.Helpers.html","topicUid":"Titanium.Web.Proxy.Helpers","type":"Namespace","items":[{"name":"UnixProxyBypassMapper","href":"Titanium.Web.Proxy.Helpers.UnixProxyBypassMapper.html","topicHref":"Titanium.Web.Proxy.Helpers.UnixProxyBypassMapper.html","topicUid":"Titanium.Web.Proxy.Helpers.UnixProxyBypassMapper","type":"Class"}]},{"name":"Titanium.Web.Proxy.Http","href":"Titanium.Web.Proxy.Http.html","topicHref":"Titanium.Web.Proxy.Http.html","topicUid":"Titanium.Web.Proxy.Http","type":"Namespace","items":[{"name":"ConnectRequest","href":"Titanium.Web.Proxy.Http.ConnectRequest.html","topicHref":"Titanium.Web.Proxy.Http.ConnectRequest.html","topicUid":"Titanium.Web.Proxy.Http.ConnectRequest","type":"Class"},{"name":"ConnectResponse","href":"Titanium.Web.Proxy.Http.ConnectResponse.html","topicHref":"Titanium.Web.Proxy.Http.ConnectResponse.html","topicUid":"Titanium.Web.Proxy.Http.ConnectResponse","type":"Class"},{"name":"HeaderCollection","href":"Titanium.Web.Proxy.Http.HeaderCollection.html","topicHref":"Titanium.Web.Proxy.Http.HeaderCollection.html","topicUid":"Titanium.Web.Proxy.Http.HeaderCollection","type":"Class"},{"name":"HeaderCollection.Enumerator","href":"Titanium.Web.Proxy.Http.HeaderCollection.Enumerator.html","topicHref":"Titanium.Web.Proxy.Http.HeaderCollection.Enumerator.html","topicUid":"Titanium.Web.Proxy.Http.HeaderCollection.Enumerator","type":"Struct"},{"name":"HttpWebClient","href":"Titanium.Web.Proxy.Http.HttpWebClient.html","topicHref":"Titanium.Web.Proxy.Http.HttpWebClient.html","topicUid":"Titanium.Web.Proxy.Http.HttpWebClient","type":"Class"},{"name":"KnownHeader","href":"Titanium.Web.Proxy.Http.KnownHeader.html","topicHref":"Titanium.Web.Proxy.Http.KnownHeader.html","topicUid":"Titanium.Web.Proxy.Http.KnownHeader","type":"Class"},{"name":"KnownHeaders","href":"Titanium.Web.Proxy.Http.KnownHeaders.html","topicHref":"Titanium.Web.Proxy.Http.KnownHeaders.html","topicUid":"Titanium.Web.Proxy.Http.KnownHeaders","type":"Class"},{"name":"ProxyResults","href":"Titanium.Web.Proxy.Http.ProxyResults.html","topicHref":"Titanium.Web.Proxy.Http.ProxyResults.html","topicUid":"Titanium.Web.Proxy.Http.ProxyResults","type":"Class"},{"name":"Request","href":"Titanium.Web.Proxy.Http.Request.html","topicHref":"Titanium.Web.Proxy.Http.Request.html","topicUid":"Titanium.Web.Proxy.Http.Request","type":"Class"},{"name":"RequestResponseBase","href":"Titanium.Web.Proxy.Http.RequestResponseBase.html","topicHref":"Titanium.Web.Proxy.Http.RequestResponseBase.html","topicUid":"Titanium.Web.Proxy.Http.RequestResponseBase","type":"Class"},{"name":"Response","href":"Titanium.Web.Proxy.Http.Response.html","topicHref":"Titanium.Web.Proxy.Http.Response.html","topicUid":"Titanium.Web.Proxy.Http.Response","type":"Class"},{"name":"StreamingProxyResult","href":"Titanium.Web.Proxy.Http.StreamingProxyResult.html","topicHref":"Titanium.Web.Proxy.Http.StreamingProxyResult.html","topicUid":"Titanium.Web.Proxy.Http.StreamingProxyResult","type":"Struct"},{"name":"TunnelType","href":"Titanium.Web.Proxy.Http.TunnelType.html","topicHref":"Titanium.Web.Proxy.Http.TunnelType.html","topicUid":"Titanium.Web.Proxy.Http.TunnelType","type":"Enum"}]},{"name":"Titanium.Web.Proxy.Http.Responses","href":"Titanium.Web.Proxy.Http.Responses.html","topicHref":"Titanium.Web.Proxy.Http.Responses.html","topicUid":"Titanium.Web.Proxy.Http.Responses","type":"Namespace","items":[{"name":"GenericResponse","href":"Titanium.Web.Proxy.Http.Responses.GenericResponse.html","topicHref":"Titanium.Web.Proxy.Http.Responses.GenericResponse.html","topicUid":"Titanium.Web.Proxy.Http.Responses.GenericResponse","type":"Class"},{"name":"OkResponse","href":"Titanium.Web.Proxy.Http.Responses.OkResponse.html","topicHref":"Titanium.Web.Proxy.Http.Responses.OkResponse.html","topicUid":"Titanium.Web.Proxy.Http.Responses.OkResponse","type":"Class"},{"name":"RedirectResponse","href":"Titanium.Web.Proxy.Http.Responses.RedirectResponse.html","topicHref":"Titanium.Web.Proxy.Http.Responses.RedirectResponse.html","topicUid":"Titanium.Web.Proxy.Http.Responses.RedirectResponse","type":"Class"}]},{"name":"Titanium.Web.Proxy.Http3","href":"Titanium.Web.Proxy.Http3.html","topicHref":"Titanium.Web.Proxy.Http3.html","topicUid":"Titanium.Web.Proxy.Http3","type":"Namespace","items":[{"name":"Http3NativeBootstrap","href":"Titanium.Web.Proxy.Http3.Http3NativeBootstrap.html","topicHref":"Titanium.Web.Proxy.Http3.Http3NativeBootstrap.html","topicUid":"Titanium.Web.Proxy.Http3.Http3NativeBootstrap","type":"Class"}]},{"name":"Titanium.Web.Proxy.Logging","href":"Titanium.Web.Proxy.Logging.html","topicHref":"Titanium.Web.Proxy.Logging.html","topicUid":"Titanium.Web.Proxy.Logging","type":"Namespace","items":[{"name":"ProxyLoggingOptions","href":"Titanium.Web.Proxy.Logging.ProxyLoggingOptions.html","topicHref":"Titanium.Web.Proxy.Logging.ProxyLoggingOptions.html","topicUid":"Titanium.Web.Proxy.Logging.ProxyLoggingOptions","type":"Class"}]},{"name":"Titanium.Web.Proxy.Middleware","href":"Titanium.Web.Proxy.Middleware.html","topicHref":"Titanium.Web.Proxy.Middleware.html","topicUid":"Titanium.Web.Proxy.Middleware","type":"Namespace","items":[{"name":"ProxyMiddlewarePipeline","href":"Titanium.Web.Proxy.Middleware.ProxyMiddlewarePipeline.html","topicHref":"Titanium.Web.Proxy.Middleware.ProxyMiddlewarePipeline.html","topicUid":"Titanium.Web.Proxy.Middleware.ProxyMiddlewarePipeline","type":"Class"}]},{"name":"Titanium.Web.Proxy.Models","href":"Titanium.Web.Proxy.Models.html","topicHref":"Titanium.Web.Proxy.Models.html","topicUid":"Titanium.Web.Proxy.Models","type":"Namespace","items":[{"name":"ExplicitProxyEndPoint","href":"Titanium.Web.Proxy.Models.ExplicitProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.ExplicitProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.ExplicitProxyEndPoint","type":"Class"},{"name":"ExternalProxy","href":"Titanium.Web.Proxy.Models.ExternalProxy.html","topicHref":"Titanium.Web.Proxy.Models.ExternalProxy.html","topicUid":"Titanium.Web.Proxy.Models.ExternalProxy","type":"Class"},{"name":"ExternalProxyType","href":"Titanium.Web.Proxy.Models.ExternalProxyType.html","topicHref":"Titanium.Web.Proxy.Models.ExternalProxyType.html","topicUid":"Titanium.Web.Proxy.Models.ExternalProxyType","type":"Enum"},{"name":"HttpHeader","href":"Titanium.Web.Proxy.Models.HttpHeader.html","topicHref":"Titanium.Web.Proxy.Models.HttpHeader.html","topicUid":"Titanium.Web.Proxy.Models.HttpHeader","type":"Class"},{"name":"HttpInterceptionContext","href":"Titanium.Web.Proxy.Models.HttpInterceptionContext.html","topicHref":"Titanium.Web.Proxy.Models.HttpInterceptionContext.html","topicUid":"Titanium.Web.Proxy.Models.HttpInterceptionContext","type":"Struct"},{"name":"IExternalProxy","href":"Titanium.Web.Proxy.Models.IExternalProxy.html","topicHref":"Titanium.Web.Proxy.Models.IExternalProxy.html","topicUid":"Titanium.Web.Proxy.Models.IExternalProxy","type":"Interface"},{"name":"OriginHttpVersionPolicy","href":"Titanium.Web.Proxy.Models.OriginHttpVersionPolicy.html","topicHref":"Titanium.Web.Proxy.Models.OriginHttpVersionPolicy.html","topicUid":"Titanium.Web.Proxy.Models.OriginHttpVersionPolicy","type":"Enum"},{"name":"ProxyAuthenticationContext","href":"Titanium.Web.Proxy.Models.ProxyAuthenticationContext.html","topicHref":"Titanium.Web.Proxy.Models.ProxyAuthenticationContext.html","topicUid":"Titanium.Web.Proxy.Models.ProxyAuthenticationContext","type":"Class"},{"name":"ProxyAuthenticationResult","href":"Titanium.Web.Proxy.Models.ProxyAuthenticationResult.html","topicHref":"Titanium.Web.Proxy.Models.ProxyAuthenticationResult.html","topicUid":"Titanium.Web.Proxy.Models.ProxyAuthenticationResult","type":"Enum"},{"name":"ProxyEndPoint","href":"Titanium.Web.Proxy.Models.ProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.ProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.ProxyEndPoint","type":"Class"},{"name":"ProxyProtocolType","href":"Titanium.Web.Proxy.Models.ProxyProtocolType.html","topicHref":"Titanium.Web.Proxy.Models.ProxyProtocolType.html","topicUid":"Titanium.Web.Proxy.Models.ProxyProtocolType","type":"Enum"},{"name":"SocksProxyEndPoint","href":"Titanium.Web.Proxy.Models.SocksProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.SocksProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.SocksProxyEndPoint","type":"Class"},{"name":"TransparentBaseProxyEndPoint","href":"Titanium.Web.Proxy.Models.TransparentBaseProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.TransparentBaseProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.TransparentBaseProxyEndPoint","type":"Class"},{"name":"TransparentProxyEndPoint","href":"Titanium.Web.Proxy.Models.TransparentProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.TransparentProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.TransparentProxyEndPoint","type":"Class"},{"name":"TransparentQuicProxyEndPoint","href":"Titanium.Web.Proxy.Models.TransparentQuicProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.TransparentQuicProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.TransparentQuicProxyEndPoint","type":"Class"},{"name":"UpstreamHttpProtocol","href":"Titanium.Web.Proxy.Models.UpstreamHttpProtocol.html","topicHref":"Titanium.Web.Proxy.Models.UpstreamHttpProtocol.html","topicUid":"Titanium.Web.Proxy.Models.UpstreamHttpProtocol","type":"Enum"},{"name":"WinAuthCredentials","href":"Titanium.Web.Proxy.Models.WinAuthCredentials.html","topicHref":"Titanium.Web.Proxy.Models.WinAuthCredentials.html","topicUid":"Titanium.Web.Proxy.Models.WinAuthCredentials","type":"Class"}]},{"name":"Titanium.Web.Proxy.Network","href":"Titanium.Web.Proxy.Network.html","topicHref":"Titanium.Web.Proxy.Network.html","topicUid":"Titanium.Web.Proxy.Network","type":"Namespace","items":[{"name":"CertificateEngine","href":"Titanium.Web.Proxy.Network.CertificateEngine.html","topicHref":"Titanium.Web.Proxy.Network.CertificateEngine.html","topicUid":"Titanium.Web.Proxy.Network.CertificateEngine","type":"Enum"},{"name":"CertificateKeyAlgorithm","href":"Titanium.Web.Proxy.Network.CertificateKeyAlgorithm.html","topicHref":"Titanium.Web.Proxy.Network.CertificateKeyAlgorithm.html","topicUid":"Titanium.Web.Proxy.Network.CertificateKeyAlgorithm","type":"Enum"},{"name":"CertificateManager","href":"Titanium.Web.Proxy.Network.CertificateManager.html","topicHref":"Titanium.Web.Proxy.Network.CertificateManager.html","topicUid":"Titanium.Web.Proxy.Network.CertificateManager","type":"Class"},{"name":"CertificateOsTrustKind","href":"Titanium.Web.Proxy.Network.CertificateOsTrustKind.html","topicHref":"Titanium.Web.Proxy.Network.CertificateOsTrustKind.html","topicUid":"Titanium.Web.Proxy.Network.CertificateOsTrustKind","type":"Enum"},{"name":"CertificateOsTrustResult","href":"Titanium.Web.Proxy.Network.CertificateOsTrustResult.html","topicHref":"Titanium.Web.Proxy.Network.CertificateOsTrustResult.html","topicUid":"Titanium.Web.Proxy.Network.CertificateOsTrustResult","type":"Class"},{"name":"DefaultCertificateDiskCache","href":"Titanium.Web.Proxy.Network.DefaultCertificateDiskCache.html","topicHref":"Titanium.Web.Proxy.Network.DefaultCertificateDiskCache.html","topicUid":"Titanium.Web.Proxy.Network.DefaultCertificateDiskCache","type":"Class"},{"name":"FirefoxCertificateTrust","href":"Titanium.Web.Proxy.Network.FirefoxCertificateTrust.html","topicHref":"Titanium.Web.Proxy.Network.FirefoxCertificateTrust.html","topicUid":"Titanium.Web.Proxy.Network.FirefoxCertificateTrust","type":"Class"},{"name":"ICertificateCache","href":"Titanium.Web.Proxy.Network.ICertificateCache.html","topicHref":"Titanium.Web.Proxy.Network.ICertificateCache.html","topicUid":"Titanium.Web.Proxy.Network.ICertificateCache","type":"Interface"}]},{"name":"Titanium.Web.Proxy.Network.Quic","href":"Titanium.Web.Proxy.Network.Quic.html","topicHref":"Titanium.Web.Proxy.Network.Quic.html","topicUid":"Titanium.Web.Proxy.Network.Quic","type":"Namespace","items":[{"name":"IOriginalDestinationResolver","href":"Titanium.Web.Proxy.Network.Quic.IOriginalDestinationResolver.html","topicHref":"Titanium.Web.Proxy.Network.Quic.IOriginalDestinationResolver.html","topicUid":"Titanium.Web.Proxy.Network.Quic.IOriginalDestinationResolver","type":"Interface"}]},{"name":"Titanium.Web.Proxy.Options","href":"Titanium.Web.Proxy.Options.html","topicHref":"Titanium.Web.Proxy.Options.html","topicUid":"Titanium.Web.Proxy.Options","type":"Namespace","items":[{"name":"PolicyFamily","href":"Titanium.Web.Proxy.Options.PolicyFamily.html","topicHref":"Titanium.Web.Proxy.Options.PolicyFamily.html","topicUid":"Titanium.Web.Proxy.Options.PolicyFamily","type":"Enum"},{"name":"PolicyMode","href":"Titanium.Web.Proxy.Options.PolicyMode.html","topicHref":"Titanium.Web.Proxy.Options.PolicyMode.html","topicUid":"Titanium.Web.Proxy.Options.PolicyMode","type":"Enum"},{"name":"ProxyPolicyModes","href":"Titanium.Web.Proxy.Options.ProxyPolicyModes.html","topicHref":"Titanium.Web.Proxy.Options.ProxyPolicyModes.html","topicUid":"Titanium.Web.Proxy.Options.ProxyPolicyModes","type":"Class"},{"name":"ProxyProfile","href":"Titanium.Web.Proxy.Options.ProxyProfile.html","topicHref":"Titanium.Web.Proxy.Options.ProxyProfile.html","topicUid":"Titanium.Web.Proxy.Options.ProxyProfile","type":"Enum"},{"name":"ProxyProfileSettings","href":"Titanium.Web.Proxy.Options.ProxyProfileSettings.html","topicHref":"Titanium.Web.Proxy.Options.ProxyProfileSettings.html","topicUid":"Titanium.Web.Proxy.Options.ProxyProfileSettings","type":"Class"},{"name":"ProxyResourceLimits","href":"Titanium.Web.Proxy.Options.ProxyResourceLimits.html","topicHref":"Titanium.Web.Proxy.Options.ProxyResourceLimits.html","topicUid":"Titanium.Web.Proxy.Options.ProxyResourceLimits","type":"Class"},{"name":"ProxyTimeoutOptions","href":"Titanium.Web.Proxy.Options.ProxyTimeoutOptions.html","topicHref":"Titanium.Web.Proxy.Options.ProxyTimeoutOptions.html","topicUid":"Titanium.Web.Proxy.Options.ProxyTimeoutOptions","type":"Class"},{"name":"ResolvedSessionPolicy","href":"Titanium.Web.Proxy.Options.ResolvedSessionPolicy.html","topicHref":"Titanium.Web.Proxy.Options.ResolvedSessionPolicy.html","topicUid":"Titanium.Web.Proxy.Options.ResolvedSessionPolicy","type":"Class"}]},{"name":"Titanium.Web.Proxy.Routing","href":"Titanium.Web.Proxy.Routing.html","topicHref":"Titanium.Web.Proxy.Routing.html","topicUid":"Titanium.Web.Proxy.Routing","type":"Namespace","items":[{"name":"ReverseProxyFastPath","href":"Titanium.Web.Proxy.Routing.ReverseProxyFastPath.html","topicHref":"Titanium.Web.Proxy.Routing.ReverseProxyFastPath.html","topicUid":"Titanium.Web.Proxy.Routing.ReverseProxyFastPath","type":"Class"},{"name":"RouteMatcher","href":"Titanium.Web.Proxy.Routing.RouteMatcher.html","topicHref":"Titanium.Web.Proxy.Routing.RouteMatcher.html","topicUid":"Titanium.Web.Proxy.Routing.RouteMatcher","type":"Class"}]},{"name":"Titanium.Web.Proxy.StreamExtended","href":"Titanium.Web.Proxy.StreamExtended.html","topicHref":"Titanium.Web.Proxy.StreamExtended.html","topicUid":"Titanium.Web.Proxy.StreamExtended","type":"Namespace","items":[{"name":"ClientHelloInfo","href":"Titanium.Web.Proxy.StreamExtended.ClientHelloInfo.html","topicHref":"Titanium.Web.Proxy.StreamExtended.ClientHelloInfo.html","topicUid":"Titanium.Web.Proxy.StreamExtended.ClientHelloInfo","type":"Class"},{"name":"ServerHelloInfo","href":"Titanium.Web.Proxy.StreamExtended.ServerHelloInfo.html","topicHref":"Titanium.Web.Proxy.StreamExtended.ServerHelloInfo.html","topicUid":"Titanium.Web.Proxy.StreamExtended.ServerHelloInfo","type":"Class"}]},{"name":"Titanium.Web.Proxy.StreamExtended.BufferPool","href":"Titanium.Web.Proxy.StreamExtended.BufferPool.html","topicHref":"Titanium.Web.Proxy.StreamExtended.BufferPool.html","topicUid":"Titanium.Web.Proxy.StreamExtended.BufferPool","type":"Namespace","items":[{"name":"IBufferPool","href":"Titanium.Web.Proxy.StreamExtended.BufferPool.IBufferPool.html","topicHref":"Titanium.Web.Proxy.StreamExtended.BufferPool.IBufferPool.html","topicUid":"Titanium.Web.Proxy.StreamExtended.BufferPool.IBufferPool","type":"Interface"}]},{"name":"Titanium.Web.Proxy.StreamExtended.Models","href":"Titanium.Web.Proxy.StreamExtended.Models.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Models.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Models","type":"Namespace","items":[{"name":"SslExtension","href":"Titanium.Web.Proxy.StreamExtended.Models.SslExtension.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Models.SslExtension.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Models.SslExtension","type":"Class"}]},{"name":"Titanium.Web.Proxy.StreamExtended.Network","href":"Titanium.Web.Proxy.StreamExtended.Network.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network","type":"Namespace","items":[{"name":"DataEventArgs","href":"Titanium.Web.Proxy.StreamExtended.Network.DataEventArgs.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.DataEventArgs.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.DataEventArgs","type":"Class"},{"name":"IHttpStreamReader","href":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamReader.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamReader.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamReader","type":"Interface"},{"name":"IHttpStreamWriter","href":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamWriter.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamWriter.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamWriter","type":"Interface"},{"name":"ILineStream","href":"Titanium.Web.Proxy.StreamExtended.Network.ILineStream.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.ILineStream.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.ILineStream","type":"Interface"},{"name":"IPeekStream","href":"Titanium.Web.Proxy.StreamExtended.Network.IPeekStream.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.IPeekStream.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.IPeekStream","type":"Interface"}]},{"name":"Titanium.Web.Proxy.Transforms","href":"Titanium.Web.Proxy.Transforms.html","topicHref":"Titanium.Web.Proxy.Transforms.html","topicUid":"Titanium.Web.Proxy.Transforms","type":"Namespace","items":[{"name":"TransformEngine","href":"Titanium.Web.Proxy.Transforms.TransformEngine.html","topicHref":"Titanium.Web.Proxy.Transforms.TransformEngine.html","topicUid":"Titanium.Web.Proxy.Transforms.TransformEngine","type":"Class"}]}],"memberLayout":"SamePage"} +{"items":[{"name":"Titanium.Web.Proxy","href":"Titanium.Web.Proxy.html","topicHref":"Titanium.Web.Proxy.html","topicUid":"Titanium.Web.Proxy","type":"Namespace","items":[{"name":"ClientProcessId","href":"Titanium.Web.Proxy.ClientProcessId.html","topicHref":"Titanium.Web.Proxy.ClientProcessId.html","topicUid":"Titanium.Web.Proxy.ClientProcessId","type":"Class"},{"name":"MitmExclusionDefaults","href":"Titanium.Web.Proxy.MitmExclusionDefaults.html","topicHref":"Titanium.Web.Proxy.MitmExclusionDefaults.html","topicUid":"Titanium.Web.Proxy.MitmExclusionDefaults","type":"Class"},{"name":"MitmExclusionMode","href":"Titanium.Web.Proxy.MitmExclusionMode.html","topicHref":"Titanium.Web.Proxy.MitmExclusionMode.html","topicUid":"Titanium.Web.Proxy.MitmExclusionMode","type":"Enum"},{"name":"ProxyLimits","href":"Titanium.Web.Proxy.ProxyLimits.html","topicHref":"Titanium.Web.Proxy.ProxyLimits.html","topicUid":"Titanium.Web.Proxy.ProxyLimits","type":"Class"},{"name":"ProxyServer","href":"Titanium.Web.Proxy.ProxyServer.html","topicHref":"Titanium.Web.Proxy.ProxyServer.html","topicUid":"Titanium.Web.Proxy.ProxyServer","type":"Class"},{"name":"SystemProxyBypassRuleMode","href":"Titanium.Web.Proxy.SystemProxyBypassRuleMode.html","topicHref":"Titanium.Web.Proxy.SystemProxyBypassRuleMode.html","topicUid":"Titanium.Web.Proxy.SystemProxyBypassRuleMode","type":"Enum"},{"name":"SystemProxyChangeResult","href":"Titanium.Web.Proxy.SystemProxyChangeResult.html","topicHref":"Titanium.Web.Proxy.SystemProxyChangeResult.html","topicUid":"Titanium.Web.Proxy.SystemProxyChangeResult","type":"Struct"},{"name":"SystemProxyLoopbackPlacement","href":"Titanium.Web.Proxy.SystemProxyLoopbackPlacement.html","topicHref":"Titanium.Web.Proxy.SystemProxyLoopbackPlacement.html","topicUid":"Titanium.Web.Proxy.SystemProxyLoopbackPlacement","type":"Enum"},{"name":"SystemProxySettings","href":"Titanium.Web.Proxy.SystemProxySettings.html","topicHref":"Titanium.Web.Proxy.SystemProxySettings.html","topicUid":"Titanium.Web.Proxy.SystemProxySettings","type":"Class"},{"name":"WebSocketDecoder","href":"Titanium.Web.Proxy.WebSocketDecoder.html","topicHref":"Titanium.Web.Proxy.WebSocketDecoder.html","topicUid":"Titanium.Web.Proxy.WebSocketDecoder","type":"Class"},{"name":"WebSocketFrame","href":"Titanium.Web.Proxy.WebSocketFrame.html","topicHref":"Titanium.Web.Proxy.WebSocketFrame.html","topicUid":"Titanium.Web.Proxy.WebSocketFrame","type":"Class"},{"name":"WebSocketFrameEncoder","href":"Titanium.Web.Proxy.WebSocketFrameEncoder.html","topicHref":"Titanium.Web.Proxy.WebSocketFrameEncoder.html","topicUid":"Titanium.Web.Proxy.WebSocketFrameEncoder","type":"Class"},{"name":"WebSocketFrameWriter","href":"Titanium.Web.Proxy.WebSocketFrameWriter.html","topicHref":"Titanium.Web.Proxy.WebSocketFrameWriter.html","topicUid":"Titanium.Web.Proxy.WebSocketFrameWriter","type":"Class"},{"name":"WebSocketProtocolException","href":"Titanium.Web.Proxy.WebSocketProtocolException.html","topicHref":"Titanium.Web.Proxy.WebSocketProtocolException.html","topicUid":"Titanium.Web.Proxy.WebSocketProtocolException","type":"Class"},{"name":"WebsocketOpCode","href":"Titanium.Web.Proxy.WebsocketOpCode.html","topicHref":"Titanium.Web.Proxy.WebsocketOpCode.html","topicUid":"Titanium.Web.Proxy.WebsocketOpCode","type":"Enum"}]},{"name":"Titanium.Web.Proxy.Caching","href":"Titanium.Web.Proxy.Caching.html","topicHref":"Titanium.Web.Proxy.Caching.html","topicUid":"Titanium.Web.Proxy.Caching","type":"Namespace","items":[{"name":"HttpResponseCacheMiddleware","href":"Titanium.Web.Proxy.Caching.HttpResponseCacheMiddleware.html","topicHref":"Titanium.Web.Proxy.Caching.HttpResponseCacheMiddleware.html","topicUid":"Titanium.Web.Proxy.Caching.HttpResponseCacheMiddleware","type":"Class"},{"name":"MemoryHttpResponseCache","href":"Titanium.Web.Proxy.Caching.MemoryHttpResponseCache.html","topicHref":"Titanium.Web.Proxy.Caching.MemoryHttpResponseCache.html","topicUid":"Titanium.Web.Proxy.Caching.MemoryHttpResponseCache","type":"Class"}]},{"name":"Titanium.Web.Proxy.Clusters","href":"Titanium.Web.Proxy.Clusters.html","topicHref":"Titanium.Web.Proxy.Clusters.html","topicUid":"Titanium.Web.Proxy.Clusters","type":"Namespace","items":[{"name":"ClusterManager","href":"Titanium.Web.Proxy.Clusters.ClusterManager.html","topicHref":"Titanium.Web.Proxy.Clusters.ClusterManager.html","topicUid":"Titanium.Web.Proxy.Clusters.ClusterManager","type":"Class"},{"name":"DestinationHealthTracker","href":"Titanium.Web.Proxy.Clusters.DestinationHealthTracker.html","topicHref":"Titanium.Web.Proxy.Clusters.DestinationHealthTracker.html","topicUid":"Titanium.Web.Proxy.Clusters.DestinationHealthTracker","type":"Class"},{"name":"LoadBalancer","href":"Titanium.Web.Proxy.Clusters.LoadBalancer.html","topicHref":"Titanium.Web.Proxy.Clusters.LoadBalancer.html","topicUid":"Titanium.Web.Proxy.Clusters.LoadBalancer","type":"Class"}]},{"name":"Titanium.Web.Proxy.Diagnostics","href":"Titanium.Web.Proxy.Diagnostics.html","topicHref":"Titanium.Web.Proxy.Diagnostics.html","topicUid":"Titanium.Web.Proxy.Diagnostics","type":"Namespace","items":[{"name":"ClientTlsTiming","href":"Titanium.Web.Proxy.Diagnostics.ClientTlsTiming.html","topicHref":"Titanium.Web.Proxy.Diagnostics.ClientTlsTiming.html","topicUid":"Titanium.Web.Proxy.Diagnostics.ClientTlsTiming","type":"Class"},{"name":"HttpRequestTiming","href":"Titanium.Web.Proxy.Diagnostics.HttpRequestTiming.html","topicHref":"Titanium.Web.Proxy.Diagnostics.HttpRequestTiming.html","topicUid":"Titanium.Web.Proxy.Diagnostics.HttpRequestTiming","type":"Class"},{"name":"TunnelConnectTiming","href":"Titanium.Web.Proxy.Diagnostics.TunnelConnectTiming.html","topicHref":"Titanium.Web.Proxy.Diagnostics.TunnelConnectTiming.html","topicUid":"Titanium.Web.Proxy.Diagnostics.TunnelConnectTiming","type":"Class"},{"name":"UpstreamConnectionTiming","href":"Titanium.Web.Proxy.Diagnostics.UpstreamConnectionTiming.html","topicHref":"Titanium.Web.Proxy.Diagnostics.UpstreamConnectionTiming.html","topicUid":"Titanium.Web.Proxy.Diagnostics.UpstreamConnectionTiming","type":"Class"}]},{"name":"Titanium.Web.Proxy.EventArguments","href":"Titanium.Web.Proxy.EventArguments.html","topicHref":"Titanium.Web.Proxy.EventArguments.html","topicUid":"Titanium.Web.Proxy.EventArguments","type":"Namespace","items":[{"name":"AsyncEventHandler","href":"Titanium.Web.Proxy.EventArguments.AsyncEventHandler-1.html","topicHref":"Titanium.Web.Proxy.EventArguments.AsyncEventHandler-1.html","topicUid":"Titanium.Web.Proxy.EventArguments.AsyncEventHandler`1","type":"Delegate"},{"name":"BeforeBodyWriteEventArgs","href":"Titanium.Web.Proxy.EventArguments.BeforeBodyWriteEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.BeforeBodyWriteEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.BeforeBodyWriteEventArgs","type":"Class"},{"name":"BeforeHttpAuthenticateEventArgs","href":"Titanium.Web.Proxy.EventArguments.BeforeHttpAuthenticateEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.BeforeHttpAuthenticateEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.BeforeHttpAuthenticateEventArgs","type":"Class"},{"name":"BeforeQuicAuthenticateEventArgs","href":"Titanium.Web.Proxy.EventArguments.BeforeQuicAuthenticateEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.BeforeQuicAuthenticateEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.BeforeQuicAuthenticateEventArgs","type":"Class"},{"name":"BeforeSslAuthenticateEventArgs","href":"Titanium.Web.Proxy.EventArguments.BeforeSslAuthenticateEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.BeforeSslAuthenticateEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.BeforeSslAuthenticateEventArgs","type":"Class"},{"name":"CertificateSelectionEventArgs","href":"Titanium.Web.Proxy.EventArguments.CertificateSelectionEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.CertificateSelectionEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.CertificateSelectionEventArgs","type":"Class"},{"name":"CertificateValidationEventArgs","href":"Titanium.Web.Proxy.EventArguments.CertificateValidationEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.CertificateValidationEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.CertificateValidationEventArgs","type":"Class"},{"name":"MultipartRequestPartSentEventArgs","href":"Titanium.Web.Proxy.EventArguments.MultipartRequestPartSentEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.MultipartRequestPartSentEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.MultipartRequestPartSentEventArgs","type":"Class"},{"name":"ProxyEventArgsBase","href":"Titanium.Web.Proxy.EventArguments.ProxyEventArgsBase.html","topicHref":"Titanium.Web.Proxy.EventArguments.ProxyEventArgsBase.html","topicUid":"Titanium.Web.Proxy.EventArguments.ProxyEventArgsBase","type":"Class"},{"name":"SessionEventArgs","href":"Titanium.Web.Proxy.EventArguments.SessionEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.SessionEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.SessionEventArgs","type":"Class"},{"name":"SessionEventArgsBase","href":"Titanium.Web.Proxy.EventArguments.SessionEventArgsBase.html","topicHref":"Titanium.Web.Proxy.EventArguments.SessionEventArgsBase.html","topicUid":"Titanium.Web.Proxy.EventArguments.SessionEventArgsBase","type":"Class"},{"name":"SocksAuthenticateEventArgs","href":"Titanium.Web.Proxy.EventArguments.SocksAuthenticateEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.SocksAuthenticateEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.SocksAuthenticateEventArgs","type":"Class"},{"name":"TunnelConnectFailureEventArgs","href":"Titanium.Web.Proxy.EventArguments.TunnelConnectFailureEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.TunnelConnectFailureEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.TunnelConnectFailureEventArgs","type":"Class"},{"name":"TunnelConnectSessionEventArgs","href":"Titanium.Web.Proxy.EventArguments.TunnelConnectSessionEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.TunnelConnectSessionEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.TunnelConnectSessionEventArgs","type":"Class"},{"name":"WebSocketFrameDirection","href":"Titanium.Web.Proxy.EventArguments.WebSocketFrameDirection.html","topicHref":"Titanium.Web.Proxy.EventArguments.WebSocketFrameDirection.html","topicUid":"Titanium.Web.Proxy.EventArguments.WebSocketFrameDirection","type":"Enum"},{"name":"WebSocketFrameInterceptAction","href":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptAction.html","topicHref":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptAction.html","topicUid":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptAction","type":"Enum"},{"name":"WebSocketFrameInterceptEventArgs","href":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptEventArgs","type":"Class"}]},{"name":"Titanium.Web.Proxy.Exceptions","href":"Titanium.Web.Proxy.Exceptions.html","topicHref":"Titanium.Web.Proxy.Exceptions.html","topicUid":"Titanium.Web.Proxy.Exceptions","type":"Namespace","items":[{"name":"BodyNotFoundException","href":"Titanium.Web.Proxy.Exceptions.BodyNotFoundException.html","topicHref":"Titanium.Web.Proxy.Exceptions.BodyNotFoundException.html","topicUid":"Titanium.Web.Proxy.Exceptions.BodyNotFoundException","type":"Class"},{"name":"OutboundDestinationBlockedException","href":"Titanium.Web.Proxy.Exceptions.OutboundDestinationBlockedException.html","topicHref":"Titanium.Web.Proxy.Exceptions.OutboundDestinationBlockedException.html","topicUid":"Titanium.Web.Proxy.Exceptions.OutboundDestinationBlockedException","type":"Class"},{"name":"ProxyAuthorizationException","href":"Titanium.Web.Proxy.Exceptions.ProxyAuthorizationException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyAuthorizationException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyAuthorizationException","type":"Class"},{"name":"ProxyConnectException","href":"Titanium.Web.Proxy.Exceptions.ProxyConnectException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyConnectException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyConnectException","type":"Class"},{"name":"ProxyException","href":"Titanium.Web.Proxy.Exceptions.ProxyException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyException","type":"Class"},{"name":"ProxyHttpException","href":"Titanium.Web.Proxy.Exceptions.ProxyHttpException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyHttpException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyHttpException","type":"Class"},{"name":"ProxyTimeoutException","href":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutException","type":"Class"},{"name":"ProxyTimeoutKind","href":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutKind.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutKind.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutKind","type":"Enum"},{"name":"UpstreamProxyConnectException","href":"Titanium.Web.Proxy.Exceptions.UpstreamProxyConnectException.html","topicHref":"Titanium.Web.Proxy.Exceptions.UpstreamProxyConnectException.html","topicUid":"Titanium.Web.Proxy.Exceptions.UpstreamProxyConnectException","type":"Class"}]},{"name":"Titanium.Web.Proxy.Helpers","href":"Titanium.Web.Proxy.Helpers.html","topicHref":"Titanium.Web.Proxy.Helpers.html","topicUid":"Titanium.Web.Proxy.Helpers","type":"Namespace","items":[{"name":"UnixProxyBypassMapper","href":"Titanium.Web.Proxy.Helpers.UnixProxyBypassMapper.html","topicHref":"Titanium.Web.Proxy.Helpers.UnixProxyBypassMapper.html","topicUid":"Titanium.Web.Proxy.Helpers.UnixProxyBypassMapper","type":"Class"}]},{"name":"Titanium.Web.Proxy.Http","href":"Titanium.Web.Proxy.Http.html","topicHref":"Titanium.Web.Proxy.Http.html","topicUid":"Titanium.Web.Proxy.Http","type":"Namespace","items":[{"name":"ConnectRequest","href":"Titanium.Web.Proxy.Http.ConnectRequest.html","topicHref":"Titanium.Web.Proxy.Http.ConnectRequest.html","topicUid":"Titanium.Web.Proxy.Http.ConnectRequest","type":"Class"},{"name":"ConnectResponse","href":"Titanium.Web.Proxy.Http.ConnectResponse.html","topicHref":"Titanium.Web.Proxy.Http.ConnectResponse.html","topicUid":"Titanium.Web.Proxy.Http.ConnectResponse","type":"Class"},{"name":"HeaderCollection","href":"Titanium.Web.Proxy.Http.HeaderCollection.html","topicHref":"Titanium.Web.Proxy.Http.HeaderCollection.html","topicUid":"Titanium.Web.Proxy.Http.HeaderCollection","type":"Class"},{"name":"HeaderCollection.Enumerator","href":"Titanium.Web.Proxy.Http.HeaderCollection.Enumerator.html","topicHref":"Titanium.Web.Proxy.Http.HeaderCollection.Enumerator.html","topicUid":"Titanium.Web.Proxy.Http.HeaderCollection.Enumerator","type":"Struct"},{"name":"HttpWebClient","href":"Titanium.Web.Proxy.Http.HttpWebClient.html","topicHref":"Titanium.Web.Proxy.Http.HttpWebClient.html","topicUid":"Titanium.Web.Proxy.Http.HttpWebClient","type":"Class"},{"name":"KnownHeader","href":"Titanium.Web.Proxy.Http.KnownHeader.html","topicHref":"Titanium.Web.Proxy.Http.KnownHeader.html","topicUid":"Titanium.Web.Proxy.Http.KnownHeader","type":"Class"},{"name":"KnownHeaders","href":"Titanium.Web.Proxy.Http.KnownHeaders.html","topicHref":"Titanium.Web.Proxy.Http.KnownHeaders.html","topicUid":"Titanium.Web.Proxy.Http.KnownHeaders","type":"Class"},{"name":"ProxyResults","href":"Titanium.Web.Proxy.Http.ProxyResults.html","topicHref":"Titanium.Web.Proxy.Http.ProxyResults.html","topicUid":"Titanium.Web.Proxy.Http.ProxyResults","type":"Class"},{"name":"Request","href":"Titanium.Web.Proxy.Http.Request.html","topicHref":"Titanium.Web.Proxy.Http.Request.html","topicUid":"Titanium.Web.Proxy.Http.Request","type":"Class"},{"name":"RequestResponseBase","href":"Titanium.Web.Proxy.Http.RequestResponseBase.html","topicHref":"Titanium.Web.Proxy.Http.RequestResponseBase.html","topicUid":"Titanium.Web.Proxy.Http.RequestResponseBase","type":"Class"},{"name":"Response","href":"Titanium.Web.Proxy.Http.Response.html","topicHref":"Titanium.Web.Proxy.Http.Response.html","topicUid":"Titanium.Web.Proxy.Http.Response","type":"Class"},{"name":"StreamingProxyResult","href":"Titanium.Web.Proxy.Http.StreamingProxyResult.html","topicHref":"Titanium.Web.Proxy.Http.StreamingProxyResult.html","topicUid":"Titanium.Web.Proxy.Http.StreamingProxyResult","type":"Struct"},{"name":"TunnelType","href":"Titanium.Web.Proxy.Http.TunnelType.html","topicHref":"Titanium.Web.Proxy.Http.TunnelType.html","topicUid":"Titanium.Web.Proxy.Http.TunnelType","type":"Enum"}]},{"name":"Titanium.Web.Proxy.Http.Responses","href":"Titanium.Web.Proxy.Http.Responses.html","topicHref":"Titanium.Web.Proxy.Http.Responses.html","topicUid":"Titanium.Web.Proxy.Http.Responses","type":"Namespace","items":[{"name":"GenericResponse","href":"Titanium.Web.Proxy.Http.Responses.GenericResponse.html","topicHref":"Titanium.Web.Proxy.Http.Responses.GenericResponse.html","topicUid":"Titanium.Web.Proxy.Http.Responses.GenericResponse","type":"Class"},{"name":"OkResponse","href":"Titanium.Web.Proxy.Http.Responses.OkResponse.html","topicHref":"Titanium.Web.Proxy.Http.Responses.OkResponse.html","topicUid":"Titanium.Web.Proxy.Http.Responses.OkResponse","type":"Class"},{"name":"RedirectResponse","href":"Titanium.Web.Proxy.Http.Responses.RedirectResponse.html","topicHref":"Titanium.Web.Proxy.Http.Responses.RedirectResponse.html","topicUid":"Titanium.Web.Proxy.Http.Responses.RedirectResponse","type":"Class"}]},{"name":"Titanium.Web.Proxy.Http3","href":"Titanium.Web.Proxy.Http3.html","topicHref":"Titanium.Web.Proxy.Http3.html","topicUid":"Titanium.Web.Proxy.Http3","type":"Namespace","items":[{"name":"Http3NativeBootstrap","href":"Titanium.Web.Proxy.Http3.Http3NativeBootstrap.html","topicHref":"Titanium.Web.Proxy.Http3.Http3NativeBootstrap.html","topicUid":"Titanium.Web.Proxy.Http3.Http3NativeBootstrap","type":"Class"}]},{"name":"Titanium.Web.Proxy.Logging","href":"Titanium.Web.Proxy.Logging.html","topicHref":"Titanium.Web.Proxy.Logging.html","topicUid":"Titanium.Web.Proxy.Logging","type":"Namespace","items":[{"name":"ProxyLoggingOptions","href":"Titanium.Web.Proxy.Logging.ProxyLoggingOptions.html","topicHref":"Titanium.Web.Proxy.Logging.ProxyLoggingOptions.html","topicUid":"Titanium.Web.Proxy.Logging.ProxyLoggingOptions","type":"Class"}]},{"name":"Titanium.Web.Proxy.Middleware","href":"Titanium.Web.Proxy.Middleware.html","topicHref":"Titanium.Web.Proxy.Middleware.html","topicUid":"Titanium.Web.Proxy.Middleware","type":"Namespace","items":[{"name":"ProxyMiddlewarePipeline","href":"Titanium.Web.Proxy.Middleware.ProxyMiddlewarePipeline.html","topicHref":"Titanium.Web.Proxy.Middleware.ProxyMiddlewarePipeline.html","topicUid":"Titanium.Web.Proxy.Middleware.ProxyMiddlewarePipeline","type":"Class"}]},{"name":"Titanium.Web.Proxy.Models","href":"Titanium.Web.Proxy.Models.html","topicHref":"Titanium.Web.Proxy.Models.html","topicUid":"Titanium.Web.Proxy.Models","type":"Namespace","items":[{"name":"DecryptFailureBypassEntry","href":"Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html","topicHref":"Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html","topicUid":"Titanium.Web.Proxy.Models.DecryptFailureBypassEntry","type":"Class"},{"name":"ExplicitProxyEndPoint","href":"Titanium.Web.Proxy.Models.ExplicitProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.ExplicitProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.ExplicitProxyEndPoint","type":"Class"},{"name":"ExternalProxy","href":"Titanium.Web.Proxy.Models.ExternalProxy.html","topicHref":"Titanium.Web.Proxy.Models.ExternalProxy.html","topicUid":"Titanium.Web.Proxy.Models.ExternalProxy","type":"Class"},{"name":"ExternalProxyType","href":"Titanium.Web.Proxy.Models.ExternalProxyType.html","topicHref":"Titanium.Web.Proxy.Models.ExternalProxyType.html","topicUid":"Titanium.Web.Proxy.Models.ExternalProxyType","type":"Enum"},{"name":"HttpHeader","href":"Titanium.Web.Proxy.Models.HttpHeader.html","topicHref":"Titanium.Web.Proxy.Models.HttpHeader.html","topicUid":"Titanium.Web.Proxy.Models.HttpHeader","type":"Class"},{"name":"HttpInterceptionContext","href":"Titanium.Web.Proxy.Models.HttpInterceptionContext.html","topicHref":"Titanium.Web.Proxy.Models.HttpInterceptionContext.html","topicUid":"Titanium.Web.Proxy.Models.HttpInterceptionContext","type":"Struct"},{"name":"IExternalProxy","href":"Titanium.Web.Proxy.Models.IExternalProxy.html","topicHref":"Titanium.Web.Proxy.Models.IExternalProxy.html","topicUid":"Titanium.Web.Proxy.Models.IExternalProxy","type":"Interface"},{"name":"OriginHttpVersionPolicy","href":"Titanium.Web.Proxy.Models.OriginHttpVersionPolicy.html","topicHref":"Titanium.Web.Proxy.Models.OriginHttpVersionPolicy.html","topicUid":"Titanium.Web.Proxy.Models.OriginHttpVersionPolicy","type":"Enum"},{"name":"ProxyAuthenticationContext","href":"Titanium.Web.Proxy.Models.ProxyAuthenticationContext.html","topicHref":"Titanium.Web.Proxy.Models.ProxyAuthenticationContext.html","topicUid":"Titanium.Web.Proxy.Models.ProxyAuthenticationContext","type":"Class"},{"name":"ProxyAuthenticationResult","href":"Titanium.Web.Proxy.Models.ProxyAuthenticationResult.html","topicHref":"Titanium.Web.Proxy.Models.ProxyAuthenticationResult.html","topicUid":"Titanium.Web.Proxy.Models.ProxyAuthenticationResult","type":"Enum"},{"name":"ProxyEndPoint","href":"Titanium.Web.Proxy.Models.ProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.ProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.ProxyEndPoint","type":"Class"},{"name":"ProxyProtocolType","href":"Titanium.Web.Proxy.Models.ProxyProtocolType.html","topicHref":"Titanium.Web.Proxy.Models.ProxyProtocolType.html","topicUid":"Titanium.Web.Proxy.Models.ProxyProtocolType","type":"Enum"},{"name":"SocksProxyEndPoint","href":"Titanium.Web.Proxy.Models.SocksProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.SocksProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.SocksProxyEndPoint","type":"Class"},{"name":"TransparentBaseProxyEndPoint","href":"Titanium.Web.Proxy.Models.TransparentBaseProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.TransparentBaseProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.TransparentBaseProxyEndPoint","type":"Class"},{"name":"TransparentProxyEndPoint","href":"Titanium.Web.Proxy.Models.TransparentProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.TransparentProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.TransparentProxyEndPoint","type":"Class"},{"name":"TransparentQuicProxyEndPoint","href":"Titanium.Web.Proxy.Models.TransparentQuicProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.TransparentQuicProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.TransparentQuicProxyEndPoint","type":"Class"},{"name":"UpstreamHttpProtocol","href":"Titanium.Web.Proxy.Models.UpstreamHttpProtocol.html","topicHref":"Titanium.Web.Proxy.Models.UpstreamHttpProtocol.html","topicUid":"Titanium.Web.Proxy.Models.UpstreamHttpProtocol","type":"Enum"},{"name":"WinAuthCredentials","href":"Titanium.Web.Proxy.Models.WinAuthCredentials.html","topicHref":"Titanium.Web.Proxy.Models.WinAuthCredentials.html","topicUid":"Titanium.Web.Proxy.Models.WinAuthCredentials","type":"Class"}]},{"name":"Titanium.Web.Proxy.Network","href":"Titanium.Web.Proxy.Network.html","topicHref":"Titanium.Web.Proxy.Network.html","topicUid":"Titanium.Web.Proxy.Network","type":"Namespace","items":[{"name":"CertificateEngine","href":"Titanium.Web.Proxy.Network.CertificateEngine.html","topicHref":"Titanium.Web.Proxy.Network.CertificateEngine.html","topicUid":"Titanium.Web.Proxy.Network.CertificateEngine","type":"Enum"},{"name":"CertificateKeyAlgorithm","href":"Titanium.Web.Proxy.Network.CertificateKeyAlgorithm.html","topicHref":"Titanium.Web.Proxy.Network.CertificateKeyAlgorithm.html","topicUid":"Titanium.Web.Proxy.Network.CertificateKeyAlgorithm","type":"Enum"},{"name":"CertificateManager","href":"Titanium.Web.Proxy.Network.CertificateManager.html","topicHref":"Titanium.Web.Proxy.Network.CertificateManager.html","topicUid":"Titanium.Web.Proxy.Network.CertificateManager","type":"Class"},{"name":"CertificateOsTrustKind","href":"Titanium.Web.Proxy.Network.CertificateOsTrustKind.html","topicHref":"Titanium.Web.Proxy.Network.CertificateOsTrustKind.html","topicUid":"Titanium.Web.Proxy.Network.CertificateOsTrustKind","type":"Enum"},{"name":"CertificateOsTrustResult","href":"Titanium.Web.Proxy.Network.CertificateOsTrustResult.html","topicHref":"Titanium.Web.Proxy.Network.CertificateOsTrustResult.html","topicUid":"Titanium.Web.Proxy.Network.CertificateOsTrustResult","type":"Class"},{"name":"DefaultCertificateDiskCache","href":"Titanium.Web.Proxy.Network.DefaultCertificateDiskCache.html","topicHref":"Titanium.Web.Proxy.Network.DefaultCertificateDiskCache.html","topicUid":"Titanium.Web.Proxy.Network.DefaultCertificateDiskCache","type":"Class"},{"name":"FirefoxCertificateTrust","href":"Titanium.Web.Proxy.Network.FirefoxCertificateTrust.html","topicHref":"Titanium.Web.Proxy.Network.FirefoxCertificateTrust.html","topicUid":"Titanium.Web.Proxy.Network.FirefoxCertificateTrust","type":"Class"},{"name":"ICertificateCache","href":"Titanium.Web.Proxy.Network.ICertificateCache.html","topicHref":"Titanium.Web.Proxy.Network.ICertificateCache.html","topicUid":"Titanium.Web.Proxy.Network.ICertificateCache","type":"Interface"}]},{"name":"Titanium.Web.Proxy.Network.Quic","href":"Titanium.Web.Proxy.Network.Quic.html","topicHref":"Titanium.Web.Proxy.Network.Quic.html","topicUid":"Titanium.Web.Proxy.Network.Quic","type":"Namespace","items":[{"name":"IOriginalDestinationResolver","href":"Titanium.Web.Proxy.Network.Quic.IOriginalDestinationResolver.html","topicHref":"Titanium.Web.Proxy.Network.Quic.IOriginalDestinationResolver.html","topicUid":"Titanium.Web.Proxy.Network.Quic.IOriginalDestinationResolver","type":"Interface"}]},{"name":"Titanium.Web.Proxy.Options","href":"Titanium.Web.Proxy.Options.html","topicHref":"Titanium.Web.Proxy.Options.html","topicUid":"Titanium.Web.Proxy.Options","type":"Namespace","items":[{"name":"PolicyFamily","href":"Titanium.Web.Proxy.Options.PolicyFamily.html","topicHref":"Titanium.Web.Proxy.Options.PolicyFamily.html","topicUid":"Titanium.Web.Proxy.Options.PolicyFamily","type":"Enum"},{"name":"PolicyMode","href":"Titanium.Web.Proxy.Options.PolicyMode.html","topicHref":"Titanium.Web.Proxy.Options.PolicyMode.html","topicUid":"Titanium.Web.Proxy.Options.PolicyMode","type":"Enum"},{"name":"ProxyPolicyModes","href":"Titanium.Web.Proxy.Options.ProxyPolicyModes.html","topicHref":"Titanium.Web.Proxy.Options.ProxyPolicyModes.html","topicUid":"Titanium.Web.Proxy.Options.ProxyPolicyModes","type":"Class"},{"name":"ProxyProfile","href":"Titanium.Web.Proxy.Options.ProxyProfile.html","topicHref":"Titanium.Web.Proxy.Options.ProxyProfile.html","topicUid":"Titanium.Web.Proxy.Options.ProxyProfile","type":"Enum"},{"name":"ProxyProfileSettings","href":"Titanium.Web.Proxy.Options.ProxyProfileSettings.html","topicHref":"Titanium.Web.Proxy.Options.ProxyProfileSettings.html","topicUid":"Titanium.Web.Proxy.Options.ProxyProfileSettings","type":"Class"},{"name":"ProxyResourceLimits","href":"Titanium.Web.Proxy.Options.ProxyResourceLimits.html","topicHref":"Titanium.Web.Proxy.Options.ProxyResourceLimits.html","topicUid":"Titanium.Web.Proxy.Options.ProxyResourceLimits","type":"Class"},{"name":"ProxyTimeoutOptions","href":"Titanium.Web.Proxy.Options.ProxyTimeoutOptions.html","topicHref":"Titanium.Web.Proxy.Options.ProxyTimeoutOptions.html","topicUid":"Titanium.Web.Proxy.Options.ProxyTimeoutOptions","type":"Class"},{"name":"ResolvedSessionPolicy","href":"Titanium.Web.Proxy.Options.ResolvedSessionPolicy.html","topicHref":"Titanium.Web.Proxy.Options.ResolvedSessionPolicy.html","topicUid":"Titanium.Web.Proxy.Options.ResolvedSessionPolicy","type":"Class"}]},{"name":"Titanium.Web.Proxy.Routing","href":"Titanium.Web.Proxy.Routing.html","topicHref":"Titanium.Web.Proxy.Routing.html","topicUid":"Titanium.Web.Proxy.Routing","type":"Namespace","items":[{"name":"ReverseProxyFastPath","href":"Titanium.Web.Proxy.Routing.ReverseProxyFastPath.html","topicHref":"Titanium.Web.Proxy.Routing.ReverseProxyFastPath.html","topicUid":"Titanium.Web.Proxy.Routing.ReverseProxyFastPath","type":"Class"},{"name":"RouteMatcher","href":"Titanium.Web.Proxy.Routing.RouteMatcher.html","topicHref":"Titanium.Web.Proxy.Routing.RouteMatcher.html","topicUid":"Titanium.Web.Proxy.Routing.RouteMatcher","type":"Class"}]},{"name":"Titanium.Web.Proxy.StreamExtended","href":"Titanium.Web.Proxy.StreamExtended.html","topicHref":"Titanium.Web.Proxy.StreamExtended.html","topicUid":"Titanium.Web.Proxy.StreamExtended","type":"Namespace","items":[{"name":"ClientHelloInfo","href":"Titanium.Web.Proxy.StreamExtended.ClientHelloInfo.html","topicHref":"Titanium.Web.Proxy.StreamExtended.ClientHelloInfo.html","topicUid":"Titanium.Web.Proxy.StreamExtended.ClientHelloInfo","type":"Class"},{"name":"ServerHelloInfo","href":"Titanium.Web.Proxy.StreamExtended.ServerHelloInfo.html","topicHref":"Titanium.Web.Proxy.StreamExtended.ServerHelloInfo.html","topicUid":"Titanium.Web.Proxy.StreamExtended.ServerHelloInfo","type":"Class"}]},{"name":"Titanium.Web.Proxy.StreamExtended.BufferPool","href":"Titanium.Web.Proxy.StreamExtended.BufferPool.html","topicHref":"Titanium.Web.Proxy.StreamExtended.BufferPool.html","topicUid":"Titanium.Web.Proxy.StreamExtended.BufferPool","type":"Namespace","items":[{"name":"IBufferPool","href":"Titanium.Web.Proxy.StreamExtended.BufferPool.IBufferPool.html","topicHref":"Titanium.Web.Proxy.StreamExtended.BufferPool.IBufferPool.html","topicUid":"Titanium.Web.Proxy.StreamExtended.BufferPool.IBufferPool","type":"Interface"}]},{"name":"Titanium.Web.Proxy.StreamExtended.Models","href":"Titanium.Web.Proxy.StreamExtended.Models.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Models.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Models","type":"Namespace","items":[{"name":"SslExtension","href":"Titanium.Web.Proxy.StreamExtended.Models.SslExtension.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Models.SslExtension.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Models.SslExtension","type":"Class"}]},{"name":"Titanium.Web.Proxy.StreamExtended.Network","href":"Titanium.Web.Proxy.StreamExtended.Network.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network","type":"Namespace","items":[{"name":"DataEventArgs","href":"Titanium.Web.Proxy.StreamExtended.Network.DataEventArgs.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.DataEventArgs.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.DataEventArgs","type":"Class"},{"name":"IHttpStreamReader","href":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamReader.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamReader.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamReader","type":"Interface"},{"name":"IHttpStreamWriter","href":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamWriter.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamWriter.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamWriter","type":"Interface"},{"name":"ILineStream","href":"Titanium.Web.Proxy.StreamExtended.Network.ILineStream.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.ILineStream.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.ILineStream","type":"Interface"},{"name":"IPeekStream","href":"Titanium.Web.Proxy.StreamExtended.Network.IPeekStream.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.IPeekStream.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.IPeekStream","type":"Interface"}]},{"name":"Titanium.Web.Proxy.Transforms","href":"Titanium.Web.Proxy.Transforms.html","topicHref":"Titanium.Web.Proxy.Transforms.html","topicUid":"Titanium.Web.Proxy.Transforms","type":"Namespace","items":[{"name":"TransformEngine","href":"Titanium.Web.Proxy.Transforms.TransformEngine.html","topicHref":"Titanium.Web.Proxy.Transforms.TransformEngine.html","topicUid":"Titanium.Web.Proxy.Transforms.TransformEngine","type":"Class"}]}],"memberLayout":"SamePage"} diff --git a/docs/index.json b/docs/index.json index 233536c33..037cae34e 100644 --- a/docs/index.json +++ b/docs/index.json @@ -349,6 +349,11 @@ "title": "Enum MitmExclusionMode | Titanium Web Proxy", "summary": "Enum MitmExclusionMode How factory MITM exclusion defaults interact with caller-supplied host lists. Namespace: Titanium.Web.Proxy Assembly: Titanium.Web.Proxy.dll Syntax public enum MitmExclusionMode Fields Name Description Merge Factory OS-bypass and tunnel/SSO decrypt skips are always applied, then caller lists add more (and optional decrypt-only allowlist). Default for back-compat. Replace Caller lists are authoritative. Factory defaults are not re-injected — use them only as a seed when building the lists you pass in." }, + "api/Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html": { + "href": "api/Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html", + "title": "Class DecryptFailureBypassEntry | Titanium Web Proxy", + "summary": "Class DecryptFailureBypassEntry A host that the proxy learned to tunnel without decrypt after repeated origin TLS handshake failures under MITM (e.g. bot / TLS-fingerprint rejection). Inheritance object DecryptFailureBypassEntry Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy.Models Assembly: Titanium.Web.Proxy.dll Syntax public sealed class DecryptFailureBypassEntry Constructors | Edit this page View Source DecryptFailureBypassEntry(string, int, DateTime, DateTime, bool) Declaration public DecryptFailureBypassEntry(string host, int strikes, DateTime learnedAtUtc, DateTime expiresAtUtc, bool bypassActive) Parameters Type Name Description string host int strikes DateTime learnedAtUtc DateTime expiresAtUtc bool bypassActive Properties | Edit this page View Source BypassActive True when subsequent CONNECTs skip decrypt for this host. Declaration public bool BypassActive { get; } Property Value Type Description bool | Edit this page View Source ExpiresAtUtc When this entry expires if not refreshed. Declaration public DateTime ExpiresAtUtc { get; } Property Value Type Description DateTime | Edit this page View Source Host Normalized hostname (no port). Declaration public string Host { get; } Property Value Type Description string | Edit this page View Source LearnedAtUtc When the first strike (or forced bypass) was recorded. Declaration public DateTime LearnedAtUtc { get; } Property Value Type Description DateTime | Edit this page View Source Strikes Origin TLS failure strikes accumulated toward the bypass threshold. Declaration public int Strikes { get; } Property Value Type Description int" + }, "api/Titanium.Web.Proxy.Models.ExplicitProxyEndPoint.html": { "href": "api/Titanium.Web.Proxy.Models.ExplicitProxyEndPoint.html", "title": "Class ExplicitProxyEndPoint | Titanium Web Proxy", @@ -437,7 +442,7 @@ "api/Titanium.Web.Proxy.Models.html": { "href": "api/Titanium.Web.Proxy.Models.html", "title": "Namespace Titanium.Web.Proxy.Models | Titanium Web Proxy", - "summary": "Namespace Titanium.Web.Proxy.Models Classes ExplicitProxyEndPoint A proxy endpoint that the client is aware of. So client application know that it is communicating with a proxy server. ExternalProxy An upstream proxy this proxy uses if any. HttpHeader Http Header object used by proxy ProxyAuthenticationContext A context container for authentication flows ProxyEndPoint An abstract endpoint where the proxy listens SocksProxyEndPoint A proxy end point client is not aware of. Useful when requests are redirected to this proxy end point through port forwarding via router. TransparentBaseProxyEndPoint TransparentProxyEndPoint A proxy end point client is not aware of. Useful when requests are redirected to this proxy end point through port forwarding via router. When EnableHttp3 is true (and EnableHttp3 is enabled with DecryptSsl), the endpoint also listens for HTTP/3 on the same IP:port over UDP and injects Alt-Svc: h3=\":PORT\" into H1/H2 responses — reverse HTTPS with Alt-Svc advertisement. TransparentQuicProxyEndPoint A transparent proxy endpoint that listens on UDP/QUIC and intercepts HTTP/3 traffic. Clients are not aware of the proxy; traffic must be directed here via firewall/NAT redirection. QUIC always terminates TLS 1.3 at the proxy — there is no pass-through mode. An IOriginalDestinationResolver must be configured (or a fixed ForwardHost / ForwardPort fallback) so the proxy knows which origin server each connection is intended for. Platform requirement: IsSupported must be true (MsQuic native library present, OS version supported). If it is false, Start(bool) will throw PlatformNotSupportedException. ECH constraint: when managed DNS advertises ECH for intercepted names, the hidden SNI is encrypted and cannot be extracted here. Either disable ECH for intercepted names in your managed DNS, or configure managed clients to disable ECH. Experimental: HTTP/3 support has not yet completed the full interop/soak/fuzz gate process. Suppress TWP001 to opt in. WinAuthCredentials Windows authentication credentials for NTLM/Negotiate/Kerberos (issue #461). Prefer supplying these through WinAuthCredentialsProvider rather than storing plaintext on session event args. Structs HttpInterceptionContext Minimal, read-only context passed to ShouldInterceptHttp to let callers route requests to the fast-forward path or the full interception path without materialising a SessionEventArgs. Interfaces IExternalProxy Enums ExternalProxyType OriginHttpVersionPolicy Controls which HTTP version the proxy declares to the origin server on the request line, independently of the version the client itself declared on its own connection to the proxy. HTTP/1.0 and HTTP/1.1 share the same start-line/header/body wire format, so switching between them needs no message translation - only the declared version and the resulting default persistence (KeepAlive) change. The response is always written back to the client using the client's own originally declared version and its own persistence rules, regardless of this policy. ProxyAuthenticationResult ProxyProtocolType UpstreamHttpProtocol Controls which HTTP version the proxy uses on its own connection to the origin server, independent of which HTTP version the client used to talk to the proxy. Set a connection-level default on UpstreamHttpProtocol (during BeforeTunnelConnectRequest), UpstreamHttpProtocol (during BeforeSslAuthenticate), or UpstreamHttpProtocol (during BeforeQuicAuthenticate). Per-request overrides are available via UpstreamHttpProtocol in BeforeRequest." + "summary": "Namespace Titanium.Web.Proxy.Models Classes DecryptFailureBypassEntry A host that the proxy learned to tunnel without decrypt after repeated origin TLS handshake failures under MITM (e.g. bot / TLS-fingerprint rejection). ExplicitProxyEndPoint A proxy endpoint that the client is aware of. So client application know that it is communicating with a proxy server. ExternalProxy An upstream proxy this proxy uses if any. HttpHeader Http Header object used by proxy ProxyAuthenticationContext A context container for authentication flows ProxyEndPoint An abstract endpoint where the proxy listens SocksProxyEndPoint A proxy end point client is not aware of. Useful when requests are redirected to this proxy end point through port forwarding via router. TransparentBaseProxyEndPoint TransparentProxyEndPoint A proxy end point client is not aware of. Useful when requests are redirected to this proxy end point through port forwarding via router. When EnableHttp3 is true (and EnableHttp3 is enabled with DecryptSsl), the endpoint also listens for HTTP/3 on the same IP:port over UDP and injects Alt-Svc: h3=\":PORT\" into H1/H2 responses — reverse HTTPS with Alt-Svc advertisement. TransparentQuicProxyEndPoint A transparent proxy endpoint that listens on UDP/QUIC and intercepts HTTP/3 traffic. Clients are not aware of the proxy; traffic must be directed here via firewall/NAT redirection. QUIC always terminates TLS 1.3 at the proxy — there is no pass-through mode. An IOriginalDestinationResolver must be configured (or a fixed ForwardHost / ForwardPort fallback) so the proxy knows which origin server each connection is intended for. Platform requirement: IsSupported must be true (MsQuic native library present, OS version supported). If it is false, Start(bool) will throw PlatformNotSupportedException. ECH constraint: when managed DNS advertises ECH for intercepted names, the hidden SNI is encrypted and cannot be extracted here. Either disable ECH for intercepted names in your managed DNS, or configure managed clients to disable ECH. Experimental: HTTP/3 support has not yet completed the full interop/soak/fuzz gate process. Suppress TWP001 to opt in. WinAuthCredentials Windows authentication credentials for NTLM/Negotiate/Kerberos (issue #461). Prefer supplying these through WinAuthCredentialsProvider rather than storing plaintext on session event args. Structs HttpInterceptionContext Minimal, read-only context passed to ShouldInterceptHttp to let callers route requests to the fast-forward path or the full interception path without materialising a SessionEventArgs. Interfaces IExternalProxy Enums ExternalProxyType OriginHttpVersionPolicy Controls which HTTP version the proxy declares to the origin server on the request line, independently of the version the client itself declared on its own connection to the proxy. HTTP/1.0 and HTTP/1.1 share the same start-line/header/body wire format, so switching between them needs no message translation - only the declared version and the resulting default persistence (KeepAlive) change. The response is always written back to the client using the client's own originally declared version and its own persistence rules, regardless of this policy. ProxyAuthenticationResult ProxyProtocolType UpstreamHttpProtocol Controls which HTTP version the proxy uses on its own connection to the origin server, independent of which HTTP version the client used to talk to the proxy. Set a connection-level default on UpstreamHttpProtocol (during BeforeTunnelConnectRequest), UpstreamHttpProtocol (during BeforeSslAuthenticate), or UpstreamHttpProtocol (during BeforeQuicAuthenticate). Per-request overrides are available via UpstreamHttpProtocol in BeforeRequest." }, "api/Titanium.Web.Proxy.Network.CertificateEngine.html": { "href": "api/Titanium.Web.Proxy.Network.CertificateEngine.html", @@ -547,7 +552,7 @@ "api/Titanium.Web.Proxy.ProxyServer.html": { "href": "api/Titanium.Web.Proxy.ProxyServer.html", "title": "Class ProxyServer | Titanium Web Proxy", - "summary": "Class ProxyServer Translates an HTTP/1.1 client connection onto an h2-only origin (Http2 with AllowHttpProtocolTranslation enabled - see ResolveHttp2ForClientAsync(SessionEventArgsBase, bool, string, int, string, int?, UpstreamHttpProtocol, bool, bool, CancellationToken, bool)), leasing one h2 stream per HTTP/1.1 request from a shared Titanium.Web.Proxy.Http2.Http2OriginConnection via Titanium.Web.Proxy.ProxyServer.Http2OriginConnectionPool rather than opening a new TCP/TLS connection for every request. Inheritance object ProxyServer Implements IDisposable Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy Assembly: Titanium.Web.Proxy.dll Syntax public class ProxyServer : IDisposable Remarks This re-implements the HTTP/1.1 client read loop (request line, headers, BeforeRequest, authorization, header preparation, CancelRequest/replaced-response handling) rather than reusing the private HandleHttpSessionRequest/HandleHttpSessionResponse methods, because those methods send/receive over TcpServerConnection.Stream using the raw HTTP/1.1 wire format, which an h2 origin connection cannot speak. This mirrors the precedent set by the h2-to-HTTP/1.1 bridge (Http2ToHttp11BridgeHandler), which similarly bypasses the wire-format-specific machinery for the leg that does not match it. Origin connections are multiplexed across independent HTTP/1.1 clients through Titanium.Web.Proxy.ProxyServer.Http2OriginConnectionPool (fan-in share). Response bodies are delivered via Titanium.Web.Proxy.Http2.Http2OriginConnection streaming writers where available. Constructors | Edit this page View Source ProxyServer(bool, bool, bool) Initializes a new instance of ProxyServer class with provided parameters. Declaration public ProxyServer(bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false, bool trustRootCertificateAsAdmin = false) Parameters Type Name Description bool userTrustRootCertificate When true (the default), EnsureRootCertificate() installs the MITM root into the current-user Personal and Trusted Root stores. Prefer user-only trust for interactive apps; pass false when trust must be fully opt-in. bool machineTrustRootCertificate When true, also trust in the local-machine stores (needs elevation). Defaults to false — machine trust is opt-in for services/admin installs, not for normal desktop use. bool trustRootCertificateAsAdmin When true, attempt elevated trust via UAC (Windows only). Defaults to false. | Edit this page View Source ProxyServer(string?, string?, bool, bool, bool) Initializes a new instance of ProxyServer class with provided parameters. Declaration public ProxyServer(string? rootCertificateName, string? rootCertificateIssuerName, bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false, bool trustRootCertificateAsAdmin = false) Parameters Type Name Description string rootCertificateName Name of the root certificate. string rootCertificateIssuerName Name of the root certificate issuer. bool userTrustRootCertificate When true (the default), EnsureRootCertificate() installs the MITM root into the current-user Personal and Trusted Root stores. Prefer user-only trust for interactive apps; pass false when trust must be fully opt-in. bool machineTrustRootCertificate When true, also trust in the local-machine stores (needs elevation). Defaults to false — machine trust is opt-in for services/admin installs, not for normal desktop use. bool trustRootCertificateAsAdmin When true, attempt elevated trust via UAC (Windows only). Defaults to false. Fields | Edit this page View Source DefaultViaHeaderPseudonym Default Via header pseudonym (RFC 9110 §7.6.3). Used by ViaHeaderPseudonym and by Inspector when Add Via header is enabled. Declaration public const string DefaultViaHeaderPseudonym = \"titanium-web-proxy\" Field Value Type Description string Properties | Edit this page View Source AdmittedClientConnectionCount Number of client connections currently admitted (accepted and past the admission gate, not yet finished being handled), across all TCP-based endpoints. Unlike ClientConnectionCount, this drops to zero as soon as the handler returns, without the trailing TIME_WAIT delay. Declaration public int AdmittedClientConnectionCount { get; } Property Value Type Description int | Edit this page View Source BlockPrivateNetworkDestinations Outbound destination policy hook: when true, every resolved destination IP address is checked against loopback, private (RFC 1918/4193), link-local (which subsumes the 169.254.169.254 cloud metadata endpoint), and other non-globally-routable ranges before connecting, and the connection attempt is rejected with an OutboundDestinationBlockedException if it matches. Off by default: blocking private destinations would break this library's most common configurations, including upstream-proxy chaining to localhost and interception of local development servers. Only enable this when the proxy accepts requests from untrusted clients (an SSRF-relevant deployment), where those same destinations become an attacker-reachable pivot into the host's private network instead of an operator's own intentional configuration. An explicitly configured upstream proxy address (UpStreamHttpProxy, UpStreamHttpsProxy, or a per-session external proxy) is always exempt - that address is operator intent, not attacker-controlled. Checked against the resolved address actually used to connect (no re-resolution afterward, which would make the check a TOCTOU no-op against DNS rebinding). Not currently enforced for a SOCKS upstream with ProxyDnsRequests enabled, since the proxy never resolves the origin itself in that mode and has no address of its own to validate. Declaration public bool BlockPrivateNetworkDestinations { get; set; } Property Value Type Description bool | Edit this page View Source BufferPool The buffer pool used throughout this proxy instance. Set custom implementations by implementing this interface. By default this uses DefaultBufferPool implementation available in StreamExtended library package. Buffer size should be at least 10 bytes. Declaration public IBufferPool BufferPool { get; set; } Property Value Type Description IBufferPool | Edit this page View Source CertificateManager Manages certificates used by this proxy. Declaration public CertificateManager CertificateManager { get; } Property Value Type Description CertificateManager | Edit this page View Source CheckCertificateRevocation Should we check for certificate revocation during SSL authentication to servers Note: If enabled can reduce performance. Defaults to false. Declaration public X509RevocationMode CheckCertificateRevocation { get; set; } Property Value Type Description X509RevocationMode | Edit this page View Source ClientConnectionCount Total number of active TCP client connections. Does not include inbound HTTP/3 (QUIC) clients; see Http3ClientConnectionCount. Declaration public int ClientConnectionCount { get; } Property Value Type Description int | Edit this page View Source ClientHeaderTimeoutSeconds Seconds to wait for a client to finish sending the request line and headers, from the moment this proxy starts reading a new request on the connection. Enforced with a linked CancellationTokenSource around the request-line and header read, not Socket.ReceiveTimeout: that property only bounds a single blocking Receive call, not the asynchronous reads this proxy actually issues, so without this deadline a client that opens a connection and trickles bytes arbitrarily slowly (or stops sending entirely) after the first byte ties up a read loop indefinitely. Default is 0 (disabled), matching every other deadline in this class - no per-session override exists because there is no SessionEventArgs for this request yet at the point this deadline applies. Declaration public int ClientHeaderTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source CompatibilityMode100Continue When true, the proxy immediately responds with a synthetic 100 Continue to any client request carrying Expect: 100-continue, before forwarding the headers to the origin and without waiting for the origin to respond. This breaks the strict handshake (client → proxy 100 → client body → origin body) but prevents the deadlock that occurs with strict clients when Enable100ContinueBehaviour is false (the default). Has no effect when Enable100ContinueBehaviour is true. Default: false. Declaration public bool CompatibilityMode100Continue { get; set; } Property Value Type Description bool | Edit this page View Source ConnectTimeOutSeconds Seconds server connection are to wait for connection to be established. Default value is 20 seconds. Declaration public int ConnectTimeOutSeconds { get; set; } Property Value Type Description int | Edit this page View Source ConnectionTimeOutSeconds Seconds client/server connection are to be kept alive when waiting for read/write to complete. This will also determine the pool eviction time when connection pool is enabled. Default value is 60 seconds. Declaration public int ConnectionTimeOutSeconds { get; set; } Property Value Type Description int | Edit this page View Source CustomUpStreamProxyFailureFunc A callback to provide a chance for an upstream proxy failure to be handled by a new upstream proxy. User should return the ExternalProxy object with valid credentials or null. Declaration public Func>? CustomUpStreamProxyFailureFunc { get; set; } Property Value Type Description Func> | Edit this page View Source DnsServerEndPoint DNS server endpoint used by Titanium.Web.Proxy.Http3.Dns.UdpSvcbDnsResolver for HTTPS/SVCB queries. Defaults to the first usable OS-configured plain-UDP DNS server discovered via NetworkInterface. This is a best-effort default and does not honor Windows NRPT, DoH, or VPN split-DNS policy. When no OS-configured DNS server can be discovered, the property reports 0.0.0.0:0 and proactive SVCB discovery is skipped (never falls back to a public third-party resolver). Assign an explicit endpoint to override discovery. Declaration [Experimental(\"TWP001\")] public IPEndPoint DnsServerEndPoint { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source Enable100ContinueBehaviour Does this proxy uses the HTTP protocol 100 continue behaviour strictly? Broken 100 continue implementations on server/client may cause problems if enabled. Defaults to false. Declaration public bool Enable100ContinueBehaviour { get; set; } Property Value Type Description bool | Edit this page View Source EnableConnectionPool Should we enable the server connection pool. Defaults to true. When connection pooling is enabled, instead of creating a new TCP connection to the server for each client TCP connection, we check if an idle server connection is available in our cached pool. If a compatible connection (same destination, scheme, upstream proxy, credentials and negotiated protocol) created from an earlier request is available, we reuse it. Only connections that are safe to reuse under the HTTP protocol are pooled: the response body must be fully received and the connection must be persistent (HTTP/1.1 keep-alive, or an HTTP/1.0 connection that explicitly opted in via \"Connection: keep-alive\"). Connections whose response asked to close, that failed, or that carry connection-oriented authentication state (WinAuth NTLM/Negotiate) or a per-session client certificate are never returned to the shared pool. The ConnectionTimeOutSeconds parameter determines the eviction time for inactive server connections. This reduces TCP (and TLS) connection establishment cost, both in wall clock time and CPU cycles. Set to false to force a fresh server connection for every client connection. Declaration public bool EnableConnectionPool { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttp2 Enable disable HTTP/2 support. Client-facing HTTP/2 is negotiated via TLS ALPN, or as prior-knowledge cleartext h2c on a transparent reverse endpoint (DecryptSsl: false). No Upgrade: h2c. Origin-facing HTTP/2 uses TLS ALPN h2 by default; with ForwardCleartext and Http2, the origin speaks cleartext HTTP/2 prior-knowledge (outbound h2c). A client/server that does not support HTTP/2 transparently falls back to HTTP/1.1 when policy allows. Request/response header and body modification in BeforeRequest/BeforeResponse, chunked trailers, interim (1xx) responses, and the synthetic-response APIs (Ok/Respond/Redirect/GenericResponse/ RespondStreaming) are all supported over HTTP/2, the same as over HTTP/1.x. Not supported: HTTP/2 server push (the wire frames are transcoded but there is no public API to originate a push) and Upgrade: h2c. Explicit-proxy inbound h2c is not implemented. See the protocol support matrix on the wiki for exact, up-to-date HTTP/1.x/HTTP/2 feature coverage. Declaration public bool EnableHttp2 { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttp3 Enable HTTP/3 (QUIC) support. When true: Any TransparentQuicProxyEndPoint is started as a UDP-only QUIC listener for transparent/NAT HTTP/3 interception. Any TransparentProxyEndPoint with EnableHttp3 also listens for HTTP/3 on the same IP:port (TCP H1/H2 + UDP H3) and injects client-facing Alt-Svc. With Auto (default), a cached Alt-Svc / HTTPS/SVCB capability only arms background QUIC warm-up. Outbound HTTP/3 is used once that origin is warm; until then the request stays on HTTP/2 or HTTP/1.1. Forced Http3 skips warm-up gating and fails closed with no TCP fallback. Requires MsQuic native library and a supported operating-system version (IsSupported). Setting to true with no inbound HTTP/3 endpoint is fine when an explicit/SOCKS/transparent TCP endpoint is present (origin-side QUIC only). A warning is emitted only when EnableHttp3 is set with no client-facing endpoints at all. Default: false (opt-in). Experimental: HTTP/3 support has not yet completed the full interop/soak/fuzz gate process. Suppress TWP001 to opt in; the attribute is removed when the feature graduates to stable. Declaration [Experimental(\"TWP001\")] public bool EnableHttp3 { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttpInterception Forces the full interception path (SessionEventArgs, BeforeRequest, etc.) even when no event handlers are subscribed. Set this when consuming SessionEventArgs for timing or metrics without subscribing to any event. Default: false. Declaration public bool EnableHttpInterception { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttpsSvcbDnsDiscovery When true, the proxy queues a background HTTPS/SVCB RR (DNS type 65) lookup after an Auto-mode capability-cache miss. A positive result (ALPN h3 found) warms Titanium.Web.Proxy.Http3.Http3OriginCapabilityCache for subsequent connections; the CONNECT / request path itself never awaits DNS. Negative results are cached for 1 minute; transient failures use a short backoff. Defaults to true whenever EnableHttp3 is true. Set explicitly to false to disable discovery even when HTTP/3 is enabled — for example, when the configured DNS server is untrusted or unreachable. First-connection HTTP/3 adoption then comes from Alt-Svc. Declaration [Experimental(\"TWP001\")] public bool EnableHttpsSvcbDnsDiscovery { get; set; } Property Value Type Description bool | Edit this page View Source EnableIpv6UnreachableSoftSkip When true (default), after one IPv6 connect failure with NetworkUnreachable (or equivalent), temporarily omit IPv6 addresses from the Happy Eyeballs race for 30 seconds. Reduces first-chance SocketException noise on dual-stack hosts with a broken IPv6 path. Disable if operators require strict IPv6 preference even when the path is unreachable. Declaration public bool EnableIpv6UnreachableSoftSkip { get; set; } Property Value Type Description bool | Edit this page View Source EnableQpackDynamicTable When true, enables RFC 9204 QPACK dynamic table encoding and decoding for inbound HTTP/3 connections. Each connection gets its own Titanium.Web.Proxy.Http3.Qpack.QpackContext with two independent 4096-byte tables (one inbound, one outbound). Defaults to false (static-table-only); existing deployments are unaffected. Declaration [Experimental(\"TWP001\")] public bool EnableQpackDynamicTable { get; set; } Property Value Type Description bool | Edit this page View Source EnableRequestTimingCapture Enables structured request/connection timing capture. When false (the default) no timing objects are allocated and no UtcNow calls are made for timing purposes anywhere in the proxy, so there is zero overhead on the hot path. When enabled, every SessionEventArgsBase exposes a populated Timing (per-request phases: client header read, connection wait, request send, time-to-first-byte, response delivery, total), every upstream connection exposes a populated UpstreamConnectionTiming (reachable from a session via UpstreamConnectionTiming, describing DNS, TCP connect, optional upstream-proxy CONNECT, and TLS handshake durations), and a decrypted TunnelConnectSessionEventArgs exposes the client-facing TLS handshake duration via ClientTlsTiming. Can be toggled at any time; it only affects sessions/connections created after the change, never mutating timing objects already handed out. Defaults to false. Declaration public bool EnableRequestTimingCapture { get; set; } Property Value Type Description bool | Edit this page View Source EnableRfc8441 When true, the proxy enables RFC 8441 WebSocket-over-HTTP/2: Accepts extended CONNECT (:protocol = websocket) from h2 clients and advertises SETTINGS_ENABLE_CONNECT_PROTOCOL=1 to them. Per origin: if the origin is HTTP/2 and advertises RFC 8441 support, DATA frames are relayed directly; if the origin is HTTP/2 and does not, the stream is reset with REFUSED_STREAM; if the origin is HTTP/1.1, the h2→h1 WebSocket upgrade bridge is used. On the HTTP/1.1-client-to-h2-origin translation bridge, translates Upgrade: websocket into extended CONNECT when the origin advertises the setting; otherwise falls back to a dedicated HTTP/1.1 origin connection for that WebSocket. When this property is false, that bridge still returns synthetic 501 Not Implemented for WebSocket upgrades (historical default). Default: false (must opt-in). Declaration public bool EnableRfc8441 { get; set; } Property Value Type Description bool | Edit this page View Source EnableTcpKeepAlive Enable TCP KeepAlive on client and server sockets so NAT/firewall mappings for long-lived CONNECT tunnels are refreshed. Default: true. Declaration public bool EnableTcpKeepAlive { get; set; } Property Value Type Description bool | Edit this page View Source EnableTcpServerConnectionPrefetch Should we enable tcp server connection prefetching? When enabled, as soon as we receive a client connection we concurrently initiate corresponding server connection process using CONNECT hostname or SNI hostname on a separate task so that after parsing client request we will have the server connection immediately ready or in the process of getting ready. If a server connection is available in cache then this prefetch task will immediately return with the available connection from cache. Defaults to true. Declaration public bool EnableTcpServerConnectionPrefetch { get; set; } Property Value Type Description bool | Edit this page View Source EnableWinAuth Enable disable Windows Authentication (NTLM/Kerberos). By default SSPI uses the process identity. To authenticate as another user, set WinAuthCredentialsProvider (issue #461). Defaults to false. Declaration public bool EnableWinAuth { get; set; } Property Value Type Description bool | Edit this page View Source EndpointAdmissionRejectionCount Total number of client connections rejected by any endpoint's MaxConcurrentClients since this instance was created. Declaration public long EndpointAdmissionRejectionCount { get; } Property Value Type Description long | Edit this page View Source ForwardToUpstreamGateway Gets or sets a value indicating whether requests will be chained to upstream gateway. Defaults to false. Declaration public bool ForwardToUpstreamGateway { get; set; } Property Value Type Description bool | Edit this page View Source GetCustomUpStreamProxyFunc A callback to provide authentication credentials for up stream proxy this proxy is using for HTTP(S) requests. User should return the ExternalProxy object with valid credentials. Declaration public Func>? GetCustomUpStreamProxyFunc { get; set; } Property Value Type Description Func> | Edit this page View Source GlobalAdmissionRejectionCount Total number of client connections rejected by MaxConcurrentClientConnections since this instance was created. Declaration public long GlobalAdmissionRejectionCount { get; } Property Value Type Description long | Edit this page View Source Http3ClientConnectionCount Total number of active inbound HTTP/3 (QUIC) client connections. Declaration public int Http3ClientConnectionCount { get; } Property Value Type Description int | Edit this page View Source Http3ServerConnectionCount Total number of active upstream HTTP/3 (QUIC) server connections. These are also included in ServerConnectionCount. Declaration public int Http3ServerConnectionCount { get; } Property Value Type Description int | Edit this page View Source IdleReadTimeoutSeconds Seconds of idle time allowed while reading from the origin (stalled header/body waits). Applied via CancelAfter on the active read operation. Default is 0 (disabled). Per-session override: IdleReadTimeout. Declaration public int IdleReadTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source IdleWriteTimeoutSeconds Seconds of idle time allowed while writing to the origin (stalled header/body waits). Applied via CancelAfter on the active write operation. Default is 0 (disabled). Per-session override: IdleWriteTimeout. Declaration public int IdleWriteTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source IgnoreServerCertificateErrors When true, origin TLS certificates that fail OS chain validation are still accepted (MITM of loopback/self-signed/private CAs). Inspector's \"Ignore server certificate errors\" maps here. Default false. A subscribed ServerCertificateValidationCallback still wins. Declaration public bool IgnoreServerCertificateErrors { get; set; } Property Value Type Description bool | Edit this page View Source ListenerBackLog TCP listener accept backlog. Default: 1024 for burst connection handling. Declaration public int ListenerBackLog { get; set; } Property Value Type Description int | Edit this page View Source Logger The live, shared logger used throughout this proxy instance. Reflects the most recent call to ApplyLoggingConfiguration(). Declaration public ILogger Logger { get; } Property Value Type Description ILogger | Edit this page View Source Logging Configuration for this proxy instance's built-in diagnostic logging - the replacement for the removed ExceptionFunc callback. Every exception the proxy catches (even when handled internally and never surfaced to user code) is reported through this logger at an appropriate severity; see ProxyLoggingOptions for the console/file sinks, enable/disable switch, and minimum level. Mutate the returned instance (or assign a new one) at any point; each assignment/mutation you want to take effect must be followed by ApplyLoggingConfiguration() (which Start(bool) also calls automatically, so the configuration active at the moment the proxy starts running is picked up for the run even if you never call it yourself). Calling it again later - including while the proxy is already running - immediately swaps in the new configuration; this is safe because logging never blocks or otherwise affects proxy traffic. Declaration public ProxyLoggingOptions Logging { get; set; } Property Value Type Description ProxyLoggingOptions | Edit this page View Source MaxBufferedBodyBytes Maximum bytes the proxy will buffer for a single request or response body when body buffering is required (body-read hooks, authentication retry, etc.). Bodies larger than this limit are rejected with 413 (upstream request) or connection teardown (upstream response). Set to 0 to disable the limit (not recommended). Default: 4,194,304 (4 MiB). Declaration public int MaxBufferedBodyBytes { get; set; } Property Value Type Description int | Edit this page View Source MaxCachedConnections Maximum number of concurrent connections per remote host in cache. Only meaningful when EnableConnectionPool is true; to disable pooling, set EnableConnectionPool to false rather than setting this to 0 - the pool eviction loop treats a value below 1 as \"evict without limit while holding the pool-wide lock\", which spins indefinitely once the cache for that host is empty and would stall every other connection acquire/release in the process. Rejected outright at assignment so that state cannot be reached. Default value is 128. Declaration public int MaxCachedConnections { get; set; } Property Value Type Description int Exceptions Type Condition ArgumentOutOfRangeException The assigned value is less than 1. | Edit this page View Source MaxConcurrentClientConnections Maximum number of client connections admitted across all TCP-based endpoints at once. null (the default) disables the global admission gate, preserving today's unbounded behavior. When set, a connection beyond this limit is rejected and disposed immediately after accept, before a handler task is even started. Enforced independently of ClientConnectionCount: see Titanium.Web.Proxy.ProxyServer.admittedClientConnectionCount for why. See also MaxConcurrentClients for a per-endpoint cap layered on top of this global one. Declaration public int? MaxConcurrentClientConnections { get; set; } Property Value Type Description int? | Edit this page View Source MaxConcurrentHttp11HttpsOriginCreates Caps concurrent new HTTPS origin TCP/TLS opens on the H2→H1 bridge only (MITM / re-encrypt). Pool hits (warm keep-alive) are uncapped. Cleartext H1 origins are not gated. Default is Clamp(ProcessorCount, 4, 32). Set before the first H2→H1 HTTPS origin open (typically before Start(bool)); changing the value after the create gate has been used has no effect on the live semaphore. Declaration public int MaxConcurrentHttp11HttpsOriginCreates { get; set; } Property Value Type Description int Exceptions Type Condition ArgumentOutOfRangeException The assigned value is less than 1. | Edit this page View Source MaxDecodedHeaderListBytes Maximum decoded HTTP/2 header list size in bytes, using RFC 7541 accounting (name.Length + value.Length + 32 per field). Requests or responses with a decoded header list exceeding this limit will be refused with RST_STREAM(ENHANCE_YOUR_CALM) (code 0xb). Set to 0 to disable the limit (not recommended). Default: 65,536 (64 KiB). Advertised via SETTINGS_MAX_HEADER_LIST_SIZE. Declaration public int MaxDecodedHeaderListBytes { get; set; } Property Value Type Description int | Edit this page View Source MaxWebSocketFramePayloadBytes Maximum WebSocket frame payload size in bytes that the proxy will accept during frame-level interception (i.e. when BeforeWebSocketFrame has at least one subscriber). Frames whose decoded payload exceeds this limit cause the WebSocket connection to be closed with Close code 1009 (Message Too Big). Raw-relay sessions (no BeforeWebSocketFrame subscriber) bypass this check entirely and pass all frames through unvalidated. Default: 16,777,216 (16 MiB). Declaration public int MaxWebSocketFramePayloadBytes { get; set; } Property Value Type Description int | Edit this page View Source NetworkFailureRetryAttempts Number of times to retry upon network failures when connection pool is enabled. Declaration public int NetworkFailureRetryAttempts { get; set; } Property Value Type Description int | Edit this page View Source NoDelay Gets or sets a Boolean value that specifies whether server and client stream Sockets are using the Nagle algorithm. Defaults to true, no nagle algorithm is used. Declaration public bool NoDelay { get; set; } Property Value Type Description bool | Edit this page View Source OriginHttpVersionPolicy Controls which HTTP version is declared to the origin server on the request line, independently of the version the client declared to the proxy. Defaults to PreserveClientVersion, which matches the proxy's historical pass-through behavior exactly. Set to NormalizeToHttp11 to let HTTP/1.0 clients share pooled, persistent origin connections the same way HTTP/1.1 clients already do. This only changes the wire version written to the origin request line - it never changes the client-facing Http.Request.HttpVersion that event handlers observe, nor the version/persistence used to write the response back to the client. Declaration public OriginHttpVersionPolicy OriginHttpVersionPolicy { get; set; } Property Value Type Description OriginHttpVersionPolicy | Edit this page View Source PolicyModes Which resource-bound PolicyFamily is enforced, observed, or disabled, per the plan's rollout section. Read live by each family's enforcement call site - not baked into a per-request snapshot at connection accept time - so assigning a new value here (a whole-object replacement, never a mutation of the previous instance) takes effect for the next check any in-flight or new request makes, without restarting the proxy. This is the \"runtime switch to drop to Observe without redeploying\" the plan requires; see WithAllObservedExceptDisabled() for the one-call way to do that. Defaults to AllEnforce, matching Balanced. Assigning Profile also replaces this value with that profile's bundle; assign PolicyModes afterward to deviate from the selected profile's modes without changing anything else the profile set. Declaration public ProxyPolicyModes PolicyModes { get; set; } Property Value Type Description ProxyPolicyModes | Edit this page View Source Profile The last profile applied via this property's setter, defaulting to Balanced - the profile every field on this instance already starts at, so a fresh new ProxyServer() reports Balanced without needing its setter to run once at construction time. Assigning this property applies its entire ProxyProfileSettings bundle - ResourceLimits, PolicyModes, SupportedSslProtocols, BlockPrivateNetworkDestinations, MaxConcurrentClientConnections and the deadline-seconds properties - as a single atomic assignment, so a reader can never observe a half-applied profile. Assigning any of those properties individually afterward overrides just that one, without reverting the rest of the profile's bundle. Logged once per Start(bool) call, by name only - never with hosts, URLs or secrets, per the plan's rollout section. Declaration public ProxyProfile Profile { get; set; } Property Value Type Description ProxyProfile | Edit this page View Source ProxyAuthenticationRealm Realm used during Proxy Basic Authentication. Declaration public string ProxyAuthenticationRealm { get; set; } Property Value Type Description string | Edit this page View Source ProxyAuthenticationSchemes A collection of scheme types, e.g. basic, NTLM, Kerberos, Negotiate, to return if scheme authentication is required. Works in relation with ProxySchemeAuthenticateFunc. Declaration public IEnumerable ProxyAuthenticationSchemes { get; set; } Property Value Type Description IEnumerable | Edit this page View Source ProxyBasicAuthenticateFunc A callback to authenticate proxy clients via basic authentication. Parameters are username and password as provided by client. Should return true for successful authentication. Declaration public Func>? ProxyBasicAuthenticateFunc { get; set; } Property Value Type Description Func> | Edit this page View Source ProxyEndPoints A list of IpAddress and port this proxy is listening to. Declaration public List ProxyEndPoints { get; set; } Property Value Type Description List | Edit this page View Source ProxyRunning Is the proxy currently running? Declaration public bool ProxyRunning { get; } Property Value Type Description bool | Edit this page View Source ProxySchemeAuthenticateFunc A pluggable callback to authenticate clients by scheme instead of requiring basic authentication through ProxyBasicAuthenticateFunc. Parameters are current working session, schemeType, and token as provided by a calling client. Should return success for successful authentication, continuation if the package requests, or failure. Declaration public Func>? ProxySchemeAuthenticateFunc { get; set; } Property Value Type Description Func> | Edit this page View Source RequestTimeoutSeconds Total seconds allowed for a single request/response exchange after BeforeRequest returns (connect, send, wait for headers, and body copy). Default is 0 (disabled). Per-session override: RequestTimeout. Declaration public int RequestTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source ResourceLimits The shared, immutable resource-bound snapshot (concurrent-stream cap, CONTINUATION frame-count/wall-clock bounds, peer-initiated incomplete-stream-reset budget, and the other limits described in ProxyResourceLimits) consulted by the HTTP/2 relay so a single proxy-owned value governs both what is enforced and what is advertised to each peer, rather than admitting purely against whatever the origin advertised. Assign a new ProxyResourceLimits (constructed via Create(long, int, long, long?, long?, double?, int?, int, int?, int, TimeSpan, bool, int, int?)) to override the Default snapshot. There is no artificial upper clamp: high-CPU/RAM hosts may pass larger maxCachedConnectionsPerHost, maxConcurrentStreamsPerConnection, etc. as needed. The live TCP pool depth knob MaxCachedConnections remains independently settable and should usually be kept in sync with MaxCachedConnectionsPerHost. Declaration public ProxyResourceLimits ResourceLimits { get; set; } Property Value Type Description ProxyResourceLimits | Edit this page View Source ResponseHeaderTimeoutSeconds Seconds to wait for the origin to send the response status line and headers after the request has been sent. Enforced with a linked CancellationTokenSource (not Socket receive timeout alone). When the deadline elapses a ProxyTimeoutException with ResponseHeader is raised (and may be converted to HTTP 504 before any response bytes have been committed to the client). Default is 0 (disabled). WebSocket upgrades, Server-Sent Events, raw tunnels, and sessions that already wrote a response status to the client are exempt; those waits use IdleReadTimeoutSeconds when configured. Per-session override: ResponseHeaderTimeout. Declaration public int ResponseHeaderTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source ReuseSocket When true (default), SO_REUSEADDR is requested where Titanium.Web.Proxy.Helpers.RunTime.IsSocketReuseAvailable() reports support (always on Windows; on non-Windows, .NET Core 3+ / compatible runtimes). Declaration public bool ReuseSocket { get; set; } Property Value Type Description bool | Edit this page View Source ReverseProxy Optional reverse-proxy route/cluster configuration. When null (default), Core keeps 6.x ForwardHost behavior with zero added cost on the hot path. Declaration public ReverseProxyOptions? ReverseProxy { get; set; } Property Value Type Description ReverseProxyOptions | Edit this page View Source ServerConnectionCount Total number of active server connections (TCP plus upstream QUIC). For HTTP/3-only upstreams see Http3ServerConnectionCount. Declaration public int ServerConnectionCount { get; } Property Value Type Description int | Edit this page View Source ShouldInterceptHttp Optional per-request/stream predicate consulted only when the global interception gate is active. Return true to use the full SessionEventArgs path; return false to use the fast-forward path. null (the default) intercepts every request — preserving today's behavior. Declaration public Func? ShouldInterceptHttp { get; set; } Property Value Type Description Func | Edit this page View Source SupportedServerSslProtocols Ssl versions offered on outbound HTTPS connections to origins (and upstream proxies). Default None means “use SupportedSslProtocols” (typically TLS 1.2 and 1.3). Set an explicit mask to restrict or expand outbound-only independently of inbound client TLS. Older docs described None as “same as the proxy client.” That coupling is incorrect across protocol translations (e.g. inbound QUIC is always TLS 1.3 while outbound TCP SslStream on macOS SecureTransport cannot offer TLS 1.3). Declaration public SslProtocols SupportedServerSslProtocols { get; set; } Property Value Type Description SslProtocols | Edit this page View Source SupportedSslProtocols List of supported Ssl versions. Defaults to TLS 1.2/1.3 only as of 5.0 - a breaking change from 4.x, which also enabled SSL 3.0/TLS 1.0/1.1. Those legacy, broken-by-design protocols require an explicit opt-in by assigning this property directly (e.g. SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13) if a legacy client/server genuinely requires them. Declaration public SslProtocols SupportedSslProtocols { get; set; } Property Value Type Description SslProtocols | Edit this page View Source TcpTimeWaitSeconds SO_LINGER timeout in seconds applied to client and upstream sockets via LingerOption (enabled with this timeout). This is not the kernel TCP TIME_WAIT duration — TIME_WAIT is controlled by the OS. A positive value means Close may block up to that many seconds flushing send buffers; use 0 for an abortive close (RST). Default is 0 so high-churn proxies avoid TIME_WAIT accumulation; the 1-second connection disposal delay already prefers peer-first close. Declaration public int TcpTimeWaitSeconds { get; set; } Property Value Type Description int | Edit this page View Source ThreadPoolWorkerThread Customize the minimum ThreadPool size (increase it on a server). Defaults to max(ProcessorCount * 2, 16) so short loopback/proxy workloads are not starved while the pool is still ramping workers. Declaration public int ThreadPoolWorkerThread { get; set; } Property Value Type Description int | Edit this page View Source UpStreamEndPoint Local adapter/NIC endpoint where proxy makes request via. Defaults via any IP addresses of this machine. When the resolved destination address family does not match this endpoint, it is ignored so dual-stack destinations can still connect (see UpStreamEndPointIPv4 / UpStreamEndPointIPv6). Declaration public IPEndPoint? UpStreamEndPoint { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source UpStreamEndPointIPv4 Local bind endpoint used when the resolved upstream destination is IPv4. Takes precedence over UpStreamEndPoint for IPv4 destinations. Declaration public IPEndPoint? UpStreamEndPointIPv4 { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source UpStreamEndPointIPv6 Local bind endpoint used when the resolved upstream destination is IPv6. Takes precedence over UpStreamEndPoint for IPv6 destinations. Declaration public IPEndPoint? UpStreamEndPointIPv6 { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source UpStreamHttpProxy External proxy used for Http requests. Declaration public IExternalProxy? UpStreamHttpProxy { get; set; } Property Value Type Description IExternalProxy | Edit this page View Source UpStreamHttpsProxy External proxy used for Https requests. Declaration public IExternalProxy? UpStreamHttpsProxy { get; set; } Property Value Type Description IExternalProxy | Edit this page View Source UpstreamProxyConfigurationScript If set, the upstream proxy will be detected by a script that will be loaded from the provided Uri Declaration public Uri? UpstreamProxyConfigurationScript { get; set; } Property Value Type Description Uri | Edit this page View Source ViaHeaderPseudonym Pseudonym used in Via header fields appended to forwarded requests and responses (RFC 9110 §7.6.3). Defaults to DefaultViaHeaderPseudonym. Set to an empty string to disable Via header injection entirely. Loop detection uses this value: a request arriving with this pseudonym already present in Via is refused with 508 Loop Detected. Declaration public string ViaHeaderPseudonym { get; set; } Property Value Type Description string | Edit this page View Source WinAuthCredentialsProvider Optional per-session credential provider for server 401 WinAuth (NTLM/Negotiate/Kerberos). Return null to use the current process identity (legacy behavior). Do not put plaintext passwords on SessionEventArgs — use this callback instead. Windows SSPI only; ignored on non-Windows platforms. Declaration public Func>? WinAuthCredentialsProvider { get; set; } Property Value Type Description Func> Methods | Edit this page View Source AddEndPoint(ProxyEndPoint) Add a proxy end point. Declaration public void AddEndPoint(ProxyEndPoint endPoint) Parameters Type Name Description ProxyEndPoint endPoint The proxy endpoint. | Edit this page View Source ApplyLoggingConfiguration() Rebuilds the active logger/logger factory from the current Logging configuration, disposing any previously owned built-in providers. Called automatically from the constructor (with the default configuration) and from Start(bool). Call this explicitly any time after changing Logging and you want the change to take effect immediately - whether the proxy is stopped (e.g. before using CertificateManager directly) or already running. Declaration public void ApplyLoggingConfiguration() | Edit this page View Source DisableAllSystemProxies() Clear all proxy settings for current machine. Declaration public void DisableAllSystemProxies() | Edit this page View Source DisableSystemHttpProxy() Clear HTTP proxy settings of current machine. Declaration public void DisableSystemHttpProxy() | Edit this page View Source DisableSystemHttpsProxy() Clear HTTPS proxy settings of current machine. Declaration public void DisableSystemHttpsProxy() | Edit this page View Source DisableSystemProxy(ProxyProtocolType) Clear the specified proxy setting for current machine. Declaration public void DisableSystemProxy(ProxyProtocolType protocolType) Parameters Type Name Description ProxyProtocolType protocolType | Edit this page View Source Dispose() Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. Declaration public void Dispose() | Edit this page View Source Dispose(bool) Declaration [SuppressMessage(\"ApiDesign\", \"RS0016:Add public types and members to the declared API\", Justification = \"Protected Dispose(bool) is required by the standard IDisposable pattern but is not public API.\")] protected virtual void Dispose(bool disposing) Parameters Type Name Description bool disposing | Edit this page View Source RemoveEndPoint(ProxyEndPoint) Remove a proxy end point. Will throw error if the end point doesn't exist. Declaration public void RemoveEndPoint(ProxyEndPoint endPoint) Parameters Type Name Description ProxyEndPoint endPoint The existing endpoint to remove. | Edit this page View Source RestoreOriginalProxySettings() Restores the original proxy settings. Declaration public void RestoreOriginalProxySettings() | Edit this page View Source SetAsSystemHttpProxy(ExplicitProxyEndPoint) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemHttpProxy(ExplicitProxyEndPoint endPoint) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. | Edit this page View Source SetAsSystemHttpProxy(ExplicitProxyEndPoint, SystemProxySettings) Set the given explicit end point as the default HTTP proxy server for current machine. Declaration public void SetAsSystemHttpProxy(ExplicitProxyEndPoint endPoint, SystemProxySettings settings) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. SystemProxySettings settings The Windows system proxy settings. | Edit this page View Source SetAsSystemHttpsProxy(ExplicitProxyEndPoint) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemHttpsProxy(ExplicitProxyEndPoint endPoint) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. | Edit this page View Source SetAsSystemHttpsProxy(ExplicitProxyEndPoint, SystemProxySettings) Set the given explicit end point as the default HTTPS proxy server for current machine. Declaration public void SetAsSystemHttpsProxy(ExplicitProxyEndPoint endPoint, SystemProxySettings settings) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. SystemProxySettings settings The Windows system proxy settings. | Edit this page View Source SetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. ProxyProtocolType protocolType The proxy protocol type. | Edit this page View Source SetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType, SystemProxySettings?) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType, SystemProxySettings? settings) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. ProxyProtocolType protocolType The proxy protocol type. SystemProxySettings settings The Windows system proxy settings, or null to preserve the current bypass list. | Edit this page View Source SetHttp3Enabled(bool) Enables or disables EnableHttp3. Enabling still requires MsQuic (IsSupported); disabling is always applied. Safe to call while the proxy is running — new origin connections pick up the change. Existing sessions keep the protocol they already negotiated. Declaration public bool SetHttp3Enabled(bool enabled) Parameters Type Name Description bool enabled Returns Type Description bool true when HTTP/3 is enabled after the call. | Edit this page View Source Start(bool) Start this proxy server instance. Transactional: if any endpoint fails to start, every listener this call already started is stopped, the system-upstream-proxy resolver (if this call created one) is disposed, and ProxyRunning is left false before the exception propagates. A caller that catches the exception is left with an instance in exactly the same state as before calling Start(bool), not a partially-bound proxy with some endpoints silently listening. Declaration public void Start(bool changeSystemProxySettings = true) Parameters Type Name Description bool changeSystemProxySettings Whether or not clear any system proxy settings which is pointing to our own endpoint (causing a cycle). E.g due to ungracious proxy shutdown before. | Edit this page View Source Stop() Stop this proxy server instance. Endpoints remain registered so Start(bool) can re-listen on the same ports. In-flight sessions are cancelled; pooled upstream connections are cleared. The connection factory itself stays usable for a subsequent Start (it is only disposed with the proxy). Declaration public void Stop() | Edit this page View Source StopAsync(TimeSpan?) Asynchronously stop this proxy server, cancel in-flight sessions, and wait briefly for client connection count to drain before clearing the upstream pool. Declaration public Task StopAsync(TimeSpan? drainTimeout = null) Parameters Type Name Description TimeSpan? drainTimeout Maximum time to wait for active client handlers to exit after cancellation. Defaults to 5 seconds. Returns Type Description Task | Edit this page View Source TryDisableAllSystemProxies() Clear all OS proxy settings without throwing. Declaration public SystemProxyChangeResult TryDisableAllSystemProxies() Returns Type Description SystemProxyChangeResult | Edit this page View Source TryDisableSystemProxy(ProxyProtocolType) Clear OS proxy for the given protocols without throwing. Declaration public SystemProxyChangeResult TryDisableSystemProxy(ProxyProtocolType protocolType) Parameters Type Name Description ProxyProtocolType protocolType Returns Type Description SystemProxyChangeResult | Edit this page View Source TryEnableHttp3IfSupported() Turns on EnableHttp3 when MsQuic is available (IsSupported). Hosts (CLI, Inspector, examples) should call this instead of setting EnableHttp3 blindly. Returns true when HTTP/3 was enabled. Declaration public bool TryEnableHttp3IfSupported() Returns Type Description bool | Edit this page View Source TryRestoreOriginalProxySettings() Restore OS proxy without throwing. Declaration public SystemProxyChangeResult TryRestoreOriginalProxySettings() Returns Type Description SystemProxyChangeResult | Edit this page View Source TrySetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType, SystemProxySettings?) Enable OS system proxy without throwing. Failures are logged and returned so Inspector/CLI can show a status message instead of crashing. Declaration public SystemProxyChangeResult TrySetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType, SystemProxySettings? settings = null) Parameters Type Name Description ExplicitProxyEndPoint endPoint ProxyProtocolType protocolType SystemProxySettings settings Returns Type Description SystemProxyChangeResult Events | Edit this page View Source AfterResponse Intercept after response event from server. Declaration public event AsyncEventHandler? AfterResponse Event Type Type Description AsyncEventHandler | Edit this page View Source BeforeRequest Intercept request event to server. Declaration public event AsyncEventHandler? BeforeRequest Event Type Type Description AsyncEventHandler | Edit this page View Source BeforeResponse Intercept response event from server. Declaration public event AsyncEventHandler? BeforeResponse Event Type Type Description AsyncEventHandler | Edit this page View Source BeforeUpStreamConnectRequest Intercept connect request sent to upstream proxy. Declaration public event AsyncEventHandler? BeforeUpStreamConnectRequest Event Type Type Description AsyncEventHandler | Edit this page View Source ClientCertificateSelectionCallback Event to override client certificate selection during mutual SSL authentication. Declaration public event AsyncEventHandler? ClientCertificateSelectionCallback Event Type Type Description AsyncEventHandler | Edit this page View Source ClientConnectionCountChanged Event occurs when client connection count changed. Declaration public event EventHandler? ClientConnectionCountChanged Event Type Type Description EventHandler | Edit this page View Source Http3ClientConnectionCountChanged Event occurs when inbound HTTP/3 client connection count changed. Declaration public event EventHandler? Http3ClientConnectionCountChanged Event Type Type Description EventHandler | Edit this page View Source Http3ServerConnectionCountChanged Event occurs when upstream HTTP/3 server connection count changed. Declaration public event EventHandler? Http3ServerConnectionCountChanged Event Type Type Description EventHandler | Edit this page View Source OnClientConnectionCreate Customize TcpClient used for client connection upon create. Declaration public event AsyncEventHandler? OnClientConnectionCreate Event Type Type Description AsyncEventHandler | Edit this page View Source OnRequestBodyWrite Intercept request body send event to server. Subscribe to inspect or modify the request body chunk-by-chunk as it streams to the server, without buffering the whole body. Do not combine with SessionEventArgs.GetRequestBody (which buffers). Declaration public event AsyncEventHandler? OnRequestBodyWrite Event Type Type Description AsyncEventHandler | Edit this page View Source OnResponseBodyWrite Intercept response body send event to client. Subscribe to inspect or modify the response body chunk-by-chunk as it streams to the client, without buffering the whole body. Do not combine with SessionEventArgs.GetResponseBody (which buffers). Declaration public event AsyncEventHandler? OnResponseBodyWrite Event Type Type Description AsyncEventHandler | Edit this page View Source OnServerConnectionCreate Customize TcpClient used for server connection upon create. Declaration public event AsyncEventHandler? OnServerConnectionCreate Event Type Type Description AsyncEventHandler | Edit this page View Source ServerCertificateValidationCallback Event to override the default verification logic of remote SSL certificate received during authentication. Declaration public event AsyncEventHandler? ServerCertificateValidationCallback Event Type Type Description AsyncEventHandler | Edit this page View Source ServerConnectionCountChanged Event occurs when server connection count changed. Declaration public event EventHandler? ServerConnectionCountChanged Event Type Type Description EventHandler Implements IDisposable" + "summary": "Class ProxyServer Translates an HTTP/1.1 client connection onto an h2-only origin (Http2 with AllowHttpProtocolTranslation enabled - see ResolveHttp2ForClientAsync(SessionEventArgsBase, bool, string, int, string, int?, UpstreamHttpProtocol, bool, bool, CancellationToken, bool)), leasing one h2 stream per HTTP/1.1 request from a shared Titanium.Web.Proxy.Http2.Http2OriginConnection via Titanium.Web.Proxy.ProxyServer.Http2OriginConnectionPool rather than opening a new TCP/TLS connection for every request. Inheritance object ProxyServer Implements IDisposable Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy Assembly: Titanium.Web.Proxy.dll Syntax public class ProxyServer : IDisposable Remarks This re-implements the HTTP/1.1 client read loop (request line, headers, BeforeRequest, authorization, header preparation, CancelRequest/replaced-response handling) rather than reusing the private HandleHttpSessionRequest/HandleHttpSessionResponse methods, because those methods send/receive over TcpServerConnection.Stream using the raw HTTP/1.1 wire format, which an h2 origin connection cannot speak. This mirrors the precedent set by the h2-to-HTTP/1.1 bridge (Http2ToHttp11BridgeHandler), which similarly bypasses the wire-format-specific machinery for the leg that does not match it. Origin connections are multiplexed across independent HTTP/1.1 clients through Titanium.Web.Proxy.ProxyServer.Http2OriginConnectionPool (fan-in share). Response bodies are delivered via Titanium.Web.Proxy.Http2.Http2OriginConnection streaming writers where available. Constructors | Edit this page View Source ProxyServer(bool, bool, bool) Initializes a new instance of ProxyServer class with provided parameters. Declaration public ProxyServer(bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false, bool trustRootCertificateAsAdmin = false) Parameters Type Name Description bool userTrustRootCertificate When true (the default), EnsureRootCertificate() installs the MITM root into the current-user Personal and Trusted Root stores. Prefer user-only trust for interactive apps; pass false when trust must be fully opt-in. bool machineTrustRootCertificate When true, also trust in the local-machine stores (needs elevation). Defaults to false — machine trust is opt-in for services/admin installs, not for normal desktop use. bool trustRootCertificateAsAdmin When true, attempt elevated trust via UAC (Windows only). Defaults to false. | Edit this page View Source ProxyServer(string?, string?, bool, bool, bool) Initializes a new instance of ProxyServer class with provided parameters. Declaration public ProxyServer(string? rootCertificateName, string? rootCertificateIssuerName, bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false, bool trustRootCertificateAsAdmin = false) Parameters Type Name Description string rootCertificateName Name of the root certificate. string rootCertificateIssuerName Name of the root certificate issuer. bool userTrustRootCertificate When true (the default), EnsureRootCertificate() installs the MITM root into the current-user Personal and Trusted Root stores. Prefer user-only trust for interactive apps; pass false when trust must be fully opt-in. bool machineTrustRootCertificate When true, also trust in the local-machine stores (needs elevation). Defaults to false — machine trust is opt-in for services/admin installs, not for normal desktop use. bool trustRootCertificateAsAdmin When true, attempt elevated trust via UAC (Windows only). Defaults to false. Fields | Edit this page View Source DefaultViaHeaderPseudonym Default Via header pseudonym (RFC 9110 §7.6.3). Used by ViaHeaderPseudonym and by Inspector when Add Via header is enabled. Declaration public const string DefaultViaHeaderPseudonym = \"titanium-web-proxy\" Field Value Type Description string Properties | Edit this page View Source AdmittedClientConnectionCount Number of client connections currently admitted (accepted and past the admission gate, not yet finished being handled), across all TCP-based endpoints. Unlike ClientConnectionCount, this drops to zero as soon as the handler returns, without the trailing TIME_WAIT delay. Declaration public int AdmittedClientConnectionCount { get; } Property Value Type Description int | Edit this page View Source BlockPrivateNetworkDestinations Outbound destination policy hook: when true, every resolved destination IP address is checked against loopback, private (RFC 1918/4193), link-local (which subsumes the 169.254.169.254 cloud metadata endpoint), and other non-globally-routable ranges before connecting, and the connection attempt is rejected with an OutboundDestinationBlockedException if it matches. Off by default: blocking private destinations would break this library's most common configurations, including upstream-proxy chaining to localhost and interception of local development servers. Only enable this when the proxy accepts requests from untrusted clients (an SSRF-relevant deployment), where those same destinations become an attacker-reachable pivot into the host's private network instead of an operator's own intentional configuration. An explicitly configured upstream proxy address (UpStreamHttpProxy, UpStreamHttpsProxy, or a per-session external proxy) is always exempt - that address is operator intent, not attacker-controlled. Checked against the resolved address actually used to connect (no re-resolution afterward, which would make the check a TOCTOU no-op against DNS rebinding). Not currently enforced for a SOCKS upstream with ProxyDnsRequests enabled, since the proxy never resolves the origin itself in that mode and has no address of its own to validate. Declaration public bool BlockPrivateNetworkDestinations { get; set; } Property Value Type Description bool | Edit this page View Source BufferPool The buffer pool used throughout this proxy instance. Set custom implementations by implementing this interface. By default this uses DefaultBufferPool implementation available in StreamExtended library package. Buffer size should be at least 10 bytes. Declaration public IBufferPool BufferPool { get; set; } Property Value Type Description IBufferPool | Edit this page View Source CertificateManager Manages certificates used by this proxy. Declaration public CertificateManager CertificateManager { get; } Property Value Type Description CertificateManager | Edit this page View Source CheckCertificateRevocation Should we check for certificate revocation during SSL authentication to servers Note: If enabled can reduce performance. Defaults to false. Declaration public X509RevocationMode CheckCertificateRevocation { get; set; } Property Value Type Description X509RevocationMode | Edit this page View Source ClientConnectionCount Total number of active TCP client connections. Does not include inbound HTTP/3 (QUIC) clients; see Http3ClientConnectionCount. Declaration public int ClientConnectionCount { get; } Property Value Type Description int | Edit this page View Source ClientHeaderTimeoutSeconds Seconds to wait for a client to finish sending the request line and headers, from the moment this proxy starts reading a new request on the connection. Enforced with a linked CancellationTokenSource around the request-line and header read, not Socket.ReceiveTimeout: that property only bounds a single blocking Receive call, not the asynchronous reads this proxy actually issues, so without this deadline a client that opens a connection and trickles bytes arbitrarily slowly (or stops sending entirely) after the first byte ties up a read loop indefinitely. Default is 0 (disabled), matching every other deadline in this class - no per-session override exists because there is no SessionEventArgs for this request yet at the point this deadline applies. Declaration public int ClientHeaderTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source CompatibilityMode100Continue When true, the proxy immediately responds with a synthetic 100 Continue to any client request carrying Expect: 100-continue, before forwarding the headers to the origin and without waiting for the origin to respond. This breaks the strict handshake (client → proxy 100 → client body → origin body) but prevents the deadlock that occurs with strict clients when Enable100ContinueBehaviour is false (the default). Has no effect when Enable100ContinueBehaviour is true. Default: false. Declaration public bool CompatibilityMode100Continue { get; set; } Property Value Type Description bool | Edit this page View Source ConnectTimeOutSeconds Seconds server connection are to wait for connection to be established. Default value is 20 seconds. Declaration public int ConnectTimeOutSeconds { get; set; } Property Value Type Description int | Edit this page View Source ConnectionTimeOutSeconds Seconds client/server connection are to be kept alive when waiting for read/write to complete. This will also determine the pool eviction time when connection pool is enabled. Default value is 60 seconds. Declaration public int ConnectionTimeOutSeconds { get; set; } Property Value Type Description int | Edit this page View Source CustomUpStreamProxyFailureFunc A callback to provide a chance for an upstream proxy failure to be handled by a new upstream proxy. User should return the ExternalProxy object with valid credentials or null. Declaration public Func>? CustomUpStreamProxyFailureFunc { get; set; } Property Value Type Description Func> | Edit this page View Source DecryptFailureBypassMaxEntries Maximum learned hosts retained (approximate LRU eviction). Default 256. Declaration public int DecryptFailureBypassMaxEntries { get; set; } Property Value Type Description int | Edit this page View Source DecryptFailureBypassThreshold Origin TLS failure strikes required before a host is bypassed on later CONNECTs. Same-CONNECT opaque fallback after an awaited H2 probe failure marks bypass immediately. Default 2. Declaration public int DecryptFailureBypassThreshold { get; set; } Property Value Type Description int | Edit this page View Source DecryptFailureBypassTtl How long a learned decrypt-bypass entry remains valid. Default 30 minutes. Declaration public TimeSpan DecryptFailureBypassTtl { get; set; } Property Value Type Description TimeSpan | Edit this page View Source DnsServerEndPoint DNS server endpoint used by Titanium.Web.Proxy.Http3.Dns.UdpSvcbDnsResolver for HTTPS/SVCB queries. Defaults to the first usable OS-configured plain-UDP DNS server discovered via NetworkInterface. This is a best-effort default and does not honor Windows NRPT, DoH, or VPN split-DNS policy. When no OS-configured DNS server can be discovered, the property reports 0.0.0.0:0 and proactive SVCB discovery is skipped (never falls back to a public third-party resolver). Assign an explicit endpoint to override discovery. Declaration [Experimental(\"TWP001\")] public IPEndPoint DnsServerEndPoint { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source Enable100ContinueBehaviour Does this proxy uses the HTTP protocol 100 continue behaviour strictly? Broken 100 continue implementations on server/client may cause problems if enabled. Defaults to false. Declaration public bool Enable100ContinueBehaviour { get; set; } Property Value Type Description bool | Edit this page View Source EnableConnectionPool Should we enable the server connection pool. Defaults to true. When connection pooling is enabled, instead of creating a new TCP connection to the server for each client TCP connection, we check if an idle server connection is available in our cached pool. If a compatible connection (same destination, scheme, upstream proxy, credentials and negotiated protocol) created from an earlier request is available, we reuse it. Only connections that are safe to reuse under the HTTP protocol are pooled: the response body must be fully received and the connection must be persistent (HTTP/1.1 keep-alive, or an HTTP/1.0 connection that explicitly opted in via \"Connection: keep-alive\"). Connections whose response asked to close, that failed, or that carry connection-oriented authentication state (WinAuth NTLM/Negotiate) or a per-session client certificate are never returned to the shared pool. The ConnectionTimeOutSeconds parameter determines the eviction time for inactive server connections. This reduces TCP (and TLS) connection establishment cost, both in wall clock time and CPU cycles. Set to false to force a fresh server connection for every client connection. Declaration public bool EnableConnectionPool { get; set; } Property Value Type Description bool | Edit this page View Source EnableDecryptFailureBypass When true, the proxy learns hosts whose origin TLS handshake fails under MITM (non-ALPN AuthenticationException, typically bot / TLS-fingerprint rejection) and tunnels subsequent CONNECTs without decrypt. Default false so library and RPS baselines are unchanged. Inspector enables this by default. Success-path cost when on is one dictionary lookup per CONNECT. Declaration public bool EnableDecryptFailureBypass { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttp2 Enable disable HTTP/2 support. Client-facing HTTP/2 is negotiated via TLS ALPN, or as prior-knowledge cleartext h2c on a transparent reverse endpoint (DecryptSsl: false). No Upgrade: h2c. Origin-facing HTTP/2 uses TLS ALPN h2 by default; with ForwardCleartext and Http2, the origin speaks cleartext HTTP/2 prior-knowledge (outbound h2c). A client/server that does not support HTTP/2 transparently falls back to HTTP/1.1 when policy allows. Request/response header and body modification in BeforeRequest/BeforeResponse, chunked trailers, interim (1xx) responses, and the synthetic-response APIs (Ok/Respond/Redirect/GenericResponse/ RespondStreaming) are all supported over HTTP/2, the same as over HTTP/1.x. Not supported: HTTP/2 server push (the wire frames are transcoded but there is no public API to originate a push) and Upgrade: h2c. Explicit-proxy inbound h2c is not implemented. See the protocol support matrix on the wiki for exact, up-to-date HTTP/1.x/HTTP/2 feature coverage. Declaration public bool EnableHttp2 { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttp3 Enable HTTP/3 (QUIC) support. When true: Any TransparentQuicProxyEndPoint is started as a UDP-only QUIC listener for transparent/NAT HTTP/3 interception. Any TransparentProxyEndPoint with EnableHttp3 also listens for HTTP/3 on the same IP:port (TCP H1/H2 + UDP H3) and injects client-facing Alt-Svc. With Auto (default), a cached Alt-Svc / HTTPS/SVCB capability only arms background QUIC warm-up. Outbound HTTP/3 is used once that origin is warm; until then the request stays on HTTP/2 or HTTP/1.1. Forced Http3 skips warm-up gating and fails closed with no TCP fallback. Requires MsQuic native library and a supported operating-system version (IsSupported). Setting to true with no inbound HTTP/3 endpoint is fine when an explicit/SOCKS/transparent TCP endpoint is present (origin-side QUIC only). A warning is emitted only when EnableHttp3 is set with no client-facing endpoints at all. Default: false (opt-in). Experimental: HTTP/3 support has not yet completed the full interop/soak/fuzz gate process. Suppress TWP001 to opt in; the attribute is removed when the feature graduates to stable. Declaration [Experimental(\"TWP001\")] public bool EnableHttp3 { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttpInterception Forces the full interception path (SessionEventArgs, BeforeRequest, etc.) even when no event handlers are subscribed. Set this when consuming SessionEventArgs for timing or metrics without subscribing to any event. Default: false. Declaration public bool EnableHttpInterception { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttpsSvcbDnsDiscovery When true, the proxy queues a background HTTPS/SVCB RR (DNS type 65) lookup after an Auto-mode capability-cache miss. A positive result (ALPN h3 found) warms Titanium.Web.Proxy.Http3.Http3OriginCapabilityCache for subsequent connections; the CONNECT / request path itself never awaits DNS. Negative results are cached for 1 minute; transient failures use a short backoff. Defaults to true whenever EnableHttp3 is true. Set explicitly to false to disable discovery even when HTTP/3 is enabled — for example, when the configured DNS server is untrusted or unreachable. First-connection HTTP/3 adoption then comes from Alt-Svc. Declaration [Experimental(\"TWP001\")] public bool EnableHttpsSvcbDnsDiscovery { get; set; } Property Value Type Description bool | Edit this page View Source EnableIpv6UnreachableSoftSkip When true (default), after one IPv6 connect failure with NetworkUnreachable (or equivalent), temporarily omit IPv6 addresses from the Happy Eyeballs race for 30 seconds. Reduces first-chance SocketException noise on dual-stack hosts with a broken IPv6 path. Disable if operators require strict IPv6 preference even when the path is unreachable. Declaration public bool EnableIpv6UnreachableSoftSkip { get; set; } Property Value Type Description bool | Edit this page View Source EnableQpackDynamicTable When true, enables RFC 9204 QPACK dynamic table encoding and decoding for inbound HTTP/3 connections. Each connection gets its own Titanium.Web.Proxy.Http3.Qpack.QpackContext with two independent 4096-byte tables (one inbound, one outbound). Defaults to false (static-table-only); existing deployments are unaffected. Declaration [Experimental(\"TWP001\")] public bool EnableQpackDynamicTable { get; set; } Property Value Type Description bool | Edit this page View Source EnableRequestTimingCapture Enables structured request/connection timing capture. When false (the default) no timing objects are allocated and no UtcNow calls are made for timing purposes anywhere in the proxy, so there is zero overhead on the hot path. When enabled, every SessionEventArgsBase exposes a populated Timing (per-request phases: client header read, connection wait, request send, time-to-first-byte, response delivery, total), every upstream connection exposes a populated UpstreamConnectionTiming (reachable from a session via UpstreamConnectionTiming, describing DNS, TCP connect, optional upstream-proxy CONNECT, and TLS handshake durations), and a decrypted TunnelConnectSessionEventArgs exposes the client-facing TLS handshake duration via ClientTlsTiming. Can be toggled at any time; it only affects sessions/connections created after the change, never mutating timing objects already handed out. Defaults to false. Declaration public bool EnableRequestTimingCapture { get; set; } Property Value Type Description bool | Edit this page View Source EnableRfc8441 When true, the proxy enables RFC 8441 WebSocket-over-HTTP/2: Accepts extended CONNECT (:protocol = websocket) from h2 clients and advertises SETTINGS_ENABLE_CONNECT_PROTOCOL=1 to them. Per origin: if the origin is HTTP/2 and advertises RFC 8441 support, DATA frames are relayed directly; if the origin is HTTP/2 and does not, the stream is reset with REFUSED_STREAM; if the origin is HTTP/1.1, the h2→h1 WebSocket upgrade bridge is used. On the HTTP/1.1-client-to-h2-origin translation bridge, translates Upgrade: websocket into extended CONNECT when the origin advertises the setting; otherwise falls back to a dedicated HTTP/1.1 origin connection for that WebSocket. When this property is false, that bridge still returns synthetic 501 Not Implemented for WebSocket upgrades (historical default). Default: false (must opt-in). Declaration public bool EnableRfc8441 { get; set; } Property Value Type Description bool | Edit this page View Source EnableTcpKeepAlive Enable TCP KeepAlive on client and server sockets so NAT/firewall mappings for long-lived CONNECT tunnels are refreshed. Default: true. Declaration public bool EnableTcpKeepAlive { get; set; } Property Value Type Description bool | Edit this page View Source EnableTcpServerConnectionPrefetch Should we enable tcp server connection prefetching? When enabled, as soon as we receive a client connection we concurrently initiate corresponding server connection process using CONNECT hostname or SNI hostname on a separate task so that after parsing client request we will have the server connection immediately ready or in the process of getting ready. If a server connection is available in cache then this prefetch task will immediately return with the available connection from cache. Defaults to true. Declaration public bool EnableTcpServerConnectionPrefetch { get; set; } Property Value Type Description bool | Edit this page View Source EnableWinAuth Enable disable Windows Authentication (NTLM/Kerberos). By default SSPI uses the process identity. To authenticate as another user, set WinAuthCredentialsProvider (issue #461). Defaults to false. Declaration public bool EnableWinAuth { get; set; } Property Value Type Description bool | Edit this page View Source EndpointAdmissionRejectionCount Total number of client connections rejected by any endpoint's MaxConcurrentClients since this instance was created. Declaration public long EndpointAdmissionRejectionCount { get; } Property Value Type Description long | Edit this page View Source ForwardToUpstreamGateway Gets or sets a value indicating whether requests will be chained to upstream gateway. Defaults to false. Declaration public bool ForwardToUpstreamGateway { get; set; } Property Value Type Description bool | Edit this page View Source GetCustomUpStreamProxyFunc A callback to provide authentication credentials for up stream proxy this proxy is using for HTTP(S) requests. User should return the ExternalProxy object with valid credentials. Declaration public Func>? GetCustomUpStreamProxyFunc { get; set; } Property Value Type Description Func> | Edit this page View Source GlobalAdmissionRejectionCount Total number of client connections rejected by MaxConcurrentClientConnections since this instance was created. Declaration public long GlobalAdmissionRejectionCount { get; } Property Value Type Description long | Edit this page View Source Http3ClientConnectionCount Total number of active inbound HTTP/3 (QUIC) client connections. Declaration public int Http3ClientConnectionCount { get; } Property Value Type Description int | Edit this page View Source Http3ServerConnectionCount Total number of active upstream HTTP/3 (QUIC) server connections. These are also included in ServerConnectionCount. Declaration public int Http3ServerConnectionCount { get; } Property Value Type Description int | Edit this page View Source IdleReadTimeoutSeconds Seconds of idle time allowed while reading from the origin (stalled header/body waits). Applied via CancelAfter on the active read operation. Default is 0 (disabled). Per-session override: IdleReadTimeout. Declaration public int IdleReadTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source IdleWriteTimeoutSeconds Seconds of idle time allowed while writing to the origin (stalled header/body waits). Applied via CancelAfter on the active write operation. Default is 0 (disabled). Per-session override: IdleWriteTimeout. Declaration public int IdleWriteTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source IgnoreServerCertificateErrors When true, origin TLS certificates that fail OS chain validation are still accepted (MITM of loopback/self-signed/private CAs). Inspector's \"Ignore server certificate errors\" maps here. Default false. A subscribed ServerCertificateValidationCallback still wins. Declaration public bool IgnoreServerCertificateErrors { get; set; } Property Value Type Description bool | Edit this page View Source ListenerBackLog TCP listener accept backlog. Default: 1024 for burst connection handling. Declaration public int ListenerBackLog { get; set; } Property Value Type Description int | Edit this page View Source Logger The live, shared logger used throughout this proxy instance. Reflects the most recent call to ApplyLoggingConfiguration(). Declaration public ILogger Logger { get; } Property Value Type Description ILogger | Edit this page View Source Logging Configuration for this proxy instance's built-in diagnostic logging - the replacement for the removed ExceptionFunc callback. Every exception the proxy catches (even when handled internally and never surfaced to user code) is reported through this logger at an appropriate severity; see ProxyLoggingOptions for the console/file sinks, enable/disable switch, and minimum level. Mutate the returned instance (or assign a new one) at any point; each assignment/mutation you want to take effect must be followed by ApplyLoggingConfiguration() (which Start(bool) also calls automatically, so the configuration active at the moment the proxy starts running is picked up for the run even if you never call it yourself). Calling it again later - including while the proxy is already running - immediately swaps in the new configuration; this is safe because logging never blocks or otherwise affects proxy traffic. Declaration public ProxyLoggingOptions Logging { get; set; } Property Value Type Description ProxyLoggingOptions | Edit this page View Source MaxBufferedBodyBytes Maximum bytes the proxy will buffer for a single request or response body when body buffering is required (body-read hooks, authentication retry, etc.). Bodies larger than this limit are rejected with 413 (upstream request) or connection teardown (upstream response). Set to 0 to disable the limit (not recommended). Default: 4,194,304 (4 MiB). Declaration public int MaxBufferedBodyBytes { get; set; } Property Value Type Description int | Edit this page View Source MaxCachedConnections Maximum number of concurrent connections per remote host in cache. Only meaningful when EnableConnectionPool is true; to disable pooling, set EnableConnectionPool to false rather than setting this to 0 - the pool eviction loop treats a value below 1 as \"evict without limit while holding the pool-wide lock\", which spins indefinitely once the cache for that host is empty and would stall every other connection acquire/release in the process. Rejected outright at assignment so that state cannot be reached. Default value is 128. Declaration public int MaxCachedConnections { get; set; } Property Value Type Description int Exceptions Type Condition ArgumentOutOfRangeException The assigned value is less than 1. | Edit this page View Source MaxConcurrentClientConnections Maximum number of client connections admitted across all TCP-based endpoints at once. null (the default) disables the global admission gate, preserving today's unbounded behavior. When set, a connection beyond this limit is rejected and disposed immediately after accept, before a handler task is even started. Enforced independently of ClientConnectionCount: see Titanium.Web.Proxy.ProxyServer.admittedClientConnectionCount for why. See also MaxConcurrentClients for a per-endpoint cap layered on top of this global one. Declaration public int? MaxConcurrentClientConnections { get; set; } Property Value Type Description int? | Edit this page View Source MaxConcurrentHttp11HttpsOriginCreates Caps concurrent new HTTPS origin TCP/TLS opens on the H2→H1 bridge only (MITM / re-encrypt). Pool hits (warm keep-alive) are uncapped. Cleartext H1 origins are not gated. Default is Clamp(ProcessorCount, 4, 32). Set before the first H2→H1 HTTPS origin open (typically before Start(bool)); changing the value after the create gate has been used has no effect on the live semaphore. Declaration public int MaxConcurrentHttp11HttpsOriginCreates { get; set; } Property Value Type Description int Exceptions Type Condition ArgumentOutOfRangeException The assigned value is less than 1. | Edit this page View Source MaxDecodedHeaderListBytes Maximum decoded HTTP/2 header list size in bytes, using RFC 7541 accounting (name.Length + value.Length + 32 per field). Requests or responses with a decoded header list exceeding this limit will be refused with RST_STREAM(ENHANCE_YOUR_CALM) (code 0xb). Set to 0 to disable the limit (not recommended). Default: 65,536 (64 KiB). Advertised via SETTINGS_MAX_HEADER_LIST_SIZE. Declaration public int MaxDecodedHeaderListBytes { get; set; } Property Value Type Description int | Edit this page View Source MaxWebSocketFramePayloadBytes Maximum WebSocket frame payload size in bytes that the proxy will accept during frame-level interception (i.e. when BeforeWebSocketFrame has at least one subscriber). Frames whose decoded payload exceeds this limit cause the WebSocket connection to be closed with Close code 1009 (Message Too Big). Raw-relay sessions (no BeforeWebSocketFrame subscriber) bypass this check entirely and pass all frames through unvalidated. Default: 16,777,216 (16 MiB). Declaration public int MaxWebSocketFramePayloadBytes { get; set; } Property Value Type Description int | Edit this page View Source NetworkFailureRetryAttempts Number of times to retry upon network failures when connection pool is enabled. Declaration public int NetworkFailureRetryAttempts { get; set; } Property Value Type Description int | Edit this page View Source NoDelay Gets or sets a Boolean value that specifies whether server and client stream Sockets are using the Nagle algorithm. Defaults to true, no nagle algorithm is used. Declaration public bool NoDelay { get; set; } Property Value Type Description bool | Edit this page View Source OriginHttpVersionPolicy Controls which HTTP version is declared to the origin server on the request line, independently of the version the client declared to the proxy. Defaults to PreserveClientVersion, which matches the proxy's historical pass-through behavior exactly. Set to NormalizeToHttp11 to let HTTP/1.0 clients share pooled, persistent origin connections the same way HTTP/1.1 clients already do. This only changes the wire version written to the origin request line - it never changes the client-facing Http.Request.HttpVersion that event handlers observe, nor the version/persistence used to write the response back to the client. Declaration public OriginHttpVersionPolicy OriginHttpVersionPolicy { get; set; } Property Value Type Description OriginHttpVersionPolicy | Edit this page View Source PolicyModes Which resource-bound PolicyFamily is enforced, observed, or disabled, per the plan's rollout section. Read live by each family's enforcement call site - not baked into a per-request snapshot at connection accept time - so assigning a new value here (a whole-object replacement, never a mutation of the previous instance) takes effect for the next check any in-flight or new request makes, without restarting the proxy. This is the \"runtime switch to drop to Observe without redeploying\" the plan requires; see WithAllObservedExceptDisabled() for the one-call way to do that. Defaults to AllEnforce, matching Balanced. Assigning Profile also replaces this value with that profile's bundle; assign PolicyModes afterward to deviate from the selected profile's modes without changing anything else the profile set. Declaration public ProxyPolicyModes PolicyModes { get; set; } Property Value Type Description ProxyPolicyModes | Edit this page View Source Profile The last profile applied via this property's setter, defaulting to Balanced - the profile every field on this instance already starts at, so a fresh new ProxyServer() reports Balanced without needing its setter to run once at construction time. Assigning this property applies its entire ProxyProfileSettings bundle - ResourceLimits, PolicyModes, SupportedSslProtocols, BlockPrivateNetworkDestinations, MaxConcurrentClientConnections and the deadline-seconds properties - as a single atomic assignment, so a reader can never observe a half-applied profile. Assigning any of those properties individually afterward overrides just that one, without reverting the rest of the profile's bundle. Logged once per Start(bool) call, by name only - never with hosts, URLs or secrets, per the plan's rollout section. Declaration public ProxyProfile Profile { get; set; } Property Value Type Description ProxyProfile | Edit this page View Source ProxyAuthenticationRealm Realm used during Proxy Basic Authentication. Declaration public string ProxyAuthenticationRealm { get; set; } Property Value Type Description string | Edit this page View Source ProxyAuthenticationSchemes A collection of scheme types, e.g. basic, NTLM, Kerberos, Negotiate, to return if scheme authentication is required. Works in relation with ProxySchemeAuthenticateFunc. Declaration public IEnumerable ProxyAuthenticationSchemes { get; set; } Property Value Type Description IEnumerable | Edit this page View Source ProxyBasicAuthenticateFunc A callback to authenticate proxy clients via basic authentication. Parameters are username and password as provided by client. Should return true for successful authentication. Declaration public Func>? ProxyBasicAuthenticateFunc { get; set; } Property Value Type Description Func> | Edit this page View Source ProxyEndPoints A list of IpAddress and port this proxy is listening to. Declaration public List ProxyEndPoints { get; set; } Property Value Type Description List | Edit this page View Source ProxyRunning Is the proxy currently running? Declaration public bool ProxyRunning { get; } Property Value Type Description bool | Edit this page View Source ProxySchemeAuthenticateFunc A pluggable callback to authenticate clients by scheme instead of requiring basic authentication through ProxyBasicAuthenticateFunc. Parameters are current working session, schemeType, and token as provided by a calling client. Should return success for successful authentication, continuation if the package requests, or failure. Declaration public Func>? ProxySchemeAuthenticateFunc { get; set; } Property Value Type Description Func> | Edit this page View Source RequestTimeoutSeconds Total seconds allowed for a single request/response exchange after BeforeRequest returns (connect, send, wait for headers, and body copy). Default is 0 (disabled). Per-session override: RequestTimeout. Declaration public int RequestTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source ResourceLimits The shared, immutable resource-bound snapshot (concurrent-stream cap, CONTINUATION frame-count/wall-clock bounds, peer-initiated incomplete-stream-reset budget, and the other limits described in ProxyResourceLimits) consulted by the HTTP/2 relay so a single proxy-owned value governs both what is enforced and what is advertised to each peer, rather than admitting purely against whatever the origin advertised. Assign a new ProxyResourceLimits (constructed via Create(long, int, long, long?, long?, double?, int?, int, int?, int, TimeSpan, bool, int, int?)) to override the Default snapshot. There is no artificial upper clamp: high-CPU/RAM hosts may pass larger maxCachedConnectionsPerHost, maxConcurrentStreamsPerConnection, etc. as needed. The live TCP pool depth knob MaxCachedConnections remains independently settable and should usually be kept in sync with MaxCachedConnectionsPerHost. Declaration public ProxyResourceLimits ResourceLimits { get; set; } Property Value Type Description ProxyResourceLimits | Edit this page View Source ResponseHeaderTimeoutSeconds Seconds to wait for the origin to send the response status line and headers after the request has been sent. Enforced with a linked CancellationTokenSource (not Socket receive timeout alone). When the deadline elapses a ProxyTimeoutException with ResponseHeader is raised (and may be converted to HTTP 504 before any response bytes have been committed to the client). Default is 0 (disabled). WebSocket upgrades, Server-Sent Events, raw tunnels, and sessions that already wrote a response status to the client are exempt; those waits use IdleReadTimeoutSeconds when configured. Per-session override: ResponseHeaderTimeout. Declaration public int ResponseHeaderTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source ReuseSocket When true (default), SO_REUSEADDR is requested where Titanium.Web.Proxy.Helpers.RunTime.IsSocketReuseAvailable() reports support (always on Windows; on non-Windows, .NET Core 3+ / compatible runtimes). Declaration public bool ReuseSocket { get; set; } Property Value Type Description bool | Edit this page View Source ReverseProxy Optional reverse-proxy route/cluster configuration. When null (default), Core keeps 6.x ForwardHost behavior with zero added cost on the hot path. Declaration public ReverseProxyOptions? ReverseProxy { get; set; } Property Value Type Description ReverseProxyOptions | Edit this page View Source ServerConnectionCount Total number of active server connections (TCP plus upstream QUIC). For HTTP/3-only upstreams see Http3ServerConnectionCount. Declaration public int ServerConnectionCount { get; } Property Value Type Description int | Edit this page View Source ShouldInterceptHttp Optional per-request/stream predicate consulted only when the global interception gate is active. Return true to use the full SessionEventArgs path; return false to use the fast-forward path. null (the default) intercepts every request — preserving today's behavior. Declaration public Func? ShouldInterceptHttp { get; set; } Property Value Type Description Func | Edit this page View Source SupportedServerSslProtocols Ssl versions offered on outbound HTTPS connections to origins (and upstream proxies). Default None means “use SupportedSslProtocols” (typically TLS 1.2 and 1.3). Set an explicit mask to restrict or expand outbound-only independently of inbound client TLS. Older docs described None as “same as the proxy client.” That coupling is incorrect across protocol translations (e.g. inbound QUIC is always TLS 1.3 while outbound TCP SslStream on macOS SecureTransport cannot offer TLS 1.3). Declaration public SslProtocols SupportedServerSslProtocols { get; set; } Property Value Type Description SslProtocols | Edit this page View Source SupportedSslProtocols List of supported Ssl versions. Defaults to TLS 1.2/1.3 only as of 5.0 - a breaking change from 4.x, which also enabled SSL 3.0/TLS 1.0/1.1. Those legacy, broken-by-design protocols require an explicit opt-in by assigning this property directly (e.g. SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13) if a legacy client/server genuinely requires them. Declaration public SslProtocols SupportedSslProtocols { get; set; } Property Value Type Description SslProtocols | Edit this page View Source TcpTimeWaitSeconds SO_LINGER timeout in seconds applied to client and upstream sockets via LingerOption (enabled with this timeout). This is not the kernel TCP TIME_WAIT duration — TIME_WAIT is controlled by the OS. A positive value means Close may block up to that many seconds flushing send buffers; use 0 for an abortive close (RST). Default is 0 so high-churn proxies avoid TIME_WAIT accumulation; the 1-second connection disposal delay already prefers peer-first close. Declaration public int TcpTimeWaitSeconds { get; set; } Property Value Type Description int | Edit this page View Source ThreadPoolWorkerThread Customize the minimum ThreadPool size (increase it on a server). Defaults to max(ProcessorCount * 2, 16) so short loopback/proxy workloads are not starved while the pool is still ramping workers. Declaration public int ThreadPoolWorkerThread { get; set; } Property Value Type Description int | Edit this page View Source UpStreamEndPoint Local adapter/NIC endpoint where proxy makes request via. Defaults via any IP addresses of this machine. When the resolved destination address family does not match this endpoint, it is ignored so dual-stack destinations can still connect (see UpStreamEndPointIPv4 / UpStreamEndPointIPv6). Declaration public IPEndPoint? UpStreamEndPoint { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source UpStreamEndPointIPv4 Local bind endpoint used when the resolved upstream destination is IPv4. Takes precedence over UpStreamEndPoint for IPv4 destinations. Declaration public IPEndPoint? UpStreamEndPointIPv4 { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source UpStreamEndPointIPv6 Local bind endpoint used when the resolved upstream destination is IPv6. Takes precedence over UpStreamEndPoint for IPv6 destinations. Declaration public IPEndPoint? UpStreamEndPointIPv6 { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source UpStreamHttpProxy External proxy used for Http requests. Declaration public IExternalProxy? UpStreamHttpProxy { get; set; } Property Value Type Description IExternalProxy | Edit this page View Source UpStreamHttpsProxy External proxy used for Https requests. Declaration public IExternalProxy? UpStreamHttpsProxy { get; set; } Property Value Type Description IExternalProxy | Edit this page View Source UpstreamProxyConfigurationScript If set, the upstream proxy will be detected by a script that will be loaded from the provided Uri Declaration public Uri? UpstreamProxyConfigurationScript { get; set; } Property Value Type Description Uri | Edit this page View Source ViaHeaderPseudonym Pseudonym used in Via header fields appended to forwarded requests and responses (RFC 9110 §7.6.3). Defaults to DefaultViaHeaderPseudonym. Set to an empty string to disable Via header injection entirely. Loop detection uses this value: a request arriving with this pseudonym already present in Via is refused with 508 Loop Detected. Declaration public string ViaHeaderPseudonym { get; set; } Property Value Type Description string | Edit this page View Source WinAuthCredentialsProvider Optional per-session credential provider for server 401 WinAuth (NTLM/Negotiate/Kerberos). Return null to use the current process identity (legacy behavior). Do not put plaintext passwords on SessionEventArgs — use this callback instead. Windows SSPI only; ignored on non-Windows platforms. Declaration public Func>? WinAuthCredentialsProvider { get; set; } Property Value Type Description Func> Methods | Edit this page View Source AddEndPoint(ProxyEndPoint) Add a proxy end point. Declaration public void AddEndPoint(ProxyEndPoint endPoint) Parameters Type Name Description ProxyEndPoint endPoint The proxy endpoint. | Edit this page View Source ApplyLoggingConfiguration() Rebuilds the active logger/logger factory from the current Logging configuration, disposing any previously owned built-in providers. Called automatically from the constructor (with the default configuration) and from Start(bool). Call this explicitly any time after changing Logging and you want the change to take effect immediately - whether the proxy is stopped (e.g. before using CertificateManager directly) or already running. Declaration public void ApplyLoggingConfiguration() | Edit this page View Source ClearDecryptFailureBypass() Clears all learned decrypt-bypass entries. Declaration public void ClearDecryptFailureBypass() | Edit this page View Source DisableAllSystemProxies() Clear all proxy settings for current machine. Declaration public void DisableAllSystemProxies() | Edit this page View Source DisableSystemHttpProxy() Clear HTTP proxy settings of current machine. Declaration public void DisableSystemHttpProxy() | Edit this page View Source DisableSystemHttpsProxy() Clear HTTPS proxy settings of current machine. Declaration public void DisableSystemHttpsProxy() | Edit this page View Source DisableSystemProxy(ProxyProtocolType) Clear the specified proxy setting for current machine. Declaration public void DisableSystemProxy(ProxyProtocolType protocolType) Parameters Type Name Description ProxyProtocolType protocolType | Edit this page View Source Dispose() Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. Declaration public void Dispose() | Edit this page View Source Dispose(bool) Declaration [SuppressMessage(\"ApiDesign\", \"RS0016:Add public types and members to the declared API\", Justification = \"Protected Dispose(bool) is required by the standard IDisposable pattern but is not public API.\")] protected virtual void Dispose(bool disposing) Parameters Type Name Description bool disposing | Edit this page View Source GetDecryptFailureBypassEntries() Snapshot of current learned decrypt-bypass entries (may include non-active strikes). Declaration public IReadOnlyList GetDecryptFailureBypassEntries() Returns Type Description IReadOnlyList | Edit this page View Source RemoveDecryptFailureBypass(string) Removes one host from the learned decrypt-bypass cache. Declaration public bool RemoveDecryptFailureBypass(string host) Parameters Type Name Description string host Returns Type Description bool | Edit this page View Source RemoveEndPoint(ProxyEndPoint) Remove a proxy end point. Will throw error if the end point doesn't exist. Declaration public void RemoveEndPoint(ProxyEndPoint endPoint) Parameters Type Name Description ProxyEndPoint endPoint The existing endpoint to remove. | Edit this page View Source RestoreOriginalProxySettings() Restores the original proxy settings. Declaration public void RestoreOriginalProxySettings() | Edit this page View Source SetAsSystemHttpProxy(ExplicitProxyEndPoint) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemHttpProxy(ExplicitProxyEndPoint endPoint) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. | Edit this page View Source SetAsSystemHttpProxy(ExplicitProxyEndPoint, SystemProxySettings) Set the given explicit end point as the default HTTP proxy server for current machine. Declaration public void SetAsSystemHttpProxy(ExplicitProxyEndPoint endPoint, SystemProxySettings settings) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. SystemProxySettings settings The Windows system proxy settings. | Edit this page View Source SetAsSystemHttpsProxy(ExplicitProxyEndPoint) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemHttpsProxy(ExplicitProxyEndPoint endPoint) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. | Edit this page View Source SetAsSystemHttpsProxy(ExplicitProxyEndPoint, SystemProxySettings) Set the given explicit end point as the default HTTPS proxy server for current machine. Declaration public void SetAsSystemHttpsProxy(ExplicitProxyEndPoint endPoint, SystemProxySettings settings) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. SystemProxySettings settings The Windows system proxy settings. | Edit this page View Source SetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. ProxyProtocolType protocolType The proxy protocol type. | Edit this page View Source SetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType, SystemProxySettings?) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType, SystemProxySettings? settings) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. ProxyProtocolType protocolType The proxy protocol type. SystemProxySettings settings The Windows system proxy settings, or null to preserve the current bypass list. | Edit this page View Source SetHttp3Enabled(bool) Enables or disables EnableHttp3. Enabling still requires MsQuic (IsSupported); disabling is always applied. Safe to call while the proxy is running — new origin connections pick up the change. Existing sessions keep the protocol they already negotiated. Declaration public bool SetHttp3Enabled(bool enabled) Parameters Type Name Description bool enabled Returns Type Description bool true when HTTP/3 is enabled after the call. | Edit this page View Source ShouldBypassDecryptForLearnedHost(string?) When EnableDecryptFailureBypass is on and host is actively bypassed, returns true (decrypt should be skipped). Declaration public bool ShouldBypassDecryptForLearnedHost(string? host) Parameters Type Name Description string host Returns Type Description bool | Edit this page View Source Start(bool) Start this proxy server instance. Transactional: if any endpoint fails to start, every listener this call already started is stopped, the system-upstream-proxy resolver (if this call created one) is disposed, and ProxyRunning is left false before the exception propagates. A caller that catches the exception is left with an instance in exactly the same state as before calling Start(bool), not a partially-bound proxy with some endpoints silently listening. Declaration public void Start(bool changeSystemProxySettings = true) Parameters Type Name Description bool changeSystemProxySettings Whether or not clear any system proxy settings which is pointing to our own endpoint (causing a cycle). E.g due to ungracious proxy shutdown before. | Edit this page View Source Stop() Stop this proxy server instance. Endpoints remain registered so Start(bool) can re-listen on the same ports. In-flight sessions are cancelled; pooled upstream connections are cleared. The connection factory itself stays usable for a subsequent Start (it is only disposed with the proxy). Declaration public void Stop() | Edit this page View Source StopAsync(TimeSpan?) Asynchronously stop this proxy server, cancel in-flight sessions, and wait briefly for client connection count to drain before clearing the upstream pool. Declaration public Task StopAsync(TimeSpan? drainTimeout = null) Parameters Type Name Description TimeSpan? drainTimeout Maximum time to wait for active client handlers to exit after cancellation. Defaults to 5 seconds. Returns Type Description Task | Edit this page View Source TryDisableAllSystemProxies() Clear all OS proxy settings without throwing. Declaration public SystemProxyChangeResult TryDisableAllSystemProxies() Returns Type Description SystemProxyChangeResult | Edit this page View Source TryDisableSystemProxy(ProxyProtocolType) Clear OS proxy for the given protocols without throwing. Declaration public SystemProxyChangeResult TryDisableSystemProxy(ProxyProtocolType protocolType) Parameters Type Name Description ProxyProtocolType protocolType Returns Type Description SystemProxyChangeResult | Edit this page View Source TryEnableHttp3IfSupported() Turns on EnableHttp3 when MsQuic is available (IsSupported). Hosts (CLI, Inspector, examples) should call this instead of setting EnableHttp3 blindly. Returns true when HTTP/3 was enabled. Declaration public bool TryEnableHttp3IfSupported() Returns Type Description bool | Edit this page View Source TryRestoreOriginalProxySettings() Restore OS proxy without throwing. Declaration public SystemProxyChangeResult TryRestoreOriginalProxySettings() Returns Type Description SystemProxyChangeResult | Edit this page View Source TrySetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType, SystemProxySettings?) Enable OS system proxy without throwing. Failures are logged and returned so Inspector/CLI can show a status message instead of crashing. Declaration public SystemProxyChangeResult TrySetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType, SystemProxySettings? settings = null) Parameters Type Name Description ExplicitProxyEndPoint endPoint ProxyProtocolType protocolType SystemProxySettings settings Returns Type Description SystemProxyChangeResult Events | Edit this page View Source AfterResponse Intercept after response event from server. Declaration public event AsyncEventHandler? AfterResponse Event Type Type Description AsyncEventHandler | Edit this page View Source BeforeRequest Intercept request event to server. Declaration public event AsyncEventHandler? BeforeRequest Event Type Type Description AsyncEventHandler | Edit this page View Source BeforeResponse Intercept response event from server. Declaration public event AsyncEventHandler? BeforeResponse Event Type Type Description AsyncEventHandler | Edit this page View Source BeforeUpStreamConnectRequest Intercept connect request sent to upstream proxy. Declaration public event AsyncEventHandler? BeforeUpStreamConnectRequest Event Type Type Description AsyncEventHandler | Edit this page View Source ClientCertificateSelectionCallback Event to override client certificate selection during mutual SSL authentication. Declaration public event AsyncEventHandler? ClientCertificateSelectionCallback Event Type Type Description AsyncEventHandler | Edit this page View Source ClientConnectionCountChanged Event occurs when client connection count changed. Declaration public event EventHandler? ClientConnectionCountChanged Event Type Type Description EventHandler | Edit this page View Source DecryptFailureBypassChanged Raised when a host becomes actively bypassed (threshold reached or same-CONNECT mark). Handlers must not block; Inspector marshals to the UI thread. Declaration public event EventHandler? DecryptFailureBypassChanged Event Type Type Description EventHandler | Edit this page View Source Http3ClientConnectionCountChanged Event occurs when inbound HTTP/3 client connection count changed. Declaration public event EventHandler? Http3ClientConnectionCountChanged Event Type Type Description EventHandler | Edit this page View Source Http3ServerConnectionCountChanged Event occurs when upstream HTTP/3 server connection count changed. Declaration public event EventHandler? Http3ServerConnectionCountChanged Event Type Type Description EventHandler | Edit this page View Source OnClientConnectionCreate Customize TcpClient used for client connection upon create. Declaration public event AsyncEventHandler? OnClientConnectionCreate Event Type Type Description AsyncEventHandler | Edit this page View Source OnRequestBodyWrite Intercept request body send event to server. Subscribe to inspect or modify the request body chunk-by-chunk as it streams to the server, without buffering the whole body. Do not combine with SessionEventArgs.GetRequestBody (which buffers). Declaration public event AsyncEventHandler? OnRequestBodyWrite Event Type Type Description AsyncEventHandler | Edit this page View Source OnResponseBodyWrite Intercept response body send event to client. Subscribe to inspect or modify the response body chunk-by-chunk as it streams to the client, without buffering the whole body. Do not combine with SessionEventArgs.GetResponseBody (which buffers). Declaration public event AsyncEventHandler? OnResponseBodyWrite Event Type Type Description AsyncEventHandler | Edit this page View Source OnServerConnectionCreate Customize TcpClient used for server connection upon create. Declaration public event AsyncEventHandler? OnServerConnectionCreate Event Type Type Description AsyncEventHandler | Edit this page View Source ServerCertificateValidationCallback Event to override the default verification logic of remote SSL certificate received during authentication. Declaration public event AsyncEventHandler? ServerCertificateValidationCallback Event Type Type Description AsyncEventHandler | Edit this page View Source ServerConnectionCountChanged Event occurs when server connection count changed. Declaration public event EventHandler? ServerConnectionCountChanged Event Type Type Description EventHandler Implements IDisposable" }, "api/Titanium.Web.Proxy.Routing.ReverseProxyFastPath.html": { "href": "api/Titanium.Web.Proxy.Routing.ReverseProxyFastPath.html", diff --git a/docs/xrefmap.yml b/docs/xrefmap.yml index 8aab15915..2a8496912 100644 --- a/docs/xrefmap.yml +++ b/docs/xrefmap.yml @@ -5294,6 +5294,96 @@ references: commentId: N:Titanium.Web.Proxy.Models fullName: Titanium.Web.Proxy.Models nameWithType: Titanium.Web.Proxy.Models +- uid: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry + name: DecryptFailureBypassEntry + href: api/Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html + commentId: T:Titanium.Web.Proxy.Models.DecryptFailureBypassEntry + fullName: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry + nameWithType: DecryptFailureBypassEntry +- uid: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.#ctor(System.String,System.Int32,System.DateTime,System.DateTime,System.Boolean) + name: DecryptFailureBypassEntry(string, int, DateTime, DateTime, bool) + href: api/Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html#Titanium_Web_Proxy_Models_DecryptFailureBypassEntry__ctor_System_String_System_Int32_System_DateTime_System_DateTime_System_Boolean_ + commentId: M:Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.#ctor(System.String,System.Int32,System.DateTime,System.DateTime,System.Boolean) + name.vb: New(String, Integer, Date, Date, Boolean) + fullName: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.DecryptFailureBypassEntry(string, int, System.DateTime, System.DateTime, bool) + fullName.vb: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.New(String, Integer, Date, Date, Boolean) + nameWithType: DecryptFailureBypassEntry.DecryptFailureBypassEntry(string, int, DateTime, DateTime, bool) + nameWithType.vb: DecryptFailureBypassEntry.New(String, Integer, Date, Date, Boolean) +- uid: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.#ctor* + name: DecryptFailureBypassEntry + href: api/Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html#Titanium_Web_Proxy_Models_DecryptFailureBypassEntry__ctor_ + commentId: Overload:Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.#ctor + isSpec: "True" + name.vb: New + fullName: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.DecryptFailureBypassEntry + fullName.vb: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.New + nameWithType: DecryptFailureBypassEntry.DecryptFailureBypassEntry + nameWithType.vb: DecryptFailureBypassEntry.New +- uid: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.BypassActive + name: BypassActive + href: api/Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html#Titanium_Web_Proxy_Models_DecryptFailureBypassEntry_BypassActive + commentId: P:Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.BypassActive + fullName: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.BypassActive + nameWithType: DecryptFailureBypassEntry.BypassActive +- uid: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.BypassActive* + name: BypassActive + href: api/Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html#Titanium_Web_Proxy_Models_DecryptFailureBypassEntry_BypassActive_ + commentId: Overload:Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.BypassActive + isSpec: "True" + fullName: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.BypassActive + nameWithType: DecryptFailureBypassEntry.BypassActive +- uid: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.ExpiresAtUtc + name: ExpiresAtUtc + href: api/Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html#Titanium_Web_Proxy_Models_DecryptFailureBypassEntry_ExpiresAtUtc + commentId: P:Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.ExpiresAtUtc + fullName: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.ExpiresAtUtc + nameWithType: DecryptFailureBypassEntry.ExpiresAtUtc +- uid: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.ExpiresAtUtc* + name: ExpiresAtUtc + href: api/Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html#Titanium_Web_Proxy_Models_DecryptFailureBypassEntry_ExpiresAtUtc_ + commentId: Overload:Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.ExpiresAtUtc + isSpec: "True" + fullName: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.ExpiresAtUtc + nameWithType: DecryptFailureBypassEntry.ExpiresAtUtc +- uid: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.Host + name: Host + href: api/Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html#Titanium_Web_Proxy_Models_DecryptFailureBypassEntry_Host + commentId: P:Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.Host + fullName: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.Host + nameWithType: DecryptFailureBypassEntry.Host +- uid: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.Host* + name: Host + href: api/Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html#Titanium_Web_Proxy_Models_DecryptFailureBypassEntry_Host_ + commentId: Overload:Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.Host + isSpec: "True" + fullName: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.Host + nameWithType: DecryptFailureBypassEntry.Host +- uid: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.LearnedAtUtc + name: LearnedAtUtc + href: api/Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html#Titanium_Web_Proxy_Models_DecryptFailureBypassEntry_LearnedAtUtc + commentId: P:Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.LearnedAtUtc + fullName: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.LearnedAtUtc + nameWithType: DecryptFailureBypassEntry.LearnedAtUtc +- uid: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.LearnedAtUtc* + name: LearnedAtUtc + href: api/Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html#Titanium_Web_Proxy_Models_DecryptFailureBypassEntry_LearnedAtUtc_ + commentId: Overload:Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.LearnedAtUtc + isSpec: "True" + fullName: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.LearnedAtUtc + nameWithType: DecryptFailureBypassEntry.LearnedAtUtc +- uid: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.Strikes + name: Strikes + href: api/Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html#Titanium_Web_Proxy_Models_DecryptFailureBypassEntry_Strikes + commentId: P:Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.Strikes + fullName: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.Strikes + nameWithType: DecryptFailureBypassEntry.Strikes +- uid: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.Strikes* + name: Strikes + href: api/Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html#Titanium_Web_Proxy_Models_DecryptFailureBypassEntry_Strikes_ + commentId: Overload:Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.Strikes + isSpec: "True" + fullName: Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.Strikes + nameWithType: DecryptFailureBypassEntry.Strikes - uid: Titanium.Web.Proxy.Models.ExplicitProxyEndPoint name: ExplicitProxyEndPoint href: api/Titanium.Web.Proxy.Models.ExplicitProxyEndPoint.html @@ -8819,6 +8909,19 @@ references: isSpec: "True" fullName: Titanium.Web.Proxy.ProxyServer.CheckCertificateRevocation nameWithType: ProxyServer.CheckCertificateRevocation +- uid: Titanium.Web.Proxy.ProxyServer.ClearDecryptFailureBypass + name: ClearDecryptFailureBypass() + href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_ClearDecryptFailureBypass + commentId: M:Titanium.Web.Proxy.ProxyServer.ClearDecryptFailureBypass + fullName: Titanium.Web.Proxy.ProxyServer.ClearDecryptFailureBypass() + nameWithType: ProxyServer.ClearDecryptFailureBypass() +- uid: Titanium.Web.Proxy.ProxyServer.ClearDecryptFailureBypass* + name: ClearDecryptFailureBypass + href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_ClearDecryptFailureBypass_ + commentId: Overload:Titanium.Web.Proxy.ProxyServer.ClearDecryptFailureBypass + isSpec: "True" + fullName: Titanium.Web.Proxy.ProxyServer.ClearDecryptFailureBypass + nameWithType: ProxyServer.ClearDecryptFailureBypass - uid: Titanium.Web.Proxy.ProxyServer.ClientCertificateSelectionCallback name: ClientCertificateSelectionCallback href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_ClientCertificateSelectionCallback @@ -8909,6 +9012,51 @@ references: isSpec: "True" fullName: Titanium.Web.Proxy.ProxyServer.CustomUpStreamProxyFailureFunc nameWithType: ProxyServer.CustomUpStreamProxyFailureFunc +- uid: Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassChanged + name: DecryptFailureBypassChanged + href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_DecryptFailureBypassChanged + commentId: E:Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassChanged + fullName: Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassChanged + nameWithType: ProxyServer.DecryptFailureBypassChanged +- uid: Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassMaxEntries + name: DecryptFailureBypassMaxEntries + href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_DecryptFailureBypassMaxEntries + commentId: P:Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassMaxEntries + fullName: Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassMaxEntries + nameWithType: ProxyServer.DecryptFailureBypassMaxEntries +- uid: Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassMaxEntries* + name: DecryptFailureBypassMaxEntries + href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_DecryptFailureBypassMaxEntries_ + commentId: Overload:Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassMaxEntries + isSpec: "True" + fullName: Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassMaxEntries + nameWithType: ProxyServer.DecryptFailureBypassMaxEntries +- uid: Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassThreshold + name: DecryptFailureBypassThreshold + href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_DecryptFailureBypassThreshold + commentId: P:Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassThreshold + fullName: Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassThreshold + nameWithType: ProxyServer.DecryptFailureBypassThreshold +- uid: Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassThreshold* + name: DecryptFailureBypassThreshold + href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_DecryptFailureBypassThreshold_ + commentId: Overload:Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassThreshold + isSpec: "True" + fullName: Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassThreshold + nameWithType: ProxyServer.DecryptFailureBypassThreshold +- uid: Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassTtl + name: DecryptFailureBypassTtl + href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_DecryptFailureBypassTtl + commentId: P:Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassTtl + fullName: Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassTtl + nameWithType: ProxyServer.DecryptFailureBypassTtl +- uid: Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassTtl* + name: DecryptFailureBypassTtl + href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_DecryptFailureBypassTtl_ + commentId: Overload:Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassTtl + isSpec: "True" + fullName: Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassTtl + nameWithType: ProxyServer.DecryptFailureBypassTtl - uid: Titanium.Web.Proxy.ProxyServer.DefaultViaHeaderPseudonym name: DefaultViaHeaderPseudonym href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_DefaultViaHeaderPseudonym @@ -9028,6 +9176,19 @@ references: isSpec: "True" fullName: Titanium.Web.Proxy.ProxyServer.EnableConnectionPool nameWithType: ProxyServer.EnableConnectionPool +- uid: Titanium.Web.Proxy.ProxyServer.EnableDecryptFailureBypass + name: EnableDecryptFailureBypass + href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_EnableDecryptFailureBypass + commentId: P:Titanium.Web.Proxy.ProxyServer.EnableDecryptFailureBypass + fullName: Titanium.Web.Proxy.ProxyServer.EnableDecryptFailureBypass + nameWithType: ProxyServer.EnableDecryptFailureBypass +- uid: Titanium.Web.Proxy.ProxyServer.EnableDecryptFailureBypass* + name: EnableDecryptFailureBypass + href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_EnableDecryptFailureBypass_ + commentId: Overload:Titanium.Web.Proxy.ProxyServer.EnableDecryptFailureBypass + isSpec: "True" + fullName: Titanium.Web.Proxy.ProxyServer.EnableDecryptFailureBypass + nameWithType: ProxyServer.EnableDecryptFailureBypass - uid: Titanium.Web.Proxy.ProxyServer.EnableHttp2 name: EnableHttp2 href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_EnableHttp2 @@ -9210,6 +9371,19 @@ references: isSpec: "True" fullName: Titanium.Web.Proxy.ProxyServer.GetCustomUpStreamProxyFunc nameWithType: ProxyServer.GetCustomUpStreamProxyFunc +- uid: Titanium.Web.Proxy.ProxyServer.GetDecryptFailureBypassEntries + name: GetDecryptFailureBypassEntries() + href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_GetDecryptFailureBypassEntries + commentId: M:Titanium.Web.Proxy.ProxyServer.GetDecryptFailureBypassEntries + fullName: Titanium.Web.Proxy.ProxyServer.GetDecryptFailureBypassEntries() + nameWithType: ProxyServer.GetDecryptFailureBypassEntries() +- uid: Titanium.Web.Proxy.ProxyServer.GetDecryptFailureBypassEntries* + name: GetDecryptFailureBypassEntries + href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_GetDecryptFailureBypassEntries_ + commentId: Overload:Titanium.Web.Proxy.ProxyServer.GetDecryptFailureBypassEntries + isSpec: "True" + fullName: Titanium.Web.Proxy.ProxyServer.GetDecryptFailureBypassEntries + nameWithType: ProxyServer.GetDecryptFailureBypassEntries - uid: Titanium.Web.Proxy.ProxyServer.GlobalAdmissionRejectionCount name: GlobalAdmissionRejectionCount href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_GlobalAdmissionRejectionCount @@ -9584,6 +9758,22 @@ references: isSpec: "True" fullName: Titanium.Web.Proxy.ProxyServer.ProxySchemeAuthenticateFunc nameWithType: ProxyServer.ProxySchemeAuthenticateFunc +- uid: Titanium.Web.Proxy.ProxyServer.RemoveDecryptFailureBypass(System.String) + name: RemoveDecryptFailureBypass(string) + href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_RemoveDecryptFailureBypass_System_String_ + commentId: M:Titanium.Web.Proxy.ProxyServer.RemoveDecryptFailureBypass(System.String) + name.vb: RemoveDecryptFailureBypass(String) + fullName: Titanium.Web.Proxy.ProxyServer.RemoveDecryptFailureBypass(string) + fullName.vb: Titanium.Web.Proxy.ProxyServer.RemoveDecryptFailureBypass(String) + nameWithType: ProxyServer.RemoveDecryptFailureBypass(string) + nameWithType.vb: ProxyServer.RemoveDecryptFailureBypass(String) +- uid: Titanium.Web.Proxy.ProxyServer.RemoveDecryptFailureBypass* + name: RemoveDecryptFailureBypass + href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_RemoveDecryptFailureBypass_ + commentId: Overload:Titanium.Web.Proxy.ProxyServer.RemoveDecryptFailureBypass + isSpec: "True" + fullName: Titanium.Web.Proxy.ProxyServer.RemoveDecryptFailureBypass + nameWithType: ProxyServer.RemoveDecryptFailureBypass - uid: Titanium.Web.Proxy.ProxyServer.RemoveEndPoint(Titanium.Web.Proxy.Models.ProxyEndPoint) name: RemoveEndPoint(ProxyEndPoint) href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_RemoveEndPoint_Titanium_Web_Proxy_Models_ProxyEndPoint_ @@ -9776,6 +9966,22 @@ references: isSpec: "True" fullName: Titanium.Web.Proxy.ProxyServer.SetHttp3Enabled nameWithType: ProxyServer.SetHttp3Enabled +- uid: Titanium.Web.Proxy.ProxyServer.ShouldBypassDecryptForLearnedHost(System.String) + name: ShouldBypassDecryptForLearnedHost(string?) + href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_ShouldBypassDecryptForLearnedHost_System_String_ + commentId: M:Titanium.Web.Proxy.ProxyServer.ShouldBypassDecryptForLearnedHost(System.String) + name.vb: ShouldBypassDecryptForLearnedHost(String) + fullName: Titanium.Web.Proxy.ProxyServer.ShouldBypassDecryptForLearnedHost(string?) + fullName.vb: Titanium.Web.Proxy.ProxyServer.ShouldBypassDecryptForLearnedHost(String) + nameWithType: ProxyServer.ShouldBypassDecryptForLearnedHost(string?) + nameWithType.vb: ProxyServer.ShouldBypassDecryptForLearnedHost(String) +- uid: Titanium.Web.Proxy.ProxyServer.ShouldBypassDecryptForLearnedHost* + name: ShouldBypassDecryptForLearnedHost + href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_ShouldBypassDecryptForLearnedHost_ + commentId: Overload:Titanium.Web.Proxy.ProxyServer.ShouldBypassDecryptForLearnedHost + isSpec: "True" + fullName: Titanium.Web.Proxy.ProxyServer.ShouldBypassDecryptForLearnedHost + nameWithType: ProxyServer.ShouldBypassDecryptForLearnedHost - uid: Titanium.Web.Proxy.ProxyServer.ShouldInterceptHttp name: ShouldInterceptHttp href: api/Titanium.Web.Proxy.ProxyServer.html#Titanium_Web_Proxy_ProxyServer_ShouldInterceptHttp diff --git a/src/Titanium.Cli/Titanium.Cli.csproj b/src/Titanium.Cli/Titanium.Cli.csproj index 526d649be..f84701d7d 100644 --- a/src/Titanium.Cli/Titanium.Cli.csproj +++ b/src/Titanium.Cli/Titanium.Cli.csproj @@ -7,7 +7,7 @@ latest enable false - 7.0.6 + 7.0.7 Jehonathan Thomas Titanium Web Proxy CLI (titanium / twp). MIT diff --git a/src/Titanium.Inspector/Services/ExclusionPreview.cs b/src/Titanium.Inspector/Services/ExclusionPreview.cs index 1d5e6c0f0..202c26811 100644 --- a/src/Titanium.Inspector/Services/ExclusionPreview.cs +++ b/src/Titanium.Inspector/Services/ExclusionPreview.cs @@ -1,53 +1,23 @@ -using System.Runtime.InteropServices; -using Titanium.Web.Proxy; -using Titanium.Web.Proxy.Helpers; - namespace Titanium.Inspector.Services; -/// Formats effective OS proxy bypass lists for the exclusions UI. +/// Formats exclusion summaries and opaque-tunnel reasons for Inspector UI. public static class ExclusionPreview { - public static string BuildWinInetOverride(InspectorSettings settings, string? currentOverride = null) - { - var proxySettings = MitmBypass.CreateSystemProxySettings(settings); - return proxySettings.BuildProxyOverride(currentOverride); - } - - public static (string Label, string Value) FormatForCurrentOs( - InspectorSettings settings, - string? currentOverride = null) - { - var winInet = BuildWinInetOverride(settings, currentOverride); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return ("WinINET bypass list", winInet); - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return ("Proxy bypass domains (networksetup)", UnixProxyBypassMapper.ToCommaSeparated(winInet)); - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - var gsettings = UnixProxyBypassMapper.ToGsettingsArray(winInet); - var noProxy = UnixProxyBypassMapper.ToNoProxyEnv(winInet); - return ("Ignored hosts / NO_PROXY", $"gsettings: {gsettings}\nNO_PROXY={noProxy}"); - } - - return ("Bypass list", winInet); - } - - public static string ExclusionSummary(InspectorSettings settings) + public static string ExclusionSummary(InspectorSettings settings, int learnedCount = 0) { var bypass = settings.SystemProxyBypassHosts?.Count(h => !string.IsNullOrWhiteSpace(h)) ?? 0; var tunnel = settings.DecryptSkipHosts?.Count(h => !string.IsNullOrWhiteSpace(h)) ?? 0; - if (bypass == 0 && tunnel == 0) + if (bypass == 0 && tunnel == 0 && learnedCount == 0) { return ""; } - return $"Exclusions: {bypass} OS bypass, {tunnel} tunnel-only"; + var parts = new List(); + if (bypass > 0 || tunnel > 0) + parts.Add($"Exclusions: {bypass} OS bypass, {tunnel} tunnel-only"); + if (learnedCount > 0) + parts.Add($"Learned: {learnedCount}"); + return string.Join(" · ", parts); } public static string DescribeOpaqueReason(OpaqueTunnelReason reason) => reason switch @@ -57,6 +27,7 @@ public static string ExclusionSummary(InspectorSettings settings) OpaqueTunnelReason.BuiltInPinning => "Encrypted: pinning host (tunnel only)", OpaqueTunnelReason.UserSkipList => "Encrypted: tunnel-only exclusion list", OpaqueTunnelReason.UserOnlyList => "Encrypted: not on decrypt-only allowlist", + OpaqueTunnelReason.LearnedFailure => "Encrypted: auto-tunneled after decrypt failure", _ => "", }; } diff --git a/src/Titanium.Inspector/Services/InterceptionService.cs b/src/Titanium.Inspector/Services/InterceptionService.cs index d25fb5331..809cdfd59 100644 --- a/src/Titanium.Inspector/Services/InterceptionService.cs +++ b/src/Titanium.Inspector/Services/InterceptionService.cs @@ -59,6 +59,12 @@ public InterceptionService(ISystemProxyController? systemProxy = null) /// public bool DecryptHttps { get; set; } + /// + /// When true, origin TLS handshake failures under MITM train an in-memory host bypass. + /// Default on for Inspector; wired to . + /// + public bool EnableDecryptFailureBypass { get; set; } = true; + /// Extra host patterns that skip HTTPS decryption (in addition to built-in bypasses). public List DecryptSkipHosts { get; set; } = []; @@ -154,6 +160,34 @@ public bool AddViaHeader public event EventHandler? SessionCaptured; public event EventHandler? SessionUpdated; + public event EventHandler? DecryptFailureBypassLearned; + + /// Applies the learning toggle to a running proxy (no-op when not started). + public void ApplyDecryptFailureBypassSetting() + { + if (_proxy is null) + return; + _proxy.EnableDecryptFailureBypass = EnableDecryptFailureBypass; + } + + public IReadOnlyList GetDecryptFailureBypassEntries() => + _proxy?.GetDecryptFailureBypassEntries() ?? Array.Empty(); + + public bool RemoveDecryptFailureBypass(string host) => + _proxy?.RemoveDecryptFailureBypass(host) ?? false; + + public void ClearDecryptFailureBypass() => _proxy?.ClearDecryptFailureBypass(); + + private bool IsLearnedDecryptBypass(string? host) + { + if (!EnableDecryptFailureBypass || _proxy is null || string.IsNullOrWhiteSpace(host)) + return false; + // O(1) cache consult — do not Snapshot the full list on every CONNECT. + return _proxy.ShouldBypassDecryptForLearnedHost(host); + } + + private void OnDecryptFailureBypassChanged(object? sender, DecryptFailureBypassEntry e) => + DecryptFailureBypassLearned?.Invoke(this, e); public async Task StartAsync(IPAddress address, int port, CancellationToken cancellationToken = default) { @@ -171,6 +205,8 @@ public async Task StartAsync(IPAddress address, int port, CancellationToken canc ApplyLoggingOptions(_loggingSettings); _proxy.EnableHttpInterception = true; _proxy.EnableRequestTimingCapture = true; + _proxy.EnableDecryptFailureBypass = EnableDecryptFailureBypass; + _proxy.DecryptFailureBypassChanged += OnDecryptFailureBypassChanged; ApplyViaHeaderOption(); // Inspector eagerly buffers bodies for the session grid; 4 MiB trips too often on // normal browsing (images, JS bundles) and RST'd the H2 stream. 32 MiB still bounds @@ -390,6 +426,7 @@ public void Stop() _proxy.OnRequestBodyWrite -= OnRequestBodyWriteThrottle; _proxy.OnResponseBodyWrite -= OnResponseBodyWriteThrottle; _proxy.ServerCertificateValidationCallback -= OnServerCertValidation; + _proxy.DecryptFailureBypassChanged -= OnDecryptFailureBypassChanged; if (_endPoint is not null) { _endPoint.BeforeTunnelConnectRequest -= OnBeforeTunnelConnect; @@ -954,10 +991,13 @@ private Task OnBeforeTunnelConnect(object sender, TunnelConnectSessionEventArgs host, DecryptSkipHosts, userOnlyHosts: null); - e.DecryptSsl = DecryptHttps && !disableDecrypt; - var opaqueReason = disableDecrypt || !DecryptHttps - ? MitmBypass.ResolveOpaqueReason(host, DecryptHttps, DecryptSkipHosts, userOnlyHosts: null) - : OpaqueTunnelReason.None; + var learnedBypass = !disableDecrypt && DecryptHttps && IsLearnedDecryptBypass(host); + e.DecryptSsl = DecryptHttps && !disableDecrypt && !learnedBypass; + var opaqueReason = learnedBypass + ? OpaqueTunnelReason.LearnedFailure + : disableDecrypt || !DecryptHttps + ? MitmBypass.ResolveOpaqueReason(host, DecryptHttps, DecryptSkipHosts, userOnlyHosts: null) + : OpaqueTunnelReason.None; if (!Capturing) { diff --git a/src/Titanium.Inspector/Services/OpaqueTunnelReason.cs b/src/Titanium.Inspector/Services/OpaqueTunnelReason.cs index 52d5ebee3..a6381fb2f 100644 --- a/src/Titanium.Inspector/Services/OpaqueTunnelReason.cs +++ b/src/Titanium.Inspector/Services/OpaqueTunnelReason.cs @@ -9,4 +9,5 @@ public enum OpaqueTunnelReason BuiltInPinning, UserSkipList, UserOnlyList, + LearnedFailure, } diff --git a/src/Titanium.Inspector/Services/OsTrustUxCopy.cs b/src/Titanium.Inspector/Services/OsTrustUxCopy.cs index d3db2e606..e5d13a3e0 100644 --- a/src/Titanium.Inspector/Services/OsTrustUxCopy.cs +++ b/src/Titanium.Inspector/Services/OsTrustUxCopy.cs @@ -94,15 +94,15 @@ public static string ExcludedHostsIntro() return "OS bypass needs Capture → System proxy. Tunnel-only rules apply to every client that hits Inspector. Factory defaults are seeded into the lists below — edit freely or reset."; } - public static string ExcludedHostsLoopbackHint() + public static string ProxyLocalhostTip() { if (OperatingSystem.IsMacOS()) - return "When off, localhost is omitted from the macOS proxy bypass list so loopback can use the system proxy."; + return "When System proxy is on, send localhost through Inspector. Off adds localhost to the macOS proxy bypass list."; if (OperatingSystem.IsLinux()) - return "When off, localhost is omitted from NO_PROXY so loopback can use the system proxy."; + return "When System proxy is on, send localhost through Inspector. Off adds localhost to NO_PROXY."; if (OperatingSystem.IsWindows()) - return "When off, adds the Windows <-loopback> bypass rule so loopback skips the system proxy."; - return "Controls whether localhost traffic uses the system proxy."; + return "When System proxy is on, send localhost through Inspector (WinINET <-loopback>). Off lets loopback skip the proxy."; + return "When System proxy is on, send localhost through Inspector."; } public static string FormatStatus(CertificateOsTrustResult? result) diff --git a/src/Titanium.Inspector/Services/SessionSearch.cs b/src/Titanium.Inspector/Services/SessionSearch.cs index 7f65aef72..41ca5ac5a 100644 --- a/src/Titanium.Inspector/Services/SessionSearch.cs +++ b/src/Titanium.Inspector/Services/SessionSearch.cs @@ -387,6 +387,7 @@ private static bool MatchOpaqueReason(SessionSnapshot s, string reasonToken) "skip" or "skiplist" => s.OpaqueReason == OpaqueTunnelReason.UserSkipList, "only" or "onlylist" => s.OpaqueReason == OpaqueTunnelReason.UserOnlyList, "decrypt-off" or "decryptoff" => s.OpaqueReason == OpaqueTunnelReason.DecryptOff, + "learned" or "auto" => s.OpaqueReason == OpaqueTunnelReason.LearnedFailure, _ => s.OpaqueReason.ToString().Equals(reasonToken, StringComparison.OrdinalIgnoreCase), }; } diff --git a/src/Titanium.Inspector/Services/SettingsService.cs b/src/Titanium.Inspector/Services/SettingsService.cs index 2f7b638c0..adc2e7602 100644 --- a/src/Titanium.Inspector/Services/SettingsService.cs +++ b/src/Titanium.Inspector/Services/SettingsService.cs @@ -131,6 +131,12 @@ public sealed class InspectorSettings /// When true, localhost uses the proxy (WinINET <-loopback> / Unix NO_PROXY parity). public bool ProxyLoopback { get; set; } = true; + /// + /// When true, Inspector auto-tunnels hosts whose origin TLS fails under decrypt (learned, session-only). + /// Default on. + /// + public bool EnableDecryptFailureBypass { get; set; } = true; + /// /// When true, and were seeded /// from factory defaults (or saved by the user). When false, load applies factory seed once. @@ -194,7 +200,7 @@ public bool EnsureExclusionsSeeded() return true; } - /// Restores factory OS-bypass and tunnel-only lists (and loopback). + /// Restores factory OS-bypass and tunnel-only lists. public void ResetExclusionsToFactoryDefaults() { ApplyFactoryExclusionDefaults(Current); @@ -207,7 +213,6 @@ public static void ApplyFactoryExclusionDefaults(InspectorSettings settings) { settings.SystemProxyBypassHosts = MitmExclusionDefaults.SystemProxyBypassRules.ToList(); settings.DecryptSkipHosts = MitmExclusionDefaults.TunnelOnlyPinningDomains.ToList(); - settings.ProxyLoopback = true; } /// diff --git a/src/Titanium.Inspector/Services/UpdateService.cs b/src/Titanium.Inspector/Services/UpdateService.cs index b16195589..9fbffa8d7 100644 --- a/src/Titanium.Inspector/Services/UpdateService.cs +++ b/src/Titanium.Inspector/Services/UpdateService.cs @@ -522,23 +522,34 @@ public static bool IsMsiInstall(string baseDirectory) return true; } - try + if (HasInspectorInstallMarker(Registry.CurrentUser) + || HasInspectorInstallMarker(Registry.LocalMachine)) { - using var key = Registry.CurrentUser.OpenSubKey(@"Software\justcoding121\TitaniumInspector"); - if (key?.GetValue("installed") is not null) - { - return true; - } - } - catch - { - // ignore registry access issues + return true; } } return false; } + private static bool HasInspectorInstallMarker(RegistryKey hive) + { + if (!OperatingSystem.IsWindows()) + { + return false; + } + + try + { + using var key = hive.OpenSubKey(@"Software\justcoding121\TitaniumInspector"); + return key?.GetValue("installed") is not null; + } + catch + { + return false; + } + } + public static string SuggestRid() { var arm = RuntimeInformation.OSArchitecture is Architecture.Arm64 or Architecture.Arm; diff --git a/src/Titanium.Inspector/Titanium.Inspector.csproj b/src/Titanium.Inspector/Titanium.Inspector.csproj index 13618670b..46684494e 100644 --- a/src/Titanium.Inspector/Titanium.Inspector.csproj +++ b/src/Titanium.Inspector/Titanium.Inspector.csproj @@ -8,7 +8,7 @@ enable true false - 7.0.6 + 7.0.7 Jehonathan Thomas Titanium Inspector desktop traffic debugger (PolyForm Noncommercial). LICENSE diff --git a/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs b/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs index db56c56e9..ce4d0bce7 100644 --- a/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs +++ b/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs @@ -225,6 +225,11 @@ public MainWindowViewModel(InspectorViewModelServices services) AddViaHeader = !AddViaHeader; return Task.CompletedTask; }); + ToggleProxyLocalhostCommand = Cmd(() => + { + ProxyLoopback = !ProxyLoopback; + return Task.CompletedTask; + }); _clearSessionsCommand = Cmd(ClearSessionsAsync, () => HasSessions); ClearSessionsCommand = _clearSessionsCommand; _removeSelectedSessionsCommand = Cmd(RemoveSelectedSessionsAsync, () => HasSelectedSessions); @@ -655,6 +660,13 @@ private void WireSessionPipelineHandlers() RefreshSelectedInspectors(); } }); + _interception.DecryptFailureBypassLearned += (_, entry) => + MarshalToUi(() => + { + StatusText = + $"Auto-tunneled {entry.Host} (decrypt failure). New connections skip MITM."; + UpdateExclusionSummary(); + }); } /// @@ -928,7 +940,8 @@ private async Task OpenExcludedHostsAsync() owner, _settings, readOnly: false, - ApplyExclusionSettingsFromSettings)); + ApplyExclusionSettingsFromSettings, + _interception)); if (saved) { if (SystemProxy && !_interception.ReapplySystemProxyIfEnabled()) @@ -1006,13 +1019,16 @@ private void ApplyExclusionSettingsFromSettings() _interception.DecryptOnlyHosts = s.DecryptOnlyHosts?.ToList() ?? []; _interception.SystemProxyBypassHosts = s.SystemProxyBypassHosts?.ToList() ?? []; _interception.ProxyLoopback = s.ProxyLoopback; + _interception.EnableDecryptFailureBypass = s.EnableDecryptFailureBypass; + _interception.ApplyDecryptFailureBypassSetting(); _interception.SystemProxySettings = s; UpdateExclusionSummary(); } private void UpdateExclusionSummary() { - ExclusionSummaryText = ExclusionPreview.ExclusionSummary(_settings.Current); + var learned = _interception.GetDecryptFailureBypassEntries().Count(e => e.BypassActive); + ExclusionSummaryText = ExclusionPreview.ExclusionSummary(_settings.Current, learned); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ExclusionSummaryText))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasExclusionSummary))); } @@ -1137,6 +1153,7 @@ private Task ApplyEditBodyAsync() public ICommand ToggleDecryptHttpsCommand { get; } public ICommand ToggleIgnoreServerCertificateErrorsCommand { get; } public ICommand ToggleAddViaHeaderCommand { get; } + public ICommand ToggleProxyLocalhostCommand { get; } public ICommand ClearSessionsCommand { get; } public ICommand RemoveSelectedSessionsCommand { get; } public ICommand ToggleSystemProxyCommand { get; } @@ -1426,6 +1443,36 @@ private void SetSystemProxyCore(bool enabled) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SystemProxy))); } + /// When true, localhost uses the system proxy (WinINET <-loopback> / Unix NO_PROXY parity). + public bool ProxyLoopback + { + get => _interception.ProxyLoopback; + set + { + if (_interception.ProxyLoopback == value) + { + return; + } + + _interception.ProxyLoopback = value; + PersistSettings(); + _interception.SystemProxySettings = _settings.Current; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ProxyLoopback))); + + if (SystemProxy && !_interception.ReapplySystemProxyIfEnabled()) + { + StatusText = "Proxy localhost saved; re-toggle System proxy to apply"; + return; + } + + StatusText = value + ? "Proxy localhost on — loopback uses the system proxy" + : "Proxy localhost off — loopback skips the system proxy"; + } + } + + public string ProxyLocalhostTip => OsTrustUxCopy.ProxyLocalhostTip(); + public bool AutoStartCapture { get => _autoStartCapture; @@ -1889,6 +1936,7 @@ private void NotifySettingsUiChanged() PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AutoStartCapture))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AutoSystemProxyOnStart))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DecryptHttps))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ProxyLoopback))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IgnoreServerCertificateErrors))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AddViaHeader))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BreakpointOnResponse))); @@ -1954,6 +2002,7 @@ private void PersistSettings() s.AutoStartCapture = AutoStartCapture; s.AutoSystemProxyOnStart = AutoSystemProxyOnStart; s.DecryptHttps = DecryptHttps; + s.ProxyLoopback = _interception.ProxyLoopback; s.IgnoreServerCertificateErrors = _interception.IgnoreServerCertificateErrors; s.AddViaHeader = _interception.AddViaHeader; s.AutoResponderEnabled = AutoResponder.Enabled; diff --git a/src/Titanium.Inspector/Views/ExcludedHostsWindow.axaml b/src/Titanium.Inspector/Views/ExcludedHostsWindow.axaml index 732f89d23..3715bf2c4 100644 --- a/src/Titanium.Inspector/Views/ExcludedHostsWindow.axaml +++ b/src/Titanium.Inspector/Views/ExcludedHostsWindow.axaml @@ -3,7 +3,7 @@ x:Class="Titanium.Inspector.Views.ExcludedHostsWindow" AutomationProperties.AutomationId="ExcludedHostsWindow" Title="Excluded hosts" - Width="640" Height="680" + Width="640" Height="600" WindowStartupLocation="CenterOwner"> @@ -36,19 +36,24 @@ FontFamily="Consolas,Courier New,monospace" AutomationProperties.AutomationId="ExcludedSkipHosts" /> - - - - - - - - - + + + + + + + + internal bool IsFastPath { get; set; } + /// + /// When true, tear down the client TCP after the response is committed (seamless decrypt-bypass + /// meta-refresh must not keep an H2/MITM connection alive). + /// + internal bool CloseClientConnectionAfterResponse { get; set; } + /// /// Native HTTP/3 only: reads remaining client DATA frames into a bounded buffer (wire bytes). /// Set by Http3RequestStream before BeforeRequest; cleared once the body is @@ -375,6 +382,14 @@ private async Task ReadResponseBodyAsync(CancellationToken cancellationToken) // If not already read (not cached yet) if (!response.IsBodyRead) { + // Synthetic Ok/Respond may already have Body bytes without going through the wire. + if (response.BodyAvailable) + { + response.IsBodyRead = true; + response.IsBodyReceived = true; + return; + } + if (response.IsBodyReceived) throw new InvalidOperationException("Response body was already received."); if (response.HttpVersion == HttpHeader.Version20) @@ -862,9 +877,14 @@ public void Respond(Response response, bool closeServerConnection = false) response.SetOriginalHeaders(HttpClient.Response); + // Suppress origin DATA while synthetic emission is queued (H2 frame loop reads + // HttpClient.Response.Http2IgnoreBodyFrames on each DATA frame). + HttpClient.Response.Http2IgnoreBodyFrames = true; + // response already received from server but not yet ready to sent to client. HttpClient.Response = response; HttpClient.Response.Locked = true; + HttpClient.Response.Http2IgnoreBodyFrames = true; } // request not yet sent/not yet ready to be sent. else @@ -876,6 +896,17 @@ public void Respond(Response response, bool closeServerConnection = false) HttpClient.Response = response; HttpClient.Response.Locked = true; } + + // Buffered synthetics already carry the body; mark read so H2 GetResponseBody does not + // wait on ReadHttp2BeforeHandlerTaskCompletionSource (null after replacement). + // Do not set IsBodyReceived when replacing an origin response — SyphonOutBodyAsync must + // still drain unread origin bytes so pooled H1 connections stay reusable. + if (HttpClient.Response.BodyAvailable) + { + HttpClient.Response.IsBodyRead = true; + if (HttpClient.Request.CancelRequest) + HttpClient.Response.IsBodyReceived = true; + } } /// @@ -933,6 +964,7 @@ private void ApplySyntheticResponseDefaults(Response response) { if (response.HttpVersion == HttpHeader.VersionUnknown) response.HttpVersion = HttpClient.Request.HttpVersion; + response.IsSynthetic = true; } /// diff --git a/src/Titanium.Web.Proxy/Extensions/SslExtensions.cs b/src/Titanium.Web.Proxy/Extensions/SslExtensions.cs index efe29f1b9..09462b310 100644 --- a/src/Titanium.Web.Proxy/Extensions/SslExtensions.cs +++ b/src/Titanium.Web.Proxy/Extensions/SslExtensions.cs @@ -19,6 +19,14 @@ internal static class SslExtensions internal static readonly List Http2ProtocolAsList = new() { SslApplicationProtocol.Http2 }; + /// + /// Safe browser-facing ALPN when HTTP/2 should be offered: always include HTTP/1.1 so a + /// mismatched or partially-parsed ClientHello cannot fail AuthenticateAsServer with + /// SEC_E_NO_APPLICATION_PROTOCOL (0x80090367). + /// + internal static readonly List Http2AndHttp11ProtocolAsList = + new() { SslApplicationProtocol.Http2, SslApplicationProtocol.Http11 }; + internal static string? GetServerName(this ClientHelloInfo clientHelloInfo) { if (clientHelloInfo.Extensions != null && diff --git a/src/Titanium.Web.Proxy/Handlers/ExplicitClientHandler.cs b/src/Titanium.Web.Proxy/Handlers/ExplicitClientHandler.cs index b00a0faff..b827c527e 100644 --- a/src/Titanium.Web.Proxy/Handlers/ExplicitClientHandler.cs +++ b/src/Titanium.Web.Proxy/Handlers/ExplicitClientHandler.cs @@ -93,6 +93,13 @@ public partial class ProxyServer // filter out excluded host names var decryptSsl = endPoint.DecryptSsl && connectArgs.DecryptSsl; + var (bypassCheckHost, _) = ParseHostAndPort(requestLine.RequestUri.GetString(), 443); + if (decryptSsl && ShouldBypassDecryptForLearnedHost(bypassCheckHost)) + { + decryptSsl = false; + connectArgs.DecryptSsl = false; + } + var sendRawData = !decryptSsl; if (connectArgs.DenyConnect) @@ -256,6 +263,25 @@ public partial class ProxyServer http2Supported = (negotiation.OriginSupportsHttp2 && !requiresH2OriginBridge) || requiresHttp11Bridge; prefetchConnectionTask = negotiation.RetainedConnectionTask; + + // Same-CONNECT opaque fallback: awaited cold probe failed with learnable origin TLS + // (e.g. fingerprint). Client is still waiting for ServerHello — do not MITM. + if (EnableDecryptFailureBypass && negotiation.LearnableOriginTlsFailure) + { + TryRecordDecryptFailure(connectHost, error: null, forceBypass: true); + // Prefetch is not started on learnable probe failure; drain any race without + // blocking ClientHello relay on a doomed MITM handshake. + var doomedPrefetch = prefetchConnectionTask; + prefetchConnectionTask = null; + if (doomedPrefetch != null) + _ = TcpConnectionFactory.Release(doomedPrefetch, true); + sendRawData = true; + connectArgs.DecryptSsl = false; + // Learned-at-start opaque path never sets IsHttps; clear so GetServerConnection + // opens raw TCP and splices the peeked ClientHello (not a third origin TLS). + if (connectArgs.HttpClient.ConnectRequest != null) + connectArgs.HttpClient.ConnectRequest.IsHttps = false; + } } // Skip the generic single-connection prefetch entirely when the session will be routed @@ -265,7 +291,7 @@ public partial class ProxyServer // worse, using http2Supported (true in the bridge case, so the client can be offered // "h2") to pick the prefetch's ALPN offer would incorrectly probe the origin - which this // policy pins to HTTP/1.1 - with "h2" too. - if (prefetchConnectionTask == null && EnableTcpServerConnectionPrefetch + if (!sendRawData && prefetchConnectionTask == null && EnableTcpServerConnectionPrefetch && !requiresHttp11Bridge && !requiresH3Bridge) // don't pass cancellation token here // it could cause floating server connections when client exits. @@ -280,6 +306,8 @@ public partial class ProxyServer // connectHostname and certGenerationTask were prepared above before the // DNS/H2 probes so cert generation could run in parallel with those probes. + if (!sendRawData) + { X509Certificate2? certificate = null; SslStream? sslStream = null; try @@ -296,12 +324,11 @@ public partial class ProxyServer // Offer h2 whenever capability negotiation / H3 bridging decided the client // should see it — including EnableHttp2=false + H3 bridge, which still speaks // h2 on the browser leg. - if (http2Supported) - { - options.ApplicationProtocols = clientHelloInfo.GetAlpn(); - if (options.ApplicationProtocols == null || options.ApplicationProtocols.Count == 0) - options.ApplicationProtocols = SslExtensions.Http11ProtocolAsList; - } + // Offer a fixed safe ALPN set rather than mirroring a possibly truncated + // ClientHello peek (large PQ hellos). Always include http/1.1 when offering h2. + options.ApplicationProtocols = http2Supported + ? SslExtensions.Http2AndHttp11ProtocolAsList + : SslExtensions.Http11ProtocolAsList; options.ServerCertificateContext = CertificateManager.CreateSslCertificateContext(certificate); options.ClientCertificateRequired = false; @@ -358,6 +385,7 @@ public partial class ProxyServer await TcpConnectionFactory.Release(prefetchConnectionTask, true); prefetchConnectionTask = null; } + } // !sendRawData (MITM) } else if (clientHelloInfo == null) { diff --git a/src/Titanium.Web.Proxy/Handlers/Http2NegotiationHandler.cs b/src/Titanium.Web.Proxy/Handlers/Http2NegotiationHandler.cs index 84d074b8e..85fe7907a 100644 --- a/src/Titanium.Web.Proxy/Handlers/Http2NegotiationHandler.cs +++ b/src/Titanium.Web.Proxy/Handlers/Http2NegotiationHandler.cs @@ -72,38 +72,112 @@ private async Task NegotiateHttp2Async(SessionEventArgsB var capabilityCacheKey = TcpConnectionFactory.GetConnectionCacheKey(remoteHostName, remotePort, true, null, upStreamEndPoint, externalProxy, connectHost, connectPort); - if (Http2OriginCapabilityCache.TryGet(capabilityCacheKey, out var cachedSupport)) + var learnableOriginTlsFailure = false; + TcpServerConnection? adoptedColdProbe = null; + if (!Http2OriginCapabilityCache.TryGet(capabilityCacheKey, out var cachedSupport)) + { + Diagnostics.ProxyMetrics.Http2CapabilityLookup(cacheHit: false); + // Coalesce concurrent cold probes: browsers open many CONNECTs to the same host at once. + // Each used to block AuthenticateAsServer on its own origin TLS probe; Chrome then aborted + // waiting tunnels (EOF / "Couldn't authenticate host"). One in-flight probe feeds them all. + // Only the probe leader adopts the discovery socket as the session connection; waiters + // receive the cached bool and open their own connection when needed. + (cachedSupport, learnableOriginTlsFailure, adoptedColdProbe) = + await CoalesceHttp2CapabilityProbeAsync(capabilityCacheKey, sessionArgs, remoteHostName, + remotePort, connectHost, connectPort, upStreamEndPoint, externalProxy, + cancellationToken); + } + else { Diagnostics.ProxyMetrics.Http2CapabilityLookup(cacheHit: true); ProxyLog.Http2ProbeResult(logger, capabilityCacheKey, true, cachedSupport, null); - - Task? retained = null; - if (enablePrefetch) - // Correctly keyed up front, so a cache hit never needs a separate discovery connection: - // once validated by the caller, this single connection becomes the session connection - // instead of being opened, checked, and then wastefully discarded. - // Don't pass cancellationToken here - it could leave a floating server connection if the - // client disconnects before this completes. - retained = TcpConnectionFactory.GetServerConnection(this, remoteHostName, remotePort, - HttpHeader.Version20, true, cachedSupport ? SslExtensions.Http2ProtocolAsList : null, true, - sessionArgs, upStreamEndPoint, externalProxy, false, true, CancellationToken.None, - connectHost, connectPort); - - return new Http2NegotiationResult(cachedSupport, retained); } - Diagnostics.ProxyMetrics.Http2CapabilityLookup(cacheHit: false); + Task? retained = null; + // Do not start a MITM session prefetch when the cold probe already showed a learnable TLS + // failure — same-CONNECT opaque fallback will discard it; awaiting that handshake only + // delays ClientHello relay. + if (adoptedColdProbe != null && !learnableOriginTlsFailure) + // Cold-cache leader: the ALPN discovery connection is already correctly keyed — reuse it + // as the session connection (same as pre-coalesce behavior) so we do not open a second + // origin socket for this tunnel. + retained = Task.FromResult(adoptedColdProbe); + else if (enablePrefetch && !learnableOriginTlsFailure) + // Cache hit or coalesce waiter: correctly keyed prefetch started while the client TLS + // handshake is still in progress becomes the session connection. + // Don't pass cancellationToken here - it could leave a floating server connection if the + // client disconnects before this completes. + retained = TcpConnectionFactory.GetServerConnection(this, remoteHostName, remotePort, + HttpHeader.Version20, true, cachedSupport ? SslExtensions.Http2ProtocolAsList : null, true, + sessionArgs, upStreamEndPoint, externalProxy, false, true, CancellationToken.None, + connectHost, connectPort); + else if (adoptedColdProbe != null) + await TcpConnectionFactory.Release(adoptedColdProbe, true).ConfigureAwait(false); + + return new Http2NegotiationResult(cachedSupport, retained, + learnableOriginTlsFailure: learnableOriginTlsFailure); + } + + /// + /// Runs at most one cold HTTP/2 ALPN probe per capability-cache key; concurrent callers await the + /// same capability result. Definitive ALPN rejection (SEC_E_NO_APPLICATION_PROTOCOL) is + /// cached as unsupported; transient network/cert failures are not cached. Only the probe leader + /// receives the discovery connection for session adoption; waiters get + /// and must open their own connection. + /// + private async Task<(bool Supported, bool LearnableFailure, TcpServerConnection? AdoptedProbe)> + CoalesceHttp2CapabilityProbeAsync( + string capabilityCacheKey, SessionEventArgsBase sessionArgs, string remoteHostName, int remotePort, + string? connectHost, int? connectPort, IPEndPoint? upStreamEndPoint, IExternalProxy? externalProxy, + CancellationToken cancellationToken) + { + while (true) + { + if (Http2OriginCapabilityCache.TryGet(capabilityCacheKey, out var racedCache)) + return (racedCache, false, null); + + var probeTcs = new TaskCompletionSource<(bool Supported, bool LearnableFailure)>( + TaskCreationOptions.RunContinuationsAsynchronously); + var probeTask = pendingHttp2CapabilityProbes.GetOrAdd(capabilityCacheKey, probeTcs.Task); + if (!ReferenceEquals(probeTask, probeTcs.Task)) + { + var (supported, learnable) = await probeTask.ConfigureAwait(false); + return (supported, learnable, null); + } - // Cold cache: client ALPN advertisement depends on origin capability, so this single discovery - // connection must be awaited before the client is authenticated. It doubles as the capability - // probe and, on success, the retained connection reused for the session that follows - replacing - // what used to be up to three separate origin connections (probe, prefetch, session) with one. - var probeStarted = System.Diagnostics.Stopwatch.StartNew(); + var probeStarted = System.Diagnostics.Stopwatch.StartNew(); + try + { + var (supported, learnable, adopted) = await ProbeHttp2CapabilityOnceAsync( + capabilityCacheKey, sessionArgs, remoteHostName, remotePort, connectHost, connectPort, + upStreamEndPoint, externalProxy, cancellationToken, probeStarted).ConfigureAwait(false); + probeTcs.TrySetResult((supported, learnable)); + return (supported, learnable, adopted); + } + catch (Exception ex) + { + probeTcs.TrySetException(ex); + throw; + } + finally + { + pendingHttp2CapabilityProbes.TryRemove(capabilityCacheKey, out _); + } + } + } + + private async Task<(bool Supported, bool LearnableFailure, TcpServerConnection? AdoptedProbe)> + ProbeHttp2CapabilityOnceAsync( + string capabilityCacheKey, SessionEventArgsBase sessionArgs, string remoteHostName, int remotePort, + string? connectHost, int? connectPort, IPEndPoint? upStreamEndPoint, IExternalProxy? externalProxy, + CancellationToken cancellationToken, System.Diagnostics.Stopwatch probeStarted) + { + TcpServerConnection? connection = null; try { - var connection = await TcpConnectionFactory.GetServerConnection(this, remoteHostName, remotePort, + connection = await TcpConnectionFactory.GetServerConnection(this, remoteHostName, remotePort, HttpHeader.Version20, true, SslExtensions.Http2ProtocolAsList, true, sessionArgs, upStreamEndPoint, - externalProxy, true, true, cancellationToken, connectHost, connectPort); + externalProxy, true, true, cancellationToken, connectHost, connectPort).ConfigureAwait(false); var supported = connection != null && connection.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2; @@ -111,17 +185,27 @@ private async Task NegotiateHttp2Async(SessionEventArgsB Http2OriginCapabilityCache.Set(capabilityCacheKey, supported); Diagnostics.ProxyMetrics.Http2ProbeCompleted(probeStarted.Elapsed.TotalMilliseconds); ProxyLog.Http2ProbeResult(logger, capabilityCacheKey, false, supported, null); - - return new Http2NegotiationResult(supported, Task.FromResult(connection)); + // Transfer ownership to the leader; do not Release here. + var adopted = connection; + connection = null; + return (supported, false, adopted); } catch (Exception ex) { - // Do not cache a failed probe: it may be a transient network/cert issue rather than a genuine - // lack of HTTP/2 support, and caching "false" here would pin every subsequent tunnel to this - // host to HTTP/1.1 for the full TTL. + // ALPN rejection is definitive for this offer (h2); cache false so parallel CONNECT + // tunnels do not each re-probe and delay browser AuthenticateAsServer. Transient + // network/cert failures must not be cached — they would pin the host to HTTP/1.1. + if (AlpnNegotiation.IsAlpnNegotiationFailure(ex)) + Http2OriginCapabilityCache.Set(capabilityCacheKey, false); + Diagnostics.ProxyMetrics.Http2ProbeCompleted(probeStarted.Elapsed.TotalMilliseconds); ProxyLog.Http2ProbeResult(logger, capabilityCacheKey, false, false, ex); - return new Http2NegotiationResult(false, null); + return (false, DecryptFailureLearning.IsLearnableOriginTlsFailure(ex), null); + } + finally + { + if (connection != null) + await TcpConnectionFactory.Release(connection, true).ConfigureAwait(false); } } diff --git a/src/Titanium.Web.Proxy/Handlers/ResponseHandler.cs b/src/Titanium.Web.Proxy/Handlers/ResponseHandler.cs index d71d55b2c..7bd5ec3c7 100644 --- a/src/Titanium.Web.Proxy/Handlers/ResponseHandler.cs +++ b/src/Titanium.Web.Proxy/Handlers/ResponseHandler.cs @@ -314,31 +314,93 @@ internal static bool ShouldReuseConnectionForAuthReRequest(int responseStatusCod /// /// /// - private Task OnBeforeResponse(SessionEventArgs args) + private async Task OnBeforeResponse(SessionEventArgs args) { if (args.IsFastPath) - return Task.CompletedTask; + return; // Staged ResponseHeaderSet/Remove from route transforms (null when unused). ReverseProxySessionDispatch.ApplyResponseTransforms(args); // Rewrite gRPC → JSON before user handlers when the request was transcoded. if (ReverseProxy?.GrpcJsonTranscoder is { } transcoder) - return OnBeforeResponseWithTranscoderAsync(args, transcoder); + await transcoder.TryRewriteResponseAsync(args, args.CancellationToken).ConfigureAwait(false); + + // Before user handlers: native H2 GetResponseBody (Inspector capture) signals + // ReadHttp2BeforeHandlerTaskCompletionSource and locks the origin response before + // BeforeResponse returns, which would block Respond()/Ok() for the meta-refresh. + TrySeamlessDecryptBypassRetry(args); if (BeforeResponse != null) - return BeforeResponse.InvokeAsync(this, args, logger); + await BeforeResponse.InvokeAsync(this, args, logger).ConfigureAwait(false); + } - return Task.CompletedTask; + /// + /// Records learnable MITM HTTP blocks. For document navigations, activates bypass immediately and + /// replaces the response with a meta-refresh so the browser opens a new CONNECT (opaque tunnel). + /// + /// True when a seamless retry interstitial was installed. + internal bool TrySeamlessDecryptBypassRetry(SessionEventArgs args) + { + if (!EnableDecryptFailureBypass || !Network.Tcp.DecryptFailureLearning.IsLearnableHttpBlock(args)) + return false; + + // User (or earlier handler) already replaced the response. + if (args.HttpClient.Response.Locked) + return false; + + var host = Network.Tcp.DecryptFailureLearning.ResolveSessionHost(args); + var request = args.HttpClient.Request; + var isDocument = Network.Tcp.DecryptFailureLearning.IsDocumentNavigation(request); + var status = args.HttpClient.Response.StatusCode; + + var isActive = TryRecordDecryptFailureFromHttpStatus(host, status, isSynthetic: false, + forceImmediate: isDocument); + + if (!isDocument || !isActive) + return false; + + var url = request.Url; + if (!Network.Tcp.DecryptFailureLearning.IsSafeMetaRefreshUrl(url)) + return false; + + var html = Network.Tcp.DecryptFailureLearning.BuildMetaRefreshHtml(url!); + // Connection: close is H1-only; H2 strips hop-by-hop headers and we cancel/dispose the client + // connection after the response so the forged MITM multiplex is not reused. + HttpHeader[] headers = request.HttpVersion >= HttpHeader.Version20 + ? + [ + new HttpHeader("Cache-Control", "no-store") + ] + : + [ + new HttpHeader("Cache-Control", "no-store"), + new HttpHeader(KnownHeaders.Connection, KnownHeaders.ConnectionClose) + ]; + args.Ok(html, headers, closeServerConnection: true); + args.CloseClientConnectionAfterResponse = true; + return true; } - private async Task OnBeforeResponseWithTranscoderAsync( - SessionEventArgs args, - Abstractions.Plugins.IGrpcJsonTranscoder transcoder) + private async Task MaybeCloseClientAfterSeamlessRetryAsync(SessionEventArgs args) { - await transcoder.TryRewriteResponseAsync(args, args.CancellationToken).ConfigureAwait(false); - if (BeforeResponse != null) - await BeforeResponse.InvokeAsync(this, args, logger).ConfigureAwait(false); + if (!args.CloseClientConnectionAfterResponse) + return; + + // Do not CancelAsync first — that races the just-completed synthetic body write and + // can surface as net::ERR_CONNECTION_RESET before the browser applies meta-refresh. + // Graceful FIN (not linger-0 RST) tears down the forged MITM multiplex so the refresh + // opens a new CONNECT that hits the learned opaque bypass. + try + { + await Task.Yield(); + args.ClientConnection.CloseGracefully(); + } + catch (Exception ex) + { + ProxyDiagnostics.ReportBenign(logger, + "Seamless decrypt-bypass: closing client connection after meta-refresh", ex); + } } /// @@ -352,16 +414,32 @@ private async Task OnBeforeResponseWithTranscoderAsync( /// private Task OnAfterResponse(SessionEventArgs args) { + // Fast-path skips OnBeforeResponse; still learn HTTP blocks (no meta-refresh rewrite). + if (args.IsFastPath && EnableDecryptFailureBypass && + Network.Tcp.DecryptFailureLearning.IsLearnableHttpBlock(args)) + { + var host = Network.Tcp.DecryptFailureLearning.ResolveSessionHost(args); + var isDocument = Network.Tcp.DecryptFailureLearning.IsDocumentNavigation(args.HttpClient.Request); + TryRecordDecryptFailureFromHttpStatus(host, args.HttpClient.Response.StatusCode, + args.HttpClient.Response.IsSynthetic, forceImmediate: isDocument); + } + var success = args.Exception is null && args.HttpClient.Response.StatusCode is >= 200 and < 500; ReverseProxySessionDispatch.ReportUpstreamResult(this, args, success); if (!args.IsFastPath && AfterResponse != null) - return OnAfterResponseWithHandlerAsync(args); + return OnAfterResponseWithHandlerAndClientCloseAsync(args); TryUpdateHttp3CapabilityFromResponse(args); args.Timing?.MarkComplete(); - return Task.CompletedTask; + return MaybeCloseClientAfterSeamlessRetryAsync(args); + } + + private async Task OnAfterResponseWithHandlerAndClientCloseAsync(SessionEventArgs args) + { + await OnAfterResponseWithHandlerAsync(args).ConfigureAwait(false); + await MaybeCloseClientAfterSeamlessRetryAsync(args).ConfigureAwait(false); } private async Task OnAfterResponseWithHandlerAsync(SessionEventArgs args) diff --git a/src/Titanium.Web.Proxy/Handlers/TransparentClientHandler.cs b/src/Titanium.Web.Proxy/Handlers/TransparentClientHandler.cs index 3cb16b5d6..9fb7313d2 100644 --- a/src/Titanium.Web.Proxy/Handlers/TransparentClientHandler.cs +++ b/src/Titanium.Web.Proxy/Handlers/TransparentClientHandler.cs @@ -81,6 +81,8 @@ private Task HandleClient(TransparentProxyEndPoint endPoint, TcpClientConnection await endPoint.InvokeBeforeSslAuthenticate(this, args, logger); hookUpstreamProtocol = args.UpstreamHttpProtocol; decryptSsl = args.DecryptSsl; + if (decryptSsl && ShouldBypassDecryptForLearnedHost(httpsHostName)) + decryptSsl = false; } transparentUpstreamProtocol = hookUpstreamProtocol; @@ -174,6 +176,10 @@ private Task HandleClient(TransparentProxyEndPoint endPoint, TcpClientConnection if (cancellationTokenSource.IsCancellationRequested) return; + if (endPoint.DecryptSsl && args.DecryptSsl && + ShouldBypassDecryptForLearnedHost(httpsHostName)) + args.DecryptSsl = false; + if (endPoint.DecryptSsl && args.DecryptSsl) { var sslProtocol = clientHelloInfo.SslProtocol & SupportedSslProtocols; @@ -196,6 +202,7 @@ private Task HandleClient(TransparentProxyEndPoint endPoint, TcpClientConnection var requiresH3Bridge = false; string? http2ConnectHost = null; int? http2ConnectPort = null; + var fallThroughOpaque = false; http2ConnectHost = string.Equals(args.ForwardHttpsHostName, httpsHostName, StringComparison.OrdinalIgnoreCase) @@ -242,8 +249,21 @@ private Task HandleClient(TransparentProxyEndPoint endPoint, TcpClientConnection // discarded here. Always null when requiresHttp11Bridge (nothing to adopt/flow down - // the bridge opens its own per-h2-stream HTTP/1.1 connections instead). prefetchConnectionTask = negotiation.RetainedConnectionTask; + + if (EnableDecryptFailureBypass && negotiation.LearnableOriginTlsFailure) + { + TryRecordDecryptFailure(httpsHostName, error: null, forceBypass: true); + var doomedPrefetch = prefetchConnectionTask; + prefetchConnectionTask = null; + if (doomedPrefetch != null) + _ = TcpConnectionFactory.Release(doomedPrefetch, true); + fallThroughOpaque = true; + args.DecryptSsl = false; + } } + if (!fallThroughOpaque) + { // do client authentication using certificate X509Certificate2? certificate = null; SslStream? sslStream = null; @@ -272,16 +292,9 @@ private Task HandleClient(TransparentProxyEndPoint endPoint, TcpClientConnection CertificateRevocationCheckMode = X509RevocationMode.NoCheck }; - if (http2Supported) - { - options.ApplicationProtocols = clientHelloInfo.GetAlpn(); - if (options.ApplicationProtocols == null || options.ApplicationProtocols.Count == 0) - options.ApplicationProtocols = SslExtensions.Http11ProtocolAsList; - } - else - { - options.ApplicationProtocols = SslExtensions.Http11ProtocolAsList; - } + options.ApplicationProtocols = http2Supported + ? SslExtensions.Http2AndHttp11ProtocolAsList + : SslExtensions.Http11ProtocolAsList; // Successfully managed to authenticate the client using the certificate await sslStream.AuthenticateAsServerAsync(options, cancellationToken); @@ -472,6 +485,7 @@ await Http2Helper.SendHttp2(clientStream, connection.Stream, // handling of the same (never expected from a compliant client) edge case. } } + } // !fallThroughOpaque — MITM completed (or continued below for HTTP/1) } else { diff --git a/src/Titanium.Web.Proxy/Http/RequestResponseBase.cs b/src/Titanium.Web.Proxy/Http/RequestResponseBase.cs index 0859f96f8..f52d662e5 100644 --- a/src/Titanium.Web.Proxy/Http/RequestResponseBase.cs +++ b/src/Titanium.Web.Proxy/Http/RequestResponseBase.cs @@ -246,6 +246,12 @@ internal set /// internal bool Locked { get; set; } + /// + /// True when this message was produced by SessionEventArgs.Respond / Ok / GenericResponse + /// (or equivalent), not received from the origin. + /// + internal bool IsSynthetic { get; set; } + /// /// True when field names were already lowercased for HTTP/2 (bridge prepare). /// EncodeHeaderBlock can skip the per-header ASCII scan under writeLock. @@ -385,6 +391,7 @@ internal void ResetWireState() IsBodySent = false; BodyIsWireEncoded = false; Locked = false; + IsSynthetic = false; KeepBody = false; HeaderNamesAreHttp2Normalized = false; OriginalHasBody = false; diff --git a/src/Titanium.Web.Proxy/Http2/Http2NegotiationResult.cs b/src/Titanium.Web.Proxy/Http2/Http2NegotiationResult.cs index f18a05b20..cc81c3660 100644 --- a/src/Titanium.Web.Proxy/Http2/Http2NegotiationResult.cs +++ b/src/Titanium.Web.Proxy/Http2/Http2NegotiationResult.cs @@ -13,12 +13,14 @@ namespace Titanium.Web.Proxy.Http2; internal sealed class Http2NegotiationResult { internal Http2NegotiationResult(bool originSupportsHttp2, Task? retainedConnectionTask, - bool requiresHttp11Bridge = false, bool requiresH2OriginBridge = false) + bool requiresHttp11Bridge = false, bool requiresH2OriginBridge = false, + bool learnableOriginTlsFailure = false) { OriginSupportsHttp2 = originSupportsHttp2; RetainedConnectionTask = retainedConnectionTask; RequiresHttp11Bridge = requiresHttp11Bridge; RequiresH2OriginBridge = requiresH2OriginBridge; + LearnableOriginTlsFailure = learnableOriginTlsFailure; } /// @@ -27,6 +29,12 @@ internal Http2NegotiationResult(bool originSupportsHttp2, Task internal bool OriginSupportsHttp2 { get; } + /// + /// True when a cold awaited H2 capability probe failed with a learnable origin TLS error + /// (non-ALPN authentication failure). Callers may same-CONNECT opaque-fallback without MITM. + /// + internal bool LearnableOriginTlsFailure { get; } + /// /// A not-yet-consumed origin connection opened while negotiating, if any. Its application-protocol /// offer already matches (or, when diff --git a/src/Titanium.Web.Proxy/Http2/Http2OriginRelayPool.cs b/src/Titanium.Web.Proxy/Http2/Http2OriginRelayPool.cs index 3faeaeb2d..5d475808a 100644 --- a/src/Titanium.Web.Proxy/Http2/Http2OriginRelayPool.cs +++ b/src/Titanium.Web.Proxy/Http2/Http2OriginRelayPool.cs @@ -127,7 +127,7 @@ private int SoftCapPerLeg() { // Spread streams across origin legs before any single connection saturates. Dividing by // MaxOrigin*4 (default 8) opens additional legs under typical RPS concurrency (32–128). - // Soft=1/2 fan-out was tried; cool remeasure showed no gain vs Soft≈8 (extra legs tax + // Soft=1/2 fan-out was tried; cool re-measure showed no gain vs Soft≈8 (extra legs tax // cleartext connect without helping FrameWriter parallelism enough). return Math.Max(1, resourceLimits.MaxConcurrentStreamsPerConnection / Math.Max(1, resourceLimits.MaxOriginHttp2ConnectionsPerAuthority * 4)); diff --git a/src/Titanium.Web.Proxy/Models/DecryptFailureBypassEntry.cs b/src/Titanium.Web.Proxy/Models/DecryptFailureBypassEntry.cs new file mode 100644 index 000000000..8a07fadef --- /dev/null +++ b/src/Titanium.Web.Proxy/Models/DecryptFailureBypassEntry.cs @@ -0,0 +1,35 @@ +using System; + +namespace Titanium.Web.Proxy.Models; + +/// +/// A host that the proxy learned to tunnel without decrypt after repeated origin TLS +/// handshake failures under MITM (e.g. bot / TLS-fingerprint rejection). +/// +public sealed class DecryptFailureBypassEntry +{ + public DecryptFailureBypassEntry(string host, int strikes, DateTime learnedAtUtc, DateTime expiresAtUtc, + bool bypassActive) + { + Host = host; + Strikes = strikes; + LearnedAtUtc = learnedAtUtc; + ExpiresAtUtc = expiresAtUtc; + BypassActive = bypassActive; + } + + /// Normalized hostname (no port). + public string Host { get; } + + /// Origin TLS failure strikes accumulated toward the bypass threshold. + public int Strikes { get; } + + /// When the first strike (or forced bypass) was recorded. + public DateTime LearnedAtUtc { get; } + + /// When this entry expires if not refreshed. + public DateTime ExpiresAtUtc { get; } + + /// True when subsequent CONNECTs skip decrypt for this host. + public bool BypassActive { get; } +} diff --git a/src/Titanium.Web.Proxy/Network/DecryptBypassCache.cs b/src/Titanium.Web.Proxy/Network/DecryptBypassCache.cs new file mode 100644 index 000000000..7a3ad3df7 --- /dev/null +++ b/src/Titanium.Web.Proxy/Network/DecryptBypassCache.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Titanium.Web.Proxy.Models; + +namespace Titanium.Web.Proxy.Network; + +/// +/// Process-lifetime cache of hosts that failed origin TLS under MITM. Approximate LRU: +/// last-access is updated on bypass hit and on record only — not on every CONNECT miss. +/// +internal sealed class DecryptBypassCache +{ + private readonly ConcurrentDictionary cache = + new(StringComparer.OrdinalIgnoreCase); + + private TimeSpan ttl = TimeSpan.FromMinutes(30); + private int maxEntries = 256; + private int strikeThreshold = 2; + + internal TimeSpan Ttl + { + get => ttl; + set => ttl = value <= TimeSpan.Zero ? TimeSpan.FromMinutes(30) : value; + } + + internal int MaxEntries + { + get => maxEntries; + set => maxEntries = value < 1 ? 1 : value; + } + + internal int StrikeThreshold + { + get => strikeThreshold; + set => strikeThreshold = value < 1 ? 1 : value; + } + + internal int Count => cache.Count; + + /// + /// Returns true when decrypt should be skipped. Updates last-access only on a bypass hit. + /// + internal bool ShouldBypass(string? host) + { + if (!IsBypassActive(host)) + return false; + + var key = Normalize(host!); + if (cache.TryGetValue(key, out var entry)) + entry.LastAccessUtc = DateTime.UtcNow; + return true; + } + + /// + /// Whether bypass is active without mutating last-access (for newly-active event gating). + /// + internal bool IsBypassActive(string? host) + { + if (string.IsNullOrWhiteSpace(host)) + return false; + + var key = Normalize(host); + if (!cache.TryGetValue(key, out var entry)) + return false; + + return entry.ExpiresAtUtc > DateTime.UtcNow && entry.BypassActive; + } + + /// + /// Records an origin TLS handshake failure. When strikes reach the threshold, bypass becomes active. + /// Returns true when bypass is (now) active. + /// + internal bool RecordFailure(string? host) + { + if (string.IsNullOrWhiteSpace(host)) + return false; + + var key = Normalize(host); + var now = DateTime.UtcNow; + var entry = cache.AddOrUpdate(key, + _ => new Entry + { + Strikes = 1, + LearnedAtUtc = now, + ExpiresAtUtc = now.Add(Ttl), + LastAccessUtc = now, + BypassActive = 1 >= StrikeThreshold + }, + (_, existing) => + { + if (existing.ExpiresAtUtc <= now) + { + existing.Strikes = 1; + existing.LearnedAtUtc = now; + existing.BypassActive = 1 >= StrikeThreshold; + } + else + { + existing.Strikes++; + if (existing.Strikes >= StrikeThreshold) + existing.BypassActive = true; + } + + existing.ExpiresAtUtc = now.Add(Ttl); + existing.LastAccessUtc = now; + return existing; + }); + + EvictIfOverCapacity(); + return entry.BypassActive; + } + + /// + /// Forces bypass for a host (same-CONNECT opaque fallback after an awaited probe failure). + /// + internal void MarkBypassed(string? host) + { + if (string.IsNullOrWhiteSpace(host)) + return; + + var key = Normalize(host); + var now = DateTime.UtcNow; + cache.AddOrUpdate(key, + _ => new Entry + { + Strikes = Math.Max(StrikeThreshold, 1), + LearnedAtUtc = now, + ExpiresAtUtc = now.Add(Ttl), + LastAccessUtc = now, + BypassActive = true + }, + (_, existing) => + { + existing.Strikes = Math.Max(existing.Strikes, StrikeThreshold); + existing.BypassActive = true; + existing.ExpiresAtUtc = now.Add(Ttl); + existing.LastAccessUtc = now; + if (existing.LearnedAtUtc == default) + existing.LearnedAtUtc = now; + return existing; + }); + + EvictIfOverCapacity(); + } + + internal bool Remove(string? host) + { + if (string.IsNullOrWhiteSpace(host)) + return false; + return cache.TryRemove(Normalize(host), out _); + } + + internal void Clear() => cache.Clear(); + + internal IReadOnlyList Snapshot() + { + var now = DateTime.UtcNow; + return cache + .Where(kv => kv.Value.ExpiresAtUtc > now) + .OrderByDescending(kv => kv.Value.LastAccessUtc) + .Select(kv => new DecryptFailureBypassEntry( + kv.Key, + kv.Value.Strikes, + kv.Value.LearnedAtUtc, + kv.Value.ExpiresAtUtc, + kv.Value.BypassActive)) + .ToList(); + } + + internal void TrimExpired() + { + var now = DateTime.UtcNow; + foreach (var key in cache.Keys) + if (cache.TryGetValue(key, out var entry) && entry.ExpiresAtUtc <= now) + cache.TryRemove(key, out _); + } + + private void EvictIfOverCapacity() + { + var over = cache.Count - MaxEntries; + if (over <= 0) + return; + + TrimExpired(); + over = cache.Count - MaxEntries; + if (over <= 0) + return; + + var victims = cache + .OrderBy(kv => kv.Value.LastAccessUtc) + .Take(over) + .Select(kv => kv.Key) + .ToList(); + + foreach (var key in victims) + cache.TryRemove(key, out _); + } + + internal static string Normalize(string host) + { + host = host.Trim(); + // Strip brackets / port from host:port or [ipv6]:port + if (host.StartsWith('[') && host.Contains(']')) + { + var end = host.IndexOf(']'); + host = host.Substring(1, end - 1); + } + else + { + var colon = host.LastIndexOf(':'); + if (colon > 0 && host.IndexOf(':') == colon && int.TryParse(host.AsSpan(colon + 1), out _)) + host = host.Substring(0, colon); + } + + return host.Trim().ToLowerInvariant(); + } + + private sealed class Entry + { + internal int Strikes; + internal DateTime LearnedAtUtc; + internal DateTime ExpiresAtUtc; + internal DateTime LastAccessUtc; + internal bool BypassActive; + } +} diff --git a/src/Titanium.Web.Proxy/Network/Tcp/DecryptFailureLearning.cs b/src/Titanium.Web.Proxy/Network/Tcp/DecryptFailureLearning.cs new file mode 100644 index 000000000..eda8afc36 --- /dev/null +++ b/src/Titanium.Web.Proxy/Network/Tcp/DecryptFailureLearning.cs @@ -0,0 +1,105 @@ +using System; +using System.Net.Sockets; +using System.Security.Authentication; +using Titanium.Web.Proxy.EventArguments; +using Titanium.Web.Proxy.Http; + +namespace Titanium.Web.Proxy.Network.Tcp; + +/// +/// Classifies failures that are safe to learn as "tunnel without decrypt" +/// (bot / fingerprint style). ALPN mismatches and plain TCP/DNS failures are excluded. +/// +internal static class DecryptFailureLearning +{ + internal static bool IsLearnableOriginTlsFailure(Exception? error) + { + if (error == null || AlpnNegotiation.IsAlpnNegotiationFailure(error)) + return false; + + // Prefer not to learn pure connectivity failures. + for (Exception? e = error; e != null; e = e.InnerException) + { + if (e is SocketException) + return false; + } + + for (Exception? e = error; e != null; e = e.InnerException) + { + if (e is AuthenticationException) + return true; + } + + return false; + } + + /// + /// MITM HTTPS 403/429 from the origin (not synthetic Ok/Respond/GenericResponse). + /// + internal static bool IsLearnableHttpBlock(SessionEventArgs args) + { + if (!args.IsHttps || args.Exception is not null) + return false; + + if (!args.HttpClient.HasResponse) + return false; + + var response = args.HttpClient.Response; + if (response.IsSynthetic) + return false; + + return response.StatusCode is 403 or 429; + } + + /// + /// Top-level document navigation (seamless meta-refresh candidate). + /// Prefers Sec-Fetch-Dest: document; otherwise Accept prefers text/html. + /// + internal static bool IsDocumentNavigation(Request request) + { + var dest = request.Headers.GetFirstHeader("Sec-Fetch-Dest"); + if (dest != null) + return dest.Value.Equals("document", StringComparison.OrdinalIgnoreCase); + + var accept = request.Headers.GetHeaderValueOrNull(KnownHeaders.Accept); + if (string.IsNullOrEmpty(accept)) + return false; + + // First Accept token prefers HTML (Chrome document navigations lead with text/html). + var comma = accept.IndexOf(','); + var first = (comma >= 0 ? accept.AsSpan(0, comma) : accept.AsSpan()).Trim(); + var semi = first.IndexOf(';'); + if (semi >= 0) + first = first.Slice(0, semi).Trim(); + + return first.Equals("text/html", StringComparison.OrdinalIgnoreCase) + || first.Equals("application/xhtml+xml", StringComparison.OrdinalIgnoreCase) + || first.StartsWith("text/html", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Meta-refresh targets must be absolute http(s) URLs (never javascript: / data:). + /// + internal static bool IsSafeMetaRefreshUrl(string? url) => + Uri.TryCreate(url, UriKind.Absolute, out var uri) + && (uri.Scheme == Uri.UriSchemeHttps || uri.Scheme == Uri.UriSchemeHttp); + + internal static string? ResolveSessionHost(SessionEventArgs args) + { + var host = args.HttpClient.Request.RequestUri?.Host; + if (string.IsNullOrEmpty(host)) + host = args.HttpClient.ConnectRequest?.RequestUri?.Host; + return host; + } + + /// + /// Minimal interstitial that triggers an immediate navigation retry (new CONNECT). + /// + internal static string BuildMetaRefreshHtml(string absoluteUrl) + { + var escaped = System.Net.WebUtility.HtmlEncode(absoluteUrl); + return "" + + "" + + ""; + } +} \ No newline at end of file diff --git a/src/Titanium.Web.Proxy/Network/TcpConnection/TcpClientConnection.cs b/src/Titanium.Web.Proxy/Network/TcpConnection/TcpClientConnection.cs index 9dc27a1cb..05b3d2cc0 100644 --- a/src/Titanium.Web.Proxy/Network/TcpConnection/TcpClientConnection.cs +++ b/src/Titanium.Web.Proxy/Network/TcpConnection/TcpClientConnection.cs @@ -157,6 +157,45 @@ private void CloseClientSocket(int lingerSeconds) } } + /// + /// Closes with a TCP FIN (not linger-0 RST) so the peer can finish reading the last + /// response — required for seamless decrypt-bypass meta-refresh before a new CONNECT. + /// + internal virtual void CloseGracefully() + { + if (Interlocked.Exchange(ref disposed, 1) != 0) return; + + if (trackClientConnectionCount) + ProxyServer.UpdateClientConnectionCount(false); + + if (tcpClientSocket == null) return; + try + { + tcpClientSocket.LingerState = new LingerOption(false, 0); + try + { + tcpClientSocket.Shutdown(SocketShutdown.Both); + } + catch (SocketException) + { + // Already half-closed / reset by peer. + } + + tcpClientSocket.Close(); + } + catch (ObjectDisposedException) + { + } + catch (SocketException) + { + } + catch (Exception ex) + { + Logging.ProxyDiagnostics.ReportBenign(ProxyServer.Logger, + "Failed to gracefully close a client socket after seamless retry.", ex); + } + } + public Stream GetStream() { if (tcpClientSocket == null) diff --git a/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs b/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs index 3d30503f2..f3c51c5f9 100644 --- a/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs +++ b/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs @@ -1127,6 +1127,8 @@ internal bool TryRentPooled(ProxyServer proxyServer, string cacheKey, { ProxyDiagnostics.ReportCaught(proxyServer.Logger, "TcpConnectionFactory TLS downgrade exhausted after IOException; rethrowing", ex); + if (isHttps) + proxyServer.TryRecordDecryptFailure(remoteHostName, ex); throw; } @@ -1149,6 +1151,8 @@ internal bool TryRentPooled(ProxyServer proxyServer, string cacheKey, { ProxyDiagnostics.ReportCaught(proxyServer.Logger, "TcpConnectionFactory TLS downgrade exhausted after AuthenticationException; rethrowing", ex); + if (isHttps) + proxyServer.TryRecordDecryptFailure(remoteHostName, ex); throw; } @@ -1172,6 +1176,10 @@ internal bool TryRentPooled(ProxyServer proxyServer, string cacheKey, if (stream != null) await stream.DisposeAsync(); tcpServerSocket?.Close(); ProxyLog.OriginConnectionFailed(proxyServer.Logger, remoteHostName, remotePort, ex); + // Post-MITM (or other) origin TLS failure: learn for subsequent CONNECTs when enabled. + // Do not learn ALPN (handled above) or TCP-only failures (IsLearnable filters those). + if (isHttps) + proxyServer.TryRecordDecryptFailure(remoteHostName, ex); throw; } diff --git a/src/Titanium.Web.Proxy/Properties/AssemblyInfo.cs b/src/Titanium.Web.Proxy/Properties/AssemblyInfo.cs index 78c299c32..12fb34aa7 100644 --- a/src/Titanium.Web.Proxy/Properties/AssemblyInfo.cs +++ b/src/Titanium.Web.Proxy/Properties/AssemblyInfo.cs @@ -77,5 +77,5 @@ // file-properties version disagreed with the package it was published in. Keep both of the values // below equal to (as Major.Minor.Build.0) whenever that property changes. -[assembly: AssemblyVersion("7.0.6.0")] -[assembly: AssemblyFileVersion("7.0.6.0")] +[assembly: AssemblyVersion("7.0.7.0")] +[assembly: AssemblyFileVersion("7.0.7.0")] diff --git a/src/Titanium.Web.Proxy/ProxyServer.cs b/src/Titanium.Web.Proxy/ProxyServer.cs index d09ac5722..35e26dad4 100644 --- a/src/Titanium.Web.Proxy/ProxyServer.cs +++ b/src/Titanium.Web.Proxy/ProxyServer.cs @@ -228,6 +228,20 @@ public ProxyServer(string? rootCertificateName, string? rootCertificateIssuerNam internal Http2OriginCapabilityCache Http2OriginCapabilityCache { get; } = new(TimeSpan.FromMinutes(30)); + /// + /// In-flight HTTP/2 capability probes keyed like . + /// Browsers open many parallel CONNECTs to the same host; without coalescing, each cold tunnel + /// blocked browser AuthenticateAsServer on its own origin probe and Chrome aborted with EOF. + /// + private readonly ConcurrentDictionary> + pendingHttp2CapabilityProbes = new(); + + /// + /// Hosts learned to skip MITM decrypt after origin TLS handshake failures. + /// See . + /// + internal Network.DecryptBypassCache DecryptFailureBypassCache { get; } = new(); + /// /// Caches, per upstream host:port, whether the real origin supports HTTP/3 (QUIC), as discovered via /// Alt-Svc response headers or HTTPS/SVCB DNS records. See . @@ -244,6 +258,7 @@ internal void TrimOriginCapabilityCaches() { Http2OriginCapabilityCache.TrimExpired(); Http3OriginCapabilityCache.TrimExpired(); + DecryptFailureBypassCache.TrimExpired(); // Read the backing fields directly (not the HttpsSvcbResolver/SvcbDiscoveryCoordinator // properties) so this periodic sweep never itself instantiates either one when SVCB @@ -332,6 +347,139 @@ internal void TrimOriginCapabilityCaches() /// public bool EnableHttp2 { get; set; } = true; + /// + /// When , the proxy learns hosts whose origin TLS handshake fails under + /// MITM (non-ALPN , typically + /// bot / TLS-fingerprint rejection) and tunnels subsequent CONNECTs without decrypt. + /// Default so library and RPS baselines are unchanged. Inspector enables + /// this by default. Success-path cost when on is one dictionary lookup per CONNECT. + /// + public bool EnableDecryptFailureBypass { get; set; } + + /// + /// How long a learned decrypt-bypass entry remains valid. Default 30 minutes. + /// + public TimeSpan DecryptFailureBypassTtl + { + get => DecryptFailureBypassCache.Ttl; + set => DecryptFailureBypassCache.Ttl = value; + } + + /// + /// Maximum learned hosts retained (approximate LRU eviction). Default 256. + /// + public int DecryptFailureBypassMaxEntries + { + get => DecryptFailureBypassCache.MaxEntries; + set => DecryptFailureBypassCache.MaxEntries = value; + } + + /// + /// Origin TLS failure strikes required before a host is bypassed on later CONNECTs. + /// Same-CONNECT opaque fallback after an awaited H2 probe failure marks bypass immediately. + /// Default 2. + /// + public int DecryptFailureBypassThreshold + { + get => DecryptFailureBypassCache.StrikeThreshold; + set => DecryptFailureBypassCache.StrikeThreshold = value; + } + + /// + /// Raised when a host becomes actively bypassed (threshold reached or same-CONNECT mark). + /// Handlers must not block; Inspector marshals to the UI thread. + /// + public event EventHandler? DecryptFailureBypassChanged; + + /// Snapshot of current learned decrypt-bypass entries (may include non-active strikes). + public IReadOnlyList GetDecryptFailureBypassEntries() => + DecryptFailureBypassCache.Snapshot(); + + /// Clears all learned decrypt-bypass entries. + public void ClearDecryptFailureBypass() => DecryptFailureBypassCache.Clear(); + + /// Removes one host from the learned decrypt-bypass cache. + public bool RemoveDecryptFailureBypass(string host) => DecryptFailureBypassCache.Remove(host); + + /// + /// When is on and is actively + /// bypassed, returns (decrypt should be skipped). + /// + public bool ShouldBypassDecryptForLearnedHost(string? host) => + EnableDecryptFailureBypass && DecryptFailureBypassCache.ShouldBypass(host); + + /// + /// Records a learnable origin TLS failure when the feature is enabled. Returns whether bypass is active. + /// + internal bool TryRecordDecryptFailure(string? host, Exception? error, bool forceBypass = false) + { + if (!EnableDecryptFailureBypass || string.IsNullOrWhiteSpace(host)) + return false; + + if (!forceBypass && !Network.Tcp.DecryptFailureLearning.IsLearnableOriginTlsFailure(error)) + return false; + + var wasActive = DecryptFailureBypassCache.IsBypassActive(host); + bool isActive; + if (forceBypass) + { + DecryptFailureBypassCache.MarkBypassed(host); + isActive = true; + } + else + { + isActive = DecryptFailureBypassCache.RecordFailure(host); + } + + if (isActive && !wasActive) + RaiseDecryptFailureBypassChanged(host); + + return isActive; + } + + /// + /// Learns from MITM HTTPS 403/429 (bot/WAF after TLS succeeds). + /// Document navigations activate bypass immediately; other requests use the shared strike threshold. + /// Does not convert the current MITM CONNECT — a seamless meta-refresh (documents) or a later + /// CONNECT tunnels with the browser fingerprint. + /// + /// Whether bypass is active after this call. + internal bool TryRecordDecryptFailureFromHttpStatus(string? host, int statusCode, + bool isSynthetic = false, bool forceImmediate = false) + { + if (!EnableDecryptFailureBypass || string.IsNullOrWhiteSpace(host) || isSynthetic) + return false; + + if (statusCode is not (403 or 429)) + return false; + + var wasActive = DecryptFailureBypassCache.IsBypassActive(host); + bool isActive; + if (forceImmediate) + { + DecryptFailureBypassCache.MarkBypassed(host); + isActive = true; + } + else + { + isActive = DecryptFailureBypassCache.RecordFailure(host); + } + + if (isActive && !wasActive) + RaiseDecryptFailureBypassChanged(host); + + return isActive; + } + + private void RaiseDecryptFailureBypassChanged(string host) + { + var snap = DecryptFailureBypassCache.Snapshot() + .FirstOrDefault(e => string.Equals(e.Host, Network.DecryptBypassCache.Normalize(host), + StringComparison.OrdinalIgnoreCase)); + if (snap != null) + DecryptFailureBypassChanged?.Invoke(this, snap); + } + /// /// When , the proxy enables RFC 8441 WebSocket-over-HTTP/2: /// @@ -1320,8 +1468,9 @@ internal Task InvokeOnResponseBodyWriteAsync(object sender, BeforeBodyWriteEvent /// /// Returns when the global interception gate is active for the given /// endpoint: any session event handler is subscribed, is - /// set on the server, or the endpoint's own - /// override is set. + /// set on the server, the endpoint's own + /// override is set, or is on (seamless document + /// meta-refresh requires ). /// internal bool NeedsHttpInterception(ProxyEndPoint? endPoint = null) => (endPoint?.EnableHttpInterception ?? EnableHttpInterception) @@ -1329,7 +1478,8 @@ internal bool NeedsHttpInterception(ProxyEndPoint? endPoint = null) => || BeforeResponse != null || AfterResponse != null || OnRequestBodyWrite != null - || OnResponseBodyWrite != null; + || OnResponseBodyWrite != null + || EnableDecryptFailureBypass; /// /// Returns when this specific request/stream should go through the diff --git a/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt b/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt index 198925073..8d1900c90 100644 --- a/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt +++ b/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt @@ -236,3 +236,23 @@ Titanium.Web.Proxy.ProxyServer.TryDisableSystemProxy(Titanium.Web.Proxy.Models.P Titanium.Web.Proxy.ProxyServer.TryRestoreOriginalProxySettings() -> Titanium.Web.Proxy.SystemProxyChangeResult Titanium.Web.Proxy.ProxyServer.TrySetAsSystemProxy(Titanium.Web.Proxy.Models.ExplicitProxyEndPoint! endPoint, Titanium.Web.Proxy.Models.ProxyProtocolType protocolType, Titanium.Web.Proxy.SystemProxySettings? settings = null) -> Titanium.Web.Proxy.SystemProxyChangeResult const Titanium.Web.Proxy.ProxyServer.DefaultViaHeaderPseudonym = "titanium-web-proxy" -> string! +Titanium.Web.Proxy.Models.DecryptFailureBypassEntry +Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.BypassActive.get -> bool +Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.DecryptFailureBypassEntry(string! host, int strikes, System.DateTime learnedAtUtc, System.DateTime expiresAtUtc, bool bypassActive) -> void +Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.ExpiresAtUtc.get -> System.DateTime +Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.Host.get -> string! +Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.LearnedAtUtc.get -> System.DateTime +Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.Strikes.get -> int +Titanium.Web.Proxy.ProxyServer.ClearDecryptFailureBypass() -> void +Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassChanged -> System.EventHandler? +Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassMaxEntries.get -> int +Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassMaxEntries.set -> void +Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassThreshold.get -> int +Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassThreshold.set -> void +Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassTtl.get -> System.TimeSpan +Titanium.Web.Proxy.ProxyServer.DecryptFailureBypassTtl.set -> void +Titanium.Web.Proxy.ProxyServer.EnableDecryptFailureBypass.get -> bool +Titanium.Web.Proxy.ProxyServer.EnableDecryptFailureBypass.set -> void +Titanium.Web.Proxy.ProxyServer.GetDecryptFailureBypassEntries() -> System.Collections.Generic.IReadOnlyList! +Titanium.Web.Proxy.ProxyServer.RemoveDecryptFailureBypass(string! host) -> bool +Titanium.Web.Proxy.ProxyServer.ShouldBypassDecryptForLearnedHost(string? host) -> bool diff --git a/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj b/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj index bf293649e..aea4a232f 100644 --- a/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj +++ b/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj @@ -13,7 +13,7 @@ - 7.0.6 + 7.0.7 - - + + FileRef="InspectorExe" + ExeCommand="" + Impersonate="yes" + Return="asyncNoWait" /> @@ -46,13 +43,13 @@ - + Order="2" + Condition="WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 AND NOT Installed" /> @@ -66,17 +63,23 @@ - + + + + + + - + - true, ``` CLI: when `server.decryptSkipHosts` and/or `server.decryptOnlyHosts` are present in `twp.yaml`, exclusions apply with **Replace**. Omit both to leave Merge defaults. Optional `server.systemProxyBypassHosts` / `server.proxyLoopback` build OS-bypass settings the same way (present ⇒ Replace; omit ⇒ Merge). Removing identity hosts from OS bypass can break Microsoft SSO while System proxy is enabled. + +### Decrypt failure bypass (learned tunnel) + +Optional heuristic for hosts that reject the proxy’s TLS fingerprint under MITM (bot / WAF style). **Default off** on `ProxyServer` (library and RPS baselines unchanged). Titanium Inspector enables it by default. + +```csharp +proxyServer.EnableDecryptFailureBypass = true; +proxyServer.DecryptFailureBypassThreshold = 2; // strikes before later CONNECTs skip decrypt +proxyServer.DecryptFailureBypassTtl = TimeSpan.FromMinutes(30); +proxyServer.DecryptFailureBypassMaxEntries = 256; // approximate LRU + +// Snapshot / manage +foreach (var e in proxyServer.GetDecryptFailureBypassEntries()) + Console.WriteLine($"{e.Host} active={e.BypassActive}"); +proxyServer.RemoveDecryptFailureBypass("www.example.com"); +proxyServer.ClearDecryptFailureBypass(); +``` + +Behavior: + +- Learns from **origin** TLS `AuthenticationException` failures (not ALPN-only “no h2”, not TCP/DNS). +- Also learns from MITM HTTPS responses with status **403** or **429** (bot/WAF after TLS succeeds), excluding synthetic `Respond`/`Ok`/`GenericResponse`. + - **Document** navigations (`Sec-Fetch-Dest: document` or Accept prefers `text/html`): activate bypass **immediately** and answer with a one-shot **meta-refresh** (200) plus connection close so the browser opens a new CONNECT over an opaque tunnel—seamless recovery without a manual reload. + - **Non-document** (XHR/fetch): shared strike threshold (default **2**); no body rewrite. +- Prefetch (MITM origin sockets) is not started for learned hosts. The current MITM CONNECT is never converted in place. +- If a cold awaited HTTP/2 capability probe fails with a learnable error, the **same CONNECT** can fall back to opaque relay before browser MITM (ClientHello was only peeked); session prefetch is not started on that failure path. +- Does **not** replace `MitmExclusionDefaults` pinning/SSO lists. Does not auto-learn browser-leg pinning aborts. +- Not a `twp.yaml` key in v1 — set on `ProxyServer` (or use Inspector). Use `ShouldBypassDecryptForLearnedHost` for O(1) consults (avoid snapshotting the full list on hot paths). +- Automation/CDP browsers may still see Akamai-style captchas **after** a successful tunnel (site bot score), which is outside MITM recovery. diff --git a/website/docs/performance.md b/website/docs/performance.md index 527431d96..4236c83a8 100644 --- a/website/docs/performance.md +++ b/website/docs/performance.md @@ -34,11 +34,13 @@ Typical reverse wires with **64 KB GET/POST** (plus 256 KB H1 terminate) — bod ## Heavier reverse workloads -Larger bodies, POST, lossy links, TLS termination cost, and architecture-sensitive shapes (slow consumers, duplex, WebSocket). Full tables are on the [Performance wiki](https://github.com/justcoding121/titanium-web-proxy/wiki/Performance#heavier-reverse-workloads). +Larger bodies, POST, lossy links, TLS termination cost, and architecture-sensitive shapes (slow consumers, duplex, WebSocket H1 Upgrade). Full tables are on the [Performance wiki](https://github.com/justcoding121/titanium-web-proxy/wiki/Performance#heavier-reverse-workloads). + +Additional real-world tables (wiki only, not plotted here): [Unary gRPC H2→h2c](https://github.com/justcoding121/titanium-web-proxy/wiki/Performance#unary-grpc-h2-tls--h2c), [WebSocket H1 TLS→H1 TLS](https://github.com/justcoding121/titanium-web-proxy/wiki/Performance#websocket-h1-tls--h1-tls), [WebSocket RFC 8441](https://github.com/justcoding121/titanium-web-proxy/wiki/Performance#websocket-h2-tls-8441--h1). ## Full measurements -Detailed tables and methodology: [Performance wiki](https://github.com/justcoding121/titanium-web-proxy/wiki/Performance) · [Performance profiling](https://github.com/justcoding121/titanium-web-proxy/wiki/Performance-Profiling) +Product 5×5 reverse/MITM matrices, saturation calibration, heavier reverse (bodies/POST/lossy/TLS/arch), unary gRPC (H2↔H2 and H2→h2c), and WebSocket (H1 Upgrade, dual-TLS H1, RFC 8441 H2) tables live on the [Performance wiki](https://github.com/justcoding121/titanium-web-proxy/wiki/Performance). Profiling notes: [Performance profiling](https://github.com/justcoding121/titanium-web-proxy/wiki/Performance-Profiling). --- diff --git a/wiki/Home.md b/wiki/Home.md index 045664231..c8ee74e73 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -158,6 +158,8 @@ socksEndPoint.BeforeSslAuthenticate += (sender, e) => proxyServer.AddEndPoint(socksEndPoint); ``` +See also website docs **library** → *Decrypt failure bypass (learned tunnel)* (`ProxyServer.EnableDecryptFailureBypass`) and Inspector **Excluded hosts → Auto-tunnel on decrypt failure**. + ## Decrypting HTTPS To inspect HTTPS traffic the proxy generates per-host certificates signed by its own root certificate, which the client must trust. diff --git a/wiki/Performance-Local-Lab.md b/wiki/Performance-Local-Lab.md index 25515f625..a45ced692 100644 --- a/wiki/Performance-Local-Lab.md +++ b/wiki/Performance-Local-Lab.md @@ -2,7 +2,7 @@ > **For contributors** — local laptop cool A/B tables. Not publishable; do not compare these absolutes to [Performance](Performance). -Local Windows laptop debugging / cool A/B tables. **Not publishable** — do not compare these absolutes to [Performance](Performance) GHA tables. Use cool paired ratios as a gate, then remeasure on matched Windows+Linux GHA and paste CI medians onto Performance. +Local Windows laptop debugging / cool A/B tables. **Not publishable** — do not compare these absolutes to [Performance](Performance) GHA tables. Use cool paired ratios as a gate, then re-measure on matched Windows+Linux GHA and paste CI medians onto Performance. Playbook (harness, dumps, stage timing, Memory techniques) stays on [Performance Profiling](Performance-Profiling). @@ -30,7 +30,7 @@ Local debug setup and historical High-perf / cool-paired tables. **Do not paste | Harness | RpsLoadProbe Release; arms run **sequentially** | -This box is **8 logical / ~32 GiB** — not the 4 vCPU / 16 GiB GHA class. Treat ratios as the local gate; remeasure on CI before claiming a publishable win. +This box is **8 logical / ~32 GiB** — not the 4 vCPU / 16 GiB GHA class. Treat ratios as the local gate; re-measure on CI before claiming a publishable win. **Cool** = ~2 min idle, then paired A/B (alternate who goes first; **mean of both orders @ c=32**) — **authoritative local gate**; reverse tiny-GET / body cells below use those cool absolutes when cited. **Heated** = long sequential matrix (thermal skew). **🥇** = higher cool sustain in that row (or heated sustain only when no cool pair exists — noted). @@ -84,7 +84,7 @@ Reverse TWP/YARP cells for cool-audited arms are **cool paired means** (see note **TWP Memory / CPU** (heated 1-rep @ tip, `laptop-matrix-memory-20260824/`; warmup 2s / measure 8s; c=8–64): filled from compare-same / compare-bridges / compare-mitm peak-RPS step. Cool RPS cells unchanged. H2→H1 ~190–207 MiB (was ~425 laptop / ~848 CI); no outsized Memory arms vs prior H2 bag leak. Windows reverse tiny-GET: base matrix **2026-08-20** High-perf, Linux-matched harness (warmup 2s / measure 8s; concurrency 8, 16, 32, 64; median of 3 repeats except H2 TLS→H3 and H3→H1/H2, which have 2). CSVs under `tools/RpsLoadProbe/results/windows-20260820/` (`compare-same`, `compare-bridges`). MITM and heavier reverse: 1-repeat follow-up under `windows-20260820-quick/`. Absolute RPS swings with sequential-arm heat; prefer TWP÷YARP ratios. -**2026-08-21 remeasure (through exact-body + H3 QPACK-normalized names):** H1 plain, H1 TLS, H1→H2, H3→H1, H3→H2 refreshed as mean of both arm orders at c=32 (`win-final-`*). Exact-size H2 origin body materialize (no MemoryStream+ToArray) and `HeaderNamesAreHttp2Normalized` on the H3 fast Request. Other reverse Windows rows still **2026-08-20** unless noted. +**2026-08-21 re-measure (through exact-body + H3 QPACK-normalized names):** H1 plain, H1 TLS, H1→H2, H3→H1, H3→H2 refreshed as mean of both arm orders at c=32 (`win-final-`*). Exact-size H2 origin body materialize (no MemoryStream+ToArray) and `HeaderNamesAreHttp2Normalized` on the H3 fast Request. Other reverse Windows rows still **2026-08-20** unless noted. **2026-08-22 matrix fill (missing plain cells):** Library fix so cleartext-listen reverse (`DecryptSsl=false`) honors `ForwardCleartext=false` as origin HTTPS (H1 plain→HTTPS). New probe arms: `reverse-http1-to-https` / `yarp-reverse-http1-to-https`, `http-mitm` (explicit plain→plain). Full Windows `compare-same` + `compare-bridges` + plain twins under `tools/RpsLoadProbe/results/windows-20260822-matrix/` (1-rep; warmup 2s / measure 8s; c=8,16,32,64). @@ -92,7 +92,7 @@ Windows reverse tiny-GET: base matrix **2026-08-20** High-perf, Linux-matched ha **Load generators:** Reverse inbound H3 arms use `**dotnet-httpclient`** (`http_version=3.0`, `RequestVersionExact`) after dual-listen reverse H3. MITM H3→H2 / H3→H3 / H3→H1 plain reuse the same dual-listen transparent reverse path as their reverse twins (`ForwardCleartext` / decrypt knobs). Older UDP-only `quic-http3` MITM H3→H1 TLS numbers are dual-crypto extras (`mitm-http3-to-http1`). -**Matched HttpClient TWP÷YARP — table cells are cool absolutes where cited:** **parity audit** `win-parity-audit-20260822-004214/` (both orders @ c=32): H1 plain **41,390 / 38,772** ≈ **1.07×**; H1 TLS **35,205 / 29,750** ≈ **1.18×**; H1→H3 **21,819 / 20,712** ≈ **1.05×**; H3→H3 **26,299 / 14,942** ≈ **1.76×** (YARP soft — treat absolute cautiously). **2026-08-22 cool paste** `win-cool-paste-20260822-063226/` (both orders @ c=32): H1→H2 **25,540 / 24,920** ≈ **1.02×**; H1 plain→HTTPS **30,844 / 30,621** ≈ **1.01×**; h2c→H3 **27,493 / 24,535** ≈ **1.12×**; H2 TLS→H3 **30,813 / 24,039** ≈ **1.28×**; h2c→H1 **46,517 / 42,994** ≈ **1.08×**; H3→H1 **22,325 / 23,773** ≈ **0.94×**; H3→H2 **22,297 / 21,914** ≈ **1.02×**. **2026-08-23 soft coolish (both orders @ c=32, after session-lite H2/H3 gate):** H3→H1 ≈ **1.09×**; h2c→H1 ≈ **1.05×**; H1→H3 ≈ **1.25×**. Published CI Win bridges @ `11e32f1c` still show those three ≤1.00× — tip remeasure @ `62e5efcd` in flight. TWP-led H2 same-protocol rows unchanged (h2c↔h2c ≈ **1.17×**, etc.). +**Matched HttpClient TWP÷YARP — table cells are cool absolutes where cited:** **parity audit** `win-parity-audit-20260822-004214/` (both orders @ c=32): H1 plain **41,390 / 38,772** ≈ **1.07×**; H1 TLS **35,205 / 29,750** ≈ **1.18×**; H1→H3 **21,819 / 20,712** ≈ **1.05×**; H3→H3 **26,299 / 14,942** ≈ **1.76×** (YARP soft — treat absolute cautiously). **2026-08-22 cool paste** `win-cool-paste-20260822-063226/` (both orders @ c=32): H1→H2 **25,540 / 24,920** ≈ **1.02×**; H1 plain→HTTPS **30,844 / 30,621** ≈ **1.01×**; h2c→H3 **27,493 / 24,535** ≈ **1.12×**; H2 TLS→H3 **30,813 / 24,039** ≈ **1.28×**; h2c→H1 **46,517 / 42,994** ≈ **1.08×**; H3→H1 **22,325 / 23,773** ≈ **0.94×**; H3→H2 **22,297 / 21,914** ≈ **1.02×**. **2026-08-23 soft coolish (both orders @ c=32, after session-lite H2/H3 gate):** H3→H1 ≈ **1.09×**; h2c→H1 ≈ **1.05×**; H1→H3 ≈ **1.25×**. Published CI Win bridges @ `11e32f1c` still show those three ≤1.00× — tip re-measure @ `62e5efcd` in flight. TWP-led H2 same-protocol rows unchanged (h2c↔h2c ≈ **1.17×**, etc.). **Attempted H1→H3 micro-opts (2026-08-22, reverted):** Lowercasing H1 request names before QPACK + buffering tiny H3 origin bodies **without draining to FIN** **regressed** cool H1→H3 from ~1.13× to ~0.65× — kept out. **2026-08-23 kept:** same ≤64 KiB eager materialize in `ForwardOverQuicAsync` **plus drain-to-FIN before Dispose** (else RST poisons the QUIC pool → handshake-per-request; first attempt ~1.16×→~0.7×). Cool both orders ≈ **1.03–1.23×** (`cool-h3-origin-eager64-drain-20260823/`). Smoke H2→H3 / H3→H3 still lead; H3→H1 unchanged (~0.98× TY). @@ -123,7 +123,7 @@ nginx/Windows is a limited port — use it for **same-OS** comparison only, not nginx/Windows collapses on large reverse bodies in this harness; treat as same-OS only. -**2026-08-22 cool remeasure (bodies) — H2→H1 cells are cool means** (`win-bodies-coalesce288-20260822/`, both orders @ c=32): 64 KiB **7,744 / 6,844** ≈ **1.13×** → TWP leads; 256 KiB **1,935 / 2,179** ≈ **0.89×** → YARP leads. H1 TLS→H1 64 KiB still heated (marker follows heated). H3→H1 64 KiB cool ≈ **0.96×** → YARP leads. 256 KiB H1/H3: heated → YARP leads. +**2026-08-22 cool re-measure (bodies) — H2→H1 cells are cool means** (`win-bodies-coalesce288-20260822/`, both orders @ c=32): 64 KiB **7,744 / 6,844** ≈ **1.13×** → TWP leads; 256 KiB **1,935 / 2,179** ≈ **0.89×** → YARP leads. H1 TLS→H1 64 KiB still heated (marker follows heated). H3→H1 64 KiB cool ≈ **0.96×** → YARP leads. 256 KiB H1/H3: heated → YARP leads. ### POST 64 KiB request + 64 KiB response @@ -137,7 +137,7 @@ nginx/Windows collapses on large reverse bodies in this harness; treat as same-O | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **1,973** | **2,001** | *Not possible* | *Not possible* | **1,802** | **1,893** | -H1 POST: TWP leads (heated and cool). H2 POST: YARP leads — heated ≈ **0.74×**, cool ≈ **~0.88–0.95×** with c=1 TWP ahead (~**1.2×**); residual is multiplex scaling, not single-stream cost. **H3 POST (2026-08-22):** `UpdateContentLength` on streamed uploads stamped CL=0 (`ab16a871`). Heated remeasure `sustain0-verify/h3-post/` (c=8–64): TWP sustain **1,973** / YARP **1,802** ≈ **1.09×**. +H1 POST: TWP leads (heated and cool). H2 POST: YARP leads — heated ≈ **0.74×**, cool ≈ **~0.88–0.95×** with c=1 TWP ahead (~**1.2×**); residual is multiplex scaling, not single-stream cost. **H3 POST (2026-08-22):** `UpdateContentLength` on streamed uploads stamped CL=0 (`ab16a871`). Heated re-measure `sustain0-verify/h3-post/` (c=8–64): TWP sustain **1,973** / YARP **1,802** ≈ **1.09×**. ### Lossy / high-RTT (H2 HOL / H3 packet loss) @@ -168,7 +168,7 @@ H1 stays usable; H2 collapses under connection stalls (HOL). **H3 is the protoco | Duplex (both directions live) | HTTP/2 · TLS | HTTP/2 · TLS | **9** | **590** | *Not possible* | *Not possible* | 🥇 **2,455** | **2,455** | | Duplex (WebSocket / extended CONNECT) | HTTP/1 · TLS | HTTP/1 · plain | 🥇 **38,235** | **38,823** | **18,251** | **19,054** | **37,803** | **38,454** | -Slow consumer is sleep-bound (~16 × 8 ms per 256 KiB); H1/H2 sit in the same band. **H3 slow-consumer (2026-08-22):** fast path closed the origin socket for CL>16 KiB without `StreamBodyWriter` (`36d21f67`); remeasure `sustain0-verify/h3-slow/` matches YARP at **248** sustain. Early-response H1: TWP leads (~1.25× YARP) — sequential H1 still finishes the exchange quickly when the origin answers after 8 KiB. **H3 early-response (2026-08-22):** cool mean ≈ **1.21×** YARP after overlapping origin upload with `ReceiveResponse` / `StreamBodyWriter` (`fix-early-tls/`). Early-response H2 and duplex H2: YARP leads on heated matrix; TWP H2↔H2 duplex sustain **9** vs peak **590** (errors at higher concurrency) vs YARP **2,455**. WebSocket echo: TWP leads (~1.01× YARP); nginx/Windows same-OS only. +Slow consumer is sleep-bound (~16 × 8 ms per 256 KiB); H1/H2 sit in the same band. **H3 slow-consumer (2026-08-22):** fast path closed the origin socket for CL>16 KiB without `StreamBodyWriter` (`36d21f67`); re-measure `sustain0-verify/h3-slow/` matches YARP at **248** sustain. Early-response H1: TWP leads (~1.25× YARP) — sequential H1 still finishes the exchange quickly when the origin answers after 8 KiB. **H3 early-response (2026-08-22):** cool mean ≈ **1.21×** YARP after overlapping origin upload with `ReceiveResponse` / `StreamBodyWriter` (`fix-early-tls/`). Early-response H2 and duplex H2: YARP leads on heated matrix; TWP H2↔H2 duplex sustain **9** vs peak **590** (errors at higher concurrency) vs YARP **2,455**. WebSocket echo: TWP leads (~1.01× YARP); nginx/Windows same-OS only. ### TLS termination cost (H1 TLS → cleartext origin) diff --git a/wiki/Performance-Profiling.md b/wiki/Performance-Profiling.md index 9df9dfc45..c551e33ae 100644 --- a/wiki/Performance-Profiling.md +++ b/wiki/Performance-Profiling.md @@ -29,7 +29,7 @@ All throughput work starts from [RpsLoadProbe](https://github.com/justcoding121/ - Every TWP arm has a **control arm** — the managed reverse peer (and the native reverse peer where it can run the path) hosting the *identical* workload in the same session, so both sides see the same machine state. - Every `--ramp` arm is **three OS processes** (parent load generator + origin child + proxy child) with a parent-seeded loopback CA (`TWP_RPS_CERT_DIR`). Combined `--serve` is debug-only. Absolute RPS from older combined TLS/QUIC-origin cells is not comparable to split runs — prefer TWP÷peer ratios. - The probe **ramps concurrency** (typically c=8→64) and reports **sustainable RPS**: the last concurrency step that still met the error-rate and p99-latency SLO. A ramp that grows RPS but blows p99 is a queue, not throughput. -- Results land in timestamped CSVs under `tools/RpsLoadProbe/results/`. Publishable [Performance](Performance) tables cite **GitHub Actions** run IDs (median of 3 on matched 4 vCPU / 16 GiB runners). Cool paired A/B on a laptop proves a win before CI remeasure; result tables live on [Performance Local Lab](Performance-Local-Lab). +- Results land in timestamped CSVs under `tools/RpsLoadProbe/results/`. Publishable [Performance](Performance) tables cite **GitHub Actions** run IDs (median of 3 on matched 4 vCPU / 16 GiB runners). Cool paired A/B on a laptop proves a win before CI re-measure; result tables live on [Performance Local Lab](Performance-Local-Lab). ```powershell # one suite @@ -47,7 +47,7 @@ On a laptop, thermal throttling dominates everything else: the *same* arm measur - **Prefer TWP÷peer ratios over absolutes.** The control arm soaks up the same throttling. - **For a targeted A/B question, run the two arms paired**: cooldown (~2 min idle), arm A, arm B immediately after — and alternate which goes first across repeats so heat bias cancels. This is how "MITM costs 0.65–0.75× of its reverse twin, and the delta is purely the extra origin TLS leg" was established: the two probe arms differ by exactly one flag (`ForwardCleartext`). - If an arm's ratio looks newly bad, **re-measure before profiling** — several "regressions" were heat. -- **Gate before publishing:** optimize against cool paired ratios on the [local lab](Performance-Local-Lab). After a cool win, remeasure on matched Windows+Linux GHA (`workflow_dispatch` [RPS saturation](https://github.com/justcoding121/titanium-web-proxy/actions/workflows/rps-saturation.yml)). For wiki-grade `compare-product` / bodies / arch, dispatch **comparison-group** `arm_shard` partitions (`1/3`…`3/3`, etc.) on the **same SHA**, then paste with `-RunIds` / multi-run heavier union. Prefer TWP÷peer ratios; do not mix absolute RPS across shards. +- **Gate before publishing:** optimize against cool paired ratios on the [local lab](Performance-Local-Lab). After a cool win, re-measure on matched Windows+Linux GHA (`workflow_dispatch` [RPS saturation](https://github.com/justcoding121/titanium-web-proxy/actions/workflows/rps-saturation.yml)). For wiki-grade `compare-product` / bodies / arch, dispatch **comparison-group** `arm_shard` partitions (`1/3`…`3/3`, etc.) on the **same SHA**, then paste with `-RunIds` / multi-run heavier union. Prefer TWP÷peer ratios; do not mix absolute RPS across shards. ## Laptop result tables @@ -124,11 +124,11 @@ Systematic Windows reverse inventory (published TWP RSS > YARP) ranked H2→H1 a | H2 TLS→H3 | **139** MiB | **124** MiB | **~1.12×** | **~1.26×** (20.3k / 16.1k) | | h2c→H3 | **113** MiB | **119** MiB | **~0.95×** | **~1.10×** (21.1k / 19.1k) | -GC heap post-fix ~5.4 MiB; `VolatileNode[]` collapsed to one table (~0.6 MiB) with no Node storm. **GHA paste @ `571b6fba`:** Win H2→H1 Memory ÷YARP ~**1.04×** (95 / 92 MiB); Linux ~**0.99×**. H2→H3 / h2c→H3 cool remeasure ≤~1.2× Memory ÷YARP — **no further dig** on those arms (same keep). +GC heap post-fix ~5.4 MiB; `VolatileNode[]` collapsed to one table (~0.6 MiB) with no Node storm. **GHA paste @ `571b6fba`:** Win H2→H1 Memory ÷YARP ~**1.04×** (95 / 92 MiB); Linux ~**0.99×**. H2→H3 / h2c→H3 cool re-measure ≤~1.2× Memory ÷YARP — **no further dig** on those arms (same keep). Harness: `tools/RpsLoadProbe/profile-memory-arm.ps1` (mid-measure gcdump + Heap dump). Linux: `Dockerfile.mem-profile` + `profile-memory-arm.sh` in Docker with `libmsquic` (diagnosis only — publishable numbers stay GHA). -**H3→H1 cool remeasure (same session, `mem-audit-h3h1-post-synth`):** TWP **183** MiB / YARP **169** MiB ≈ **1.08×** Memory ÷YARP (RPS ~10.2k / ~6.4k — YARP soft on this box). GC heaps ~10 / ~9 MiB; no unbounded dict Node storm. Published CI was ~**1.57×** — laptop gap is already under the **≥1.3×** dig gate; residual treated as MsQuic / structural. **No H3 Memory code change** this pass; confirm with GHA Block C paste. +**H3→H1 cool re-measure (same session, `mem-audit-h3h1-post-synth`):** TWP **183** MiB / YARP **169** MiB ≈ **1.08×** Memory ÷YARP (RPS ~10.2k / ~6.4k — YARP soft on this box). GC heaps ~10 / ~9 MiB; no unbounded dict Node storm. Published CI was ~**1.57×** — laptop gap is already under the **≥1.3×** dig gate; residual treated as MsQuic / structural. **No H3 Memory code change** this pass; confirm with GHA Block C paste. ### Unjustified Memory dig gate (post-syntheticStreams) @@ -185,7 +185,7 @@ Dig through nginx `src/http` (proxy + upstream keepalive), `src/event`, and `src | Write coalesce + `sendfile` / `writev` chain | `ngx_output_chain.c`, `ngx_writev_chain.c` (“coalesce the neighbouring bufs”) | `Http2FrameWriter` coalesce budget (already have); H1 is already buffered `HttpStream` writes | **Done** for H2; H1 not a syscall-storm residual | | `worker_processes` + `accept_mutex` | `nginx.c` / `ngx_event_accept.c` | N/A — native multi-process fan-out; TWP is one managed process + thread pool | **N/A** — do not fake workers for RSS/RPS games | -**Concrete managed cut worth trying now:** **N/A** for new session-lites. Portable lessons are already landed or process-model only. Decode-time H2 session skip was **reverted**; the Windows Memory audit then found and **kept** the `syntheticStreams` registry leak fix (see [Memory (RSS)](#memory-rss--h2h1-vs-h1--h3)). Next dig is residual native RSS / MsQuic after GHA remeasure. +**Concrete managed cut worth trying now:** **N/A** for new session-lites. Portable lessons are already landed or process-model only. Decode-time H2 session skip was **reverted**; the Windows Memory audit then found and **kept** the `syntheticStreams` registry leak fix (see [Memory (RSS)](#memory-rss--h2h1-vs-h1--h3)). Next dig is residual native RSS / MsQuic after GHA re-measure. ## Technique 2: async dumps — find where requests wait @@ -228,7 +228,7 @@ dotnet-dump collect -p --type Full dotnet-trace collect -p --profile dotnet-sampled-thread-time --duration 00:00:25 ``` -This is a *confirmation* tool more than a discovery tool here: it confirmed the residual H1→H2 gap after origin-connection sharing is still whole-box cost (dual TLS legs plus the per-request session pipeline). Sharing lifted the arm from **0.33× to 0.53×** peer at c=32 (`rps-ramp-20260818-130040` / `130112`); cool remeasure after grow-at-4 stayed ~**0.51×** (`profile-baseline` / `profile-post-fix`). TTFB still rises with concurrency. At c=32 dumpasync showed **8** origin `ReadLoopAsync` instances (pool already spreading) plus `Monitor` / `SslStream` in the sampled stacks — not a single-conn convoy. Honest remainder: dual-TLS + session cost on this 8-thread box. +This is a *confirmation* tool more than a discovery tool here: it confirmed the residual H1→H2 gap after origin-connection sharing is still whole-box cost (dual TLS legs plus the per-request session pipeline). Sharing lifted the arm from **0.33× to 0.53×** peer at c=32 (`rps-ramp-20260818-130040` / `130112`); cool re-measure after grow-at-4 stayed ~**0.51×** (`profile-baseline` / `profile-post-fix`). TTFB still rises with concurrency. At c=32 dumpasync showed **8** origin `ReadLoopAsync` instances (pool already spreading) plus `Monitor` / `SslStream` in the sampled stacks — not a single-conn convoy. Honest remainder: dual-TLS + session cost on this 8-thread box. ## Technique 5: reference-source comparison @@ -278,17 +278,17 @@ The [architecture-sensitive](Performance-Local-Lab#architecture-sensitive) lapto | H1→H2 / H3→H2 still ≪0.80 after pool | Cool A/B + c=1 + `dumpasync`/`dotnet-trace` @ c=32 (`results/h2-origin-choke/`) | **Not** dual-TLS polish: c=1 TWP **faster** (1.49×). Residual is **outbound `Http2OriginConnection.SendAsync` queueing** (TTFB≈SendAsync wait grows 624→2263 µs c=8→32; 13 parked `SendAsync` on H3→H2; 102 `SemaphoreSlim` TaskNodes on H1→H2; managed reverse peer only ~7 in-flight forwarders). Monitor slow-path ~2× managed reverse peer | Origin `Http2FrameWriter` exclusive drain: encode+enqueue under short `writeLock`, no `WriteAsync` under the lock (reference .NET server stack model). Cool H1→H2 **0.87× @ c=32** (28,996 / 33,336, `rps-ramp-20260818-170412`/`170452`); H3→H2 **0.64× @ c=32**. TTFB p50 262→894 µs. See `h2-origin-choke/POSTFIX.md` | | H1→H2 / H3→H2 still <0.80 after origin frame writer | Cool A/B + grow A/B + gcdump/trace (`results/residual-sub08/`) | Scaling wait on origin HEADERS (c=1 **1.04×**); **grow 4→32 regresses**; ForceRead/HPACK noise; Channel/Pipe not retained-heap | Ranked in `residual-sub08/CONCLUSIONS.md` | | Monitor.Enter_Slowpath ~9.5% after frame writer | syncblk + speedscope + pool-pick diag; post-fix traces (`POSTFIX.md`) | **~70%** Monitor was `TryPick` + `ConcurrentDictionary.Count` under `entry.Gate`. **c=32:** 0% soft-miss. **c=64:** ~21% soft-miss + CreationGate at max | **Shipped A+B+C:** Interlocked `ActiveStreamCount`; skip CreationGate on Gate-held `Count >= max`; snapshot pick outside Gate. Monitor exclusive **9.5% → 3.1%**. Phase C no further win. Long-window TWP @ c=32 unchanged (~28.7k). Residual still HEADERS fan-in | -| H1→H2 still ~0.71× after pool-pick; dumpasync showed ForceRead on origin ReadLoop | Cool remeasure + code path (`POSTFIX-INTAKE.md`) | Origin ReadLoop still did ForceRead 9+payload and copied HEADERS to MemoryStream; DATA awaited BodyPipe on the loop | Shared `Http2FrameIntake` on origin + in-place END_HEADERS decode + sync BodyPipe write. **ForceRead removed.** Best long cool pair this session still **0.71×** (thermally soft absolutes) — next dig is post-headers path, not another receive rewrite | -| Post-intake: is residual WriteResponse / SessionEventArgs / still HEADERS wait? | dumpasync + topN + gcdump + stage timing (`POSTFIX-POST-HEADERS.md`) | Soft box (IDE CPU); dumpasync: **no** ForceRead, **no** InterimChannel/`SendAsync` park — bridges on client `ReadRequestLine`, origin on `FrameIntake.Fill`. Stage: TTFB ~93% of total, delivery ~5%. Pooling gates not cleared | **Wait shape fixed.** Do not pool or rewrite H1 write yet. Need cool quiet remeasure + high-RPS alloc/CPU sample before next code change | -| Quiet remeasure after restart: does cool ratio move? Gate A/B at high RPS? | Cool pairs + dumpasync + AllocationTick (`quiet-remeasure/QUIET-REMEASURE.md`) | High perf: H1→H2 c=32 **0.71×** (31.9k/44.7k); c=64 **0.87×**. High-RPS dump: ForceRead/Interim park still **0**. AllocTick: SessionEventArgs+HeaderCollection **4.5%** (<5% Gate A). Interim channel arrays ~7%+ but gated behind A. Monitor exclusive ~2.5% | **No library change.** c=32 residual confirmed; pooling/write gates still not cleared. Optional: YARP twin AllocTick for asymmetry | -| YARP twin AllocTick + InterimChannel passthrough lite | Twin `gc-verbose` + remeasure (`INTERIM-LITE.md`) | TWP ~3× AllocTicks/request vs YARP; Interim Channel/segment ~11% TWP-only. Lazy `InterimChannel` when `on1xx` null; H1→H2 passthrough skips relay when no interception | Landed. Soft post-lite pair **0.82×** (26.8k/32.5k); cool High-perf confirm blocked by IDE CPU — remeasure on quiet box before publishing ≥0.80 | +| H1→H2 still ~0.71× after pool-pick; dumpasync showed ForceRead on origin ReadLoop | Cool re-measure + code path (`POSTFIX-INTAKE.md`) | Origin ReadLoop still did ForceRead 9+payload and copied HEADERS to MemoryStream; DATA awaited BodyPipe on the loop | Shared `Http2FrameIntake` on origin + in-place END_HEADERS decode + sync BodyPipe write. **ForceRead removed.** Best long cool pair this session still **0.71×** (thermally soft absolutes) — next dig is post-headers path, not another receive rewrite | +| Post-intake: is residual WriteResponse / SessionEventArgs / still HEADERS wait? | dumpasync + topN + gcdump + stage timing (`POSTFIX-POST-HEADERS.md`) | Soft box (IDE CPU); dumpasync: **no** ForceRead, **no** InterimChannel/`SendAsync` park — bridges on client `ReadRequestLine`, origin on `FrameIntake.Fill`. Stage: TTFB ~93% of total, delivery ~5%. Pooling gates not cleared | **Wait shape fixed.** Do not pool or rewrite H1 write yet. Need cool quiet re-measure + high-RPS alloc/CPU sample before next code change | +| Quiet re-measure after restart: does cool ratio move? Gate A/B at high RPS? | Cool pairs + dumpasync + AllocationTick (`quiet-re-measure/QUIET-RE-MEASURE.md`) | High perf: H1→H2 c=32 **0.71×** (31.9k/44.7k); c=64 **0.87×**. High-RPS dump: ForceRead/Interim park still **0**. AllocTick: SessionEventArgs+HeaderCollection **4.5%** (<5% Gate A). Interim channel arrays ~7%+ but gated behind A. Monitor exclusive ~2.5% | **No library change.** c=32 residual confirmed; pooling/write gates still not cleared. Optional: YARP twin AllocTick for asymmetry | +| YARP twin AllocTick + InterimChannel passthrough lite | Twin `gc-verbose` + re-measure (`INTERIM-LITE.md`) | TWP ~3× AllocTicks/request vs YARP; Interim Channel/segment ~11% TWP-only. Lazy `InterimChannel` when `on1xx` null; H1→H2 passthrough skips relay when no interception | Landed. Soft post-lite pair **0.82×** (26.8k/32.5k); cool High-perf confirm blocked by IDE CPU — re-measure on quiet box before publishing ≥0.80 | | Cool confirm after InterimChannel lite | Paired c=32 High perf (`interim-lite-confirm/CONFIRM.md`) | TWP **33.6k** / YARP **44.0k** = **0.76×** (was **0.71×** pre-lite). Phase-A-class absolutes | Lite helped (~+5–7% relative) but still ≪0.80. Next: TTFB residual dig on no-intercept path | | Post-lite TTFB dig @ ~31k RPS | dumpasync `--fields` + topN (`interim-lite-confirm/TTFB-DIG.md`) | **20** `SendAsync` on origin **writeLock** (Semaphore maxCount=1, `streamOpened=false`); **6** on `HeadersReceived`; InterimChannel still 0. Lite `on1xx=null` confirmed | Residual is **writeLock stream-open convoy**, not headers wait / WriteResponse. Next: shrink work under origin writeLock (HPACK encode+enqueue) | -| H3→H2 cool remeasure + gap fix plan | Cool c=32 pair (`h3h2-fresh/CONFIRM.md`) + `FIX-PLAN.md` / canvas | H3→H2 **0.70×** (26.0k/36.9k) — wiki 0.33× stale. Same origin writeLock; H3 still always allocates InterimChannel | **P0** H3 Interim lite → **P1** shrink encode under writeLock → **P2** H3 Via/prep trim → **P3** remeasure other H2 arms | -| P0+P1 bundle: H3 lite, Via skip, SoftStream=2, HPACK method cache | Cool High perf (`post-p0p1/`) | H1→H2 **0.89×** (37.3k/42.0k); soft confirm **0.82×**. H3→H2 **0.73×** (24.9k/34.2k). Max-conn 16 aborted (soft regress) | **H1→H2 c=32 bar closed.** Continue H3→H2 (≥0.80) + remeasure other H2 arms | -| Remeasure H2 TLS→h2c / h2c→h2c after intake+lite era | Cool High perf c=32 20s (`passthrough-fresh/`) | H2 TLS→h2c **0.78×** (51.0k/65.8k); h2c→h2c **0.73×** (49.7k/68.3k) — up from ~0.66/0.70 wiki | Still ≪0.80 on passthrough; next dig client FrameWriter/HPACK (not origin pool) | +| H3→H2 cool re-measure + gap fix plan | Cool c=32 pair (`h3h2-fresh/CONFIRM.md`) + `FIX-PLAN.md` / canvas | H3→H2 **0.70×** (26.0k/36.9k) — wiki 0.33× stale. Same origin writeLock; H3 still always allocates InterimChannel | **P0** H3 Interim lite → **P1** shrink encode under writeLock → **P2** H3 Via/prep trim → **P3** re-measure other H2 arms | +| P0+P1 bundle: H3 lite, Via skip, SoftStream=2, HPACK method cache | Cool High perf (`post-p0p1/`) | H1→H2 **0.89×** (37.3k/42.0k); soft confirm **0.82×**. H3→H2 **0.73×** (24.9k/34.2k). Max-conn 16 aborted (soft regress) | **H1→H2 c=32 bar closed.** Continue H3→H2 (≥0.80) + re-measure other H2 arms | +| Re-measure H2 TLS→h2c / h2c→h2c after intake+lite era | Cool High perf c=32 20s (`passthrough-fresh/`) | H2 TLS→h2c **0.78×** (51.0k/65.8k); h2c→h2c **0.73×** (49.7k/68.3k) — up from ~0.66/0.70 wiki | Still ≪0.80 on passthrough; next dig client FrameWriter/HPACK (not origin pool) | | HPACK static GetIndex bug + encode under writeLock + scheme patch | Cool High perf (`post-hpack-static/` + `post-hpack-confirm/`) | `StaticTable.GetIndex(name,value)` compared ByteString to string → never matched; EncodeHeaderBlock allocated `new Uri` under writeLock; mixed-transport scheme 0x86↔0x87 patch; SoftStream=1; skip Via on H2 response IsFastPath; skip NoOp HPACK decode on verbatim compressed relay | **H2 TLS→h2c 0.81×** (45.8k/56.2k) **closed**. H1→H2 **0.85×**. H3→H2 **0.72×**, h2c→h2c **0.74×** still open | -| OriginRelayPool SoftCap 8→1/2 fan-out | Cool remeasure (`post-relay-soft1/2`) | Soft=1/2 did not beat Soft≈8 on h2c→h2c (extra cleartext legs) | **Reverted** SoftCap formula; residual is not origin-leg count | +| OriginRelayPool SoftCap 8→1/2 fan-out | Cool re-measure (`post-relay-soft1/2`) | Soft=1/2 did not beat Soft≈8 on h2c→h2c (extra cleartext legs) | **Reverted** SoftCap formula; residual is not origin-leg count | | H3→H2 dump @ 26k RPS + QPACK dict encode | dumpasync (`h3-profile/`) + QPACK O(1) static lookup | **32/32** `SendAsync` on `HeadersReceived` (not writeLock); **8** origin `ReadLoop`s. SoftStream fan-out already enough | Residual is H3 session/QPACK/bridge CPU, not origin write convoy. QPACK static dict + response header path trim shipped; cool ratio still ≪0.80 — next SessionEventArgs-lite / pool | | H3 inbound ≪ H2 / ≪ YARP H3→H3 | Cool YARP-first matrix + shape (`h3-vs-h2/`, `h3-verbatim-fair/`) | Full `SessionEventArgs` + response QPACK decode/re-encode on every H3→H3 GET; YARP cool H3→H3 ~26–28k while TWP sat ~20k (0.70×) | Session-lite for H3→H2/H3/H1 + **verbatim origin→client H3 frame relay** (H2 compressed-relay analogue). Cool H3→H3 **1.14× YARP** (29.6k / 26.0k); H3→H2 / H3→H1 ≥0.80; MITM÷cleartext **0.93** | | H3 bodiless fast path + PrepareH2 skip + EncodeResponse + compressed DATA→wire | Cool High perf (`post-encode-response/`) | Skip InterceptionContext; drain FIN without body-pump lambdas; skip PrepareH2 RemoveHeader scan on IsFastPath; `QpackEncoder.EncodeResponse` (no List); compressed-relay DATA `ReadExact` into rented wire buffer; ReturnPayload after QPACK decode | Absolutes up (H3→H2 **31.7k**/44.1k; h2c→h2c **65.7k**/91.3k) but ratios still **~0.72×**. Lazy `BoundedBodyPipe` aborted (empty-body race). Skip linked-CTS on fast path aborted (abort cancel lost → ~0.67×). Next: SessionEventArgs-lite / pool | @@ -305,30 +305,30 @@ The [architecture-sensitive](Performance-Local-Lab#architecture-sensitive) lapto | Cool H3→H1 ~0.36× peer (12.1k / 33.4k) | Cool pair + trace @ c=32 | **Invalid ratio**: TWP `quic-http3` vs peer HttpClient. Trace was session/`HandleAsync`, not MsQuic-native | Match clients; later dual-listen reverse H3 enables **HttpClient both sides** (`matched-httpclient-h3/`, H3→H1 ≈ **0.87**) | | H3→H1 integ empty body; Windows ~0.79× YARP | DualListen / ForcedHttp11Origin + cool pair | Fast path buffered only known `Content-Length`; Kestrel `WriteAsync` often **chunked** → body never drained before pool Release; H1 Title-Case names paid QPACK `ToLower` every response | Drain chunked/connection-close via `LimitedStream` before Release; `NormalizeNamesToLowerAscii` + `HeaderNamesAreHttp2Normalized`; decode H2 HEADERS into Response headers (no second collection). Cool Windows H3→H1 ≈ **0.96×**, H3→H2 ≈ **1.06×**; Linux H3→H1/H2/H3 ≈ **1.04× / 1.15× / 1.20×** ([32552296839](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32552296839), [32552295495](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32552295495)) | | H1→H3 “name normalize + tiny-body coalesce” looked like H1→H2 gap | Cool A/B (`win-parity-audit-20260822-*`) | Hypothesis: Title-Case QPACK tax + missing H1→H2-style fast commit / TLS coalesce | Name-normalize path **fully reverted** (~**1.13× → 0.65×**). Eager-buffer alone also poisoned the pool — see next row. | -| H1→H3 Win CI ~0.94×; H1 client + StreamBodyWriter = header-only TLS record | Cool A/B + dispose/RST dig (`cool-h3-origin-eager64-drain-20260823/`) | Known-CL ≤64 KiB H3 origin bodies streamed via `StreamBodyWriter` → H1 `WriteResponseAsync` then body (same class as lossy H1). First eager-buffer attempt disposed the Quic stream before FIN → **RST / pool poison** (~1.16×→~0.7×) | Eager-buffer ≤64 KiB **and drain frames to FIN** before `DisposeAsync` in `ForwardOverQuicAsync`. Cool TY/YT ≈ **1.23× / 1.03×**. CI remeasure bridges next. | +| H1→H3 Win CI ~0.94×; H1 client + StreamBodyWriter = header-only TLS record | Cool A/B + dispose/RST dig (`cool-h3-origin-eager64-drain-20260823/`) | Known-CL ≤64 KiB H3 origin bodies streamed via `StreamBodyWriter` → H1 `WriteResponseAsync` then body (same class as lossy H1). First eager-buffer attempt disposed the Quic stream before FIN → **RST / pool poison** (~1.16×→~0.7×) | Eager-buffer ≤64 KiB **and drain frames to FIN** before `DisposeAsync` in `ForwardOverQuicAsync`. Cool TY/YT ≈ **1.23× / 1.03×**. CI re-measure bridges next. | | H2→H1 64 KiB ~0.87× YARP (tiny-GET already parity) | Cool pair + code compare vs Kestrel/YARP | Streamed path stripped Content-Length then empty END_STREAM DATA; pump wrote 8 KiB fills → 8 DATA frames + trailer; `HttpStream` double-buffered socket→8 KiB→dest; QueueDataFrame + 32 KiB flatten | Keep CL + END_STREAM on last DATA; `HttpStream` large-read bypass; in-place DATA framing (flatten **kept**); skip LimitedStream/Via on known-CL fast path; raise flatten budget to **288 KiB**. Cool 64 KiB ≈ **1.13×**; 256 KiB ≈ **0.89×**. Dropping flatten alone still ~0.65× | | H2 POST cool ~0.88× / 256 KiB H2→H1 ~0.90× | Shape c=1 vs c=32 + YARP `StreamCopier` (64 KiB) compare | c=1 TWP **leads** POST (~1.2×); c=32 loses when YARP healthy — multiplex tax (frame-loop copy + shared client writer). Extra body memcpy / coalesce experiments | **Kept:** ArrayPool request-body channel + `TryReserve` on `CopyFromAsync`. **Do not:** reserve >1 frame before enqueue; slice control frames into coalesced DATA; drop flatten | | H3 early-response Win CI ~0.76× (Linux already ~1.02×) | Cool A/B (`fix-early-tls/`) + origin/YARP duplex compare | `ForwardOverTcpAsync` wrote the full request body before `ReceiveResponse` while the probe origin overlaps after 8 KiB (YARP `StreamCopier` same). H3+MsQuic amplifies the serialization on Windows | Overlap streamed upload with `ReceiveResponse`; fold remaining upload into `StreamBodyWriter` via `Task.WhenAll`. Cool mean ≈ **1.21×** YARP. Do **not** re-land Http3Frame coalesce 256→16 KiB (hurt POST) | | Duplex H2 Win CI ~0.63× / Linux ~0.31× (`compare-arch`) | Code path + CI medians [32688089789](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32688089789); short local cool noisy | H2 TLS↔H2 TLS overlapping 64 KiB POST. Interception-off reverse already concurrent-relays frames both ways (not H1 sequential; not the H3 pre-overlap `ForwardOverTcpAsync` bug). YARP `HttpForwarder`/`StreamCopier` + Kestrel still leads; TWP CPU underutilized vs YARP on the cell | **No product cut.** Document as irreducible YARP-led concurrent-copier cell; keep published CI ratios. Do **not** port Kestrel/`StreamCopier` for this row alone. | -| H3→H1 64 KiB GET Win CI ~0.56× / Linux ~0.82× | Cool A/B (`h3-64k-rebaseline/`) + CI remeasure | Cool mean ≈ **1.13×** (3118/2688 & 3488/3181); stale CI was pre-`StreamBodyWriter` | No library change. Publishable [32611185635](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32611185635) @ `cd276c83`: Win ≈ **1.15×** (3,752 / 3,269), Linux ≈ **1.25×** (5,295 / 4,247). Next body gap: Win H1 TLS 256 KiB ≈ **0.85×** | +| H3→H1 64 KiB GET Win CI ~0.56× / Linux ~0.82× | Cool A/B (`h3-64k-rebaseline/`) + CI re-measure | Cool mean ≈ **1.13×** (3118/2688 & 3488/3181); stale CI was pre-`StreamBodyWriter` | No library change. Publishable [32611185635](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32611185635) @ `cd276c83`: Win ≈ **1.15×** (3,752 / 3,269), Linux ≈ **1.25×** (5,295 / 4,247). Next body gap: Win H1 TLS 256 KiB ≈ **0.85×** | | H1 TLS→H1 256 KiB Win CI ~0.85× | Cool A/B + shape (`h1-256k-cool/`) + YARP StreamCopier compare | Cool c=1 ≈ **0.83×** (per-request); `CopyBytesToStream` FillBuffer’d **8 KiB** forever — H2 large-read bypass never ran on H1 known-CL copy | Rent **64 KiB** + `ReadAsync` when parser window empty (`HttpStream.CopyBytesToStream`, `106e73b9`). Cool c=1 ≈ **1.16×**, c=32 ≈ **1.09×**. Publishable [32614286032](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32614286032): Win ≈ **1.12×** (2,617 / 2,347). | | H1 TLS new-conn Win CI ~0.84x (Linux TWP leads) | Cool A/B + Kestrel SocketConnectionListener / ConnectionDispatcher + bare ceiling | Nested SslStream + ClientHello peek + ECDSA + Task.Run + BeginAccept APM + per-accept linger/timeouts + RetryPolicy closures; **lite path forwarded `Connection: close` to origin** → no origin pool under NC | Peek/unwrap/RSA/no-keepalive; abortive SO_LINGER(0) on close; AcceptAsync; CTS pool; session-lite; WaitForData-before-SslStream; 8 KiB rent. AcceptIOQueue **no win**. Bare NC `Connection: close` response-skip fixed. **Strip hop-by-hop Connection before origin write** on H1 terminate lite. Publishable [32625349927](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32625349927) @ `13059143`: Win NC ≈ **1.01×**, Linux NC ≈ **1.01×** YARP (nginx 1st on Linux NC — TWP 2nd). | | H1→H3 100% err after session-lite (`03159694`) | Bisect `11e32f1c`→`03159694` + curl serve | H1 terminate lite gated only on `ForwardHost` + bodiless GET — **H1→H3/H2** with forced upstream H3/H2 took TCP H1 lite against QUIC/h2-only origins | Skip session-lite when connection-level `UpstreamHttpProtocol` is Http2/Http3 (`62e5efcd`). Soft coolish H1→H3 ≈ **1.25×**, h2c→H1 ≈ **1.05×**, H3→H1 ≈ **1.09×**. | -| WarmTls H1→H3 + CachedServerAuthOptions gate broke H2 reverse | Local lossy H2 + ALPN fail (`No common application protocol`) | Expanding fixed-cert to any warmed `CachedServerAuthOptions` pinned **http/1.1-only ALPN** while H2 clients offer `h2` | Gate fixed-cert on `!EnableHttp2` only (H3 clients use QuicListener). H1→H3 host: `EnableHttp2=false` + WarmTls (`8ac422ee`). Cool H1→H3 ≈ **1.16×**; remeasure bridges/bodies/lossy @ tip. | +| WarmTls H1→H3 + CachedServerAuthOptions gate broke H2 reverse | Local lossy H2 + ALPN fail (`No common application protocol`) | Expanding fixed-cert to any warmed `CachedServerAuthOptions` pinned **http/1.1-only ALPN** while H2 clients offer `h2` | Gate fixed-cert on `!EnableHttp2` only (H3 clients use QuicListener). H1→H3 host: `EnableHttp2=false` + WarmTls (`8ac422ee`). Cool H1→H3 ≈ **1.16×**; re-measure bridges/bodies/lossy @ tip. | | H2/H3→H1 64 KiB / lossy H2 still tax many DATA fills | Code compare vs H1 ≤64 KiB coalesce | H2→H1 / H3→H1 eager-buffer capped at **16 KiB** while lossy/bodies GET is **64 KiB** → stream via ~4× 16 KiB fills (shim delayMs per read) | Raise eager known-CL threshold to **64 KiB** (`Http2ToHttp11BridgeHandler` + `Http3OriginBridge`, `8ac422ee`). CI bodies Win H3→H1 64 KiB ≈ **1.09×** ([32631121563](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32631121563)). | | H3→H1 tiny Win CI ~0.90×; cool order-noisy ~0.94–1.02× | Cool both-order dig + HEADERS+DATA coalesce experiment | `SendResponseAsync` does separate QuicStream writes for HEADERS then DATA + Flush | **Reverted** single-write HEADERS+DATA coalesce (`cool-h3-headers-data-coalesce-20260823/`): cool mean ≈ **0.96×** (no win). | -| H3→H1 c=1 leads ~1.25×; c=32 loses (~0.96×) — multiplex shape | Cool c=1 both orders + `dumpasync` @ c=32 (`dig-h3h1-dump2-20260823/`) + SampleProfiler | **32/32** parked on origin `ReceiveResponse`→`FillBuffer` (not writeLock/SoftCap). CPU: both TWP and YARP ~60% `LowLevelLifoSemaphore` wait — not a unique ThreadPool starve. QPACK/Normalize ≪1% exclusive | Next: cycle-time after status line (body buffer → QPACK → Quic write → pool Release) vs YARP `HttpForwarder`; optional AllocTick asymmetry. Soft remeasure @ `8e5c181b` (`cool-h3h1-shape-20260823/`): c=1 ≈ **1.60×**, c=32 ≈ **1.07×** — CI Win still ~0.89× @ prior tip; remeasure bridges in flight. | +| H3→H1 c=1 leads ~1.25×; c=32 loses (~0.96×) — multiplex shape | Cool c=1 both orders + `dumpasync` @ c=32 (`dig-h3h1-dump2-20260823/`) + SampleProfiler | **32/32** parked on origin `ReceiveResponse`→`FillBuffer` (not writeLock/SoftCap). CPU: both TWP and YARP ~60% `LowLevelLifoSemaphore` wait — not a unique ThreadPool starve. QPACK/Normalize ≪1% exclusive | Next: cycle-time after status line (body buffer → QPACK → Quic write → pool Release) vs YARP `HttpForwarder`; optional AllocTick asymmetry. Soft re-measure @ `8e5c181b` (`cool-h3h1-shape-20260823/`): c=1 ≈ **1.60×**, c=32 ≈ **1.07×** — CI Win still ~0.89× @ prior tip; re-measure bridges in flight. | | H3→H1 early origin Release / skip client Flush before CompleteWrites | Cool A/B (`cool-h3h1-early-release-20260823/`, `cool-h3h1-skip-flush-20260823/`) | Hypothesis: free H1 socket sooner / avoid MsQuic Flush tax on fast path | **No cool win** (~0.95–0.98×). Reverted both. | | H1→H3 / h2c→H3 Win CI closed @ `8789d6de` | CI bridges [32636039240](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32636039240) | Eager ≤64 KiB H3 origin body + FIN drain before Dispose | Win H1→H3 ≈ **1.04×**, h2c→H3 ≈ **1.05×**. Still open: Win h2c→H1 ≈ **0.96×**, H3→H1 ≈ **0.89×**; lossy H2 16 vs 17. | | H3→H1 ForwardHost Host rewrite (match YARP HttpForwarder) | Cool A/B (`cool-h3h1-host-rewrite-20260823/`) | Hypothesis: :authority `localhost:` on H1 Host wire vs origin `127.0.0.1:` | **No cool win** (~0.94–0.99×). Reverted. | -| h2c→H1 early origin Release before EmitSynthetic | Cool lead ~1.03×; CI remeasure [32638840153](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32638840153) @ `253e8716` | Hypothesis: free H1 socket before H2 frame emit | **CI miss**: Win h2c→H1 still ~**0.95×**; **Lin h2c→H1 regressed** ~1.03×→**0.96×**. Reverted. | +| h2c→H1 early origin Release before EmitSynthetic | Cool lead ~1.03×; CI re-measure [32638840153](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32638840153) @ `253e8716` | Hypothesis: free H1 socket before H2 frame emit | **CI miss**: Win h2c→H1 still ~**0.95×**; **Lin h2c→H1 regressed** ~1.03×→**0.96×**. Reverted. | | h2c→H1 ForwardHost Host rewrite | Cool A/B (`cool-h2ch1-fwdhost2-20260823/`) | Same Host rewrite on H2→H1 bridge | **No clear cool win** (YT ~0.97×). Reverted. | -| Lossy H1 Win remeasure @ `253e8716` ≈ **1.00×** (662/662) | CI lossy [32638842839](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32638842839) | Prior ~0.99× noise | Closed **≥1.00×** (nginx 1st 634 — TWP 2nd). Win lossy H2 still 16 vs 17. | -| Lossy H2 HOL: NullOriginStream lacked SETTINGS_MAX_CONCURRENT_STREAMS | Cool + CI lossy [32643126466](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32643126466); bridges tax @ same tip | Hardcoding MaxStreams=8 on NullOriginStream closed lossy (Win **3.47×**) but tax'd Win tiny-GET h2c→H1 / H2→H1 (~0.90×) via extra TCP handshakes at c=64. | **Landed** lossy-only: probe sets `ResourceLimits.MaxConcurrentStreamsPerConnection=8` when `IsLossy` (`WithMaxConcurrentStreams…`); Http2Helper appends SETTINGS. Tiny-GET keeps default 256. Cool lossy **29/15**; cool h2c ≈ **1.04×**. Remeasure lossy+bridges. Dispose harden on `TcpClientConnection` kept. | +| Lossy H1 Win re-measure @ `253e8716` ≈ **1.00×** (662/662) | CI lossy [32638842839](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32638842839) | Prior ~0.99× noise | Closed **≥1.00×** (nginx 1st 634 — TWP 2nd). Win lossy H2 still 16 vs 17. | +| Lossy H2 HOL: NullOriginStream lacked SETTINGS_MAX_CONCURRENT_STREAMS | Cool + CI lossy [32643126466](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32643126466); bridges tax @ same tip | Hardcoding MaxStreams=8 on NullOriginStream closed lossy (Win **3.47×**) but tax'd Win tiny-GET h2c→H1 / H2→H1 (~0.90×) via extra TCP handshakes at c=64. | **Landed** lossy-only: probe sets `ResourceLimits.MaxConcurrentStreamsPerConnection=8` when `IsLossy` (`WithMaxConcurrentStreams…`); Http2Helper appends SETTINGS. Tiny-GET keeps default 256. Cool lossy **29/15**; cool h2c ≈ **1.04×**. Re-measure lossy+bridges. Dispose harden on `TcpClientConnection` kept. | | Lossy H1 Win cool ~0.86× (p50 +16 ms vs YARP) | Cool A/B + userspace delay shim analysis | Fast-path `WriteResponse` then `CopyBody` emitted a **header-only TLS record** before body; shim pays `delayMs` per read → ~3 extra 5 ms trips | Materialize known-CL ≤64 KiB on fast path + coalesce headers+body (`bc768069`). Cool ≈ **1.00×**; CI [32620889168](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32620889168): Win **663/664**, Linux **1199/1196**. | | GHA `compare-post`/`compare-arch` failed; laptop H3 POST/slow passed | Failed run logs ([32602145518](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32602145518), [32602146550](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32602146550)) | (1) Dual-listen: TCP ephemeral then QUIC UDP same port → Windows `WSAEADDRINUSE` when UDP busy/excluded. (2) Incomplete `StreamBodyWriter` + `DataAvailable==0` pooled origin sockets with unread CL → next request `H3_INTERNAL_ERROR` (HeadersRead slow-consumer + warmup cancel amplifies on 4 vCPU) | Retry ephemeral TCP+QUIC bind in `ProxyServer.Start`/`AddEndPoint`; always close origin on incomplete StreamBodyWriter; YARP/nginx dual-stack free-port pick | | H3→H1 latency bundle (skip drain / skip Flush / HEADERS+DATA coalesce) | Cool absolute win (`cool-h3h1-latency-bundle-20260823/`) + CI bridges [32652931261](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32652931261) @ `3f948409` | Cool c=64 TWP ~25k (tip ~20–23k); laptop YARP ~30k → cool TY ~0.85×. Trace @ c=64: p50 gap not exclusive CPU. | **CI miss**: Win H3→H1 **0.92× → ~0.87×** (13,391 / 15,444). Lin H3→H1 still leads (~1.11×). Lossy Lin H3 improved ~0.76×→~**0.89×** (278/314) but still <1.00×. **Reverted** (`2bf18d75`). | -| H2→H1 Memory ~5–9× YARP at RPS parity | Saturation RSS sampler + bag lifetime analysis | `ConcurrentBag` PendingSynthetics/Finalizations retained completed Tasks (session closures) for client H2 conn life; full SessionEventArgs per stream on IsFastPath | **`Http2PendingWork`** remove-on-complete; H2→H1 warm `TryRentPooled` + `HeaderBuilder` wire (H3→H1 analogue). Remeasure Block B Memory. Do not shrink windows / single-conn. | +| H2→H1 Memory ~5–9× YARP at RPS parity | Saturation RSS sampler + bag lifetime analysis | `ConcurrentBag` PendingSynthetics/Finalizations retained completed Tasks (session closures) for client H2 conn life; full SessionEventArgs per stream on IsFastPath | **`Http2PendingWork`** remove-on-complete; H2→H1 warm `TryRentPooled` + `HeaderBuilder` wire (H3→H1 analogue). Re-measure Block B Memory. Do not shrink windows / single-conn. | | H3→H1 Win ~0.993× residual after one-pass QPACK | Cool A/B + gen0 on post-status path | Per-response `MemoryStream` QPACK builder + Latin-1 string round-trip + `new byte[]` tiny body | ThreadStatic **`ResponseBlockBuilder` rent**; span `AddHeader`; BufferPool body when `Available` covers CL. Bridges CI @ `0ff3673c` / [32685354747](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32685354747): Win **1.12×** (27,046 / 24,085); Linux **1.09×**. | | H3→H3 Win CI ~0.85× YARP; MITM Full÷Reverse ≪0.80 on H2/H3/H1 arms ([32960766249](https://github.com/justcoding121/titanium-web-proxy/actions/runs/32960766249)) | Cool paired A/B (`h3h3-dig/`) + `compare-product` MITM arms | H3→H3: per-frame QUIC I/O. MITM Full: probe-only fast path coupled product code to RPS harness header name; H2 `!wouldInjectVia` bail; H3 Via gate blocked preencoded relay with default pseudonym | **Landed v1** @ `df172718`: `MitmCompressedRelayHelper` append-only (max 4 unique headers); H2 zero-copy append suffix; all MITM arms ≥ **0.70×** on GHA median ([33041445371](https://github.com/justcoding121/titanium-web-proxy/actions/runs/33041445371) ×3). **Landed v2** @ `acfb27e1` ([#981](https://github.com/justcoding121/titanium-web-proxy/pull/981)): `MitmStaticRebuildHelper` drop-only static rebuild (max 4 unique drops) + non-unique trailing append (second `Set-Cookie`); modify value / body still full re-encode. Three MITM tiers: **Lite** (unchanged relay), **Append/Strip-lite** (append or drop-only on static GET), **Full session** (modify/body/multi-edit). v2 GHA medians @ `acfb27e1` — Win [33087085235](https://github.com/justcoding121/titanium-web-proxy/actions/runs/33087085235)/[88466](https://github.com/justcoding121/titanium-web-proxy/actions/runs/33087088466)/[91622](https://github.com/justcoding121/titanium-web-proxy/actions/runs/33087091622), Lin [88466](https://github.com/justcoding121/titanium-web-proxy/actions/runs/33087088466)/[91622](https://github.com/justcoding121/titanium-web-proxy/actions/runs/33087091622)/[33105885748](https://github.com/justcoding121/titanium-web-proxy/actions/runs/33105885748); all MITM arms ≥ **0.70×**; one Lin arm (H1 TLS→H2 plain Full÷Reverse) −6% vs v1 (0.931 vs 0.991), within harness noise. | | H3→H1 sticky ConcurrentBag TCP pool (bypass factory poolLock) | Cool tip-vs-sticky both orders @ c=32 (`cool-sticky-vs-tip-20260823/`) | Hypothesis: CI 4 vCPU multiplex tax on poolLock/queue | **Cool flat** (~1.07× both). Reverted. | diff --git a/wiki/Performance.md b/wiki/Performance.md index a38fc7f63..f3d64ca11 100644 --- a/wiki/Performance.md +++ b/wiki/Performance.md @@ -11,7 +11,7 @@ For pooling knobs and certificate first-visit tuning, see [Performance and pooli - Same load generator, same origin process, and the same warmup / measure windows (2s / 8s) with the same concurrency ramp (8, 16, 32, 64). - Every reverse arm is three OS processes: load generator + origin + proxy. Origin-direct omits the proxy; peers are never in-process with the client. - Same runner class per table (`windows-latest` / `ubuntu-latest` / `macos-15-intel`). Laptop numbers are never mixed into these tables. -- Peers use equivalent TLS/ALPN and streaming-friendly settings on the same loopback shape. HAProxy and Envoy are Linux/macOS only — Windows cells are *Not possible*. +- Peers use equivalent TLS/ALPN and streaming-friendly settings on the same loopback shape. HAProxy and Envoy are Linux/macOS only — *Not possible* on Windows, so their columns are omitted there. - MITM (HTTPS decryption with forged certificates) is Titanium-only; peers cannot MITM. Those tables show Titanium MITM overhead versus its own reverse path on the same wires. - **Tiny keep-alive GET** (~56-byte JSON) is the industry RPS shape (same class as wrk / TechEmpower). It is also real for small JSON APIs and health checks. - **Same-protocol H2↔H2 / H3↔H3** on that shape is Titanium’s **best case**: with interception off, Titanium copies frames instead of decoding and re-encoding headers (peers do a full HTTP decode). Medals there are not the typical reverse-proxy job. @@ -19,10 +19,10 @@ For pooling knobs and certificate first-visit tuning, see [Performance and pooli ## How to read the tables -- **Sustain** = last concurrency that still met error/latency SLOs. **Peak** = highest RPS in that ramp. +- Bold RPS is **sustain** (last concurrency that still met error/latency SLOs). When peak differs, it appears in the same cell as `(peak N · …)` with RSS/CPU. When sustain equals peak, peak is omitted. - 🥇 = best among Titanium / nginx / YARP on Reverse rows (highest RPS; on a tie, lower memory then lower CPU%). Gold medals are **per cell on tiny GET** — an H2↔H2 gold is that frame-copy best case, not “Titanium is 1.7× on all reverse.” For larger-body H2→H2, see the [heavier tables](#heavier-reverse-workloads). - **MITM** tables are Titanium-only. **Lite÷Reverse** / **Full÷Reverse** = Titanium MITM sustain ÷ Titanium reverse sustain on the same Client×Origin pair — the overhead of decrypting and intercepting versus bare reverse, not versus nginx or YARP. -- *Not possible* = cannot do that path. *Not measured* = path exists but no published number yet. +- *Not possible* = cannot do that path. *Not measured* = path exists but no published number yet. When every row would be *Not possible* for a peer, that column is omitted and a note above the table explains why. ## Contents @@ -33,13 +33,16 @@ For pooling knobs and certificate first-visit tuning, see [Performance and pooli - [Linux (GitHub-hosted `ubuntu-latest`)](#linux-github-hosted-ubuntu-latest) - [macOS (GitHub-hosted `macos-15-intel`)](#macos-github-hosted-macos-15-intel) - [Saturation control](#saturation-control) -- [Windows — Titanium vs nginx vs HAProxy vs Envoy vs YARP](#windows--titanium-vs-nginx-vs-haproxy-vs-envoy-vs-yarp) +- [Windows — Titanium vs nginx vs YARP](#windows--titanium-vs-nginx-vs-yarp) - [Linux — Titanium vs nginx vs HAProxy vs Envoy vs YARP](#linux--titanium-vs-nginx-vs-haproxy-vs-envoy-vs-yarp) - [macOS — Titanium vs nginx vs HAProxy vs Envoy vs YARP](#macos--titanium-vs-nginx-vs-haproxy-vs-envoy-vs-yarp) - [Editions (CLI / Plus / Intercept)](#editions-cli--plus--intercept) - [Cross-version (7.0 vs 6.0)](#cross-version-70-vs-60) - [Heavier reverse workloads](#heavier-reverse-workloads) - [Unary gRPC (H2 TLS)](#unary-grpc-h2-tls) +- [Unary gRPC (H2 TLS → h2c)](#unary-grpc-h2-tls--h2c) +- [WebSocket (H1 TLS → H1 TLS)](#websocket-h1-tls--h1-tls) +- [WebSocket (H2 TLS 8441 → H1)](#websocket-h2-tls-8441--h1) - [Other measurements](#other-measurements) - [Raising limits on large hosts](#raising-limits-on-large-hosts) - [Maintainer notes](#maintainer-notes) @@ -52,7 +55,7 @@ Laptop High-perf / cool-paired Windows numbers live on [Performance Local Lab](P ### Windows (GitHub-hosted `windows-latest`) -| | | +||| |---|---| | OS | Windows Server (GitHub-hosted `windows-latest`) | | CPU | **4** logical processors | @@ -66,7 +69,7 @@ Laptop High-perf / cool-paired Windows numbers live on [Performance Local Lab](P ### Linux (GitHub-hosted `ubuntu-latest`) -| | | +||| |---|---| | OS | Ubuntu 24.04.x LTS | | CPU | **4** logical processors (AMD EPYC; runners this pass were 7763 / 9V74) | @@ -80,7 +83,7 @@ Laptop High-perf / cool-paired Windows numbers live on [Performance Local Lab](P ### macOS (GitHub-hosted `macos-15-intel`) -| | | +||| |---|---| | OS | macOS 15 (GitHub-hosted `macos-15-intel`, Intel x86_64) | | CPU | **4** logical processors | @@ -110,25 +113,25 @@ Calibration for the shared 4 vCPU loopback shape: how close client + origin are **Windows** (`windows-latest`) -| Arm | Generator | Sustain | Peak | % of origin-HttpClient | -|---|---|---:|---:|---:| -| origin-direct | dotnet-httpclient | **50,319**
      (55 MiB / 40.4% CPU) | **50,319**
      (55 MiB / 40.4% CPU) | **100.0%** | -| origin-direct-bombardier | bombardier | **38,565**
      (56 MiB / 21.8% CPU) | **38,565**
      (56 MiB / 21.8% CPU) | **76.6%** | -| bare-reverse-http1 | dotnet-httpclient | **25,016**
      (58 MiB / 43.8% CPU) | **25,016**
      (58 MiB / 43.8% CPU) | **49.7%** | -| nginx-reverse-http1 | dotnet-httpclient | **13,405**
      (125 MiB / 24.8% CPU) | **13,405**
      (125 MiB / 24.8% CPU) | **26.6%** | -| yarp-reverse-http1 | dotnet-httpclient | **20,802**
      (87 MiB / 49.0% CPU) | **20,802**
      (87 MiB / 49.0% CPU) | **41.3%** | -| twp-reverse-http1 | dotnet-httpclient | 🥇 **24,558**
      (75 MiB / 48.0% CPU) | **24,558**
      (75 MiB / 48.0% CPU) | **48.8%** | +| Arm | Generator | RPS | % of origin-HttpClient | +|---|---|---:|---:| +| origin-direct | dotnet-httpclient | **50,319**
      (55 MiB / 40.4% CPU) | **100.0%** | +| origin-direct-bombardier | bombardier | **38,565**
      (56 MiB / 21.8% CPU) | **76.6%** | +| bare-reverse-http1 | dotnet-httpclient | **25,016**
      (58 MiB / 43.8% CPU) | **49.7%** | +| nginx-reverse-http1 | dotnet-httpclient | **13,405**
      (125 MiB / 24.8% CPU) | **26.6%** | +| yarp-reverse-http1 | dotnet-httpclient | **20,802**
      (87 MiB / 49.0% CPU) | **41.3%** | +| twp-reverse-http1 | dotnet-httpclient | 🥇 **24,558**
      (75 MiB / 48.0% CPU) | **48.8%** | **Linux** (`ubuntu-latest`) -| Arm | Generator | Sustain | Peak | % of origin-HttpClient | -|---|---|---:|---:|---:| -| origin-direct | dotnet-httpclient | **102,038**
      (80 MiB / 44.2% CPU) | **102,038**
      (80 MiB / 44.2% CPU) | **100.0%** | -| origin-direct-bombardier | bombardier | **61,434**
      (80 MiB / 37.4% CPU) | **61,434**
      (80 MiB / 37.4% CPU) | **60.2%** | -| bare-reverse-http1 | dotnet-httpclient | **46,442**
      (70 MiB / 46.0% CPU) | **46,442**
      (70 MiB / 46.0% CPU) | **45.5%** | -| nginx-reverse-http1 | dotnet-httpclient | 🥇 **56,585**
      (76 MiB / 40.3% CPU) | **56,585**
      (76 MiB / 40.3% CPU) | **55.5%** | -| yarp-reverse-http1 | dotnet-httpclient | **41,835**
      (116 MiB / 49.3% CPU) | **41,835**
      (116 MiB / 49.3% CPU) | **41.0%** | -| twp-reverse-http1 | dotnet-httpclient | **47,721**
      (95 MiB / 50.9% CPU) | **47,721**
      (95 MiB / 50.9% CPU) | **46.8%** | +| Arm | Generator | RPS | % of origin-HttpClient | +|---|---|---:|---:| +| origin-direct | dotnet-httpclient | **102,038**
      (80 MiB / 44.2% CPU) | **100.0%** | +| origin-direct-bombardier | bombardier | **61,434**
      (80 MiB / 37.4% CPU) | **60.2%** | +| bare-reverse-http1 | dotnet-httpclient | **46,442**
      (70 MiB / 46.0% CPU) | **45.5%** | +| nginx-reverse-http1 | dotnet-httpclient | 🥇 **56,585**
      (76 MiB / 40.3% CPU) | **55.5%** | +| yarp-reverse-http1 | dotnet-httpclient | **41,835**
      (116 MiB / 49.3% CPU) | **41.0%** | +| twp-reverse-http1 | dotnet-httpclient | **47,721**
      (95 MiB / 50.9% CPU) | **46.8%** | Reverse peers are about **50–46%** of the origin-direct HttpClient peak on this runner class (Win TWP **50.0%**, Lin TWP **45.6%**). Prefer the **%** column over absolute RPS across runs. Bare and origin-direct are controls (not medal peers). @@ -138,19 +141,19 @@ Peer ratios (÷YARP / ÷nginx) on median peak; **RPS cells** embed `(MiB / CPU%) **Windows** (`windows-latest`) -| Arm | Generator | Sustain | Peak | ÷YARP | ÷nginx | -|---|---|---:|---:|---:|---:| -| nginx-reverse-http2 | dotnet-httpclient | **7,992**
      (141 MiB / 24.6% CPU) | **7,992**
      (141 MiB / 24.6% CPU) | **0.28×** | **1.00×** | -| yarp-reverse-http2 | dotnet-httpclient | **28,294**
      (97 MiB / 54.9% CPU) | **28,294**
      (97 MiB / 54.9% CPU) | **1.00×** | **3.54×** | -| twp-reverse-http2-cleartext | dotnet-httpclient | 🥇 **33,720**
      (105 MiB / 52.2% CPU) | **33,720**
      (105 MiB / 52.2% CPU) | **1.19×** | **4.22×** | +| Arm | Generator | RPS | ÷YARP | ÷nginx | +|---|---|---:|---:|---:| +| nginx-reverse-http2 | dotnet-httpclient | **7,992**
      (141 MiB / 24.6% CPU) | **0.28×** | **1.00×** | +| yarp-reverse-http2 | dotnet-httpclient | **28,294**
      (97 MiB / 54.9% CPU) | **1.00×** | **3.54×** | +| twp-reverse-http2-cleartext | dotnet-httpclient | 🥇 **33,720**
      (105 MiB / 52.2% CPU) | **1.19×** | **4.22×** | **Linux** (`ubuntu-latest`) -| Arm | Generator | Sustain | Peak | ÷YARP | ÷nginx | -|---|---|---:|---:|---:|---:| -| nginx-reverse-http2 | dotnet-httpclient | **23,276**
      (102 MiB / 18.9% CPU) | **23,276**
      (102 MiB / 18.9% CPU) | **0.53×** | **1.00×** | -| yarp-reverse-http2 | dotnet-httpclient | **44,299**
      (122 MiB / 48.0% CPU) | **44,299**
      (122 MiB / 48.0% CPU) | **1.00×** | **1.90×** | -| twp-reverse-http2-cleartext | dotnet-httpclient | 🥇 **49,167**
      (124 MiB / 52.1% CPU) | **49,167**
      (124 MiB / 52.1% CPU) | **1.11×** | **2.11×** | +| Arm | Generator | RPS | ÷YARP | ÷nginx | +|---|---|---:|---:|---:| +| nginx-reverse-http2 | dotnet-httpclient | **23,276**
      (102 MiB / 18.9% CPU) | **0.53×** | **1.00×** | +| yarp-reverse-http2 | dotnet-httpclient | **44,299**
      (122 MiB / 48.0% CPU) | **1.00×** | **1.90×** | +| twp-reverse-http2-cleartext | dotnet-httpclient | 🥇 **49,167**
      (124 MiB / 52.1% CPU) | **1.11×** | **2.11×** | #### Block C — H3→H1 @@ -158,57 +161,59 @@ Same layout as Block B. Requires QuicListener. nginx needs `http_v3_module` (Win **Windows** (`windows-latest`) -| Arm | Generator | Sustain | Peak | ÷YARP | ÷nginx | -|---|---|---:|---:|---:|---:| -| nginx-reverse-http3-cleartext | dotnet-httpclient | *Not possible (no QUIC)* | *Not possible (no QUIC)* | — | — | -| yarp-reverse-http3-cleartext | dotnet-httpclient | **14,041**
      (142 MiB / 51.8% CPU) | **14,041**
      (142 MiB / 51.8% CPU) | **1.00×** | — | -| twp-reverse-http3-cleartext | dotnet-httpclient | 🥇 **14,348**
      (104 MiB / 43.8% CPU) | **14,348**
      (104 MiB / 43.8% CPU) | **1.02×** | — | +| Arm | Generator | RPS | ÷YARP | ÷nginx | +|---|---|---:|---:|---:| +| nginx-reverse-http3-cleartext | dotnet-httpclient | *Not possible (no QUIC)* | — | — | +| yarp-reverse-http3-cleartext | dotnet-httpclient | **14,041**
      (142 MiB / 51.8% CPU) | **1.00×** | — | +| twp-reverse-http3-cleartext | dotnet-httpclient | 🥇 **14,348**
      (104 MiB / 43.8% CPU) | **1.02×** | — | **Linux** (`ubuntu-latest`) -| Arm | Generator | Sustain | Peak | ÷YARP | ÷nginx | -|---|---|---:|---:|---:|---:| -| nginx-reverse-http3-cleartext | dotnet-httpclient | **0**
      (110 MiB / 21.8% CPU) | **24,928**
      (110 MiB / 21.8% CPU) | **0.89×** | **1.00×** | -| yarp-reverse-http3-cleartext | dotnet-httpclient | **27,936**
      (195 MiB / 48.8% CPU) | **27,936**
      (195 MiB / 48.8% CPU) | **1.00×** | **1.12×** | -| twp-reverse-http3-cleartext | dotnet-httpclient | 🥇 **30,636**
      (159 MiB / 52.7% CPU) | **30,636**
      (159 MiB / 52.7% CPU) | **1.10×** | **1.23×** | +| Arm | Generator | RPS | ÷YARP | ÷nginx | +|---|---|---:|---:|---:| +| nginx-reverse-http3-cleartext | dotnet-httpclient | **0**
      (peak 24,928 · 110 MiB / 21.8% CPU) | **0.89×** | **1.00×** | +| yarp-reverse-http3-cleartext | dotnet-httpclient | **27,936**
      (195 MiB / 48.8% CPU) | **1.00×** | **1.12×** | +| twp-reverse-http3-cleartext | dotnet-httpclient | 🥇 **30,636**
      (159 MiB / 52.7% CPU) | **1.10×** | **1.23×** | -## Windows — Titanium vs nginx vs HAProxy vs Envoy vs YARP +## Windows — Titanium vs nginx vs YARP Client / origin: HTTP version and whether TLS is used (`plain` = cleartext, `TLS` = encrypted, `QUIC` = HTTP/3). ### Reverse -Median of **3 repeats** on `windows-latest` (4 vCPU / 16 GiB). Bare reverse 5×5 @ `9a2b3a1e` — `compare-product` [34441526151](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34441526151). Warmup 2s / measure 8s; concurrency 8, 16, 32, 64. Prefer TWP÷peer ratios over absolute RPS. **RPS cells** include median RSS / CPU at the peak-RPS step as `
      (MiB / CPU%)`. nginx terminate peers use `keepalive 256` + streaming buffers. **HAProxy / Envoy are Linux-only peers** (no official Windows port). Laptop High-perf / cool-paired numbers stay on the [local lab](Performance-Local-Lab). Product 5×5 is **~56-byte JSON keep-alive GET**; H2/H3 same-protocol cells are mostly header work with a tiny body (Titanium best case) — see [Why this comparison is fair](#why-this-comparison-is-fair). - -**Load generators:** Reverse inbound H3 arms use **`dotnet-httpclient`** (`http_version=3.0`, `RequestVersionExact`). nginx/Windows is same-OS only (no QUIC). HAProxy/Envoy are Linux-only terminate peers. - -| Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | HAProxy sustain | HAProxy peak | Envoy sustain | Envoy peak | YARP sustain | YARP peak | -|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| -| HTTP/1 · plain | HTTP/1 · plain | 🥇 **23134**
      (75 MiB / 47.5% CPU) | 🥇 **23134**
      (75 MiB / 47.5% CPU) | **13916**
      (125 MiB / 24.9% CPU) | **13916**
      (125 MiB / 24.9% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **21699**
      (86 MiB / 48.8% CPU) | **21699**
      (86 MiB / 48.8% CPU) | -| HTTP/1 · plain | HTTP/1 · TLS | 🥇 **20749**
      (89 MiB / 52.8% CPU) | 🥇 **20749**
      (89 MiB / 52.8% CPU) | **8366**
      (135 MiB / 24.6% CPU) | **8366**
      (135 MiB / 24.6% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **18784**
      (100 MiB / 49% CPU) | **18784**
      (100 MiB / 49% CPU) | -| HTTP/1 · plain | HTTP/2 · plain | 🥇 **45733**
      (115 MiB / 50.5% CPU) | 🥇 **45733**
      (115 MiB / 50.5% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **39820**
      (90 MiB / 49.2% CPU) | **39820**
      (90 MiB / 49.2% CPU) | -| HTTP/1 · plain | HTTP/2 · TLS | 🥇 **32162**
      (120 MiB / 46.7% CPU) | 🥇 **32162**
      (120 MiB / 46.7% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **29670**
      (98 MiB / 49.7% CPU) | **29670**
      (98 MiB / 49.7% CPU) | -| HTTP/1 · plain | HTTP/3 · QUIC | 🥇 **18315**
      (106 MiB / 50.6% CPU) | 🥇 **18315**
      (106 MiB / 50.6% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **17823**
      (117 MiB / 51% CPU) | **17823**
      (117 MiB / 51% CPU) | -| HTTP/1 · TLS | HTTP/1 · plain | 🥇 **25720**
      (90 MiB / 48.4% CPU) | 🥇 **25720**
      (90 MiB / 48.4% CPU) | **12987**
      (141 MiB / 24.8% CPU) | **12987**
      (141 MiB / 24.8% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **22690**
      (102 MiB / 48.2% CPU) | **22690**
      (102 MiB / 48.2% CPU) | -| HTTP/1 · TLS | HTTP/1 · TLS | 🥇 **18942**
      (91 MiB / 48.6% CPU) | 🥇 **18942**
      (91 MiB / 48.6% CPU) | **7218**
      (143 MiB / 24.8% CPU) | **7218**
      (143 MiB / 24.8% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **17296**
      (104 MiB / 49.7% CPU) | **17296**
      (104 MiB / 49.7% CPU) | -| HTTP/1 · TLS | HTTP/2 · plain | 🥇 **27614**
      (111 MiB / 44.5% CPU) | 🥇 **27614**
      (111 MiB / 44.5% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **26015**
      (104 MiB / 47.6% CPU) | **26015**
      (104 MiB / 47.6% CPU) | -| HTTP/1 · TLS | HTTP/2 · TLS | 🥇 **32132**
      (117 MiB / 45.4% CPU) | 🥇 **32132**
      (117 MiB / 45.4% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **29999**
      (103 MiB / 48.2% CPU) | **29999**
      (103 MiB / 48.2% CPU) | -| HTTP/1 · TLS | HTTP/3 · QUIC | 🥇 **16275**
      (110 MiB / 51.3% CPU) | 🥇 **16275**
      (110 MiB / 51.3% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **15797**
      (124 MiB / 51.4% CPU) | **15797**
      (124 MiB / 51.4% CPU) | -| HTTP/2 · plain | HTTP/1 · plain | 🥇 **34933**
      (89 MiB / 55% CPU) | 🥇 **34933**
      (89 MiB / 55% CPU) | **9362**
      (127 MiB / 24.4% CPU) | **9362**
      (127 MiB / 24.4% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **31625**
      (86 MiB / 54.4% CPU) | **31625**
      (86 MiB / 54.4% CPU) | -| HTTP/2 · plain | HTTP/1 · TLS | 🥇 **35086**
      (105 MiB / 49% CPU) | 🥇 **35086**
      (105 MiB / 49% CPU) | **8849**
      (138 MiB / 24.9% CPU) | **8849**
      (138 MiB / 24.9% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **32740**
      (92 MiB / 50.1% CPU) | **32740**
      (92 MiB / 50.1% CPU) | -| HTTP/2 · plain | HTTP/2 · plain | 🥇 **112638**
      (61 MiB / 28.3% CPU) | 🥇 **112638**
      (61 MiB / 28.3% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **64290**
      (90 MiB / 49.6% CPU) | **64290**
      (90 MiB / 49.6% CPU) | -| HTTP/2 · plain | HTTP/2 · TLS | 🥇 **70105**
      (62 MiB / 20.1% CPU) | 🥇 **70105**
      (62 MiB / 20.1% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **43971**
      (99 MiB / 35.9% CPU) | **43971**
      (99 MiB / 35.9% CPU) | -| HTTP/2 · plain | HTTP/3 · QUIC | 🥇 **33088**
      (128 MiB / 53.1% CPU) | 🥇 **33088**
      (128 MiB / 53.1% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **31406**
      (130 MiB / 52% CPU) | **31406**
      (130 MiB / 52% CPU) | -| HTTP/2 · TLS | HTTP/1 · plain | 🥇 **34943**
      (97 MiB / 52.2% CPU) | 🥇 **34943**
      (97 MiB / 52.2% CPU) | **8430**
      (141 MiB / 24.8% CPU) | **8430**
      (141 MiB / 24.8% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **29441**
      (96 MiB / 53.8% CPU) | **29441**
      (96 MiB / 53.8% CPU) | -| HTTP/2 · TLS | HTTP/1 · TLS | 🥇 **29487**
      (97 MiB / 53.6% CPU) | 🥇 **29487**
      (97 MiB / 53.6% CPU) | **6542**
      (144 MiB / 24.6% CPU) | **6542**
      (144 MiB / 24.6% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **25117**
      (98 MiB / 54.2% CPU) | **25117**
      (98 MiB / 54.2% CPU) | -| HTTP/2 · TLS | HTTP/2 · plain | 🥇 **107152**
      (75 MiB / 29.1% CPU) | 🥇 **107152**
      (75 MiB / 29.1% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **59098**
      (102 MiB / 52.7% CPU) | **59098**
      (102 MiB / 52.7% CPU) | -| HTTP/2 · TLS | HTTP/2 · TLS | 🥇 **84487**
      (73 MiB / 28.8% CPU) | 🥇 **84487**
      (73 MiB / 28.8% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **49546**
      (98 MiB / 49% CPU) | **49546**
      (98 MiB / 49% CPU) | -| HTTP/2 · TLS | HTTP/3 · QUIC | 🥇 **30034**
      (127 MiB / 53.4% CPU) | 🥇 **30034**
      (127 MiB / 53.4% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **25937**
      (123 MiB / 51.5% CPU) | **25937**
      (123 MiB / 51.5% CPU) | -| HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **19348**
      (107 MiB / 44% CPU) | 🥇 **19348**
      (107 MiB / 44% CPU) | *Not measured* | *Not measured* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **18041**
      (143 MiB / 50.9% CPU) | **18041**
      (143 MiB / 50.9% CPU) | -| HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 **13646**
      (113 MiB / 46.7% CPU) | 🥇 **13646**
      (113 MiB / 46.7% CPU) | *Not measured* | *Not measured* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **13437**
      (145 MiB / 52.4% CPU) | **13437**
      (145 MiB / 52.4% CPU) | -| HTTP/3 · QUIC | HTTP/2 · plain | 🥇 **34128**
      (136 MiB / 47.6% CPU) | 🥇 **34128**
      (136 MiB / 47.6% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **25450**
      (166 MiB / 50.1% CPU) | **25450**
      (166 MiB / 50.1% CPU) | -| HTTP/3 · QUIC | HTTP/2 · TLS | 🥇 **33850**
      (138 MiB / 48% CPU) | 🥇 **33850**
      (138 MiB / 48% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **26828**
      (168 MiB / 48.5% CPU) | **26828**
      (168 MiB / 48.5% CPU) | -| HTTP/3 · QUIC | HTTP/3 · QUIC | 🥇 **23150**
      (121 MiB / 41.8% CPU) | 🥇 **23150**
      (121 MiB / 41.8% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **12064**
      (167 MiB / 53.1% CPU) | **12064**
      (167 MiB / 53.1% CPU) | +Median of **3 repeats** on `windows-latest` (4 vCPU / 16 GiB). Bare reverse 5×5 @ `9a2b3a1e` — `compare-product` [34441526151](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34441526151). Warmup 2s / measure 8s; concurrency 8, 16, 32, 64. Prefer TWP÷peer ratios over absolute RPS. **RPS cells** show sustain RPS; `
      ` holds peak (when higher) plus median RSS / CPU at the peak-RPS step. nginx terminate peers use `keepalive 256` + streaming buffers. Laptop High-perf / cool-paired numbers stay on the [local lab](Performance-Local-Lab). Product 5×5 is **~56-byte JSON keep-alive GET**; H2/H3 same-protocol cells are mostly header work with a tiny body (Titanium best case) — see [Why this comparison is fair](#why-this-comparison-is-fair). + +*Not possible:* **HAProxy** and **Envoy** columns are omitted (no official Windows port). + +**Load generators:** Reverse inbound H3 arms use **`dotnet-httpclient`** (`http_version=3.0`, `RequestVersionExact`). nginx/Windows is same-OS only (no QUIC). + +| Client | Origin | TWP | nginx | YARP | +|---|---|---:|---:|---:| +| HTTP/1 · plain | HTTP/1 · plain | 🥇 **23,134**
      (75 MiB / 47.5% CPU) | **13,916**
      (125 MiB / 24.9% CPU) | **21,699**
      (86 MiB / 48.8% CPU) | +| HTTP/1 · plain | HTTP/1 · TLS | 🥇 **20,749**
      (89 MiB / 52.8% CPU) | **8,366**
      (135 MiB / 24.6% CPU) | **18,784**
      (100 MiB / 49% CPU) | +| HTTP/1 · plain | HTTP/2 · plain | 🥇 **45,733**
      (115 MiB / 50.5% CPU) | *Not possible (no H2 upstream)* | **39,820**
      (90 MiB / 49.2% CPU) | +| HTTP/1 · plain | HTTP/2 · TLS | 🥇 **32,162**
      (120 MiB / 46.7% CPU) | *Not possible (no H2 upstream)* | **29,670**
      (98 MiB / 49.7% CPU) | +| HTTP/1 · plain | HTTP/3 · QUIC | 🥇 **18,315**
      (106 MiB / 50.6% CPU) | *Not possible (no H3 upstream)* | **17,823**
      (117 MiB / 51% CPU) | +| HTTP/1 · TLS | HTTP/1 · plain | 🥇 **25,720**
      (90 MiB / 48.4% CPU) | **12,987**
      (141 MiB / 24.8% CPU) | **22,690**
      (102 MiB / 48.2% CPU) | +| HTTP/1 · TLS | HTTP/1 · TLS | 🥇 **18,942**
      (91 MiB / 48.6% CPU) | **7,218**
      (143 MiB / 24.8% CPU) | **17,296**
      (104 MiB / 49.7% CPU) | +| HTTP/1 · TLS | HTTP/2 · plain | 🥇 **27,614**
      (111 MiB / 44.5% CPU) | *Not possible (no H2 upstream)* | **26,015**
      (104 MiB / 47.6% CPU) | +| HTTP/1 · TLS | HTTP/2 · TLS | 🥇 **32,132**
      (117 MiB / 45.4% CPU) | *Not possible (no H2 upstream)* | **29,999**
      (103 MiB / 48.2% CPU) | +| HTTP/1 · TLS | HTTP/3 · QUIC | 🥇 **16,275**
      (110 MiB / 51.3% CPU) | *Not possible (no H3 upstream)* | **15,797**
      (124 MiB / 51.4% CPU) | +| HTTP/2 · plain | HTTP/1 · plain | 🥇 **34,933**
      (89 MiB / 55% CPU) | **9,362**
      (127 MiB / 24.4% CPU) | **31,625**
      (86 MiB / 54.4% CPU) | +| HTTP/2 · plain | HTTP/1 · TLS | 🥇 **35,086**
      (105 MiB / 49% CPU) | **8,849**
      (138 MiB / 24.9% CPU) | **32,740**
      (92 MiB / 50.1% CPU) | +| HTTP/2 · plain | HTTP/2 · plain | 🥇 **112,638**
      (61 MiB / 28.3% CPU) | *Not possible (no H2 upstream)* | **64,290**
      (90 MiB / 49.6% CPU) | +| HTTP/2 · plain | HTTP/2 · TLS | 🥇 **70,105**
      (62 MiB / 20.1% CPU) | *Not possible (no H2 upstream)* | **43,971**
      (99 MiB / 35.9% CPU) | +| HTTP/2 · plain | HTTP/3 · QUIC | 🥇 **33,088**
      (128 MiB / 53.1% CPU) | *Not possible (no H3 upstream)* | **31,406**
      (130 MiB / 52% CPU) | +| HTTP/2 · TLS | HTTP/1 · plain | 🥇 **34,943**
      (97 MiB / 52.2% CPU) | **8,430**
      (141 MiB / 24.8% CPU) | **29,441**
      (96 MiB / 53.8% CPU) | +| HTTP/2 · TLS | HTTP/1 · TLS | 🥇 **29,487**
      (97 MiB / 53.6% CPU) | **6,542**
      (144 MiB / 24.6% CPU) | **25,117**
      (98 MiB / 54.2% CPU) | +| HTTP/2 · TLS | HTTP/2 · plain | 🥇 **107,152**
      (75 MiB / 29.1% CPU) | *Not possible (no H2 upstream)* | **59,098**
      (102 MiB / 52.7% CPU) | +| HTTP/2 · TLS | HTTP/2 · TLS | 🥇 **84,487**
      (73 MiB / 28.8% CPU) | *Not possible (no H2 upstream)* | **49,546**
      (98 MiB / 49% CPU) | +| HTTP/2 · TLS | HTTP/3 · QUIC | 🥇 **30,034**
      (127 MiB / 53.4% CPU) | *Not possible (no H3 upstream)* | **25,937**
      (123 MiB / 51.5% CPU) | +| HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **19,348**
      (107 MiB / 44% CPU) | *Not possible (no QUIC)* | **18,041**
      (143 MiB / 50.9% CPU) | +| HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 **13,646**
      (113 MiB / 46.7% CPU) | *Not possible (no QUIC)* | **13,437**
      (145 MiB / 52.4% CPU) | +| HTTP/3 · QUIC | HTTP/2 · plain | 🥇 **34,128**
      (136 MiB / 47.6% CPU) | *Not possible (no H2 upstream)* | **25,450**
      (166 MiB / 50.1% CPU) | +| HTTP/3 · QUIC | HTTP/2 · TLS | 🥇 **33,850**
      (138 MiB / 48% CPU) | *Not possible (no H2 upstream)* | **26,828**
      (168 MiB / 48.5% CPU) | +| HTTP/3 · QUIC | HTTP/3 · QUIC | 🥇 **23,150**
      (121 MiB / 41.8% CPU) | *Not possible (no H3 upstream)* | **12,064**
      (167 MiB / 53.1% CPU) | ### MITM (TWP only) @@ -252,33 +257,33 @@ Same Client×Origin wires with interception on (`compare-product` [34441526151]( Median of **3 repeats** on `ubuntu-latest` (4 vCPU / 16 GiB). Bare reverse 5×5 @ `9a2b3a1e` — `compare-product` [34441526151](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34441526151). Warmup 2s / measure 8s; concurrency 8, 16, 32, 64. **Linux nginx is the authoritative nginx baseline.** HAProxy (3.2 `USE_QUIC`) and Envoy (GitHub release, HTTP/3 compiled in) run on the same loopback shape as nginx/YARP. nginx terminate peers use `keepalive 256` + streaming buffers. The RPS workflow installs nginx.org mainline (`http_v3_module`), a QUIC-enabled HAProxy, Envoy, and `libmsquic`. Prefer ratios over absolute RPS. Product 5×5 is **~56-byte JSON keep-alive GET**; H2/H3 same-protocol cells are mostly header work with a tiny body (Titanium best case) — see [Why this comparison is fair](#why-this-comparison-is-fair). -| Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | HAProxy sustain | HAProxy peak | Envoy sustain | Envoy peak | YARP sustain | YARP peak | -|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| -| HTTP/1 · plain | HTTP/1 · plain | **36672**
      (94 MiB / 50.5% CPU) | **36672**
      (94 MiB / 50.5% CPU) | 🥇 **44112**
      (76 MiB / 40.4% CPU) | 🥇 **44112**
      (76 MiB / 40.4% CPU) | **41748**
      (67 MiB / 41.3% CPU) | **41748**
      (67 MiB / 41.3% CPU) | **24415**
      (116 MiB / 59.5% CPU) | **24415**
      (116 MiB / 59.5% CPU) | **32393**
      (115 MiB / 48.8% CPU) | **32393**
      (115 MiB / 48.8% CPU) | -| HTTP/1 · plain | HTTP/1 · TLS | **28506**
      (107 MiB / 49.3% CPU) | **28506**
      (107 MiB / 49.3% CPU) | 🥇 **34687**
      (93 MiB / 40.7% CPU) | 🥇 **34687**
      (93 MiB / 40.7% CPU) | **32839**
      (71 MiB / 43% CPU) | **32839**
      (71 MiB / 43% CPU) | **22383**
      (117 MiB / 56.4% CPU) | **22383**
      (117 MiB / 56.4% CPU) | **25710**
      (138 MiB / 49.6% CPU) | **25710**
      (138 MiB / 49.6% CPU) | -| HTTP/1 · plain | HTTP/2 · plain | 🥇 **45673**
      (129 MiB / 52.7% CPU) | 🥇 **45673**
      (129 MiB / 52.7% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | **26830**
      (68 MiB / 42.8% CPU) | **26830**
      (68 MiB / 42.8% CPU) | **27447**
      (116 MiB / 61% CPU) | **27447**
      (116 MiB / 61% CPU) | **40965**
      (125 MiB / 49.4% CPU) | **40965**
      (125 MiB / 49.4% CPU) | -| HTTP/1 · plain | HTTP/2 · TLS | 🥇 **37934**
      (147 MiB / 49.1% CPU) | 🥇 **37934**
      (147 MiB / 49.1% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | **35699**
      (68 MiB / 43% CPU) | **35699**
      (68 MiB / 43% CPU) | **19949**
      (117 MiB / 61.9% CPU) | **19949**
      (117 MiB / 61.9% CPU) | **35520**
      (132 MiB / 47.3% CPU) | **35520**
      (132 MiB / 47.3% CPU) | -| HTTP/1 · plain | HTTP/3 · QUIC | 🥇 **26151**
      (141 MiB / 53.8% CPU) | 🥇 **26151**
      (141 MiB / 53.8% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | *Not measured* | *Not measured* | **12126**
      (120 MiB / 56.3% CPU) | **12126**
      (120 MiB / 56.3% CPU) | **24086**
      (155 MiB / 48.3% CPU) | **24086**
      (155 MiB / 48.3% CPU) | -| HTTP/1 · TLS | HTTP/1 · plain | **27742**
      (111 MiB / 48.9% CPU) | **27742**
      (111 MiB / 48.9% CPU) | **33232**
      (100 MiB / 41.5% CPU) | **33232**
      (100 MiB / 41.5% CPU) | 🥇 **33283**
      (85 MiB / 43.6% CPU) | 🥇 **33283**
      (85 MiB / 43.6% CPU) | **21660**
      (127 MiB / 56.4% CPU) | **21660**
      (127 MiB / 56.4% CPU) | **24735**
      (142 MiB / 49.4% CPU) | **24735**
      (142 MiB / 49.4% CPU) | -| HTTP/1 · TLS | HTTP/1 · TLS | **23878**
      (111 MiB / 48.2% CPU) | **23878**
      (111 MiB / 48.2% CPU) | 🥇 **28042**
      (103 MiB / 41.6% CPU) | 🥇 **28042**
      (103 MiB / 41.6% CPU) | **27394**
      (86 MiB / 43.8% CPU) | **27394**
      (86 MiB / 43.8% CPU) | **19309**
      (128 MiB / 55% CPU) | **19309**
      (128 MiB / 55% CPU) | **21716**
      (142 MiB / 49.4% CPU) | **21716**
      (142 MiB / 49.4% CPU) | -| HTTP/1 · TLS | HTTP/2 · plain | 🥇 **34010**
      (140 MiB / 51.4% CPU) | 🥇 **34010**
      (140 MiB / 51.4% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | **21760**
      (83 MiB / 42.9% CPU) | **21760**
      (83 MiB / 42.9% CPU) | **18208**
      (127 MiB / 58.6% CPU) | **18208**
      (127 MiB / 58.6% CPU) | **30979**
      (141 MiB / 49.2% CPU) | **30979**
      (141 MiB / 49.2% CPU) | -| HTTP/1 · TLS | HTTP/2 · TLS | 🥇 **29656**
      (157 MiB / 48.6% CPU) | 🥇 **29656**
      (157 MiB / 48.6% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | **29078**
      (83 MiB / 44% CPU) | **29078**
      (83 MiB / 44% CPU) | **21903**
      (126 MiB / 56.9% CPU) | **21903**
      (126 MiB / 56.9% CPU) | **27210**
      (146 MiB / 48.3% CPU) | **27210**
      (146 MiB / 48.3% CPU) | -| HTTP/1 · TLS | HTTP/3 · QUIC | 🥇 **22269**
      (152 MiB / 52.9% CPU) | 🥇 **22269**
      (152 MiB / 52.9% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | *Not measured* | *Not measured* | **11093**
      (130 MiB / 55.9% CPU) | **11093**
      (130 MiB / 55.9% CPU) | **19851**
      (166 MiB / 49.4% CPU) | **19851**
      (166 MiB / 49.4% CPU) | -| HTTP/2 · plain | HTTP/1 · plain | 🥇 **41263**
      (126 MiB / 53.1% CPU) | 🥇 **41263**
      (126 MiB / 53.1% CPU) | **18170**
      (79 MiB / 19.6% CPU) | **18170**
      (79 MiB / 19.6% CPU) | **27652**
      (69 MiB / 24.4% CPU) | **27652**
      (69 MiB / 24.4% CPU) | **17979**
      (118 MiB / 23% CPU) | **17979**
      (118 MiB / 23% CPU) | **39219**
      (115 MiB / 48.3% CPU) | **39219**
      (115 MiB / 48.3% CPU) | -| HTTP/2 · plain | HTTP/1 · TLS | 🥇 **32750**
      (131 MiB / 51% CPU) | 🥇 **32750**
      (131 MiB / 51% CPU) | **15078**
      (100 MiB / 19.2% CPU) | **15078**
      (100 MiB / 19.2% CPU) | **22285**
      (71 MiB / 24.5% CPU) | **22285**
      (71 MiB / 24.5% CPU) | **16529**
      (119 MiB / 23.1% CPU) | **16529**
      (119 MiB / 23.1% CPU) | **31159**
      (125 MiB / 48.4% CPU) | **31159**
      (125 MiB / 48.4% CPU) | -| HTTP/2 · plain | HTTP/2 · plain | 🥇 **97239**
      (90 MiB / 38% CPU) | 🥇 **97239**
      (90 MiB / 38% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | **30261**
      (69 MiB / 24.3% CPU) | **30261**
      (69 MiB / 24.3% CPU) | **24868**
      (116 MiB / 21.8% CPU) | **24868**
      (116 MiB / 21.8% CPU) | **55752**
      (122 MiB / 47.1% CPU) | **55752**
      (122 MiB / 47.1% CPU) | -| HTTP/2 · plain | HTTP/2 · TLS | 🥇 **67702**
      (93 MiB / 38.2% CPU) | 🥇 **67702**
      (93 MiB / 38.2% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | **23815**
      (68 MiB / 24.4% CPU) | **23815**
      (68 MiB / 24.4% CPU) | **16231**
      (118 MiB / 21.5% CPU) | **16231**
      (118 MiB / 21.5% CPU) | **44692**
      (129 MiB / 45.3% CPU) | **44692**
      (129 MiB / 45.3% CPU) | -| HTTP/2 · plain | HTTP/3 · QUIC | 🥇 **30188**
      (150 MiB / 51.4% CPU) | 🥇 **30188**
      (150 MiB / 51.4% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | *Not measured* | *Not measured* | **5340**
      (121 MiB / 24.8% CPU) | **5340**
      (121 MiB / 24.8% CPU) | **28445**
      (156 MiB / 45.5% CPU) | **28445**
      (156 MiB / 45.5% CPU) | -| HTTP/2 · TLS | HTTP/1 · plain | 🥇 **38662**
      (128 MiB / 52.3% CPU) | 🥇 **38662**
      (128 MiB / 52.3% CPU) | **17745**
      (100 MiB / 18.8% CPU) | **17745**
      (100 MiB / 18.8% CPU) | **24393**
      (81 MiB / 24.3% CPU) | **24393**
      (81 MiB / 24.3% CPU) | **17919**
      (128 MiB / 22.5% CPU) | **17919**
      (128 MiB / 22.5% CPU) | **34084**
      (122 MiB / 48.1% CPU) | **34084**
      (122 MiB / 48.1% CPU) | -| HTTP/2 · TLS | HTTP/1 · TLS | 🥇 **30318**
      (119 MiB / 49.8% CPU) | 🥇 **30318**
      (119 MiB / 49.8% CPU) | **15083**
      (110 MiB / 19.6% CPU) | **15083**
      (110 MiB / 19.6% CPU) | **20646**
      (85 MiB / 24.4% CPU) | **20646**
      (85 MiB / 24.4% CPU) | **16394**
      (128 MiB / 22.2% CPU) | **16394**
      (128 MiB / 22.2% CPU) | **27788**
      (131 MiB / 48.5% CPU) | **27788**
      (131 MiB / 48.5% CPU) | -| HTTP/2 · TLS | HTTP/2 · plain | 🥇 **85544**
      (102 MiB / 38% CPU) | 🥇 **85544**
      (102 MiB / 38% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | **53648**
      (85 MiB / 24.2% CPU) | **53648**
      (85 MiB / 24.2% CPU) | **24277**
      (126 MiB / 21.5% CPU) | **24277**
      (126 MiB / 21.5% CPU) | **44751**
      (125 MiB / 46.4% CPU) | **44751**
      (125 MiB / 46.4% CPU) | -| HTTP/2 · TLS | HTTP/2 · TLS | 🥇 **63379**
      (99 MiB / 36% CPU) | 🥇 **63379**
      (99 MiB / 36% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | **22388**
      (84 MiB / 24.4% CPU) | **22388**
      (84 MiB / 24.4% CPU) | **21510**
      (125 MiB / 20.6% CPU) | **21510**
      (125 MiB / 20.6% CPU) | **38643**
      (128 MiB / 45.3% CPU) | **38643**
      (128 MiB / 45.3% CPU) | -| HTTP/2 · TLS | HTTP/3 · QUIC | 🥇 **28423**
      (151 MiB / 50.5% CPU) | 🥇 **28423**
      (151 MiB / 50.5% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | *Not measured* | *Not measured* | **5476**
      (129 MiB / 24.6% CPU) | **5476**
      (129 MiB / 24.6% CPU) | **25085**
      (163 MiB / 45.8% CPU) | **25085**
      (163 MiB / 45.8% CPU) | -| HTTP/3 · QUIC | HTTP/1 · plain | **23715**
      (149 MiB / 52.2% CPU) | **23715**
      (149 MiB / 52.2% CPU) | **0**
      (107 MiB / 21.6% CPU) | **19206**
      (107 MiB / 21.6% CPU) | 🥇 **30835**
      (86 MiB / 25.2% CPU) | 🥇 **30835**
      (86 MiB / 25.2% CPU) | **3**
      (130 MiB / 0.1% CPU) | **3**
      (130 MiB / 0.1% CPU) | **21546**
      (186 MiB / 49% CPU) | **21546**
      (186 MiB / 49% CPU) | -| HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 **19505**
      (160 MiB / 49.3% CPU) | 🥇 **19505**
      (160 MiB / 49.3% CPU) | **0**
      (117 MiB / 22.7% CPU) | **14850**
      (117 MiB / 22.7% CPU) | *Not measured* | *Not measured* | *Not measured* | *Not measured* | **18423**
      (197 MiB / 49.9% CPU) | **18423**
      (197 MiB / 49.9% CPU) | -| HTTP/3 · QUIC | HTTP/2 · plain | 🥇 **31386**
      (160 MiB / 56.2% CPU) | 🥇 **31386**
      (160 MiB / 56.2% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | **29864**
      (86 MiB / 25.4% CPU) | **29864**
      (86 MiB / 25.4% CPU) | *Not measured* | *Not measured* | **26924**
      (197 MiB / 48% CPU) | **26924**
      (197 MiB / 48% CPU) | -| HTTP/3 · QUIC | HTTP/2 · TLS | 🥇 **27669**
      (154 MiB / 52.3% CPU) | 🥇 **27669**
      (154 MiB / 52.3% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | *Not measured* | *Not measured* | *Not measured* | *Not measured* | **24488**
      (199 MiB / 47.6% CPU) | **24488**
      (199 MiB / 47.6% CPU) | -| HTTP/3 · QUIC | HTTP/3 · QUIC | 🥇 **21858**
      (164 MiB / 47.9% CPU) | 🥇 **21858**
      (164 MiB / 47.9% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | *Not measured* | *Not measured* | *Not measured* | *Not measured* | **17840**
      (209 MiB / 47% CPU) | **17840**
      (209 MiB / 47% CPU) | +| Client | Origin | TWP | nginx | HAProxy | Envoy | YARP | +|---|---|---:|---:|---:|---:|---:| +| HTTP/1 · plain | HTTP/1 · plain | **36,672**
      (94 MiB / 50.5% CPU) | 🥇 **44,112**
      (76 MiB / 40.4% CPU) | **41,748**
      (67 MiB / 41.3% CPU) | **24,415**
      (116 MiB / 59.5% CPU) | **32,393**
      (115 MiB / 48.8% CPU) | +| HTTP/1 · plain | HTTP/1 · TLS | **28,506**
      (107 MiB / 49.3% CPU) | 🥇 **34,687**
      (93 MiB / 40.7% CPU) | **32,839**
      (71 MiB / 43% CPU) | **22,383**
      (117 MiB / 56.4% CPU) | **25,710**
      (138 MiB / 49.6% CPU) | +| HTTP/1 · plain | HTTP/2 · plain | 🥇 **45,673**
      (129 MiB / 52.7% CPU) | *Not possible (no H2 upstream)* | **26,830**
      (68 MiB / 42.8% CPU) | **27,447**
      (116 MiB / 61% CPU) | **40,965**
      (125 MiB / 49.4% CPU) | +| HTTP/1 · plain | HTTP/2 · TLS | 🥇 **37,934**
      (147 MiB / 49.1% CPU) | *Not possible (no H2 upstream)* | **35,699**
      (68 MiB / 43% CPU) | **19,949**
      (117 MiB / 61.9% CPU) | **35,520**
      (132 MiB / 47.3% CPU) | +| HTTP/1 · plain | HTTP/3 · QUIC | 🥇 **26,151**
      (141 MiB / 53.8% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | **12,126**
      (120 MiB / 56.3% CPU) | **24,086**
      (155 MiB / 48.3% CPU) | +| HTTP/1 · TLS | HTTP/1 · plain | **27,742**
      (111 MiB / 48.9% CPU) | **33,232**
      (100 MiB / 41.5% CPU) | 🥇 **33,283**
      (85 MiB / 43.6% CPU) | **21,660**
      (127 MiB / 56.4% CPU) | **24,735**
      (142 MiB / 49.4% CPU) | +| HTTP/1 · TLS | HTTP/1 · TLS | **23,878**
      (111 MiB / 48.2% CPU) | 🥇 **28,042**
      (103 MiB / 41.6% CPU) | **27,394**
      (86 MiB / 43.8% CPU) | **19,309**
      (128 MiB / 55% CPU) | **21,716**
      (142 MiB / 49.4% CPU) | +| HTTP/1 · TLS | HTTP/2 · plain | 🥇 **34,010**
      (140 MiB / 51.4% CPU) | *Not possible (no H2 upstream)* | **21,760**
      (83 MiB / 42.9% CPU) | **18,208**
      (127 MiB / 58.6% CPU) | **30,979**
      (141 MiB / 49.2% CPU) | +| HTTP/1 · TLS | HTTP/2 · TLS | 🥇 **29,656**
      (157 MiB / 48.6% CPU) | *Not possible (no H2 upstream)* | **29,078**
      (83 MiB / 44% CPU) | **21,903**
      (126 MiB / 56.9% CPU) | **27,210**
      (146 MiB / 48.3% CPU) | +| HTTP/1 · TLS | HTTP/3 · QUIC | 🥇 **22,269**
      (152 MiB / 52.9% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | **11,093**
      (130 MiB / 55.9% CPU) | **19,851**
      (166 MiB / 49.4% CPU) | +| HTTP/2 · plain | HTTP/1 · plain | 🥇 **41,263**
      (126 MiB / 53.1% CPU) | **18,170**
      (79 MiB / 19.6% CPU) | **27,652**
      (69 MiB / 24.4% CPU) | **17,979**
      (118 MiB / 23% CPU) | **39,219**
      (115 MiB / 48.3% CPU) | +| HTTP/2 · plain | HTTP/1 · TLS | 🥇 **32,750**
      (131 MiB / 51% CPU) | **15,078**
      (100 MiB / 19.2% CPU) | **22,285**
      (71 MiB / 24.5% CPU) | **16,529**
      (119 MiB / 23.1% CPU) | **31,159**
      (125 MiB / 48.4% CPU) | +| HTTP/2 · plain | HTTP/2 · plain | 🥇 **97,239**
      (90 MiB / 38% CPU) | *Not possible (no H2 upstream)* | **30,261**
      (69 MiB / 24.3% CPU) | **24,868**
      (116 MiB / 21.8% CPU) | **55,752**
      (122 MiB / 47.1% CPU) | +| HTTP/2 · plain | HTTP/2 · TLS | 🥇 **67,702**
      (93 MiB / 38.2% CPU) | *Not possible (no H2 upstream)* | **23,815**
      (68 MiB / 24.4% CPU) | **16,231**
      (118 MiB / 21.5% CPU) | **44,692**
      (129 MiB / 45.3% CPU) | +| HTTP/2 · plain | HTTP/3 · QUIC | 🥇 **30,188**
      (150 MiB / 51.4% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | **5,340**
      (121 MiB / 24.8% CPU) | **28,445**
      (156 MiB / 45.5% CPU) | +| HTTP/2 · TLS | HTTP/1 · plain | 🥇 **38,662**
      (128 MiB / 52.3% CPU) | **17,745**
      (100 MiB / 18.8% CPU) | **24,393**
      (81 MiB / 24.3% CPU) | **17,919**
      (128 MiB / 22.5% CPU) | **34,084**
      (122 MiB / 48.1% CPU) | +| HTTP/2 · TLS | HTTP/1 · TLS | 🥇 **30,318**
      (119 MiB / 49.8% CPU) | **15,083**
      (110 MiB / 19.6% CPU) | **20,646**
      (85 MiB / 24.4% CPU) | **16,394**
      (128 MiB / 22.2% CPU) | **27,788**
      (131 MiB / 48.5% CPU) | +| HTTP/2 · TLS | HTTP/2 · plain | 🥇 **85,544**
      (102 MiB / 38% CPU) | *Not possible (no H2 upstream)* | **53,648**
      (85 MiB / 24.2% CPU) | **24,277**
      (126 MiB / 21.5% CPU) | **44,751**
      (125 MiB / 46.4% CPU) | +| HTTP/2 · TLS | HTTP/2 · TLS | 🥇 **63,379**
      (99 MiB / 36% CPU) | *Not possible (no H2 upstream)* | **22,388**
      (84 MiB / 24.4% CPU) | **21,510**
      (125 MiB / 20.6% CPU) | **38,643**
      (128 MiB / 45.3% CPU) | +| HTTP/2 · TLS | HTTP/3 · QUIC | 🥇 **28,423**
      (151 MiB / 50.5% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | **5,476**
      (129 MiB / 24.6% CPU) | **25,085**
      (163 MiB / 45.8% CPU) | +| HTTP/3 · QUIC | HTTP/1 · plain | **23,715**
      (149 MiB / 52.2% CPU) | **0**
      (peak 19,206 · 107 MiB / 21.6% CPU) | 🥇 **30,835**
      (86 MiB / 25.2% CPU) | **3**
      (130 MiB / 0.1% CPU) | **21,546**
      (186 MiB / 49% CPU) | +| HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 **19,505**
      (160 MiB / 49.3% CPU) | **0**
      (peak 14,850 · 117 MiB / 22.7% CPU) | **18,067**
      (94 MiB / 29.8% CPU) | **4**
      (peak 3,743 · 131 MiB / 23.1% CPU) | **18,423**
      (197 MiB / 49.9% CPU) | +| HTTP/3 · QUIC | HTTP/2 · plain | 🥇 **31,386**
      (160 MiB / 56.2% CPU) | *Not possible (no H2 upstream)* | **29,864**
      (86 MiB / 25.4% CPU) | **18**
      (peak 4,898 · 138 MiB / 24.5% CPU) | **26,924**
      (197 MiB / 48% CPU) | +| HTTP/3 · QUIC | HTTP/2 · TLS | 🥇 **27,669**
      (154 MiB / 52.3% CPU) | *Not possible (no H2 upstream)* | **32,742**
      (peak 32,880 · 92 MiB / 25.9% CPU) | **5**
      (peak 4,411 · 139 MiB / 25.0% CPU) | **24,488**
      (199 MiB / 47.6% CPU) | +| HTTP/3 · QUIC | HTTP/3 · QUIC | 🥇 **21,858**
      (164 MiB / 47.9% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | **2,964**
      (peak 3,679 · 134 MiB / 24.8% CPU) | **17,840**
      (209 MiB / 47% CPU) | ### MITM (TWP only) @@ -320,35 +325,35 @@ Same Client×Origin wires with interception on (`compare-product` [34441526151]( ### Reverse -Median of **3 repeats** on `macos-15-intel` (4-core / 14 GB). Bare reverse 5×5 @ `9a2b3a1e` — `compare-product` [34441526151](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34441526151). Warmup 2s / measure 8s; concurrency 8, 16, 32, 64. Prefer TWP÷peer ratios over absolute RPS. **RPS cells** include median RSS / CPU at the peak-RPS step as `
      (MiB / CPU%)`. The RPS workflow installs Homebrew nginx (`http_v3_module`), Homebrew HAProxy with `USE_QUIC` (3.2 source fallback), Envoy (Homebrew bottle or pinned darwin-amd64 1.36.7), Homebrew `libmsquic` (+ `DYLD_*`), and YARP. Do not publish from `macos-latest` (3-core / 7 GB). Product 5×5 is **~56-byte JSON keep-alive GET**; H2/H3 same-protocol cells are mostly header work with a tiny body (Titanium best case) — see [Why this comparison is fair](#why-this-comparison-is-fair). - -| Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | HAProxy sustain | HAProxy peak | Envoy sustain | Envoy peak | YARP sustain | YARP peak | -|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| -| HTTP/1 · plain | HTTP/1 · plain | 🥇 **12131**
      (82 MiB / 32.8% CPU) | 🥇 **12131**
      (82 MiB / 32.8% CPU) | **6938**
      (52 MiB / 11.6% CPU) | **6938**
      (52 MiB / 11.6% CPU) | **10945**
      (48 MiB / 23.2% CPU) | **10945**
      (48 MiB / 23.2% CPU) | **3332**
      (72 MiB / 22.4% CPU) | **3332**
      (72 MiB / 22.4% CPU) | **10805**
      (105 MiB / 33.6% CPU) | **10805**
      (105 MiB / 33.6% CPU) | -| HTTP/1 · plain | HTTP/1 · TLS | 🥇 **11064**
      (93 MiB / 36.5% CPU) | 🥇 **11064**
      (93 MiB / 36.5% CPU) | **5152**
      (73 MiB / 17.4% CPU) | **5152**
      (73 MiB / 17.4% CPU) | **7159**
      (53 MiB / 25.9% CPU) | **7159**
      (53 MiB / 25.9% CPU) | **0**
      (75 MiB / 21% CPU) | **2011**
      (75 MiB / 21% CPU) | **9146**
      (129 MiB / 36.8% CPU) | **9146**
      (129 MiB / 36.8% CPU) | -| HTTP/1 · plain | HTTP/2 · plain | 🥇 **19185**
      (88 MiB / 33.8% CPU) | 🥇 **19185**
      (88 MiB / 33.8% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | **11853**
      (50 MiB / 25.6% CPU) | **11853**
      (50 MiB / 25.6% CPU) | **6000**
      (73 MiB / 36.6% CPU) | **6000**
      (73 MiB / 36.6% CPU) | **18729**
      (111 MiB / 34% CPU) | **18729**
      (111 MiB / 34% CPU) | -| HTTP/1 · plain | HTTP/2 · TLS | **9927**
      (129 MiB / 32.9% CPU) | **9927**
      (129 MiB / 32.9% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | **8770**
      (51 MiB / 26.5% CPU) | **8770**
      (51 MiB / 26.5% CPU) | **5948**
      (73 MiB / 34.9% CPU) | **5948**
      (73 MiB / 34.9% CPU) | 🥇 **13674**
      (116 MiB / 32.9% CPU) | 🥇 **13674**
      (116 MiB / 32.9% CPU) | -| HTTP/1 · plain | HTTP/3 · QUIC | **3662**
      (90 MiB / 45.3% CPU) | **3662**
      (90 MiB / 45.3% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | *Not measured* | *Not measured* | **3393**
      (75 MiB / 38.2% CPU) | **2631**
      (75 MiB / 38.2% CPU) | 🥇 **5774**
      (116 MiB / 33.4% CPU) | 🥇 **5774**
      (116 MiB / 33.4% CPU) | -| HTTP/1 · TLS | HTTP/1 · plain | 🥇 **14162**
      (94 MiB / 31.5% CPU) | 🥇 **14162**
      (94 MiB / 31.5% CPU) | **8184**
      (74 MiB / 18.3% CPU) | **8184**
      (74 MiB / 18.3% CPU) | **9938**
      (64 MiB / 25.6% CPU) | **9938**
      (64 MiB / 25.6% CPU) | **4084**
      (79 MiB / 37.4% CPU) | **4328**
      (79 MiB / 37.4% CPU) | **7647**
      (115 MiB / 32.3% CPU) | **7647**
      (115 MiB / 32.3% CPU) | -| HTTP/1 · TLS | HTTP/1 · TLS | **6128**
      (97 MiB / 31.7% CPU) | **6128**
      (97 MiB / 31.7% CPU) | **4903**
      (81 MiB / 15.5% CPU) | **4903**
      (81 MiB / 15.5% CPU) | 🥇 **9241**
      (67 MiB / 28% CPU) | 🥇 **9241**
      (67 MiB / 28% CPU) | **3594**
      (80 MiB / 38.9% CPU) | **3594**
      (80 MiB / 38.9% CPU) | **6601**
      (172 MiB / 35% CPU) | **6601**
      (172 MiB / 35% CPU) | -| HTTP/1 · TLS | HTTP/2 · plain | **8317**
      (96 MiB / 32.8% CPU) | **8317**
      (96 MiB / 32.8% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | 🥇 **12353**
      (66 MiB / 28% CPU) | 🥇 **12353**
      (66 MiB / 28% CPU) | **5378**
      (80 MiB / 20.9% CPU) | **3107**
      (80 MiB / 20.9% CPU) | **10735**
      (122 MiB / 32.2% CPU) | **10735**
      (122 MiB / 32.2% CPU) | -| HTTP/1 · TLS | HTTP/2 · TLS | **8457**
      (161 MiB / 33.8% CPU) | **8457**
      (161 MiB / 33.8% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | **8558**
      (65 MiB / 29.3% CPU) | **8558**
      (65 MiB / 29.3% CPU) | **4938**
      (79 MiB / 39.8% CPU) | **5195**
      (79 MiB / 39.8% CPU) | 🥇 **9023**
      (120 MiB / 28.7% CPU) | 🥇 **9023**
      (120 MiB / 28.7% CPU) | -| HTTP/1 · TLS | HTTP/3 · QUIC | **2677**
      (112 MiB / 44.9% CPU) | **2677**
      (112 MiB / 44.9% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | *Not measured* | *Not measured* | 🥇 **4392**
      (81 MiB / 37.8% CPU) | 🥇 **2176**
      (81 MiB / 37.8% CPU) | **3589**
      (152 MiB / 31.7% CPU) | **3589**
      (152 MiB / 31.7% CPU) | -| HTTP/2 · plain | HTTP/1 · plain | **12398**
      (88 MiB / 35.1% CPU) | **12398**
      (88 MiB / 35.1% CPU) | **10718**
      (58 MiB / 15.4% CPU) | **10718**
      (58 MiB / 15.4% CPU) | **7841**
      (54 MiB / 18% CPU) | **7841**
      (54 MiB / 18% CPU) | **3391**
      (74 MiB / 20.7% CPU) | **3391**
      (74 MiB / 20.7% CPU) | 🥇 **12934**
      (105 MiB / 37.8% CPU) | 🥇 **12934**
      (105 MiB / 37.8% CPU) | -| HTTP/2 · plain | HTTP/1 · TLS | **13814**
      (94 MiB / 38.6% CPU) | **13814**
      (94 MiB / 38.6% CPU) | **12186**
      (85 MiB / 16% CPU) | **12186**
      (85 MiB / 16% CPU) | **6688**
      (56 MiB / 18.9% CPU) | **6688**
      (56 MiB / 18.9% CPU) | **5045**
      (77 MiB / 21.3% CPU) | **5045**
      (77 MiB / 21.3% CPU) | 🥇 **14337**
      (121 MiB / 42.3% CPU) | 🥇 **14337**
      (121 MiB / 42.3% CPU) | -| HTTP/2 · plain | HTTP/2 · plain | 🥇 **30992**
      (73 MiB / 25.8% CPU) | 🥇 **30992**
      (73 MiB / 25.8% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | **8615**
      (54 MiB / 17.5% CPU) | **8615**
      (54 MiB / 17.5% CPU) | **5442**
      (72 MiB / 20% CPU) | **5442**
      (72 MiB / 20% CPU) | **22898**
      (109 MiB / 37.5% CPU) | **22898**
      (109 MiB / 37.5% CPU) | -| HTTP/2 · plain | HTTP/2 · TLS | 🥇 **24573**
      (77 MiB / 27.7% CPU) | 🥇 **24573**
      (77 MiB / 27.7% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | **11876**
      (58 MiB / 19% CPU) | **11876**
      (58 MiB / 19% CPU) | **6393**
      (73 MiB / 20.2% CPU) | **6393**
      (73 MiB / 20.2% CPU) | **18700**
      (116 MiB / 34.6% CPU) | **18700**
      (116 MiB / 34.6% CPU) | -| HTTP/2 · plain | HTTP/3 · QUIC | **4840**
      (97 MiB / 49.3% CPU) | **4840**
      (97 MiB / 49.3% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | *Not measured* | *Not measured* | **3760**
      (74 MiB / 22.4% CPU) | **3760**
      (74 MiB / 22.4% CPU) | 🥇 **8092**
      (117 MiB / 33.9% CPU) | 🥇 **8092**
      (117 MiB / 33.9% CPU) | -| HTTP/2 · TLS | HTTP/1 · plain | 🥇 **13829**
      (94 MiB / 39.2% CPU) | 🥇 **13829**
      (94 MiB / 39.2% CPU) | **9070**
      (73 MiB / 14.5% CPU) | **9070**
      (73 MiB / 14.5% CPU) | **5237**
      (66 MiB / 16.1% CPU) | **5237**
      (66 MiB / 16.1% CPU) | **3516**
      (81 MiB / 20.4% CPU) | **3516**
      (81 MiB / 20.4% CPU) | **13572**
      (113 MiB / 37.3% CPU) | **13572**
      (113 MiB / 37.3% CPU) | -| HTTP/2 · TLS | HTTP/1 · TLS | 🥇 **9182**
      (96 MiB / 37.3% CPU) | 🥇 **9182**
      (96 MiB / 37.3% CPU) | **6812**
      (90 MiB / 15.3% CPU) | **6812**
      (90 MiB / 15.3% CPU) | **4920**
      (66 MiB / 18.2% CPU) | **4920**
      (66 MiB / 18.2% CPU) | **2505**
      (83 MiB / 20.1% CPU) | **2505**
      (83 MiB / 20.1% CPU) | **9080**
      (124 MiB / 40.6% CPU) | **9080**
      (124 MiB / 40.6% CPU) | -| HTTP/2 · TLS | HTTP/2 · plain | 🥇 **47325**
      (81 MiB / 29% CPU) | 🥇 **47325**
      (81 MiB / 29% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | **5750**
      (67 MiB / 5.9% CPU) | **2851**
      (67 MiB / 5.9% CPU) | **4704**
      (79 MiB / 19% CPU) | **4704**
      (79 MiB / 19% CPU) | **22095**
      (115 MiB / 37.6% CPU) | **22095**
      (115 MiB / 37.6% CPU) | -| HTTP/2 · TLS | HTTP/2 · TLS | 🥇 **29038**
      (83 MiB / 27.1% CPU) | 🥇 **29038**
      (83 MiB / 27.1% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | **6022**
      (67 MiB / 18.3% CPU) | **6022**
      (67 MiB / 18.3% CPU) | **4494**
      (79 MiB / 18.6% CPU) | **4494**
      (79 MiB / 18.6% CPU) | **16458**
      (118 MiB / 35% CPU) | **16458**
      (118 MiB / 35% CPU) | -| HTTP/2 · TLS | HTTP/3 · QUIC | **4226**
      (108 MiB / 36% CPU) | **4226**
      (108 MiB / 36% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | *Not measured* | *Not measured* | **1772**
      (81 MiB / 21.6% CPU) | **1772**
      (81 MiB / 21.6% CPU) | 🥇 **4805**
      (125 MiB / 33.7% CPU) | 🥇 **4805**
      (125 MiB / 33.7% CPU) | -| HTTP/3 · QUIC | HTTP/1 · plain | **5834**
      (101 MiB / 39.5% CPU) | **5834**
      (101 MiB / 39.5% CPU) | **0**
      (63 MiB / 11.2% CPU) | **7464**
      (63 MiB / 11.2% CPU) | 🥇 **10727**
      (66 MiB / 22% CPU) | 🥇 **10727**
      (66 MiB / 22% CPU) | **1595**
      (85 MiB / 26.4% CPU) | **1595**
      (85 MiB / 26.4% CPU) | **5558**
      (187 MiB / 35% CPU) | **5558**
      (187 MiB / 35% CPU) | -| HTTP/3 · QUIC | HTTP/1 · TLS | **3723**
      (119 MiB / 38.2% CPU) | **3723**
      (119 MiB / 38.2% CPU) | **0**
      (66 MiB / 11.1% CPU) | **4316**
      (66 MiB / 11.1% CPU) | 🥇 **5430**
      (69 MiB / 21.8% CPU) | 🥇 **5430**
      (69 MiB / 21.8% CPU) | *Not measured* | *Not measured* | **4786**
      (197 MiB / 35.5% CPU) | **4786**
      (197 MiB / 35.5% CPU) | -| HTTP/3 · QUIC | HTTP/2 · plain | **4576**
      (97 MiB / 41.3% CPU) | **4576**
      (97 MiB / 41.3% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | 🥇 **5954**
      (66 MiB / 14.4% CPU) | 🥇 **5954**
      (66 MiB / 14.4% CPU) | *Not measured* | *Not measured* | **5642**
      (176 MiB / 34.2% CPU) | **5642**
      (176 MiB / 34.2% CPU) | -| HTTP/3 · QUIC | HTTP/2 · TLS | **5532**
      (106 MiB / 41.4% CPU) | **5532**
      (106 MiB / 41.4% CPU) | *Not possible (no H2 upstream)* | *Not possible (no H2 upstream)* | **6633**
      (68 MiB / 21.5% CPU) | **6633**
      (68 MiB / 21.5% CPU) | *Not measured* | *Not measured* | 🥇 **7854**
      (167 MiB / 32.5% CPU) | 🥇 **7854**
      (167 MiB / 32.5% CPU) | -| HTTP/3 · QUIC | HTTP/3 · QUIC | **3460**
      (96 MiB / 35% CPU) | **3460**
      (96 MiB / 35% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | *Not measured* | *Not measured* | *Not measured* | *Not measured* | 🥇 **4659**
      (188 MiB / 34.4% CPU) | 🥇 **4659**
      (188 MiB / 34.4% CPU) | +Median of **3 repeats** on `macos-15-intel` (4-core / 14 GB). Bare reverse 5×5 @ `9a2b3a1e` — `compare-product` [34441526151](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34441526151); Envoy H3 inbound remainder @ `a495a9ae` — [34557758404](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34557758404)–[34557765742](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34557765742). Warmup 2s / measure 8s; concurrency 8, 16, 32, 64. Prefer TWP÷peer ratios over absolute RPS. **RPS cells** show sustain RPS; `
      ` holds peak (when higher) plus median RSS / CPU at the peak-RPS step. The RPS workflow installs Homebrew nginx (`http_v3_module`), Homebrew HAProxy with `USE_QUIC` (3.2 source fallback), Envoy (Homebrew bottle or pinned darwin-amd64 1.36.7), Homebrew `libmsquic` (+ `DYLD_*`), and YARP. Do not publish from `macos-latest` (3-core / 7 GB). Product 5×5 is **~56-byte JSON keep-alive GET**; H2/H3 same-protocol cells are mostly header work with a tiny body (Titanium best case) — see [Why this comparison is fair](#why-this-comparison-is-fair). + +| Client | Origin | TWP | nginx | HAProxy | Envoy | YARP | +|---|---|---:|---:|---:|---:|---:| +| HTTP/1 · plain | HTTP/1 · plain | 🥇 **12,131**
      (82 MiB / 32.8% CPU) | **6,938**
      (52 MiB / 11.6% CPU) | **10,945**
      (48 MiB / 23.2% CPU) | **3,332**
      (72 MiB / 22.4% CPU) | **10,805**
      (105 MiB / 33.6% CPU) | +| HTTP/1 · plain | HTTP/1 · TLS | 🥇 **11,064**
      (93 MiB / 36.5% CPU) | **5,152**
      (73 MiB / 17.4% CPU) | **7,159**
      (53 MiB / 25.9% CPU) | **0**
      (peak 2,011 · 75 MiB / 21% CPU) | **9,146**
      (129 MiB / 36.8% CPU) | +| HTTP/1 · plain | HTTP/2 · plain | 🥇 **19,185**
      (88 MiB / 33.8% CPU) | *Not possible (no H2 upstream)* | **11,853**
      (50 MiB / 25.6% CPU) | **6,000**
      (73 MiB / 36.6% CPU) | **18,729**
      (111 MiB / 34% CPU) | +| HTTP/1 · plain | HTTP/2 · TLS | **9,927**
      (129 MiB / 32.9% CPU) | *Not possible (no H2 upstream)* | **8,770**
      (51 MiB / 26.5% CPU) | **5,948**
      (73 MiB / 34.9% CPU) | 🥇 **13,674**
      (116 MiB / 32.9% CPU) | +| HTTP/1 · plain | HTTP/3 · QUIC | **3,662**
      (90 MiB / 45.3% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | **3,393**
      (75 MiB / 38.2% CPU) | 🥇 **5,774**
      (116 MiB / 33.4% CPU) | +| HTTP/1 · TLS | HTTP/1 · plain | 🥇 **14,162**
      (94 MiB / 31.5% CPU) | **8,184**
      (74 MiB / 18.3% CPU) | **9,938**
      (64 MiB / 25.6% CPU) | **4,084**
      (peak 4,328 · 79 MiB / 37.4% CPU) | **7,647**
      (115 MiB / 32.3% CPU) | +| HTTP/1 · TLS | HTTP/1 · TLS | **6,128**
      (97 MiB / 31.7% CPU) | **4,903**
      (81 MiB / 15.5% CPU) | 🥇 **9,241**
      (67 MiB / 28% CPU) | **3,594**
      (80 MiB / 38.9% CPU) | **6,601**
      (172 MiB / 35% CPU) | +| HTTP/1 · TLS | HTTP/2 · plain | **8,317**
      (96 MiB / 32.8% CPU) | *Not possible (no H2 upstream)* | 🥇 **12,353**
      (66 MiB / 28% CPU) | **5,378**
      (80 MiB / 20.9% CPU) | **10,735**
      (122 MiB / 32.2% CPU) | +| HTTP/1 · TLS | HTTP/2 · TLS | **8,457**
      (161 MiB / 33.8% CPU) | *Not possible (no H2 upstream)* | **8,558**
      (65 MiB / 29.3% CPU) | **4,938**
      (peak 5,195 · 79 MiB / 39.8% CPU) | 🥇 **9,023**
      (120 MiB / 28.7% CPU) | +| HTTP/1 · TLS | HTTP/3 · QUIC | **2,677**
      (112 MiB / 44.9% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | 🥇 **4,392**
      (81 MiB / 37.8% CPU) | **3,589**
      (152 MiB / 31.7% CPU) | +| HTTP/2 · plain | HTTP/1 · plain | **12,398**
      (88 MiB / 35.1% CPU) | **10,718**
      (58 MiB / 15.4% CPU) | **7,841**
      (54 MiB / 18% CPU) | **3,391**
      (74 MiB / 20.7% CPU) | 🥇 **12,934**
      (105 MiB / 37.8% CPU) | +| HTTP/2 · plain | HTTP/1 · TLS | **13,814**
      (94 MiB / 38.6% CPU) | **12,186**
      (85 MiB / 16% CPU) | **6,688**
      (56 MiB / 18.9% CPU) | **5,045**
      (77 MiB / 21.3% CPU) | 🥇 **14,337**
      (121 MiB / 42.3% CPU) | +| HTTP/2 · plain | HTTP/2 · plain | 🥇 **30,992**
      (73 MiB / 25.8% CPU) | *Not possible (no H2 upstream)* | **8,615**
      (54 MiB / 17.5% CPU) | **5,442**
      (72 MiB / 20% CPU) | **22,898**
      (109 MiB / 37.5% CPU) | +| HTTP/2 · plain | HTTP/2 · TLS | 🥇 **24,573**
      (77 MiB / 27.7% CPU) | *Not possible (no H2 upstream)* | **11,876**
      (58 MiB / 19% CPU) | **6,393**
      (73 MiB / 20.2% CPU) | **18,700**
      (116 MiB / 34.6% CPU) | +| HTTP/2 · plain | HTTP/3 · QUIC | **4,840**
      (97 MiB / 49.3% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | **3,760**
      (74 MiB / 22.4% CPU) | 🥇 **8,092**
      (117 MiB / 33.9% CPU) | +| HTTP/2 · TLS | HTTP/1 · plain | 🥇 **13,829**
      (94 MiB / 39.2% CPU) | **9,070**
      (73 MiB / 14.5% CPU) | **5,237**
      (66 MiB / 16.1% CPU) | **3,516**
      (81 MiB / 20.4% CPU) | **13,572**
      (113 MiB / 37.3% CPU) | +| HTTP/2 · TLS | HTTP/1 · TLS | 🥇 **9,182**
      (96 MiB / 37.3% CPU) | **6,812**
      (90 MiB / 15.3% CPU) | **4,920**
      (66 MiB / 18.2% CPU) | **2,505**
      (83 MiB / 20.1% CPU) | **9,080**
      (124 MiB / 40.6% CPU) | +| HTTP/2 · TLS | HTTP/2 · plain | 🥇 **47,325**
      (81 MiB / 29% CPU) | *Not possible (no H2 upstream)* | **5,750**
      (67 MiB / 5.9% CPU) | **4,704**
      (79 MiB / 19% CPU) | **22,095**
      (115 MiB / 37.6% CPU) | +| HTTP/2 · TLS | HTTP/2 · TLS | 🥇 **29,038**
      (83 MiB / 27.1% CPU) | *Not possible (no H2 upstream)* | **6,022**
      (67 MiB / 18.3% CPU) | **4,494**
      (79 MiB / 18.6% CPU) | **16,458**
      (118 MiB / 35% CPU) | +| HTTP/2 · TLS | HTTP/3 · QUIC | **4,226**
      (108 MiB / 36% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | **1,772**
      (81 MiB / 21.6% CPU) | 🥇 **4,805**
      (125 MiB / 33.7% CPU) | +| HTTP/3 · QUIC | HTTP/1 · plain | **5,834**
      (101 MiB / 39.5% CPU) | **0**
      (peak 7,464 · 63 MiB / 11.2% CPU) | 🥇 **10,727**
      (66 MiB / 22% CPU) | **1,595**
      (85 MiB / 26.4% CPU) | **5,558**
      (187 MiB / 35% CPU) | +| HTTP/3 · QUIC | HTTP/1 · TLS | **3,723**
      (119 MiB / 38.2% CPU) | **0**
      (peak 4,316 · 66 MiB / 11.1% CPU) | 🥇 **5,430**
      (69 MiB / 21.8% CPU) | **1,893**
      (89 MiB / 31.2% CPU) | **4,786**
      (197 MiB / 35.5% CPU) | +| HTTP/3 · QUIC | HTTP/2 · plain | **4,576**
      (97 MiB / 41.3% CPU) | *Not possible (no H2 upstream)* | 🥇 **5,954**
      (66 MiB / 14.4% CPU) | **1,262**
      (83 MiB / 25.3% CPU) | **5,642**
      (176 MiB / 34.2% CPU) | +| HTTP/3 · QUIC | HTTP/2 · TLS | **5,532**
      (106 MiB / 41.4% CPU) | *Not possible (no H2 upstream)* | **6,633**
      (68 MiB / 21.5% CPU) | **2,692**
      (84 MiB / 32.1% CPU) | 🥇 **7,854**
      (167 MiB / 32.5% CPU) | +| HTTP/3 · QUIC | HTTP/3 · QUIC | **3,460**
      (96 MiB / 35% CPU) | *Not possible (no H3 upstream)* | *Not possible (no H3 upstream)* | **1,036**
      (peak 1,116 · 82 MiB / 28.4% CPU) | 🥇 **4,659**
      (188 MiB / 34.4% CPU) | ### MITM (TWP only) @@ -390,28 +395,28 @@ Same Client×Origin wires with interception on (`compare-product` [34441526151]( **Note:** `twp-reverse-http1` and other library rows use Core with **probe-tuned** settings (no logging, no Via header, probe-warmed certs). Edition rows use `titanium run -c twp.yaml` **product defaults** — prefer the ÷baseline ratio column over absolute RPS. Inspector GUI is not spawnable in the harness; session-path overhead is `twp-cli-intercept-http1` (route `RequestHeaderSet` transform). Pre-origin Plus middleware (CIDR/WAF/JWT/rate-limit) runs on H1 terminate-lite without `SessionEventArgs`; JWT caches successful bearer validations. Maintainer gate thresholds live under [Maintainer notes](#maintainer-notes). -Median of **3** repeats @ `6d2a7c9d`. Source: Actions [33259699099](https://github.com/justcoding121/titanium-web-proxy/actions/runs/33259699099). Warmup 2s / measure 8s; concurrency 8–64; sustain = median peak RPS among SLO-pass steps @ **c=64**. **RPS cells** include `(MiB / CPU%)` at that step. - -| Arm | Win sustain | Win peak | Linux sustain | Linux peak | Win÷ | Lin÷ | Gate | -|---|---:|---:|---:|---:|---:|---:|---:| -| `twp-cli-reverse-http1` vs library | **32214**
      (118 MiB / 43.4% CPU) | **32219** | **47734**
      (151 MiB / 49.0% CPU) | **47844** | **1.02×** | **1.04×** | ≥ **0.80×** | -| `twp-cli-reverse-http1-tls` vs library TLS | **26495**
      (138 MiB / 48.3% CPU) | **26651** | **36932**
      (174 MiB / 48.5% CPU) | **37200** | **1.01×** | **1.00×** | ≥ **0.80×** | -| `twp-cli-reverse-http1-route` vs CLI | **32394**
      (120 MiB / 47.7% CPU) | **32428** | **47369**
      (149 MiB / 49.2% CPU) | **47398** | **1.01×** | **0.99×** | ≥ **0.90×** | -| `twp-cli-plus-base-http1` vs CLI | **32475**
      (123 MiB / 46.0% CPU) | **32695** | **47322**
      (154 MiB / 49.8% CPU) | **47744** | **1.01×** | **0.99×** | ≥ **0.90×** | -| `twp-cli-plus-cache-http1` (cold) vs CLI | **34029**
      (118 MiB / 64.5% CPU) | **34460** | **49409**
      (148 MiB / 62.3% CPU) | **50104** | **1.06×** | **1.04×** | ≥ **0.70×** | -| `twp-cli-intercept-http1` vs CLI | **25219**
      (124 MiB / 54.0% CPU) | **25377** | **35969**
      (155 MiB / 51.2% CPU) | **35977** | **0.78×** | **0.75×** | ≥ **0.70×** | -| `twp-cli-plus-waf-http1` vs CLI | **31653**
      (124 MiB / 46.8% CPU) | **31837** | **46367**
      (151 MiB / 49.6% CPU) | **46685** | **0.98×** | **0.97×** | ≥ **0.80×** | -| `twp-cli-plus-cidr-http1` vs CLI | **31730**
      (123 MiB / 51.1% CPU) | **31757** | **46828**
      (150 MiB / 50.2% CPU) | **47093** | **0.98×** | **0.98×** | ≥ **0.80×** | -| `twp-cli-plus-jwt-http1` vs CLI | **30326**
      (138 MiB / 46.6% CPU) | **30386** | **43906**
      (179 MiB / 49.5% CPU) | **44433** | **0.94×** | **0.92×** | ≥ **0.70×** | -| `twp-cli-plus-ratelimit-http1` vs CLI | **31713**
      (123 MiB / 48.8% CPU) | **31944** | **46435**
      (151 MiB / 49.4% CPU) | **46860** | **0.98×** | **0.97×** | ≥ **0.80×** | -| `twp-cli-plus-resilience-http1` vs CLI | **32373**
      (128 MiB / 50.5% CPU) | **32416** | **47302**
      (155 MiB / 49.5% CPU) | **47654** | **1.00×** | **0.99×** | ≥ **0.85×** | -| `twp-cli-plus-discovery-file-http1` vs CLI | **32209**
      (121 MiB / 46.5% CPU) | **32332** | **47485**
      (151 MiB / 49.1% CPU) | **47770** | **1.00×** | **0.99×** | ≥ **0.80×** | -| `twp-cli-plus-metrics-scrape-http1` vs CLI | **32192**
      (126 MiB / 44.8% CPU) | **32427** | **47348**
      (159 MiB / 49.5% CPU) | **48665** | **1.00×** | **0.99×** | ≥ **0.80×** | -| `twp-cli-plus-cache-hit-http1` vs cache cold | **34015**
      (117 MiB / 66.0% CPU) | **34022** | **49153**
      (149 MiB / 63.1% CPU) | **49358** | **1.00×** | **0.99×** | ≥ **0.90×** | -| `twp-cli-static-http1` vs CLI | **53078**
      (109 MiB / 52.1% CPU) | **53980** | **78333**
      (150 MiB / 48.4% CPU) | **82955** | **1.65×** | **1.64×** | ≥ **0.85×** | -| `twp-cli-logging-http1` vs CLI | **32440**
      (119 MiB / 47.4% CPU) | **32581** | **47624**
      (150 MiB / 49.3% CPU) | **48604** | **1.01×** | **1.00×** | ≥ **0.90×** | -| `twp-cli-lb-leasttime-http1` vs route | **29649**
      (134 MiB / 54.7% CPU) | **30386** | **44790**
      (180 MiB / 50.6% CPU) | **44825** | **0.92×** | **0.95×** | ≥ **0.85×** | -| `twp-cli-dialect-twp-http1` vs CLI | **32461**
      (114 MiB / 49.7% CPU) | **32780** | **47898**
      (145 MiB / 49.3% CPU) | **48154** | **1.01×** | **1.00×** | ≥ **0.90×** | +Median of **3** repeats @ `6d2a7c9d`. Source: Actions [33259699099](https://github.com/justcoding121/titanium-web-proxy/actions/runs/33259699099). Warmup 2s / measure 8s; concurrency 8–64; sustain = median peak RPS among SLO-pass steps @ **c=64**. **RPS cells** show sustain; `` holds peak (when higher) plus `(MiB / CPU%)`. + +| Arm | Win | Linux | Win÷ | Lin÷ | Gate | +|---|---:|---:|---:|---:|---| +| `twp-cli-reverse-http1` vs library | **32,214**
      (peak 32,219 · 118 MiB / 43.4% CPU) | **47,734**
      (peak 47,844 · 151 MiB / 49.0% CPU) | **1.02×** | **1.04×** | ≥ **0.80×** | +| `twp-cli-reverse-http1-tls` vs library TLS | **26,495**
      (peak 26,651 · 138 MiB / 48.3% CPU) | **36,932**
      (peak 37,200 · 174 MiB / 48.5% CPU) | **1.01×** | **1.00×** | ≥ **0.80×** | +| `twp-cli-reverse-http1-route` vs CLI | **32,394**
      (peak 32,428 · 120 MiB / 47.7% CPU) | **47,369**
      (peak 47,398 · 149 MiB / 49.2% CPU) | **1.01×** | **0.99×** | ≥ **0.90×** | +| `twp-cli-plus-base-http1` vs CLI | **32,475**
      (peak 32,695 · 123 MiB / 46.0% CPU) | **47,322**
      (peak 47,744 · 154 MiB / 49.8% CPU) | **1.01×** | **0.99×** | ≥ **0.90×** | +| `twp-cli-plus-cache-http1` (cold) vs CLI | **34,029**
      (peak 34,460 · 118 MiB / 64.5% CPU) | **49,409**
      (peak 50,104 · 148 MiB / 62.3% CPU) | **1.06×** | **1.04×** | ≥ **0.70×** | +| `twp-cli-intercept-http1` vs CLI | **25,219**
      (peak 25,377 · 124 MiB / 54.0% CPU) | **35,969**
      (peak 35,977 · 155 MiB / 51.2% CPU) | **0.78×** | **0.75×** | ≥ **0.70×** | +| `twp-cli-plus-waf-http1` vs CLI | **31,653**
      (peak 31,837 · 124 MiB / 46.8% CPU) | **46,367**
      (peak 46,685 · 151 MiB / 49.6% CPU) | **0.98×** | **0.97×** | ≥ **0.80×** | +| `twp-cli-plus-cidr-http1` vs CLI | **31,730**
      (peak 31,757 · 123 MiB / 51.1% CPU) | **46,828**
      (peak 47,093 · 150 MiB / 50.2% CPU) | **0.98×** | **0.98×** | ≥ **0.80×** | +| `twp-cli-plus-jwt-http1` vs CLI | **30,326**
      (peak 30,386 · 138 MiB / 46.6% CPU) | **43,906**
      (peak 44,433 · 179 MiB / 49.5% CPU) | **0.94×** | **0.92×** | ≥ **0.70×** | +| `twp-cli-plus-ratelimit-http1` vs CLI | **31,713**
      (peak 31,944 · 123 MiB / 48.8% CPU) | **46,435**
      (peak 46,860 · 151 MiB / 49.4% CPU) | **0.98×** | **0.97×** | ≥ **0.80×** | +| `twp-cli-plus-resilience-http1` vs CLI | **32,373**
      (peak 32,416 · 128 MiB / 50.5% CPU) | **47,302**
      (peak 47,654 · 155 MiB / 49.5% CPU) | **1.00×** | **0.99×** | ≥ **0.85×** | +| `twp-cli-plus-discovery-file-http1` vs CLI | **32,209**
      (peak 32,332 · 121 MiB / 46.5% CPU) | **47,485**
      (peak 47,770 · 151 MiB / 49.1% CPU) | **1.00×** | **0.99×** | ≥ **0.80×** | +| `twp-cli-plus-metrics-scrape-http1` vs CLI | **32,192**
      (peak 32,427 · 126 MiB / 44.8% CPU) | **47,348**
      (peak 48,665 · 159 MiB / 49.5% CPU) | **1.00×** | **0.99×** | ≥ **0.80×** | +| `twp-cli-plus-cache-hit-http1` vs cache cold | **34,015**
      (peak 34,022 · 117 MiB / 66.0% CPU) | **49,153**
      (peak 49,358 · 149 MiB / 63.1% CPU) | **1.00×** | **0.99×** | ≥ **0.90×** | +| `twp-cli-static-http1` vs CLI | **53,078**
      (peak 53,980 · 109 MiB / 52.1% CPU) | **78,333**
      (peak 82,955 · 150 MiB / 48.4% CPU) | **1.65×** | **1.64×** | ≥ **0.85×** | +| `twp-cli-logging-http1` vs CLI | **32,440**
      (peak 32,581 · 119 MiB / 47.4% CPU) | **47,624**
      (peak 48,604 · 150 MiB / 49.3% CPU) | **1.01×** | **1.00×** | ≥ **0.90×** | +| `twp-cli-lb-leasttime-http1` vs route | **29,649**
      (peak 30,386 · 134 MiB / 54.7% CPU) | **44,790**
      (peak 44,825 · 180 MiB / 50.6% CPU) | **0.92×** | **0.95×** | ≥ **0.85×** | +| `twp-cli-dialect-twp-http1` vs CLI | **32,461**
      (peak 32,780 · 114 MiB / 49.7% CPU) | **47,898**
      (peak 48,154 · 145 MiB / 49.3% CPU) | **1.01×** | **1.00×** | ≥ **0.90×** | `validate-edition-gates.ps1` **passed** on both OS for this run. Library baselines @ c=64 (same job): Win H1 **31561** / TLS **26128**; Linux H1 **45818** / TLS **36869**. Laptop smoke ratios stay on [Performance Local Lab — Editions](Performance-Local-Lab#editions-cli--plus-stress). @@ -440,51 +445,53 @@ Lossy link = **userspace** delay/drop shim (not kernel `netem`): TCP gets per-bu ### Windows — heavier reverse GET (64 KiB / 256 KiB) -Median of **3** repeats on `windows-latest` @ `9a2b3a1e`. Source: Actions [34441570199](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34441570199) (`compare-bodies`). Warmup 2s / measure 8s. **RPS cells** include `(MiB / CPU%)` footprints. - -| Body | Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | HAProxy sustain | HAProxy peak | Envoy sustain | Envoy peak | YARP sustain | YARP peak | -|---|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| -| 64 KiB | HTTP/1 · TLS | HTTP/1 · plain | 🥇 **13,191**
      (122 MiB / 46.7% CPU) | **13,191**
      (122 MiB / 46.7% CPU) | **924**
      (142 MiB / 24.8% CPU) | **924**
      (142 MiB / 24.8% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **11,473**
      (133 MiB / 47.5% CPU) | **11,473**
      (133 MiB / 47.5% CPU) | -| 64 KiB | HTTP/2 · TLS | HTTP/1 · plain | 🥇 **11,769**
      (173 MiB / 46.2% CPU) | **11,769**
      (173 MiB / 46.2% CPU) | **898**
      (142 MiB / 24.8% CPU) | **898**
      (142 MiB / 24.8% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **9,560**
      (135 MiB / 48.1% CPU) | **9,560**
      (135 MiB / 48.1% CPU) | -| 64 KiB | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **5,958**
      (139 MiB / 41.5% CPU) | **5,958**
      (139 MiB / 41.5% CPU) | *Not possible (no QUIC)* | *Not possible (no QUIC)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **5,060**
      (185 MiB / 51.0% CPU) | **5,060**
      (185 MiB / 51.0% CPU) | -| 256 KiB | HTTP/1 · TLS | HTTP/1 · plain | 🥇 **3,693**
      (136 MiB / 42.1% CPU) | **3,693**
      (136 MiB / 42.1% CPU) | **241**
      (142 MiB / 24.9% CPU) | **241**
      (142 MiB / 24.9% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **3,325**
      (130 MiB / 47.2% CPU) | **3,325**
      (130 MiB / 47.2% CPU) | -| 256 KiB | HTTP/2 · TLS | HTTP/1 · plain | 🥇 **3,784**
      (149 MiB / 36.2% CPU) | **3,784**
      (149 MiB / 36.2% CPU) | **230**
      (142 MiB / 24.8% CPU) | **230**
      (142 MiB / 24.8% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **2,625**
      (135 MiB / 45.2% CPU) | **2,625**
      (135 MiB / 45.2% CPU) | -| 256 KiB | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **1,535**
      (111 MiB / 40.4% CPU) | **1,535**
      (111 MiB / 40.4% CPU) | *Not possible (no QUIC)* | *Not possible (no QUIC)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **1,385**
      (169 MiB / 44.6% CPU) | **1,385**
      (169 MiB / 44.6% CPU) | -| 64 KiB | HTTP/2 · plain | HTTP/1 · plain | 🥇 **14,010**
      (174 MiB / 40.5% CPU) | **14,010**
      (174 MiB / 40.5% CPU) | **5,865**
      (127 MiB / 24.0% CPU) | **5,865**
      (127 MiB / 24.0% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **12,443**
      (111 MiB / 39.6% CPU) | **12,443**
      (111 MiB / 39.6% CPU) | -| 64 KiB | HTTP/2 · TLS | HTTP/2 · plain | **6,319**
      (79 MiB / 38.1% CPU) | **6,319**
      (79 MiB / 38.1% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | 🥇 **9,493**
      (131 MiB / 50.3% CPU) | **9,493**
      (131 MiB / 50.3% CPU) | -| 64 KiB | HTTP/2 · TLS | HTTP/2 · TLS | **5,660**
      (80 MiB / 36.4% CPU) | **5,660**
      (80 MiB / 36.4% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | 🥇 **8,201**
      (141 MiB / 47.6% CPU) | **8,201**
      (141 MiB / 47.6% CPU) | -| 64 KiB | HTTP/3 · QUIC | HTTP/2 · TLS | **4,657**
      (129 MiB / 46.1% CPU) | **4,657**
      (129 MiB / 46.1% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | 🥇 **5,021**
      (195 MiB / 47.6% CPU) | **5,021**
      (195 MiB / 47.6% CPU) | -| 64 KiB | HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 **6,149**
      (148 MiB / 42.2% CPU) | **6,149**
      (148 MiB / 42.2% CPU) | *Not possible (no QUIC)* | *Not possible (no QUIC)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **4,496**
      (191 MiB / 49.0% CPU) | **4,496**
      (191 MiB / 49.0% CPU) | -| 256 KiB | HTTP/2 · plain | HTTP/1 · plain | 🥇 **4,457**
      (144 MiB / 31.8% CPU) | **4,457**
      (144 MiB / 31.8% CPU) | **1,759**
      (127 MiB / 23.6% CPU) | **1,759**
      (127 MiB / 23.6% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **3,719**
      (123 MiB / 37.8% CPU) | **3,719**
      (123 MiB / 37.8% CPU) | -| 256 KiB | HTTP/2 · TLS | HTTP/2 · plain | **1,976**
      (84 MiB / 29.9% CPU) | **1,976**
      (84 MiB / 29.9% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | 🥇 **2,325**
      (169 MiB / 46.5% CPU) | **2,325**
      (169 MiB / 46.5% CPU) | -| 256 KiB | HTTP/2 · TLS | HTTP/2 · TLS | **1,758**
      (87 MiB / 29.7% CPU) | **1,758**
      (87 MiB / 29.7% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | 🥇 **2,109**
      (153 MiB / 44.9% CPU) | **2,109**
      (153 MiB / 44.9% CPU) | -| 256 KiB | HTTP/3 · QUIC | HTTP/2 · TLS | **1,164**
      (153 MiB / 43.0% CPU) | **1,164**
      (153 MiB / 43.0% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | 🥇 **1,319**
      (186 MiB / 44.8% CPU) | **1,319**
      (186 MiB / 44.8% CPU) | -| 256 KiB | HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 **1,404**
      (146 MiB / 41.0% CPU) | **1,404**
      (146 MiB / 41.0% CPU) | *Not possible (no QUIC)* | *Not possible (no QUIC)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **1,269**
      (196 MiB / 44.8% CPU) | **1,269**
      (196 MiB / 44.8% CPU) | +Median of **3** repeats on `windows-latest` @ `9a2b3a1e`. Source: Actions [34441570199](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34441570199) (`compare-bodies`). Warmup 2s / measure 8s. **RPS cells** show sustain; `` holds peak (when higher) plus `(MiB / CPU%)`. + +*Not possible:* **HAProxy** and **Envoy** columns are omitted (no official Windows port). + +| Body | Client | Origin | TWP | nginx | YARP | +|---|---|---|---:|---:|---:| +| 64 KiB | HTTP/1 · TLS | HTTP/1 · plain | 🥇 **13,191**
      (122 MiB / 46.7% CPU) | **924**
      (142 MiB / 24.8% CPU) | **11,473**
      (133 MiB / 47.5% CPU) | +| 64 KiB | HTTP/2 · TLS | HTTP/1 · plain | 🥇 **11,769**
      (173 MiB / 46.2% CPU) | **898**
      (142 MiB / 24.8% CPU) | **9,560**
      (135 MiB / 48.1% CPU) | +| 64 KiB | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **5,958**
      (139 MiB / 41.5% CPU) | *Not possible (no QUIC)* | **5,060**
      (185 MiB / 51.0% CPU) | +| 256 KiB | HTTP/1 · TLS | HTTP/1 · plain | 🥇 **3,693**
      (136 MiB / 42.1% CPU) | **241**
      (142 MiB / 24.9% CPU) | **3,325**
      (130 MiB / 47.2% CPU) | +| 256 KiB | HTTP/2 · TLS | HTTP/1 · plain | 🥇 **3,784**
      (149 MiB / 36.2% CPU) | **230**
      (142 MiB / 24.8% CPU) | **2,625**
      (135 MiB / 45.2% CPU) | +| 256 KiB | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **1,535**
      (111 MiB / 40.4% CPU) | *Not possible (no QUIC)* | **1,385**
      (169 MiB / 44.6% CPU) | +| 64 KiB | HTTP/2 · plain | HTTP/1 · plain | 🥇 **14,010**
      (174 MiB / 40.5% CPU) | **5,865**
      (127 MiB / 24.0% CPU) | **12,443**
      (111 MiB / 39.6% CPU) | +| 64 KiB | HTTP/2 · TLS | HTTP/2 · plain | **6,319**
      (79 MiB / 38.1% CPU) | *Not possible* | 🥇 **9,493**
      (131 MiB / 50.3% CPU) | +| 64 KiB | HTTP/2 · TLS | HTTP/2 · TLS | **5,660**
      (80 MiB / 36.4% CPU) | *Not possible* | 🥇 **8,201**
      (141 MiB / 47.6% CPU) | +| 64 KiB | HTTP/3 · QUIC | HTTP/2 · TLS | **4,657**
      (129 MiB / 46.1% CPU) | *Not possible* | 🥇 **5,021**
      (195 MiB / 47.6% CPU) | +| 64 KiB | HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 **6,149**
      (148 MiB / 42.2% CPU) | *Not possible (no QUIC)* | **4,496**
      (191 MiB / 49.0% CPU) | +| 256 KiB | HTTP/2 · plain | HTTP/1 · plain | 🥇 **4,457**
      (144 MiB / 31.8% CPU) | **1,759**
      (127 MiB / 23.6% CPU) | **3,719**
      (123 MiB / 37.8% CPU) | +| 256 KiB | HTTP/2 · TLS | HTTP/2 · plain | **1,976**
      (84 MiB / 29.9% CPU) | *Not possible* | 🥇 **2,325**
      (169 MiB / 46.5% CPU) | +| 256 KiB | HTTP/2 · TLS | HTTP/2 · TLS | **1,758**
      (87 MiB / 29.7% CPU) | *Not possible* | 🥇 **2,109**
      (153 MiB / 44.9% CPU) | +| 256 KiB | HTTP/3 · QUIC | HTTP/2 · TLS | **1,164**
      (153 MiB / 43.0% CPU) | *Not possible* | 🥇 **1,319**
      (186 MiB / 44.8% CPU) | +| 256 KiB | HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 **1,404**
      (146 MiB / 41.0% CPU) | *Not possible (no QUIC)* | **1,269**
      (196 MiB / 44.8% CPU) | nginx/Windows collapses on large reverse bodies in this harness; treat as same-OS only. H1 TLS **64 KiB** ≈ **1.11×** YARP; **256 KiB** ≈ **1.23×**. H2→H1 64 KiB ≈ **1.21×**; H3→H1 64 KiB ≈ **1.18×**. ### Linux — heavier reverse GET (64 KiB / 256 KiB) -Median of **3** repeats @ `9a2b3a1e`. Source: Actions [34441570199](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34441570199) (`compare-bodies`). Warmup 2s / measure 8s. - -| Body | Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | HAProxy sustain | HAProxy peak | Envoy sustain | Envoy peak | YARP sustain | YARP peak | -|---|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| -| 64 KiB | HTTP/1 · TLS | HTTP/1 · plain | **8,010**
      (176 MiB / 45.3% CPU) | **8,010**
      (176 MiB / 45.3% CPU) | **5,555**
      (100 MiB / 52.4% CPU) | **5,555**
      (100 MiB / 52.4% CPU) | 🥇 **9,012**
      (84 MiB / 40.6% CPU) | **9,012**
      (84 MiB / 40.6% CPU) | **7,524**
      (131 MiB / 45.8% CPU) | **7,524**
      (131 MiB / 45.8% CPU) | **6,504**
      (169 MiB / 48.4% CPU) | **6,504**
      (169 MiB / 48.4% CPU) | -| 64 KiB | HTTP/2 · TLS | HTTP/1 · plain | **9,850**
      (231 MiB / 39.4% CPU) | **9,850**
      (231 MiB / 39.4% CPU) | **3,314**
      (103 MiB / 10.9% CPU) | **3,314**
      (103 MiB / 10.9% CPU) | 🥇 **10,145**
      (87 MiB / 24.0% CPU) | **10,145**
      (87 MiB / 24.0% CPU) | **8,389**
      (141 MiB / 23.7% CPU) | **8,389**
      (141 MiB / 23.7% CPU) | **8,231**
      (164 MiB / 46.6% CPU) | **8,231**
      (164 MiB / 46.6% CPU) | -| 64 KiB | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **5,646**
      (184 MiB / 44.2% CPU) | **5,646**
      (184 MiB / 44.2% CPU) | **0**
      (129 MiB / 22.4% CPU) | **1,678**
      (129 MiB / 22.4% CPU) | *Not measured* | *Not measured* | *Not measured* | *Not measured* | **4,421**
      (231 MiB / 51.5% CPU) | **4,421**
      (231 MiB / 51.5% CPU) | -| 256 KiB | HTTP/1 · TLS | HTTP/1 · plain | **2,766**
      (128 MiB / 37.7% CPU) | **2,766**
      (128 MiB / 37.7% CPU) | **1,735**
      (100 MiB / 53.6% CPU) | **1,735**
      (100 MiB / 53.6% CPU) | 🥇 **2,974**
      (83 MiB / 34.0% CPU) | **2,974**
      (83 MiB / 34.0% CPU) | **2,701**
      (145 MiB / 33.9% CPU) | **2,701**
      (145 MiB / 33.9% CPU) | **2,164**
      (166 MiB / 45.8% CPU) | **2,164**
      (166 MiB / 45.8% CPU) | -| 256 KiB | HTTP/2 · TLS | HTTP/1 · plain | **2,550**
      (209 MiB / 32.3% CPU) | **2,550**
      (209 MiB / 32.3% CPU) | **1,728**
      (103 MiB / 19.0% CPU) | **1,728**
      (103 MiB / 19.0% CPU) | 🥇 **3,276**
      (86 MiB / 19.5% CPU) | **3,276**
      (86 MiB / 19.5% CPU) | **3,128**
      (168 MiB / 20.2% CPU) | **3,128**
      (168 MiB / 20.2% CPU) | **2,201**
      (163 MiB / 43.1% CPU) | **2,201**
      (163 MiB / 43.1% CPU) | -| 256 KiB | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **1,458**
      (159 MiB / 43.3% CPU) | **1,458**
      (159 MiB / 43.3% CPU) | **0**
      (120 MiB / 20.0% CPU) | **24**
      (120 MiB / 20.0% CPU) | *Not measured* | *Not measured* | *Not measured* | *Not measured* | **1,278**
      (218 MiB / 48.1% CPU) | **1,278**
      (218 MiB / 48.1% CPU) | -| 64 KiB | HTTP/2 · plain | HTTP/1 · plain | 🥇 **13,722**
      (251 MiB / 41.5% CPU) | **13,722**
      (251 MiB / 41.5% CPU) | **4,929**
      (80 MiB / 10.4% CPU) | **4,929**
      (80 MiB / 10.4% CPU) | **13,114**
      (72 MiB / 23.9% CPU) | **13,114**
      (72 MiB / 23.9% CPU) | **11,084**
      (133 MiB / 23.5% CPU) | **11,084**
      (133 MiB / 23.5% CPU) | **13,042**
      (143 MiB / 45.4% CPU) | **13,042**
      (143 MiB / 45.4% CPU) | -| 64 KiB | HTTP/2 · TLS | HTTP/2 · plain | **3,419**
      (101 MiB / 40.6% CPU) | **3,419**
      (101 MiB / 40.6% CPU) | *Not possible* | *Not possible* | **0**
      (83 MiB / 19.9% CPU) | **0**
      (83 MiB / 19.9% CPU) | **0**
      (126 MiB / 22.4% CPU) | **0**
      (126 MiB / 22.4% CPU) | 🥇 **4,578**
      (185 MiB / 46.3% CPU) | **4,578**
      (185 MiB / 46.3% CPU) | -| 64 KiB | HTTP/2 · TLS | HTTP/2 · TLS | **4,682**
      (104 MiB / 37.3% CPU) | **4,682**
      (104 MiB / 37.3% CPU) | *Not possible* | *Not possible* | **3,969**
      (83 MiB / 24.5% CPU) | **3,969**
      (83 MiB / 24.5% CPU) | 🥇 **6,266**
      (154 MiB / 23.0% CPU) | **6,266**
      (154 MiB / 23.0% CPU) | **6,174**
      (184 MiB / 46.0% CPU) | **6,174**
      (184 MiB / 46.0% CPU) | -| 64 KiB | HTTP/3 · QUIC | HTTP/2 · TLS | **2,941**
      (168 MiB / 52.7% CPU) | **2,941**
      (168 MiB / 52.7% CPU) | *Not possible* | *Not possible* | *Not measured* | *Not measured* | *Not measured* | *Not measured* | 🥇 **3,040**
      (234 MiB / 48.9% CPU) | **3,040**
      (234 MiB / 48.9% CPU) | -| 64 KiB | HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 **6,442**
      (214 MiB / 41.6% CPU) | **6,442**
      (214 MiB / 41.6% CPU) | **0**
      (145 MiB / 17.0% CPU) | **2,278**
      (145 MiB / 17.0% CPU) | *Not measured* | *Not measured* | *Not measured* | *Not measured* | **4,761**
      (248 MiB / 49.0% CPU) | **4,761**
      (248 MiB / 49.0% CPU) | -| 256 KiB | HTTP/2 · plain | HTTP/1 · plain | **3,918**
      (201 MiB / 33.0% CPU) | **3,918**
      (201 MiB / 33.0% CPU) | **1,946**
      (80 MiB / 14.2% CPU) | **1,946**
      (80 MiB / 14.2% CPU) | **4,300**
      (72 MiB / 18.6% CPU) | **4,300**
      (72 MiB / 18.6% CPU) | 🥇 **4,479**
      (153 MiB / 21.0% CPU) | **4,479**
      (153 MiB / 21.0% CPU) | **3,945**
      (157 MiB / 38.1% CPU) | **3,945**
      (157 MiB / 38.1% CPU) | -| 256 KiB | HTTP/2 · TLS | HTTP/2 · plain | **1,139**
      (111 MiB / 33.5% CPU) | **1,139**
      (111 MiB / 33.5% CPU) | *Not possible* | *Not possible* | **0**
      (81 MiB / 19.8% CPU) | **0**
      (81 MiB / 19.8% CPU) | **0**
      (126 MiB / 22.3% CPU) | **0**
      (126 MiB / 22.3% CPU) | 🥇 **1,293**
      (186 MiB / 42.8% CPU) | **1,293**
      (186 MiB / 42.8% CPU) | -| 256 KiB | HTTP/2 · TLS | HTTP/2 · TLS | **1,436**
      (114 MiB / 28.9% CPU) | **1,436**
      (114 MiB / 28.9% CPU) | *Not possible* | *Not possible* | **1,114**
      (89 MiB / 24.3% CPU) | **1,114**
      (89 MiB / 24.3% CPU) | 🥇 **1,826**
      (160 MiB / 22.8% CPU) | **1,826**
      (160 MiB / 22.8% CPU) | **1,734**
      (185 MiB / 42.8% CPU) | **1,734**
      (185 MiB / 42.8% CPU) | -| 256 KiB | HTTP/3 · QUIC | HTTP/2 · TLS | **837**
      (187 MiB / 51.5% CPU) | **837**
      (187 MiB / 51.5% CPU) | *Not possible* | *Not possible* | *Not measured* | *Not measured* | *Not measured* | *Not measured* | 🥇 **915**
      (246 MiB / 46.9% CPU) | **915**
      (246 MiB / 46.9% CPU) | -| 256 KiB | HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 **1,745**
      (189 MiB / 42.2% CPU) | **1,745**
      (189 MiB / 42.2% CPU) | **0**
      (143 MiB / 18.7% CPU) | **619**
      (143 MiB / 18.7% CPU) | *Not measured* | *Not measured* | *Not measured* | *Not measured* | **1,487**
      (249 MiB / 47.5% CPU) | **1,487**
      (249 MiB / 47.5% CPU) | +Median of **3** repeats @ `a495a9ae`. Source: Actions [34557778171](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34557778171) + [34557780393](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34557780393) (`compare-bodies`; H3 HAProxy/Envoy peers). Warmup 2s / measure 8s. + +| Body | Client | Origin | TWP | nginx | HAProxy | Envoy | YARP | +|---|---|---|---:|---:|---:|---:|---:| +| 64 KiB | HTTP/1 · TLS | HTTP/1 · plain | **15,191**
      (177 MiB / 41.8% CPU) | **10,978**
      (103 MiB / 44.9% CPU) | 🥇 **17,384**
      (85 MiB / 36.7% CPU) | **14,303**
      (131 MiB / 45.2% CPU) | **11,995**
      (154 MiB / 48.6% CPU) | +| 64 KiB | HTTP/2 · TLS | HTTP/1 · plain | 🥇 **8,911**
      (238 MiB / 39.8% CPU) | **2,775**
      (103 MiB / 16.7% CPU) | **8,042**
      (84 MiB / 24.1% CPU) | **7,272**
      (139 MiB / 23.6% CPU) | **7,672**
      (162 MiB / 45.6% CPU) | +| 64 KiB | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **8,792**
      (198 MiB / 42.4% CPU) | **0**
      (peak 2,666 · 133 MiB / 16.1% CPU) | **7,200**
      (90 MiB / 29.5% CPU) | **7**
      (137 MiB / 0.1% CPU) | **6,048**
      (238 MiB / 52.5% CPU) | +| 256 KiB | HTTP/1 · TLS | HTTP/1 · plain | **4,783**
      (129 MiB / 31.1% CPU) | **3,268**
      (101 MiB / 42.5% CPU) | 🥇 **5,304**
      (85 MiB / 26.4% CPU) | **4,485**
      (146 MiB / 31.5% CPU) | **3,582**
      (174 MiB / 43.2% CPU) | +| 256 KiB | HTTP/2 · TLS | HTTP/1 · plain | **2,275**
      (221 MiB / 32.4% CPU) | **861**
      (102 MiB / 19.2% CPU) | 🥇 **2,802**
      (83 MiB / 20.4% CPU) | **2,700**
      (167 MiB / 19.8% CPU) | **2,068**
      (162 MiB / 41.8% CPU) | +| 256 KiB | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **2,104**
      (165 MiB / 41.6% CPU) | **0**
      (peak 549 · 128 MiB / 15.8% CPU) | **2,046**
      (90 MiB / 28.9% CPU) | **20**
      (161 MiB / 0.5% CPU) | **1,765**
      (224 MiB / 47.7% CPU) | +| 64 KiB | HTTP/2 · plain | HTTP/1 · plain | 🥇 **13,135**
      (223 MiB / 42.0% CPU) | **3,378**
      (80 MiB / 14.6% CPU) | **10,431**
      (69 MiB / 24.5% CPU) | **10,400**
      (132 MiB / 23.4% CPU) | **11,945**
      (145 MiB / 43.6% CPU) | +| 64 KiB | HTTP/2 · TLS | HTTP/2 · plain | **6,803**
      (103 MiB / 38.8% CPU) | *Not possible* | **5,393**
      (86 MiB / 24.5% CPU) | 🥇 **8,349**
      (141 MiB / 22.1% CPU) | **8,200**
      (191 MiB / 47.6% CPU) | +| 64 KiB | HTTP/2 · TLS | HTTP/2 · TLS | **4,299**
      (103 MiB / 38.9% CPU) | *Not possible* | **2,634**
      (82 MiB / 24.7% CPU) | **4,879**
      (147 MiB / 23.1% CPU) | 🥇 **5,624**
      (180 MiB / 44.4% CPU) | +| 64 KiB | HTTP/3 · QUIC | HTTP/2 · TLS | **4,570**
      (177 MiB / 54.5% CPU) | *Not possible* | **4,441**
      (92 MiB / 32.6% CPU) | **22**
      (143 MiB / 0.2% CPU) | 🥇 **4,826**
      (237 MiB / 49.5% CPU) | +| 64 KiB | HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 **6,089**
      (218 MiB / 42.4% CPU) | **0**
      (peak 2,056 · 144 MiB / 23.2% CPU) | **5,152**
      (96 MiB / 34.0% CPU) | **13**
      (139 MiB / 0.2% CPU) | **4,813**
      (246 MiB / 49.6% CPU) | +| 256 KiB | HTTP/2 · plain | HTTP/1 · plain | **3,541**
      (198 MiB / 34.2% CPU) | **0**
      (peak 526 · 80 MiB / 8.7% CPU) | **3,864**
      (70 MiB / 20.3% CPU) | 🥇 **3,876**
      (159 MiB / 19.9% CPU) | **3,710**
      (154 MiB / 33.6% CPU) | +| 256 KiB | HTTP/2 · TLS | HTTP/2 · plain | **1,912**
      (110 MiB / 28.6% CPU) | *Not possible* | **1,565**
      (89 MiB / 24.2% CPU) | 🥇 **3,000**
      (171 MiB / 21.9% CPU) | **2,032**
      (200 MiB / 39.8% CPU) | +| 256 KiB | HTTP/2 · TLS | HTTP/2 · TLS | **1,291**
      (116 MiB / 31.1% CPU) | *Not possible* | **737**
      (84 MiB / 24.5% CPU) | **1,365**
      (161 MiB / 22.0% CPU) | 🥇 **1,502**
      (184 MiB / 40.2% CPU) | +| 256 KiB | HTTP/3 · QUIC | HTTP/2 · TLS | **1,210**
      (196 MiB / 54.0% CPU) | *Not possible* | **1,260**
      (94 MiB / 31.2% CPU) | **0**
      (162 MiB / 0.1% CPU) | 🥇 **1,424**
      (231 MiB / 47.5% CPU) | +| 256 KiB | HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 **1,715**
      (195 MiB / 43.0% CPU) | **0**
      (145 MiB / 0.3% CPU) | **1,336**
      (96 MiB / 30.6% CPU) | **0**
      (152 MiB / 0.1% CPU) | **1,471**
      (247 MiB / 47.1% CPU) | On this GHA pass TWP÷YARP H1 TLS ≈ **1.23×** (64 KiB) / **1.28×** (256 KiB); H2→H1 ≈ **1.23×** / **1.16×**; H3→H1 ≈ **1.27×** / **1.13×**. TWP÷nginx H1 TLS ≈ **1.43** / **1.58**. Absolute RPS swings by VM; prefer ratios. @@ -492,66 +499,71 @@ On this GHA pass TWP÷YARP H1 TLS ≈ **1.23×** (64 KiB) / **1.28×** (256 KiB) Median of **3** repeats on `windows-latest` @ `9a2b3a1e`. Source: Actions [34441591377](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34441591377) (`compare-post`). -| Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | HAProxy sustain | HAProxy peak | Envoy sustain | Envoy peak | YARP sustain | YARP peak | -|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| -| HTTP/1 · TLS | HTTP/1 · plain | 🥇 **5,883**
      (94 MiB / 46.6% CPU) | **5,883**
      (94 MiB / 46.6% CPU) | **352**
      (142 MiB / 24.7% CPU) | **352**
      (142 MiB / 24.7% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **4,093**
      (137 MiB / 55.7% CPU) | **4,093**
      (137 MiB / 55.7% CPU) | -| HTTP/2 · TLS | HTTP/1 · plain | 🥇 **3,997**
      (184 MiB / 49.3% CPU) | **3,997**
      (184 MiB / 49.3% CPU) | **352**
      (143 MiB / 24.7% CPU) | **352**
      (143 MiB / 24.7% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **3,494**
      (134 MiB / 52.1% CPU) | **3,494**
      (134 MiB / 52.1% CPU) | -| HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **2,039**
      (162 MiB / 40.4% CPU) | **2,039**
      (162 MiB / 40.4% CPU) | *Not possible (no QUIC)* | *Not possible (no QUIC)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **1,893**
      (203 MiB / 48.6% CPU) | **1,893**
      (203 MiB / 48.6% CPU) | -| HTTP/2 · plain | HTTP/1 · plain | 🥇 **6,237**
      (182 MiB / 46.1% CPU) | **6,237**
      (182 MiB / 46.1% CPU) | **1,622**
      (130 MiB / 24.6% CPU) | **1,622**
      (130 MiB / 24.6% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **5,889**
      (125 MiB / 53.5% CPU) | **5,889**
      (125 MiB / 53.5% CPU) | -| HTTP/2 · TLS | HTTP/2 · plain | **8**
      (83 MiB / 0.2% CPU) | **8**
      (83 MiB / 0.2% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | 🥇 **3,602**
      (143 MiB / 49.3% CPU) | **3,602**
      (143 MiB / 49.3% CPU) | -| HTTP/2 · TLS | HTTP/2 · TLS | **8**
      (88 MiB / 0.1% CPU) | **8**
      (88 MiB / 0.1% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | 🥇 **2,837**
      (142 MiB / 46.8% CPU) | **2,837**
      (142 MiB / 46.8% CPU) | -| HTTP/3 · QUIC | HTTP/2 · TLS | **1,769**
      (178 MiB / 44.3% CPU) | **1,769**
      (178 MiB / 44.3% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | 🥇 **1,857**
      (194 MiB / 47.4% CPU) | **1,857**
      (194 MiB / 47.4% CPU) | -| HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 **1,935**
      (175 MiB / 42.5% CPU) | **1,935**
      (175 MiB / 42.5% CPU) | *Not possible (no QUIC)* | *Not possible (no QUIC)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **1,768**
      (213 MiB / 47.8% CPU) | **1,768**
      (213 MiB / 47.8% CPU) | +*Not possible:* **HAProxy** and **Envoy** columns are omitted (no official Windows port). + +| Client | Origin | TWP | nginx | YARP | +|---|---|---:|---:|---:| +| HTTP/1 · TLS | HTTP/1 · plain | 🥇 **5,883**
      (94 MiB / 46.6% CPU) | **352**
      (142 MiB / 24.7% CPU) | **4,093**
      (137 MiB / 55.7% CPU) | +| HTTP/2 · TLS | HTTP/1 · plain | 🥇 **3,997**
      (184 MiB / 49.3% CPU) | **352**
      (143 MiB / 24.7% CPU) | **3,494**
      (134 MiB / 52.1% CPU) | +| HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **2,039**
      (162 MiB / 40.4% CPU) | *Not possible (no QUIC)* | **1,893**
      (203 MiB / 48.6% CPU) | +| HTTP/2 · plain | HTTP/1 · plain | 🥇 **6,237**
      (182 MiB / 46.1% CPU) | **1,622**
      (130 MiB / 24.6% CPU) | **5,889**
      (125 MiB / 53.5% CPU) | +| HTTP/2 · TLS | HTTP/2 · plain | **8**
      (83 MiB / 0.2% CPU) | *Not possible* | 🥇 **3,602**
      (143 MiB / 49.3% CPU) | +| HTTP/2 · TLS | HTTP/2 · TLS | **8**
      (88 MiB / 0.1% CPU) | *Not possible* | 🥇 **2,837**
      (142 MiB / 46.8% CPU) | +| HTTP/3 · QUIC | HTTP/2 · TLS | **1,769**
      (178 MiB / 44.3% CPU) | *Not possible* | 🥇 **1,857**
      (194 MiB / 47.4% CPU) | +| HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 **1,935**
      (175 MiB / 42.5% CPU) | *Not possible (no QUIC)* | **1,768**
      (213 MiB / 47.8% CPU) | TWP leads H1 POST (~**1.5×** YARP), H2 POST (~**1.2×** YARP), and H3 POST (~**1.1×** YARP). ### Linux — POST 64 KiB request + 64 KiB response -Median of **3** repeats @ `9a2b3a1e`. Source: Actions [34441591377](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34441591377) (`compare-post`). +Median of **3** repeats @ `a495a9ae`. Source: Actions [34557782264](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34557782264) (`compare-post`; H3 HAProxy/Envoy peers). -| Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | HAProxy sustain | HAProxy peak | Envoy sustain | Envoy peak | YARP sustain | YARP peak | -|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| -| HTTP/1 · TLS | HTTP/1 · plain | **5,113**
      (133 MiB / 43.7% CPU) | **5,113**
      (133 MiB / 43.7% CPU) | **4,029**
      (100 MiB / 48.2% CPU) | **4,029**
      (100 MiB / 48.2% CPU) | **5,273**
      (86 MiB / 40.5% CPU) | **5,273**
      (86 MiB / 40.5% CPU) | 🥇 **5,337**
      (131 MiB / 40.4% CPU) | **5,337**
      (131 MiB / 40.4% CPU) | **3,410**
      (176 MiB / 54.5% CPU) | **3,410**
      (176 MiB / 54.5% CPU) | -| HTTP/2 · TLS | HTTP/1 · plain | 🥇 **3,356**
      (220 MiB / 47.1% CPU) | **3,356**
      (220 MiB / 47.1% CPU) | **1,563**
      (116 MiB / 21.2% CPU) | **1,563**
      (116 MiB / 21.2% CPU) | **2,012**
      (83 MiB / 24.0% CPU) | **2,012**
      (83 MiB / 24.0% CPU) | **0**
      (146 MiB / 20.5% CPU) | **2,914**
      (146 MiB / 20.5% CPU) | **2,767**
      (171 MiB / 47.9% CPU) | **2,767**
      (171 MiB / 47.9% CPU) | -| HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **3,052**
      (226 MiB / 43.7% CPU) | **3,052**
      (226 MiB / 43.7% CPU) | **557**
      (110 MiB / 25.0% CPU) | **557**
      (110 MiB / 25.0% CPU) | *Not measured* | *Not measured* | *Not measured* | *Not measured* | **2,778**
      (240 MiB / 49.2% CPU) | **2,778**
      (240 MiB / 49.2% CPU) | -| HTTP/2 · plain | HTTP/1 · plain | 🥇 **5,779**
      (234 MiB / 42.8% CPU) | **5,779**
      (234 MiB / 42.8% CPU) | **2,488**
      (94 MiB / 22.3% CPU) | **2,488**
      (94 MiB / 22.3% CPU) | **3,011**
      (70 MiB / 23.1% CPU) | **3,011**
      (70 MiB / 23.1% CPU) | **0**
      (134 MiB / 22.7% CPU) | **5,195**
      (134 MiB / 22.7% CPU) | **4,169**
      (161 MiB / 41.4% CPU) | **4,169**
      (161 MiB / 41.4% CPU) | -| HTTP/2 · TLS | HTTP/2 · plain | **6**
      (118 MiB / 0.2% CPU) | **6**
      (118 MiB / 0.2% CPU) | *Not possible* | *Not possible* | **0**
      (90 MiB / 23.7% CPU) | **0**
      (90 MiB / 23.7% CPU) | **0**
      (153 MiB / 22.1% CPU) | **0**
      (153 MiB / 22.1% CPU) | 🥇 **2,620**
      (179 MiB / 46.6% CPU) | **2,620**
      (179 MiB / 46.6% CPU) | -| HTTP/2 · TLS | HTTP/2 · TLS | **8**
      (115 MiB / 0.3% CPU) | **8**
      (115 MiB / 0.3% CPU) | *Not possible* | *Not possible* | **1,216**
      (88 MiB / 24.6% CPU) | **1,216**
      (88 MiB / 24.6% CPU) | 🥇 **2,222**
      (147 MiB / 23.3% CPU) | **2,222**
      (147 MiB / 23.3% CPU) | **2,085**
      (181 MiB / 46.1% CPU) | **2,085**
      (181 MiB / 46.1% CPU) | -| HTTP/3 · QUIC | HTTP/2 · TLS | 🥇 **1,999**
      (236 MiB / 49.4% CPU) | **1,999**
      (236 MiB / 49.4% CPU) | *Not possible* | *Not possible* | *Not measured* | *Not measured* | *Not measured* | *Not measured* | **1,950**
      (245 MiB / 46.9% CPU) | **1,950**
      (245 MiB / 46.9% CPU) | -| HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 **2,301**
      (253 MiB / 44.2% CPU) | **2,301**
      (253 MiB / 44.2% CPU) | **471**
      (120 MiB / 25.1% CPU) | **471**
      (120 MiB / 25.1% CPU) | *Not measured* | *Not measured* | *Not measured* | *Not measured* | **2,138**
      (274 MiB / 47.8% CPU) | **2,138**
      (274 MiB / 47.8% CPU) | +| Client | Origin | TWP | nginx | HAProxy | Envoy | YARP | +|---|---|---:|---:|---:|---:|---:| +| HTTP/1 · TLS | HTTP/1 · plain | **5,003**
      (137 MiB / 44.0% CPU) | **4,021**
      (99 MiB / 48.4% CPU) | **5,302**
      (85 MiB / 40.0% CPU) | 🥇 **5,331**
      (131 MiB / 40.0% CPU) | **3,440**
      (175 MiB / 54.5% CPU) | +| HTTP/2 · TLS | HTTP/1 · plain | 🥇 **3,311**
      (219 MiB / 46.8% CPU) | **1,627**
      (114 MiB / 21.9% CPU) | **2,082**
      (84 MiB / 24.0% CPU) | **0**
      (peak 3,225 · 145 MiB / 22.6% CPU) | **2,730**
      (164 MiB / 48.9% CPU) | +| HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **3,051**
      (224 MiB / 43.8% CPU) | **556**
      (112 MiB / 24.9% CPU) | **2,210**
      (92 MiB / 34.9% CPU) | **0**
      (126 MiB / 0.1% CPU) | **2,672**
      (245 MiB / 49.2% CPU) | +| HTTP/2 · plain | HTTP/1 · plain | 🥇 **5,664**
      (230 MiB / 42.2% CPU) | **2,442**
      (97 MiB / 23.0% CPU) | **2,928**
      (67 MiB / 23.1% CPU) | **0**
      (peak 5,034 · 130 MiB / 23.4% CPU) | **4,432**
      (156 MiB / 46.2% CPU) | +| HTTP/2 · TLS | HTTP/2 · plain | **8**
      (112 MiB / 0.2% CPU) | *Not possible* | **1,566**
      (88 MiB / 23.5% CPU) | 🥇 **3,108**
      (141 MiB / 23.5% CPU) | **2,548**
      (186 MiB / 47.7% CPU) | +| HTTP/2 · TLS | HTTP/2 · TLS | **8**
      (124 MiB / 0.2% CPU) | *Not possible* | **1,209**
      (85 MiB / 24.6% CPU) | **1,949**
      (149 MiB / 20.7% CPU) | 🥇 **2,010**
      (179 MiB / 46.2% CPU) | +| HTTP/3 · QUIC | HTTP/2 · TLS | 🥇 **1,974**
      (240 MiB / 49.3% CPU) | *Not possible* | **1,292**
      (94 MiB / 32.3% CPU) | **0**
      (126 MiB / 0.1% CPU) | **1,905**
      (253 MiB / 46.8% CPU) | +| HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 **2,286**
      (236 MiB / 44.1% CPU) | **462**
      (120 MiB / 24.8% CPU) | **1,727**
      (95 MiB / 35.8% CPU) | **0**
      (126 MiB / 0.1% CPU) | **2,079**
      (272 MiB / 47.5% CPU) | Linux nginx H1/H2/H3 POST completed (nginx.org mainline). TWP÷YARP H1 ≈ **1.5×**; H2 ≈ **1.3×**; H3 ≈ **1.1×**. TWP÷nginx H3 ≈ **4×**. ### Windows — lossy / high-RTT (H2 HOL / H3 loss) Userspace **5 ms** one-way delay + **1%** TCP connection stall (H1/H2) or UDP datagram drop (H3); **64 KiB** GET. Median of **3** repeats on `windows-latest` @ `9a2b3a1e` — [34441595456](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34441595456) (`compare-lossy`). -| Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | HAProxy sustain | HAProxy peak | Envoy sustain | Envoy peak | YARP sustain | YARP peak | -|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| -| HTTP/1 · TLS | HTTP/1 · plain | 🥇 **663**
      (112 MiB / 3.0% CPU) | **663**
      (112 MiB / 3.0% CPU) | **652**
      (143 MiB / 16.4% CPU) | **652**
      (143 MiB / 16.4% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **662**
      (121 MiB / 4.5% CPU) | **662**
      (121 MiB / 4.5% CPU) | -| HTTP/2 · TLS | HTTP/1 · plain | **0**
      (120 MiB / 1.4% CPU) | **86**
      (120 MiB / 1.4% CPU) | **0**
      (142 MiB / 0.6% CPU) | **17**
      (142 MiB / 0.6% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **0**
      (99 MiB / 0.8% CPU) | **16**
      (99 MiB / 0.8% CPU) | -| HTTP/3 · QUIC | HTTP/1 · plain | **0**
      (67 MiB / 0.1% CPU) | **0**
      (67 MiB / 0.1% CPU) | *Not possible (no QUIC)* | *Not possible (no QUIC)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **0**
      (80 MiB / 0.0% CPU) | **0**
      (80 MiB / 0.0% CPU) | -| HTTP/2 · plain | HTTP/1 · plain | **0**
      (130 MiB / 1.5% CPU) | **89**
      (130 MiB / 1.5% CPU) | **0**
      (128 MiB / 0.1% CPU) | **17**
      (128 MiB / 0.1% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **0**
      (91 MiB / 0.7% CPU) | **16**
      (91 MiB / 0.7% CPU) | -| HTTP/2 · TLS | HTTP/2 · plain | **0**
      (68 MiB / 0.6% CPU) | **8**
      (68 MiB / 0.6% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **0**
      (100 MiB / 0.7% CPU) | **16**
      (100 MiB / 0.7% CPU) | -| HTTP/2 · TLS | HTTP/2 · TLS | **0**
      (71 MiB / 0.2% CPU) | **8**
      (71 MiB / 0.2% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **0**
      (99 MiB / 0.8% CPU) | **16**
      (99 MiB / 0.8% CPU) | -| HTTP/3 · QUIC | HTTP/2 · TLS | **0**
      (70 MiB / 0.1% CPU) | **0**
      (70 MiB / 0.1% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **0**
      (80 MiB / 0.0% CPU) | **0**
      (80 MiB / 0.0% CPU) | -| HTTP/3 · QUIC | HTTP/1 · TLS | **0**
      (69 MiB / 0.0% CPU) | **0**
      (69 MiB / 0.0% CPU) | *Not possible (no QUIC)* | *Not possible (no QUIC)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **0**
      (78 MiB / 0.0% CPU) | **0**
      (78 MiB / 0.0% CPU) | - -TWP H2 HOL leads (~**3.31×** YARP). H3 is the protocol-shape win vs H2 HOL on the same lossy session; Win H3 GHA remains 0 (laptop remeasure kept above). + +*Not possible:* **HAProxy** and **Envoy** columns are omitted (no official Windows port). + +| Client | Origin | TWP | nginx | YARP | +|---|---|---:|---:|---:| +| HTTP/1 · TLS | HTTP/1 · plain | 🥇 **663**
      (112 MiB / 3.0% CPU) | **652**
      (143 MiB / 16.4% CPU) | **662**
      (121 MiB / 4.5% CPU) | +| HTTP/2 · TLS | HTTP/1 · plain | **0**
      (peak 86 · 120 MiB / 1.4% CPU) | **0**
      (peak 17 · 142 MiB / 0.6% CPU) | **0**
      (peak 16 · 99 MiB / 0.8% CPU) | +| HTTP/3 · QUIC | HTTP/1 · plain | **0**
      (67 MiB / 0.1% CPU) | *Not possible (no QUIC)* | **0**
      (80 MiB / 0.0% CPU) | +| HTTP/2 · plain | HTTP/1 · plain | **0**
      (peak 89 · 130 MiB / 1.5% CPU) | **0**
      (peak 17 · 128 MiB / 0.1% CPU) | **0**
      (peak 16 · 91 MiB / 0.7% CPU) | +| HTTP/2 · TLS | HTTP/2 · plain | **0**
      (peak 8 · 68 MiB / 0.6% CPU) | *Not possible* | **0**
      (peak 16 · 100 MiB / 0.7% CPU) | +| HTTP/2 · TLS | HTTP/2 · TLS | **0**
      (peak 8 · 71 MiB / 0.2% CPU) | *Not possible* | **0**
      (peak 16 · 99 MiB / 0.8% CPU) | +| HTTP/3 · QUIC | HTTP/2 · TLS | **0**
      (70 MiB / 0.1% CPU) | *Not possible* | **0**
      (80 MiB / 0.0% CPU) | +| HTTP/3 · QUIC | HTTP/1 · TLS | **0**
      (69 MiB / 0.0% CPU) | *Not possible (no QUIC)* | **0**
      (78 MiB / 0.0% CPU) | + +TWP H2 HOL leads (~**3.31×** YARP). H3 is the protocol-shape win vs H2 HOL on the same lossy session; Win H3 GHA remains 0 (laptop re-measure kept above). ### Linux — lossy / high-RTT (H2 HOL / H3 loss) -Median of **3** repeats @ `9a2b3a1e`. Source: [34441595456](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34441595456) (`compare-lossy`; lossy H3 uses `quic-http3`). +Median of **3** repeats @ `a495a9ae`. Source: [34557784262](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34557784262) (`compare-lossy`; H3 HAProxy/Envoy peers; lossy H3 uses `quic-http3`). -| Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | HAProxy sustain | HAProxy peak | Envoy sustain | Envoy peak | YARP sustain | YARP peak | -|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| -| HTTP/1 · TLS | HTTP/1 · plain | **1,195**
      (142 MiB / 13.7% CPU) | **1,195**
      (142 MiB / 13.7% CPU) | **1,205**
      (100 MiB / 12.6% CPU) | **1,205**
      (100 MiB / 12.6% CPU) | 🥇 **1,207**
      (83 MiB / 7.8% CPU) | **1,207**
      (83 MiB / 7.8% CPU) | **1,193**
      (128 MiB / 9.3% CPU) | **1,193**
      (128 MiB / 9.3% CPU) | **1,194**
      (150 MiB / 17.5% CPU) | **1,194**
      (150 MiB / 17.5% CPU) | -| HTTP/2 · TLS | HTTP/1 · plain | 🥇 **314**
      (182 MiB / 7.0% CPU) | **314**
      (182 MiB / 7.0% CPU) | **40**
      (99 MiB / 0.3% CPU) | **40**
      (99 MiB / 0.3% CPU) | **40**
      (86 MiB / 0.3% CPU) | **40**
      (86 MiB / 0.3% CPU) | **40**
      (135 MiB / 0.4% CPU) | **40**
      (135 MiB / 0.4% CPU) | **40**
      (127 MiB / 1.5% CPU) | **40**
      (127 MiB / 1.5% CPU) | -| HTTP/3 · QUIC | HTTP/1 · plain | **319**
      (151 MiB / 13.9% CPU) | **319**
      (151 MiB / 13.9% CPU) | **92**
      (109 MiB / 2.8% CPU) | **92**
      (109 MiB / 2.8% CPU) | *Not measured* | *Not measured* | *Not measured* | *Not measured* | 🥇 **338**
      (177 MiB / 22.4% CPU) | **338**
      (177 MiB / 22.4% CPU) | -| HTTP/2 · plain | HTTP/1 · plain | 🥇 **344**
      (174 MiB / 7.2% CPU) | **344**
      (174 MiB / 7.2% CPU) | **40**
      (78 MiB / 0.2% CPU) | **40**
      (78 MiB / 0.2% CPU) | **40**
      (69 MiB / 0.2% CPU) | **40**
      (69 MiB / 0.2% CPU) | **40**
      (129 MiB / 0.4% CPU) | **40**
      (129 MiB / 0.4% CPU) | **41**
      (121 MiB / 1.2% CPU) | **41**
      (121 MiB / 1.2% CPU) | -| HTTP/2 · TLS | HTTP/2 · plain | **0**
      (93 MiB / 0.6% CPU) | **13**
      (93 MiB / 0.6% CPU) | *Not possible* | *Not possible* | **0**
      (83 MiB / 2.7% CPU) | **0**
      (83 MiB / 2.7% CPU) | **0**
      (127 MiB / 6.3% CPU) | **0**
      (127 MiB / 6.3% CPU) | 🥇 **40**
      (129 MiB / 1.6% CPU) | **40**
      (129 MiB / 1.6% CPU) | -| HTTP/2 · TLS | HTTP/2 · TLS | **0**
      (95 MiB / 0.7% CPU) | **11**
      (95 MiB / 0.7% CPU) | *Not possible* | *Not possible* | 🥇 **42**
      (88 MiB / 0.6% CPU) | **42**
      (88 MiB / 0.6% CPU) | **40**
      (138 MiB / 0.5% CPU) | **40**
      (138 MiB / 0.5% CPU) | **40**
      (129 MiB / 1.6% CPU) | **40**
      (129 MiB / 1.6% CPU) | -| HTTP/3 · QUIC | HTTP/2 · TLS | **322**
      (140 MiB / 24.2% CPU) | **322**
      (140 MiB / 24.2% CPU) | *Not possible* | *Not possible* | *Not measured* | *Not measured* | *Not measured* | *Not measured* | 🥇 **333**
      (183 MiB / 24.3% CPU) | **333**
      (183 MiB / 24.3% CPU) | -| HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 **341**
      (161 MiB / 15.8% CPU) | **341**
      (161 MiB / 15.8% CPU) | **96**
      (113 MiB / 3.0% CPU) | **96**
      (113 MiB / 3.0% CPU) | *Not measured* | *Not measured* | *Not measured* | *Not measured* | **340**
      (185 MiB / 23.1% CPU) | **340**
      (185 MiB / 23.1% CPU) | +| Client | Origin | TWP | nginx | HAProxy | Envoy | YARP | +|---|---|---:|---:|---:|---:|---:| +| HTTP/1 · TLS | HTTP/1 · plain | **1,198**
      (144 MiB / 13.5% CPU) | **1,208**
      (100 MiB / 12.6% CPU) | 🥇 **1,210**
      (84 MiB / 7.3% CPU) | **1,200**
      (128 MiB / 9.4% CPU) | **1,195**
      (146 MiB / 17.3% CPU) | +| HTTP/2 · TLS | HTTP/1 · plain | 🥇 **315**
      (175 MiB / 6.8% CPU) | **40**
      (99 MiB / 0.3% CPU) | **40**
      (84 MiB / 0.2% CPU) | **40**
      (136 MiB / 0.4% CPU) | **40**
      (129 MiB / 1.4% CPU) | +| HTTP/3 · QUIC | HTTP/1 · plain | **336**
      (148 MiB / 14.1% CPU) | **95**
      (109 MiB / 2.5% CPU) | **62**
      (85 MiB / 3.9% CPU) | 🥇 **1,225**
      (141 MiB / 21.5% CPU) | **299**
      (181 MiB / 20.8% CPU) | +| HTTP/2 · plain | HTTP/1 · plain | 🥇 **358**
      (174 MiB / 7.0% CPU) | **40**
      (78 MiB / 0.2% CPU) | **41**
      (69 MiB / 0.2% CPU) | **40**
      (128 MiB / 0.3% CPU) | **40**
      (122 MiB / 1.0% CPU) | +| HTTP/2 · TLS | HTTP/2 · plain | **0**
      (peak 12 · 97 MiB / 0.5% CPU) | *Not possible* | 🥇 **42**
      (90 MiB / 0.4% CPU) | **40**
      (136 MiB / 0.3% CPU) | **41**
      (131 MiB / 1.5% CPU) | +| HTTP/2 · TLS | HTTP/2 · TLS | **0**
      (peak 12 · 96 MiB / 0.7% CPU) | *Not possible* | **40**
      (85 MiB / 0.6% CPU) | **40**
      (137 MiB / 0.5% CPU) | 🥇 **40**
      (130 MiB / 1.7% CPU) | +| HTTP/3 · QUIC | HTTP/2 · TLS | **319**
      (140 MiB / 25.3% CPU) | *Not possible* | **63**
      (92 MiB / 4.3% CPU) | 🥇 **1,101**
      (145 MiB / 24.4% CPU) | **336**
      (185 MiB / 24.1% CPU) | +| HTTP/3 · QUIC | HTTP/1 · TLS | **338**
      (166 MiB / 15.2% CPU) | **91**
      (114 MiB / 2.9% CPU) | **59**
      (91 MiB / 4.4% CPU) | 🥇 **1,136**
      (140 MiB / 23.1% CPU) | **338**
      (187 MiB / 21.3% CPU) | TWP H2 HOL ≫ YARP (~**7.7×**). H3 TWP÷YARP ≈ **1×**. @@ -559,41 +571,43 @@ TWP H2 HOL ≫ YARP (~**7.7×**). H3 TWP÷YARP ≈ **1×**. These runs isolate slow app readers, origin-early response, HTTP/2 duplex, and WebSocket echo. See [TWP vs YARP IO model](Performance-Profiling#twp-vs-yarp-io-model). Laptop 1-rep numbers are on [Performance Local Lab](Performance-Local-Lab#architecture-sensitive). -Median of **3** repeats on matched 4 vCPU / 16 GiB runners @ `9a2b3a1e` ([34441578556](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34441578556)). Slow consumer = 256 KiB GET, 16 KiB read + 8 ms sleep. Early response = 64 KiB POST, origin writes after 8 KiB. Duplex HTTP/2 = overlapping 64 KiB POST on H2 TLS↔H2 TLS. WebSocket = echo round-trips/sec. +Median of **3** repeats on matched 4 vCPU / 16 GiB runners @ `9a2b3a1e` ([34441578556](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34441578556)). Slow consumer = 256 KiB GET, 16 KiB read + 8 ms sleep. Early response = 64 KiB POST, origin writes after 8 KiB. Duplex HTTP/2 = overlapping 64 KiB POST on H2 TLS↔H2 TLS. WebSocket row = H1 Upgrade echo round-trips/sec (not RFC 8441; see WebSocket RFC 8441 table below). Lossy-link runs (slow **network**) are already published above; they are not a slow **app** reader. #### Windows -| Scenario | Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | HAProxy sustain | HAProxy peak | Envoy sustain | Envoy peak | YARP sustain | YARP peak | -|---|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| -| Slow consumer (256 KiB GET, throttled client read) | HTTP/1 · TLS | HTTP/1 · plain | 🥇 **243**
      (101 MiB / 5.9% CPU) | **243**
      (101 MiB / 5.9% CPU) | **204**
      (143 MiB / 24.6% CPU) | **204**
      (143 MiB / 24.6% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **241**
      (109 MiB / 4.8% CPU) | **241**
      (109 MiB / 4.8% CPU) | -| Slow consumer (256 KiB GET, throttled client read) | HTTP/2 · TLS | HTTP/1 · plain | **256**
      (131 MiB / 3.7% CPU) | **256**
      (131 MiB / 3.7% CPU) | **230**
      (142 MiB / 24.4% CPU) | **230**
      (142 MiB / 24.4% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | 🥇 **256**
      (111 MiB / 5.4% CPU) | **256**
      (111 MiB / 5.4% CPU) | -| Slow consumer (256 KiB GET, throttled client read) | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **264**
      (101 MiB / 17.4% CPU) | **264**
      (101 MiB / 17.4% CPU) | *Not possible (no QUIC)* | *Not possible (no QUIC)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **255**
      (158 MiB / 20.4% CPU) | **255**
      (158 MiB / 20.4% CPU) | -| Early response (origin writes after first request chunk) | HTTP/1 · TLS | HTTP/1 · plain | 🥇 **5,611**
      (99 MiB / 45.5% CPU) | **5,611**
      (99 MiB / 45.5% CPU) | **363**
      (143 MiB / 24.7% CPU) | **363**
      (143 MiB / 24.7% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **3,919**
      (138 MiB / 53.9% CPU) | **3,919**
      (138 MiB / 53.9% CPU) | -| Early response (origin writes after first request chunk) | HTTP/2 · TLS | HTTP/1 · plain | 🥇 **3,934**
      (169 MiB / 48.8% CPU) | **3,934**
      (169 MiB / 48.8% CPU) | **0**
      (144 MiB / 24.8% CPU) | **318**
      (144 MiB / 24.8% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **3,200**
      (135 MiB / 53.4% CPU) | **3,200**
      (135 MiB / 53.4% CPU) | -| Early response (origin writes after first request chunk) | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **3,012**
      (152 MiB / 42.0% CPU) | **3,012**
      (152 MiB / 42.0% CPU) | *Not possible (no QUIC)* | *Not possible (no QUIC)* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **2,602**
      (203 MiB / 51.0% CPU) | **2,602**
      (203 MiB / 51.0% CPU) | -| Slow consumer (256 KiB GET, throttled client read) | HTTP/2 · plain | HTTP/1 · plain | **248**
      (112 MiB / 3.6% CPU) | **248**
      (112 MiB / 3.6% CPU) | **248**
      (127 MiB / 9.4% CPU) | **248**
      (127 MiB / 9.4% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | 🥇 **256**
      (104 MiB / 6.0% CPU) | **256**
      (104 MiB / 6.0% CPU) | -| Slow consumer (256 KiB GET, throttled client read) | HTTP/2 · TLS | HTTP/2 · TLS | **0**
      (70 MiB / 1.0% CPU) | **8**
      (70 MiB / 1.0% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | 🥇 **256**
      (136 MiB / 9.3% CPU) | **256**
      (136 MiB / 9.3% CPU) | -| Early response (origin writes after first request chunk) | HTTP/2 · TLS | HTTP/2 · TLS | **0**
      (92 MiB / 0.1% CPU) | **0**
      (92 MiB / 0.1% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **0**
      (110 MiB / 0.1% CPU) | **0**
      (110 MiB / 0.1% CPU) | -| Duplex (both directions live) | HTTP/2 · TLS | HTTP/2 · TLS | **0**
      (91 MiB / 0.1% CPU) | **0**
      (91 MiB / 0.1% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **0**
      (111 MiB / 0.1% CPU) | **0**
      (111 MiB / 0.1% CPU) | -| Duplex (WebSocket / extended CONNECT) | HTTP/1 · TLS | HTTP/1 · plain | 🥇 **24,498**
      (97 MiB / 43.0% CPU) | **24,498**
      (97 MiB / 43.0% CPU) | **12,337**
      (143 MiB / 24.6% CPU) | **12,337**
      (143 MiB / 24.6% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **23,100**
      (89 MiB / 44.6% CPU) | **23,100**
      (89 MiB / 44.6% CPU) | +*Not possible:* **HAProxy** and **Envoy** columns are omitted (no official Windows port). + +| Scenario | Client | Origin | TWP | nginx | YARP | +|---|---|---|---:|---:|---:| +| Slow consumer (256 KiB GET, throttled client read) | HTTP/1 · TLS | HTTP/1 · plain | 🥇 **243**
      (101 MiB / 5.9% CPU) | **204**
      (143 MiB / 24.6% CPU) | **241**
      (109 MiB / 4.8% CPU) | +| Slow consumer (256 KiB GET, throttled client read) | HTTP/2 · TLS | HTTP/1 · plain | **256**
      (131 MiB / 3.7% CPU) | **230**
      (142 MiB / 24.4% CPU) | 🥇 **256**
      (111 MiB / 5.4% CPU) | +| Slow consumer (256 KiB GET, throttled client read) | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **264**
      (101 MiB / 17.4% CPU) | *Not possible (no QUIC)* | **255**
      (158 MiB / 20.4% CPU) | +| Early response (origin writes after first request chunk) | HTTP/1 · TLS | HTTP/1 · plain | 🥇 **5,611**
      (99 MiB / 45.5% CPU) | **363**
      (143 MiB / 24.7% CPU) | **3,919**
      (138 MiB / 53.9% CPU) | +| Early response (origin writes after first request chunk) | HTTP/2 · TLS | HTTP/1 · plain | 🥇 **3,934**
      (169 MiB / 48.8% CPU) | **0**
      (peak 318 · 144 MiB / 24.8% CPU) | **3,200**
      (135 MiB / 53.4% CPU) | +| Early response (origin writes after first request chunk) | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **3,012**
      (152 MiB / 42.0% CPU) | *Not possible (no QUIC)* | **2,602**
      (203 MiB / 51.0% CPU) | +| Slow consumer (256 KiB GET, throttled client read) | HTTP/2 · plain | HTTP/1 · plain | **248**
      (112 MiB / 3.6% CPU) | **248**
      (127 MiB / 9.4% CPU) | 🥇 **256**
      (104 MiB / 6.0% CPU) | +| Slow consumer (256 KiB GET, throttled client read) | HTTP/2 · TLS | HTTP/2 · TLS | **0**
      (peak 8 · 70 MiB / 1.0% CPU) | *Not possible* | 🥇 **256**
      (136 MiB / 9.3% CPU) | +| Early response (origin writes after first request chunk) | HTTP/2 · TLS | HTTP/2 · TLS | **0**
      (92 MiB / 0.1% CPU) | *Not possible* | **0**
      (110 MiB / 0.1% CPU) | +| Duplex (both directions live) | HTTP/2 · TLS | HTTP/2 · TLS | **0**
      (91 MiB / 0.1% CPU) | *Not possible* | **0**
      (111 MiB / 0.1% CPU) | +| Duplex (WebSocket / H1 Upgrade) | HTTP/1 · TLS | HTTP/1 · plain | 🥇 **24,498**
      (97 MiB / 43.0% CPU) | **12,337**
      (143 MiB / 24.6% CPU) | **23,100**
      (89 MiB / 44.6% CPU) | #### Linux -| Scenario | Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | HAProxy sustain | HAProxy peak | Envoy sustain | Envoy peak | YARP sustain | YARP peak | -|---|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| -| Slow consumer (256 KiB GET, throttled client read) | HTTP/1 · TLS | HTTP/1 · plain | **469**
      (122 MiB / 9.7% CPU) | **469**
      (122 MiB / 9.7% CPU) | **411**
      (100 MiB / 9.9% CPU) | **411**
      (100 MiB / 9.9% CPU) | **466**
      (83 MiB / 6.0% CPU) | **466**
      (83 MiB / 6.0% CPU) | 🥇 **470**
      (140 MiB / 6.2% CPU) | **470**
      (140 MiB / 6.2% CPU) | **411**
      (146 MiB / 14.2% CPU) | **411**
      (146 MiB / 14.2% CPU) | -| Slow consumer (256 KiB GET, throttled client read) | HTTP/2 · TLS | HTTP/1 · plain | 🥇 **480**
      (149 MiB / 13.1% CPU) | **480**
      (149 MiB / 13.1% CPU) | **374**
      (102 MiB / 5.0% CPU) | **374**
      (102 MiB / 5.0% CPU) | **475**
      (85 MiB / 4.3% CPU) | **475**
      (85 MiB / 4.3% CPU) | **477**
      (149 MiB / 4.5% CPU) | **477**
      (149 MiB / 4.5% CPU) | **477**
      (146 MiB / 15.0% CPU) | **477**
      (146 MiB / 15.0% CPU) | -| Slow consumer (256 KiB GET, throttled client read) | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **473**
      (131 MiB / 34.1% CPU) | **473**
      (131 MiB / 34.1% CPU) | **0**
      (119 MiB / 21.9% CPU) | **122**
      (119 MiB / 21.9% CPU) | *Not measured* | *Not measured* | *Not measured* | *Not measured* | **468**
      (198 MiB / 39.8% CPU) | **468**
      (198 MiB / 39.8% CPU) | -| Early response (origin writes after first request chunk) | HTTP/1 · TLS | HTTP/1 · plain | **4,671**
      (132 MiB / 47.4% CPU) | **4,671**
      (132 MiB / 47.4% CPU) | **0**
      (100 MiB / 49.6% CPU) | **3,397**
      (100 MiB / 49.6% CPU) | 🥇 **4,886**
      (84 MiB / 43.3% CPU) | **4,886**
      (84 MiB / 43.3% CPU) | **0**
      (129 MiB / 25.6% CPU) | **2,584**
      (129 MiB / 25.6% CPU) | **3,159**
      (176 MiB / 56.3% CPU) | **3,159**
      (176 MiB / 56.3% CPU) | -| Early response (origin writes after first request chunk) | HTTP/2 · TLS | HTTP/1 · plain | 🥇 **3,266**
      (242 MiB / 48.3% CPU) | **3,266**
      (242 MiB / 48.3% CPU) | **0**
      (113 MiB / 24.7% CPU) | **1,365**
      (113 MiB / 24.7% CPU) | **2,480**
      (85 MiB / 24.6% CPU) | **2,480**
      (85 MiB / 24.6% CPU) | **0**
      (148 MiB / 23.9% CPU) | **2,653**
      (148 MiB / 23.9% CPU) | **2,198**
      (169 MiB / 48.3% CPU) | **2,198**
      (169 MiB / 48.3% CPU) | -| Early response (origin writes after first request chunk) | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **4,504**
      (218 MiB / 43.2% CPU) | **4,504**
      (218 MiB / 43.2% CPU) | **0**
      (125 MiB / 22.7% CPU) | **1,090**
      (125 MiB / 22.7% CPU) | *Not measured* | *Not measured* | *Not measured* | *Not measured* | **3,217**
      (274 MiB / 48.7% CPU) | **3,217**
      (274 MiB / 48.7% CPU) | -| Slow consumer (256 KiB GET, throttled client read) | HTTP/2 · plain | HTTP/1 · plain | 🥇 **478**
      (136 MiB / 15.5% CPU) | **478**
      (136 MiB / 15.5% CPU) | **360**
      (79 MiB / 8.6% CPU) | **360**
      (79 MiB / 8.6% CPU) | **473**
      (69 MiB / 6.4% CPU) | **473**
      (69 MiB / 6.4% CPU) | **473**
      (147 MiB / 4.7% CPU) | **473**
      (147 MiB / 4.7% CPU) | **475**
      (150 MiB / 16.2% CPU) | **475**
      (150 MiB / 16.2% CPU) | -| Slow consumer (256 KiB GET, throttled client read) | HTTP/2 · TLS | HTTP/2 · TLS | **8**
      (97 MiB / 2.5% CPU) | **8**
      (97 MiB / 2.5% CPU) | *Not possible* | *Not possible* | **455**
      (89 MiB / 22.6% CPU) | **455**
      (89 MiB / 22.6% CPU) | **466**
      (152 MiB / 12.9% CPU) | **466**
      (152 MiB / 12.9% CPU) | 🥇 **466**
      (168 MiB / 31.1% CPU) | **466**
      (168 MiB / 31.1% CPU) | -| Early response (origin writes after first request chunk) | HTTP/2 · TLS | HTTP/2 · TLS | **0**
      (112 MiB / 0.1% CPU) | **0**
      (112 MiB / 0.1% CPU) | *Not possible* | *Not possible* | *Not measured* | *Not measured* | **0**
      (156 MiB / 19.5% CPU) | **3,680**
      (156 MiB / 19.5% CPU) | **0**
      (146 MiB / 0.2% CPU) | **0**
      (146 MiB / 0.2% CPU) | -| Duplex (both directions live) | HTTP/2 · TLS | HTTP/2 · TLS | **0**
      (112 MiB / 0.1% CPU) | **0**
      (112 MiB / 0.1% CPU) | *Not possible* | *Not possible* | **0**
      (91 MiB / 0.1% CPU) | **0**
      (91 MiB / 0.1% CPU) | **0**
      (151 MiB / 19.4% CPU) | **3,724**
      (151 MiB / 19.4% CPU) | **0**
      (151 MiB / 0.1% CPU) | **0**
      (151 MiB / 0.1% CPU) | -| Duplex (WebSocket / extended CONNECT) | HTTP/1 · TLS | HTTP/1 · plain | **28,567**
      (125 MiB / 44.0% CPU) | **28,567**
      (125 MiB / 44.0% CPU) | 🥇 **33,775**
      (100 MiB / 35.5% CPU) | **33,775**
      (100 MiB / 35.5% CPU) | **31,870**
      (84 MiB / 39.3% CPU) | **31,870**
      (84 MiB / 39.3% CPU) | **31,525**
      (127 MiB / 39.4% CPU) | **31,525**
      (127 MiB / 39.4% CPU) | **27,109**
      (125 MiB / 44.1% CPU) | **27,109**
      (125 MiB / 44.1% CPU) | +| Scenario | Client | Origin | TWP | nginx | HAProxy | Envoy | YARP | +|---|---|---|---:|---:|---:|---:|---:| +| Slow consumer (256 KiB GET, throttled client read) | HTTP/1 · TLS | HTTP/1 · plain | **467**
      (119 MiB / 9.7% CPU) | **412**
      (100 MiB / 9.7% CPU) | 🥇 **472**
      (82 MiB / 5.9% CPU) | **466**
      (139 MiB / 6.3% CPU) | **419**
      (148 MiB / 14.5% CPU) | +| Slow consumer (256 KiB GET, throttled client read) | HTTP/2 · TLS | HTTP/1 · plain | **472**
      (145 MiB / 20.3% CPU) | **459**
      (100 MiB / 22.4% CPU) | 🥇 **474**
      (84 MiB / 9.3% CPU) | **466**
      (157 MiB / 7.6% CPU) | **473**
      (152 MiB / 25.1% CPU) | +| Slow consumer (256 KiB GET, throttled client read) | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **472**
      (131 MiB / 35.5% CPU) | **0**
      (peak 379 · 124 MiB / 22.5% CPU) | **468**
      (88 MiB / 11.7% CPU) | **0**
      (145 MiB / 0.1% CPU) | **470**
      (199 MiB / 42.5% CPU) | +| Early response (origin writes after first request chunk) | HTTP/1 · TLS | HTTP/1 · plain | **4,751**
      (132 MiB / 47.4% CPU) | **3,736**
      (100 MiB / 50.2% CPU) | 🥇 **4,951**
      (85 MiB / 43.0% CPU) | **0**
      (peak 2,679 · 130 MiB / 25.6% CPU) | **3,212**
      (170 MiB / 56.5% CPU) | +| Early response (origin writes after first request chunk) | HTTP/2 · TLS | HTTP/1 · plain | 🥇 **3,300**
      (232 MiB / 48.8% CPU) | **0**
      (peak 1,308 · 115 MiB / 23.5% CPU) | **2,508**
      (84 MiB / 23.9% CPU) | **0**
      (peak 2,660 · 150 MiB / 24.1% CPU) | **2,234**
      (171 MiB / 49.0% CPU) | +| Early response (origin writes after first request chunk) | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 **2,868**
      (195 MiB / 45.0% CPU) | **0**
      (peak 456 · 115 MiB / 24.8% CPU) | **1,977**
      (91 MiB / 34.7% CPU) | **0**
      (127 MiB / 0.1% CPU) | **2,117**
      (244 MiB / 48.3% CPU) | +| Slow consumer (256 KiB GET, throttled client read) | HTTP/2 · plain | HTTP/1 · plain | **476**
      (140 MiB / 15.2% CPU) | **454**
      (79 MiB / 11.8% CPU) | 🥇 **478**
      (68 MiB / 6.4% CPU) | **473**
      (151 MiB / 4.6% CPU) | **478**
      (148 MiB / 15.8% CPU) | +| Slow consumer (256 KiB GET, throttled client read) | HTTP/2 · TLS | HTTP/2 · TLS | **8**
      (97 MiB / 2.5% CPU) | *Not possible* | **448**
      (88 MiB / 21.4% CPU) | **455**
      (148 MiB / 12.7% CPU) | 🥇 **462**
      (159 MiB / 31.1% CPU) | +| Early response (origin writes after first request chunk) | HTTP/2 · TLS | HTTP/2 · TLS | **0**
      (108 MiB / 0.1% CPU) | *Not possible* | 🥇 **0**
      (93 MiB / 0.1% CPU) | **0**
      (peak 1,942 · 154 MiB / 20.0% CPU) | **0**
      (148 MiB / 0.2% CPU) | +| Duplex (both directions live) | HTTP/2 · TLS | HTTP/2 · TLS | **0**
      (108 MiB / 0.1% CPU) | *Not possible* | **0**
      (93 MiB / 0.1% CPU) | **0**
      (peak 1,994 · 156 MiB / 20.8% CPU) | **0**
      (145 MiB / 0.2% CPU) | +| Duplex (WebSocket / H1 Upgrade) | HTTP/1 · TLS | HTTP/1 · plain | **31,261**
      (126 MiB / 43.2% CPU) | 🥇 **34,370**
      (100 MiB / 35.8% CPU) | **33,060**
      (82 MiB / 38.5% CPU) | **32,066**
      (127 MiB / 39.8% CPU) | **27,020**
      (125 MiB / 43.8% CPU) | Slow consumer is sleep-bound; H1/H2/H3 sit in the same band. Early-response H1/H2/H3: TWP leads (H1 early ≈ **2.00×** / **1.47×** YARP Win/Linux). **Duplex H2**: YARP leads by design — Win ≈ **0.59×** (1,270 / 2,135), Linux ≈ **0.15×** (282 / 1,882); irreducible concurrent-copier cell (see [IO model](Performance-Profiling#twp-vs-yarp-io-model)). WebSocket: TWP÷YARP Windows ≈ **1.06×**; Linux nginx leads. @@ -605,21 +619,23 @@ Isolates keep-alive tiny GET vs **new connection per request** (handshake-domina Median of **3** repeats on `windows-latest` @ `9a2b3a1e`. Source: Actions [34441599658](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34441599658). Absolute RPS on GHA swings hard; prefer **TWP÷YARP**. -| Workload | TWP sustain | TWP peak | nginx sustain | nginx peak | HAProxy sustain | HAProxy peak | Envoy sustain | Envoy peak | YARP sustain | YARP peak | -|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| -| Keep-alive · tiny GET | 🥇 **20,612**
      (87 MiB / 47.9% CPU) | **20,612**
      (87 MiB / 47.9% CPU) | **8,972**
      (142 MiB / 24.8% CPU) | **8,972**
      (142 MiB / 24.8% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **18,392**
      (105 MiB / 50.9% CPU) | **18,392**
      (105 MiB / 50.9% CPU) | -| New-connection · tiny GET | 🥇 **732**
      (88 MiB / 9.5% CPU) | **732**
      (88 MiB / 9.5% CPU) | **0**
      (140 MiB / 24.4% CPU) | **248**
      (140 MiB / 24.4% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **725**
      (113 MiB / 10.7% CPU) | **725**
      (113 MiB / 10.7% CPU) | -| Keep-alive · 256 KiB GET | 🥇 **2,741**
      (130 MiB / 47.0% CPU) | **2,741**
      (130 MiB / 47.0% CPU) | **0**
      (142 MiB / 24.6% CPU) | **162**
      (142 MiB / 24.6% CPU) | *Not possible* | *Not possible* | *Not possible* | *Not possible* | **2,699**
      (136 MiB / 49.4% CPU) | **2,699**
      (136 MiB / 49.4% CPU) | +*Not possible:* **HAProxy** and **Envoy** columns are omitted (no official Windows port). + +| Workload | TWP | nginx | YARP | +|---|---:|---:|---:| +| Keep-alive · tiny GET | 🥇 **20,612**
      (87 MiB / 47.9% CPU) | **8,972**
      (142 MiB / 24.8% CPU) | **18,392**
      (105 MiB / 50.9% CPU) | +| New-connection · tiny GET | 🥇 **732**
      (88 MiB / 9.5% CPU) | **0**
      (peak 248 · 140 MiB / 24.4% CPU) | **725**
      (113 MiB / 10.7% CPU) | +| Keep-alive · 256 KiB GET | 🥇 **2,741**
      (130 MiB / 47.0% CPU) | **0**
      (peak 162 · 142 MiB / 24.6% CPU) | **2,699**
      (136 MiB / 49.4% CPU) | #### Linux Median of **3** repeats @ `9a2b3a1e`. Source: Actions [34441599658](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34441599658). -| Workload | TWP sustain | TWP peak | nginx sustain | nginx peak | HAProxy sustain | HAProxy peak | Envoy sustain | Envoy peak | YARP sustain | YARP peak | -|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| -| Keep-alive · tiny GET | **36,114**
      (108 MiB / 49.4% CPU) | **36,114**
      (108 MiB / 49.4% CPU) | 🥇 **44,372**
      (102 MiB / 40.8% CPU) | **44,372**
      (102 MiB / 40.8% CPU) | **43,256**
      (84 MiB / 43.3% CPU) | **43,256**
      (84 MiB / 43.3% CPU) | **27,636**
      (128 MiB / 57.3% CPU) | **27,636**
      (128 MiB / 57.3% CPU) | **32,712**
      (135 MiB / 49.8% CPU) | **32,712**
      (135 MiB / 49.8% CPU) | -| New-connection · tiny GET | **1,549**
      (128 MiB / 40.0% CPU) | **1,549**
      (128 MiB / 40.0% CPU) | 🥇 **1,598**
      (103 MiB / 36.2% CPU) | **1,598**
      (103 MiB / 36.2% CPU) | **1,454**
      (85 MiB / 37.7% CPU) | **1,454**
      (85 MiB / 37.7% CPU) | **1,379**
      (129 MiB / 44.5% CPU) | **1,379**
      (129 MiB / 44.5% CPU) | **1,525**
      (149 MiB / 38.9% CPU) | **1,525**
      (149 MiB / 38.9% CPU) | -| Keep-alive · 256 KiB GET | **3,790**
      (126 MiB / 31.8% CPU) | **3,790**
      (126 MiB / 31.8% CPU) | **2,421**
      (101 MiB / 48.7% CPU) | **2,421**
      (101 MiB / 48.7% CPU) | 🥇 **3,940**
      (84 MiB / 28.4% CPU) | **3,940**
      (84 MiB / 28.4% CPU) | **3,544**
      (145 MiB / 32.3% CPU) | **3,544**
      (145 MiB / 32.3% CPU) | **3,031**
      (160 MiB / 42.1% CPU) | **3,031**
      (160 MiB / 42.1% CPU) | +| Workload | TWP | nginx | HAProxy | Envoy | YARP | +|---|---:|---:|---:|---:|---:| +| Keep-alive · tiny GET | **36,114**
      (108 MiB / 49.4% CPU) | 🥇 **44,372**
      (102 MiB / 40.8% CPU) | **43,256**
      (84 MiB / 43.3% CPU) | **27,636**
      (128 MiB / 57.3% CPU) | **32,712**
      (135 MiB / 49.8% CPU) | +| New-connection · tiny GET | **1,549**
      (128 MiB / 40.0% CPU) | 🥇 **1,598**
      (103 MiB / 36.2% CPU) | **1,454**
      (85 MiB / 37.7% CPU) | **1,379**
      (129 MiB / 44.5% CPU) | **1,525**
      (149 MiB / 38.9% CPU) | +| Keep-alive · 256 KiB GET | **3,790**
      (126 MiB / 31.8% CPU) | **2,421**
      (101 MiB / 48.7% CPU) | 🥇 **3,940**
      (84 MiB / 28.4% CPU) | **3,544**
      (145 MiB / 32.3% CPU) | **3,031**
      (160 MiB / 42.1% CPU) | All three workloads are **>1.00×** YARP on both OS. nginx leads Linux keep-alive tiny and Linux new-connection; TWP is second, YARP third. @@ -629,9 +645,43 @@ Unary Echo **RPC/s** @ c=64 over H2 TLS→H2 TLS for Titanium, YARP, nginx (`grp | OS | Titanium | YARP | nginx | HAProxy | Envoy | |---|---:|---:|---:|---:|---:| -| Windows | **53,762**
      (88 MiB / 23.6% CPU) | **30,754**
      (123 MiB / 46.9% CPU) | *Not measured* | *Not possible* | *Not possible* | -| Linux | **42,675**
      (120 MiB / 30.3% CPU) | **24,232**
      (159 MiB / 41.6% CPU) | *Not measured* | **8,683**
      (84 MiB / 24.6% CPU) | **16,110**
      (128 MiB / 20.6% CPU) | -| macOS | **14,924**
      (94 MiB / 20.1% CPU) | **8,581**
      (128 MiB / 28.4% CPU) | *Not measured* | *Not measured* | *Not measured* | +| Windows | **53,762**
      (88 MiB / 23.6% CPU) | **30,754**
      (123 MiB / 46.9% CPU) | **0**
      (153 MiB / 24.8% CPU) | *Not possible* | *Not possible* | +| Linux | **42,675**
      (120 MiB / 30.3% CPU) | **24,232**
      (159 MiB / 41.6% CPU) | **0**
      (120 MiB / 24.9% CPU) | **8,683**
      (84 MiB / 24.6% CPU) | **16,110**
      (128 MiB / 20.6% CPU) | +| macOS | **14,924**
      (94 MiB / 20.1% CPU) | **8,581**
      (128 MiB / 28.4% CPU) | **0**
      (97 MiB / 2.5% CPU) | **0**
      (68 MiB / 5.1% CPU) | **0**
      (84 MiB / 19.0% CPU) | + +## Unary gRPC (H2 TLS → h2c) + +Unary Echo **RPC/s** @ c=64 over **H2 TLS → h2c** (edge TLS, cleartext H2 origin). Peers: Titanium, YARP, HAProxy, Envoy. Mode: `compare-grpc` (`*-grpc-h2c`). Win/Linux from prior paste; macOS @ `a495a9ae` — [34557768163](https://github.com/justcoding121/titanium-web-proxy/actions/runs/34557768163). + +*Not possible:* **nginx** column omitted (no H2 upstream). + +| OS | Titanium | YARP | HAProxy | Envoy | +|---|---:|---:|---:|---:| +| Windows | **60,942**
      (93 MiB / 22.8% CPU) | **33,648**
      (111 MiB / 49.7% CPU) | *Not possible* | *Not possible* | +| Linux | **44,967**
      (121 MiB / 29.2% CPU) | **24,678**
      (150 MiB / 44.1% CPU) | **7,735**
      (82 MiB / 24.6% CPU) | **12,516**
      (128 MiB / 22.6% CPU) | +| macOS | **14,333**
      (90 MiB / 23.8% CPU) | **9,316**
      (132 MiB / 34.8% CPU) | **0**
      (65 MiB / 17.6% CPU) | **0**
      (80 MiB / 20.6% CPU) | + +## WebSocket (H1 TLS → H1 TLS) + +WebSocket echo round-trips/sec over **dual-TLS** H1 (`proxy_ssl` style). Mode: `compare-ws-h1tls` (`*-duplex-ws-h1tls`). + +| OS | Titanium | YARP | nginx | HAProxy | Envoy | +|---|---:|---:|---:|---:|---:| +| Windows | **27,472**
      (101 MiB / 44.3% CPU) | **26,068**
      (92 MiB / 44.1% CPU) | **13,043**
      (142 MiB / 24.7% CPU) | *Not possible* | *Not possible* | +| Linux | **25,489**
      (138 MiB / 43.2% CPU) | **22,409**
      (134 MiB / 44.1% CPU) | **27,071**
      (103 MiB / 36.8% CPU) | **26,782**
      (85 MiB / 38.9% CPU) | **27,011**
      (127 MiB / 38.1% CPU) | +| macOS | **8,868**
      (118 MiB / 35.0% CPU) | **12,323**
      (127 MiB / 28.2% CPU) | **6,231**
      (74 MiB / 21.1% CPU) | **11,067**
      (68 MiB / 30.2% CPU) | **4,940**
      (80 MiB / 30.3% CPU) | + +## WebSocket (H2 TLS 8441 → H1) + +WebSocket echo over **RFC 8441** extended CONNECT (H2 TLS client → H1 plain origin). Mode: `compare-ws-h2` (`*-duplex-ws-h2`). + +*Not possible:* **nginx** column omitted (no RFC 8441 extended CONNECT reverse). + +| OS | Titanium | YARP | HAProxy | Envoy | +|---|---:|---:|---:|---:| +| Windows | **25,713**
      (144 MiB / 42.9% CPU) | **0**
      (167 MiB / 20.9% CPU) | *Not possible* | *Not possible* | +| Linux | **1,531**
      (147 MiB / 16.0% CPU) | **0**
      (161 MiB / 25.4% CPU) | **1,534**
      (82 MiB / 5.3% CPU) | **0**
      (124 MiB / 0.3% CPU) | +| macOS | **11,061**
      (384 MiB / 43.3% CPU) | **0**
      (132 MiB / 17.7% CPU) | **10,080**
      (66 MiB / 33.1% CPU) | **0**
      (73 MiB / 0.3% CPU) | ## Other measurements @@ -703,7 +753,9 @@ CI cadence, shards, paste scripts, and gate floors for people refreshing these t | Cross-version (Gate 2) | `compare-cross-version` | ~1–2h vs committed 6.0 baselines | | Pre-wiki smoke | `compare-product-smoke` (Linux 2 shards, `repeats=1`) | ~30–60 min; required before full product | | Release / wiki | `compare-product` (**3** comparison-group shards × Win/Linux/mac) | ~2–2½h wall (Free account queues beyond 20 jobs) | -| Unary gRPC | `compare-grpc` | Win/Linux/mac; RPC/s @ c=64 | +| Unary gRPC | `compare-grpc` | Win/Linux/mac; H2↔H2 + H2→h2c (`arm_shard` 1/2, 2/2) | +| WebSocket dual-TLS | `compare-ws-h1tls` | Win/Linux/mac; H1 TLS→H1 TLS echo | +| WebSocket RFC 8441 | `compare-ws-h2` | Win/Linux/mac; H2 TLS→H1 plain (nginx N/A) | | Heavier tables | `compare-bodies` (**2** shards) / `post` / `lossy` / `arch` (**3** shards) / `tls-cost` | dispatch independently | See [PERF-GATES.md](https://github.com/justcoding121/titanium-web-proxy/blob/develop/tools/RpsLoadProbe/PERF-GATES.md). diff --git a/wiki/Security-Considerations.md b/wiki/Security-Considerations.md index 9c2134c02..068cf69d1 100644 --- a/wiki/Security-Considerations.md +++ b/wiki/Security-Considerations.md @@ -74,6 +74,18 @@ shared, multi-tenant build/CI user), protect the key with OS-level mechanisms ou control — a hardware-backed key store, a dedicated service account, or equivalent — rather than expecting the file relocation alone to isolate it. +## Decrypt failure bypass is a heuristic, not fingerprint matching + +`ProxyServer.EnableDecryptFailureBypass` (off by default in the library; on by default in Inspector) tunnels +later CONNECTs without decrypt after repeated **origin** TLS handshake failures under MITM, or after +MITM HTTPS **403/429** (document navigations activate immediately and meta-refresh onto a new opaque +CONNECT; other requests use the shared strike threshold; synthetic inspector/script responses are +ignored). It does **not** detect JA3/Akamai by name — TLS failures look like ordinary +`AuthenticationException`. False positives are possible; use a TTL/LRU-bounded session list and +**Promote** known-bad hosts into permanent tunnel-only exclusions. Post-MITM conversion of the forged-cert +session is impossible; seamless recovery relies on a new CONNECT. Learned hosts skip MITM prefetch. +Embedded proxies should leave the flag off unless they need this behavior. + ## See also - [Migration guide: 4.x → 5.0](Migration-4.x-to-5.0) — the full list of behavior changes this release diff --git a/wiki/images/rps-practical-heavier-linux.png b/wiki/images/rps-practical-heavier-linux.png index 8a0e5576b..e2c02dc6c 100644 Binary files a/wiki/images/rps-practical-heavier-linux.png and b/wiki/images/rps-practical-heavier-linux.png differ diff --git a/wiki/images/rps-practical-heavier-windows.png b/wiki/images/rps-practical-heavier-windows.png index 817772390..6519edc8d 100644 Binary files a/wiki/images/rps-practical-heavier-windows.png and b/wiki/images/rps-practical-heavier-windows.png differ diff --git a/wiki/images/rps-practical-linux.png b/wiki/images/rps-practical-linux.png index 73218d626..e6b83823d 100644 Binary files a/wiki/images/rps-practical-linux.png and b/wiki/images/rps-practical-linux.png differ diff --git a/wiki/images/rps-practical-macos.png b/wiki/images/rps-practical-macos.png index 556900768..8d8b8a5af 100644 Binary files a/wiki/images/rps-practical-macos.png and b/wiki/images/rps-practical-macos.png differ diff --git a/wiki/images/rps-practical-windows.png b/wiki/images/rps-practical-windows.png index 7b18a7dea..86bba41d9 100644 Binary files a/wiki/images/rps-practical-windows.png and b/wiki/images/rps-practical-windows.png differ