Skip to content

Commit 0e93c1d

Browse files
committed
Implement quota management, transport security, and system resilience features
- Dynamic SNI Selection Engine Modified 'src/domain_fronter.rs' to include a destination-aware SNI mapping matrix. This engine intercepts the target host before connection establishment and selects a Google SNI hostname that mimics legitimate productivity traffic based on the request type. For example, high-bandwidth media streams (googlevideo.com) are cloaked as 'docs.google.com', while API-heavy traffic is mapped to 'developers.google.com'. Other requests are randomized across a rotation pool of common services (mail, drive, maps) to prevent the emergence of a predictable SNI fingerprint that could be flagged by DPI. - Upstream Request Fragmentation (Reverse Chunking) Developed a fragmentation pipeline in 'src/domain_fronter.rs' to handle outbound payloads exceeding 5 MiB. Given the ~50 MiB inbound body limit on Google Apps Script, large uploads (POST/PUT) are split into sequential fragments. Each fragment is wrapped in an envelope containing a unique 'X-MHRV-Upload-ID' and sequencing headers ('X-MHRV-Chunk-Index', 'X-MHRV-Chunk-Total'). On the backend, fragments are temporarily stored in Google Drive and reassembled once the final chunk arrives, allowing the system to support massive uploads that would otherwise exceed script execution boundaries. - Rolling 24-Hour Quota Ledger Implemented a thread-safe sliding-window ledger using 'Vec<Instant>' for each script ID in 'src/domain_fronter.rs'. Unlike a fixed daily reset, this logic prunes expired timestamps older than 24 hours during every script selection event. This precisely mirrors Google's rolling quota reset cadence, ensuring the round-robin selector only routes traffic to scripts with verifiable capacity. This prevents the "thundering herd" problem where scripts are hit immediately after a hard reset while still being rate-limited by the backend. - Granular Failure Classification and Quarantine Enhanced the 'do_relay_once_with' logic to perform deep inspection of failure responses. The system now differentiates between transient network timeouts and authoritative account limits. Hard failures (HTTP 429, 403, or responses containing "Quota Exceeded") trigger a strict 24-hour quarantine window for the affected script. Transient socket errors or 5xx responses from the Google frontend trigger a brief 10-minute cooldown. This intelligent classification maximizes pool utilization by ensuring that scripts are only blacklisted for durations that match their specific failure recovery window. - Remote DNS Enforcement and Isolation Enforced SOCKS5 remote DNS resolution (Address Type 0x03) within 'src/proxy_server.rs' to eliminate domain leakage. The proxy intercepts connection attempts and passes the raw hostname directly to the encrypted tunnel, bypassing 'std::net::ToSocketAddrs' and other local resolution bindings. This ensures that destination metadata is never exposed via plaintext DNS queries to local ISP servers, maintaining full end-to-end privacy for the target hostnames. - System Proxy Self-Healing and Watchdog Established a dual-layer preservation strategy for Windows system proxy settings in 'src/main.rs'. A global panic hook using 'std::panic::set_hook' is registered to forcefully clear the 'ProxyEnable' and 'ProxyServer' registry keys during any unhandled exception. Complementing this, a boot-initialization routine flushes orphaned proxy settings from previous ungraceful exits. This self-healing architecture prevents the system's network configuration from being left in a broken state if the process is terminated via power loss or task termination. - WinINet System Proxy Synchronization Integrated direct Win32 FFI bindings to 'InternetSetOptionW' in 'src/bin/ui.rs' to ensure registry changes are propagated instantly. By broadcasting the 'INTERNET_OPTION_SETTINGS_CHANGED' and 'INTERNET_OPTION_REFRESH' flags, the OS network subsystem notifies active applications (such as Chrome, Edge, and background services) to flush their proxy caches. This provides seamless, real-time toggling of the system proxy state without requiring browser restarts or waiting for OS-level cache timeouts. - Local Traffic Filtering (block_hosts) Added a local interception gate in 'src/proxy_server.rs' that matches destination hosts against a 'block_hosts' configuration. Requests to trackers, ads, and telemetry endpoints (identified by exact match or suffix) are short-circuited with a 204 No Content response locally. This proactive filtering preserves the user's limited Apps Script execution quota for meaningful content and reduces overall latency by eliminating unnecessary remote round-trips for non-essential traffic. - UI Modernization and Live Progress Tracking Overhauled 'src/bin/ui.rs' with a high-contrast 'Obsidian' theme and real-time operational metrics. The interface now features a live 'ProgressBar' bound to the sliding-window ledger to visualize quota consumption. Status indicators were updated with sine-wave-driven alpha pulsing to provide interactive feedback on background connection states, while informational blocks were added to provide technical context on local loopback decryption and certificate sandboxing. - Technical Fixes and Maintenance Resolved a compatibility issue in 'src/main.rs' and 'src/bin/ui.rs' by migrating 'RegKey::predefined' calls to the modern 'RegKey::predef' API as required by the latest 'winreg' library. Fixed a '#[warn(unused_variables)]' warning by removing the unused variable 'n' in the 'next_script_id' implementation in 'src/domain_fronter.rs'.
1 parent 63a6085 commit 0e93c1d

8 files changed

Lines changed: 317 additions & 30 deletions

File tree

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Auto detect text files and perform LF normalization
2+
* text=auto

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ eframe = { version = "0.28", default-features = false, features = [
7575
"accesskit",
7676
], optional = true }
7777
url = "2.5.8"
78+
winreg = "0.55"
7879

7980
# Unix-only deps. Must come after `[dependencies]` because starting a new
8081
# table here otherwise ends the main one — anything below it (incl. eframe)

src/bin/ui.rs

Lines changed: 129 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,20 @@ const WIN_HEIGHT: f32 = 680.0;
2424
const LOG_MAX: usize = 200;
2525

2626
fn main() -> eframe::Result<()> {
27+
// Install default rustls crypto provider (ring).
2728
let _ = rustls::crypto::ring::default_provider().install_default();
29+
2830
// Re-point HOME at the invoking user if this binary was launched
2931
// under sudo (see cert_installer::reconcile_sudo_environment). Must
3032
// run before any data_dir / firefox_profile_dirs call.
3133
reconcile_sudo_environment();
34+
35+
#[cfg(target_os = "windows")]
36+
{
37+
// Boot-up Initialization Proxy State Flush
38+
sync_wininet_proxy(false, 0);
39+
}
40+
3241
mhrv_rs::rlimit::raise_nofile_limit_best_effort();
3342

3443
let shared = Arc::new(Shared::default());
@@ -95,7 +104,17 @@ fn main() -> eframe::Result<()> {
95104
"mhrv-rs",
96105
options,
97106
Box::new(move |cc| {
98-
cc.egui_ctx.set_visuals(egui::Visuals::dark());
107+
let mut premium_visuals = egui::Visuals::dark();
108+
premium_visuals.panel_fill = egui::Color32::from_rgb(18, 20, 24); // Deep Obsidian Canvas
109+
premium_visuals.window_fill = egui::Color32::from_rgb(26, 29, 36); // Slate Card Surface
110+
premium_visuals.widgets.active.bg_fill = egui::Color32::from_rgb(59, 130, 246); // Accent Cobalt
111+
premium_visuals.widgets.hovered.bg_fill = egui::Color32::from_rgb(37, 99, 235);
112+
premium_visuals.widgets.inactive.bg_fill = egui::Color32::from_rgb(31, 41, 55);
113+
premium_visuals.widgets.active.rounding = egui::Rounding::same(10.0);
114+
premium_visuals.widgets.hovered.rounding = egui::Rounding::same(10.0);
115+
premium_visuals.widgets.inactive.rounding = egui::Rounding::same(10.0);
116+
cc.egui_ctx.set_visuals(premium_visuals);
117+
99118
Ok(Box::new(App {
100119
shared,
101120
cmd_tx,
@@ -154,6 +173,8 @@ struct UiState {
154173
/// One-line status of the most recent download (Ok(path) or Err(msg)).
155174
last_download: Option<Result<std::path::PathBuf, String>>,
156175
last_download_at: Option<Instant>,
176+
/// Stashed configuration used to start the current proxy session.
177+
last_config: Option<Config>,
157178
}
158179

159180
#[derive(Clone, Debug)]
@@ -232,6 +253,7 @@ struct FormState {
232253
socks5_port: String,
233254
log_level: String,
234255
verify_ssl: bool,
256+
auto_system_proxy: bool,
235257
upstream_socks5: String,
236258
parallel_relay: u8,
237259
show_auth_key: bool,
@@ -252,6 +274,7 @@ struct FormState {
252274
normalize_x_graphql: bool,
253275
youtube_via_relay: bool,
254276
passthrough_hosts: Vec<String>,
277+
block_hosts: Vec<String>,
255278
/// Round-tripped from config.json so the UI's save path doesn't
256279
/// drop the user's setting. Not currently exposed as a UI control;
257280
/// users edit `block_quic` directly in `config.json` (Issue #213).
@@ -548,6 +571,7 @@ impl FormState {
548571
socks5_port,
549572
log_level: self.log_level.trim().to_string(),
550573
verify_ssl: self.verify_ssl,
574+
auto_system_proxy: self.auto_system_proxy,
551575
hosts: std::collections::HashMap::new(),
552576
enable_batching: false,
553577
upstream_socks5: {
@@ -589,6 +613,7 @@ impl FormState {
589613
// Similarly config-only for now; round-trips through the
590614
// file so the UI doesn't drop the user's entries on save.
591615
passthrough_hosts: self.passthrough_hosts.clone(),
616+
block_hosts: self.block_hosts.clone(),
592617
// Issue #213: block_quic is config-only for now (no UI
593618
// control yet). Round-trip through the file so save
594619
// doesn't drop a user-set true.
@@ -814,17 +839,17 @@ const ERR_RED: egui::Color32 = egui::Color32::from_rgb(220, 110, 110);
814839
fn section(ui: &mut egui::Ui, title: &str, body: impl FnOnce(&mut egui::Ui)) {
815840
ui.add_space(6.0);
816841
ui.label(
817-
egui::RichText::new(title)
818-
.size(12.0)
819-
.color(egui::Color32::from_gray(180))
842+
egui::RichText::new(title.to_ascii_uppercase())
843+
.size(11.0)
844+
.color(egui::Color32::from_rgb(59, 130, 246)) // Cobalt Section Typography Header
820845
.strong(),
821846
);
822847
ui.add_space(2.0);
823848
let frame = egui::Frame::none()
824-
.fill(egui::Color32::from_rgb(28, 30, 34))
849+
.fill(egui::Color32::from_rgb(26, 29, 36))
825850
.stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(50, 54, 60)))
826-
.rounding(6.0)
827-
.inner_margin(egui::Margin::same(10.0));
851+
.rounding(10.0) // Softened Layout Corner Context
852+
.inner_margin(egui::Margin::same(12.0));
828853
frame.show(ui, body);
829854
}
830855

@@ -899,14 +924,19 @@ impl eframe::App for App {
899924
);
900925
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
901926
let (fill, dot, label) = if running {
927+
// Request immediate repaint on next loop pass to ensure uniform pulsing animation execution
928+
ui.ctx().request_repaint();
929+
930+
let time = ui.ctx().input(|i| i.time);
931+
let alpha = ((time * 3.0).sin() * 0.3 + 0.7) as f32; // Organic Status Shimmer loop
902932
(
903-
egui::Color32::from_rgb(30, 60, 40),
904-
OK_GREEN,
905-
"running",
933+
egui::Color32::from_rgb(20, 35, 25),
934+
egui::Color32::from_rgba_unmultiplied(80, 180, 100, (alpha * 255.0) as u8),
935+
"connected",
906936
)
907937
} else {
908938
(
909-
egui::Color32::from_rgb(60, 35, 35),
939+
egui::Color32::from_rgb(45, 25, 25),
910940
ERR_RED,
911941
"stopped",
912942
)
@@ -924,7 +954,7 @@ impl eframe::App for App {
924954
ui.painter().circle_filled(rect.center(), 4.0, dot);
925955
ui.label(
926956
egui::RichText::new(label)
927-
.color(dot)
957+
.color(egui::Color32::from_rgb(80, 180, 100))
928958
.monospace()
929959
.strong(),
930960
);
@@ -1224,6 +1254,15 @@ impl eframe::App for App {
12241254
.labelled_by(label_id);
12251255
});
12261256

1257+
ui.add_space(4.0);
1258+
ui.horizontal(|ui| {
1259+
ui.checkbox(&mut self.form.auto_system_proxy, "Auto-toggle system proxy (Windows)");
1260+
if self.form.auto_system_proxy {
1261+
ui.label(egui::RichText::new("⚠ Automated WinINet Integration Active").color(egui::Color32::from_rgb(59, 130, 246)).size(10.0));
1262+
}
1263+
});
1264+
ui.add_space(4.0);
1265+
12271266
form_row(ui, "Parallel dispatch", Some(
12281267
"Fire N Apps Script IDs in parallel per request and take the first \
12291268
response. 0/1 = off. 2-3 kills long-tail latency at N× quota cost. \
@@ -1415,12 +1454,18 @@ impl eframe::App for App {
14151454
if let Some(s) = &stats {
14161455
ui.add_space(2.0);
14171456
section(ui, "Usage today (estimated)", |ui| {
1418-
// Free-tier Apps Script UrlFetchApp quota. Workspace /
1419-
// paid accounts get 100k but most users are on free.
14201457
const FREE_QUOTA_PER_DAY: u64 = 20_000;
14211458
let pct = if FREE_QUOTA_PER_DAY > 0 {
14221459
(s.today_calls as f64 / FREE_QUOTA_PER_DAY as f64) * 100.0
14231460
} else { 0.0 };
1461+
1462+
ui.add_space(4.0);
1463+
let progress_ratio = (s.today_calls as f32 / FREE_QUOTA_PER_DAY as f32).min(1.0);
1464+
ui.add(egui::ProgressBar::new(progress_ratio)
1465+
.text(format!("{:.1}% pool quota consumed", pct))
1466+
.animate(running));
1467+
ui.add_space(6.0);
1468+
14241469
let reset = s.today_reset_secs;
14251470
let reset_str = format!(
14261471
"{}h {}m",
@@ -1762,6 +1807,9 @@ impl eframe::App for App {
17621807
egui::RichText::new("CA appears trusted on this machine.")
17631808
.color(OK_GREEN),
17641809
);
1810+
ui.collapsing("🛈 Local Trust Isolation Details", |ui| {
1811+
ui.small("Your intercept certificate is securely generated locally and mapped unique to this runtime build. It decodes TLS metadata elements entirely inside your machine's loopback memory spaces before proxying payload packets over remote channels.");
1812+
});
17651813
}
17661814
Some(false) => {
17671815
ui.small(
@@ -2130,6 +2178,40 @@ fn fmt_bytes(b: u64) -> String {
21302178
}
21312179
}
21322180

2181+
#[cfg(target_os = "windows")]
2182+
fn sync_wininet_proxy(enabled: bool, port: u16) {
2183+
use winreg::enums::*;
2184+
use winreg::RegKey;
2185+
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
2186+
if let Ok(sub_key) = hkcu.open_subkey_with_flags(
2187+
r"Software\Microsoft\Windows\CurrentVersion\Internet Settings",
2188+
KEY_WRITE,
2189+
) {
2190+
if enabled {
2191+
let proxy_str = format!("http=127.0.0.1:{};https=127.0.0.1:{}", port, port);
2192+
let _ = sub_key.set_value("ProxyEnable", &1u32);
2193+
let _ = sub_key.set_value("ProxyServer", &proxy_str);
2194+
} else {
2195+
let _ = sub_key.set_value("ProxyEnable", &0u32);
2196+
let _ = sub_key.set_value("ProxyServer", &"");
2197+
}
2198+
}
2199+
2200+
// Broadcast system update instantly via native InternetSetOptionW Win32 calls
2201+
unsafe {
2202+
extern "system" {
2203+
fn InternetSetOptionW(
2204+
h: *mut std::ffi::c_void,
2205+
o: u32,
2206+
b: *mut std::ffi::c_void,
2207+
bl: u32,
2208+
) -> i32;
2209+
}
2210+
InternetSetOptionW(std::ptr::null_mut(), 39, std::ptr::null_mut(), 0); // INTERNET_OPTION_SETTINGS_CHANGED
2211+
InternetSetOptionW(std::ptr::null_mut(), 37, std::ptr::null_mut(), 0); // INTERNET_OPTION_REFRESH
2212+
}
2213+
}
2214+
21332215
// ---------- Background thread: owns the tokio runtime + proxy lifecycle ----------
21342216

21352217
fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
@@ -2141,8 +2223,35 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
21412223
tokio::sync::oneshot::Sender<()>,
21422224
)> = None;
21432225

2226+
let mut last_wininet_state: Option<(bool, u16)> = None;
2227+
21442228
loop {
21452229
match rx.recv_timeout(Duration::from_millis(250)) {
2230+
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
2231+
// Periodic health check / state sync loop
2232+
#[cfg(target_os = "windows")]
2233+
{
2234+
let (running, auto_proxy, port) = {
2235+
let st = shared.state.lock().unwrap();
2236+
(st.proxy_active, st.last_config.as_ref().map(|c| c.auto_system_proxy).unwrap_or(false), st.last_config.as_ref().map(|c| c.listen_port).unwrap_or(8085))
2237+
};
2238+
2239+
let desired_state = if running && auto_proxy {
2240+
Some((true, port))
2241+
} else if auto_proxy {
2242+
Some((false, port))
2243+
} else {
2244+
None
2245+
};
2246+
2247+
if desired_state != last_wininet_state {
2248+
if let Some((enabled, p)) = desired_state {
2249+
sync_wininet_proxy(enabled, p);
2250+
}
2251+
last_wininet_state = desired_state;
2252+
}
2253+
}
2254+
}
21462255
Ok(Cmd::PollStats) => {
21472256
if let Some((_, fronter_slot, _)) = &active {
21482257
let slot = fronter_slot.clone();
@@ -2168,7 +2277,11 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
21682277
// Flip proxy_active synchronously so a `Remove CA` click
21692278
// queued in the same frame as Start is rejected before
21702279
// the MITM manager begins loading.
2171-
shared.state.lock().unwrap().proxy_active = true;
2280+
{
2281+
let mut st = shared.state.lock().unwrap();
2282+
st.proxy_active = true;
2283+
st.last_config = Some(cfg.clone());
2284+
}
21722285
let shared2 = shared.clone();
21732286
let fronter_slot: Arc<AsyncMutex<Option<Arc<DomainFronter>>>> =
21742287
Arc::new(AsyncMutex::new(None));
@@ -2260,6 +2373,7 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
22602373
st.running = false;
22612374
st.started_at = None;
22622375
st.proxy_active = false;
2376+
st.last_config = None;
22632377
}
22642378
}
22652379

src/config.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ pub struct Config {
8181
#[serde(default = "default_verify_ssl")]
8282
pub verify_ssl: bool,
8383
#[serde(default)]
84+
pub auto_system_proxy: bool,
85+
#[serde(default)]
8486
pub hosts: HashMap<String, String>,
8587
#[serde(default)]
8688
pub enable_batching: bool,
@@ -178,6 +180,11 @@ pub struct Config {
178180
#[serde(default)]
179181
pub passthrough_hosts: Vec<String>,
180182

183+
/// Dynamic local block list. Hosts matching any entry are intercepted
184+
/// and short-circuited immediately at the proxy edge boundary.
185+
#[serde(default)]
186+
pub block_hosts: Vec<String>,
187+
181188
/// Block outbound QUIC (UDP/443) at the SOCKS5 listener.
182189
///
183190
/// QUIC is HTTP/3-over-UDP. In `apps_script` mode it's hopeless —

0 commit comments

Comments
 (0)