mirror of
https://github.com/remnawave/node.git
synced 2026-10-21 10:20:51 +00:00
feat!: gate node API behind a derived SNI
This commit is contained in:
parent
5213041e12
commit
2b7c70780a
2 changed files with 49 additions and 3 deletions
|
|
@ -0,0 +1,32 @@
|
|||
import { hkdfSync, timingSafeEqual } from 'node:crypto';
|
||||
|
||||
const HKDF_INFO = 'rw-v1';
|
||||
const TLDS = ['com', 'net', 'org', 'io', 'dev', 'app'];
|
||||
|
||||
export function deriveSni(caCertPem: string, jwtPublicKey: string): string {
|
||||
const canon = (pem: string) =>
|
||||
pem.replace(/-----[^-]+-----/g, '').replace(/[^A-Za-z0-9+/=]/g, '');
|
||||
|
||||
const ikm = Buffer.concat([
|
||||
Buffer.from(canon(jwtPublicKey), 'utf8'),
|
||||
Buffer.from(canon(caCertPem), 'utf8'),
|
||||
]);
|
||||
|
||||
const okm = Buffer.from(hkdfSync('sha256', ikm, Buffer.alloc(0), HKDF_INFO, 22));
|
||||
const host = okm.subarray(0, 16).toString('hex');
|
||||
return `${host}.${okm.subarray(16, 21).toString('hex')}.${TLDS[okm[21] % TLDS.length]}`;
|
||||
}
|
||||
|
||||
export function makeSniVerifier(
|
||||
caCertPem: string,
|
||||
jwtPublicKey: string,
|
||||
): (servername: string) => boolean {
|
||||
const expected = Buffer.from(deriveSni(caCertPem, jwtPublicKey));
|
||||
|
||||
return (servername: string): boolean => {
|
||||
if (!servername) return false;
|
||||
const got = Buffer.from(servername);
|
||||
if (got.length !== expected.length) return false;
|
||||
return timingSafeEqual(got, expected);
|
||||
};
|
||||
}
|
||||
20
src/main.ts
20
src/main.ts
|
|
@ -8,7 +8,7 @@ import { Server } from 'https';
|
|||
import morgan from 'morgan';
|
||||
import { utilities as nestWinstonModuleUtilities, WinstonModule } from 'nest-winston';
|
||||
import { ZodValidationPipe } from 'nestjs-zod';
|
||||
import { SecureVersion } from 'node:tls';
|
||||
import { createSecureContext, SecureContext, SecureVersion } from 'node:tls';
|
||||
import { createLogger } from 'winston';
|
||||
import * as winston from 'winston';
|
||||
|
||||
|
|
@ -19,6 +19,10 @@ import { NestFactory } from '@nestjs/core';
|
|||
import { NotFoundExceptionFilter } from '@common/exception';
|
||||
import { acquireInstanceLock } from '@common/utils/acquire-instance-lock';
|
||||
import { parseNodePayload } from '@common/utils/decode-node-payload';
|
||||
import {
|
||||
deriveSni,
|
||||
makeSniVerifier,
|
||||
} from '@common/utils/decode-node-payload/decode-servername.util';
|
||||
import { customLogFilter } from '@common/utils/filter-logs';
|
||||
import { getDuplicateInstanceMessage } from '@common/utils/get-duplicate-instance-message';
|
||||
import { getStartMessage } from '@common/utils/get-start-message';
|
||||
|
|
@ -55,14 +59,24 @@ async function bootstrap(): Promise<void> {
|
|||
|
||||
const nodePayload = parseNodePayload();
|
||||
|
||||
const httpsOptions: { minVersion?: SecureVersion } & HttpsOptions = {
|
||||
const realCtx: SecureContext = createSecureContext({
|
||||
key: nodePayload.nodeKeyPem,
|
||||
cert: nodePayload.nodeCertPem,
|
||||
ca: [nodePayload.caCertPem],
|
||||
minVersion: 'TLSv1.3',
|
||||
});
|
||||
|
||||
logger.info(`Expected SNI: ${deriveSni(nodePayload.caCertPem, nodePayload.jwtPublicKey)}`);
|
||||
|
||||
const verifySni = makeSniVerifier(nodePayload.caCertPem, nodePayload.jwtPublicKey);
|
||||
|
||||
const httpsOptions: { minVersion?: SecureVersion } & HttpsOptions = {
|
||||
SNICallback: (servername: string, cb: (err: Error | null, ctx?: SecureContext) => void) =>
|
||||
verifySni(servername) ? cb(null, realCtx) : cb(new Error('unknown sni')),
|
||||
requestCert: true,
|
||||
rejectUnauthorized: true,
|
||||
minVersion: 'TLSv1.3',
|
||||
};
|
||||
} as { minVersion?: SecureVersion } & HttpsOptions;
|
||||
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
httpsOptions,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue