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 @@
Both also sit in the toolbar's extension row if you want them there: right-click the row and choose "Show hidden".
+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".
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).