Skip to content

Deliver server data that arrives with the proxy handshake reply - #280

Open
disarray09 wants to merge 1 commit into
tun2proxy:masterfrom
Aether-ProjectX:fix/early-server-data
Open

disarray09 wants to merge 1 commit into
tun2proxy:masterfrom
Aether-ProjectX:fix/early-server-data

Conversation

@disarray09

@disarray09 disarray09 commented Nov 8, 2026 •

Copy link
Copy Markdown

Problem

A destination that speaks first — SSH, SMTP, FTP, POP3, IMAP, MySQL — can lose up to one read's worth of its first bytes.

handle_proxy_session reads the proxy's handshake reply into a 4096-byte buffer and stops as soon as the handler reports the connection established. If the destination's first bytes arrive in the same read as the SOCKS5 reply (or the HTTP 200 to CONNECT), they are still in the handler at that point:

  • SocksProxyHandler has parsed the reply and switched to Established, but only moves server_inbuf to client_outbuf (relay_traffic) on its next push_data, which never comes.
  • HttpConnection has moved them to client_outbuf, but nothing reads the client-bound buffer after the handshake.

handle_tcp_session then starts the copy loops, and those bytes are never written to the client. The connection stays open, so the client sees a stream with its first bytes missing — e.g. an SSH client that never receives the server's version banner.

Whether it happens depends on how the reply and the first data coalesce, so it is intermittent. Behind a local SOCKS5 server on loopback between 0 and 3 of every 10 connections to a server-first target lost 1450 or 4086 bytes. Client-first protocols (HTTP, TLS) are not affected, because the server has nothing to send before the client speaks.

Fix

After handle_proxy_session succeeds, take_early_server_data pushes one empty FromServer event so the handler runs its established step, then drains ToClient. handle_tcp_session writes those bytes to the client before the copy loops start. It is a no-op when nothing followed the reply.

Tests

New early_server_data_tests in src/lib.rs:

  • socks5_data_read_with_the_reply_is_kept — SOCKS5 reply and an SSH banner in one read; the banner is returned exactly once. Fails without the fix.
  • http_connect_data_read_with_the_response_is_kept — 200 Connection established followed by an SMTP greeting in one read.
  • a_bare_reply_leaves_nothing_behind — no extra data, nothing returned.

cargo test --lib (18 passed), cargo clippy --lib --tests -- -D warnings and rustfmt --check are clean. We also run this change in a downstream project's end-to-end tests (fake TUN with a real TCP stack, real SOCKS5 server): 0 bytes lost in 30 server-first connections, against 10–30% before.

🤖 Generated with Claude Code

A destination that speaks first (SSH, SMTP, FTP, IMAP, MySQL) can have its
first bytes arrive in the same read as the SOCKS5 reply or the HTTP
CONNECT response. handle_proxy_session stops as soon as the handler
reports the connection established; at that point those bytes are still
in the handler's buffers (SOCKS5 has not yet moved them from its server
input to its client output, HTTP has moved them but nothing reads the
client output), and they were never written to the client. The client
saw a stream with its first bytes missing.

After the handshake, run the handler's established step once with an
empty server event and write whatever is bound for the client before
the copy loops start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Copilot was unable to run its full agentic suite in this review.

Copilot review overview

3 open issues
What changed in this PR

This PR ensures any “early” server bytes that arrive in the same read as the proxy handshake reply are forwarded to the client before the main relay loop begins.

Changes:

  • Clone the proxy handler so it can be reused after handle_proxy_session.
  • Add take_early_server_data to drain buffered server→client bytes left behind after handshake establishment.
  • Add Tokio tests covering SOCKS5 and HTTP CONNECT scenarios where early server data piggybacks on the handshake response.
File Description
src/​lib.rs Flushes and forwards early server data post-handshake; adds targeted async tests validating the behavior.

Review effort: Lite

Comment thread src/lib.rs
Comment on lines +1029 to +1042
/// `handle_proxy_session` stops as soon as the handler reports the connection
/// established. Any bytes that followed the reply in the same read are left in
/// the handler's server-side input buffer, because the handler only moves that
/// buffer to its client-bound output on its next `push_data`. Pushing an empty
/// server event runs that step once, and the client-bound output is drained.
async fn take_early_server_data(proxy_handler: &Arc<Mutex<dyn ProxyHandler>>) -> crate::Result<Vec<u8>> {
let mut handler = proxy_handler.lock().await;
let event = IncomingDataEvent {
direction: IncomingDirection::FromServer,
buffer: &[],
};
handler.push_data(event).await?;
let data = handler.peek_data(OutgoingDirection::ToClient).buffer.to_vec();
handler.consume_data(OutgoingDirection::ToClient, data.len());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please review this suggestion. @disarray09

Comment thread src/lib.rs
Comment on lines +1034 to +1042
async fn take_early_server_data(proxy_handler: &Arc<Mutex<dyn ProxyHandler>>) -> crate::Result<Vec<u8>> {
let mut handler = proxy_handler.lock().await;
let event = IncomingDataEvent {
direction: IncomingDirection::FromServer,
buffer: &[],
};
handler.push_data(event).await?;
let data = handler.peek_data(OutgoingDirection::ToClient).buffer.to_vec();
handler.consume_data(OutgoingDirection::ToClient, data.len());
Comment thread src/lib.rs
Comment on lines +1041 to +1042
let data = handler.peek_data(OutgoingDirection::ToClient).buffer.to_vec();
handler.consume_data(OutgoingDirection::ToClient, data.len());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants