Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
15 changes: 13 additions & 2 deletions crates/yaak-proxy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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),
Expand Down
45 changes: 39 additions & 6 deletions crates/yaak-proxy/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
43 changes: 43 additions & 0 deletions crates/yaak-proxy/tests/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
Loading