Skip to content

Commit e8fb8e8

Browse files
authored
fix: prevent stdout/stderr race (#19)
1 parent 6577560 commit e8fb8e8

2 files changed

Lines changed: 90 additions & 25 deletions

File tree

src/proxy.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,13 @@ pub async fn run(args: ProxyArgs) -> Result<()> {
138138
local_port, pod_name, remote_port
139139
);
140140

141-
let stream = TcpStream::connect(("127.0.0.1", local_port))
141+
let pump_result = match TcpStream::connect(("127.0.0.1", local_port))
142142
.await
143-
.context("failed to connect to forwarded sshd port")?;
144-
145-
let pump_result = proxy_io::pump(stream).await;
143+
.context("failed to connect to forwarded sshd port")
144+
{
145+
Ok(stream) => proxy_io::pump(stream).await,
146+
Err(err) => Err(err),
147+
};
146148
let stop_result = forward.stop().await;
147149

148150
pump_result?;

src/proxy_io.rs

Lines changed: 84 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,94 @@
11
use anyhow::Result;
2-
use tokio::io::{self, AsyncWriteExt};
2+
use log::debug;
3+
use tokio::io::{self, AsyncRead, AsyncWrite, AsyncWriteExt};
34
use tokio::net::TcpStream;
45

56
pub async fn pump(stream: TcpStream) -> Result<()> {
6-
let (mut reader, mut writer) = stream.into_split();
7-
let mut stdin = io::stdin();
8-
let mut stdout = io::stdout();
7+
let (reader, writer) = stream.into_split();
8+
pump_io(io::stdin(), io::stdout(), reader, writer).await
9+
}
910

10-
let to_remote = tokio::spawn(async move {
11-
let copied = tokio::io::copy(&mut stdin, &mut writer).await?;
12-
writer.shutdown().await?;
11+
async fn pump_io<I, O, R, W>(
12+
mut input: I,
13+
mut output: O,
14+
mut remote_reader: R,
15+
mut remote_writer: W,
16+
) -> Result<()>
17+
where
18+
I: AsyncRead + Unpin,
19+
O: AsyncWrite + Unpin,
20+
R: AsyncRead + Unpin,
21+
W: AsyncWrite + Unpin,
22+
{
23+
let to_remote = async {
24+
let copied = tokio::io::copy(&mut input, &mut remote_writer).await?;
25+
remote_writer.shutdown().await?;
1326
Ok::<_, anyhow::Error>(copied)
14-
});
27+
};
1528

16-
let from_remote = tokio::spawn(async move {
17-
let copied = tokio::io::copy(&mut reader, &mut stdout).await?;
18-
stdout.flush().await?;
29+
let from_remote = async {
30+
let copied = tokio::io::copy(&mut remote_reader, &mut output).await?;
31+
output.flush().await?;
1932
Ok::<_, anyhow::Error>(copied)
20-
});
21-
22-
let (a, b) = tokio::join!(to_remote, from_remote);
23-
let to_bytes = a??;
24-
let from_bytes = b??;
25-
// Debug logging kept compact
26-
eprintln!(
27-
"[sshpod][proxy_io] bytes_to_remote={} bytes_from_remote={}",
28-
to_bytes, from_bytes
29-
);
33+
};
34+
35+
tokio::pin!(to_remote);
36+
tokio::pin!(from_remote);
37+
38+
tokio::select! {
39+
result = &mut to_remote => {
40+
let bytes = result?;
41+
debug!("[sshpod][proxy_io] bytes_to_remote={}", bytes);
42+
}
43+
result = &mut from_remote => {
44+
let bytes = result?;
45+
debug!("[sshpod][proxy_io] bytes_from_remote={}", bytes);
46+
}
47+
}
48+
3049
Ok(())
3150
}
51+
52+
#[cfg(test)]
53+
mod tests {
54+
use super::pump_io;
55+
use tokio::io::{self, AsyncReadExt};
56+
use tokio::net::{TcpListener, TcpStream};
57+
use tokio::time::{timeout, Duration};
58+
59+
#[tokio::test]
60+
async fn remote_eof_finishes_even_when_local_input_stays_open() {
61+
let (_local_writer, local_reader) = io::duplex(64);
62+
63+
let result = timeout(
64+
Duration::from_millis(100),
65+
pump_io(local_reader, io::sink(), io::empty(), io::sink()),
66+
)
67+
.await;
68+
69+
assert!(result.is_ok(), "pump_io should not wait for local input");
70+
result.unwrap().unwrap();
71+
}
72+
73+
#[tokio::test]
74+
async fn local_eof_shuts_down_tcp_write_half() {
75+
let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
76+
let addr = listener.local_addr().unwrap();
77+
78+
let client = tokio::spawn(TcpStream::connect(addr));
79+
let (mut server, _) = listener.accept().await.unwrap();
80+
let client = client.await.unwrap().unwrap();
81+
let (remote_reader, remote_writer) = client.into_split();
82+
83+
pump_io(io::empty(), io::sink(), remote_reader, remote_writer)
84+
.await
85+
.unwrap();
86+
87+
let mut buf = [0; 1];
88+
let read = timeout(Duration::from_millis(100), server.read(&mut buf))
89+
.await
90+
.unwrap()
91+
.unwrap();
92+
assert_eq!(read, 0);
93+
}
94+
}

0 commit comments

Comments
 (0)