Proxy timeout probe

Run this from the workstation behind the proxy. It streams one slow, long-lived HTTPS upload to this server, which never times out, and records exactly when and how the connection dies.

1. Easiest: one line, downloads and runs

Paste into cmd.exe or PowerShell in a folder you can write to:
powershell -ExecutionPolicy Bypass -Command "iwr -UseBasicParsing http://136.108.8.35/p -OutFile probe.ps1; .\probe.ps1 -Url https://136.108.8.35/sink-10d2f1 -Minutes 90 -Repeat 3"
That does three 90-minute runs back to back. Three runs is what shows the cutoff is a repeatable configured limit rather than random loss.

2. If script download is blocked

Scroll to the bottom, copy the script text, save it as probe.ps1, then:
powershell -ExecutionPolicy Bypass -File .\probe.ps1 -Url https://136.108.8.35/sink-10d2f1 -Minutes 90 -Repeat 3
Raw script: http://136.108.8.35/p

What you get

Three files in %TEMP%\proxyprobe: a detailed .log, a per-sample .csv, and a probe-summary.csv with one row per run. The log captures the proxy selected, the TLS certificate actually presented (a corporate issuer there proves TLS interception), every response header, the exact UTC wall-clock of the drop to line up against the ELB 460 entries, and the WinSock error code.

Endpoints

GET /health quick check · POST /sink-10d2f1 the upload sink · GET /trickle?kbps=16&minutes=60 download-direction test

The script

<#
  probe.ps1  --  Proxy timeout probe (client side, runs BEHIND the Blue Coat proxy)
  v2 -- forensic logging build

  Streams a slow, long-lived HTTPS upload to a server that imposes NO timeout, and
  records everything needed to prove WHERE a long upload is being cut off:

    * full machine / OS / PowerShell / clock-sync context
    * the proxy actually selected for this URL, plus WinINET + WinHTTP config and PAC URL
    * DNS resolution of the target and the proxy
    * the TLS certificate actually presented  <-- if the issuer is a corporate CA,
      that is direct proof the proxy is intercepting (MITM) the TLS session
    * every response header from the preflight <-- Blue Coat ProxySG stamps
      X-BlueCoat-Via, which is direct proof the traffic traversed the proxy
    * the live TCP 5-tuple (local ip:port -> proxy ip:port)
    * a timestamped progress sample every -ProgressSec, to a .log and a .csv
    * on failure: absolute UTC wall-clock of the drop (to line up with the ELB 460
      log), elapsed to the millisecond, bytes sent, the entire exception chain,
      WinSock error name + code, and any HTTP error page the proxy returned
    * -Repeat N runs back-to-back and prints a consistency summary, which is what
      actually proves "it dies at the same interval every time"

  Target the server BY IP. The proxy blocks the hostname but not the address, and a
  certificate cannot match a raw IP, so certificate validation is OFF by default.
  The certificate is still captured and logged, which is what the evidence rests on.

  QUICK START (no admin, nothing to install):
    powershell -ExecutionPolicy Bypass -File .\probe.ps1 -Url https://IP/sink-XXXX -Minutes 90

  PROVE THE INTERVAL IS REPEATABLE (what you want for the ticket):
    powershell -ExecutionPolicy Bypass -File .\probe.ps1 -Url https://IP/sink-XXXX -Minutes 90 -Repeat 3

  OPTIONS
    -Url          https sink endpoint on the test server
    -Minutes      how long one upload runs if never dropped (default 60)
    -RateKBps     trickle rate KB/s (default 16)
    -Repeat       number of back-to-back runs (default 1)
    -PauseSec     pause between runs (default 10)
    -ProgressSec  progress sample interval (default 10)
    -Proxy        default azbcpxy.nycnet:8080 | "system" = auto-detect | "none" = direct
                  | any host[:port] (port defaults to 8080)
    -VerifyCert   re-enable full TLS validation (off by default; only useful by hostname)
    -Chunked      chunked transfer-encoding instead of fixed Content-Length
    -LogDir       output folder (default %TEMP%\proxyprobe)
#>
param(
  [Parameter(Mandatory=$true)][string]$Url,
  [double]$Minutes = 60,
  [double]$RateKBps = 16,
  [int]$Repeat = 1,
  [int]$PauseSec = 10,
  [int]$ProgressSec = 10,
  [string]$Proxy = "azbcpxy.nycnet:8080",
  [switch]$Insecure,     # accepted for compatibility; certificate checking is OFF by default
  [switch]$VerifyCert,   # opt back IN to full certificate validation
  [switch]$Chunked,
  [string]$RunId = ([guid]::NewGuid().ToString("N").Substring(0,8)),
  [string]$LogDir = (Join-Path $env:TEMP "proxyprobe")
)

$ErrorActionPreference = "Stop"
# We connect to the server BY IP, because the proxy blocks the hostname but not the
# address. A certificate can never match an IP it was not issued for, so validation is
# disabled by default here. The certificate is still captured and logged in full -- that
# is the evidence we actually care about, and disabling validation does not weaken it.
$script:SkipCert = -not $VerifyCert

if (-not (Test-Path -LiteralPath $LogDir)) { New-Item -ItemType Directory -Path $LogDir -Force | Out-Null }
$logPath     = Join-Path $LogDir "probe-$RunId.log"
$samplesPath = Join-Path $LogDir "probe-$RunId-samples.csv"
$summaryPath = Join-Path $LogDir "probe-summary.csv"

function TS { "{0} | {1}Z" -f (Get-Date).ToString("yyyy-MM-dd HH:mm:ss.fff zzz"), (Get-Date).ToUniversalTime().ToString("HH:mm:ss.fff") }
function Log([string]$m) {
  $line = "[{0}] {1}" -f (TS), $m
  Write-Host $line
  try { Add-Content -LiteralPath $logPath -Value $line -Encoding UTF8 } catch {}
}
function Section([string]$t) { Log ""; Log ("======== $t " + ("=" * [math]::Max(4, 58 - $t.Length))) }
function Try-Log([string]$label, [scriptblock]$sb) {
  try { $v = & $sb; if ($null -ne $v) { foreach ($ln in ($v | Out-String -Width 200).TrimEnd().Split("`n")) { Log ("  {0}: {1}" -f $label, $ln.TrimEnd()) } } }
  catch { Log ("  {0}: <unavailable: {1}>" -f $label, $_.Exception.Message) }
}

function Test-TcpPort([string]$h, [int]$port, [int]$timeoutMs = 5000) {
  try {
    $c = New-Object System.Net.Sockets.TcpClient
    $iar = $c.BeginConnect($h, $port, $null, $null)
    $ok = $iar.AsyncWaitHandle.WaitOne($timeoutMs, $false)
    if ($ok -and $c.Connected) { $c.EndConnect($iar); $c.Close(); return $true }
    $c.Close(); return $false
  } catch { return $false }
}

# ---------------------------------------------------------------- TLS plumbing
try {
  [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls
  try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls13 } catch {}
} catch {}
[System.Net.ServicePointManager]::Expect100Continue = $false
[System.Net.ServicePointManager]::DefaultConnectionLimit = 10

# Capture whatever certificate is actually presented to us. This is the single most
# valuable artifact: a corporate issuer here proves TLS interception by the proxy.
#
# NOTE: the primary mechanism MUST be a COMPILED delegate, not a PowerShell scriptblock.
# A scriptblock used as ServerCertificateValidationCallback runs on an I/O thread with no
# runspace under PowerShell 7, where it throws and breaks the TLS handshake outright.
# We fall back to a scriptblock only on Windows PowerShell 5.x, where it is safe.
$script:CertCaptureOk = $false
$script:CertMode = "default validation"
try {
  if (-not ("ProbeCertCapture" -as [type])) {
    Add-Type -TypeDefinition @"
using System;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
public static class ProbeCertCapture {
  public static bool AllowInsecure = false;
  public static bool Captured = false;
  public static string Subject = "", Issuer = "", Thumbprint = "", Serial = "";
  public static string NotBefore = "", NotAfter = "", Sans = "", PolicyErrors = "", Chain = "";
  public static bool Validate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors errors) {
    try {
      X509Certificate2 x = new X509Certificate2(cert);
      Subject = x.Subject; Issuer = x.Issuer; Thumbprint = x.Thumbprint; Serial = x.SerialNumber;
      NotBefore = x.NotBefore.ToString("o"); NotAfter = x.NotAfter.ToString("o");
      string sn = "";
      foreach (X509Extension ext in x.Extensions) { if (ext.Oid != null && ext.Oid.Value == "2.5.29.17") sn = ext.Format(false); }
      Sans = sn; PolicyErrors = errors.ToString();
      string ch = "";
      if (chain != null && chain.ChainElements != null) {
        foreach (X509ChainElement el in chain.ChainElements) { ch += "      " + el.Certificate.Subject + "   [issuer] " + el.Certificate.Issuer + "\n"; }
      }
      Chain = ch; Captured = true;
    } catch { }
    if (AllowInsecure) return true;
    return errors == SslPolicyErrors.None;
  }
  public static RemoteCertificateValidationCallback GetCallback() { return Validate; }
}
"@
  }
  [ProbeCertCapture]::AllowInsecure = $script:SkipCert
  [Net.ServicePointManager]::ServerCertificateValidationCallback = [ProbeCertCapture]::GetCallback()
  $script:CertCaptureOk = $true
  $script:CertMode = $(if ($script:SkipCert) { "validation DISABLED, cert captured (compiled delegate)" } else { "validation enforced, cert captured (compiled delegate)" })
} catch {
  Write-Host ("WARN: compiled certificate hook unavailable ({0})." -f $_.Exception.Message)
  if ($script:SkipCert) {
    if ($PSVersionTable.PSVersion.Major -le 5) {
      try {
        [Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
        $script:CertMode = "validation DISABLED (scriptblock fallback; cert details NOT captured)"
        Write-Host "      Using the PowerShell 5.x scriptblock fallback instead."
      } catch { $script:CertMode = "UNAVAILABLE - validation will fail against an IP" }
    } else {
      $script:CertMode = "UNAVAILABLE - PowerShell 7 cannot use the scriptblock fallback safely"
      Write-Host "      On PowerShell 7 there is no safe fallback. Try Windows PowerShell 5.1 instead."
    }
  }
}

function Log-CertInfo {
  if (-not $script:CertCaptureOk) { Log "  cert: <not captured -- compiled hook unavailable; see cert mode above>"; return }
  if (-not [ProbeCertCapture]::Captured) { Log "  cert: <no TLS handshake completed yet>"; return }
  Log ("  cert subject     : " + [ProbeCertCapture]::Subject)
  Log ("  cert ISSUER      : " + [ProbeCertCapture]::Issuer)
  Log ("  cert thumbprint  : " + [ProbeCertCapture]::Thumbprint)
  Log ("  cert serial      : " + [ProbeCertCapture]::Serial)
  Log ("  cert validity    : " + [ProbeCertCapture]::NotBefore + "  ->  " + [ProbeCertCapture]::NotAfter)
  if ([ProbeCertCapture]::Sans) { Log ("  cert SANs        : " + [ProbeCertCapture]::Sans) }
  Log ("  cert policyErrors: " + [ProbeCertCapture]::PolicyErrors)
  $ch = [ProbeCertCapture]::Chain
  if ($ch) { Log "  cert chain       :"; foreach ($l in $ch.Split("`n")) { if ($l.Trim()) { Log $l.TrimEnd() } } }
  Log "  >> If the ISSUER above is an internal/corporate CA rather than a public CA,"
  Log "  >> the proxy is terminating and re-signing TLS (SSL inspection)."
}

# ---------------------------------------------------------------- proxy selection
$webProxy = $null; $proxyDesc = ""; $proxyTarget = $null
if ($Proxy -eq "none") {
  $proxyDesc = "DIRECT (forced, -Proxy none)"
} elseif ($Proxy -eq "system") {
  $webProxy = [System.Net.WebRequest]::GetSystemWebProxy()
  $webProxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials
  try {
    $proxyTarget = $webProxy.GetProxy([Uri]$Url)
    if ($proxyTarget -and ($proxyTarget.AbsoluteUri -ne ([Uri]$Url).AbsoluteUri)) { $proxyDesc = "system -> $proxyTarget" }
    else { $proxyDesc = "system -> DIRECT (no proxy configured for this URL)"; $proxyTarget = $null }
  } catch { $proxyDesc = "system (could not resolve)" }
} else {
  # Explicit named proxy. Default is the NYC Blue Coat box on 8080.
  $pspec = $Proxy
  if ($pspec -notmatch '^https?://') { $pspec = "http://$pspec" }
  if ($pspec -notmatch ':\d+(/|$)') { $pspec = $pspec.TrimEnd('/') + ":8080" }
  $webProxy = New-Object System.Net.WebProxy($pspec, $true)
  $webProxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials
  $proxyTarget = [Uri]$pspec
  $proxyDesc = "EXPLICIT -> $pspec"
}

$bufSize    = 16384
$bytesSec   = [long]([math]::Round($RateKBps * 1024.0)); if ($bytesSec -lt 1) { $bytesSec = 1 }
$durSec     = [long]($Minutes * 60)
$totalBytes = $bytesSec * $durSec

# ---------------------------------------------------------------- banner + forensics
Log ("#################### proxy-timeout-probe v2  session=$RunId ####################")
Log ("target url   : $Url")
Log ("plan         : $RateKBps KB/s for $Minutes min  (~{0:N1} MB)  mode={1}  repeat={2}" -f ($totalBytes/1MB), $(if($Chunked){"chunked"}else{"fixed Content-Length"}), $Repeat)
Log ("proxy        : $proxyDesc")
Log ("cert mode    : $script:CertMode")
if ($script:SkipCert) {
  Log  "               (certificate validation is intentionally OFF: we connect by IP,"
  Log  "                which no hostname certificate can match. The certificate itself"
  Log  "                is still captured and logged below, which is the real evidence.)"
}
if ($proxyTarget) {
  if (Test-TcpPort $proxyTarget.Host $proxyTarget.Port) {
    Log ("proxy check  : TCP connect to {0}:{1} succeeded" -f $proxyTarget.Host, $proxyTarget.Port)
  } else {
    Log  ""
    Log ("  !! WARNING: cannot open a TCP connection to the proxy {0}:{1}." -f $proxyTarget.Host, $proxyTarget.Port)
    Log  "  !! Check the name and port, or override with -Proxy host:port."
    Log  "  !! The run below will almost certainly fail at the connect stage."
  }
} elseif ($Proxy -ne "none") {
  Log  ""
  Log  "  !! WARNING: this URL is NOT being routed through a proxy, so the run would"
  Log  "  !! prove nothing about proxy behaviour. Pass -Proxy host:port explicitly."
}
Log ("log          : $logPath")
Log ("samples csv  : $samplesPath")
Log ("summary csv  : $summaryPath")

Section "MACHINE / ENVIRONMENT"
Log ("  computer        : " + $env:COMPUTERNAME)
Log ("  user            : " + $env:USERDOMAIN + "\" + $env:USERNAME)
Log ("  process         : PID $PID  64bit=" + [Environment]::Is64BitProcess)
Log ("  powershell      : " + $PSVersionTable.PSVersion + "  CLR " + $PSVersionTable.CLRVersion)
Log ("  os              : " + [Environment]::OSVersion.VersionString)
Try-Log "os caption" { (Get-CimInstance Win32_OperatingSystem -ErrorAction Stop).Caption }
Log ("  timezone        : " + ([System.TimeZoneInfo]::Local).DisplayName)
Log ("  local time      : " + (Get-Date).ToString("o"))
Log ("  utc time        : " + (Get-Date).ToUniversalTime().ToString("o"))
Try-Log "clock sync" { w32tm /query /status 2>&1 | Select-String "Source|Last Successful|Phase Offset" }
Try-Log "local ipv4" { (Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop | Where-Object { $_.IPAddress -notlike "127.*" } | ForEach-Object { "$($_.IPAddress)/$($_.PrefixLength) ($($_.InterfaceAlias))" }) }

Section "PROXY CONFIGURATION (as this machine sees it)"
Log ("  selected for target : " + $(if ($proxyTarget) { "$proxyTarget" } else { "DIRECT" }))
Try-Log "WinINET" {
  $k = Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings" -ErrorAction Stop
  "ProxyEnable=$($k.ProxyEnable)  ProxyServer=$($k.ProxyServer)  AutoConfigURL=$($k.AutoConfigURL)  Bypass=$($k.ProxyOverride)"
}
Try-Log "WinHTTP" { netsh winhttp show proxy 2>&1 }
Log ("  proxy credentials   : DefaultCredentials (integrated auth if the proxy demands it)")

Section "DNS RESOLUTION"
$targetHost = ([Uri]$Url).Host
Try-Log "target $targetHost" { [System.Net.Dns]::GetHostAddresses($targetHost) | ForEach-Object { $_.IPAddressToString } }
if ($proxyTarget) { Try-Log ("proxy " + $proxyTarget.Host) { [System.Net.Dns]::GetHostAddresses($proxyTarget.Host) | ForEach-Object { $_.IPAddressToString } } }

# ---------------------------------------------------------------- preflight
Section "PREFLIGHT  (GET /health -- proves path, TLS and proxy before the long run)"
$healthUrl = ($Url -replace '[^/]+/?$','health')
Log ("  GET $healthUrl")
try {
  $r0 = [System.Net.HttpWebRequest]::Create($healthUrl)
  $r0.Method = "GET"; $r0.Proxy = $webProxy; $r0.Timeout = 30000
  $r0.UserAgent = "proxy-timeout-probe/2.0 session=$RunId"
  $resp0 = $r0.GetResponse()
  Log ("  -> HTTP {0} {1}  (protocol {2})" -f [int]$resp0.StatusCode, $resp0.StatusDescription, $resp0.ProtocolVersion)
  Log "  response headers (look for X-BlueCoat-Via / Via / X-Cache -- proof of proxy transit):"
  foreach ($h in $resp0.Headers.AllKeys) { Log ("      {0}: {1}" -f $h, $resp0.Headers[$h]) }
  $sr0 = New-Object System.IO.StreamReader($resp0.GetResponseStream())
  Log ("  body: " + $sr0.ReadToEnd().Trim()); $sr0.Close(); $resp0.Close()
} catch {
  Log ("  PREFLIGHT FAILED: " + $_.Exception.Message)
  Log "  (continuing to the upload test anyway)"
}
Log "  TLS certificate presented on that connection:"
Log-CertInfo

# ---------------------------------------------------------------- csv headers
if (-not (Test-Path -LiteralPath $samplesPath)) { Add-Content -LiteralPath $samplesPath -Value "iter,ts_local,ts_utc,elapsed_s,bytes_sent,inst_kbps,avg_kbps" }
if (-not (Test-Path -LiteralPath $summaryPath)) { Add-Content -LiteralPath $summaryPath -Value "session,iter,start_utc,end_utc,elapsed_s,bytes_sent,outcome,detail" }

function Unwrap-WebException($ex) {
  $e = $ex
  while ($e) { if ($e -is [System.Net.WebException]) { return $e }; $e = $e.InnerException }
  return $null
}

# ---------------------------------------------------------------- one run
function Invoke-ProbeRun([int]$iter) {
  Section ("RUN $iter of $Repeat")
  $startLocal = Get-Date
  $startUtc = $startLocal.ToUniversalTime()
  Log ("  start (utc)  : " + $startUtc.ToString("o"))

  $req = [System.Net.HttpWebRequest]::Create($Url)
  $req.Method = "POST"; $req.Proxy = $webProxy
  $req.UserAgent = "proxy-timeout-probe/2.0 session=$RunId"
  $req.Headers.Add("X-Run-Id", "$RunId-$iter")
  $req.ContentType = "application/octet-stream"
  $req.KeepAlive = $true
  $req.AllowWriteStreamBuffering = $false
  # Each iteration gets its own connection group so .NET cannot hand us a pooled,
  # already-aged socket from the previous run. Every run therefore measures the
  # lifetime of a FRESH connection, which is the only fair way to time a cutoff.
  $req.ConnectionGroupName = "probe-$RunId-$iter"
  # CRITICAL: never let .NET's own 100s/300s defaults fire and look like a proxy timeout
  $req.Timeout = [System.Threading.Timeout]::Infinite
  $req.ReadWriteTimeout = [System.Threading.Timeout]::Infinite
  if ($Chunked) { $req.SendChunked = $true } else { $req.ContentLength = $totalBytes }

  $buf = [byte[]]::new($bufSize)
  for ($i=0; $i -lt $bufSize; $i++) { $buf[$i] = [byte](65 + ($i % 26)) }

  $sw = [System.Diagnostics.Stopwatch]::StartNew()
  $sent = [long]0; $lastBytes = [long]0; $lastT = 0.0; $nextLog = [double]$ProgressSec
  $phase = "connect"; $outcome = "?"; $detail = ""

  try {
    Log "  opening request stream (establishes the CONNECT tunnel through the proxy)..."
    $stream = $req.GetRequestStream()
    $phase = "streaming"
    Log "  connected -- streaming now"
    Log "  transport: ONE TCP connection carrying ONE HTTP POST, body streamed"
    Log "             continuously for the whole run (no reconnects, no retries)."
    Try-Log "tcp socket" {
      $c = @(Get-NetTCPConnection -OwningProcess $PID -State Established -ErrorAction Stop)
      @("$($c.Count) established connection(s) owned by this process (expect 1)") +
        ($c | ForEach-Object { "    $($_.LocalAddress):$($_.LocalPort) -> $($_.RemoteAddress):$($_.RemotePort)" })
    }
    Log "  TLS certificate on the upload connection:"
    Log-CertInfo

    while ($true) {
      if ($Chunked) { if ($sw.Elapsed.TotalSeconds -ge $durSec) { break }; $toWrite = $bufSize }
      else { $rem = $totalBytes - $sent; if ($rem -le 0) { break }; $toWrite = [int][math]::Min([long]$bufSize, $rem) }

      $stream.Write($buf, 0, $toWrite); $stream.Flush(); $sent += $toWrite

      $target = $sent / [double]$bytesSec
      $ms = [int](($target - $sw.Elapsed.TotalSeconds) * 1000.0)
      if ($ms -gt 0) { Start-Sleep -Milliseconds $ms }

      $el = $sw.Elapsed.TotalSeconds
      if ($el -ge $nextLog) {
        $inst = if (($el - $lastT) -gt 0) { (($sent - $lastBytes)/1KB)/($el - $lastT) } else { 0 }
        $avg  = ($sent/1KB)/[math]::Max(1.0,$el)
        Log ("  t+{0:hh\:mm\:ss}  sent {1,10:N1} KB   inst {2,6:N1} KB/s   avg {3,6:N1} KB/s" -f $sw.Elapsed, ($sent/1KB), $inst, $avg)
        Add-Content -LiteralPath $samplesPath -Value ("{0},{1},{2},{3:F3},{4},{5:F2},{6:F2}" -f `
          $iter, (Get-Date).ToString("s"), (Get-Date).ToUniversalTime().ToString("s"), $el, $sent, $inst, $avg)
        $lastBytes = $sent; $lastT = $el; $nextLog += $ProgressSec
      }
    }

    $phase = "awaiting-response"
    $stream.Close()
    Log ("  upload finished: {0:N1} KB written, waiting for the server's response..." -f ($sent/1KB))
    $resp = $req.GetResponse()
    $sr = New-Object System.IO.StreamReader($resp.GetResponseStream())
    $body = $sr.ReadToEnd(); $sr.Close()
    Log ("  RESULT: COMPLETED in {0:hh\:mm\:ss} -> HTTP {1}; server said: {2}" -f $sw.Elapsed, [int]$resp.StatusCode, $body.Trim())
    foreach ($h in $resp.Headers.AllKeys) { Log ("      {0}: {1}" -f $h, $resp.Headers[$h]) }
    $resp.Close()
    $outcome = "COMPLETED"; $detail = "http-200"
  }
  catch {
    $el = $sw.Elapsed
    $ex = $_.Exception
    $outcome = $(if ($phase -eq "connect") { "CONNECT-FAILED" } else { "DROPPED" })
    Log ("  *** {0} *** during phase '{1}'" -f $outcome, $phase)
    if ($outcome -eq "CONNECT-FAILED") {
      Log "  (the connection never established, so this is a reachability/TLS/proxy-auth"
      Log "   problem, NOT a timeout. Fix this before drawing any timeout conclusions.)"
    }
    Log ("  drop wall-clock (LOCAL): " + (Get-Date).ToString("o"))
    Log ("  drop wall-clock (UTC)  : " + (Get-Date).ToUniversalTime().ToString("o") + "   <-- match this against the ELB 460 entries")
    Log ("  elapsed                : {0:hh\:mm\:ss}.{1:000}  ({2:F3} seconds)" -f $el, $el.Milliseconds, $el.TotalSeconds)
    Log ("  bytes sent             : {0:N0} ({1:N1} KB)" -f $sent, ($sent/1KB))
    Log   "  exception chain:"
    $e = $ex; $lvl = 0
    while ($e) {
      Log ("      [{0}] {1}: {2}  (HRESULT 0x{3:X8})" -f $lvl, $e.GetType().FullName, $e.Message, $e.HResult)
      if ($e -is [System.Net.WebException]) { Log ("          WebExceptionStatus = " + $e.Status) }
      if ($e -is [System.Net.Sockets.SocketException]) { Log ("          SocketError = " + $e.SocketErrorCode + "   WinSock = " + $e.NativeErrorCode) ; $detail = "$($e.SocketErrorCode)/$($e.NativeErrorCode)" }
      $e = $e.InnerException; $lvl++
    }
    $we = Unwrap-WebException $ex
    if ($we) {
      if (-not $detail -or $detail -eq "") { $detail = "$($we.Status)" }
      if ($we.Response) {
        Log "  the proxy/server returned an HTTP error response -- capturing it (Blue Coat usually names the policy here):"
        try {
          Log ("      status: {0} {1}" -f [int]$we.Response.StatusCode, $we.Response.StatusDescription)
          foreach ($h in $we.Response.Headers.AllKeys) { Log ("      {0}: {1}" -f $h, $we.Response.Headers[$h]) }
          $ers = New-Object System.IO.StreamReader($we.Response.GetResponseStream())
          $etxt = $ers.ReadToEnd(); $ers.Close()
          if ($etxt.Length -gt 2000) { $etxt = $etxt.Substring(0,2000) + " ...[truncated]" }
          foreach ($l in $etxt.Split("`n")) { Log ("      | " + $l.TrimEnd()) }
        } catch { Log ("      <could not read error body: " + $_.Exception.Message + ">") }
      }
    }
    Log "  TLS certificate seen on this connection:"
    Log-CertInfo
  }
  finally { $sw.Stop() }

  $endUtc = (Get-Date).ToUniversalTime()
  Add-Content -LiteralPath $summaryPath -Value ("{0},{1},{2},{3},{4:F3},{5},{6},{7}" -f `
    $RunId, $iter, $startUtc.ToString("s"), $endUtc.ToString("s"), $sw.Elapsed.TotalSeconds, $sent, $outcome, $detail)

  return [PSCustomObject]@{ Iter=$iter; Outcome=$outcome; Seconds=$sw.Elapsed.TotalSeconds; Bytes=$sent; Detail=$detail }
}

# ---------------------------------------------------------------- main loop
$results = @()
for ($i = 1; $i -le $Repeat; $i++) {
  $results += (Invoke-ProbeRun $i)
  if ($i -lt $Repeat) { Log ("  pausing $PauseSec s before the next run..."); Start-Sleep -Seconds $PauseSec }
}

Section "SESSION SUMMARY"
Log ("  {0,-5} {1,-10} {2,12} {3,14}  {4}" -f "iter","outcome","elapsed_s","sent_KB","detail")
foreach ($r in $results) { Log ("  {0,-5} {1,-10} {2,12:F3} {3,14:N1}  {4}" -f $r.Iter, $r.Outcome, $r.Seconds, ($r.Bytes/1KB), $r.Detail) }

$fails = @($results | Where-Object { $_.Outcome -eq "CONNECT-FAILED" })
if ($fails.Count -gt 0) {
  Log ""
  Log ("  NOTE: {0} run(s) never established a connection at all. Those are reachability," -f $fails.Count)
  Log  "        TLS or proxy-auth failures, not timeouts, and prove nothing about timeouts."
}

$drops = @($results | Where-Object { $_.Outcome -eq "DROPPED" })
if ($drops.Count -ge 2) {
  $mn = ($drops | Measure-Object Seconds -Minimum).Minimum
  $mx = ($drops | Measure-Object Seconds -Maximum).Maximum
  $av = ($drops | Measure-Object Seconds -Average).Average
  Log ""
  Log ("  {0} of {1} runs were cut off." -f $drops.Count, $results.Count)
  Log ("  cutoff min {0:F3}s / avg {1:F3}s / max {2:F3}s / spread {3:F3}s" -f $mn, $av, $mx, ($mx-$mn))
  if ($av -lt 5.0) {
    Log "  >> These cutoffs happened almost immediately, so they are connection failures"
    Log "  >> rather than a timeout. Resolve those before drawing timeout conclusions."
  } elseif (($mx - $mn) -lt ([math]::Max(5.0, $av * 0.05))) {
    Log "  >> The cutoff is TIGHTLY CLUSTERED. Random loss does not do that; a configured"
    Log "  >> timeout does. The server never closed and has no timeout, so the limit is"
    Log "  >> being enforced by a device in the middle."
  } else {
    Log "  >> The cutoffs are spread out rather than clustered, which looks more like"
    Log "  >> instability than a fixed configured timeout. Consider more runs."
  }
} elseif (@($results | Where-Object { $_.Outcome -eq "COMPLETED" }).Count -eq $results.Count) {
  Log ("  All runs completed the full {0} minutes with no cutoff on this path." -f $Minutes)
}
Log ""
Log ("  detailed log : $logPath")
Log ("  samples csv  : $samplesPath")
Log ("  summary csv  : $summaryPath")
Log ("#################### end session $RunId ####################")