Files
tubestation/toolkit/components/extensions/parent/ext-proxy.js
Manuel Bucher acacdfe55c Bug 1741375 - Proxy DNS by default when using SOCKS v5 r=necko-reviewers,extension-reviewers,kershaw,perftest-reviewers,robwu,sparky
Initially reported and discussed in Bug 610896.

The simple solution of just flipping the pref `network.proxy.socks_remote_dns`
is risky due to potentially breaking SOCKS4 proxy users.  Proxying
DNS on SOCKS4 isn't supported.  Therefore we speak the incompatible
SOCKS4a protocol when `socks_remote_dns` is enabled, potentially
breaking users setup.

To keep backwards compatibility on SOCKS4 proxy users, that don't have
SOCKS4a support, the pref `network.proxy.socks_remote_dns` is split into
two prefs:

* `network.proxy.socks_remote_dns`: remote DNS for SOCKS4
* `network.proxy.socks5_remote_dns`: remote DNS for SOCKS5.

This way we proxy DNS by default on SOCKS5 while keeping user settings
on SOCKS4.  This is a similar approach to the one described in
[Bug 610896 comment 17].

Proxying DNS in SOCKS4 by default is desireable (See [Bug 610896 comment 11]),
but out of scope for this patch.  [Telemetry] on proxy usage by socks
version indicated that changing the default for SOCKS4 is likely break
some users setup and needs to be taken with more care.

The default values of [proxyDNS] now defaults to true for SOCKS5 proxies.
When creating nsIProxyInfo objects of SOCKS4 proxies, the default value
false is kept.  Setting proxyDNS affects both SOCKS4 and SOCKS5 proxy by
modifying both `socks_remote_dns` and `socks5_remote_dns`.  Therefore no
extension breakage is expected.

The enterprise policy can also modify the new pref
`network.proxy.socks5_remote_dns`.

Follow up bugs filed while implementing:

* Bug 1890542 - Also disable Prefetch non-manual configurations of socks
                proxy
* Bug 1890554 - Use `ProxyInfo::TRANSPARENT_PROXY_RESOLVES_HOST` flag in
                `nsHttpChannel::GetProxyDNSStrategy`
* Bug 1890549 - nsHttpChannel implementation DNS resolve strategy for
                proxies incomplete
* Bug 1893670 - Proxy DNS by default for SOCK4 proxies. Defaulting to
                SOCKS4a

[Bug 610896 comment 17]: https://bugzilla.mozilla.org/show_bug.cgi?id=610896#c17
[Bug 610896 comment 11]: https://bugzilla.mozilla.org/show_bug.cgi?id=610896#c11
[Telemetry]: https://bugzilla.mozilla.org/show_bug.cgi?id=1741375#c27
[proxyDNS]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/proxy/settings#proxydns

Differential Revision: https://phabricator.services.mozilla.com/D207532
2024-05-21 11:55:26 +00:00

348 lines
11 KiB
JavaScript

/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
ChromeUtils.defineESModuleGetters(this, {
ProxyChannelFilter: "resource://gre/modules/ProxyChannelFilter.sys.mjs",
});
// Delayed wakeup is tied to ExtensionParent.browserPaintedPromise, which is
// when the first browser window has been painted. On Android, parts of the
// browser can trigger requests without browser "window" (geckoview.xhtml).
// Therefore we allow such proxy events to trigger wakeup.
// On desktop, we do not wake up early, to minimize the amount of work before
// a browser window is painted.
XPCOMUtils.defineLazyPreferenceGetter(
this,
"isEarlyWakeupOnRequestEnabled",
"extensions.webextensions.early_background_wakeup_on_request",
false
);
var { ExtensionPreferencesManager } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionPreferencesManager.sys.mjs"
);
var { ExtensionError } = ExtensionUtils;
var { getSettingsAPI } = ExtensionPreferencesManager;
const proxySvc = Ci.nsIProtocolProxyService;
const PROXY_TYPES_MAP = new Map([
["none", proxySvc.PROXYCONFIG_DIRECT],
["autoDetect", proxySvc.PROXYCONFIG_WPAD],
["system", proxySvc.PROXYCONFIG_SYSTEM],
["manual", proxySvc.PROXYCONFIG_MANUAL],
["autoConfig", proxySvc.PROXYCONFIG_PAC],
]);
const DEFAULT_PORTS = new Map([
["http", 80],
["ssl", 443],
["socks", 1080],
]);
ExtensionPreferencesManager.addSetting("proxy.settings", {
permission: "proxy",
prefNames: [
"network.proxy.type",
"network.proxy.http",
"network.proxy.http_port",
"network.proxy.share_proxy_settings",
"network.proxy.ssl",
"network.proxy.ssl_port",
"network.proxy.socks",
"network.proxy.socks_port",
"network.proxy.socks_version",
"network.proxy.socks_remote_dns",
"network.proxy.socks5_remote_dns",
"network.proxy.no_proxies_on",
"network.proxy.autoconfig_url",
"signon.autologin.proxy",
"network.http.proxy.respect-be-conservative",
],
setCallback(value) {
let prefs = {
"network.proxy.type": PROXY_TYPES_MAP.get(value.proxyType),
"signon.autologin.proxy": value.autoLogin,
"network.proxy.socks_remote_dns": value.proxyDNS,
"network.proxy.socks5_remote_dns": value.proxyDNS,
"network.proxy.autoconfig_url": value.autoConfigUrl,
"network.proxy.share_proxy_settings": value.httpProxyAll,
"network.proxy.socks_version": value.socksVersion,
"network.proxy.no_proxies_on": value.passthrough,
"network.http.proxy.respect-be-conservative": value.respectBeConservative,
};
for (let prop of ["http", "ssl", "socks"]) {
if (value[prop]) {
let url = new URL(`http://${value[prop]}`);
prefs[`network.proxy.${prop}`] = url.hostname;
// Only fall back to defaults if no port provided.
let [, rawPort] = value[prop].split(":");
let port = parseInt(rawPort, 10) || DEFAULT_PORTS.get(prop);
prefs[`network.proxy.${prop}_port`] = port;
}
}
return prefs;
},
});
function registerProxyFilterEvent(
context,
extension,
fire,
filterProps,
extraInfoSpec = []
) {
let listener = data => {
if (isEarlyWakeupOnRequestEnabled && fire.wakeup) {
// Starts the background script if it has not started, no-op otherwise.
extension.emit("start-background-script");
}
return fire.sync(data);
};
let filter = { ...filterProps };
if (filter.urls) {
let perms = new MatchPatternSet([
...extension.allowedOrigins.patterns,
...extension.optionalOrigins.patterns,
]);
filter.urls = new MatchPatternSet(filter.urls);
if (!perms.overlapsAll(filter.urls)) {
Cu.reportError(
"The proxy.onRequest filter doesn't overlap with host permissions."
);
}
}
let proxyFilter = new ProxyChannelFilter(
context,
extension,
listener,
filter,
extraInfoSpec
);
return {
unregister: () => {
proxyFilter.destroy();
},
convert(_fire, _context) {
fire = _fire;
proxyFilter.context = _context;
},
};
}
this.proxy = class extends ExtensionAPIPersistent {
PERSISTENT_EVENTS = {
onRequest({ fire, context }, params) {
return registerProxyFilterEvent(context, this.extension, fire, ...params);
},
};
getAPI(context) {
let { extension } = context;
let self = this;
return {
proxy: {
onRequest: new EventManager({
context,
module: "proxy",
event: "onRequest",
extensionApi: self,
}).api(),
// Leaving as non-persistent. By itself it's not useful since proxy-error
// is emitted from the proxy filter.
onError: new EventManager({
context,
name: "proxy.onError",
register: fire => {
let listener = (name, error) => {
fire.async(error);
};
extension.on("proxy-error", listener);
return () => {
extension.off("proxy-error", listener);
};
},
}).api(),
settings: Object.assign(
getSettingsAPI({
context,
name: "proxy.settings",
callback() {
let prefValue = Services.prefs.getIntPref("network.proxy.type");
let socksVersion = Services.prefs.getIntPref(
"network.proxy.socks_version"
);
let proxyDNS;
if (socksVersion == 4) {
proxyDNS = Services.prefs.getBoolPref(
"network.proxy.socks_remote_dns"
);
} else {
proxyDNS = Services.prefs.getBoolPref(
"network.proxy.socks5_remote_dns"
);
}
let proxyConfig = {
proxyType: Array.from(PROXY_TYPES_MAP.entries()).find(
entry => entry[1] === prefValue
)[0],
autoConfigUrl: Services.prefs.getCharPref(
"network.proxy.autoconfig_url"
),
autoLogin: Services.prefs.getBoolPref("signon.autologin.proxy"),
proxyDNS,
httpProxyAll: Services.prefs.getBoolPref(
"network.proxy.share_proxy_settings"
),
socksVersion,
passthrough: Services.prefs.getCharPref(
"network.proxy.no_proxies_on"
),
};
if (extension.isPrivileged) {
proxyConfig.respectBeConservative = Services.prefs.getBoolPref(
"network.http.proxy.respect-be-conservative"
);
}
for (let prop of ["http", "ssl", "socks"]) {
let host = Services.prefs.getCharPref(`network.proxy.${prop}`);
let port = Services.prefs.getIntPref(
`network.proxy.${prop}_port`
);
proxyConfig[prop] = port ? `${host}:${port}` : host;
}
return proxyConfig;
},
// proxy.settings is unsupported on android.
validate() {
if (AppConstants.platform == "android") {
throw new ExtensionError(
`proxy.settings is not supported on android.`
);
}
},
}),
{
set: details => {
if (AppConstants.platform === "android") {
throw new ExtensionError(
"proxy.settings is not supported on android."
);
}
if (!extension.privateBrowsingAllowed) {
throw new ExtensionError(
"proxy.settings requires private browsing permission."
);
}
if (!Services.policies.isAllowed("changeProxySettings")) {
throw new ExtensionError(
"Proxy settings are being managed by the Policies manager."
);
}
let value = details.value;
// proxyType is optional and it should default to "system" when missing.
if (value.proxyType == null) {
value.proxyType = "system";
}
if (!PROXY_TYPES_MAP.has(value.proxyType)) {
throw new ExtensionError(
`${value.proxyType} is not a valid value for proxyType.`
);
}
if (value.httpProxyAll) {
// Match what about:preferences does with proxy settings
// since the proxy service does not check the value
// of share_proxy_settings.
value.ssl = value.http;
}
for (let prop of ["http", "ssl", "socks"]) {
let host = value[prop];
if (host) {
try {
// Fixup in case a full url is passed.
if (host.includes("://")) {
value[prop] = new URL(host).host;
} else {
// Validate the host value.
new URL(`http://${host}`);
}
} catch (e) {
throw new ExtensionError(
`${value[prop]} is not a valid value for ${prop}.`
);
}
}
}
if (value.proxyType === "autoConfig" || value.autoConfigUrl) {
try {
new URL(value.autoConfigUrl);
} catch (e) {
throw new ExtensionError(
`${value.autoConfigUrl} is not a valid value for autoConfigUrl.`
);
}
}
if (value.socksVersion !== undefined) {
if (
!Number.isInteger(value.socksVersion) ||
value.socksVersion < 4 ||
value.socksVersion > 5
) {
throw new ExtensionError(
`${value.socksVersion} is not a valid value for socksVersion.`
);
}
}
if (
value.respectBeConservative !== undefined &&
!extension.isPrivileged &&
Services.prefs.getBoolPref(
"network.http.proxy.respect-be-conservative"
) != value.respectBeConservative
) {
throw new ExtensionError(
`respectBeConservative can be set by privileged extensions only.`
);
}
return ExtensionPreferencesManager.setSetting(
extension.id,
"proxy.settings",
value
);
},
}
),
},
};
}
};