diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml index 6753dfb6e..fb8b6a281 100644 --- a/.github/workflows/deploy-website.yml +++ b/.github/workflows/deploy-website.yml @@ -58,6 +58,10 @@ jobs: mkdir -p website/.vitepress/dist/api if [ -d docs/api ]; then cp -a docs/api/. website/.vitepress/dist/api/ + # DocFX has toc.html but no index; /api/ 404s without this. + if [ -f website/.vitepress/dist/api/toc.html ] && [ ! -f website/.vitepress/dist/api/index.html ]; then + cp -f website/.vitepress/dist/api/toc.html website/.vitepress/dist/api/index.html + fi # DocFX HTML references ../styles and ../fonts from /api/*.html if [ -d docs/styles ]; then cp -a docs/styles website/.vitepress/dist/styles @@ -76,13 +80,18 @@ jobs: echo "Warning: docs/api missing; API pages will not be published" fi - # GitHub Pages + VitePress cleanUrls: /download sometimes keeps a stale - # object while /download.html updates. Publish both shapes so the nav link works. + # GitHub Pages cleanUrls: keep BOTH `page.html` and `page/index.html`. + # Deleting the sibling `.html` forces `/page` (no slash) through a Pages 301 + # that rewrites the host to `*.github.io` when the custom-domain CNAME is not + # fully active on the edge (CloudFront → GitHub). That is what made + # titaniumproxy.com/download bounce to github.io. - name: Mirror cleanUrls HTML as index.html shell: bash run: | set -euo pipefail dist=website/.vitepress/dist + # Do NOT write a Pages CNAME: titaniumproxy.com DNS points at CloudFront, + # not GitHub. A Pages custom domain causes http↔https redirect loops. for f in "$dist"/*.html; do base="$(basename "$f" .html)" [[ "$base" == "index" || "$base" == "404" ]] && continue @@ -106,13 +115,13 @@ jobs: path: website/.vitepress/dist deploy: + # Push deploys only from develop (Pages env protection). Manual workflow_dispatch + # may publish from a fix branch to clear a bad artifact urgently. if: > github.event_name != 'pull_request' && ( github.ref == 'refs/heads/develop' || - github.ref == 'refs/heads/beta' || - github.ref == 'refs/heads/stable' || - github.event_name == 'release' || - github.event_name == 'workflow_dispatch' + github.event_name == 'workflow_dispatch' || + (github.event_name == 'release' && startsWith(github.event.release.tag_name, 'v')) ) needs: build runs-on: ubuntu-latest diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 763dd8cfe..5db312b47 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -152,7 +152,7 @@ jobs: pull: '--rebase --autostash' # Cross-OS Inspector + Plus dashboard UI gates (Headless / Visual / Playwright). - # Inspector unit suite stays on Windows `build` only - do not re-run it here. + # Inspector unit suite stays on Windows `build` only except Inspector-Stress (below). ui-portable: runs-on: ${{ matrix.os }} timeout-minutes: 35 @@ -169,6 +169,90 @@ jobs: with: dotnet-version: | 10.0.x + - name: Assert in-box MsQuic (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + if (-not [System.Net.Quic.QuicListener]::IsSupported) { + throw 'QuicListener.IsSupported is false on windows-latest (expected in-box MsQuic)' + } + Write-Host "QuicListener.IsSupported=$([System.Net.Quic.QuicListener]::IsSupported)" + - name: Install libmsquic (Linux HTTP/3) + if: runner.os == 'Linux' + run: | + set -euo pipefail + . /etc/os-release + curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \ + "https://packages.microsoft.com/config/${ID}/${VERSION_ID}/packages-microsoft-prod.deb" \ + -o packages-microsoft-prod.deb + sudo dpkg -i packages-microsoft-prod.deb + rm -f packages-microsoft-prod.deb + sudo apt-get update + sudo apt-get install -y libmsquic + pwsh -NoProfile -Command 'if (-not [System.Net.Quic.QuicListener]::IsSupported) { throw "QuicListener.IsSupported is false after libmsquic install" }; Write-Host "QuicListener.IsSupported=$([System.Net.Quic.QuicListener]::IsSupported)"' + - name: Install MsQuic (macOS HTTP/3) + if: runner.os == 'macOS' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + # Intel macOS bottles are sparse; do not force-upgrade openssl (no bottle → job fail). + $env:HOMEBREW_NO_AUTO_UPDATE = '1' + $env:HOMEBREW_NO_INSTALL_UPGRADE = '1' + brew install openssl@3 libmsquic + $prefix = (& brew --prefix).Trim() + $msquicLib = Join-Path $prefix 'opt/libmsquic/lib' + $sslLib = Join-Path $prefix 'opt/openssl@3/lib' + $libDirs = @($msquicLib, $sslLib, (Join-Path $prefix 'lib')) | + Where-Object { Test-Path $_ } | + Select-Object -Unique + $dyld = ($libDirs -join ':') + Add-Content -Path $env:GITHUB_ENV -Value "DYLD_LIBRARY_PATH=$dyld" + Add-Content -Path $env:GITHUB_ENV -Value "DYLD_FALLBACK_LIBRARY_PATH=$dyld" + $env:DYLD_LIBRARY_PATH = $dyld + $env:DYLD_FALLBACK_LIBRARY_PATH = $dyld + Write-Host "DYLD_LIBRARY_PATH=$dyld" + + function Test-QuicSupported { + $out = & pwsh -NoProfile -Command { + if (-not [System.Net.Quic.QuicListener]::IsSupported) { '0' } else { '1' } + } + return ($out.Trim() -eq '1') + } + + if (-not (Test-QuicSupported)) { + Write-Host 'QuicListener.IsSupported still false after brew; trying Microsoft libmsquic drop…' + $arch = (& uname -m).Trim() + $rid = if ($arch -eq 'arm64') { 'osx-arm64' } else { 'osx-x64' } + $dest = Join-Path $env:RUNNER_TEMP 'msquic-osx' + New-Item -ItemType Directory -Path $dest -Force | Out-Null + $tag = 'v2.4.7' + $url = "https://github.com/microsoft/msquic/releases/download/$tag/msquic_${rid}_$tag.zip" + $zip = Join-Path $env:RUNNER_TEMP 'msquic-osx.zip' + try { + & curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 $url -o $zip + if ($LASTEXITCODE -ne 0) { throw "curl exit $LASTEXITCODE" } + Expand-Archive -Path $zip -DestinationPath $dest -Force + } catch { + Write-Warning "Microsoft release download failed ($url): $_" + } + $found = Get-ChildItem -Path $dest -Recurse -Filter 'libmsquic*.dylib' -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($found) { + $extra = $found.Directory.FullName + # ${extra} — bare $extra: is parsed as a PowerShell drive-qualified variable. + $dyld2 = "${extra}:${dyld}" + Add-Content -Path $env:GITHUB_ENV -Value "DYLD_LIBRARY_PATH=$dyld2" + Add-Content -Path $env:GITHUB_ENV -Value "DYLD_FALLBACK_LIBRARY_PATH=$dyld2" + $env:DYLD_LIBRARY_PATH = $dyld2 + $env:DYLD_FALLBACK_LIBRARY_PATH = $dyld2 + Write-Host "Added Microsoft dylib dir: $extra" + } + } + + if (-not (Test-QuicSupported)) { + throw 'QuicListener.IsSupported is false after macOS MsQuic install (brew + optional Microsoft drop)' + } + Write-Host 'QuicListener.IsSupported=True' - name: Linux UI fonts + Playwright OS deps if: runner.os == 'Linux' run: | @@ -194,6 +278,9 @@ jobs: - name: Inspector Headless + Visual + Plus Playwright run: | dotnet test tests/Titanium.E2E.Tests/Titanium.E2E.Tests.csproj --configuration Release --no-build --no-restore --filter "TestCategory=E2E-UI-Headless|TestCategory=E2E-UI-Visual|TestCategory=E2E-UI-Plus-Dashboard" + - name: Inspector retention stress (spill + H3) + run: | + dotnet test tests/Titanium.Inspector.Tests/Titanium.Inspector.Tests.csproj --configuration Release --no-restore --filter "TestCategory=Inspector-Stress" - name: OS proxy-backend filters run: | dotnet test tests/Titanium.Web.Proxy.UnitTests/Titanium.Web.Proxy.UnitTests.csproj --configuration Release --no-build --no-restore --filter "FullyQualifiedName~UnixProxyBypassMapperTests|FullyQualifiedName~MacOsSystemProxyBackendTests|FullyQualifiedName~LinuxSystemProxyBackendTests|FullyQualifiedName~ElevationPromptCancelTests|FullyQualifiedName~SystemProxyBackendFactoryPlatformTests" @@ -220,7 +307,9 @@ jobs: **/playwright-report/** if-no-files-found: ignore - # Tiered RPS gate for beta/stable publish (editions). Spot runs on PRs via rps-saturation.yml. + # Tiered RPS gates for beta/stable publish (parallel — wall clock ~max of the two). + # Editions: CLI/Plus tax vs Core. Peer: Core reverse vs YARP (+ MITM÷Reverse) so a + # uniform Core slowdown cannot hide behind green edition ratios. rps-publish-gate: if: github.event_name == 'push' && (github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/stable') runs-on: ubuntu-latest @@ -251,9 +340,42 @@ jobs: if (-not $csv) { throw 'No CSV found for edition gate validation' } pwsh tools/RpsLoadProbe/validate-edition-gates.ps1 -CsvPath $csv.FullName + # Parallel with rps-publish-gate: Core vs YARP on the release SHA (c=64 spot). + # Same validator as PR compare-spot / run-spot-matrix.ps1; does not extend wall clock + # past editions (~60m). + rps-peer-gate: + if: github.event_name == 'push' && (github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/stable') + runs-on: ubuntu-latest + timeout-minutes: 45 + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: | + 10.0.x + - name: Install libmsquic (HTTP/3) + run: | + set -euo pipefail + . /etc/os-release + curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \ + "https://packages.microsoft.com/config/${ID}/${VERSION_ID}/packages-microsoft-prod.deb" \ + -o packages-microsoft-prod.deb + sudo dpkg -i packages-microsoft-prod.deb + rm -f packages-microsoft-prod.deb + sudo apt-get update + sudo apt-get install -y libmsquic + pwsh -NoProfile -Command 'if (-not [System.Net.Quic.QuicListener]::IsSupported) { throw "QuicListener.IsSupported is false after libmsquic install" }; Write-Host "QuicListener.IsSupported=$([System.Net.Quic.QuicListener]::IsSupported)"' + - name: compare-spot (Core÷YARP + MITM÷Reverse) + shell: pwsh + run: | + pwsh tools/RpsLoadProbe/run-spot-matrix.ps1 + publish: if: github.event_name == 'push' && (github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/stable') - needs: [build, ui-portable, rps-publish-gate] + needs: [build, ui-portable, rps-publish-gate, rps-peer-gate] runs-on: windows-latest environment: nuget-publish permissions: @@ -306,7 +428,7 @@ jobs: # version tag and dispatch release.yml (GITHUB_TOKEN tag pushes do not re-trigger workflows). cut-product-tag: if: github.event_name == 'push' && (github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/stable') - needs: [build, ui-portable, rps-publish-gate] + needs: [build, ui-portable, rps-publish-gate, rps-peer-gate] runs-on: ubuntu-latest permissions: contents: write diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7248d2238..e710d5800 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -461,10 +461,7 @@ jobs: set -euo pipefail # GITHUB_TOKEN `gh release create` does not fire `release: published` for # other workflows. Rebuild Pages so download.data.ts sees new zip/MSI assets. - REF=develop - case "$RELEASE_CHANNEL" in - beta) REF=beta ;; - stable) REF=stable ;; - esac - echo "Dispatching deploy-website.yml --ref $REF (channel=$RELEASE_CHANNEL)" - gh workflow run deploy-website.yml --ref "$REF" + # github-pages environment only allows deploy from develop (beta/stable are + # blocked by environment protection rules). + echo "Dispatching deploy-website.yml --ref develop (channel=$RELEASE_CHANNEL)" + gh workflow run deploy-website.yml --ref develop diff --git a/.github/workflows/rps-saturation.yml b/.github/workflows/rps-saturation.yml index e24a885db..d3f9e3b2a 100644 --- a/.github/workflows/rps-saturation.yml +++ b/.github/workflows/rps-saturation.yml @@ -5,8 +5,10 @@ # Prefer --repeats >= 3 for publishable numbers (runner noise). Linux nginx # is the authoritative nginx baseline; nginx/Windows is same-OS only. # -# Both matrix legs use the standard public-repo runner class (4 vCPU / 16 GiB). -# Do not mix larger or slim runners unless both OS get the same size. +# Matrix uses the 4-core public-repo runner class: ubuntu-latest / windows-latest +# (4 vCPU / 16 GiB) and macos-15-intel (4-core / 14 GiB). Do not use macos-latest +# (3-core M1 / 7 GiB) for publishable numbers — wrong size and architecture. +# Do not mix larger or slim runners unless every OS gets the same size class. name: RPS saturation @@ -30,6 +32,7 @@ on: - compare-mitm - compare-matrix - compare-product + - compare-product-smoke - compare-editions - compare-cross-version - compare-ceiling @@ -118,6 +121,16 @@ on: description: 'Full arm sequence repeats (median peaks)' required: true default: '3' + runner_os: + description: 'OS matrix filter (all = Win+Linux+Mac Intel)' + required: true + default: all + type: choice + options: + - all + - ubuntu-latest + - windows-latest + - macos-15-intel permissions: contents: read @@ -136,12 +149,14 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest] + # workflow_dispatch runner_os filters to one OS; otherwise Win+Linux+Mac Intel. + os: ${{ fromJSON(inputs.runner_os == 'macos-15-intel' && '["macos-15-intel"]' || inputs.runner_os == 'ubuntu-latest' && '["ubuntu-latest"]' || inputs.runner_os == 'windows-latest' && '["windows-latest"]' || '["ubuntu-latest","windows-latest","macos-15-intel"]') }} runs-on: ${{ matrix.os }} - # Intentionally no jobs.*.container — saturation RPS on a container network measures the wrong thing. + # Intentionally no jobs.*.container — saturation RPS on a container network measures the wrong thing. # compare-product with MITM Lite+Full can exceed 3.5h per OS on hosted runners. # compare-editions (expanded Plus/CLI stress arms) needs ~60m; other modes keep the long ceiling. - timeout-minutes: ${{ github.event_name == 'pull_request' && 45 || (github.event_name == 'push' && 90 || (inputs.mode == 'compare-editions' && 90 || 420)) }} + # compare-product-smoke is a short gate-arm subset (Mac quick check). + timeout-minutes: ${{ github.event_name == 'pull_request' && 45 || (github.event_name == 'push' && 90 || (inputs.mode == 'compare-editions' && 90 || (inputs.mode == 'compare-product-smoke' && 60 || 420))) }} steps: - uses: actions/checkout@v6 @@ -172,6 +187,31 @@ jobs: Write-Host "RAM_GiB=$([math]::Round($cs.TotalPhysicalMemory / 1GB, 1))" Write-Host "CPU=$($cpu.Name)" + - name: Log runner shape (macOS) + if: runner.os == 'macOS' + run: | + set -euo pipefail + echo "os=$(uname -s) $(uname -r) $(uname -m)" + echo "sw_vers:" + sw_vers + ncpu=$(sysctl -n hw.ncpu) + # hw.memsize is bytes; assert >= 4 CPUs and >= 12 GiB (macos-15-intel is 4 / 14). + mem_bytes=$(sysctl -n hw.memsize) + mem_gib=$(awk -v b="$mem_bytes" 'BEGIN { printf "%.1f", b / (1024*1024*1024) }') + echo "ncpu=$ncpu" + echo "RAM_GiB=$mem_gib" + sysctl -n machdep.cpu.brand_string || true + if [ "$ncpu" -lt 4 ]; then + echo "Expected >=4 CPUs on macos-15-intel; got $ncpu (do not use macos-latest for publishable RPS)." >&2 + exit 1 + fi + # 12 GiB floor leaves headroom under the documented 14 GiB Intel runner. + min_bytes=$((12 * 1024 * 1024 * 1024)) + if [ "$mem_bytes" -lt "$min_bytes" ]; then + echo "Expected >=12 GiB RAM on macos-15-intel; got ${mem_gib} GiB." >&2 + exit 1 + fi + # Ubuntu 24.04 distro nginx is 1.24 without HTTP/3. Official nginx.org mainline # packages are built with --with-http_v3_module so the H3 terminate arm can run. - name: Install nginx (HTTP/3-capable) @@ -220,6 +260,44 @@ jobs: Add-Content -Path $env:GITHUB_PATH -Value $nginxDir & (Join-Path $nginxDir 'nginx.exe') -v + # Homebrew nginx bottles include --with-http_v3_module (OpenSSL 3). Fail hard if missing. + # macos-15-intel: Homebrew no longer mass-bottles Intel; avoid brew update/upgrade of openssl + # (already-installed bottle works; upgrading hits "no bottle available"). + - name: Install nginx (HTTP/3-capable, macOS) + if: runner.os == 'macOS' + env: + HOMEBREW_NO_AUTO_UPDATE: '1' + HOMEBREW_NO_INSTALL_UPGRADE: '1' + run: | + set -euo pipefail + # Use preinstalled openssl@3 / pcre2 when present; do not upgrade (Intel bottles missing). + brew list --versions openssl@3 || brew install openssl@3 + brew list --versions pcre2 || brew install pcre2 + if ! brew list --versions nginx >/dev/null 2>&1; then + if ! brew install nginx; then + echo "brew install nginx failed (likely missing Intel bottle); building from source…" >&2 + brew install --build-from-source nginx + fi + fi + # Prefer brew nginx; if a bottle somehow lacks http_v3, rebuild from source with the flag. + if ! nginx -V 2>&1 | grep -q http_v3_module; then + echo "brew nginx lacks http_v3_module; rebuilding from source with --with-http_v3_module" >&2 + brew reinstall --build-from-source nginx + fi + brew services stop nginx 2>/dev/null || true + # Kill any leftover master so our temp-prefix nginx owns the ports we pick. + pkill -x nginx 2>/dev/null || true + NGINX_BIN="$(brew --prefix nginx)/bin" + echo "$NGINX_BIN" >> "$GITHUB_PATH" + export PATH="$NGINX_BIN:$PATH" + nginx -v + if ! nginx -V 2>&1 | grep -q http_v3_module; then + echo "nginx was installed but lacks --with-http_v3_module; failing job." >&2 + nginx -V + exit 1 + fi + nginx -V 2>&1 | tr ' ' '\n' | grep http_v3 || true + # .NET System.Net.Quic on Linux requires native libmsquic (shipped in-box on Windows). - name: Install libmsquic (HTTP/3) if: runner.os == 'Linux' @@ -244,6 +322,74 @@ jobs: } Write-Host "QuicListener.IsSupported=$([System.Net.Quic.QuicListener]::IsSupported)" + # macOS: System.Net.Quic needs Homebrew libmsquic (and OpenSSL) on DYLD_* path. + # Prefer brew; fall back to Microsoft osx-x64 release dylibs if brew is insufficient. + - name: Install MsQuic (HTTP/3, macOS Intel) + if: runner.os == 'macOS' + shell: pwsh + env: + HOMEBREW_NO_AUTO_UPDATE: '1' + HOMEBREW_NO_INSTALL_UPGRADE: '1' + run: | + $ErrorActionPreference = 'Stop' + # Do not upgrade openssl@3 on Intel — new formula often has no bottle. + brew install openssl@3 libmsquic + $prefix = (& brew --prefix).Trim() + $msquicLib = Join-Path $prefix 'opt/libmsquic/lib' + $sslLib = Join-Path $prefix 'opt/openssl@3/lib' + $libDirs = @($msquicLib, $sslLib, (Join-Path $prefix 'lib')) | + Where-Object { Test-Path $_ } | + Select-Object -Unique + $dyld = ($libDirs -join ':') + # Persist for later steps (ramp / QuicListener). + Add-Content -Path $env:GITHUB_ENV -Value "DYLD_LIBRARY_PATH=$dyld" + Add-Content -Path $env:GITHUB_ENV -Value "DYLD_FALLBACK_LIBRARY_PATH=$dyld" + $env:DYLD_LIBRARY_PATH = $dyld + $env:DYLD_FALLBACK_LIBRARY_PATH = $dyld + Write-Host "DYLD_LIBRARY_PATH=$dyld" + + function Test-QuicSupported { + # Child process so dyld sees DYLD_* (in-process env changes are too late for loaded runtime). + $out = & pwsh -NoProfile -Command { + if (-not [System.Net.Quic.QuicListener]::IsSupported) { '0' } else { '1' } + } + return ($out.Trim() -eq '1') + } + + if (-not (Test-QuicSupported)) { + Write-Host 'QuicListener.IsSupported still false after brew; trying Microsoft osx-x64 libmsquic drop…' + $dest = Join-Path $env:RUNNER_TEMP 'msquic-osx' + New-Item -ItemType Directory -Path $dest -Force | Out-Null + # Pin a known MsQuic release asset layout; adjust tag if the download 404s. + $tag = 'v2.4.7' + $url = "https://github.com/microsoft/msquic/releases/download/$tag/msquic_osx-x64_$tag.zip" + $zip = Join-Path $env:RUNNER_TEMP 'msquic-osx.zip' + try { + & curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 $url -o $zip + if ($LASTEXITCODE -ne 0) { throw "curl exit $LASTEXITCODE" } + Expand-Archive -Path $zip -DestinationPath $dest -Force + } catch { + Write-Warning "Microsoft release download failed ($url): $_" + } + $found = Get-ChildItem -Path $dest -Recurse -Filter 'libmsquic*.dylib' -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($found) { + $extra = $found.Directory.FullName + # ${extra} — bare $extra: is parsed as a PowerShell drive-qualified variable. + $dyld2 = "${extra}:${dyld}" + Add-Content -Path $env:GITHUB_ENV -Value "DYLD_LIBRARY_PATH=$dyld2" + Add-Content -Path $env:GITHUB_ENV -Value "DYLD_FALLBACK_LIBRARY_PATH=$dyld2" + $env:DYLD_LIBRARY_PATH = $dyld2 + $env:DYLD_FALLBACK_LIBRARY_PATH = $dyld2 + Write-Host "Added Microsoft dylib dir: $extra" + } + } + + if (-not (Test-QuicSupported)) { + throw 'QuicListener.IsSupported is false after macOS MsQuic install (brew + optional Microsoft drop)' + } + Write-Host 'QuicListener.IsSupported=True' + - name: Install bombardier (optional external generator) if: runner.os == 'Linux' run: | @@ -269,8 +415,22 @@ jobs: Add-Content -Path $env:GITHUB_PATH -Value $dir & $exe --version + - name: Install bombardier (optional external generator) + if: runner.os == 'macOS' + run: | + set -euo pipefail + # macos-15-intel is x86_64 — use darwin amd64 binary. + dir="${RUNNER_TEMP}/bombardier" + mkdir -p "$dir" + curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \ + "https://github.com/codesenberg/bombardier/releases/download/v1.2.6/bombardier-darwin-amd64" \ + -o "$dir/bombardier" + chmod +x "$dir/bombardier" + echo "$dir" >> "$GITHUB_PATH" + "$dir/bombardier" --version || true + - name: Raise open-file limit - if: runner.os == 'Linux' + if: runner.os == 'Linux' || runner.os == 'macOS' run: | ulimit -n 65535 || true ulimit -n @@ -278,7 +438,7 @@ jobs: # Pass workflow_dispatch inputs via env (not ${{ }} in the script) to avoid # githubactions:S7630 script-injection findings on user-controlled values. - name: Run saturation ramp - timeout-minutes: ${{ github.event_name == 'pull_request' && 40 || (github.event_name == 'push' && 60 || (inputs.mode == 'compare-editions' && 60 || 400)) }} + timeout-minutes: ${{ github.event_name == 'pull_request' && 40 || (github.event_name == 'push' && 60 || (inputs.mode == 'compare-editions' && 60 || (inputs.mode == 'compare-product-smoke' && 50 || 400))) }} shell: pwsh env: RPS_MODE: ${{ env.RPS_MODE }} @@ -306,13 +466,23 @@ jobs: pwsh tools/RpsLoadProbe/validate-edition-gates.ps1 -CsvPath $csv.FullName - name: Validate compare-product gates - if: env.RPS_MODE == 'compare-product' + if: env.RPS_MODE == 'compare-product' || env.RPS_MODE == 'compare-product-smoke' shell: pwsh run: | $csv = Get-ChildItem tools/RpsLoadProbe/results -Filter 'rps-ramp-*.csv' | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if (-not $csv) { throw 'No CSV found for compare-product gate validation' } - pwsh tools/RpsLoadProbe/validate-compare-product-gates.ps1 -CsvPath $csv.FullName + # macos-15-intel first-baseline floors (PERF-GATES.md) — Win/Linux keep defaults. + $macFloors = @() + if ('${{ matrix.os }}' -eq 'macos-15-intel') { + $macFloors = @( + '-MitmHttp3TlsFullGate', '0.65', + '-MitmHttp1PlainFullGate', '0.55', + '-ReverseYarpHttp3ToHttp1Gate', '0.55', + '-ReverseYarpHttp3Gate', '0.74' + ) + } + pwsh tools/RpsLoadProbe/validate-compare-product-gates.ps1 -CsvPath $csv.FullName @macFloors - name: Validate cross-version gates if: env.RPS_MODE == 'compare-cross-version' diff --git a/README.md b/README.md index 5a5bbef25..3cb442dc2 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ A lightweight, high-performance HTTP(S) proxy — reverse / edge CLI, desktop In | **Titanium.Cli** (`titanium` / `twp`) | Standalone reverse / edge proxy for any stack: `run`, `test`, `version`, `update` | [Download (Windows, Linux & Mac)](https://titaniumproxy.com/download#cli) | | **Titanium Inspector** | Desktop MITM debugger (session grid, inspectors, AutoResponder, breakpoints, HAR) | [Download (Windows, Linux & Mac)](https://titaniumproxy.com/download#inspector) | | **Titanium.Plus** | Optional advanced features: control plane, ops, observability, and dashboard | After installing CLI, run `titanium update --plus` | -| **Titanium.Web.Proxy** | Core library. Embed a MITM and/or reverse proxy in a .NET app | [NuGet](https://www.nuget.org/packages/Titanium.Web.Proxy/7.0.3-beta) (`dotnet add package Titanium.Web.Proxy --prerelease`) | +| **Titanium.Web.Proxy** | Core library. Embed a MITM and/or reverse proxy in a .NET app | [NuGet](https://www.nuget.org/packages/Titanium.Web.Proxy/7.0.4-beta) (`dotnet add package Titanium.Web.Proxy --prerelease`) | CLI and Plus target reverse-proxy / edge workloads (routing, load balancing, health, discovery) on Windows, Linux, and macOS. Inspector is the MITM debugging product. The Core library is the embed path for .NET. Requires .NET 10 or later. @@ -67,7 +67,7 @@ On Windows, **winget is stable-only**: winget install justcoding121.TitaniumCli ``` -For **beta**, download self-contained zips from [Download](https://titaniumproxy.com/download) / [GitHub Releases](https://github.com/justcoding121/titanium-web-proxy/releases) when a product release includes `Titanium.Cli-*.zip` assets (e.g. `v7.0.3-beta`). Extract and run: +For **beta**, download self-contained zips from [Download](https://titaniumproxy.com/download) / [GitHub Releases](https://github.com/justcoding121/titanium-web-proxy/releases) when a product release includes `Titanium.Cli-*.zip` assets (e.g. `v7.0.4-beta`). Extract and run: ```shell titanium run -c twp.yaml @@ -82,7 +82,7 @@ Optional Plus: run `titanium update --plus` (add `--channel beta` for prerelease ### Titanium Inspector -Prefer [Download](https://titaniumproxy.com/download). On Windows, winget id `justcoding121.TitaniumInspector` is **stable-only**; MSI / portable zip for beta come from the product `v*` release (e.g. `v7.0.3-beta`). Start interception from the Capture menu, install the root CA, then toggle system proxy. +Prefer [Download](https://titaniumproxy.com/download). On Windows, winget id `justcoding121.TitaniumInspector` is **stable-only**; MSI / portable zip for beta come from the product `v*` release (e.g. `v7.0.4-beta`). Start interception from the Capture menu, install the root CA, then toggle system proxy. ## Quick start diff --git a/benchmarks/Titanium.Web.Proxy.Benchmarks/Http1ProxyThroughputBenchmarks.cs b/benchmarks/Titanium.Web.Proxy.Benchmarks/Http1ProxyThroughputBenchmarks.cs index 5b5b56f1c..dfc0e70d5 100644 --- a/benchmarks/Titanium.Web.Proxy.Benchmarks/Http1ProxyThroughputBenchmarks.cs +++ b/benchmarks/Titanium.Web.Proxy.Benchmarks/Http1ProxyThroughputBenchmarks.cs @@ -38,10 +38,7 @@ public class Http1ProxyThroughputBenchmarks [GlobalSetup] public void Setup() { - originListener = new HttpListener(); - var originPort = GetFreeTcpPort(); - originListener.Prefixes.Add($"http://127.0.0.1:{originPort}/"); - originListener.Start(); + (originListener, var originPort) = BindHttpListenerOrRetry(port => $"http://127.0.0.1:{port}/"); _ = Task.Run(RunOriginLoop); proxyServer = new ProxyServer(false, false, false); @@ -106,6 +103,38 @@ private async Task RunOriginLoop() } } + private static (HttpListener Listener, int Port) BindHttpListenerOrRetry( + Func prefixFactory, int maxAttempts = 8) + { + Exception? last = null; + for (var i = 0; i < maxAttempts; i++) + { + var port = GetFreeTcpPort(); + var listener = new HttpListener(); + listener.Prefixes.Add(prefixFactory(port)); + try + { + listener.Start(); + return (listener, port); + } + catch (Exception ex) when (ex is HttpListenerException or System.Net.Sockets.SocketException) + { + last = ex; + try + { + listener.Close(); + } + catch + { + // ignore + } + } + } + + throw new InvalidOperationException( + $"Failed to bind HttpListener after {maxAttempts} attempts.", last); + } + private static int GetFreeTcpPort() { var listener = new System.Net.Sockets.TcpListener(IPAddress.Loopback, 0); diff --git a/docs/api/Titanium.Web.Proxy.ProxyServer.html b/docs/api/Titanium.Web.Proxy.ProxyServer.html index e16995d7c..b28743188 100644 --- a/docs/api/Titanium.Web.Proxy.ProxyServer.html +++ b/docs/api/Titanium.Web.Proxy.ProxyServer.html @@ -293,7 +293,7 @@
Property Value
Edit this page - View Source + View Source

BlockPrivateNetworkDestinations

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

BufferPool

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

CertificateManager

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

CustomUpStreamProxyFailureFunc

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

EnableHttpInterception

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

EnableRequestTimingCapture

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

GetCustomUpStreamProxyFunc

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

Logger

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

Logging

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

PolicyModes

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

Profile

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

ProxyAuthenticationSchemes

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

ProxyBasicAuthenticateFunc

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

ProxyEndPoints

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

ProxySchemeAuthenticateFunc

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

ReverseProxy

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

ShouldInterceptHttp

@@ -2448,12 +2448,21 @@
Property Value
Edit this page - View Source + View Source

SupportedServerSslProtocols

-

List of supported Server Ssl versions. -Using SslProtocol.None means to require the same SSL protocol as the proxy client.

+

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
@@ -2554,7 +2563,7 @@
Property Value
Edit this page - View Source + View Source

ThreadPoolWorkerThread

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

UpStreamEndPoint

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

UpStreamEndPointIPv4

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

UpStreamEndPointIPv6

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

UpStreamHttpProxy

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

UpStreamHttpsProxy

@@ -2849,7 +2858,7 @@

Methods Edit this page - View Source + View Source

AddEndPoint(ProxyEndPoint)

@@ -2883,7 +2892,7 @@
Parameters
Edit this page - View Source + View Source

ApplyLoggingConfiguration()

@@ -2904,7 +2913,7 @@
Declaration
Edit this page - View Source + View Source

DisableAllSystemProxies()

@@ -2920,7 +2929,7 @@
Declaration
Edit this page - View Source + View Source

DisableSystemHttpProxy()

@@ -2936,7 +2945,7 @@
Declaration
Edit this page - View Source + View Source

DisableSystemHttpsProxy()

@@ -2952,7 +2961,7 @@
Declaration
Edit this page - View Source + View Source

DisableSystemProxy(ProxyProtocolType)

@@ -2985,7 +2994,7 @@
Parameters
Edit this page - View Source + View Source

Dispose()

@@ -3001,7 +3010,7 @@
Declaration
Edit this page - View Source + View Source

Dispose(bool)

@@ -3034,7 +3043,7 @@
Parameters
Edit this page - View Source + View Source

RemoveEndPoint(ProxyEndPoint)

@@ -3069,7 +3078,7 @@
Parameters
Edit this page - View Source + View Source

RestoreOriginalProxySettings()

@@ -3085,7 +3094,7 @@
Declaration
Edit this page - View Source + View Source

SetAsSystemHttpProxy(ExplicitProxyEndPoint)

@@ -3119,7 +3128,7 @@
Parameters
Edit this page - View Source + View Source

SetAsSystemHttpProxy(ExplicitProxyEndPoint, SystemProxySettings)

@@ -3159,7 +3168,7 @@
Parameters
Edit this page - View Source + View Source

SetAsSystemHttpsProxy(ExplicitProxyEndPoint)

@@ -3193,7 +3202,7 @@
Parameters
Edit this page - View Source + View Source

SetAsSystemHttpsProxy(ExplicitProxyEndPoint, SystemProxySettings)

@@ -3233,7 +3242,7 @@
Parameters
Edit this page - View Source + View Source

SetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType)

@@ -3273,7 +3282,7 @@
Parameters
Edit this page - View Source + View Source

SetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType, SystemProxySettings?)

@@ -3371,7 +3380,7 @@
Returns
Edit this page - View Source + View Source

Start(bool)

@@ -3414,7 +3423,7 @@
Parameters
Edit this page - View Source + View Source

Stop()

@@ -3433,7 +3442,7 @@
Declaration
Edit this page - View Source + View Source

StopAsync(TimeSpan?)

@@ -3520,7 +3529,7 @@

Events Edit this page - View Source + View Source

AfterResponse

Intercept after response event from server.

@@ -3550,7 +3559,7 @@
Event Type
Edit this page - View Source + View Source

BeforeRequest

Intercept request event to server.

@@ -3580,7 +3589,7 @@
Event Type
Edit this page - View Source + View Source

BeforeResponse

Intercept response event from server.

@@ -3610,7 +3619,7 @@
Event Type
Edit this page - View Source + View Source

BeforeUpStreamConnectRequest

Intercept connect request sent to upstream proxy.

@@ -3640,7 +3649,7 @@
Event Type
Edit this page - View Source + View Source

ClientCertificateSelectionCallback

Event to override client certificate selection during mutual SSL authentication.

@@ -3670,7 +3679,7 @@
Event Type
Edit this page - View Source + View Source

ClientConnectionCountChanged

Event occurs when client connection count changed.

@@ -3700,7 +3709,7 @@
Event Type
Edit this page - View Source + View Source

Http3ClientConnectionCountChanged

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

@@ -3730,7 +3739,7 @@
Event Type
Edit this page - View Source + View Source

Http3ServerConnectionCountChanged

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

@@ -3760,7 +3769,7 @@
Event Type
Edit this page - View Source + View Source

OnClientConnectionCreate

Customize TcpClient used for client connection upon create.

@@ -3790,7 +3799,7 @@
Event Type
Edit this page - View Source + View Source

OnRequestBodyWrite

Intercept request body send event to server. @@ -3822,7 +3831,7 @@

Event Type
Edit this page - View Source + View Source

OnResponseBodyWrite

Intercept response body send event to client. @@ -3854,7 +3863,7 @@

Event Type
Edit this page - View Source + View Source

OnServerConnectionCreate

Customize TcpClient used for server connection upon create.

@@ -3884,7 +3893,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.

@@ -3914,7 +3923,7 @@
Event Type
Edit this page - View Source + View Source

ServerConnectionCountChanged

Event occurs when server connection count changed.

diff --git a/docs/index.json b/docs/index.json index 145288fb5..2b1937ad7 100644 --- a/docs/index.json +++ b/docs/index.json @@ -497,7 +497,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. 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 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 List of supported Server Ssl versions. Using SslProtocol.None means to require the same SSL protocol as the proxy client. 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 \"titanium-web-proxy\". 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 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 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. 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 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 \"titanium-web-proxy\". 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 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 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" }, "api/Titanium.Web.Proxy.Routing.ReverseProxyFastPath.html": { "href": "api/Titanium.Web.Proxy.Routing.ReverseProxyFastPath.html", diff --git a/examples/Titanium.Web.Proxy.Examples.Basic/ProxyTestController.cs b/examples/Titanium.Web.Proxy.Examples.Basic/ProxyTestController.cs index aa8391499..adb8e6bfa 100644 --- a/examples/Titanium.Web.Proxy.Examples.Basic/ProxyTestController.cs +++ b/examples/Titanium.Web.Proxy.Examples.Basic/ProxyTestController.cs @@ -182,7 +182,20 @@ public void StartProxy() } #pragma warning restore TWP001 - proxyServer.Start(); + try + { + proxyServer.Start(); + } + catch (Exception ex) when (IsAddressAlreadyInUse(ex)) + { + Logger.LogWarning(ex, "Port 8000 in use; falling back to ephemeral port 0"); + proxyServer.RemoveEndPoint(explicitEndPoint); + explicitEndPoint = new ExplicitProxyEndPoint(IPAddress.Any, 0); + explicitEndPoint.BeforeTunnelConnectRequest += OnBeforeTunnelConnectRequest; + explicitEndPoint.BeforeTunnelConnectResponse += OnBeforeTunnelConnectResponse; + proxyServer.AddEndPoint(explicitEndPoint); + proxyServer.Start(); + } foreach (var endPoint in proxyServer.ProxyEndPoints) Logger.LogWarning("Listening on '{EndPointType}' endpoint at Ip {IpAddress} and port: {Port}", @@ -221,6 +234,19 @@ private static bool ReadEnvBool(string name, bool defaultValue) }; } + private static bool IsAddressAlreadyInUse(Exception ex) + { + for (var cur = ex; cur != null; cur = cur.InnerException) + { + if (cur is SocketException se && + (se.SocketErrorCode == SocketError.AddressAlreadyInUse + || se.NativeErrorCode is 10048 or 98)) + return true; + } + + return false; + } + public void Stop() { Logger.LogWarning("Stopping proxy..."); diff --git a/examples/Titanium.Web.Proxy.Examples.WindowsService/ProxyWorker.cs b/examples/Titanium.Web.Proxy.Examples.WindowsService/ProxyWorker.cs index 3bb24fb41..4216ff453 100644 --- a/examples/Titanium.Web.Proxy.Examples.WindowsService/ProxyWorker.cs +++ b/examples/Titanium.Web.Proxy.Examples.WindowsService/ProxyWorker.cs @@ -82,9 +82,10 @@ public override Task StartAsync(CancellationToken cancellationToken) explicitEndPointV4.BeforeTunnelConnectRequest += OnBeforeTunnelConnectRequest; proxyServer.AddEndPoint(explicitEndPointV4); + ExplicitProxyEndPoint? explicitEndPointV6 = null; if (settings.EnableIpV6) { - var explicitEndPointV6 = + explicitEndPointV6 = new ExplicitProxyEndPoint(IPAddress.IPv6Any, settings.ListeningPort, settings.DecryptSsl); explicitEndPointV6.BeforeTunnelConnectRequest += OnBeforeTunnelConnectRequest; proxyServer.AddEndPoint(explicitEndPointV6); @@ -130,7 +131,33 @@ public override Task StartAsync(CancellationToken cancellationToken) if (settings.LogRequests) proxyServer.BeforeResponse += OnBeforeResponse; - proxyServer.Start(); + try + { + proxyServer.Start(); + } + catch (Exception ex) when (IsAddressAlreadyInUse(ex)) + { + logger.LogWarning(ex, + "ListeningPort {ListeningPort} unavailable; falling back to ephemeral port 0", + settings.ListeningPort); + proxyServer.RemoveEndPoint(explicitEndPointV4); + if (explicitEndPointV6 != null) + proxyServer.RemoveEndPoint(explicitEndPointV6); + + explicitEndPointV4 = new ExplicitProxyEndPoint(IPAddress.Any, 0, settings.DecryptSsl); + explicitEndPointV4.BeforeTunnelConnectRequest += OnBeforeTunnelConnectRequest; + proxyServer.AddEndPoint(explicitEndPointV4); + + if (settings.EnableIpV6) + { + explicitEndPointV6 = + new ExplicitProxyEndPoint(IPAddress.IPv6Any, 0, settings.DecryptSsl); + explicitEndPointV6.BeforeTunnelConnectRequest += OnBeforeTunnelConnectRequest; + proxyServer.AddEndPoint(explicitEndPointV6); + } + + proxyServer.Start(); + } if (settings.SetAsSystemProxy) { @@ -140,7 +167,7 @@ public override Task StartAsync(CancellationToken cancellationToken) KnownMitmExclusions.CreateSystemProxySettings()); logger.LogInformation( "Registered as Windows system proxy on port {ListeningPort} with identity host bypass (cleared on stop)", - settings.ListeningPort); + explicitEndPointV4.Port); } catch (NotSupportedException ex) { @@ -148,11 +175,24 @@ public override Task StartAsync(CancellationToken cancellationToken) } } - logger.LogInformation("Service listening on port {ListeningPort}", settings.ListeningPort); + logger.LogInformation("Service listening on port {ListeningPort}", explicitEndPointV4.Port); return base.StartAsync(cancellationToken); } + private static bool IsAddressAlreadyInUse(Exception ex) + { + for (var cur = ex; cur != null; cur = cur.InnerException) + { + if (cur is System.Net.Sockets.SocketException se && + (se.SocketErrorCode == System.Net.Sockets.SocketError.AddressAlreadyInUse + || se.NativeErrorCode is 10048 or 98)) + return true; + } + + return false; + } + private static Task OnBeforeTunnelConnectRequest(object sender, TunnelConnectSessionEventArgs e) { if (KnownMitmExclusions.ShouldDisableSslDecrypt(e.HttpClient.Request.RequestUri.Host)) diff --git a/examples/Titanium.Web.Proxy.Examples.Wpf/MainWindow.xaml.cs b/examples/Titanium.Web.Proxy.Examples.Wpf/MainWindow.xaml.cs index ab44d01f7..99979fd24 100644 --- a/examples/Titanium.Web.Proxy.Examples.Wpf/MainWindow.xaml.cs +++ b/examples/Titanium.Web.Proxy.Examples.Wpf/MainWindow.xaml.cs @@ -142,7 +142,24 @@ public MainWindow() { Dispatcher.BeginInvoke(() => { Http3ServerConnectionCount = proxyServer.Http3ServerConnectionCount; }); }; - proxyServer.Start(); + + try + { + proxyServer.Start(); + } + catch (Exception ex) when (IsAddressAlreadyInUse(ex)) + { + System.Diagnostics.Debug.WriteLine("Port 8000 in use; falling back to ephemeral port 0"); + proxyServer.RemoveEndPoint(explicitEndPoint); + explicitEndPoint = new ExplicitProxyEndPoint(IPAddress.Any, 0); + explicitEndPoint.BeforeTunnelConnectRequest += ProxyServer_BeforeTunnelConnectRequest; + explicitEndPoint.BeforeTunnelConnectResponse += ProxyServer_BeforeTunnelConnectResponse; + proxyServer.AddEndPoint(explicitEndPoint); + proxyServer.Start(); + } + + System.Diagnostics.Debug.WriteLine( + $"Listening on ExplicitProxyEndPoint at {explicitEndPoint.IpAddress}:{explicitEndPoint.Port}"); // Screenshot automation (TWP_CAPTURE_PATH) skips system-proxy registration so CI/desktop // capture runs do not alter the machine's proxy settings. @@ -173,6 +190,19 @@ public MainWindow() } } + private static bool IsAddressAlreadyInUse(Exception ex) + { + for (var cur = ex; cur != null; cur = cur.InnerException) + { + if (cur is System.Net.Sockets.SocketException se && + (se.SocketErrorCode == System.Net.Sockets.SocketError.AddressAlreadyInUse + || se.NativeErrorCode is 10048 or 98)) + return true; + } + + return false; + } + /// /// Renders this window to JPEG via (works when desktop /// bit-blit cannot see the WPF surface) then shuts down. Used for wiki screenshot refresh. diff --git a/src/Titanium.Cli/Config/RunCommand.cs b/src/Titanium.Cli/Config/RunCommand.cs index e9d993adc..abf482de7 100644 --- a/src/Titanium.Cli/Config/RunCommand.cs +++ b/src/Titanium.Cli/Config/RunCommand.cs @@ -478,6 +478,11 @@ internal static Dictionary BuildPlusOptions(PlusConfig plus) { options["controlPlane.host"] = plus.ControlPlane.Host; options["controlPlane.port"] = plus.ControlPlane.Port.ToString(); + if (plus.ControlPlane.DashboardPort is > 0 and < 65536) + { + options["controlPlane.dashboardPort"] = plus.ControlPlane.DashboardPort.Value.ToString(); + } + if (!string.IsNullOrEmpty(plus.ControlPlane.SharedSecret)) { options["controlPlane.sharedSecret"] = plus.ControlPlane.SharedSecret; diff --git a/src/Titanium.Cli/Titanium.Cli.csproj b/src/Titanium.Cli/Titanium.Cli.csproj index e7d35c46c..db66f9bcd 100644 --- a/src/Titanium.Cli/Titanium.Cli.csproj +++ b/src/Titanium.Cli/Titanium.Cli.csproj @@ -7,7 +7,7 @@ latest enable false - 7.0.3 + 7.0.4 Jehonathan Thomas Titanium Web Proxy CLI (titanium / twp). MIT diff --git a/src/Titanium.Cli/Updates/VersionAndUpdateCommands.cs b/src/Titanium.Cli/Updates/VersionAndUpdateCommands.cs index 3b6f92667..c72a8de15 100644 --- a/src/Titanium.Cli/Updates/VersionAndUpdateCommands.cs +++ b/src/Titanium.Cli/Updates/VersionAndUpdateCommands.cs @@ -320,7 +320,8 @@ public static void StartDetached( File.WriteAllText(ps1, BuildWindowsScript(pid, zipPath, installDir, relaunchPath, version, channel), Encoding.UTF8); Process.Start(new ProcessStartInfo { - FileName = "powershell.exe", + // Absolute path: Sonar S4036 (PATH lookup for powershell.exe is a vulnerability). + FileName = ResolveWindowsPowerShellPath(), Arguments = $"-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File \"{ps1}\"", UseShellExecute = true, CreateNoWindow = true, @@ -341,6 +342,14 @@ public static void StartDetached( }); } + /// Absolute Windows PowerShell path — avoids PATH-based Process.Start (Sonar S4036). + private static string ResolveWindowsPowerShellPath() => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.System), + "WindowsPowerShell", + "v1.0", + "powershell.exe"); + internal static string BuildWindowsScript( int pid, string zipPath, diff --git a/src/Titanium.Inspector/Services/InterceptionService.cs b/src/Titanium.Inspector/Services/InterceptionService.cs index ae35142bf..65396c7e4 100644 --- a/src/Titanium.Inspector/Services/InterceptionService.cs +++ b/src/Titanium.Inspector/Services/InterceptionService.cs @@ -22,7 +22,7 @@ public sealed class InterceptionService : IDisposable public const int MaxBodyBytes = 2 * 1024 * 1024; public const int MaxBodyTextChars = 256 * 1024; - private static long _nextId = 1; + private long _nextId; private readonly ConcurrentDictionary _live = new(); private readonly ISystemProxyController _systemProxy; private ProxyServer? _proxy; @@ -40,6 +40,9 @@ public InterceptionService(ISystemProxyController? systemProxy = null) public bool IsRunning => _proxy?.ProxyRunning == true; + /// OS-assigned listen port after (supports port == 0). + public int BoundPort { get; private set; } + /// When false, the listener stays up but sessions are not published to the grid. public bool Capturing { get; set; } = true; @@ -108,6 +111,8 @@ public async Task StartAsync(IPAddress address, int port, CancellationToken canc return; } + Interlocked.Exchange(ref _nextId, 0); + // Explicit trust flags: do not silently install into the user store on start. // Callers must InstallRootCertificate (or set AutoTrustRootOnStart) so UI can report success/failure. _proxy = new ProxyServer(userTrustRootCertificate: false, machineTrustRootCertificate: false); @@ -143,6 +148,7 @@ public async Task StartAsync(IPAddress address, int port, CancellationToken canc _endPoint.BeforeTunnelConnectResponse += OnBeforeTunnelConnectResponse; _proxy.AddEndPoint(_endPoint); _proxy.Start(); + BoundPort = _endPoint.Port; IsRootTrusted = UseInMemoryTrustState ? _inMemoryTrusted : IsRootPresentInStore(machineStore: false); @@ -321,6 +327,7 @@ public void Stop() _proxy.Dispose(); _proxy = null; _endPoint = null; + BoundPort = 0; _live.Clear(); IsRootTrusted = false; _systemProxyEnabled = false; @@ -663,7 +670,7 @@ private Task OnBeforeTunnelConnectResponse(object sender, TunnelConnectSessionEv return Task.CompletedTask; } - private static SessionSnapshot CreateTunnelSnapshot(TunnelConnectSessionEventArgs e) + private SessionSnapshot CreateTunnelSnapshot(TunnelConnectSessionEventArgs e) { var req = e.HttpClient.Request; var processId = 0; @@ -683,7 +690,7 @@ private static SessionSnapshot CreateTunnelSnapshot(TunnelConnectSessionEventArg return new SessionSnapshot { - Id = Interlocked.Increment(ref _nextId), + Id = NextSessionId(), Method = "CONNECT", Url = req.RequestUriString ?? req.Url ?? "", Host = TryHost(req), @@ -723,8 +730,8 @@ private async Task OnBeforeRequest(object sender, SessionEventArgs e) e.GenericResponse(rule.Body, (HttpStatusCode)rule.StatusCode, headers); } - if (Breakpoints is not null && - Breakpoints.TryEnter(CreatePreviewSnapshot(e), out var hit)) + if (Breakpoints is { Enabled: true } && + Breakpoints.TryEnter(CreatePreviewSnapshot(e, assignId: false), out var hit)) { var action = await hit.WaitAsync(); if (action == BreakpointAction.Abort) @@ -744,7 +751,7 @@ private async Task OnBeforeRequest(object sender, SessionEventArgs e) return; } - var snap = CreatePreviewSnapshot(e); + var snap = CreatePreviewSnapshot(e, assignId: true); _live[e.HttpClient] = snap; SessionCaptured?.Invoke(this, snap); } @@ -767,8 +774,8 @@ private async Task OnBeforeResponse(object sender, SessionEventArgs e) SessionScriptHost.ApplyOnResponse(ScriptOnResponse, e); if (BreakpointOnResponse && - Breakpoints is not null && - Breakpoints.TryEnter(CreatePreviewSnapshot(e), out var hit)) + Breakpoints is { Enabled: true } && + Breakpoints.TryEnter(CreatePreviewSnapshot(e, assignId: false), out var hit)) { var action = await hit.WaitAsync(); if (action == BreakpointAction.Abort) @@ -790,7 +797,7 @@ Breakpoints is not null && return; } - snap = CreatePreviewSnapshot(e); + snap = CreatePreviewSnapshot(e, assignId: true); _live[e.HttpClient] = snap; SessionCaptured?.Invoke(this, snap); } @@ -825,7 +832,7 @@ private Task OnServerCertValidation(object sender, CertificateValidationEventArg return Task.CompletedTask; } - private static SessionSnapshot CreatePreviewSnapshot(SessionEventArgs e) + private SessionSnapshot CreatePreviewSnapshot(SessionEventArgs e, bool assignId) { var req = e.HttpClient.Request; var bodyBytes = req.IsBodyRead ? TruncateBytes(req.Body) : null; @@ -847,7 +854,7 @@ private static SessionSnapshot CreatePreviewSnapshot(SessionEventArgs e) return new SessionSnapshot { - Id = Interlocked.Increment(ref _nextId), + Id = assignId ? NextSessionId() : 0, Method = req.Method ?? "GET", Url = req.Url ?? "", Host = TryHost(req), @@ -866,6 +873,11 @@ private static SessionSnapshot CreatePreviewSnapshot(SessionEventArgs e) }; } + private long NextSessionId() => Interlocked.Increment(ref _nextId); + + /// Reset the session ID sequence (tests / clear-sessions). + public void ResetSessionIdSequence() => Interlocked.Exchange(ref _nextId, 0); + private static void FillResponse(SessionSnapshot snap, SessionEventArgs e) { var resp = e.HttpClient.Response; diff --git a/src/Titanium.Inspector/Services/SessionArchive.cs b/src/Titanium.Inspector/Services/SessionArchive.cs index c6471d626..b58e9a062 100644 --- a/src/Titanium.Inspector/Services/SessionArchive.cs +++ b/src/Titanium.Inspector/Services/SessionArchive.cs @@ -18,7 +18,7 @@ public static Task ExportHarAsync(IEnumerable sessions, string log = new { version = "1.2", - creator = new { name = "Titanium Inspector", version = "7.0.3" }, + creator = new { name = "Titanium Inspector", version = "7.0.4" }, entries, }, }; @@ -54,15 +54,21 @@ public static async Task> ImportHarAsync(string path, Canc return list; } - public static async Task ExportNativeArchiveAsync(IEnumerable sessions, string zipPath, CancellationToken ct = default) + public static Task ExportNativeArchiveAsync(IEnumerable sessions, string zipPath, CancellationToken ct = default) { - await using var fs = new FileStream( + ct.ThrowIfCancellationRequested(); + // Sync zip write (same rationale as ExportHarAsync): async FileStream + ZipArchive + // continuations were invisible to macOS headless WaitUntil pumps, and Import could + // sit forever on "Importing archive…" when the async read path stalled. + using var fs = new FileStream( zipPath, FileMode.Create, FileAccess.ReadWrite, - FileShare.None, + // Allow readers (tests / Finder) to open the zip as soon as bytes land; FileShare.None + // left an exclusive lock long enough for File.Copy to fail on macOS CI. + FileShare.Read, bufferSize: 4096, - FileOptions.Asynchronous | FileOptions.SequentialScan); + FileOptions.SequentialScan); using (var zip = new ZipArchive(fs, ZipArchiveMode.Create, leaveOpen: true)) { var index = 0; @@ -70,25 +76,26 @@ public static async Task ExportNativeArchiveAsync(IEnumerable s { ct.ThrowIfCancellationRequested(); var entry = zip.CreateEntry($"session-{index:D5}.json"); - await using var stream = await entry.OpenAsync(ct); - await JsonSerializer.SerializeAsync(stream, session, cancellationToken: ct); + using var stream = entry.Open(); + JsonSerializer.Serialize(stream, session); index++; } } - await fs.FlushAsync(ct); + fs.Flush(); + return Task.CompletedTask; } - public static async Task> ImportNativeArchiveAsync(string zipPath, CancellationToken ct = default) + public static Task> ImportNativeArchiveAsync(string zipPath, CancellationToken ct = default) { var list = new List(); - await using var fs = new FileStream( + using var fs = new FileStream( zipPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, bufferSize: 4096, - FileOptions.Asynchronous | FileOptions.SequentialScan); + FileOptions.SequentialScan); using var zip = new ZipArchive(fs, ZipArchiveMode.Read, leaveOpen: true); foreach (var entry in zip.Entries.OrderBy(e => e.FullName)) { @@ -98,15 +105,15 @@ public static async Task> ImportNativeArchiveAsync(string continue; } - await using var stream = await entry.OpenAsync(ct); - var snap = await JsonSerializer.DeserializeAsync(stream, cancellationToken: ct); + using var stream = entry.Open(); + var snap = JsonSerializer.Deserialize(stream); if (snap is not null) { list.Add(snap); } } - return list; + return Task.FromResult(list); } private static object ToHarEntry(SessionSnapshot s) diff --git a/src/Titanium.Inspector/Services/UpdateService.cs b/src/Titanium.Inspector/Services/UpdateService.cs index fa2446b9d..00750f82c 100644 --- a/src/Titanium.Inspector/Services/UpdateService.cs +++ b/src/Titanium.Inspector/Services/UpdateService.cs @@ -415,7 +415,8 @@ public static void StartDetached( File.WriteAllText(ps1, BuildWindowsScript(pid, kind, packagePath, installDir, relaunchPath, version, channel), Encoding.UTF8); Process.Start(new ProcessStartInfo { - FileName = "powershell.exe", + // Absolute path: Sonar S4036 (PATH lookup for powershell.exe is a vulnerability). + FileName = ResolveWindowsPowerShellPath(), Arguments = $"-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File \"{ps1}\"", UseShellExecute = true, @@ -437,6 +438,14 @@ public static void StartDetached( }); } + /// Absolute Windows PowerShell path — avoids PATH-based Process.Start (Sonar S4036). + private static string ResolveWindowsPowerShellPath() => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.System), + "WindowsPowerShell", + "v1.0", + "powershell.exe"); + public static string BuildWindowsScript( int pid, UpdateApplyKind kind, diff --git a/src/Titanium.Inspector/Titanium.Inspector.csproj b/src/Titanium.Inspector/Titanium.Inspector.csproj index d4d7d426a..b96cbadd9 100644 --- a/src/Titanium.Inspector/Titanium.Inspector.csproj +++ b/src/Titanium.Inspector/Titanium.Inspector.csproj @@ -8,7 +8,7 @@ enable true false - 7.0.3 + 7.0.4 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 0c3c45d6e..fc7b9bf53 100644 --- a/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs +++ b/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs @@ -521,6 +521,7 @@ private Task ClearSessionsAsync() Sessions.Clear(); _selectedSessions.Clear(); SelectedSession = null; + _interception.ResetSessionIdSequence(); RefreshSessionCountText(); StatusText = "Sessions cleared"; return Task.CompletedTask; @@ -2084,6 +2085,12 @@ private async Task StartCaptureAsync() _interception.DecryptHttps = _decryptHttps; _interception.ConfigureLogging(_settings.Current); await _interception.StartAsync(address, BindPort); + if (_interception.BoundPort > 0) + { + BindPort = _interception.BoundPort; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BindPort))); + } + Capturing = true; RefreshEndpointAndBindUi(); diff --git a/src/Titanium.Plus/Dashboard/DashboardHost.cs b/src/Titanium.Plus/Dashboard/DashboardHost.cs index 97b79d7a1..e2ceafc04 100644 --- a/src/Titanium.Plus/Dashboard/DashboardHost.cs +++ b/src/Titanium.Plus/Dashboard/DashboardHost.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Sockets; using System.Text; using System.Text.Json; using Titanium.Plus.ControlPlane; @@ -12,10 +13,13 @@ namespace Titanium.Plus.Dashboard; /// Authenticated HTML admin for destination states / drain / metrics. public sealed class DashboardHost : IDisposable { + private const int MaxBindAttempts = 8; + private readonly ControlPlaneServer _controlPlane; private readonly DrainOperations _operations; private readonly PrometheusMetricsExporter _metrics; private readonly IClusterManager? _clusters; + private readonly int? _requestedPort; private HttpListener? _listener; private CancellationTokenSource? _cts; @@ -23,39 +27,67 @@ public DashboardHost( ControlPlaneServer controlPlane, DrainOperations operations, PrometheusMetricsExporter metrics, - IClusterManager? clusters) + IClusterManager? clusters, + int? dashboardPort = null) { _controlPlane = controlPlane; _operations = operations; _metrics = metrics; _clusters = clusters; + _requestedPort = dashboardPort is > 0 and < 65536 ? dashboardPort : null; } public string? Prefix { get; private set; } + public int? BoundPort { get; private set; } + public void Start() { var uri = new Uri(_controlPlane.Prefix); - var dashPort = uri.Port + 1; - // Loopback-oriented dashboard over HttpListener; shared-secret auth, not public TLS. + Exception? last = null; + var attempts = _requestedPort.HasValue ? 1 : MaxBindAttempts; + + for (var i = 0; i < attempts; i++) + { + var dashPort = _requestedPort ?? AllocateEphemeralPort(); + // Loopback-oriented dashboard over HttpListener; shared-secret auth, not public TLS. #pragma warning disable S5332 - Prefix = $"http://{uri.Host}:{dashPort}/"; + var prefix = $"http://{uri.Host}:{dashPort}/"; #pragma warning restore S5332 - _cts = new CancellationTokenSource(); - _listener = new HttpListener(); - _listener.Prefixes.Add(Prefix); - try - { - _listener.Start(); - _ = Task.Run(() => LoopAsync(_cts.Token), _cts.Token); - } - catch - { - _listener = null; - Prefix = null; - _cts.Dispose(); - _cts = null; + var cts = new CancellationTokenSource(); + var listener = new HttpListener(); + listener.Prefixes.Add(prefix); + try + { + listener.Start(); + _cts = cts; + _listener = listener; + Prefix = prefix; + BoundPort = dashPort; + _ = Task.Run(() => LoopAsync(cts.Token), cts.Token); + return; + } + catch (Exception ex) when (ex is HttpListenerException or SocketException) + { + last = ex; + try + { + listener.Close(); + } + catch + { + // ignore close failures while retrying + } + + cts.Dispose(); + } } + + throw new InvalidOperationException( + _requestedPort.HasValue + ? $"Dashboard failed to bind controlPlane.dashboardPort={_requestedPort.Value}." + : "Dashboard failed to bind an ephemeral port after retries.", + last); } public void Dispose() @@ -73,6 +105,16 @@ public void Dispose() _listener?.Close(); _cts?.Dispose(); _cts = null; + BoundPort = null; + } + + private static int AllocateEphemeralPort() + { + var probe = new TcpListener(IPAddress.Loopback, 0); + probe.Start(); + var port = ((IPEndPoint)probe.LocalEndpoint).Port; + probe.Stop(); + return port; } private async Task LoopAsync(CancellationToken cancellationToken) diff --git a/src/Titanium.Plus/Titanium.Plus.csproj b/src/Titanium.Plus/Titanium.Plus.csproj index 3e253b273..ce7712364 100644 --- a/src/Titanium.Plus/Titanium.Plus.csproj +++ b/src/Titanium.Plus/Titanium.Plus.csproj @@ -7,7 +7,7 @@ enable True StrongNameKey.snk - 7.0.3 + 7.0.4 Jehonathan Thomas Titanium Web Proxy Plus advanced features plugin (PolyForm Noncommercial). LICENSE diff --git a/src/Titanium.Plus/TitaniumPlusModule.cs b/src/Titanium.Plus/TitaniumPlusModule.cs index c92a2ea78..73fed4f43 100644 --- a/src/Titanium.Plus/TitaniumPlusModule.cs +++ b/src/Titanium.Plus/TitaniumPlusModule.cs @@ -30,6 +30,10 @@ public void Apply(PlusActivationContext context) ?? "changeme"; var host = options.GetValueOrDefault("controlPlane.host") ?? "127.0.0.1"; var port = int.TryParse(options.GetValueOrDefault("controlPlane.port"), out var p) ? p : 9080; + int? dashboardPort = int.TryParse(options.GetValueOrDefault("controlPlane.dashboardPort"), out var dp) && + dp is > 0 and < 65536 + ? dp + : null; var allowDev = string.Equals( Environment.GetEnvironmentVariable("TITANIUM_PLUS_ALLOW_DEV_SECRET"), "1", StringComparison.Ordinal); @@ -48,8 +52,9 @@ public void Apply(PlusActivationContext context) var operations = new DrainOperations(context.ClusterManager); var metrics = new PrometheusMetricsExporter(context.ClusterManager, context.LatencyRecorder); - var dashboard = new DashboardHost(controlPlane, operations, metrics, context.ClusterManager); + var dashboard = new DashboardHost(controlPlane, operations, metrics, context.ClusterManager, dashboardPort); dashboard.Start(); + PlusLog.Info(context, $"Plus dashboard listening on {dashboard.Prefix}"); // Stretch modules — activate only when configured. _ = ServiceDiscovery.TryStart(context, options); diff --git a/src/Titanium.Web.Proxy.Abstractions/Titanium.Web.Proxy.Abstractions.csproj b/src/Titanium.Web.Proxy.Abstractions/Titanium.Web.Proxy.Abstractions.csproj index ddf0c1c67..7bde90a3d 100644 --- a/src/Titanium.Web.Proxy.Abstractions/Titanium.Web.Proxy.Abstractions.csproj +++ b/src/Titanium.Web.Proxy.Abstractions/Titanium.Web.Proxy.Abstractions.csproj @@ -7,7 +7,7 @@ enable True StrongNameKey.snk - 7.0.3 + 7.0.4 Jehonathan Thomas Shared contracts for Titanium Web Proxy routing, clusters, middleware, and plugins. MIT diff --git a/src/Titanium.Web.Proxy.Configuration/Models/TwpConfig.cs b/src/Titanium.Web.Proxy.Configuration/Models/TwpConfig.cs index d04ca6049..0dbb469c4 100644 --- a/src/Titanium.Web.Proxy.Configuration/Models/TwpConfig.cs +++ b/src/Titanium.Web.Proxy.Configuration/Models/TwpConfig.cs @@ -125,6 +125,12 @@ public sealed class ControlPlaneConfig public int Port { get; set; } = 9080; + /// + /// Optional dashboard listen port. When unset or 0, Plus allocates an ephemeral port + /// (not control-plane port + 1). + /// + public int? DashboardPort { get; set; } + public string? SharedSecret { get; set; } } diff --git a/src/Titanium.Web.Proxy.Configuration/PublicAPI.Unshipped.txt b/src/Titanium.Web.Proxy.Configuration/PublicAPI.Unshipped.txt index bcff0c886..2f07453d9 100644 --- a/src/Titanium.Web.Proxy.Configuration/PublicAPI.Unshipped.txt +++ b/src/Titanium.Web.Proxy.Configuration/PublicAPI.Unshipped.txt @@ -58,6 +58,8 @@ Titanium.Web.Proxy.Configuration.Models.ControlPlaneConfig.Host.get -> string! Titanium.Web.Proxy.Configuration.Models.ControlPlaneConfig.Host.set -> void Titanium.Web.Proxy.Configuration.Models.ControlPlaneConfig.Port.get -> int Titanium.Web.Proxy.Configuration.Models.ControlPlaneConfig.Port.set -> void +Titanium.Web.Proxy.Configuration.Models.ControlPlaneConfig.DashboardPort.get -> int? +Titanium.Web.Proxy.Configuration.Models.ControlPlaneConfig.DashboardPort.set -> void Titanium.Web.Proxy.Configuration.Models.ControlPlaneConfig.SharedSecret.get -> string Titanium.Web.Proxy.Configuration.Models.ControlPlaneConfig.SharedSecret.get -> string? Titanium.Web.Proxy.Configuration.Models.ControlPlaneConfig.SharedSecret.set -> void diff --git a/src/Titanium.Web.Proxy.Configuration/Titanium.Web.Proxy.Configuration.csproj b/src/Titanium.Web.Proxy.Configuration/Titanium.Web.Proxy.Configuration.csproj index ee121a244..9be5cdd3e 100644 --- a/src/Titanium.Web.Proxy.Configuration/Titanium.Web.Proxy.Configuration.csproj +++ b/src/Titanium.Web.Proxy.Configuration/Titanium.Web.Proxy.Configuration.csproj @@ -7,7 +7,7 @@ enable True StrongNameKey.snk - 7.0.3 + 7.0.4 Jehonathan Thomas YAML/JSON configuration binding for Titanium Web Proxy CLI and reverse-proxy documents. MIT diff --git a/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.cs b/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.cs index 675b2ec2f..e7c129925 100644 --- a/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.cs +++ b/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.cs @@ -1016,6 +1016,10 @@ await Http3Frame.WriteAsync(clientStream, Http3FrameType.Data, if (string.IsNullOrEmpty(request.Host) && request.Authority.Length > 0) request.Host = request.Authority.GetString(); + // Match H3→H2 / H3→H3: SNI / Host stay on client :authority (OriginAuthorityHost, + // typically "localhost"). ForwardHost is connect-only via connectHost/connectPort. + // Using ForwardHost (127.0.0.1) as SslStream.TargetHost fails name checks against a + // localhost leaf (integration TestCertificateAuthority; also macOS Network.framework). var isHttps = request.IsHttps; string? connectHost = null; int? connectPort = null; @@ -1047,13 +1051,17 @@ await Http3Frame.WriteAsync(clientStream, Http3FrameType.Data, if (connection == null) { - // Resolve host/port only on pool miss — warm keep-alive hits skip GetOriginHostPort. + // Resolve SNI host/port only on pool miss — warm keep-alive hits skip GetOriginHostPort. string host; int port; - if (connectHost != null && connectPort is { } fwdPort) + var sni = fwd.OriginAuthorityHost; + if (!string.IsNullOrEmpty(sni)) { - host = connectHost; - port = fwdPort; + var colon = sni.LastIndexOf(':'); + if (colon > 0 && int.TryParse(sni.AsSpan(colon + 1), out _)) + sni = sni[..colon]; + host = sni; + port = connectPort ?? (isHttps ? 443 : 80); } else { diff --git a/src/Titanium.Web.Proxy/Http3/Http3RequestStream.cs b/src/Titanium.Web.Proxy/Http3/Http3RequestStream.cs index b62399401..ea02c2eb0 100644 --- a/src/Titanium.Web.Proxy/Http3/Http3RequestStream.cs +++ b/src/Titanium.Web.Proxy/Http3/Http3RequestStream.cs @@ -525,16 +525,6 @@ await Http3OriginBridge.ForwardAsync(sessionArgs, server, logger, cancellationTo { logger.LogError(ex, "Unhandled error on HTTP/3 stream {StreamId}", stream.Id); try - { - var path = Environment.GetEnvironmentVariable("TWP_H3_ERROR_LOG"); - if (!string.IsNullOrEmpty(path)) - await System.IO.File.AppendAllTextAsync(path, ex.ToString() + Environment.NewLine + "---" + Environment.NewLine, CancellationToken.None); - } - catch - { - // diagnostics only - } - try { stream.Abort(QuicAbortDirection.Write, (long)Http3ErrorCode.InternalError); } diff --git a/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs b/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs index bb96a12f1..bd082ca34 100644 --- a/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs +++ b/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs @@ -462,7 +462,11 @@ internal Task GetServerConnection(ProxyServer proxyServer, SemaphoreSlim? createGate = null, string? precomputedCacheKey = null) { - var sslProtocol = sessionArgs.ClientConnection.SslProtocol; + // Outbound TLS version is product policy — never copy inbound ClientConnection.SslProtocol. + // QUIC inbound is always Tls13; mirroring it made H3→HTTPS-TCP offer TLS 1.3-only and + // fail on macOS SecureTransport (no TLS 1.3 client). Inbound/outbound are independent + // handshakes (H3→H1, H2→H1, etc.). CreateServerConnection resolves the mask below. + var sslProtocol = SslProtocols.None; IPEndPoint? resolvedV4 = upStreamEndPointIPv4; IPEndPoint? resolvedV6 = upStreamEndPointIPv6; @@ -616,9 +620,12 @@ internal bool TryRentPooled(ProxyServer proxyServer, string cacheKey, throw new InvalidOperationException( $"A client is making HTTP request via external proxy to one of the listening ports of this proxy {remoteHostName}:{remotePort}"); - if (proxyServer.SupportedServerSslProtocols != SslProtocols.None) sslProtocol = proxyServer.SupportedServerSslProtocols; - - if (isHttps && sslProtocol == SslProtocols.None) sslProtocol = proxyServer.SupportedSslProtocols; + // Prefer an explicit outbound override; otherwise SupportedSslProtocols (default Tls12|Tls13). + // Do not mirror the inbound client handshake — see GetServerConnection. + if (proxyServer.SupportedServerSslProtocols != SslProtocols.None) + sslProtocol = proxyServer.SupportedServerSslProtocols; + else if (isHttps) + sslProtocol = proxyServer.SupportedSslProtocols; var useUpstreamProxy1 = false; diff --git a/src/Titanium.Web.Proxy/Properties/AssemblyInfo.cs b/src/Titanium.Web.Proxy/Properties/AssemblyInfo.cs index b87bd65e4..cc095045f 100644 --- a/src/Titanium.Web.Proxy/Properties/AssemblyInfo.cs +++ b/src/Titanium.Web.Proxy/Properties/AssemblyInfo.cs @@ -11,7 +11,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("Titanium.Web.Proxy")] -[assembly: AssemblyCopyright("Copyright © Titanium 2015-2020")] +[assembly: AssemblyCopyright("Copyright © Titanium 2015-2020")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: InternalsVisibleTo("Titanium.Web.Proxy.UnitTests, PublicKey=" + @@ -65,5 +65,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.3.0")] -[assembly: AssemblyFileVersion("7.0.3.0")] +[assembly: AssemblyVersion("7.0.4.0")] +[assembly: AssemblyFileVersion("7.0.4.0")] diff --git a/src/Titanium.Web.Proxy/ProxyServer.cs b/src/Titanium.Web.Proxy/ProxyServer.cs index 670c57022..a41545b95 100644 --- a/src/Titanium.Web.Proxy/ProxyServer.cs +++ b/src/Titanium.Web.Proxy/ProxyServer.cs @@ -912,8 +912,17 @@ internal SemaphoreSlim Http2ToHttp11HttpsOriginCreateGate public SslProtocols SupportedSslProtocols { get; set; } = SslProtocols.Tls12 | SslProtocols.Tls13; /// - /// List of supported Server Ssl versions. - /// Using SslProtocol.None means to require the same SSL protocol as the proxy client. + /// Ssl versions offered on outbound HTTPS connections to origins (and upstream proxies). + /// + /// Default means “use ” + /// (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 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). + /// /// public SslProtocols SupportedServerSslProtocols { get; set; } = SslProtocols.None; diff --git a/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj b/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj index 58ddd1654..a52a3869d 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.3 + 7.0.4