mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-17 21:24:48 +03:00
v0.7.0: editable SNI rotation pool with reachability probes
New feature — users can now edit exactly which SNI names are rotated
through the outbound Google-edge tunnel, and probe each one's
reachability. Useful when an ISP selectively blocks individual Google
subdomains (e.g. mail.google.com in Iran at various times).
=== Data model ===
Config gains an optional 'sni_hosts' field:
"sni_hosts": ["www.google.com", "drive.google.com"]
Precedence in domain_fronter::build_sni_pool_for():
1. If sni_hosts is set & non-empty, use that list verbatim.
2. Else, if front_domain is one of the default Google-edge names,
auto-expand to {www, mail, drive, docs, calendar}.google.com.
3. Else, use just [front_domain].
Empty / all-disabled list saves as None so the backend falls back to
the defaults instead of having zero names to rotate through.
=== New scan_sni module ===
probe_one(ip, sni) / probe_all(ip, snis) does, for each candidate:
1. DNS lookup on the SNI (catches typos / non-existent names — Google
GFE returns a valid wildcard cert for ANY *.google.com, so the
TLS handshake alone can't tell apart a real name from gibberish).
2. TCP connect to google_ip:443 (3s timeout).
3. TLS handshake with the candidate SNI (3s timeout). RST mid-
handshake signals DPI block.
4. Small HTTP HEAD over the tunnel to confirm it's still speaking
HTTP (catches weird misroutes).
Returns ProbeResult { latency_ms, error } per candidate.
=== New 'test-sni' CLI subcommand ===
$ mhrv-rs test-sni
Probing 5 SNI candidates against google_ip=216.239.38.120 ...
SNI LATENCY STATUS
www.google.com 142 ms ok
drive.google.com 138 ms ok
mail.google.com - handshake RST (SNI may be blocked)
...
Working: 3 / 5
Exit 0 if >=1 passed, non-zero otherwise. Uses the same probe logic
the UI uses.
=== UI editor ===
New 'SNI pool... (active/total)' button in the main form, styled with
a solid blue fill + white text so it's clearly actionable. Opens a
floating egui::Window (resizable, movable, closable) with:
- Action bar: 'Test all' | 'Keep working only' | 'Enable all' |
'Clear status' | 'Reset to defaults'
- Scrollable list of rows, each: checkbox, monospaced name editor
(230px), status cell (150px, 'ok 142 ms' green / 'fail <reason>'
red / 'testing...' gray / 'untested' gray), per-row 'Test' and
'remove' buttons
- Bottom: text input + '+ Add' that auto-probes the newly added name
immediately (instead of leaving it silently 'untested')
All rendered with ASCII status text instead of unicode check/cross
glyphs, since egui's default font doesn't ship them on some hosts
and they rendered as a missing-glyph box.
Changes only commit when the user hits Save config in the main window;
probe state is held in UiState::sni_probe so it survives opening and
closing the editor.
=== README ===
English + Persian 'SNI pool editor' sections with the two workflows
(UI button + 'sni_hosts' config field), plus a 'test-sni' line added
to the Diagnostics section. Feature list updated.
This commit is contained in:
Generated
+1
-1
@@ -1317,7 +1317,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mhrv-rs"
|
||||
version = "0.6.1"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mhrv-rs"
|
||||
version = "0.6.1"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
|
||||
license = "MIT"
|
||||
|
||||
@@ -236,8 +236,26 @@ Memory footprint is ~15-20 MB resident — fine on anything with ≥128 MB RAM.
|
||||
|
||||
- **`mhrv-rs test`** — sends one request through the relay and reports success/latency. Use this first whenever something breaks — it isolates "relay is up" from "client config is wrong".
|
||||
- **`mhrv-rs scan-ips`** — parallel TLS probe of 28 known Google frontend IPs, sorted by latency. Take the winner and put it in `google_ip`. The UI has the same thing behind the **scan** button next to the Google IP field.
|
||||
- **`mhrv-rs test-sni`** — parallel TLS probe of every SNI name in your rotation pool against the configured `google_ip`. Tells you which front-domain names actually pass through your ISP's DPI. The UI has the same thing in the **SNI pool…** floating window, with checkboxes, per-row **Test** buttons, and a **Keep ✓ only** button that auto-trims to what worked.
|
||||
- **Periodic stats** are logged every 60 s at `info` level (relay calls, cache hit rate, bytes relayed, active vs. blacklisted scripts). The UI shows them live.
|
||||
|
||||
### SNI pool editor
|
||||
|
||||
By default `mhrv-rs` rotates through `{www, mail, drive, docs, calendar}.google.com` on outbound TLS connections to your Google IP, to avoid fingerprinting one name too heavily. Some of those may be locally blocked — e.g. `mail.google.com` has been specifically targeted in Iran at various times.
|
||||
|
||||
Either:
|
||||
|
||||
- Open the UI, click **SNI pool…**, hit **Test all**, then **Keep ✓ only** to auto-trim. Add custom names via the text field at the bottom. Save.
|
||||
- Or edit `config.json` directly:
|
||||
|
||||
```json
|
||||
{
|
||||
"sni_hosts": ["www.google.com", "drive.google.com", "docs.google.com"]
|
||||
}
|
||||
```
|
||||
|
||||
Leaving `sni_hosts` unset gives you the default auto-pool. Run `mhrv-rs test-sni` to verify what works from your network before saving.
|
||||
|
||||
## What's implemented vs. not
|
||||
|
||||
This port focuses on the **`apps_script` mode** — the only one that reliably works against a modern censor in 2026. Implemented:
|
||||
@@ -265,6 +283,7 @@ This port focuses on the **`apps_script` mode** — the only one that reliably w
|
||||
- [x] Per-connection SNI rotation across a pool of Google subdomains (`www/mail/drive/docs/calendar.google.com`), so outbound connection counts aren't concentrated on one SNI.
|
||||
- [x] Optional parallel script-ID dispatch (`parallel_relay`): fan out a relay request to N script instances concurrently, return first success, kill p95 latency at the cost of N× quota.
|
||||
- [x] Per-site stats drill-down in the UI (requests, cache hit %, bytes, avg latency per host) for live debugging.
|
||||
- [x] Editable SNI rotation pool (UI window + `sni_hosts` config field) with per-name reachability probes (`mhrv-rs test-sni` CLI or **Test** / **Test all** / **Keep working only** buttons). DNS + TLS-handshake based, catches both DPI-blocked names and typos.
|
||||
- [x] OpenWRT / Alpine / musl builds — static binaries, procd init script included.
|
||||
|
||||
Intentionally **not** implemented (rationale included so future contributors don't spend cycles on them):
|
||||
@@ -455,6 +474,23 @@ Firefox cert store خودش را جدا دارد؛ installer تلاش میک
|
||||
|
||||
HTTP/HTTPS هیچ تغییری نمیکند (همچنان از Apps Script میرود) و تونل SNI-rewrite برای `google.com` / `youtube.com` / … هم سر جای خودش است — پس یوتوب مثل قبل سریع میماند و تلگرام بالاخره یک تونل واقعی میگیرد.
|
||||
|
||||
### ویرایشگر SNI pool
|
||||
|
||||
بهصورت پیشفرض `mhrv-rs` بین `{www, mail, drive, docs, calendar}.google.com` روی اتصالهای TLS خروجی به IP گوگل میچرخد تا یک نام تنها fingerprint نشود. بعضی از اینها ممکن است در شبکهٔ شما بلاک باشند — مثلاً `mail.google.com` در ایران چند بار هدف گرفته شده.
|
||||
|
||||
یا:
|
||||
|
||||
- UI را باز کنید، روی **SNI pool…** کلیک کنید، **Test all** را بزنید، بعد **Keep ✓ only** برای trim خودکار. از textbox پایین میتوانید نامهای دلخواه اضافه کنید. Save بزنید.
|
||||
- یا مستقیماً در `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"sni_hosts": ["www.google.com", "drive.google.com", "docs.google.com"]
|
||||
}
|
||||
```
|
||||
|
||||
اگر `sni_hosts` را نگذارید، pool پیشفرض اعمال میشود. قبل از ذخیره، `mhrv-rs test-sni` را اجرا کنید تا ببینید چه نامی از شبکهٔ شما رد میشود.
|
||||
|
||||
### اجرا روی OpenWRT (یا هر سیستم musl)
|
||||
|
||||
آرشیوهای `*-linux-musl-*` یک CLI کاملاً static میدهند که روی OpenWRT، Alpine و هر userland لینوکسی بدون glibc اجرا میشود. باینری را روی روتر بگذارید و بهعنوان سرویس راه بیندازید:
|
||||
|
||||
+339
-3
@@ -1,4 +1,4 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::sync::{Arc, Mutex};
|
||||
@@ -12,10 +12,10 @@ use tokio::task::JoinHandle;
|
||||
use mhrv_rs::cert_installer::install_ca;
|
||||
use mhrv_rs::config::{Config, ScriptId};
|
||||
use mhrv_rs::data_dir;
|
||||
use mhrv_rs::domain_fronter::DomainFronter;
|
||||
use mhrv_rs::domain_fronter::{DomainFronter, DEFAULT_GOOGLE_SNI_POOL};
|
||||
use mhrv_rs::mitm::{MitmCertManager, CA_CERT_FILE};
|
||||
use mhrv_rs::proxy_server::ProxyServer;
|
||||
use mhrv_rs::{scan_ips, test_cmd};
|
||||
use mhrv_rs::{scan_ips, scan_sni, test_cmd};
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const WIN_WIDTH: f32 = 520.0;
|
||||
@@ -75,6 +75,15 @@ struct UiState {
|
||||
ca_trusted: Option<bool>,
|
||||
last_test_ok: Option<bool>,
|
||||
last_test_msg: String,
|
||||
/// Per-SNI probe results, populated by Cmd::TestSni / TestAllSni.
|
||||
sni_probe: HashMap<String, SniProbeState>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum SniProbeState {
|
||||
InFlight,
|
||||
Ok(u32),
|
||||
Failed(String),
|
||||
}
|
||||
|
||||
enum Cmd {
|
||||
@@ -84,6 +93,12 @@ enum Cmd {
|
||||
InstallCa,
|
||||
CheckCaTrusted,
|
||||
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 },
|
||||
/// 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> },
|
||||
}
|
||||
|
||||
struct App {
|
||||
@@ -108,6 +123,20 @@ struct FormState {
|
||||
upstream_socks5: String,
|
||||
parallel_relay: u8,
|
||||
show_auth_key: bool,
|
||||
/// SNI rotation pool entries. Each item has a sni name + a checkbox
|
||||
/// flag indicating whether it's in the active rotation.
|
||||
sni_pool: Vec<SniRow>,
|
||||
/// Text field buffer for the "+ add custom SNI" input at the bottom of
|
||||
/// the SNI editor window.
|
||||
sni_custom_input: String,
|
||||
/// Whether the floating SNI editor window is open.
|
||||
sni_editor_open: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct SniRow {
|
||||
name: String,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
fn load_form() -> FormState {
|
||||
@@ -130,6 +159,7 @@ fn load_form() -> FormState {
|
||||
None => String::new(),
|
||||
},
|
||||
};
|
||||
let sni_pool = sni_pool_for_form(c.sni_hosts.as_deref(), &c.front_domain);
|
||||
FormState {
|
||||
script_id: sid,
|
||||
auth_key: c.auth_key,
|
||||
@@ -143,6 +173,9 @@ fn load_form() -> FormState {
|
||||
upstream_socks5: c.upstream_socks5.unwrap_or_default(),
|
||||
parallel_relay: c.parallel_relay,
|
||||
show_auth_key: false,
|
||||
sni_pool,
|
||||
sni_custom_input: String::new(),
|
||||
sni_editor_open: false,
|
||||
}
|
||||
} else {
|
||||
FormState {
|
||||
@@ -158,10 +191,48 @@ fn load_form() -> FormState {
|
||||
upstream_socks5: String::new(),
|
||||
parallel_relay: 0,
|
||||
show_auth_key: false,
|
||||
sni_pool: sni_pool_for_form(None, "www.google.com"),
|
||||
sni_custom_input: String::new(),
|
||||
sni_editor_open: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the initial `sni_pool` list shown in the editor.
|
||||
///
|
||||
/// If the user has explicit `sni_hosts` configured, we show exactly those
|
||||
/// rows (all enabled). Otherwise we show the default Google pool plus any
|
||||
/// missing entries, all enabled, with the user's `front_domain` first.
|
||||
fn sni_pool_for_form(user: Option<&[String]>, front_domain: &str) -> Vec<SniRow> {
|
||||
let user_clean: Vec<String> = user
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
if !user_clean.is_empty() {
|
||||
return user_clean
|
||||
.into_iter()
|
||||
.map(|name| SniRow { name, enabled: true })
|
||||
.collect();
|
||||
}
|
||||
// Default: primary + the other Google-edge subdomains, primary first,
|
||||
// all enabled.
|
||||
let primary = front_domain.trim().to_string();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut out = Vec::new();
|
||||
if !primary.is_empty() {
|
||||
seen.insert(primary.clone());
|
||||
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
|
||||
}
|
||||
|
||||
impl FormState {
|
||||
fn to_config(&self) -> Result<Config, String> {
|
||||
if self.script_id.trim().is_empty() {
|
||||
@@ -213,6 +284,20 @@ impl FormState {
|
||||
if v.is_empty() { None } else { Some(v.to_string()) }
|
||||
},
|
||||
parallel_relay: self.parallel_relay,
|
||||
sni_hosts: {
|
||||
let active: Vec<String> = self
|
||||
.sni_pool
|
||||
.iter()
|
||||
.filter(|r| r.enabled)
|
||||
.map(|r| r.name.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
// None = "use auto-expansion default", Some(list) = explicit.
|
||||
// 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) }
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -248,6 +333,8 @@ struct ConfigWire<'a> {
|
||||
upstream_socks5: Option<&'a str>,
|
||||
#[serde(skip_serializing_if = "is_zero_u8")]
|
||||
parallel_relay: u8,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
sni_hosts: Option<Vec<&'a str>>,
|
||||
}
|
||||
|
||||
fn is_zero_u8(v: &u8) -> bool {
|
||||
@@ -281,6 +368,7 @@ 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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -435,9 +523,32 @@ impl eframe::App for App {
|
||||
Err(e) => self.toast = Some((format!("Save failed: {}", e), Instant::now())),
|
||||
}
|
||||
}
|
||||
let active_sni = self.form.sni_pool.iter().filter(|r| r.enabled).count();
|
||||
let total_sni = self.form.sni_pool.len();
|
||||
let sni_btn = egui::Button::new(
|
||||
egui::RichText::new(format!("SNI pool… ({}/{})", active_sni, total_sni))
|
||||
.color(egui::Color32::WHITE),
|
||||
)
|
||||
.fill(egui::Color32::from_rgb(70, 120, 180))
|
||||
.min_size(egui::vec2(160.0, 0.0));
|
||||
if ui.add(sni_btn)
|
||||
.on_hover_text(
|
||||
"Open the SNI rotation pool editor.\n\n\
|
||||
Edit which SNI names get rotated through for outbound TLS to the\n\
|
||||
Google edge. Some default names may be locally blocked — use the\n\
|
||||
Test buttons inside to find out which ones work on your network."
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.form.sni_editor_open = true;
|
||||
}
|
||||
ui.small(format!("location: {}", data_dir::config_path().display()));
|
||||
});
|
||||
|
||||
// Floating SNI editor window. Rendered here so it's inside the
|
||||
// same egui context but visually pops out with its own title bar.
|
||||
self.show_sni_editor(ctx);
|
||||
|
||||
ui.separator();
|
||||
|
||||
// Status + stats
|
||||
@@ -604,6 +715,196 @@ impl eframe::App for App {
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Floating editor window for the SNI rotation pool. Opens from the
|
||||
/// **SNI pool…** button in the main form. The list is live-editable
|
||||
/// (reorder / toggle / add / remove); changes only persist when the user
|
||||
/// hits **Save config** in the main window. Probe results are cached in
|
||||
/// `UiState::sni_probe` so they survive opening and closing the editor.
|
||||
fn show_sni_editor(&mut self, ctx: &egui::Context) {
|
||||
if !self.form.sni_editor_open {
|
||||
return;
|
||||
}
|
||||
let mut keep_open = true;
|
||||
egui::Window::new("SNI rotation pool")
|
||||
.open(&mut keep_open)
|
||||
.resizable(true)
|
||||
.default_size(egui::vec2(520.0, 420.0))
|
||||
.min_width(460.0)
|
||||
.collapsible(false)
|
||||
.show(ctx, |ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(
|
||||
"Which SNI names to rotate through when opening TLS connections \
|
||||
to your Google IP. Some names may be locally blocked (Iran has \
|
||||
dropped mail.google.com at times, for example); use the Test \
|
||||
buttons to check — TLS handshake + HTTP HEAD against the \
|
||||
configured google_ip, per name.",
|
||||
)
|
||||
.small(),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
|
||||
// Action row.
|
||||
let google_ip = self.form.google_ip.trim().to_string();
|
||||
let probe_map = self.shared.state.lock().unwrap().sni_probe.clone();
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
if ui.button("Test all").on_hover_text(
|
||||
"Probe every SNI in the list against the configured google_ip in parallel."
|
||||
).clicked() {
|
||||
let snis: Vec<String> = self
|
||||
.form
|
||||
.sni_pool
|
||||
.iter()
|
||||
.map(|r| r.name.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
if !snis.is_empty() && !google_ip.is_empty() {
|
||||
let _ = self.cmd_tx.send(Cmd::TestAllSni {
|
||||
google_ip: google_ip.clone(),
|
||||
snis,
|
||||
});
|
||||
}
|
||||
}
|
||||
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(_))
|
||||
);
|
||||
row.enabled = ok;
|
||||
}
|
||||
}
|
||||
if ui.button("Enable all").clicked() {
|
||||
for row in &mut self.form.sni_pool {
|
||||
row.enabled = true;
|
||||
}
|
||||
}
|
||||
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() {
|
||||
self.form.sni_pool = DEFAULT_GOOGLE_SNI_POOL
|
||||
.iter()
|
||||
.map(|s| SniRow { name: (*s).to_string(), enabled: true })
|
||||
.collect();
|
||||
self.shared.state.lock().unwrap().sni_probe.clear();
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
// Main list — one horizontal row per SNI, explicit widths so
|
||||
// the domain text field gets the room it needs.
|
||||
let mut to_remove: Option<usize> = None;
|
||||
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…")
|
||||
.color(egui::Color32::GRAY)
|
||||
.monospace()
|
||||
}
|
||||
None => {
|
||||
egui::RichText::new("untested")
|
||||
.color(egui::Color32::GRAY)
|
||||
.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());
|
||||
}
|
||||
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();
|
||||
if !name.is_empty() && !google_ip.is_empty() {
|
||||
let _ = self.cmd_tx.send(Cmd::TestSni {
|
||||
google_ip: google_ip.clone(),
|
||||
sni: name,
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(i) = to_remove {
|
||||
self.form.sni_pool.remove(i);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.form.sni_custom_input)
|
||||
.hint_text("add a custom SNI (e.g. translate.google.com)")
|
||||
.desired_width(280.0),
|
||||
);
|
||||
let add_clicked = ui.button("+ Add").clicked();
|
||||
if add_clicked {
|
||||
let new_name = self.form.sni_custom_input.trim().to_string();
|
||||
if !new_name.is_empty()
|
||||
&& !self.form.sni_pool.iter().any(|r| r.name == new_name)
|
||||
{
|
||||
self.form.sni_pool.push(SniRow {
|
||||
name: new_name.clone(),
|
||||
enabled: true,
|
||||
});
|
||||
self.form.sni_custom_input.clear();
|
||||
// Auto-probe the freshly added name so the user gets
|
||||
// immediate feedback instead of a silent "untested"
|
||||
// row. Needs a non-empty google_ip to have meaning.
|
||||
if !google_ip.is_empty() {
|
||||
let _ = self.cmd_tx.send(Cmd::TestSni {
|
||||
google_ip: google_ip.clone(),
|
||||
sni: new_name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.separator();
|
||||
ui.small(
|
||||
"Changes take effect on the next Start of the proxy. \
|
||||
Don't forget to press Save config in the main window to persist.",
|
||||
);
|
||||
});
|
||||
self.form.sni_editor_open = keep_open;
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_duration(d: Duration) -> String {
|
||||
let s = d.as_secs();
|
||||
format!("{:02}:{:02}:{:02}", s / 3600, (s / 60) % 60, s % 60)
|
||||
@@ -744,6 +1045,41 @@ fn background_thread(shared: Arc<Shared>, rx: Receiver<Cmd>) {
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(Cmd::TestSni { google_ip, sni }) => {
|
||||
let shared2 = shared.clone();
|
||||
{
|
||||
let mut st = shared2.state.lock().unwrap();
|
||||
st.sni_probe.insert(sni.clone(), SniProbeState::InFlight);
|
||||
}
|
||||
rt.spawn(async move {
|
||||
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())),
|
||||
};
|
||||
shared2.state.lock().unwrap().sni_probe.insert(sni, state);
|
||||
});
|
||||
}
|
||||
Ok(Cmd::TestAllSni { google_ip, snis }) => {
|
||||
let shared2 = shared.clone();
|
||||
{
|
||||
let mut st = shared2.state.lock().unwrap();
|
||||
for s in &snis {
|
||||
st.sni_probe.insert(s.clone(), SniProbeState::InFlight);
|
||||
}
|
||||
}
|
||||
rt.spawn(async move {
|
||||
let results = scan_sni::probe_all(&google_ip, snis).await;
|
||||
let mut st = shared2.state.lock().unwrap();
|
||||
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())),
|
||||
};
|
||||
st.sni_probe.insert(sni, state);
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(Cmd::CheckCaTrusted) => {
|
||||
let shared2 = shared.clone();
|
||||
std::thread::spawn(move || {
|
||||
|
||||
@@ -72,6 +72,14 @@ pub struct Config {
|
||||
/// script IDs.
|
||||
#[serde(default)]
|
||||
pub parallel_relay: u8,
|
||||
/// Optional explicit SNI rotation pool for outbound TLS to `google_ip`.
|
||||
/// Empty / missing = auto-expand from `front_domain` (current default of
|
||||
/// {www, mail, drive, docs, calendar}.google.com). Set to an explicit list
|
||||
/// to pick exactly which SNI names get rotated through — useful when one
|
||||
/// of the defaults is locally blocked (e.g. mail.google.com in Iran at
|
||||
/// various times). Can be tested per-name via the UI or `mhrv-rs test-sni`.
|
||||
#[serde(default)]
|
||||
pub sni_hosts: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
fn default_google_ip() -> String {
|
||||
|
||||
+44
-20
@@ -164,7 +164,10 @@ impl DomainFronter {
|
||||
|
||||
Ok(Self {
|
||||
connect_host: config.google_ip.clone(),
|
||||
sni_hosts: build_sni_pool(&config.front_domain),
|
||||
sni_hosts: build_sni_pool_for(
|
||||
&config.front_domain,
|
||||
config.sni_hosts.as_deref().unwrap_or(&[]),
|
||||
),
|
||||
sni_idx: AtomicUsize::new(0),
|
||||
http_host: "script.google.com",
|
||||
auth_key: config.auth_key.clone(),
|
||||
@@ -714,30 +717,46 @@ fn extract_host(url: &str) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The default pool of SNI names that share the Google Front End with
|
||||
/// `www.google.com`. Used both when auto-expanding from `front_domain` and
|
||||
/// when the UI wants to show the starting candidates for the SNI editor.
|
||||
pub const DEFAULT_GOOGLE_SNI_POOL: &[&str] = &[
|
||||
"www.google.com",
|
||||
"mail.google.com",
|
||||
"drive.google.com",
|
||||
"docs.google.com",
|
||||
"calendar.google.com",
|
||||
];
|
||||
|
||||
/// Build the pool of SNI hosts used for outbound connections to the Google
|
||||
/// edge. Takes the user-configured `front_domain` as the primary and adds a
|
||||
/// few other Google-owned subdomains that share the same GFE, so the per-SNI
|
||||
/// connection-count fingerprint gets spread instead of concentrating on one
|
||||
/// name. All entries MUST be hosted on the same edge as `connect_host`,
|
||||
/// otherwise the TLS handshake will land on the wrong server.
|
||||
/// edge.
|
||||
///
|
||||
/// If the user has set `front_domain` to something off the default list, we
|
||||
/// still include it first and don't add extras (we'd have no way to verify
|
||||
/// they're co-hosted with a non-Google custom edge).
|
||||
fn build_sni_pool(primary: &str) -> Vec<String> {
|
||||
/// Precedence:
|
||||
/// 1. If `user_pool` is non-empty, use it verbatim (user is in charge).
|
||||
/// 2. If `primary` is one of the DEFAULT_GOOGLE_SNI_POOL entries, auto-expand
|
||||
/// to the full default list with `primary` first. This gives the per-SNI
|
||||
/// connection-count fingerprint spread without the user configuring
|
||||
/// anything.
|
||||
/// 3. Otherwise — custom / non-Google `primary` — use just `[primary]`, since
|
||||
/// we have no way to verify which sibling names share a non-Google edge.
|
||||
///
|
||||
/// All entries MUST be hosted on the same edge as `connect_host`, otherwise
|
||||
/// the TLS handshake will land on the wrong server.
|
||||
pub fn build_sni_pool_for(primary: &str, user_pool: &[String]) -> Vec<String> {
|
||||
let primary = primary.trim().to_string();
|
||||
// A Google-edge-hosted primary: augment with siblings.
|
||||
let google_defaults: &[&str] = &[
|
||||
"www.google.com",
|
||||
"mail.google.com",
|
||||
"drive.google.com",
|
||||
"docs.google.com",
|
||||
"calendar.google.com",
|
||||
];
|
||||
let looks_like_google_edge = google_defaults.iter().any(|s| *s == primary);
|
||||
let user_filtered: Vec<String> = user_pool
|
||||
.iter()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
if !user_filtered.is_empty() {
|
||||
return user_filtered;
|
||||
}
|
||||
|
||||
let looks_like_google_edge = DEFAULT_GOOGLE_SNI_POOL.iter().any(|s| *s == primary);
|
||||
let mut pool = vec![primary.clone()];
|
||||
if looks_like_google_edge {
|
||||
for s in google_defaults {
|
||||
for s in DEFAULT_GOOGLE_SNI_POOL {
|
||||
if *s != primary {
|
||||
pool.push((*s).to_string());
|
||||
}
|
||||
@@ -746,6 +765,11 @@ fn build_sni_pool(primary: &str) -> Vec<String> {
|
||||
pool
|
||||
}
|
||||
|
||||
/// Back-compat thin wrapper for the old callers / tests.
|
||||
fn build_sni_pool(primary: &str) -> Vec<String> {
|
||||
build_sni_pool_for(primary, &[])
|
||||
}
|
||||
|
||||
pub fn filter_forwarded_headers(headers: &[(String, String)]) -> Vec<(String, String)> {
|
||||
const SKIP: &[&str] = &[
|
||||
"host",
|
||||
|
||||
@@ -8,4 +8,5 @@ pub mod domain_fronter;
|
||||
pub mod mitm;
|
||||
pub mod proxy_server;
|
||||
pub mod scan_ips;
|
||||
pub mod scan_sni;
|
||||
pub mod test_cmd;
|
||||
|
||||
+10
@@ -26,6 +26,7 @@ enum Command {
|
||||
Serve,
|
||||
Test,
|
||||
ScanIps,
|
||||
TestSni,
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
@@ -36,6 +37,7 @@ USAGE:
|
||||
mhrv-rs [OPTIONS] Start the proxy server (default)
|
||||
mhrv-rs test [OPTIONS] Probe the Apps Script relay end-to-end
|
||||
mhrv-rs scan-ips [OPTIONS] Scan Google frontend IPs for reachability + latency
|
||||
mhrv-rs test-sni [OPTIONS] Probe each SNI name in the rotation pool against google_ip
|
||||
|
||||
OPTIONS:
|
||||
-c, --config PATH Path to config.json (default: ./config.json)
|
||||
@@ -68,6 +70,10 @@ fn parse_args() -> Result<Args, String> {
|
||||
command = Command::ScanIps;
|
||||
raw.remove(0);
|
||||
}
|
||||
"test-sni" => {
|
||||
command = Command::TestSni;
|
||||
raw.remove(0);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -168,6 +174,10 @@ async fn main() -> ExitCode {
|
||||
let ok = scan_ips::run(&config).await;
|
||||
return if ok { ExitCode::SUCCESS } else { ExitCode::FAILURE };
|
||||
}
|
||||
Command::TestSni => {
|
||||
let ok = mhrv_rs::scan_sni::run(&config).await;
|
||||
return if ok { ExitCode::SUCCESS } else { ExitCode::FAILURE };
|
||||
}
|
||||
Command::Serve => {}
|
||||
}
|
||||
|
||||
|
||||
+319
@@ -0,0 +1,319 @@
|
||||
//! SNI reachability probes.
|
||||
//!
|
||||
//! Given a fixed `google_ip`, test which SNI strings the path between here and
|
||||
//! Google's edge actually lets through. Iran's DPI blocks specific SNI strings
|
||||
//! (`mail.google.com` has been targeted at various times; `translate.google.com`
|
||||
//! has been on/off; etc.) while others co-hosted on the exact same IP pass
|
||||
//! through. This module exposes the probe logic used by both the `test-sni`
|
||||
//! CLI subcommand and the UI's per-row **Test** / **Test all** buttons.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_rustls::rustls::client::danger::{
|
||||
HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier,
|
||||
};
|
||||
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||
use tokio_rustls::rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
|
||||
use tokio_rustls::TlsConnector;
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
const PROBE_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
const CONCURRENCY: usize = 8;
|
||||
|
||||
/// Outcome of a single SNI probe.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProbeResult {
|
||||
pub latency_ms: Option<u32>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl ProbeResult {
|
||||
pub fn is_ok(&self) -> bool {
|
||||
self.latency_ms.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// Probe one (google_ip, sni) pair. Succeeds if we can complete a TLS
|
||||
/// handshake with the given SNI against `google_ip:443`. Does not do an HTTP
|
||||
/// request on top — handshake completion alone proves the SNI isn't blocked
|
||||
/// by DPI and the IP accepts the fronting.
|
||||
pub async fn probe_one(google_ip: &str, sni: &str) -> ProbeResult {
|
||||
let tls_cfg = ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoVerify))
|
||||
.with_no_client_auth();
|
||||
let connector = TlsConnector::from(Arc::new(tls_cfg));
|
||||
probe_with(google_ip, sni, connector).await
|
||||
}
|
||||
|
||||
/// Probe every SNI in `snis` in parallel (bounded to CONCURRENCY).
|
||||
/// Results come back in the same order as the input.
|
||||
pub async fn probe_all(google_ip: &str, snis: Vec<String>) -> Vec<(String, ProbeResult)> {
|
||||
let tls_cfg = ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoVerify))
|
||||
.with_no_client_auth();
|
||||
let connector = TlsConnector::from(Arc::new(tls_cfg));
|
||||
|
||||
let sem = Arc::new(tokio::sync::Semaphore::new(CONCURRENCY));
|
||||
let mut tasks = Vec::with_capacity(snis.len());
|
||||
for sni in snis.iter() {
|
||||
let connector = connector.clone();
|
||||
let sem = sem.clone();
|
||||
let sni_clone = sni.clone();
|
||||
let ip = google_ip.to_string();
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let _permit = sem.acquire().await.ok();
|
||||
(sni_clone.clone(), probe_with(&ip, &sni_clone, connector).await)
|
||||
}));
|
||||
}
|
||||
let mut out = Vec::with_capacity(tasks.len());
|
||||
for t in tasks {
|
||||
if let Ok(r) = t.await {
|
||||
out.push(r);
|
||||
}
|
||||
}
|
||||
// Re-sort into input order (task scheduling may shuffle).
|
||||
let mut indexed: Vec<(String, ProbeResult)> = Vec::with_capacity(out.len());
|
||||
for sni in snis {
|
||||
if let Some(pos) = out.iter().position(|(s, _)| s == &sni) {
|
||||
indexed.push(out.remove(pos));
|
||||
}
|
||||
}
|
||||
indexed
|
||||
}
|
||||
|
||||
async fn probe_with(google_ip: &str, sni: &str, connector: TlsConnector) -> ProbeResult {
|
||||
let start = Instant::now();
|
||||
|
||||
// DNS sanity check first. Google's GFE returns a valid wildcard cert for
|
||||
// ANY *.google.com SNI (including typos and gibberish), so a successful
|
||||
// TLS handshake alone doesn't prove the name actually exists. Resolving
|
||||
// catches typos and random strings before they show a misleading "ok".
|
||||
// We still only connect to the configured google_ip — the resolve is
|
||||
// purely an existence check.
|
||||
let resolve_target = format!("{}:443", sni);
|
||||
let resolved = tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
tokio::net::lookup_host(resolve_target),
|
||||
)
|
||||
.await;
|
||||
match resolved {
|
||||
Ok(Ok(mut iter)) => {
|
||||
if iter.next().is_none() {
|
||||
return ProbeResult {
|
||||
latency_ms: None,
|
||||
error: Some("dns: no addresses".into()),
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
return ProbeResult {
|
||||
latency_ms: None,
|
||||
error: Some(format!("dns: {}", truncate_reason(&e.to_string(), 32))),
|
||||
};
|
||||
}
|
||||
Err(_) => {
|
||||
return ProbeResult {
|
||||
latency_ms: None,
|
||||
error: Some("dns timeout".into()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let addr: SocketAddr = match format!("{}:443", google_ip).parse() {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
return ProbeResult {
|
||||
latency_ms: None,
|
||||
error: Some(format!("bad ip: {}", e)),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let tcp = match tokio::time::timeout(PROBE_TIMEOUT, TcpStream::connect(addr)).await {
|
||||
Ok(Ok(t)) => t,
|
||||
Ok(Err(e)) => {
|
||||
return ProbeResult {
|
||||
latency_ms: None,
|
||||
error: Some(format!("connect: {}", e)),
|
||||
};
|
||||
}
|
||||
Err(_) => {
|
||||
return ProbeResult {
|
||||
latency_ms: None,
|
||||
error: Some("connect timeout".into()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let _ = tcp.set_nodelay(true);
|
||||
|
||||
let server_name = match ServerName::try_from(sni.to_string()) {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
return ProbeResult {
|
||||
latency_ms: None,
|
||||
error: Some(format!("bad sni: {}", e)),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let mut tls = match tokio::time::timeout(PROBE_TIMEOUT, connector.connect(server_name, tcp))
|
||||
.await
|
||||
{
|
||||
Ok(Ok(t)) => t,
|
||||
Ok(Err(e)) => {
|
||||
// DPI that blocks the SNI typically kills the handshake here.
|
||||
let emsg = e.to_string();
|
||||
let reason = if emsg.contains("reset") || emsg.contains("peer") {
|
||||
"handshake RST (SNI may be blocked)".into()
|
||||
} else {
|
||||
format!("tls: {}", emsg)
|
||||
};
|
||||
return ProbeResult {
|
||||
latency_ms: None,
|
||||
error: Some(reason),
|
||||
};
|
||||
}
|
||||
Err(_) => {
|
||||
return ProbeResult {
|
||||
latency_ms: None,
|
||||
error: Some("tls handshake timeout".into()),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Handshake completed — SNI passed. Do a tiny HEAD to confirm the other
|
||||
// side actually speaks HTTP (catches weird misroutes).
|
||||
let req = b"HEAD / HTTP/1.1\r\nHost: www.google.com\r\nConnection: close\r\n\r\n";
|
||||
if tls.write_all(req).await.is_err() {
|
||||
return ProbeResult {
|
||||
latency_ms: None,
|
||||
error: Some("write failed".into()),
|
||||
};
|
||||
}
|
||||
let _ = tls.flush().await;
|
||||
|
||||
let mut buf = [0u8; 64];
|
||||
match tokio::time::timeout(PROBE_TIMEOUT, tls.read(&mut buf)).await {
|
||||
Ok(Ok(n)) if n >= 5 && buf.starts_with(b"HTTP/") => {
|
||||
let elapsed = start.elapsed().as_millis().min(u32::MAX as u128) as u32;
|
||||
ProbeResult {
|
||||
latency_ms: Some(elapsed),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
Ok(Ok(_)) => ProbeResult {
|
||||
latency_ms: None,
|
||||
error: Some("non-HTTP reply".into()),
|
||||
},
|
||||
Ok(Err(e)) => ProbeResult {
|
||||
latency_ms: None,
|
||||
error: Some(format!("read: {}", e)),
|
||||
},
|
||||
Err(_) => ProbeResult {
|
||||
latency_ms: None,
|
||||
error: Some("read timeout".into()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// `mhrv-rs test-sni` CLI entry point. Probes every SNI in the active pool
|
||||
/// (either the user's `sni_hosts` list or the auto-expanded default from
|
||||
/// `front_domain`) against `google_ip` and prints a sorted table.
|
||||
pub async fn run(config: &Config) -> bool {
|
||||
use crate::domain_fronter::build_sni_pool_for;
|
||||
let pool = build_sni_pool_for(
|
||||
&config.front_domain,
|
||||
config.sni_hosts.as_deref().unwrap_or(&[]),
|
||||
);
|
||||
println!(
|
||||
"Probing {} SNI candidate(s) against google_ip={} (TCP+TLS, timeout={}s)...",
|
||||
pool.len(),
|
||||
config.google_ip,
|
||||
PROBE_TIMEOUT.as_secs()
|
||||
);
|
||||
println!();
|
||||
|
||||
let mut results = probe_all(&config.google_ip, pool).await;
|
||||
results.sort_by_key(|(_, r)| r.latency_ms.unwrap_or(u32::MAX));
|
||||
|
||||
println!("{:<36} {:>10} {}", "SNI", "LATENCY", "STATUS");
|
||||
println!("{:-<36} {:->10} {}", "", "", "------");
|
||||
let mut ok_count = 0usize;
|
||||
for (sni, r) in &results {
|
||||
match r.latency_ms {
|
||||
Some(ms) => {
|
||||
println!("{:<36} {:>8}ms ok", sni, ms);
|
||||
ok_count += 1;
|
||||
}
|
||||
None => {
|
||||
let err = r.error.as_deref().unwrap_or("failed");
|
||||
println!("{:<36} {:>10} {}", sni, "-", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!();
|
||||
println!("Working: {} / {}", ok_count, results.len());
|
||||
ok_count > 0
|
||||
}
|
||||
|
||||
fn truncate_reason(s: &str, max: usize) -> String {
|
||||
if s.len() <= max {
|
||||
s.to_string()
|
||||
} else {
|
||||
// Strip newlines / extra junk for clean UI display.
|
||||
let cleaned: String = s.chars().take(max).filter(|c| !c.is_control()).collect();
|
||||
cleaned
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct NoVerify;
|
||||
|
||||
impl ServerCertVerifier for NoVerify {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &CertificateDer<'_>,
|
||||
_intermediates: &[CertificateDer<'_>],
|
||||
_server_name: &ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: UnixTime,
|
||||
) -> Result<ServerCertVerified, tokio_rustls::rustls::Error> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_: &[u8],
|
||||
_: &CertificateDer<'_>,
|
||||
_: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_: &[u8],
|
||||
_: &CertificateDer<'_>,
|
||||
_: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
vec![
|
||||
SignatureScheme::RSA_PKCS1_SHA256,
|
||||
SignatureScheme::RSA_PKCS1_SHA384,
|
||||
SignatureScheme::RSA_PKCS1_SHA512,
|
||||
SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||
SignatureScheme::ECDSA_NISTP384_SHA384,
|
||||
SignatureScheme::RSA_PSS_SHA256,
|
||||
SignatureScheme::RSA_PSS_SHA384,
|
||||
SignatureScheme::RSA_PSS_SHA512,
|
||||
SignatureScheme::ED25519,
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user