Merge branch 'main' into feat/google-ipv4-fetch

This commit is contained in:
v4g4b0nd_0x76
2026-04-22 13:54:41 +03:30
committed by GitHub
17 changed files with 403 additions and 269 deletions
+163 -82
View File
@@ -39,7 +39,7 @@ fn main() -> eframe::Result<()> {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([WIN_WIDTH, WIN_HEIGHT])
.with_min_inner_size([420.0, 540.0])
.with_min_inner_size([420.0, 400.0])
.with_title(format!("mhrv-rs {}", VERSION)),
..Default::default()
};
@@ -95,10 +95,16 @@ enum Cmd {
PollStats,
/// Probe a single SNI against the given google_ip. Result is written
/// into UiState::sni_probe keyed by the SNI string.
TestSni { google_ip: String, sni: String },
TestSni {
google_ip: String,
sni: String,
},
/// Probe a batch of SNI names. Results appear in UiState::sni_probe one
/// by one as each probe finishes.
TestAllSni { google_ip: String, snis: Vec<String> },
TestAllSni {
google_ip: String,
snis: Vec<String>,
},
}
struct App {
@@ -225,7 +231,10 @@ fn sni_pool_for_form(user: Option<&[String]>, front_domain: &str) -> Vec<SniRow>
if !user_clean.is_empty() {
return user_clean
.into_iter()
.map(|name| SniRow { name, enabled: true })
.map(|name| SniRow {
name,
enabled: true,
})
.collect();
}
// Default: primary + the other Google-edge subdomains, primary first,
@@ -235,11 +244,17 @@ fn sni_pool_for_form(user: Option<&[String]>, front_domain: &str) -> Vec<SniRow>
let mut out = Vec::new();
if !primary.is_empty() {
seen.insert(primary.clone());
out.push(SniRow { name: primary, enabled: true });
out.push(SniRow {
name: primary,
enabled: true,
});
}
for s in DEFAULT_GOOGLE_SNI_POOL {
if seen.insert(s.to_string()) {
out.push(SniRow { name: (*s).to_string(), enabled: true });
out.push(SniRow {
name: (*s).to_string(),
enabled: true,
});
}
}
out
@@ -293,7 +308,11 @@ impl FormState {
enable_batching: false,
upstream_socks5: {
let v = self.upstream_socks5.trim();
if v.is_empty() { None } else { Some(v.to_string()) }
if v.is_empty() {
None
} else {
Some(v.to_string())
}
},
parallel_relay: self.parallel_relay,
sni_hosts: {
@@ -308,7 +327,11 @@ impl FormState {
// If the user's pool is empty/all-off we still save as None so
// the backend falls back to sensible defaults instead of dying
// on an empty pool.
if active.is_empty() { None } else { Some(active) }
if active.is_empty() {
None
} else {
Some(active)
}
},
fetch_ips_from_api:self.fetch_ips_from_api,
max_ips_to_scan: self.max_ips_to_scan,
@@ -323,8 +346,7 @@ fn save_config(cfg: &Config) -> Result<PathBuf, String> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let json = serde_json::to_string_pretty(&ConfigWire::from(cfg))
.map_err(|e| e.to_string())?;
let json = serde_json::to_string_pretty(&ConfigWire::from(cfg)).map_err(|e| e.to_string())?;
std::fs::write(&path, json).map_err(|e| e.to_string())?;
Ok(path)
}
@@ -384,7 +406,10 @@ impl<'a> From<&'a Config> for ConfigWire<'a> {
hosts: &c.hosts,
upstream_socks5: c.upstream_socks5.as_deref(),
parallel_relay: c.parallel_relay,
sni_hosts: c.sni_hosts.as_ref().map(|v| v.iter().map(String::as_str).collect()),
sni_hosts: c
.sni_hosts
.as_ref()
.map(|v| v.iter().map(String::as_str).collect()),
}
}
}
@@ -400,6 +425,13 @@ impl eframe::App for App {
egui::CentralPanel::default().show(ctx, |ui| {
ui.style_mut().spacing.item_spacing = egui::vec2(8.0, 6.0);
// Wrap the whole central panel in a vertical scroll area so the
// form + stats + log panel stay accessible on short screens
// (~13" laptops at default scaling). Nested scroll areas still
// work fine within this outer scroller.
egui::ScrollArea::vertical()
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.label(egui::RichText::new(format!("mhrv-rs {}", VERSION))
.size(16.0));
@@ -727,6 +759,7 @@ impl eframe::App for App {
self.toast = None;
}
}
}); // end ScrollArea
});
}
}
@@ -782,14 +815,13 @@ impl App {
});
}
}
if ui.button("Keep working only").on_hover_text(
"Uncheck every SNI that didn't pass the last probe."
).clicked() {
if ui
.button("Keep working only")
.on_hover_text("Uncheck every SNI that didn't pass the last probe.")
.clicked()
{
for row in &mut self.form.sni_pool {
let ok = matches!(
probe_map.get(&row.name),
Some(SniProbeState::Ok(_))
);
let ok = matches!(probe_map.get(&row.name), Some(SniProbeState::Ok(_)));
row.enabled = ok;
}
}
@@ -801,13 +833,20 @@ impl App {
if ui.button("Clear status").clicked() {
self.shared.state.lock().unwrap().sni_probe.clear();
}
if ui.button("Reset to defaults").on_hover_text(
"Replace the list with the built-in Google SNI pool. Custom entries \
are dropped."
).clicked() {
if ui
.button("Reset to defaults")
.on_hover_text(
"Replace the list with the built-in Google SNI pool. Custom entries \
are dropped.",
)
.clicked()
{
self.form.sni_pool = DEFAULT_GOOGLE_SNI_POOL
.iter()
.map(|s| SniRow { name: (*s).to_string(), enabled: true })
.map(|s| SniRow {
name: (*s).to_string(),
enabled: true,
})
.collect();
self.shared.state.lock().unwrap().sni_probe.clear();
}
@@ -820,51 +859,55 @@ impl App {
let mut test_name: Option<String> = None;
const STATUS_W: f32 = 150.0;
const NAME_W: f32 = 230.0;
egui::ScrollArea::vertical().max_height(280.0).show(ui, |ui| {
for (i, row) in self.form.sni_pool.iter_mut().enumerate() {
ui.horizontal(|ui| {
ui.checkbox(&mut row.enabled, "");
ui.add(
egui::TextEdit::singleline(&mut row.name)
.desired_width(NAME_W)
.font(egui::TextStyle::Monospace),
);
let status_txt = match probe_map.get(&row.name) {
Some(SniProbeState::Ok(ms)) => {
egui::RichText::new(format!("ok {} ms", ms))
.color(egui::Color32::from_rgb(80, 180, 100))
.monospace()
}
Some(SniProbeState::Failed(e)) => {
let short = if e.len() > 22 { &e[..22] } else { e };
egui::RichText::new(format!("fail {}", short))
.color(egui::Color32::from_rgb(220, 110, 110))
.monospace()
}
Some(SniProbeState::InFlight) => {
egui::RichText::new("testing…")
egui::ScrollArea::vertical()
.max_height(280.0)
.show(ui, |ui| {
for (i, row) in self.form.sni_pool.iter_mut().enumerate() {
ui.horizontal(|ui| {
ui.checkbox(&mut row.enabled, "");
ui.add(
egui::TextEdit::singleline(&mut row.name)
.desired_width(NAME_W)
.font(egui::TextStyle::Monospace),
);
let status_txt = match probe_map.get(&row.name) {
Some(SniProbeState::Ok(ms)) => {
egui::RichText::new(format!("ok {} ms", ms))
.color(egui::Color32::from_rgb(80, 180, 100))
.monospace()
}
Some(SniProbeState::Failed(e)) => {
let short = if e.len() > 22 { &e[..22] } else { e };
egui::RichText::new(format!("fail {}", short))
.color(egui::Color32::from_rgb(220, 110, 110))
.monospace()
}
Some(SniProbeState::InFlight) => {
egui::RichText::new("testing…")
.color(egui::Color32::GRAY)
.monospace()
}
None => egui::RichText::new("untested")
.color(egui::Color32::GRAY)
.monospace()
.monospace(),
};
ui.add_sized(
[STATUS_W, 18.0],
egui::Label::new(status_txt).truncate(),
);
if ui.small_button("Test").clicked() {
test_name = Some(row.name.clone());
}
None => {
egui::RichText::new("untested")
.color(egui::Color32::GRAY)
.monospace()
if ui
.small_button("remove")
.on_hover_text("Remove this row")
.clicked()
{
to_remove = Some(i);
}
};
ui.add_sized([STATUS_W, 18.0], egui::Label::new(status_txt).truncate());
if ui.small_button("Test").clicked() {
test_name = Some(row.name.clone());
}
if ui.small_button("remove")
.on_hover_text("Remove this row")
.clicked()
{
to_remove = Some(i);
}
});
}
});
});
}
});
if let Some(name) = test_name {
let name = name.trim().to_string();
@@ -946,12 +989,16 @@ fn fmt_bytes(b: u64) -> String {
fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
let rt = Runtime::new().expect("failed to create tokio runtime");
let mut active: Option<(JoinHandle<()>, Arc<AsyncMutex<Option<Arc<DomainFronter>>>>)> = None;
let mut active: Option<(
JoinHandle<()>,
Arc<AsyncMutex<Option<Arc<DomainFronter>>>>,
tokio::sync::oneshot::Sender<()>,
)> = None;
loop {
match rx.recv_timeout(Duration::from_millis(250)) {
Ok(Cmd::PollStats) => {
if let Some((_, fronter_slot)) = &active {
if let Some((_, fronter_slot, _)) = &active {
let slot = fronter_slot.clone();
let shared = shared.clone();
rt.spawn(async move {
@@ -966,6 +1013,7 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
});
}
}
// In background_thread function, modify the Cmd::Start handler:
Ok(Cmd::Start(cfg)) => {
if active.is_some() {
push_log(&shared, "[ui] already running");
@@ -977,6 +1025,8 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
Arc::new(AsyncMutex::new(None));
let fronter_slot2 = fronter_slot.clone();
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
let handle = rt.spawn(async move {
let base = data_dir::data_dir();
let mitm = match MitmCertManager::new_in(&base) {
@@ -1002,27 +1052,49 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
s.running = true;
s.started_at = Some(Instant::now());
}
push_log(&shared2, &format!(
"[ui] listening HTTP {}:{} SOCKS5 {}:{}",
cfg.listen_host, cfg.listen_port,
cfg.listen_host, cfg.socks5_port.unwrap_or(cfg.listen_port + 1)
));
let _ = server.run().await;
push_log(
&shared2,
&format!(
"[ui] listening HTTP {}:{} SOCKS5 {}:{}",
cfg.listen_host,
cfg.listen_port,
cfg.listen_host,
cfg.socks5_port.unwrap_or(cfg.listen_port + 1)
),
);
let _ = server.run(shutdown_rx).await;
shared2.state.lock().unwrap().running = false;
push_log(&shared2, "[ui] proxy stopped");
});
active = Some((handle, fronter_slot));
active = Some((handle, fronter_slot, shutdown_tx));
}
Ok(Cmd::Stop) => {
if let Some((handle, _)) = active.take() {
handle.abort();
if let Some((handle, _, shutdown_tx)) = active.take() {
push_log(&shared, "[ui] stop requested");
let _ = shutdown_tx.send(());
// Give the proxy 2 seconds to shut down gracefully
rt.block_on(async {
tokio::select! {
_ = handle => {
push_log(&shared, "[ui] proxy stopped gracefully");
}
_ = tokio::time::sleep(tokio::time::Duration::from_secs(2)) => {
push_log(&shared, "[ui] shutdown timeout, forcing abort");
}
}
});
shared.state.lock().unwrap().running = false;
shared.state.lock().unwrap().started_at = None;
shared.state.lock().unwrap().last_stats = None;
push_log(&shared, "[ui] stop requested");
}
}
Ok(Cmd::Test(cfg)) => {
let shared2 = shared.clone();
push_log(&shared, "[ui] running test...");
@@ -1034,7 +1106,10 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
} else {
"Test failed — see terminal for details.".into()
};
push_log(&shared2, &format!("[ui] test result: {}", if ok { "pass" } else { "fail" }));
push_log(
&shared2,
&format!("[ui] test result: {}", if ok { "pass" } else { "fail" }),
);
// Also run ip scan on demand (cheap).
let _ = scan_ips::run(&cfg).await;
});
@@ -1071,7 +1146,9 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
let result = scan_sni::probe_one(&google_ip, &sni).await;
let state = match result.latency_ms {
Some(ms) => SniProbeState::Ok(ms),
None => SniProbeState::Failed(result.error.unwrap_or_else(|| "failed".into())),
None => {
SniProbeState::Failed(result.error.unwrap_or_else(|| "failed".into()))
}
};
shared2.state.lock().unwrap().sni_probe.insert(sni, state);
});
@@ -1090,7 +1167,9 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
for (sni, r) in results {
let state = match r.latency_ms {
Some(ms) => SniProbeState::Ok(ms),
None => SniProbeState::Failed(r.error.unwrap_or_else(|| "failed".into())),
None => {
SniProbeState::Failed(r.error.unwrap_or_else(|| "failed".into()))
}
};
st.sni_probe.insert(sni, state);
}
@@ -1109,7 +1188,7 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
}
// Clean up finished task.
if let Some((handle, _)) = &active {
if let Some((handle, _, _)) = &active {
if handle.is_finished() {
active = None;
shared.state.lock().unwrap().running = false;
@@ -1121,7 +1200,9 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
fn push_log(shared: &Shared, msg: &str) {
let line = format!(
"{} {}",
time::OffsetDateTime::now_utc().format(&time::format_description::well_known::Iso8601::DEFAULT).unwrap_or_default(),
time::OffsetDateTime::now_utc()
.format(&time::format_description::well_known::Iso8601::DEFAULT)
.unwrap_or_default(),
msg
);
let mut s = shared.state.lock().unwrap();