From 27819684d5f271e7ced44717d5d108fcffaa209a Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 27 Sep 2026 22:10:06 +0200 Subject: [PATCH] feat(theseus): DNS over HTTPS and Global Privacy Control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DNS over HTTPS through Chromium's secure DNS (app.configureHostResolver), under Privacy › Network: Default protection (encrypted via the chosen provider, plain if that fails — the default), Increased protection (always the provider, never plain) or Off, with Quad9, Cloudflare, Mullvad, AdGuard or a custom resolver URL. Any DoH mode also turns on Chromium's built-in resolver, as Chrome does. Silent Mode names never touch DNS, and Tor resolves remotely through the SOCKS proxy, so neither path goes around it. Global Privacy Control, on by default, under Tracking protection: the Sec-GPC header on every request (added in the one request-header hook beside the client hints) and navigator.globalPrivacyControl in pages. --- main.js | 41 +++++++++++++++++++++++++++++++++++++++++ settings.html | 51 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/main.js b/main.js index 21649456..ba830e1b 100644 --- a/main.js +++ b/main.js @@ -394,6 +394,13 @@ const SETTINGS_DEFAULTS = { dockOrder: [], dockHidden: [], dockAutoHidden: [], // add-ons already started hidden once (manifest dock:"hidden") + // DNS over HTTPS: off | automatic (encrypt when the system resolver has a + // known DoH endpoint, otherwise plain) | secure (always the chosen provider, + // no plain fallback). Provider is a preset id or "custom" + a URL. + dohMode: "automatic", dohProvider: "quad9", dohCustom: "", + // Global Privacy Control: the Sec-GPC header + navigator.globalPrivacyControl, + // a legally meaningful "do not sell or share" signal in several jurisdictions. + gpc: true, // Sidebar width in px. Adjusted by dragging the grip on the panel's left // edge; persisted across launches. Clamped to [200, 800] on load. sidebarWidth: 340, @@ -842,6 +849,8 @@ async function applyFingerprint(wc) { if (settings.hideMediaDevices) { src += `try{const md=navigator.mediaDevices;if(md&&md.enumerateDevices){const o=md.enumerateDevices.bind(md);md.enumerateDevices=async()=>{let l=[];try{l=await o()}catch(e){}const ks=[...new Set(l.map(d=>d.kind))];return ks.map(kind=>({deviceId:'',kind:kind,label:'',groupId:'',toJSON(){return{deviceId:'',kind:kind,label:'',groupId:''}}}))};}}catch(e){}`; } + // Global Privacy Control's JavaScript half; the header is added in applyClientHintsSpoof. + if (settings.gpc) src += `try{Object.defineProperty(navigator,'globalPrivacyControl',{get:()=>true,configurable:true})}catch(e){}`; // Always on: Chrome-shaped window.chrome (see CHROME_SHIM_SRC). src += CHROME_SHIM_SRC; if (src) { @@ -983,6 +992,32 @@ function applyAcceptLanguage() { // (so the story stays consistent — no version straddling to fingerprint). // sec-ch-ua-mobile is pinned to "?0" (desktop) and sec-ch-ua-platform to // the actual OS name so a Linux user still looks like a Linux user. +// ---- DNS over HTTPS ---- +// Chromium's secure DNS lives in its built-in resolver, so any DoH mode +// also turns that resolver on (as Chrome does). BCNR names never touch DNS +// (bns:// is served in-process), and with Tor on the SOCKS proxy resolves +// remotely, so neither path leaks around this. +const DOH_PROVIDERS = { + quad9: { name: "Quad9", url: "https://dns.quad9.net/dns-query" }, + cloudflare: { name: "Cloudflare", url: "https://cloudflare-dns.com/dns-query" }, + mullvad: { name: "Mullvad", url: "https://dns.mullvad.net/dns-query" }, + adguard: { name: "AdGuard", url: "https://dns.adguard-dns.com/dns-query" }, +}; +function dohServerUrl() { + const p = String(settings.dohProvider || "quad9"); + const url = p === "custom" ? String(settings.dohCustom || "").trim() : (DOH_PROVIDERS[p] || DOH_PROVIDERS.quad9).url; + return /^https:\/\/[^\s]+$/i.test(url) ? url : ""; +} +function applyDoh() { + const mode = ["off", "automatic", "secure"].includes(settings.dohMode) ? settings.dohMode : "automatic"; + const url = dohServerUrl(); + const opts = mode === "off" + ? { secureDnsMode: "off", secureDnsServers: [] } + : mode === "secure" && url ? { enableBuiltInResolver: true, secureDnsMode: "secure", secureDnsServers: [url] } + : { enableBuiltInResolver: true, secureDnsMode: "automatic", secureDnsServers: url ? [url] : [] }; + try { app.configureHostResolver(opts); console.log(`[dns] secure DNS ${opts.secureDnsMode}${opts.secureDnsServers.length ? " via " + opts.secureDnsServers[0] : ""}`); } + catch (e) { console.warn("[dns] configureHostResolver failed:", e?.message); } +} function applyClientHintsSpoof() { try { const meta = chromeBrandMetadata(); @@ -1005,6 +1040,9 @@ function applyClientHintsSpoof() { h["sec-ch-ua-full-version-list"] = fullList; h["sec-ch-ua-mobile"] = "?0"; h["sec-ch-ua-platform"] = platform; + // Global Privacy Control (read live so the switch applies at once). + for (const k of Object.keys(h)) if (k.toLowerCase() === "sec-gpc") delete h[k]; + if (settings.gpc) h["Sec-GPC"] = "1"; callback({ requestHeaders: h }); }); } catch (e) { console.warn("client-hints spoof setup failed:", e?.message); } @@ -5811,6 +5849,8 @@ ipcMain.handle("settings-get", () => settings); ipcMain.handle("settings-set", (_e, key, val) => { if (key in SETTINGS_DEFAULTS) { settings[key] = val; saveSettings(); } if (key === "webrtcMode") applyWebRTCPolicy(); + if (key === "dohMode" || key === "dohProvider" || key === "dohCustom") applyDoh(); + if (key === "gpc") applyFingerprintAll(); if (key === "theme") applyTheme(); if (key === "backgroundThrottle") applyThrottle(); if (["timezoneMode", "timezoneValue", "languageMode", "languageSpoof", "languageValue", @@ -6439,6 +6479,7 @@ if (!process.env.THESEUS_NO_AUTOSTART && !app.requestSingleInstanceLock()) { applyEmbedCookieShim(); applyAcceptLanguage(); applyClientHintsSpoof(); + applyDoh(); // Session-wide preload for `window.bcnr` — runs BEFORE per-WebContentsView // preloads (home/settings/popover/etc.), which stack on top of it. Must be // called before any tab is created; whenReady runs before createWindow(). diff --git a/settings.html b/settings.html index b52930ac..d11746ba 100644 --- a/settings.html +++ b/settings.html @@ -469,7 +469,11 @@
Manage exceptions
Sites where Shield is allowed through or cookie pop-ups are left alone.
› -

Both also sit in the toolbar's extension row if you want them there: right-click the row and choose "Show hidden".

+
+
Tell sites not to sell or share my data
Sends the Global Privacy Control signal with every request. Sites in California, Colorado, Connecticut and other places with a privacy law must honour it.
+ +
+

Shield and Cookie Pop-ups also sit in the toolbar's extension row if you want them there: right-click the row and choose "Show hidden".

Device access

WebRTC IP policy
@@ -650,6 +654,27 @@
VPN
Route everything through a Silent Mode exit, or your own. Managed in its panel.
›
+
+
DNS over HTTPS
Encrypts name lookups so your network cannot see or alter which sites you are about to visit. Silent Mode names never use DNS; this covers the rest of the web.
+
+
+
+
Provider
+
+ + +
+

Browsing data

By default Theseus keeps nothing across sessions — everything toggled on here is wiped when you quit. Untoggle a bucket to keep it (e.g. cookies to stay signed in on trusted sites).

@@ -820,7 +845,29 @@ document.addEventListener("section", (e) => { if (String(e.detail).startsWith("privacy")) refreshAll(); }); refreshAll(); })(); - const TOGGLES = ["restoreSession", "backgroundThrottle", "blockCamera", "blockMicrophone", "hideMediaDevices", + // DNS over HTTPS controls: mode select, provider select (+ custom URL) shown + // unless the mode is Off; the help line explains the current mode. + (function () { + const el = (i) => document.getElementById(i); + const HELP = { + automatic: "Encrypted when your network's resolver offers it, plain otherwise. The provider below is used when the system's resolver has no encrypted endpoint.", + secure: "Always encrypted through the chosen provider, never plain. If the provider is unreachable, sites will not load.", + off: "Lookups go to the system resolver in the clear.", + }; + function show(mode, provider) { + el("dohProviderRow").hidden = mode === "off"; + el("dohCustom").hidden = provider !== "custom"; + el("dohHelp").textContent = HELP[mode] || HELP.automatic; + } + C.get().then((s) => { + el("dohMode").value = s.dohMode || "automatic"; el("dohProvider").value = s.dohProvider || "quad9"; el("dohCustom").value = s.dohCustom || ""; + show(el("dohMode").value, el("dohProvider").value); + el("dohMode").addEventListener("change", () => { C.set("dohMode", el("dohMode").value); show(el("dohMode").value, el("dohProvider").value); }); + el("dohProvider").addEventListener("change", () => { C.set("dohProvider", el("dohProvider").value); show(el("dohMode").value, el("dohProvider").value); if (el("dohProvider").value === "custom") el("dohCustom").focus(); }); + el("dohCustom").addEventListener("change", () => C.set("dohCustom", el("dohCustom").value.trim())); + }); + })(); + const TOGGLES = ["restoreSession", "backgroundThrottle", "blockCamera", "blockMicrophone", "hideMediaDevices", "gpc", "clearCookiesOnQuit", "clearCacheOnQuit", "clearStorageOnQuit", "clearHistoryOnQuit"]; C.get().then((s) => { for (const k of TOGGLES) {