fix(async): finish the blocking-call sweep — scan, 3 SSH handlers, DNS
A codebase sweep for siblings of e282c059 (blocking network I/O parked
on the tokio runtime) found the openwrt fix was incomplete:
- openwrt.scan: scan_subnet is async in name only — up to 255 SEQUENTIAL
blocking TCP probes at 500ms each (~2 min on a /24 that silently
drops) plus a blocking SSH verify per candidate. One click of 'scan
for routers' held a worker for that whole time. Now spawn_blocking.
- provision-tollgate / scan-wifi / configure-wan still ran their SSH
exchanges inline; bounded_tcp caps each socket op but a session is
many sequential ops (provision runs opkg install over SSH), so worst
case was minutes. All three now spawn_blocking.
- network::check_dns: blocking glibc to_socket_addrs with no app-level
bound, on every Server-tab load via network.diagnostics. Against a
stale resolver — the moved-network case — that is 5-40s per refresh.
Now spawn_blocking plus a 5s cap, so the tile reports 'no DNS'
instead of hanging.
Verified false positives left alone: every other bare TcpStream::connect
targets 127.0.0.1 (fails instantly), and every remote reqwest client
already sets a timeout.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6e89acced7
commit
203c2b6e50
@@ -38,7 +38,16 @@ impl RpcHandler {
|
|||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let routers = detect::scan_subnet(subnet, prefix, &ssh_user, &ssh_password).await;
|
// scan_subnet is `async` in name only: up to 255 SEQUENTIAL blocking
|
||||||
|
// TCP probes at 500ms each (~2 min on a /24 that silently drops),
|
||||||
|
// plus a blocking SSH verify per candidate. Inline, one click of
|
||||||
|
// "scan for routers" held a tokio worker for that whole time.
|
||||||
|
let routers = tokio::task::spawn_blocking(move || {
|
||||||
|
tokio::runtime::Handle::current()
|
||||||
|
.block_on(detect::scan_subnet(subnet, prefix, &ssh_user, &ssh_password))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context("openwrt scan task")?;
|
||||||
let ips: Vec<String> = routers.iter().map(|ip| ip.to_string()).collect();
|
let ips: Vec<String> = routers.iter().map(|ip| ip.to_string()).collect();
|
||||||
|
|
||||||
Ok(serde_json::json!({ "routers": ips }))
|
Ok(serde_json::json!({ "routers": ips }))
|
||||||
@@ -245,9 +254,21 @@ impl RpcHandler {
|
|||||||
enabled: p.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true),
|
enabled: p.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Blocking SSH session, and provision runs `opkg install` over it —
|
||||||
|
// minutes of held worker if the router stalls mid-exchange.
|
||||||
|
{
|
||||||
|
let host = host.clone();
|
||||||
|
let ssh_user = ssh_user.clone();
|
||||||
|
let ssh_password = ssh_password.clone();
|
||||||
|
let config = config.clone();
|
||||||
|
tokio::task::spawn_blocking(move || -> Result<()> {
|
||||||
let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?;
|
let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?;
|
||||||
router.verify_openwrt()?;
|
router.verify_openwrt()?;
|
||||||
tollgate::provision(&router, &config).await?;
|
tokio::runtime::Handle::current().block_on(tollgate::provision(&router, &config))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context("openwrt provision task")??;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"ok": true,
|
"ok": true,
|
||||||
@@ -296,10 +317,20 @@ impl RpcHandler {
|
|||||||
.or_else(|| saved.password.clone())
|
.or_else(|| saved.password.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// A radio scan is seconds of SSH round-trips even on a healthy
|
||||||
|
// router; keep it off the runtime.
|
||||||
|
let networks = {
|
||||||
|
let host = host.clone();
|
||||||
|
let ssh_user = ssh_user.clone();
|
||||||
|
let ssh_password = ssh_password.clone();
|
||||||
|
tokio::task::spawn_blocking(move || -> Result<Vec<wifi_scan::ScannedNetwork>> {
|
||||||
let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?;
|
let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?;
|
||||||
router.verify_openwrt()?;
|
router.verify_openwrt()?;
|
||||||
|
wifi_scan::scan_networks(&router)
|
||||||
let networks = wifi_scan::scan_networks(&router)?;
|
})
|
||||||
|
.await
|
||||||
|
.context("openwrt wifi scan task")??
|
||||||
|
};
|
||||||
let result: Vec<serde_json::Value> = networks
|
let result: Vec<serde_json::Value> = networks
|
||||||
.iter()
|
.iter()
|
||||||
.map(|n| {
|
.map(|n| {
|
||||||
@@ -374,9 +405,6 @@ impl RpcHandler {
|
|||||||
let dhcp_limit = p.get("dhcp_limit").and_then(|v| v.as_u64()).unwrap_or(150) as u32;
|
let dhcp_limit = p.get("dhcp_limit").and_then(|v| v.as_u64()).unwrap_or(150) as u32;
|
||||||
let masq = p.get("masq").and_then(|v| v.as_bool()).unwrap_or(true);
|
let masq = p.get("masq").and_then(|v| v.as_bool()).unwrap_or(true);
|
||||||
|
|
||||||
let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?;
|
|
||||||
router.verify_openwrt()?;
|
|
||||||
|
|
||||||
let config = wan::WispConfig {
|
let config = wan::WispConfig {
|
||||||
ssid: ssid.clone(),
|
ssid: ssid.clone(),
|
||||||
password,
|
password,
|
||||||
@@ -385,7 +413,20 @@ impl RpcHandler {
|
|||||||
dhcp_limit,
|
dhcp_limit,
|
||||||
masq,
|
masq,
|
||||||
};
|
};
|
||||||
wan::configure_wisp(&router, &config)?;
|
// Reconfiguring WAN drops and re-establishes the router's uplink, so
|
||||||
|
// the SSH exchange can stall for its full timeout budget mid-command.
|
||||||
|
{
|
||||||
|
let host = host.clone();
|
||||||
|
let ssh_user = ssh_user.clone();
|
||||||
|
let ssh_password = ssh_password.clone();
|
||||||
|
tokio::task::spawn_blocking(move || -> Result<()> {
|
||||||
|
let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?;
|
||||||
|
router.verify_openwrt()?;
|
||||||
|
wan::configure_wisp(&router, &config)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context("openwrt configure-wan task")??;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(serde_json::json!({ "ok": true, "host": host, "ssid": ssid }))
|
Ok(serde_json::json!({ "ok": true, "host": host, "ssid": ssid }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -302,9 +302,24 @@ async fn check_tor_connectivity() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check DNS resolution works.
|
/// Check DNS resolution works.
|
||||||
|
///
|
||||||
|
/// `to_socket_addrs` is blocking glibc resolution with no app-level bound:
|
||||||
|
/// against a dead or stale resolver — the moved-network case — it can block
|
||||||
|
/// 5–40s (timeout × attempts × nameservers). This runs on every Server-tab
|
||||||
|
/// load via `network.diagnostics`, so inline it parked a tokio worker each
|
||||||
|
/// refresh. Off the runtime, and bounded so the tile reports "no DNS"
|
||||||
|
/// instead of hanging.
|
||||||
async fn check_dns() -> bool {
|
async fn check_dns() -> bool {
|
||||||
|
let probe = tokio::task::spawn_blocking(|| {
|
||||||
use std::net::ToSocketAddrs;
|
use std::net::ToSocketAddrs;
|
||||||
"cloudflare.com:443".to_socket_addrs().is_ok()
|
"cloudflare.com:443".to_socket_addrs().is_ok()
|
||||||
|
});
|
||||||
|
match tokio::time::timeout(std::time::Duration::from_secs(5), probe).await {
|
||||||
|
Ok(Ok(ok)) => ok,
|
||||||
|
// Timed out or the task failed: the blocking resolve may still be
|
||||||
|
// running on the pool, but the caller is no longer waiting on it.
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Router Compatibility Abstraction ---
|
// --- Router Compatibility Abstraction ---
|
||||||
|
|||||||
Reference in New Issue
Block a user