Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request filtering in main process2 #16264

Merged
merged 2 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/request-manager/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# @trezor/request-manager

Library to allow efficient and stable proxy for requests using Tor or other similar systems.
For now it works specifically with Tor, but it may be more generic in future and integrate with other similar proxy systems like Tor.
For now, it works specifically with Tor, but it may be more generic in future and integrate with other similar proxy systems like Tor.

## Examples

Expand Down
6 changes: 4 additions & 2 deletions packages/request-manager/e2e/identities-stress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import path from 'path';

import { createInterceptor, TorController } from '../src';
import { torRunner } from './torRunner';
import { InterceptorOptions } from '../src/types';

// The purpose of this script is to allow "manual" testing Tor identities changing some parameters.
// Run it like:
Expand All @@ -16,7 +17,8 @@ const processId = process.pid;
const torDataDir = path.join(__dirname, 'tmp');
const ipRegex = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/;

const INTERCEPTOR = {
const interceptorOptions: InterceptorOptions = {
getWhitelistedDomains: () => ['check.torproject.org'],
handler: () => {},
getTorSettings: () => ({ running: true, host, port }),
};
Expand All @@ -28,7 +30,7 @@ const intervalBetweenRequests = 1000 * 20;

(async () => {
// Callback in in createInterceptor should return true in order for the request to use Tor.
createInterceptor(INTERCEPTOR);
createInterceptor(interceptorOptions);

console.log('Starting Tor.');
// Starting Tor controller to make sure that Tor is running.
Expand Down
16 changes: 14 additions & 2 deletions packages/request-manager/e2e/interceptor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import path from 'path';
import http from 'http';
import WebSocket from 'ws';

// Todo: Currently this needs to be done in order to interceptor in test to work.
// This shall be taken care of, as we shall be intercepting the native fetch as well.
// import fetch from 'node-fetch';
peter-sanderson marked this conversation as resolved.
Show resolved Hide resolved

import { TorController, createInterceptor } from '../src';
import { torRunner } from './torRunner';
import { TorIdentities } from '../src/torIdentities';
import { InterceptorOptions } from '../src/types';

const hostIp = '127.0.0.1';
const port = 38835;
Expand All @@ -30,14 +35,21 @@ describe('Interceptor', () => {

const torSettings = { running: true, host: hostIp, port };

const INTERCEPTOR = {
const interceptorOptions: InterceptorOptions = {
getWhitelistedDomains: () => [
'check.torproject.org',
'httpbin.org',
'tbtc1.trezor.io',
'localhost',
'127.0.0.1',
],
handler: () => {},
getTorSettings: () => torSettings,
};

beforeAll(async () => {
// Callback in createInterceptor should return true in order for the request to use Tor.
torIdentities = createInterceptor(INTERCEPTOR).torIdentities;
torIdentities = createInterceptor(interceptorOptions).torIdentities;
// Starting Tor controller to make sure that Tor is running.
torController = new TorController({
host: hostIp,
Expand Down
2 changes: 2 additions & 0 deletions packages/request-manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"sideEffects": false,
"main": "src/index.ts",
"scripts": {
"test:unit": "yarn g:jest --testPathIgnorePatterns e2e -c ../../jest.config.base.js",
"test:e2e": "yarn g:jest --runInBand -c ../../jest.config.base.js",
"type-check": "yarn g:tsc --build tsconfig.json",
"test:stress": "ts-node -O '{\"module\": \"commonjs\"}' ./e2e/identities-stress.ts"
Expand All @@ -17,6 +18,7 @@
},
"devDependencies": {
"@trezor/eslint": "workspace:*",
"node-fetch": "^2.6.4",
"ts-node": "^10.9.1",
"ws": "^8.18.0"
}
Expand Down
21 changes: 16 additions & 5 deletions packages/request-manager/src/interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isWhitelistedHost } from '@trezor/utils';

import { TorIdentities } from './torIdentities';
import { InterceptorOptions } from './types';
import { createRequestPool } from './httpPool';
Expand All @@ -12,11 +14,20 @@ export const createInterceptor = (interceptorOptions: InterceptorOptions) => {
const torIdentities = new TorIdentities(interceptorOptions.getTorSettings);
const context = { ...interceptorOptions, requestPool, torIdentities };

interceptNetSocketConnect(context);
interceptNetConnect(context);
interceptHttp(context);
interceptHttps(context);
interceptTlsConnect(context);
const validateRequest = ({ hostname }: { hostname: string }) => {
if (!isWhitelistedHost(hostname, context.getWhitelistedDomains())) {
// Sometimes the error is not reported correctly so for debug reasons we log it as well
console.error(`Request blocked, not whitelisted domain: ${hostname}`);

throw new Error(`Request blocked, not whitelisted domain: ${hostname}`);
}
};

interceptNetSocketConnect({ context, validateRequest });
interceptNetConnect({ context, validateRequest });
interceptHttp({ context, validateRequest });
interceptHttps({ context, validateRequest });
interceptTlsConnect({ context, validateRequest });

return { requestPool, torIdentities };
};
28 changes: 24 additions & 4 deletions packages/request-manager/src/interceptor/interceptHttp.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import http from 'http';
import https from 'https';

import { InterceptorContext } from './interceptorTypesAndUtils';
import { overloadHttpRequest } from './overloadHttpRequest';
import { overloadWebsocketHandshake } from './overloadWebsocketHandshake';
import { Interceptor } from './interceptorTypes';

export const interceptHttp = (context: InterceptorContext) => {
export const interceptHttp: Interceptor = ({ context, validateRequest }) => {
const originalHttpRequest = http.request;

http.request = (...args) => {
const overload = overloadHttpRequest(context, 'http', ...args);
const [url, options, callback] = args;

const overload = overloadHttpRequest({
context,
protocol: 'http',
url,
options,
callback,
validateRequest,
});

if (overload) {
const [identity, ...overloadedArgs] = overload;

Expand All @@ -23,7 +33,17 @@ export const interceptHttp = (context: InterceptorContext) => {
const originalHttpGet = http.get;

http.get = (...args) => {
const overload = overloadWebsocketHandshake(context, 'http', ...args);
const [url, options, callback] = args;

const overload = overloadWebsocketHandshake({
context,
protocol: 'http',
url,
options,
callback,
validateRequest,
});

if (overload) {
const [identity, ...overloadedArgs] = overload;

Expand Down
28 changes: 24 additions & 4 deletions packages/request-manager/src/interceptor/interceptHttps.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import https from 'https';

import { InterceptorContext } from './interceptorTypesAndUtils';
import { overloadHttpRequest } from './overloadHttpRequest';
import { overloadWebsocketHandshake } from './overloadWebsocketHandshake';
import { Interceptor } from './interceptorTypes';

export const interceptHttps = (context: InterceptorContext) => {
export const interceptHttps: Interceptor = ({ context, validateRequest }) => {
const originalHttpsRequest = https.request;

https.request = (...args) => {
const overload = overloadHttpRequest(context, 'https', ...args);
const [url, options, callback] = args;

const overload = overloadHttpRequest({
context,
protocol: 'https',
url,
options,
callback,
validateRequest,
});

if (overload) {
const [identity, ...overloadedArgs] = overload;

Expand All @@ -22,7 +32,17 @@ export const interceptHttps = (context: InterceptorContext) => {
const originalHttpsGet = https.get;

https.get = (...args) => {
const overload = overloadWebsocketHandshake(context, 'https', ...args);
const [url, options, callback] = args;

const overload = overloadWebsocketHandshake({
context,
protocol: 'https',
url,
options,
callback,
validateRequest,
});

if (overload) {
const [identity, ...overloadedArgs] = overload;

Expand Down
15 changes: 13 additions & 2 deletions packages/request-manager/src/interceptor/interceptNetConnect.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
import net from 'net';

import { InterceptorContext } from './interceptorTypesAndUtils';
import { Interceptor } from './interceptorTypes';

export const interceptNetConnect = (context: InterceptorContext) => {
export const interceptNetConnect: Interceptor = ({ context, validateRequest }) => {
const originalConnect = net.connect;

net.connect = function (...args) {
const [options, callback] = args;

let details: string;
let hostname: string;
peter-sanderson marked this conversation as resolved.
Show resolved Hide resolved

if (typeof options === 'object') {
// case: connect(options: NetConnectOpts, connectionListener?: () => void): Socket;
if ('port' in options) {
// TcpNetConnectOpts
details = `${options.host}:${options.port}`;
hostname = options.host ?? '';
} else {
// IpcNetConnectOpts
details = options.path;
hostname = options.path;
}
} else if (typeof options === 'string') {
// case: connect(path: string, connectionListener?: () => void): Socket;
details = options;
hostname = options;
} else {
// case connect(port: number, host?: string, connectionListener?: () => void): Socket;
details = typeof callback === 'string' ? `${callback}:${options}` : options.toString();
hostname = typeof callback === 'string' ? callback : options.toString();
}

validateRequest({ hostname });

context.handler({
type: 'INTERCEPTED_REQUEST',
method: 'net.connect',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import net from 'net';

import { InterceptorContext } from './interceptorTypesAndUtils';
import { Interceptor } from './interceptorTypes';

export const interceptNetSocketConnect = (context: InterceptorContext) => {
export const interceptNetSocketConnect: Interceptor = ({ context, validateRequest }) => {
// To avoid disclosure that the request was sent by trezor-suite
// remove headers added by underlying libs before they are sent to the server.
// 1. nodejs http always(!) adds "Connection: close" header
// https://github.com/nodejs/node/blob/e48763840625c037282681456ecd1e1cb034f636/lib/_http_outgoing.js#L508-L510
// 2. node-fetch always(!) adds "User-Agent", "Accept", "Connection"...
// https://github.com/node-fetch/node-fetch/blob/7b86e946b02dfdd28f4f8fca3d73a022cbb5ca1e/src/request.js#L226
const originalSocketWrite = net.Socket.prototype.write;

net.Socket.prototype.write = function (data, ...args) {
const overloadedHeaders: string[] = [];

if (typeof data === 'string' && /Allowed-Headers/gi.test(data)) {
const headers = data.split('\r\n');
const allowedHeaders = headers
Expand Down Expand Up @@ -50,6 +52,7 @@ export const interceptNetSocketConnect = (context: InterceptorContext) => {

net.Socket.prototype.connect = function (...args) {
const [options, callback] = args;

let request: typeof options;
let details: string;
if (Array.isArray(options)) {
Expand All @@ -73,6 +76,9 @@ export const interceptNetSocketConnect = (context: InterceptorContext) => {
details = typeof callback === 'string' ? `${callback}:${request}` : request.toString();
}

const hostname = details.split(':')[0];
peter-sanderson marked this conversation as resolved.
Show resolved Hide resolved
validateRequest({ hostname });

context.handler({
type: 'INTERCEPTED_REQUEST',
method: 'net.Socket.connect',
Expand Down
45 changes: 33 additions & 12 deletions packages/request-manager/src/interceptor/interceptTlsConnect.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,46 @@
import tls from 'tls';

import { InterceptorContext, isWhitelistedHost } from './interceptorTypesAndUtils';
import { isWhitelistedHost } from '@trezor/utils';

export const interceptTlsConnect = (context: InterceptorContext) => {
import { Interceptor } from './interceptorTypes';

export const interceptTlsConnect: Interceptor = ({ context, validateRequest }) => {
const originalTlsConnect = tls.connect;

tls.connect = (...args) => {
const [options] = args;
if (typeof options === 'object') {
context.handler({
type: 'INTERCEPTED_REQUEST',
method: 'tls.connect',
details: options.host || options.servername || 'unknown',
});
const [optionsOrPort, optionsOrHost] = args;

let hostname: string;

if (typeof optionsOrPort === 'object') {
// case: connect(options: ConnectionOptions, secureConnectListener?: () => void): TLSSocket;
hostname = optionsOrPort.host || optionsOrPort.servername || 'unknown';

// allow untrusted/self-signed certificates for whitelisted domains (like https://*.sldev.cz)
options.rejectUnauthorized =
options.rejectUnauthorized ??
!isWhitelistedHost(options.host, context.notRequiredTorDomainsList);
optionsOrPort.rejectUnauthorized =
optionsOrPort.rejectUnauthorized ??
!isWhitelistedHost(optionsOrPort.host, context.notRequiredTorDomainsList);
} else {
if (typeof optionsOrHost === 'object') {
// case: connect(port: number, options?: ConnectionOptions, secureConnectListener?: () => void): TLSSocket;
hostname = optionsOrHost.host ?? '';
} else {
// case: connect(port: number, host?: string, options?: ConnectionOptions, secureConnectListener?: () => void): TLSSocket;
hostname = typeof optionsOrHost === 'string' ? optionsOrHost : 'unknown';
}
}

context.handler({
type: 'INTERCEPTED_REQUEST',
method: 'tls.connect',
details: hostname,
});

// This is here for defensive reasons, the original `tls.connect` implementation (AFAIK)
// uses net.connect to create new socket, and it already contains the interception logic.
// But to be 100% sure, lets do the check here as well.
validateRequest({ hostname });

return originalTlsConnect(...(args as Parameters<typeof tls.connect>));
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ export type InterceptorContext = InterceptorOptions & {
torIdentities: TorIdentities;
};

export const isWhitelistedHost = (
hostname: unknown,
whitelist: string[] = ['127.0.0.1', 'localhost'],
) =>
typeof hostname === 'string' &&
whitelist.some(url => url === hostname || hostname.endsWith(url));
export type Interceptor = (params: {
context: InterceptorContext;
validateRequest: ({ hostname }: { hostname: string }) => void;
}) => void;
Loading
Loading