From f399b34ef49f99974421c56e6afc8df0df1419c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 02:29:57 +0000 Subject: [PATCH 1/2] Harden panic-prone security paths Co-authored-by: madeye <627917+madeye@users.noreply.github.com> --- src/args/mod.rs | 28 ++++++++++++++++++++-------- src/client.rs | 21 +++++++++++++-------- src/server.rs | 4 +++- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/args/mod.rs b/src/args/mod.rs index 0b078d2..2a15c69 100644 --- a/src/args/mod.rs +++ b/src/args/mod.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{anyhow, Result}; use std::collections::HashMap; use std::env::var; use std::net::{SocketAddr, ToSocketAddrs}; @@ -12,11 +12,11 @@ pub fn parse_env_addr() -> Result<(SocketAddr, SocketAddr)> { let ss_local_addr = format!("{}:{}", ss_local_host, ss_local_port) .to_socket_addrs()? .next() - .unwrap(); + .ok_or_else(|| anyhow!("resolved no local socket address"))?; let ss_remote_addr = format!("{}:{}", ss_remote_host, ss_remote_port) .to_socket_addrs()? .next() - .unwrap(); + .ok_or_else(|| anyhow!("resolved no remote socket address"))?; Ok((ss_local_addr, ss_remote_addr)) } @@ -39,12 +39,15 @@ pub fn parse_env_opts() -> Result> { fn parse_plugin_options(options: &str) -> HashMap { let mut plugin_options = HashMap::::new(); - let opts: Vec<&str> = options.split(';').collect(); - // FIXME: backslash is not escaped in this plugin - for opt in opts { - let o: Vec<&str> = opt.splitn(2, '=').collect(); - plugin_options.insert(o[0].to_string(), o[1].to_string()); + for opt in options.split(';').filter(|s| !s.is_empty()) { + let mut parts = opt.splitn(2, '='); + let Some(key) = parts.next() else { continue }; + if key.is_empty() { + continue; + } + let value = parts.next().unwrap_or_default(); + plugin_options.insert(key.to_string(), value.to_string()); } plugin_options @@ -84,6 +87,15 @@ mod tests { assert_eq!(opts.get("secret").unwrap(), "second"); } + #[test] + fn test_parse_plugin_options_malformed_is_ignored_or_empty() { + let opts = parse_plugin_options("host=example.com;flag_only;=no_key;empty="); + assert_eq!(opts.get("host").unwrap(), "example.com"); + assert_eq!(opts.get("flag_only").unwrap(), ""); + assert_eq!(opts.get("empty").unwrap(), ""); + assert!(opts.get("").is_none()); + } + #[test] #[serial] fn test_parse_env_addr_valid() { diff --git a/src/client.rs b/src/client.rs index 4972dff..2c3c3da 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,10 +4,10 @@ use std::sync::Arc; use quinn::crypto::rustls::QuicClientConfig; use tokio::net::{TcpListener, TcpStream}; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use clap::Parser; use futures::future::try_join; -use log::{error, info}; +use log::{error, info, warn}; use quinn::ConnectionError; use quinn::Endpoint; @@ -92,8 +92,10 @@ async fn main() -> Result<()> { roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(), }; - for certs in rustls_native_certs::load_native_certs().expect("could not load platform certs") { - roots.add(certs).unwrap(); + for certs in rustls_native_certs::load_native_certs().context("could not load platform certs")? { + if let Err(err) = roots.add(certs) { + warn!("skipping invalid native cert: {:?}", err); + } } let mut client_crypto = rustls::ClientConfig::builder() @@ -150,11 +152,15 @@ async fn transfer( if e == ConnectionError::TimedOut { match create_udp_socket(vpn_mode) { Ok(socket) => { - let addr = socket.local_addr().unwrap(); + let addr = socket.local_addr().ok(); let ret = endpoint.rebind(socket); match ret { Ok(_) => { - info!("rebinding to: {}", addr); + if let Some(addr) = addr { + info!("rebinding to: {}", addr); + } else { + info!("rebinding to new local socket"); + } } Err(e) => { error!("rebind fail: {:?}", e); @@ -167,8 +173,7 @@ async fn transfer( } } anyhow!("failed to connect: {:?}", e) - }) - .unwrap(); + })?; let (mut ri, mut wi) = inbound.split(); let (mut wo, mut ro) = new_conn diff --git a/src/server.rs b/src/server.rs index 5aee92c..82f7371 100644 --- a/src/server.rs +++ b/src/server.rs @@ -147,7 +147,9 @@ async fn main() -> Result<()> { conn.refuse(); } else if options.stateless_retry && !conn.remote_address_validated() { info!("requiring connection to validate its address"); - conn.retry().unwrap(); + if let Err(e) = conn.retry() { + error!("failed to request stateless retry: {:?}", e); + } } else { info!("accepting connection"); let fut = handle_connection(remote.clone(), conn); From 613da1093298f89102f909f648ac1525e537ef56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 02:33:09 +0000 Subject: [PATCH 2/2] Fix panic-based security weaknesses Co-authored-by: madeye <627917+madeye@users.noreply.github.com> --- Cargo.lock | 1 + src/client.rs | 55 ++++++++++++++++++++++++++------------------------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b0d4590..8b1c86a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -665,6 +665,7 @@ dependencies = [ "dirs", "env_logger", "futures", + "libc", "log", "quinn", "quinn-proto", diff --git a/src/client.rs b/src/client.rs index 2c3c3da..e55274c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use quinn::crypto::rustls::QuicClientConfig; use tokio::net::{TcpListener, TcpStream}; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Result}; use clap::Parser; use futures::future::try_join; use log::{error, info, warn}; @@ -92,8 +92,12 @@ async fn main() -> Result<()> { roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(), }; - for certs in rustls_native_certs::load_native_certs().context("could not load platform certs")? { - if let Err(err) = roots.add(certs) { + let native_certs = rustls_native_certs::load_native_certs(); + for err in native_certs.errors { + warn!("error loading native cert: {:?}", err); + } + for cert in native_certs.certs { + if let Err(err) = roots.add(cert) { warn!("skipping invalid native cert: {:?}", err); } } @@ -145,35 +149,32 @@ async fn transfer( mut inbound: TcpStream, vpn_mode: bool, ) -> Result<()> { - let new_conn = endpoint - .connect(*remote, &host)? - .await - .map_err(|e| { - if e == ConnectionError::TimedOut { - match create_udp_socket(vpn_mode) { - Ok(socket) => { - let addr = socket.local_addr().ok(); - let ret = endpoint.rebind(socket); - match ret { - Ok(_) => { - if let Some(addr) = addr { - info!("rebinding to: {}", addr); - } else { - info!("rebinding to new local socket"); - } - } - Err(e) => { - error!("rebind fail: {:?}", e); + let new_conn = endpoint.connect(*remote, &host)?.await.map_err(|e| { + if e == ConnectionError::TimedOut { + match create_udp_socket(vpn_mode) { + Ok(socket) => { + let addr = socket.local_addr().ok(); + let ret = endpoint.rebind(socket); + match ret { + Ok(_) => { + if let Some(addr) = addr { + info!("rebinding to: {}", addr); + } else { + info!("rebinding to new local socket"); } } + Err(e) => { + error!("rebind fail: {:?}", e); + } } - Err(e) => { - error!("failed to create socket for rebind: {:?}", e); - } + } + Err(e) => { + error!("failed to create socket for rebind: {:?}", e); } } - anyhow!("failed to connect: {:?}", e) - })?; + } + anyhow!("failed to connect: {:?}", e) + })?; let (mut ri, mut wi) = inbound.split(); let (mut wo, mut ro) = new_conn