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

This commit is contained in:
ospab 2026-05-15 19:23:50 +03:00
parent 5ac59c92ea
commit a3c8b3a750
6 changed files with 81 additions and 19 deletions

View File

@ -92,14 +92,13 @@ jobs:
target: mipsel-unknown-linux-musl target: mipsel-unknown-linux-musl
artifact_name: ostp artifact_name: ostp
release_name: ostp-linux-mipsle.tar.gz release_name: ostp-linux-mipsle.tar.gz
tun2socks_arch: linux-mipsle tun2socks_arch: linux-mipsle-softfloat
use_cross: true use_cross: true
toolchain: nightly toolchain: nightly
- os: ubuntu-latest - os: ubuntu-latest
target: riscv64gc-unknown-linux-gnu target: riscv64gc-unknown-linux-gnu
artifact_name: ostp artifact_name: ostp
release_name: ostp-linux-riscv64.tar.gz release_name: ostp-linux-riscv64.tar.gz
tun2socks_arch: linux-riscv64
use_cross: true use_cross: true
# ========================================== # ==========================================
@ -162,7 +161,7 @@ jobs:
# 1. Acquire tun2socks # 1. Acquire tun2socks
Invoke-WebRequest -Uri "https://github.com/xjasonlyu/tun2socks/releases/download/v2.6.0/tun2socks-${{ matrix.tun2socks_arch }}.zip" -OutFile "tun2socks.zip" 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 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 # 2. Acquire wintun
Invoke-WebRequest -Uri "https://www.wintun.net/builds/wintun-0.14.1.zip" -OutFile "wintun.zip" 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 Expand-Archive -Path "wintun.zip" -DestinationPath "wintun_temp" -Force
@ -177,7 +176,7 @@ jobs:
cd target/${{ matrix.target }}/release cd target/${{ matrix.target }}/release
# All platforms in tun2socks v2.6.0 use .zip packaging # 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" 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" unzip -o "tun2socks.zip"
find . -maxdepth 2 -name "tun2socks*" ! -name "*.zip" -type f -exec mv {} ./tun2socks \; find . -maxdepth 2 -name "tun2socks*" ! -name "*.zip" -type f -exec mv {} ./tun2socks \;
rm -f "tun2socks.zip" rm -f "tun2socks.zip"

View File

@ -21,6 +21,18 @@ pub struct BridgeMetrics {
pub bytes_recv: AtomicU64, pub bytes_recv: AtomicU64,
} }
async fn send_datagram(socket: &UdpSocket, frame: &Bytes, turn_enabled: bool) -> std::io::Result<usize> {
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 { struct SessionState {
socket: Arc<UdpSocket>, socket: Arc<UdpSocket>,
machine: ProtocolMachine, machine: ProtocolMachine,
@ -144,12 +156,22 @@ impl Bridge {
let socket = Arc::new(sock); let socket = Arc::new(sock);
let socket_clone = socket.clone(); let socket_clone = socket.clone();
let udp_tx_clone = udp_tx.clone(); let udp_tx_clone = udp_tx.clone();
let is_turn = self.turn_enabled;
tokio::spawn(async move { tokio::spawn(async move {
let mut buf = vec![0_u8; 65535]; let mut buf = vec![0_u8; 65535];
loop { loop {
match socket_clone.recv(&mut buf).await { match socket_clone.recv(&mut buf).await {
Ok(n) => { 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() { if udp_tx_clone.send((idx, inbound)).await.is_err() {
break; break;
} }
@ -274,7 +296,7 @@ impl Bridge {
} }
} }
ProtocolAction::SendDatagram(frame) => { 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); 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()); let out_payload = Bytes::from(relay_msg.encode());
match session.machine.on_event(OstpEvent::Outbound(stream_id, out_payload)) { match session.machine.on_event(OstpEvent::Outbound(stream_id, out_payload)) {
Ok(ProtocolAction::SendDatagram(frame)) => { 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); self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed);
if self.debug { if self.debug {
let _ = tx.send(UiEvent::Log(format!( let _ = tx.send(UiEvent::Log(format!(
@ -333,7 +355,7 @@ impl Bridge {
let mut sent = 0usize; let mut sent = 0usize;
for item in list { for item in list {
if let ProtocolAction::SendDatagram(frame) = item { 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); self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed);
sent += 1; sent += 1;
} }
@ -436,7 +458,7 @@ impl Bridge {
} }
} }
ProtocolAction::SendDatagram(frame) => { 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); self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed);
} }
_ => {} _ => {}
@ -568,7 +590,7 @@ impl Bridge {
ProtocolAction::SendDatagram(frame) => frame, ProtocolAction::SendDatagram(frame) => frame,
_ => anyhow::bail!("protocol did not emit handshake datagram"), _ => 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); self.metrics.bytes_sent.fetch_add(handshake_frame.len() as u64, Ordering::Relaxed);
let mut buf = vec![0_u8; 4096]; let mut buf = vec![0_u8; 4096];
@ -580,7 +602,16 @@ impl Bridge {
.context("handshake timeout waiting server response")??; .context("handshake timeout waiting server response")??;
self.metrics.bytes_recv.fetch_add(size as u64, Ordering::Relaxed); 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))?; machine.on_event(OstpEvent::Inbound(inbound))?;
let rtt_ms = start.elapsed().as_secs_f64() * 1000.0; let rtt_ms = start.elapsed().as_secs_f64() * 1000.0;

View File

@ -61,10 +61,37 @@ pub async fn run_linux_tunnel(
if in_path { if in_path {
tun2socks_exe = std::path::PathBuf::from("tun2socks"); tun2socks_exe = std::path::PathBuf::from("tun2socks");
} else { } 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 // 2. Resolve Server IP for routing table exclusion
let server_ip = config.ostp.server_addr.to_socket_addrs() let server_ip = config.ostp.server_addr.to_socket_addrs()
.map_err(|e| anyhow!("Failed to resolve remote server IP: {}", e))? .map_err(|e| anyhow!("Failed to resolve remote server IP: {}", e))?

View File

@ -41,7 +41,13 @@ pub async fn run_wintun_tunnel(
let tun2socks_exe = dir.join("tun2socks.exe"); let tun2socks_exe = dir.join("tun2socks.exe");
if !tun2socks_exe.exists() { 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 // 2. Resolve Server IP for routing table exclusion

View File

@ -35,7 +35,6 @@ impl TryFrom<u8> for FrameKind {
pub struct FrameHeader { pub struct FrameHeader {
pub version: u8, pub version: u8,
pub kind: FrameKind, pub kind: FrameKind,
pub flags: u8,
pub stream_id: u16, pub stream_id: u16,
pub payload_len: u32, pub payload_len: u32,
pub pad_len: u16, pub pad_len: u16,
@ -45,8 +44,7 @@ impl FrameHeader {
pub fn encode(&self, out: &mut BytesMut) { pub fn encode(&self, out: &mut BytesMut) {
out.put_u8(self.version); out.put_u8(self.version);
out.put_u8(self.kind as u8); out.put_u8(self.kind as u8);
out.put_u8(self.flags); out.put_u16(0); // 2 reserved bytes
out.put_u8(0); // reserved
out.put_u16(self.stream_id); out.put_u16(self.stream_id);
out.put_u32(self.payload_len); out.put_u32(self.payload_len);
out.put_u16(self.pad_len); out.put_u16(self.pad_len);
@ -59,7 +57,7 @@ impl FrameHeader {
let version = buf[0]; let version = buf[0];
let kind = FrameKind::try_from(buf[1])?; 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 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 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]]); let pad_len = u16::from_be_bytes([buf[10], buf[11]]);
@ -67,7 +65,6 @@ impl FrameHeader {
Ok(Self { Ok(Self {
version, version,
kind, kind,
flags,
stream_id, stream_id,
payload_len, payload_len,
pad_len, pad_len,

View File

@ -294,6 +294,7 @@ impl ProtocolMachine {
if nonce == self.expected_recv_nonce { if nonce == self.expected_recv_nonce {
app_actions.push(action); app_actions.push(action);
self.expected_recv_nonce = self.expected_recv_nonce.checked_add(1).ok_or_else(|| { 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()) 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) { while let Some(buffered_action) = self.reorder_buffer.remove(&self.expected_recv_nonce) {
app_actions.push(buffered_action); app_actions.push(buffered_action);
self.expected_recv_nonce = self.expected_recv_nonce.checked_add(1).ok_or_else(|| { 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()) ProtocolError::Crypto("recv nonce sequence exhausted".to_string())
})?; })?;
} }
@ -359,7 +361,6 @@ impl ProtocolMachine {
let header = FrameHeader { let header = FrameHeader {
version: 1, version: 1,
kind, kind,
flags: 0,
stream_id, stream_id,
payload_len: payload.len() as u32, payload_len: payload.len() as u32,
pad_len: padding.len() as u16, pad_len: padding.len() as u16,
@ -379,6 +380,7 @@ impl ProtocolMachine {
let nonce = self.send_nonce; let nonce = self.send_nonce;
self.send_nonce = self.send_nonce.checked_add(1).ok_or_else(|| { 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()) ProtocolError::Crypto("send nonce sequence exhausted".to_string())
})?; })?;