Skip to content

Commit 09f1f5f

Browse files
committed
proxy: add youtube_via_relay config toggle (#102)
Ports the upstream Python `youtube_via_relay` flag (commit a0fd8a0 in masterking32/MasterHttpRelayVPN). When enabled, YouTube-family suffixes (youtube.com, youtu.be, youtube-nocookie.com, ytimg.com) opt out of the SNI-rewrite tunnel and fall through to the Apps Script relay path. Why it helps some users: when YouTube is reached via SNI-rewrite to google_ip with SNI=www.google.com, Google's frontend can enforce SafeSearch / Restricted Mode based on the SNI name, causing "video restricted" errors on some regular videos. Routing through Apps Script bypasses that specific filter at the cost of (a) UrlFetchApp's fixed `User-Agent: Google-Apps-Script`, and (b) counting YouTube traffic against the script's daily quota. Off by default so existing behaviour is unchanged. Users who hit the SafeSearch-on-SNI issue can set `"youtube_via_relay": true` in their config.json and observe. Explicit `hosts` overrides always beat the toggle — that's a user choice and should win over the default policy. Added tests for all three branches (youtube_via_relay off, on, and with hosts override). Matching Android-side UI toggle deferred — `normalize_x_graphql` is also config-only on Android today; users can edit config.json directly if needed.
1 parent 26d2d36 commit 09f1f5f

2 files changed

Lines changed: 90 additions & 7 deletions

File tree

src/config.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,23 @@ pub struct Config {
127127
/// features. Turn on and observe.
128128
#[serde(default)]
129129
pub normalize_x_graphql: bool,
130+
131+
/// Route YouTube traffic through the Apps Script relay instead of
132+
/// the direct SNI-rewrite tunnel. Ported from upstream Python
133+
/// `youtube_via_relay` (issue #102).
134+
///
135+
/// Why this exists: when YouTube is SNI-rewritten to `google_ip`
136+
/// with `SNI=www.google.com`, Google's frontend can enforce
137+
/// SafeSearch / Restricted Mode based on the SNI → some videos show
138+
/// as "restricted." Routing through Apps Script bypasses that check
139+
/// (it hits YouTube from Google's own backend, not via www.google.com
140+
/// SNI) but introduces the UrlFetchApp User-Agent and quota costs.
141+
///
142+
/// Trade-off: enabling removes SafeSearch-on-SNI, adds `User-Agent:
143+
/// Google-Apps-Script` header and counts YouTube traffic against
144+
/// your Apps Script quota. Off by default.
145+
#[serde(default)]
146+
pub youtube_via_relay: bool,
130147
}
131148

132149
fn default_fetch_ips_from_api() -> bool { false }

src/proxy_server.rs

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,27 @@ const SNI_REWRITE_SUFFIXES: &[&str] = &[
6767
"blogger.com",
6868
];
6969

70-
fn matches_sni_rewrite(host: &str) -> bool {
70+
/// YouTube-family suffixes. Extracted so `youtube_via_relay` config can
71+
/// pull them out of the SNI-rewrite dispatch at runtime.
72+
const YOUTUBE_SNI_SUFFIXES: &[&str] = &[
73+
"youtube.com",
74+
"youtu.be",
75+
"youtube-nocookie.com",
76+
"ytimg.com",
77+
];
78+
79+
fn matches_sni_rewrite(host: &str, youtube_via_relay: bool) -> bool {
7180
let h = host.to_ascii_lowercase();
7281
let h = h.trim_end_matches('.');
7382
SNI_REWRITE_SUFFIXES
7483
.iter()
84+
.filter(|s| {
85+
// If the user opted into youtube_via_relay, skip YouTube
86+
// suffixes so they fall through to the Apps Script relay
87+
// path. See config.rs `youtube_via_relay` docs for the
88+
// trade-off. Issue #102.
89+
!(youtube_via_relay && YOUTUBE_SNI_SUFFIXES.contains(s))
90+
})
7591
.any(|s| h == *s || h.ends_with(&format!(".{}", s)))
7692
}
7793

@@ -118,6 +134,10 @@ pub struct RewriteCtx {
118134
pub tls_connector: TlsConnector,
119135
pub upstream_socks5: Option<String>,
120136
pub mode: Mode,
137+
/// If true, YouTube traffic bypasses the SNI-rewrite tunnel and
138+
/// goes through the Apps Script relay instead. See config.rs for
139+
/// the trade-off. Issue #102.
140+
pub youtube_via_relay: bool,
121141
}
122142

123143
impl ProxyServer {
@@ -160,6 +180,7 @@ impl ProxyServer {
160180
tls_connector,
161181
upstream_socks5: config.upstream_socks5.clone(),
162182
mode,
183+
youtube_via_relay: config.youtube_via_relay,
163184
});
164185

165186
let socks5_port = config.socks5_port.unwrap_or(config.listen_port + 1);
@@ -473,13 +494,20 @@ fn should_use_sni_rewrite(
473494
hosts: &std::collections::HashMap<String, String>,
474495
host: &str,
475496
port: u16,
497+
youtube_via_relay: bool,
476498
) -> bool {
477499
// The SNI-rewrite path expects TLS from the client: it accepts inbound
478500
// TLS, then opens a second TLS connection to the Google edge with a front
479501
// SNI. Auto-forcing that path for non-TLS ports (for example a SOCKS5
480502
// CONNECT to google.com:80) makes the proxy wait for a ClientHello that
481503
// will never arrive.
482-
port == 443 && (matches_sni_rewrite(host) || hosts_override(hosts, host).is_some())
504+
//
505+
// youtube_via_relay=true removes YouTube suffixes from the match so
506+
// YouTube traffic falls through to the Apps Script relay path instead
507+
// of the SNI-rewrite tunnel. An explicit hosts override still wins
508+
// over the config toggle.
509+
port == 443
510+
&& (matches_sni_rewrite(host, youtube_via_relay) || hosts_override(hosts, host).is_some())
483511
}
484512

485513
async fn dispatch_tunnel(
@@ -492,7 +520,12 @@ async fn dispatch_tunnel(
492520
) -> std::io::Result<()> {
493521
// 1. Explicit hosts override or SNI-rewrite suffix: for HTTPS targets,
494522
// always use the TLS SNI-rewrite tunnel.
495-
if should_use_sni_rewrite(&rewrite_ctx.hosts, &host, port) {
523+
if should_use_sni_rewrite(
524+
&rewrite_ctx.hosts,
525+
&host,
526+
port,
527+
rewrite_ctx.youtube_via_relay,
528+
) {
496529
tracing::info!("dispatch {}:{} -> sni-rewrite tunnel (Google edge direct)", host, port);
497530
return do_sni_rewrite_tunnel_from_tcp(sock, &host, port, mitm, rewrite_ctx).await;
498531
}
@@ -1537,9 +1570,42 @@ mod tests {
15371570
let mut hosts = std::collections::HashMap::new();
15381571
hosts.insert("example.com".to_string(), "1.2.3.4".to_string());
15391572

1540-
assert!(should_use_sni_rewrite(&hosts, "google.com", 443));
1541-
assert!(!should_use_sni_rewrite(&hosts, "google.com", 80));
1542-
assert!(should_use_sni_rewrite(&hosts, "www.example.com", 443));
1543-
assert!(!should_use_sni_rewrite(&hosts, "www.example.com", 80));
1573+
assert!(should_use_sni_rewrite(&hosts, "google.com", 443, false));
1574+
assert!(!should_use_sni_rewrite(&hosts, "google.com", 80, false));
1575+
assert!(should_use_sni_rewrite(&hosts, "www.example.com", 443, false));
1576+
assert!(!should_use_sni_rewrite(&hosts, "www.example.com", 80, false));
1577+
}
1578+
1579+
#[test]
1580+
fn youtube_via_relay_routes_youtube_through_relay_path() {
1581+
// Issue #102. When youtube_via_relay=true, YouTube suffixes
1582+
// must NOT match the SNI-rewrite path, so traffic falls
1583+
// through to Apps Script relay. Other Google suffixes are
1584+
// unaffected.
1585+
let hosts = std::collections::HashMap::new();
1586+
1587+
// Default behaviour: everything in the pool rewrites.
1588+
assert!(should_use_sni_rewrite(&hosts, "www.youtube.com", 443, false));
1589+
assert!(should_use_sni_rewrite(&hosts, "i.ytimg.com", 443, false));
1590+
assert!(should_use_sni_rewrite(&hosts, "youtu.be", 443, false));
1591+
assert!(should_use_sni_rewrite(&hosts, "www.google.com", 443, false));
1592+
1593+
// With the toggle on: YouTube opts out, Google stays.
1594+
assert!(!should_use_sni_rewrite(&hosts, "www.youtube.com", 443, true));
1595+
assert!(!should_use_sni_rewrite(&hosts, "i.ytimg.com", 443, true));
1596+
assert!(!should_use_sni_rewrite(&hosts, "youtu.be", 443, true));
1597+
assert!(should_use_sni_rewrite(&hosts, "www.google.com", 443, true));
1598+
assert!(should_use_sni_rewrite(&hosts, "fonts.gstatic.com", 443, true));
1599+
}
1600+
1601+
#[test]
1602+
fn hosts_override_beats_youtube_via_relay() {
1603+
// If the user added an explicit hosts override for a YouTube
1604+
// subdomain, it should win — the override is a deliberate
1605+
// user choice, the toggle is a default policy.
1606+
let mut hosts = std::collections::HashMap::new();
1607+
hosts.insert("rr4.googlevideo.com".to_string(), "1.2.3.4".to_string());
1608+
1609+
assert!(should_use_sni_rewrite(&hosts, "rr4.googlevideo.com", 443, true));
15441610
}
15451611
}

0 commit comments

Comments
 (0)