mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-11-13 12:13:42 +01:00
Fix proxy passthrough on Windows (#779)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1248112fe0
commit
349e6f9197
4 files changed
+121
-8
No files matched your search
@@ -35,3 +35,29 @@ jobs:
|
||||
run: |
|
||||
cargo build -p yaak-cli
|
||||
node plugins/importer-openapi/tests/roundtrip.mjs
|
||||
|
||||
test-windows:
|
||||
name: Rust Tests (Windows)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: voidzero-dev/setup-vp@v1
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: true
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: ci
|
||||
cache-on-failure: true
|
||||
- name: Install Protoc for plugin-runtime
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- run: vp install
|
||||
- run: npm run bootstrap
|
||||
env:
|
||||
YAAK_TARGET_ARCH: x64
|
||||
- name: Run Rust Tests
|
||||
run: cargo test --all --exclude yaak-cli --features yaak-app-client/wry
|
||||
@@ -133,6 +133,7 @@ pub struct ProxyHandle {
|
||||
shutdown_tx: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
thread_handle: Option<std::thread::JoinHandle<()>>,
|
||||
event_rx: Option<std_mpsc::Receiver<ProxyEvent>>,
|
||||
engine: Arc<Engine>,
|
||||
/// The bound port.
|
||||
pub port: u16,
|
||||
}
|
||||
@@ -142,6 +143,14 @@ impl ProxyHandle {
|
||||
pub fn take_event_rx(&mut self) -> Option<std_mpsc::Receiver<ProxyEvent>> {
|
||||
self.event_rx.take()
|
||||
}
|
||||
|
||||
/// Handshake failures counted toward passing `addr` (`host:port`) through, and whether it
|
||||
/// already is. For tests that must wait for the engine to record a failure.
|
||||
#[doc(hidden)]
|
||||
pub fn handshake_state(&self, addr: &str) -> (u32, bool) {
|
||||
let failures = self.engine.handshake_failures.lock().unwrap().get(addr).copied();
|
||||
(failures.unwrap_or_default(), self.engine.is_passthrough(addr))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ProxyHandle {
|
||||
@@ -180,8 +189,8 @@ impl Engine {
|
||||
self.passthrough.lock().unwrap().contains(addr)
|
||||
}
|
||||
|
||||
/// Returns whether `addr` is now passed through. macOS clients reject the CA with a bare
|
||||
/// reset, indistinguishable from an abandoned handshake, so those need two in a row.
|
||||
/// Returns whether `addr` is now passed through. macOS and Windows clients reject the CA with
|
||||
/// a bare hang-up, indistinguishable from an abandoned connection, so those need two in a row.
|
||||
fn record_handshake_failure(&self, addr: &str, certificate_rejected: bool) -> bool {
|
||||
let mut failures = self.handshake_failures.lock().unwrap();
|
||||
let count = failures.entry(addr.to_string()).or_default();
|
||||
@@ -237,6 +246,7 @@ pub fn start_proxy(config: ProxyConfig) -> Result<ProxyHandle, String> {
|
||||
});
|
||||
|
||||
let port = config.port;
|
||||
let handle_engine = engine.clone();
|
||||
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
|
||||
let (ready_tx, ready_rx) = std_mpsc::channel();
|
||||
|
||||
@@ -286,6 +296,7 @@ pub fn start_proxy(config: ProxyConfig) -> Result<ProxyHandle, String> {
|
||||
shutdown_tx: Some(shutdown_tx),
|
||||
thread_handle: Some(thread_handle),
|
||||
event_rx: Some(event_rx),
|
||||
engine: handle_engine,
|
||||
port,
|
||||
}),
|
||||
Ok(Err(e)) => Err(e),
|
||||
|
||||
@@ -219,24 +219,57 @@ where
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| target.server_name.clone());
|
||||
let config = engine.ca.server_config(&server_name)?;
|
||||
let tls = match start.into_stream(config).await {
|
||||
let mut tls = match start.into_stream(config).await {
|
||||
Ok(tls) => tls,
|
||||
Err(e) => {
|
||||
if engine.record_handshake_failure(&target.addr, is_certificate_rejection(&e)) {
|
||||
info!("Client rejected the proxy for {}, passing it through: {e}", target.addr);
|
||||
} else {
|
||||
debug!("TLS handshake with client failed for {}: {e}", target.addr);
|
||||
}
|
||||
record_handshake_failure(&engine, &target.addr, is_certificate_rejection(&e), &e);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// SChannel (native-tls on Windows) checks the certificate only after sending its Finished,
|
||||
// so it rejects the proxy by hanging up on a completed handshake. Only a request counts as
|
||||
// success.
|
||||
let mut buf = vec![0u8; 4096];
|
||||
let n = match tls.read(&mut buf).await {
|
||||
Ok(n) => n,
|
||||
Err(e) if is_hang_up(&e) => {
|
||||
record_handshake_failure(&engine, &target.addr, false, &e);
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
if n == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
buf.truncate(n);
|
||||
engine.record_handshake_success(&target.addr);
|
||||
|
||||
let tls = Rewind::new(Bytes::from(buf), tls);
|
||||
let target = Arc::new(Target { server_name, ..target });
|
||||
serve_http1(tls, move |req| forward_https(req, target.clone(), engine.clone())).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn record_handshake_failure(
|
||||
engine: &Engine,
|
||||
addr: &str,
|
||||
certificate_rejected: bool,
|
||||
e: &std::io::Error,
|
||||
) {
|
||||
if engine.record_handshake_failure(addr, certificate_rejected) {
|
||||
info!("Client rejected the proxy for {addr}, passing it through: {e}");
|
||||
} else {
|
||||
debug!("TLS handshake with client failed for {addr}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The client closed the connection without a TLS `close_notify`, or reset it.
|
||||
fn is_hang_up(e: &std::io::Error) -> bool {
|
||||
use std::io::ErrorKind::{ConnectionAborted, ConnectionReset, UnexpectedEof};
|
||||
matches!(e.kind(), UnexpectedEof | ConnectionReset | ConnectionAborted)
|
||||
}
|
||||
|
||||
fn is_certificate_rejection(e: &std::io::Error) -> bool {
|
||||
use rustls::AlertDescription::{
|
||||
BadCertificate, CertificateExpired, CertificateRevoked, CertificateUnknown, UnknownCA,
|
||||
|
||||
@@ -203,6 +203,19 @@ impl Harness {
|
||||
Arc::new(config)
|
||||
}
|
||||
|
||||
/// Waits until the engine reports `state` (failures counted, passed through) for `addr`.
|
||||
async fn wait_for_handshake_state(&self, addr: &str, state: (u32, bool)) {
|
||||
let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
|
||||
while self.proxy.handshake_state(addr) != state {
|
||||
assert!(
|
||||
tokio::time::Instant::now() < deadline,
|
||||
"{addr} stayed at {:?}, expected {state:?}",
|
||||
self.proxy.handshake_state(addr)
|
||||
);
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Events of the next flow, through its terminal event plus any stragglers.
|
||||
fn flow_events(&self) -> Vec<ProxyEvent> {
|
||||
let mut events = Vec::new();
|
||||
@@ -464,6 +477,36 @@ async fn certificate_alert_passes_through_on_the_next_connection() {
|
||||
.expect("one certificate alert should be enough to pass the host through");
|
||||
}
|
||||
|
||||
/// SChannel (native-tls on Windows) checks the certificate only after sending its Finished, so
|
||||
/// the proxy sees a completed handshake and then a hang-up instead of an alert.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn hang_up_after_the_handshake_passes_through() {
|
||||
let h = Harness::start(1024);
|
||||
let upstream = spawn_upstream(Some(h.pki.server.clone())).await;
|
||||
let port = upstream.port();
|
||||
let name = ServerName::try_from("localhost").unwrap();
|
||||
|
||||
let addr = format!("localhost:{port}");
|
||||
let hangs_up = TlsConnector::from(h.tls_client_config());
|
||||
for state in [(1, false), (0, true)] {
|
||||
let tls = hangs_up.connect(name.clone(), h.connect_tunnel(port).await).await.unwrap();
|
||||
drop(tls);
|
||||
h.wait_for_handshake_state(&addr, state).await;
|
||||
}
|
||||
|
||||
let mut roots = RootCertStore::empty();
|
||||
roots.add(h.pki.ca_der.clone()).unwrap();
|
||||
let config = ClientConfig::builder_with_provider(ring())
|
||||
.with_safe_default_protocol_versions()
|
||||
.unwrap()
|
||||
.with_root_certificates(roots)
|
||||
.with_no_client_auth();
|
||||
TlsConnector::from(Arc::new(config))
|
||||
.connect(name, h.connect_tunnel(port).await)
|
||||
.await
|
||||
.expect("two hang-ups after the handshake should pass the host through");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn abandoned_handshake_does_not_disable_capture() {
|
||||
let h = Harness::start(1024);
|
||||
|
||||
Reference in new issue
Block a user