Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions crates/desktop/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,17 @@ pub struct ProxyInstance {
impl ProxyInstance {
pub fn new(
label: impl AsRef<str>, scope_host: impl AsRef<str>, listener: tokio::net::TcpListener,
remote: impl AsRef<str>,
remote: impl AsRef<str>, accept_invalid_certs: bool,
) -> Self {
let tunnel = Tunnel::new(remote.as_ref(), listener);
let tunnel = if accept_invalid_certs {
warn!(
"CREATE insecure tunnel: certificate verification is disabled for {}",
remote.as_ref()
);
Tunnel::with_insecure_tls(remote.as_ref(), listener)
} else {
Tunnel::new(remote.as_ref(), listener)
};

Self {
data: InstanceData {
Expand Down Expand Up @@ -304,6 +312,8 @@ pub async fn launch_instance(
) -> Result<InstanceData, (axum::http::StatusCode, String)> {
use wsrx::utils::create_tcp_listener;

let accept_invalid_certs = state.settings.read().await.insecure_tls;

let listener = create_tcp_listener(&instance_data.local).await?;

let local = listener
Expand Down Expand Up @@ -342,6 +352,7 @@ pub async fn launch_instance(
scope.clone(),
listener,
instance_data.remote.clone(),
accept_invalid_certs,
);

let instance_resp: InstanceData = (&instance).into();
Expand All @@ -351,7 +362,18 @@ pub async fn launch_instance(
let state_clone = state.clone();
let instance = instance_resp.clone();
tokio().spawn(async move {
let client = reqwest::Client::new();
let mut client =
reqwest::Client::builder().user_agent(format!("wsrx/{}", env!("CARGO_PKG_VERSION")));
if accept_invalid_certs {
client = client.danger_accept_invalid_certs(true);
}
let client = match client.build() {
Ok(client) => client,
Err(err) => {
error!("Failed to build latency probe client: {err}");
return;
}
};
match workers::update_instance_latency(&instance, &client).await {
Ok(elapsed) => workers::update_instance_state(&state_clone, &instance, elapsed).await,
Err(_) => workers::update_instance_state(&state_clone, &instance, -1).await,
Expand Down
26 changes: 25 additions & 1 deletion crates/desktop/src/daemon/workers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ use crate::{

/// Periodically pings every instance and updates its latency in the UI.
pub async fn latency_loop(state: ServerState) {
let client = reqwest::Client::new();
let mut client = build_latency_client(&state).await;
let mut client_insecure = state.settings.read().await.insecure_tls;
loop {
let instances = state.instances.read().await;
let instances_pure = instances
Expand Down Expand Up @@ -44,11 +45,34 @@ pub async fn latency_loop(state: ServerState) {
state.events.send(UiEvent::Refresh).await.ok();
}

// Rebuild the probing client when the insecure-TLS setting changes,
// so that latency probes follow the same certificate policy as the
// actual tunnels.
let insecure_tls = state.settings.read().await.insecure_tls;
if insecure_tls != client_insecure {
client = build_latency_client(&state).await;
client_insecure = insecure_tls;
}

// Sleep for 5 seconds
tokio::time::sleep(Duration::from_secs(5)).await;
}
}

/// Builds the HTTP client used to probe instance latency. When the user
/// enabled insecure TLS, invalid server certificates are accepted as well.
async fn build_latency_client(state: &ServerState) -> reqwest::Client {
let mut builder =
reqwest::Client::builder().user_agent(format!("wsrx/{}", env!("CARGO_PKG_VERSION")));
if state.settings.read().await.insecure_tls {
builder = builder.danger_accept_invalid_certs(true);
}
builder.build().unwrap_or_else(|err| {
error!("Failed to build latency probe client: {err}");
reqwest::Client::new()
})
}

#[derive(Debug, Error)]
pub enum LatencyError {
#[error("Request error: {0}")]
Expand Down
10 changes: 10 additions & 0 deletions crates/desktop/src/i18n.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ static TABLE: Lazy<Vec<Entry>> = Lazy::new(|| {
("Quit", "退出", "退出"),
("Enabled", "已启用", "已啟用"),
("Disabled", "已禁用", "已停用"),
(
"Allow insecure TLS connections",
"允许不安全的 TLS 连接",
"允許不安全的 TLS 連線",
),
(
"WARNING: when enabled, wsrx will skip certificate verification and unconditionally trust any certificate for wss:// connections, including self-signed or forged ones. This makes you vulnerable to man-in-the-middle attacks. Only enable it when you know what you are doing.",
"警告:启用后,wsrx 将跳过证书校验,无条件信任 wss:// 连接的任何证书(包括自签名或伪造的证书),使你面临中间人攻击的风险。仅在明确知晓后果时才应启用。",
"警告:啟用後,wsrx 將跳過憑證驗證,無條件信任 wss:// 連線的任何憑證(包括自簽名或偽造的憑證),使你面臨中間人攻擊的風險。僅在明確知曉後果時才應啟用。",
),
("Language / Locale", "语言 / 区域", "語言 / 地區"),
("English", "English", "English"),
("简体中文", "简体中文", "简体中文"),
Expand Down
5 changes: 5 additions & 0 deletions crates/desktop/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ pub struct WsrxDesktopConfig {
pub running_in_tray: bool,
#[serde(default = "default_language")]
pub language: String,
/// When true, connect to `wss://` remotes without verifying the server
/// certificate. Disabled by default.
#[serde(default)]
pub insecure_tls: bool,
}

impl Default for WsrxDesktopConfig {
Expand All @@ -48,6 +52,7 @@ impl Default for WsrxDesktopConfig {
theme: default_theme(),
running_in_tray: default_running_in_tray(),
language: default_language(),
insecure_tls: false,
}
}
}
Expand Down
19 changes: 16 additions & 3 deletions crates/desktop/src/ui/connections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub(crate) fn render_connections(
let theme = cx.theme();
let weak = cx.entity().downgrade();
let state = root.state().clone();
let insecure_tls = root.settings().insecure_tls;

let scope = root.scope_for_page();
let instances = root.scoped_instances();
Expand Down Expand Up @@ -218,6 +219,7 @@ pub(crate) fn render_connections(
muted_foreground: theme.muted_foreground,
border: theme.border,
},
insecure_tls,
)
}))
.child(div().h_6())
Expand Down Expand Up @@ -246,13 +248,17 @@ struct InstanceRowColors {

fn render_instance_row(
state: &crate::daemon::ServerState, instance: &InstanceData, colors: InstanceRowColors,
insecure_tls: bool,
) -> impl IntoElement {
let state = state.clone();
let local = instance.local.clone();
let local_copy = local.clone();
let remote = instance.remote.clone();
let label = instance.label.clone();
let latency = instance.latency;
// Connections over wss:// are made without certificate verification when
// insecure TLS is enabled; mark them with an insecure icon.
let is_insecure = insecure_tls && remote.starts_with("wss://");

let latency_text = if latency >= 0 {
format!("{latency} ms")
Expand Down Expand Up @@ -294,10 +300,17 @@ fn render_instance_row(
.items_center()
.gap_4()
.child(
div()
h_flex()
.flex_1()
.text_color(colors.muted_foreground)
.child(remote),
.min_w_0()
.items_center()
.gap_2()
.when(is_insecure, |this| {
this.child(
Icon::new(IconName::ShieldError).text_color(colors.danger),
)
})
.child(div().text_color(colors.muted_foreground).child(remote)),
)
.child(
h_flex()
Expand Down
55 changes: 55 additions & 0 deletions crates/desktop/src/ui/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub(crate) fn render_settings(
let version = root.version().to_string();
let language = root.settings().language.clone();
let running_in_tray = root.settings().running_in_tray;
let insecure_tls = root.settings().insecure_tls;
let cursor = if root.cursor_visible() { "_" } else { " " };
let info = root.info().to_string();

Expand Down Expand Up @@ -170,6 +171,60 @@ pub(crate) fn render_settings(
),
)
.child(div().h_px().bg(border))
.child(
// Insecure TLS: unconditionally trust server certificates
v_flex()
.gap_1()
.child(settings_row(
cx,
i18n::t("Allow insecure TLS connections"),
Button::new("insecure-tls-toggle")
.flat()
.icon(Icon::new(if insecure_tls {
IconName::ToggleRight
} else {
IconName::ToggleLeft
}))
.label(if insecure_tls {
i18n::t("Enabled")
} else {
i18n::t("Disabled")
})
.on_click({
let weak = weak.clone();
let state = state.clone();
move |_, _, cx| {
let enabled = {
let mut settings = state.settings.blocking_write();
settings.insecure_tls = !settings.insecure_tls;
settings.insecure_tls
};
if enabled {
tracing::warn!(
"Insecure TLS is enabled by the user, \
certificate verification is now disabled."
);
}
daemon::persist_settings_sync(&state);
let _ = weak.update(cx, |root, cx| {
root.settings.insecure_tls = enabled;
cx.notify();
});
}
}),
))
.child(
div()
.pl_3()
.pr_3()
.text_color(cx.theme().danger)
.opacity(0.85)
.child(i18n::t(
"WARNING: when enabled, wsrx will skip certificate verification and unconditionally trust any certificate for wss:// connections, including self-signed or forged ones. This makes you vulnerable to man-in-the-middle attacks. Only enable it when you know what you are doing.",
)),
),
)
.child(div().h_px().bg(border))
.child(
// Export network logs
settings_row(
Expand Down
92 changes: 89 additions & 3 deletions crates/wsrx/src/tunnel.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
use std::sync::Arc;

use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::crypto::CryptoProvider;
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::{DigitallySignedStruct, Error, SignatureScheme};
use serde::{Deserialize, Serialize};
use tokio::{net::TcpListener, task::JoinHandle};
use tokio_tungstenite::Connector;
use tokio_util::sync::CancellationToken;
use tracing::{error, info};

Expand Down Expand Up @@ -42,6 +47,21 @@ impl Serialize for Tunnel {
impl Tunnel {
/// Creates a new `Tunnel` instance.
pub fn new(remote: impl AsRef<str>, listener: TcpListener) -> Self {
Self::build(remote, listener, false)
}

/// Creates a new `Tunnel` instance that connects to `wss://` remotes
/// without verifying the server certificate at all. Every certificate,
/// including self-signed, expired or mismatched ones, is trusted
/// unconditionally.
///
/// This is insecure and must only be used when the user explicitly opts
/// in from the application settings.
pub fn with_insecure_tls(remote: impl AsRef<str>, listener: TcpListener) -> Self {
Self::build(remote, listener, true)
}

fn build(remote: impl AsRef<str>, listener: TcpListener, accept_invalid_certs: bool) -> Self {
let local = listener
.local_addr()
.expect("failed to bind port")
Expand All @@ -56,6 +76,12 @@ impl Tunnel {
remote: remote.as_ref().to_string(),
};

let tls_connector = if accept_invalid_certs {
Some(Connector::Rustls(Arc::new(build_insecure_client_config())))
} else {
None
};

let loop_config = Arc::new(config.clone());
let loop_token = token.clone();
let handle = tokio::spawn(async move {
Expand All @@ -80,11 +106,19 @@ impl Tunnel {

let proxy_config = loop_config.clone();
let proxy_token = loop_token.clone();
let connector = tls_connector.clone();

tokio::spawn(async move {
use tokio_tungstenite::connect_async;

let ws = match connect_async(proxy_config.remote.as_str()).await {
use tokio_tungstenite::connect_async_tls_with_config;

let ws = match connect_async_tls_with_config(
proxy_config.remote.as_str(),
None,
false,
connector,
)
.await
{
Ok((ws, _)) => ws,
Err(e) => {
error!("Failed to connect to {}: {}", proxy_config.remote, e);
Expand Down Expand Up @@ -138,3 +172,55 @@ impl std::ops::DerefMut for Tunnel {
&mut self.config
}
}

/// A `ServerCertVerifier` that accepts every server certificate without any
/// verification. Used only when the user explicitly opts in to insecure TLS
/// connections.
#[derive(Debug)]
struct AcceptAnyServerCert {
schemes: Vec<SignatureScheme>,
}

impl ServerCertVerifier for AcceptAnyServerCert {
fn verify_server_cert(
&self, _end_entity: &CertificateDer<'_>, _intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>, _ocsp_response: &[u8], _now: UnixTime,
) -> Result<ServerCertVerified, Error> {
Ok(ServerCertVerified::assertion())
}

fn verify_tls12_signature(
&self, _message: &[u8], _cert: &CertificateDer<'_>, _dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, Error> {
Ok(HandshakeSignatureValid::assertion())
}

fn verify_tls13_signature(
&self, _message: &[u8], _cert: &CertificateDer<'_>, _dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, Error> {
Ok(HandshakeSignatureValid::assertion())
}

fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.schemes.clone()
}
}

/// Builds a rustls client config that unconditionally trusts any server
/// certificate. Prefers the process-wide crypto provider when one is
/// installed, and falls back to the ring provider otherwise.
fn build_insecure_client_config() -> rustls::ClientConfig {
let provider = CryptoProvider::get_default()
.cloned()
.unwrap_or_else(|| Arc::new(rustls::crypto::ring::default_provider()));
let schemes = provider
.signature_verification_algorithms
.supported_schemes();

rustls::ClientConfig::builder_with_provider(provider)
.with_protocol_versions(rustls::ALL_VERSIONS)
.expect("supported protocol versions")
.dangerous()
.with_custom_certificate_verifier(Arc::new(AcceptAnyServerCert { schemes }))
.with_no_client_auth()
}
Loading