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) {