Skip to content

Commit 0072b3a

Browse files
committed
v0.6.1: fix OpenWRT CA install + replace --user-less CI + perf pack artifacts
v0.6.0's release CI was cancelled before it could produce artifacts. This is a clean re-cut that also fixes a user-reported bug on OpenWRT. === OpenWRT CA install fix === User on issue #2 reported that --install-cert fails on an OpenWRT router with 'install failed on this platform'. Two problems: 1. Misclassification. The old distro detector did a substring-match over all of /etc/os-release, and OpenWRT's file contains lines like OPENWRT_DEVICE_ARCH=x86_64 and OPENWRT_ARCH=x86_64 — which contain the substring 'arch' — so we classified OpenWRT as Arch Linux and tried to install into /etc/ca-certificates/trust-source/ anchors/ (which doesn't exist there) and then run 'trust' (also missing). Predictable failure. 2. Even with correct classification, OpenWRT doesn't need the CA on the router itself. LAN clients are the ones terminating TLS through mhrv-rs's MITM; they're the ones that need to trust our root. The router is just forwarding packets. So emitting an error for the no-op case is misleading. Fixes: - Detect OpenWRT explicitly (/etc/openwrt_release marker file + ID=openwrt in os-release). - Rewrite the fallback os-release parser to look at ID / ID_LIKE token-wise instead of substring-matching the whole file. Added support for raspbian / rocky / almalinux / endeavouros while we're there. - For OpenWRT: install_linux returns Ok() with a clear message explaining that the CA needs to be installed on LAN clients, not on the router. No-op success instead of confusing error. - For unknown distros: the error message now points at the CA file path and the two most common anchor dirs so the user can install manually. - Extracted classify_os_release(&str) as a pure function and added 8 unit tests, including a regression guard with a real OpenWRT 23.05 os-release file so this specific substring-match bug can't return. === v0.6.0 perf pack (same as what cancelled CI was meant to ship) === - Connection pool pre-warm on startup (skip handshake on first request) - Per-connection SNI rotation across known Google-edge subdomains - Expanded SNI-rewrite suffix list (gstatic, googleusercontent, googleapis, ggpht, ytimg, blogspot, blogger) - Per-site stats tracker + UI drill-down table - Optional parallel script-ID dispatch (config field parallel_relay) - TCP_NODELAY audit + fix on SNI-rewrite outbound All 36 unit tests pass. Closes-via-fix #2 follow-up.
1 parent 3f0bbfd commit 0072b3a

3 files changed

Lines changed: 155 additions & 12 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mhrv-rs"
3-
version = "0.6.0"
3+
version = "0.6.1"
44
edition = "2021"
55
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
66
license = "MIT"

src/cert_installer.rs

Lines changed: 153 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,27 @@ fn install_linux(cert_path: &str) -> bool {
142142
let dest = format!("/etc/ca-certificates/trust-source/anchors/{}.crt", safe_name);
143143
try_copy_and_run(cert_path, &dest, &[&["trust", "extract-compat"]])
144144
}
145+
"openwrt" => {
146+
// OpenWRT itself doesn't open HTTPS connections through the proxy —
147+
// LAN clients do. The CA needs to be trusted on the CLIENTS, not on
148+
// the router. So this is a no-op success with guidance rather than
149+
// an error.
150+
tracing::info!(
151+
"OpenWRT detected: the router doesn't need to trust the MITM CA. \
152+
Copy {} to each LAN client (browser / OS trust store) instead. \
153+
Example: scp root@<router>:{} ./ and import from there.",
154+
cert_path, cert_path
155+
);
156+
true
157+
}
145158
_ => {
146-
tracing::warn!("Unknown Linux distro — install {} manually.", cert_path);
159+
tracing::warn!(
160+
"Unknown Linux distro — CA file is at {}. Copy it into your system's \
161+
trust anchors dir (e.g. /usr/local/share/ca-certificates/ for \
162+
Debian-like, /etc/pki/ca-trust/source/anchors/ for RHEL-like) and \
163+
run the corresponding refresh command.",
164+
cert_path
165+
);
147166
false
148167
}
149168
}
@@ -198,6 +217,10 @@ fn run_cmd(args: &[&str]) -> bool {
198217
}
199218

200219
fn detect_linux_distro() -> String {
220+
// Marker-file shortcuts (most reliable).
221+
if Path::new("/etc/openwrt_release").exists() {
222+
return "openwrt".into();
223+
}
201224
if Path::new("/etc/debian_version").exists() {
202225
return "debian".into();
203226
}
@@ -208,17 +231,50 @@ fn detect_linux_distro() -> String {
208231
return "arch".into();
209232
}
210233
if let Ok(content) = std::fs::read_to_string("/etc/os-release") {
211-
let lc = content.to_lowercase();
212-
if lc.contains("debian") || lc.contains("ubuntu") || lc.contains("mint") {
213-
return "debian".into();
214-
}
215-
if lc.contains("fedora") || lc.contains("rhel") || lc.contains("centos") {
216-
return "rhel".into();
217-
}
218-
if lc.contains("arch") || lc.contains("manjaro") {
219-
return "arch".into();
234+
return classify_os_release(&content);
235+
}
236+
"unknown".into()
237+
}
238+
239+
/// Parse /etc/os-release content and return a distro family.
240+
///
241+
/// We specifically look at the `ID` and `ID_LIKE` fields (not a substring
242+
/// search over the whole file) because random other fields like
243+
/// `OPENWRT_DEVICE_ARCH=x86_64` contain substrings that false-positive on
244+
/// "arch". Exposed for unit testing.
245+
fn classify_os_release(content: &str) -> String {
246+
let mut id = String::new();
247+
let mut id_like = String::new();
248+
for line in content.lines() {
249+
let (k, v) = match line.split_once('=') {
250+
Some(x) => x,
251+
None => continue,
252+
};
253+
let v = v.trim().trim_matches('"').trim_matches('\'').to_ascii_lowercase();
254+
match k.trim() {
255+
"ID" => id = v,
256+
"ID_LIKE" => id_like = v,
257+
_ => {}
220258
}
221259
}
260+
let tokens: Vec<&str> = id
261+
.split(|c: char| c.is_whitespace() || c == ',')
262+
.chain(id_like.split(|c: char| c.is_whitespace() || c == ','))
263+
.filter(|t| !t.is_empty())
264+
.collect();
265+
let has = |needle: &str| tokens.iter().any(|t| *t == needle);
266+
if has("openwrt") {
267+
return "openwrt".into();
268+
}
269+
if has("debian") || has("ubuntu") || has("mint") || has("raspbian") {
270+
return "debian".into();
271+
}
272+
if has("fedora") || has("rhel") || has("centos") || has("rocky") || has("almalinux") {
273+
return "rhel".into();
274+
}
275+
if has("arch") || has("manjaro") || has("endeavouros") {
276+
return "arch".into();
277+
}
222278
"unknown".into()
223279
}
224280

@@ -409,3 +465,90 @@ fn firefox_profile_dirs() -> Vec<std::path::PathBuf> {
409465
}
410466
out
411467
}
468+
469+
#[cfg(test)]
470+
mod tests {
471+
use super::*;
472+
473+
#[test]
474+
fn openwrt_os_release_is_not_arch() {
475+
// Real OpenWRT 23.05 /etc/os-release. Contains OPENWRT_DEVICE_ARCH
476+
// which substring-matches "arch" — the old detector would mis-classify
477+
// this as Arch Linux. Regression guard for issue #2.
478+
let content = r#"
479+
NAME="OpenWrt"
480+
VERSION="23.05.3"
481+
ID="openwrt"
482+
ID_LIKE="lede openwrt"
483+
PRETTY_NAME="OpenWrt 23.05.3"
484+
VERSION_ID="23.05.3"
485+
HOME_URL="https://openwrt.org/"
486+
BUG_URL="https://bugs.openwrt.org/"
487+
SUPPORT_URL="https://forum.openwrt.org/"
488+
BUILD_ID="r23809-234f1a2efa"
489+
OPENWRT_BOARD="x86/64"
490+
OPENWRT_ARCH="x86_64"
491+
OPENWRT_TAINTS=""
492+
OPENWRT_DEVICE_MANUFACTURER="OpenWrt"
493+
OPENWRT_DEVICE_MANUFACTURER_URL="https://openwrt.org/"
494+
OPENWRT_DEVICE_PRODUCT="Generic"
495+
OPENWRT_DEVICE_REVISION="v0"
496+
OPENWRT_RELEASE="OpenWrt 23.05.3 r23809-234f1a2efa"
497+
"#;
498+
assert_eq!(classify_os_release(content), "openwrt");
499+
}
500+
501+
#[test]
502+
fn debian_bullseye_classified_as_debian() {
503+
let content = r#"
504+
PRETTY_NAME="Debian GNU/Linux 11 (bullseye)"
505+
NAME="Debian GNU/Linux"
506+
VERSION_ID="11"
507+
VERSION="11 (bullseye)"
508+
VERSION_CODENAME=bullseye
509+
ID=debian
510+
"#;
511+
assert_eq!(classify_os_release(content), "debian");
512+
}
513+
514+
#[test]
515+
fn ubuntu_classified_as_debian_via_id_like() {
516+
let content = r#"
517+
NAME="Ubuntu"
518+
VERSION="22.04.3 LTS (Jammy Jellyfish)"
519+
ID=ubuntu
520+
ID_LIKE=debian
521+
"#;
522+
assert_eq!(classify_os_release(content), "debian");
523+
}
524+
525+
#[test]
526+
fn fedora_classified_as_rhel() {
527+
let content = "ID=fedora\nVERSION_ID=39\n";
528+
assert_eq!(classify_os_release(content), "rhel");
529+
}
530+
531+
#[test]
532+
fn arch_classified_as_arch() {
533+
let content = "ID=arch\nID_LIKE=\n";
534+
assert_eq!(classify_os_release(content), "arch");
535+
}
536+
537+
#[test]
538+
fn manjaro_classified_as_arch() {
539+
let content = "ID=manjaro\nID_LIKE=arch\n";
540+
assert_eq!(classify_os_release(content), "arch");
541+
}
542+
543+
#[test]
544+
fn empty_os_release_is_unknown() {
545+
assert_eq!(classify_os_release(""), "unknown");
546+
}
547+
548+
#[test]
549+
fn random_file_with_arch_substring_does_not_match() {
550+
// Make sure we don't regress to the old substring-match bug.
551+
let content = "SOMEFIELD=maybearchived\nFOO=bar\n";
552+
assert_eq!(classify_os_release(content), "unknown");
553+
}
554+
}

0 commit comments

Comments
 (0)