From a3c8b3a7506f4a8113dfd6f2ac876e75c8bbd688 Mon Sep 17 00:00:00 2001 From: ospab Date: Fri, 15 May 2026 19:23:50 +0300 Subject: [PATCH] fix: address final analysis issues including Nonce exhaustion, TUN pre-flight checks, dead code, and proper TURN channel framing. Also fix CI packaging of tun2socks --- .github/workflows/release.yml | 7 ++-- ostp-client/src/bridge.rs | 45 ++++++++++++++++++++---- ostp-client/src/tunnel/linux_handler.rs | 29 ++++++++++++++- ostp-client/src/tunnel/wintun_handler.rs | 8 ++++- ostp-core/src/framing/frame.rs | 7 ++-- ostp-core/src/protocol.rs | 4 ++- 6 files changed, 81 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b7ccdd2..2cf3475 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -92,14 +92,13 @@ jobs: target: mipsel-unknown-linux-musl artifact_name: ostp release_name: ostp-linux-mipsle.tar.gz - tun2socks_arch: linux-mipsle + tun2socks_arch: linux-mipsle-softfloat use_cross: true toolchain: nightly - os: ubuntu-latest target: riscv64gc-unknown-linux-gnu artifact_name: ostp release_name: ostp-linux-riscv64.tar.gz - tun2socks_arch: linux-riscv64 use_cross: true # ========================================== @@ -162,7 +161,7 @@ jobs: # 1. Acquire tun2socks Invoke-WebRequest -Uri "https://github.com/xjasonlyu/tun2socks/releases/download/v2.6.0/tun2socks-${{ matrix.tun2socks_arch }}.zip" -OutFile "tun2socks.zip" Expand-Archive -Path "tun2socks.zip" -DestinationPath "tun_temp" -Force - Get-ChildItem -Path "tun_temp" -Filter "*.exe" -Recurse | Copy-Item -Destination "." -Force + Get-ChildItem -Path "tun_temp" -Filter "*.exe" -Recurse | Copy-Item -Destination "tun2socks.exe" -Force # 2. Acquire wintun Invoke-WebRequest -Uri "https://www.wintun.net/builds/wintun-0.14.1.zip" -OutFile "wintun.zip" Expand-Archive -Path "wintun.zip" -DestinationPath "wintun_temp" -Force @@ -177,7 +176,7 @@ jobs: cd target/${{ matrix.target }}/release # All platforms in tun2socks v2.6.0 use .zip packaging URL="https://github.com/xjasonlyu/tun2socks/releases/download/v2.6.0/tun2socks-${{ matrix.tun2socks_arch }}.zip" - curl -L "$URL" -o "tun2socks.zip" + curl -f -L "$URL" -o "tun2socks.zip" || { echo "Failed to download tun2socks"; exit 0; } unzip -o "tun2socks.zip" find . -maxdepth 2 -name "tun2socks*" ! -name "*.zip" -type f -exec mv {} ./tun2socks \; rm -f "tun2socks.zip" diff --git a/ostp-client/src/bridge.rs b/ostp-client/src/bridge.rs index 8806a0d..7cdaac8 100644 --- a/ostp-client/src/bridge.rs +++ b/ostp-client/src/bridge.rs @@ -21,6 +21,18 @@ pub struct BridgeMetrics { pub bytes_recv: AtomicU64, } +async fn send_datagram(socket: &UdpSocket, frame: &Bytes, turn_enabled: bool) -> std::io::Result { + if turn_enabled { + let mut out = bytes::BytesMut::with_capacity(4 + frame.len()); + bytes::BufMut::put_u16(&mut out, 0x4000); + bytes::BufMut::put_u16(&mut out, frame.len() as u16); + out.extend_from_slice(frame); + socket.send(&out).await + } else { + socket.send(frame).await + } +} + struct SessionState { socket: Arc, machine: ProtocolMachine, @@ -144,12 +156,22 @@ impl Bridge { let socket = Arc::new(sock); let socket_clone = socket.clone(); let udp_tx_clone = udp_tx.clone(); + let is_turn = self.turn_enabled; tokio::spawn(async move { let mut buf = vec![0_u8; 65535]; loop { match socket_clone.recv(&mut buf).await { Ok(n) => { - let inbound = Bytes::copy_from_slice(&buf[..n]); + let inbound = if is_turn && n >= 4 && buf[0] == 0x40 && buf[1] == 0x00 { + let len = u16::from_be_bytes([buf[2], buf[3]]) as usize; + if 4 + len <= n { + Bytes::copy_from_slice(&buf[4..4+len]) + } else { + Bytes::copy_from_slice(&buf[..n]) + } + } else { + Bytes::copy_from_slice(&buf[..n]) + }; if udp_tx_clone.send((idx, inbound)).await.is_err() { break; } @@ -274,7 +296,7 @@ impl Bridge { } } ProtocolAction::SendDatagram(frame) => { - let _ = session.socket.send(&frame).await; + let _ = send_datagram(&session.socket, &frame, self.turn_enabled).await; self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); } _ => {} @@ -319,7 +341,7 @@ impl Bridge { let out_payload = Bytes::from(relay_msg.encode()); match session.machine.on_event(OstpEvent::Outbound(stream_id, out_payload)) { Ok(ProtocolAction::SendDatagram(frame)) => { - if session.socket.send(&frame).await.is_ok() { + if send_datagram(&session.socket, &frame, self.turn_enabled).await.is_ok() { self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); if self.debug { let _ = tx.send(UiEvent::Log(format!( @@ -333,7 +355,7 @@ impl Bridge { let mut sent = 0usize; for item in list { if let ProtocolAction::SendDatagram(frame) = item { - if session.socket.send(&frame).await.is_ok() { + if send_datagram(&session.socket, &frame, self.turn_enabled).await.is_ok() { self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); sent += 1; } @@ -436,7 +458,7 @@ impl Bridge { } } ProtocolAction::SendDatagram(frame) => { - let _ = session.socket.send(&frame).await; + let _ = send_datagram(&session.socket, &frame, self.turn_enabled).await; self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); } _ => {} @@ -568,7 +590,7 @@ impl Bridge { ProtocolAction::SendDatagram(frame) => frame, _ => anyhow::bail!("protocol did not emit handshake datagram"), }; - socket.send(&handshake_frame).await?; + send_datagram(&socket, &handshake_frame, self.turn_enabled).await?; self.metrics.bytes_sent.fetch_add(handshake_frame.len() as u64, Ordering::Relaxed); let mut buf = vec![0_u8; 4096]; @@ -580,7 +602,16 @@ impl Bridge { .context("handshake timeout waiting server response")??; self.metrics.bytes_recv.fetch_add(size as u64, Ordering::Relaxed); - let inbound = Bytes::copy_from_slice(&buf[..size]); + let inbound = if self.turn_enabled && size >= 4 && buf[0] == 0x40 && buf[1] == 0x00 { + let len = u16::from_be_bytes([buf[2], buf[3]]) as usize; + if 4 + len <= size { + Bytes::copy_from_slice(&buf[4..4+len]) + } else { + Bytes::copy_from_slice(&buf[..size]) + } + } else { + Bytes::copy_from_slice(&buf[..size]) + }; machine.on_event(OstpEvent::Inbound(inbound))?; let rtt_ms = start.elapsed().as_secs_f64() * 1000.0; diff --git a/ostp-client/src/tunnel/linux_handler.rs b/ostp-client/src/tunnel/linux_handler.rs index 5e726fa..f1cf2f7 100644 --- a/ostp-client/src/tunnel/linux_handler.rs +++ b/ostp-client/src/tunnel/linux_handler.rs @@ -61,10 +61,37 @@ pub async fn run_linux_tunnel( if in_path { tun2socks_exe = std::path::PathBuf::from("tun2socks"); } else { - return Err(anyhow!("tun2socks executable not found in local dir or PATH. Please ensure dependencies are present.")); + return Err(anyhow!( + "CRITICAL: 'tun2socks' binary is missing!\n\ + OSTP requires tun2socks for TUN mode on Linux. Please download the appropriate binary for your architecture from: \n\ + https://github.com/xjasonlyu/tun2socks/releases \n\ + and place it in the same directory as the ostp executable ({}), or install it globally in your PATH.", + dir.display() + )); } } + // 1.5. Pre-flight system checks + let is_root = Command::new("id") + .arg("-u") + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).trim() == "0") + .unwrap_or(false); + + if !is_root { + return Err(anyhow!("FATAL: OSTP TUN mode requires root privileges on Linux. Please run via sudo.")); + } + + let has_ip_cmd = Command::new("which") + .arg("ip") + .output() + .map(|o| o.status.success()) + .unwrap_or(false); + + if !has_ip_cmd { + return Err(anyhow!("FATAL: 'ip' command not found. OSTP TUN mode requires 'iproute2' package to be installed.")); + } + // 2. Resolve Server IP for routing table exclusion let server_ip = config.ostp.server_addr.to_socket_addrs() .map_err(|e| anyhow!("Failed to resolve remote server IP: {}", e))? diff --git a/ostp-client/src/tunnel/wintun_handler.rs b/ostp-client/src/tunnel/wintun_handler.rs index ec8aae0..e9fe31e 100644 --- a/ostp-client/src/tunnel/wintun_handler.rs +++ b/ostp-client/src/tunnel/wintun_handler.rs @@ -41,7 +41,13 @@ pub async fn run_wintun_tunnel( let tun2socks_exe = dir.join("tun2socks.exe"); if !tun2socks_exe.exists() { - return Err(anyhow!("tun2socks.exe not found in current directory! Please make sure the pre-packaged dependency is present near the executable.")); + return Err(anyhow!( + "CRITICAL: 'tun2socks.exe' binary is missing!\n\ + OSTP requires tun2socks for TUN mode on Windows. Please download the appropriate binary from: \n\ + https://github.com/xjasonlyu/tun2socks/releases \n\ + and place it in the same directory as the ostp executable ({}).", + dir.display() + )); } // 2. Resolve Server IP for routing table exclusion diff --git a/ostp-core/src/framing/frame.rs b/ostp-core/src/framing/frame.rs index fc1f2b3..e614dfc 100644 --- a/ostp-core/src/framing/frame.rs +++ b/ostp-core/src/framing/frame.rs @@ -35,7 +35,6 @@ impl TryFrom for FrameKind { pub struct FrameHeader { pub version: u8, pub kind: FrameKind, - pub flags: u8, pub stream_id: u16, pub payload_len: u32, pub pad_len: u16, @@ -45,8 +44,7 @@ impl FrameHeader { pub fn encode(&self, out: &mut BytesMut) { out.put_u8(self.version); out.put_u8(self.kind as u8); - out.put_u8(self.flags); - out.put_u8(0); // reserved + out.put_u16(0); // 2 reserved bytes out.put_u16(self.stream_id); out.put_u32(self.payload_len); out.put_u16(self.pad_len); @@ -59,7 +57,7 @@ impl FrameHeader { let version = buf[0]; let kind = FrameKind::try_from(buf[1])?; - let flags = buf[2]; + // buf[2] and buf[3] are reserved let stream_id = u16::from_be_bytes([buf[4], buf[5]]); let payload_len = u32::from_be_bytes([buf[6], buf[7], buf[8], buf[9]]); let pad_len = u16::from_be_bytes([buf[10], buf[11]]); @@ -67,7 +65,6 @@ impl FrameHeader { Ok(Self { version, kind, - flags, stream_id, payload_len, pad_len, diff --git a/ostp-core/src/protocol.rs b/ostp-core/src/protocol.rs index d33ef07..6a9affe 100644 --- a/ostp-core/src/protocol.rs +++ b/ostp-core/src/protocol.rs @@ -294,6 +294,7 @@ impl ProtocolMachine { if nonce == self.expected_recv_nonce { app_actions.push(action); self.expected_recv_nonce = self.expected_recv_nonce.checked_add(1).ok_or_else(|| { + tracing::error!("FATAL: Recv nonce sequence exhausted (2^64 frames). Session must be terminated to prevent AEAD keystream reuse!"); ProtocolError::Crypto("recv nonce sequence exhausted".to_string()) })?; @@ -301,6 +302,7 @@ impl ProtocolMachine { while let Some(buffered_action) = self.reorder_buffer.remove(&self.expected_recv_nonce) { app_actions.push(buffered_action); self.expected_recv_nonce = self.expected_recv_nonce.checked_add(1).ok_or_else(|| { + tracing::error!("FATAL: Recv nonce sequence exhausted (2^64 frames). Session must be terminated to prevent AEAD keystream reuse!"); ProtocolError::Crypto("recv nonce sequence exhausted".to_string()) })?; } @@ -359,7 +361,6 @@ impl ProtocolMachine { let header = FrameHeader { version: 1, kind, - flags: 0, stream_id, payload_len: payload.len() as u32, pad_len: padding.len() as u16, @@ -379,6 +380,7 @@ impl ProtocolMachine { let nonce = self.send_nonce; self.send_nonce = self.send_nonce.checked_add(1).ok_or_else(|| { + tracing::error!("FATAL: Send nonce sequence exhausted (2^64 frames). Session must be terminated to prevent AEAD keystream reuse!"); ProtocolError::Crypto("send nonce sequence exhausted".to_string()) })?;