Skip to content

Commit 03731e5

Browse files
committed
fix(ui): disable LAN sharing while binds require loopback
The config validator now rejects non-loopback listen_host values until inbound HTTP/SOCKS authentication exists, but the desktop form still exposed the previous LAN-sharing checkbox that wrote 0.0.0.0. That made the UI capable of saving a configuration the application would intentionally reject on the next start. Align the Network row with the fail-closed bind policy. The share-with-LAN checkbox is displayed disabled, the form keeps loopback as the saved value for normal configurations, and the hover/help text explains that LAN sharing returns only after authenticated inbound proxy mode exists. If an existing form load contains a wildcard or custom non-loopback bind, show it as unsafe and provide a Reset to loopback action instead of silently overwriting it. This keeps the UI, saved TOML behavior, and startup validation consistent while preserving the user's ability to see and repair an old unsafe value.
1 parent b6806cf commit 03731e5

1 file changed

Lines changed: 37 additions & 75 deletions

File tree

src/bin/ui.rs

Lines changed: 37 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use mhrv_rs::cert_installer::{install_ca, reconcile_sudo_environment, remove_ca}
1313
use mhrv_rs::config::{Config, FrontingGroup, ScriptId};
1414
use mhrv_rs::data_dir;
1515
use mhrv_rs::domain_fronter::{DomainFronter, DEFAULT_GOOGLE_SNI_POOL};
16-
use mhrv_rs::lan_utils::{detect_lan_ip, is_share_on_lan};
16+
use mhrv_rs::lan_utils::is_share_on_lan;
1717
use mhrv_rs::mitm::{MitmCertManager, CA_CERT_FILE};
1818
use mhrv_rs::proxy_server::ProxyServer;
1919
use mhrv_rs::{scan_ips, scan_sni, test_cmd};
@@ -919,98 +919,60 @@ impl eframe::App for App {
919919
.labelled_by(label_id);
920920
});
921921

922-
// Network sharing: phones, tablets, other laptops on the
923-
// same Wi-Fi can use this proxy when the bind address is
924-
// 0.0.0.0 instead of 127.0.0.1. We expose this as a
925-
// single-checkbox UI rather than the raw `listen_host`
926-
// text field — typing `0.0.0.0` from memory is enough of
927-
// a friction point that almost no one does it. Power
928-
// users with a custom bind IP (specific NIC) can still
929-
// edit `listen_host` directly in `config.toml`; we
930-
// detect that case and show a "Custom bind" badge so
931-
// the checkbox doesn't silently overwrite their setting
932-
// on the next Save.
922+
// Network sharing is disabled until inbound proxy auth
923+
// exists. The config validator rejects non-loopback binds;
924+
// keep the form aligned so Save never writes a value that
925+
// the next startup will fail closed.
933926
//
934927
// Snapshot the relevant flags before entering form_row's
935-
// closure — we need to mutate `self.form.listen_host`
936-
// inside the closure when the checkbox toggles, so we
937-
// can't hold a borrow on it through the closure.
928+
// closure — we may reset `self.form.listen_host` inside
929+
// the closure, so avoid holding a borrow through it.
938930
let listen_host_snapshot = self.form.listen_host.trim().to_string();
939-
let listen_port_snapshot = self.form.listen_port.trim().to_string();
940-
let socks5_port_snapshot = self.form.socks5_port.trim().to_string();
941931
let was_share_on_lan = is_share_on_lan(&listen_host_snapshot);
942932
let lower_snapshot = listen_host_snapshot.to_ascii_lowercase();
943933
let is_custom_bind = !listen_host_snapshot.is_empty()
944934
&& !was_share_on_lan
945935
&& lower_snapshot != "127.0.0.1"
946936
&& lower_snapshot != "localhost";
947-
let mut new_listen_host: Option<String> = None;
948937
form_row(ui, "Network", Some(
949-
"By default the proxy is reachable only from this computer. \
950-
Turn this on to let phones, tablets, and other laptops on the \
951-
same Wi-Fi (or a hotspot you're sharing) use it too. The \
952-
other devices then point their HTTP / SOCKS5 proxy at the \
953-
LAN IP shown below. Make sure your firewall lets in the proxy \
954-
port — macOS pops up a Firewall prompt the first time."
938+
"The proxy is reachable only from this computer. Non-loopback \
939+
binds are rejected until HTTP/SOCKS inbound authentication is \
940+
implemented, which prevents accidental open-proxy exposure on \
941+
shared Wi-Fi or hotspots."
955942
), |ui, _label_id| {
956-
if is_custom_bind {
957-
// The user manually wrote a specific bind IP —
958-
// don't let the checkbox stomp on it. Show what
959-
// they have and tell them to edit config.toml
960-
// if they want to change it.
943+
if was_share_on_lan || is_custom_bind {
961944
ui.vertical(|ui| {
962945
ui.label(egui::RichText::new(format!(
963-
"Custom bind: {}",
946+
"Unsafe bind configured: {}",
964947
listen_host_snapshot
965-
)).color(egui::Color32::from_rgb(220, 180, 100)));
966-
ui.small("Edit `listen_host` in config.toml to change.");
948+
)).color(ERR_RED));
949+
ui.small(
950+
"Current builds reject non-loopback binds. Set \
951+
listen_host to 127.0.0.1 in config.toml."
952+
);
953+
if ui.small_button("Reset to loopback").clicked() {
954+
self.form.listen_host = "127.0.0.1".to_string();
955+
}
967956
});
968957
} else {
969-
let mut share = was_share_on_lan;
970-
if ui.checkbox(&mut share, "Share with other devices on my Wi-Fi / network").changed() {
971-
new_listen_host = Some(if share {
972-
"0.0.0.0".to_string()
973-
} else {
974-
"127.0.0.1".to_string()
975-
});
976-
}
977-
if share {
978-
// detect_lan_ip() opens a UDP socket and
979-
// asks the kernel which interface a packet
980-
// to a public IP would use. Cheap (no
981-
// syscall does network I/O) and accurate
982-
// (it's the same selection any outbound
983-
// connection would make).
984-
match detect_lan_ip() {
985-
Some(ip) => {
986-
let port = if listen_port_snapshot.is_empty() {
987-
"8085"
988-
} else {
989-
listen_port_snapshot.as_str()
990-
};
991-
let socks_port = if socks5_port_snapshot.is_empty() {
992-
"8086"
993-
} else {
994-
socks5_port_snapshot.as_str()
995-
};
996-
ui.small(egui::RichText::new(format!(
997-
"Other devices: HTTP {}:{} · SOCKS5 {}:{}",
998-
ip, port, ip, socks_port,
999-
)).color(egui::Color32::from_rgb(120, 200, 140)));
1000-
}
1001-
None => {
1002-
ui.small(egui::RichText::new(
1003-
"Couldn't detect your LAN IP. Find it in System Settings \
1004-
→ Network → Wi-Fi → Details (macOS) or `ipconfig` (Windows)."
1005-
).color(egui::Color32::from_rgb(220, 180, 100)));
1006-
}
1007-
}
1008-
}
958+
self.form.listen_host = "127.0.0.1".to_string();
959+
let mut share = false;
960+
ui.add_enabled(
961+
false,
962+
egui::Checkbox::new(
963+
&mut share,
964+
"Share with other devices on my Wi-Fi / network",
965+
),
966+
)
967+
.on_disabled_hover_text(
968+
"LAN sharing will return after inbound proxy authentication is available.",
969+
);
970+
ui.small(
971+
egui::RichText::new("Loopback only: HTTP/SOCKS accept local connections.")
972+
.color(egui::Color32::from_rgb(120, 200, 140)),
973+
);
1009974
}
1010975
});
1011-
if let Some(updated) = new_listen_host {
1012-
self.form.listen_host = updated;
1013-
}
1014976

1015977
ui.horizontal(|ui| {
1016978
ui.add_sized(

0 commit comments

Comments
 (0)