src,test: isolate config among presets

This commit is contained in:
Micooz 2018-02-15 11:01:24 +08:00
parent 89b8392eb3
commit 4b7c2e091c
34 changed files with 290 additions and 968 deletions

@ -1,16 +1,16 @@
import {Middleware} from '../middleware'; import {Middleware} from '../middleware';
test('Middleware#constructor', () => { test('Middleware#constructor', () => {
expect(() => new Middleware({'name': 'unknown-preset'})).toThrow(); expect(() => new Middleware({preset: {'name': 'unknown-preset'}})).toThrow();
}); });
test('Middleware#hasListener', () => { test('Middleware#hasListener', () => {
const middleware = new Middleware({'name': 'ss-base'}); const middleware = new Middleware({preset: {'name': 'ss-base'}});
expect(middleware.hasListener('event')).toBe(false); expect(middleware.hasListener('event')).toBe(false);
}); });
test('Middleware#onPresetNext', () => { test('Middleware#onPresetNext', () => {
const middleware = new Middleware({'name': 'ss-base'}); const middleware = new Middleware({preset: {'name': 'ss-base'}});
middleware.on('next_1', (arg) => { middleware.on('next_1', (arg) => {
expect(arg).toBe(null); expect(arg).toBe(null);
}); });
@ -18,6 +18,6 @@ test('Middleware#onPresetNext', () => {
}); });
test('Middleware#getImplement', () => { test('Middleware#getImplement', () => {
const middleware = new Middleware({'name': 'ss-base'}); const middleware = new Middleware({preset: {'name': 'ss-base'}});
expect(middleware.getImplement()).toBeDefined(); expect(middleware.getImplement()).toBeDefined();
}); });

@ -7,25 +7,26 @@ import url from 'url';
import qs from 'qs'; import qs from 'qs';
import winston from 'winston'; import winston from 'winston';
import isPlainObject from 'lodash.isplainobject'; import isPlainObject from 'lodash.isplainobject';
import { getPresetClassByName, IPresetAddressing } from '../presets'; import {getPresetClassByName, IPresetAddressing} from '../presets';
import { DNSCache, isValidHostname, isValidPort, logger, DNS_DEFAULT_EXPIRE } from '../utils'; import {DNSCache, isValidHostname, isValidPort, logger, DNS_DEFAULT_EXPIRE} from '../utils';
function loadFileSync(file) { function loadFileSync(file) {
return fs.readFileSync(path.resolve(process.cwd(), file)); return fs.readFileSync(path.resolve(process.cwd(), file));
} }
export class Config { export class Config {
local_protocol = null; local_protocol = null;
local_host = null; local_host = null;
local_port = null; local_port = null;
forward_host = null;
forward_port = null;
servers = null; servers = null;
is_client = null; is_client = null;
is_server = null; is_server = null;
forward_host = null;
forward_port = null;
timeout = null; timeout = null;
redirect = null; redirect = null;
workers = null; workers = null;
@ -50,8 +51,12 @@ export class Config {
log_level = null; log_level = null;
log_max_days = null; log_max_days = null;
// an isolate space where presets can store something in.
// store[i] is for presets[i]
stores = [];
constructor(json) { constructor(json) {
const { protocol, hostname, port, query } = url.parse(json.service); const {protocol, hostname, port, query} = url.parse(json.service);
this.local_protocol = protocol.slice(0, -1); this.local_protocol = protocol.slice(0, -1);
this.local_host = hostname; this.local_host = hostname;
this.local_port = +port; this.local_port = +port;
@ -72,8 +77,8 @@ export class Config {
} }
if (this.is_client && this.local_protocol === 'tcp') { if (this.is_client && this.local_protocol === 'tcp') {
const { forward } = qs.parse(query); const {forward} = qs.parse(query);
const { hostname, port } = url.parse('tcp://' + forward); const {hostname, port} = url.parse('tcp://' + forward);
this.forward_host = hostname; this.forward_host = hostname;
this.forward_port = +port; this.forward_port = +port;
} }
@ -95,7 +100,7 @@ export class Config {
initServer(server) { initServer(server) {
// service // service
const { protocol, hostname, port } = url.parse(server.service); const {protocol, hostname, port} = url.parse(server.service);
this.transport = protocol.slice(0, -1); this.transport = protocol.slice(0, -1);
this.server_host = hostname; this.server_host = hostname;
this.server_port = +port; this.server_port = +port;
@ -123,17 +128,21 @@ export class Config {
// remove unnecessary presets // remove unnecessary presets
if (this.mux) { if (this.mux) {
this.presets = this.presets.filter( this.presets = this.presets.filter(
({ name }) => !IPresetAddressing.isPrototypeOf(getPresetClassByName(name)) ({name}) => !IPresetAddressing.isPrototypeOf(getPresetClassByName(name))
); );
} }
// pre-init presets // pre-cache presets
for (const { name, params = {} } of server.presets) { this.stores = (new Array(this.presets.length)).fill({});
for (let i = 0; i < server.presets.length; i++) {
const {name, params = {}} = server.presets[i];
const clazz = getPresetClassByName(name); const clazz = getPresetClassByName(name);
clazz.checked = false; const data = clazz.onCache(params);
clazz.checkParams(params); if (data instanceof Promise) {
clazz.initialized = false; data.then((d) => this.stores[i] = d);
clazz.onInit(params); } else {
this.stores[i] = clazz.onCache(params);
}
} }
} }
@ -188,7 +197,7 @@ export class Config {
throw Error('"service" must be provided as "<protocol>://<host>:<port>[?params]"'); throw Error('"service" must be provided as "<protocol>://<host>:<port>[?params]"');
} }
const { protocol: _protocol, hostname, port, query } = url.parse(json.service); const {protocol: _protocol, hostname, port, query} = url.parse(json.service);
// service.protocol // service.protocol
if (typeof _protocol !== 'string') { if (typeof _protocol !== 'string') {
@ -216,14 +225,14 @@ export class Config {
// service.query // service.query
if (protocol === 'tcp') { if (protocol === 'tcp') {
const { forward } = qs.parse(query); const {forward} = qs.parse(query);
// ?forward // ?forward
if (!forward) { if (!forward) {
throw Error('require "?forward=<host>:<port>" parameter in service when using "tcp" on client side'); throw Error('require "?forward=<host>:<port>" parameter in service when using "tcp" on client side');
} }
const { hostname, port } = url.parse('tcp://' + forward); const {hostname, port} = url.parse('tcp://' + forward);
if (!isValidHostname(hostname)) { if (!isValidHostname(hostname)) {
throw Error('service.?forward.host is invalid'); throw Error('service.?forward.host is invalid');
} }
@ -278,7 +287,7 @@ export class Config {
throw Error('"service" must be provided as "<protocol>://<host>:<port>[?params]"'); throw Error('"service" must be provided as "<protocol>://<host>:<port>[?params]"');
} }
const { protocol: _protocol, hostname, port } = url.parse(server.service); const {protocol: _protocol, hostname, port} = url.parse(server.service);
// service.protocol // service.protocol
if (typeof _protocol !== 'string') { if (typeof _protocol !== 'string') {
@ -343,7 +352,7 @@ export class Config {
// presets[].parameters // presets[].parameters
for (const preset of server.presets) { for (const preset of server.presets) {
const { name, params } = preset; const {name, params} = preset;
if (typeof name !== 'string') { if (typeof name !== 'string') {
throw Error('"server.presets[].name" must be a string'); throw Error('"server.presets[].name" must be a string');
} }
@ -354,6 +363,8 @@ export class Config {
if (!isPlainObject(params)) { if (!isPlainObject(params)) {
throw Error('"server.presets[].params" must be an plain object'); throw Error('"server.presets[].params" must be an plain object');
} }
const clazz = getPresetClassByName(name);
clazz.onCheckParams(params);
} }
} }
} }
@ -361,7 +372,7 @@ export class Config {
static _testCommon(common) { static _testCommon(common) {
// timeout // timeout
if (common.timeout !== undefined) { if (common.timeout !== undefined) {
const { timeout } = common; const {timeout} = common;
if (typeof timeout !== 'number') { if (typeof timeout !== 'number') {
throw Error('"timeout" must be a number'); throw Error('"timeout" must be a number');
} }
@ -390,7 +401,7 @@ export class Config {
// log_max_days // log_max_days
if (common.log_max_days !== undefined) { if (common.log_max_days !== undefined) {
const { log_max_days } = common; const {log_max_days} = common;
if (typeof log_max_days !== 'number') { if (typeof log_max_days !== 'number') {
throw Error('"log_max_days" must a number'); throw Error('"log_max_days" must a number');
} }
@ -401,7 +412,7 @@ export class Config {
// workers // workers
if (common.workers !== undefined) { if (common.workers !== undefined) {
const { workers } = common; const {workers} = common;
if (typeof workers !== 'number') { if (typeof workers !== 'number') {
throw Error('"workers" must be a number'); throw Error('"workers" must be a number');
} }
@ -415,7 +426,7 @@ export class Config {
// dns // dns
if (common.dns !== undefined) { if (common.dns !== undefined) {
const { dns } = common; const {dns} = common;
if (!Array.isArray(dns)) { if (!Array.isArray(dns)) {
throw Error('"dns" must be an array'); throw Error('"dns" must be an array');
} }
@ -428,7 +439,7 @@ export class Config {
// dns_expire // dns_expire
if (common.dns_expire !== undefined) { if (common.dns_expire !== undefined) {
const { dns_expire } = common; const {dns_expire} = common;
if (typeof dns_expire !== 'number') { if (typeof dns_expire !== 'number') {
throw Error('"dns_expire" must be a number'); throw Error('"dns_expire" must be a number');
} }

@ -315,26 +315,27 @@ export class Hub {
_createRelay(context, isMux = false) { _createRelay(context, isMux = false) {
const props = { const props = {
config: this._config,
context: context, context: context,
transport: this._config.transport, transport: this._config.transport,
presets: this._config.presets presets: this._config.presets
}; };
if (isMux) { if (isMux) {
return new MuxRelay(props, this._config); return new MuxRelay(props);
} }
if (this._config.mux) { if (this._config.mux) {
if (this._config.is_client) { if (this._config.is_client) {
return new Relay({...props, transport: 'mux', presets: []}, this._config); return new Relay({...props, transport: 'mux', presets: []});
} else { } else {
return new MuxRelay(props, this._config); return new MuxRelay(props);
} }
} else { } else {
return new Relay(props, this._config); return new Relay(props);
} }
} }
_createUdpRelay(context) { _createUdpRelay(context) {
return new Relay({transport: 'udp', context, presets: this._config.udp_presets}, this._config); return new Relay({config: this._config, transport: 'udp', context, presets: this._config.udp_presets});
} }
_selectMuxRelay() { _selectMuxRelay() {

@ -1,54 +1,37 @@
import EventEmitter from 'events'; import EventEmitter from 'events';
import {getPresetClassByName, IPresetStatic} from '../presets'; import {getPresetClassByName} from '../presets';
import {PIPE_ENCODE} from '../constants'; import {PIPE_ENCODE} from '../constants';
import {kebabCase} from '../utils'; import {kebabCase} from '../utils';
const staticPresetCache = new Map(/* 'ClassName': <preset> */); function createPreset({config, preset}) {
const name = preset.name;
function createPreset(name, params = {}, config) { const params = preset.params || {};
const ImplClass = getPresetClassByName(name); const ImplClass = getPresetClassByName(name);
const createOne = () => { const instance = new ImplClass({config, params});
ImplClass.config = config; instance.onInit(params);
ImplClass.checkParams(params); return instance;
ImplClass.onInit(params);
return new ImplClass(params);
};
let preset = null;
if (IPresetStatic.isPrototypeOf(ImplClass)) {
// only create one instance for IPresetStatic
preset = staticPresetCache.get(ImplClass.name);
if (preset === undefined) {
preset = createOne();
staticPresetCache.set(ImplClass.name, preset);
}
} else {
preset = createOne();
}
return preset;
} }
/**
* abstraction of middleware
*/
export class Middleware extends EventEmitter { export class Middleware extends EventEmitter {
_impl = null;
_config = null; _config = null;
constructor(preset, config) { _impl = null;
constructor({config, preset}) {
super(); super();
this._config = config;
this.onPresetNext = this.onPresetNext.bind(this); this.onPresetNext = this.onPresetNext.bind(this);
this.onPresetBroadcast = this.onPresetBroadcast.bind(this); this.onPresetBroadcast = this.onPresetBroadcast.bind(this);
this.onPresetFail = this.onPresetFail.bind(this); this.onPresetFail = this.onPresetFail.bind(this);
this._impl = createPreset(preset.name, preset.params || {}, this._config); this._config = config;
this._impl = createPreset({config, preset});
this._impl.next = this.onPresetNext; this._impl.next = this.onPresetNext;
this._impl.broadcast = this.onPresetBroadcast; this._impl.broadcast = this.onPresetBroadcast;
this._impl.fail = this.onPresetFail; this._impl.fail = this.onPresetFail;
} }
get name() { get name() {
return this._impl.getName() || kebabCase(this._impl.constructor.name).replace(/(.*)-preset/i, '$1'); return kebabCase(this._impl.constructor.name).replace(/(.*)-preset/i, '$1');
} }
getImplement() { getImplement() {
@ -76,21 +59,10 @@ export class Middleware extends EventEmitter {
} }
onDestroy() { onDestroy() {
// prevent destroy on static preset this._impl.onDestroy();
if (!(this._impl instanceof IPresetStatic)) {
this._impl.onDestroy();
}
this.removeAllListeners(); this.removeAllListeners();
} }
/**
* call hook functions of implement in order
* @param direction
* @param buffer
* @param direct
* @param isUdp
* @param extraArgs
*/
write({direction, buffer, direct, isUdp}, extraArgs) { write({direction, buffer, direct, isUdp}, extraArgs) {
const type = (direction === PIPE_ENCODE ? 'Out' : 'In') + (isUdp ? 'Udp' : ''); const type = (direction === PIPE_ENCODE ? 'Out' : 'In') + (isUdp ? 'Udp' : '');

@ -20,7 +20,7 @@ export class Pipe extends EventEmitter {
_destroyed = false; _destroyed = false;
_presets = null; _presets = null;
_config = null; _config = null;
get destroyed() { get destroyed() {
@ -67,7 +67,7 @@ export class Pipe extends EventEmitter {
} }
createMiddlewares(presets) { createMiddlewares(presets) {
const middlewares = presets.map((preset) => this._createMiddleware(preset)); const middlewares = presets.map((preset, i) => this._createMiddleware(preset, i));
this._upstream_middlewares = middlewares; this._upstream_middlewares = middlewares;
this._downstream_middlewares = [].concat(middlewares).reverse(); this._downstream_middlewares = [].concat(middlewares).reverse();
this._presets = presets; this._presets = presets;
@ -89,7 +89,8 @@ export class Pipe extends EventEmitter {
} }
// create non-exist middleware and reuse exist one // create non-exist middleware and reuse exist one
const middlewares = []; const middlewares = [];
for (const preset of presets) { for (let i = 0; i < presets.length; i++) {
const preset = presets[i];
let md = mdIndex[preset.name]; let md = mdIndex[preset.name];
if (md) { if (md) {
// remove all listeners for later re-chain later in _feed() // remove all listeners for later re-chain later in _feed()
@ -98,7 +99,7 @@ export class Pipe extends EventEmitter {
this._attachEvents(md); this._attachEvents(md);
delete mdIndex[preset.name]; delete mdIndex[preset.name];
} else { } else {
md = this._createMiddleware(preset); md = this._createMiddleware(preset, i);
} }
middlewares.push(md); middlewares.push(md);
} }
@ -142,12 +143,13 @@ export class Pipe extends EventEmitter {
this.removeAllListeners(); this.removeAllListeners();
} }
_createMiddleware(preset) { _createMiddleware(preset, index) {
const middleware = new Middleware(preset, this._config); const middleware = new Middleware({config: this._config, preset});
this._attachEvents(middleware); this._attachEvents(middleware);
// set readProperty() // set readProperty() and getStore()
const impl = middleware.getImplement(); const impl = middleware.getImplement();
impl.readProperty = (...args) => this.onReadProperty(middleware.name, ...args); impl.readProperty = (...args) => this.onReadProperty(middleware.name, ...args);
impl.getStore = () => this._config.stores[index];
return middleware; return middleware;
} }

@ -65,14 +65,14 @@ export class Relay extends EventEmitter {
this._ctx.cid = id; this._ctx.cid = id;
} }
constructor({transport, context, presets = []}, config) { constructor({config, transport, context, presets = []}) {
super(); super();
this._config = config;
this.updatePresets = this.updatePresets.bind(this); this.updatePresets = this.updatePresets.bind(this);
this.onBroadcast = this.onBroadcast.bind(this); this.onBroadcast = this.onBroadcast.bind(this);
this.onEncoded = this.onEncoded.bind(this); this.onEncoded = this.onEncoded.bind(this);
this.onDecoded = this.onDecoded.bind(this); this.onDecoded = this.onDecoded.bind(this);
this._id = Relay.idcounter++; this._id = Relay.idcounter++;
this._config = config;
this._transport = transport; this._transport = transport;
this._remoteInfo = context.remoteInfo; this._remoteInfo = context.remoteInfo;
// pipe // pipe
@ -85,8 +85,9 @@ export class Relay extends EventEmitter {
}; };
// bounds // bounds
const {Inbound, Outbound} = this.getBounds(transport); const {Inbound, Outbound} = this.getBounds(transport);
const inbound = new Inbound({context: this._ctx, globalContext: this._config}); const props = {config, context: this._ctx};
const outbound = new Outbound({context: this._ctx, globalContext: this._config}); const inbound = new Inbound(props);
const outbound = new Outbound(props);
this._inbound = inbound; this._inbound = inbound;
this._outbound = outbound; this._outbound = outbound;
// outbound // outbound

@ -1,458 +0,0 @@
import fs from 'fs';
import os from 'os';
import net from 'net';
import path from 'path';
import readline from 'readline';
import ip from 'ip';
import {
IPreset,
CONNECTION_CREATED,
CONNECT_TO_REMOTE,
PRESET_FAILED,
PRESET_CLOSE_CONNECTION,
PRESET_PAUSE_RECV,
PRESET_PAUSE_SEND,
PRESET_RESUME_RECV,
PRESET_RESUME_SEND
} from './defs';
import {logger, isValidHostname, isValidPort} from '../utils';
let rules = [];
let cachedRules = {
// <host:port>: <rule>
};
// rule's methods
function ruleIsMatch(host, port) {
const {host: rHost, port: rPort} = this;
const slashIndex = rHost.indexOf('/');
let isHostMatch = false;
if (slashIndex !== -1) {
isHostMatch = ip.cidrSubnet(rHost).contains(host);
} else {
isHostMatch = (rHost === host);
}
if (rHost === '*' || isHostMatch) {
if (rPort === '*' || port === rPort) {
return true;
}
}
return false;
}
function ruleToString() {
return `${this.host}:${this.port} ${this.isBan ? 1 : 0} ${this.upLimit} ${this.dlLimit}`;
}
// rule parsing
function parseHost(host) {
const slashIndex = host.indexOf('/');
if (slashIndex < 0) {
if (host !== '*' && !net.isIP(host) && !isValidHostname(host)) {
return null;
}
return host;
}
if (slashIndex < 7) {
return null;
}
const parts = host.split('/');
const ip = parts[0];
const mask = parts[parts.length - 1];
if (!net.isIP(ip)) {
return null;
}
if (mask === '' || !Number.isInteger(+mask) || +mask < 0 || +mask > 32) {
return null;
}
return host;
}
function parseSpeed(speed) {
const regex = /^(\d+)(b|k|kb|m|mb|g|gb)$/g;
const results = regex.exec(speed.toLowerCase());
if (results !== null) {
const [, num, unit] = results;
return +num * {
'b': 1,
'k': 1024,
'kb': 1024,
'm': 1048576,
'mb': 1048576,
'g': 1073741824,
'gb': 1073741824
}[unit];
}
return null;
}
function parseLine(line) {
if (line.length > 300) {
return null;
}
line = line.trim();
if (line.length < 1) {
return null;
}
if (line[0] === '#') {
return null;
}
const [addr, ban, up, dl] = line.split(' ').filter(p => p.length > 0);
let _host = null;
let _port = null;
let _isBan = false;
let _upLimit = '-';
let _dlLimit = '-';
// [addr[/mask][:port]]
if (addr.indexOf(':') > 0) {
const parts = addr.split(':');
const host = parts[0];
const port = parts[parts.length - 1];
_host = parseHost(host);
if (port !== '*') {
if (!isValidPort(+port)) {
return null;
}
_port = +port;
} else {
_port = port;
}
} else {
_host = parseHost(addr);
_port = '*';
}
if (_host === null) {
return null;
}
// [ban]
if (ban !== undefined) {
if (ban !== '0' && ban !== '1') {
return null;
}
_isBan = ban !== '0';
}
// [max_upload_speed(/s)]
if (up !== undefined && up !== '-') {
_upLimit = parseSpeed(up);
if (!_upLimit) {
return null;
}
}
// [max_download_speed(/s)]
if (dl !== undefined && dl !== '-') {
_dlLimit = parseSpeed(dl);
if (!_dlLimit) {
return null;
}
}
return {
host: _host,
port: _port,
isBan: _isBan,
upLimit: _upLimit,
dlLimit: _dlLimit,
isMatch: ruleIsMatch,
toString: ruleToString
};
}
// helpers
function reloadRules(aclPath) {
logger.verbose('[acl] (re)loading access list');
const rs = fs.createReadStream(aclPath, {encoding: 'utf-8'});
rs.on('error', (err) => {
logger.warn(`[acl] fail to reload acl: ${err.message}, keep using previous rules`);
});
const rl = readline.createInterface({input: rs});
const _rules = [];
rl.on('line', (line) => {
const rule = parseLine(line);
if (rule !== null) {
_rules.push(rule);
}
});
rl.on('close', () => {
rules = _rules.reverse();
cachedRules = {};
logger.info(`[acl] ${rules.length} rules loaded`);
});
}
function findRule(host, port) {
const cacheKey = `${host}:${port}`;
const cacheRule = cachedRules[cacheKey];
if (cacheRule !== undefined) {
return cacheRule;
} else {
for (const rule of rules) {
if (rule.isMatch(host, port)) {
return cachedRules[cacheKey] = rule;
}
}
// rule not found
return cachedRules[cacheKey] = null;
}
}
const DEFAULT_MAX_TRIES = 60;
const tries = {
// <host>: <count>
};
/**
* @description
* Apply access control to each connection.
*
* @notice
* This preset can ONLY be used on server side.
*
* @params
* acl: A path to a text file which contains a list of rules in order.
* max_tries(optional): The maximum tries from client, default is 60.
*
* @examples
* {
* "name": "access-control",
* "params": {
* "acl": "acl.txt",
* "max_tries": 60
* }
* }
*
* // acl.txt
* # [addr[/mask][:port]] [ban] [max_upload_speed(/s)] [max_download_speed(/s)]
*
* example.com 1 # prevent access to example.com
* example.com:* 1 # prevent access to example.com:*, equal to above
* example.com:443 1 # prevent access to example.com:443 only
* *:25 1 # prevent access to SMTP servers
* *:* 1 # prevent all access from/to all endpoints
* 127.0.0.1 1 # ban localhost
* 192.168.0.0/16 1 # ban hosts in 192.168.*.*
* 172.27.1.100 0 120K # limit upload speed to 120KB/s
* 172.27.1.100 0 - 120K # limit download speed to 120KB/s
* 172.27.1.100 0 120K 120K # limit upload and download speed to 120KB/s
*/
export default class AccessControlPreset extends IPreset {
// params(readonly)
_aclPath = '';
_maxTries = 0;
// members
_hrTimeBegin = process.hrtime();
_remoteHost = null;
_remotePort = null;
_dstHost = null;
_dstPort = null;
_totalOut = 0;
_totalIn = 0;
// flags
_isBlocking = false;
_isDlPaused = false;
_isUpPaused = false;
static checkParams({acl, max_tries = DEFAULT_MAX_TRIES}) {
if (typeof acl !== 'string' || acl === '') {
throw Error('\'acl\' must be a non-empty string');
}
const aclPath = path.resolve(process.cwd(), acl);
if (!fs.existsSync(aclPath)) {
throw Error(`"${aclPath}" not found`);
}
if (max_tries !== undefined) {
if (typeof max_tries !== 'number' || !Number.isInteger(max_tries)) {
throw Error('\'max_tries\' must be an integer');
}
if (max_tries < 1) {
throw Error('\'max_tries\' must be greater than 0');
}
}
}
static onInit({acl}) {
const aclPath = path.resolve(process.cwd(), acl);
// note: should load rules once server up
reloadRules(aclPath);
if (process.env.NODE_ENV !== 'test') {
fs.watchFile(aclPath, (curr, prev) => {
if (curr.mtime > prev.mtime) {
reloadRules(aclPath);
}
});
}
}
constructor({acl, max_tries = DEFAULT_MAX_TRIES}) {
super();
this._aclPath = path.resolve(process.cwd(), acl);
this._maxTries = max_tries;
}
applyRule(rule) {
const {host, port, isBan, upLimit, dlLimit} = rule;
logger.debug(`[acl] [${this._remoteHost}:${this._remotePort}] apply rule: "${rule}"`);
// ban
if (isBan) {
logger.info(`[acl] [${host}:${port}] baned by rule: "${rule}"`);
this.broadcast({type: PRESET_CLOSE_CONNECTION});
this._isBlocking = true;
}
// max_upload_speed
if (upLimit !== '-') {
// calculate average download speed
const [sec, nano] = process.hrtime(this._hrTimeBegin);
const speed = Math.ceil(this._totalIn / (sec + nano / 1e9)); // b/s
logger.debug(`[acl] upload speed: ${speed}b/s`);
if (speed > upLimit && !this._isUpPaused) {
this._isUpPaused = true;
this.broadcast({type: PRESET_PAUSE_RECV});
// determine timeout to resume
const timeout = speed / upLimit * 1.1; // more 10% cost
const direction = `[${this._remoteHost}:${this._remotePort}] -> [${this._dstHost}:${this._dstPort}]`;
logger.info(`[acl] ${direction} upload speed exceed: ${speed}b/s > ${upLimit}b/s, pause for ${timeout}s...`);
setTimeout(() => {
this.broadcast({type: PRESET_RESUME_RECV});
this._isUpPaused = false;
}, timeout * 1e3);
}
}
// max_download_speed
if (dlLimit !== '-') {
// calculate average download speed
const [sec, nano] = process.hrtime(this._hrTimeBegin);
const speed = Math.ceil(this._totalOut / (sec + nano / 1e9)); // b/s
logger.debug(`[acl] download speed: ${speed}b/s`);
if (speed > dlLimit && !this._isDlPaused) {
this._isDlPaused = true;
this.broadcast({type: PRESET_PAUSE_SEND});
// determine timeout to resume
const timeout = speed / dlLimit * 1.1; // more 10% cost
const direction = `[${this._remoteHost}:${this._remotePort}] <- [${this._dstHost}:${this._dstPort}]`;
logger.info(`[acl] ${direction} download speed exceed: ${speed}b/s > ${dlLimit}b/s, pause for ${timeout}s...`);
setTimeout(() => {
this.broadcast({type: PRESET_RESUME_SEND});
this._isDlPaused = false;
}, timeout * 1e3);
}
}
}
checkRule(host, port) {
const rule = findRule(host, port);
if (rule !== null) {
this.applyRule(rule);
}
}
appendToAcl(line) {
logger.info(`[acl] append rule: "${line}" to acl`);
fs.appendFile(this._aclPath, `${os.EOL}${line}`, (err) => {
if (err) {
logger.warn(`[acl] fail to update acl: ${err.message}`);
}
rules.push(parseLine(line));
});
}
onNotified({type, payload}) {
switch (type) {
case CONNECTION_CREATED: {
const {host, port} = payload;
this._remoteHost = host;
this._remotePort = port;
this.checkRule(host, port);
break;
}
case CONNECT_TO_REMOTE: {
const {host, port} = payload;
this._dstHost = host;
this._dstPort = port;
this.checkRule(host, port);
break;
}
case PRESET_FAILED: {
const host = this._remoteHost;
const maxTries = this._maxTries;
if (tries[host] === undefined) {
tries[host] = 0;
}
if (++tries[host] >= maxTries) {
logger.warn(`[acl] ${host} max tries(${maxTries}) exceed`);
this.broadcast({type: PRESET_CLOSE_CONNECTION});
this._isBlocking = true;
if (findRule(host, '*') === null) {
this.appendToAcl(`${host}:* 1`);
}
}
return;
}
}
}
beforeOut({buffer}) {
this._totalOut += buffer.length;
if (this._isBlocking) {
return; // drop
}
this.checkRule(this._remoteHost, this._remotePort);
this.checkRule(this._dstHost, this._dstPort);
return buffer;
}
beforeIn({buffer}) {
this._totalIn += buffer.length;
if (this._isBlocking) {
return; // drop
}
this.checkRule(this._remoteHost, this._remotePort);
this.checkRule(this._dstHost, this._dstPort);
return buffer;
}
beforeOutUdp(...args) {
return this.beforeOut(...args);
}
beforeInUdp(...args) {
return this.beforeIn(...args);
}
}

@ -74,15 +74,15 @@ const HKDF_HASH_ALGORITHM = 'sha1';
*/ */
export default class AeadRandomCipherPreset extends IPreset { export default class AeadRandomCipherPreset extends IPreset {
static cipherName = ''; _cipherName = '';
static info = null; _info = null;
static factor = DEFAULT_FACTOR; _factor = DEFAULT_FACTOR;
static rawKey = null; _rawKey = null;
static keySaltSize = 0; // key and salt size _keySaltSize = 0; // key and salt size
_cipherKey = null; _cipherKey = null;
@ -99,7 +99,7 @@ export default class AeadRandomCipherPreset extends IPreset {
_adBuf = null; _adBuf = null;
static checkParams({method, info = DEFAULT_INFO, factor = DEFAULT_FACTOR}) { static onCheckParams({method, info = DEFAULT_INFO, factor = DEFAULT_FACTOR}) {
if (method === undefined || method === '') { if (method === undefined || method === '') {
throw Error('\'method\' must be set'); throw Error('\'method\' must be set');
} }
@ -118,16 +118,12 @@ export default class AeadRandomCipherPreset extends IPreset {
} }
} }
static onInit({method, info = DEFAULT_INFO, factor = DEFAULT_FACTOR}) { onInit({method, info = DEFAULT_INFO, factor = DEFAULT_FACTOR}) {
AeadRandomCipherPreset.cipherName = method; this._cipherName = method;
AeadRandomCipherPreset.info = Buffer.from(info); this._info = Buffer.from(info);
AeadRandomCipherPreset.factor = factor; this._factor = factor;
AeadRandomCipherPreset.rawKey = Buffer.from(AeadRandomCipherPreset.config.key); this._rawKey = Buffer.from(this._config.key);
AeadRandomCipherPreset.keySaltSize = ciphers[method]; this._keySaltSize = ciphers[method];
}
constructor() {
super();
this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)}); this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)});
this._adBuf.on('data', this.onChunkReceived.bind(this)); this._adBuf.on('data', this.onChunkReceived.bind(this));
} }
@ -145,9 +141,9 @@ export default class AeadRandomCipherPreset extends IPreset {
beforeOut({buffer}) { beforeOut({buffer}) {
let salt = null; let salt = null;
if (this._cipherKey === null) { if (this._cipherKey === null) {
const size = AeadRandomCipherPreset.keySaltSize; const size = this._keySaltSize;
salt = crypto.randomBytes(size); salt = crypto.randomBytes(size);
this._cipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, AeadRandomCipherPreset.rawKey, AeadRandomCipherPreset.info, size); this._cipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, this._rawKey, this._info, size);
} }
const chunks = getRandomChunks(buffer, MIN_CHUNK_SPLIT_LEN, MAX_CHUNK_SPLIT_LEN).map((chunk) => { const chunks = getRandomChunks(buffer, MIN_CHUNK_SPLIT_LEN, MAX_CHUNK_SPLIT_LEN).map((chunk) => {
// random padding // random padding
@ -174,12 +170,12 @@ export default class AeadRandomCipherPreset extends IPreset {
onReceiving(buffer, {fail}) { onReceiving(buffer, {fail}) {
// 1. init this._decipherKey // 1. init this._decipherKey
if (this._decipherKey === null) { if (this._decipherKey === null) {
const size = AeadRandomCipherPreset.keySaltSize; const size = this._keySaltSize;
if (buffer.length < size) { if (buffer.length < size) {
return; // too short to get salt return; // too short to get salt
} }
const salt = buffer.slice(0, size); const salt = buffer.slice(0, size);
this._decipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, AeadRandomCipherPreset.rawKey, AeadRandomCipherPreset.info, size); this._decipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, this._rawKey, this._info, size);
return buffer.slice(size); // drop salt return buffer.slice(size); // drop salt
} }
@ -225,15 +221,15 @@ export default class AeadRandomCipherPreset extends IPreset {
getPaddingLength(key, nonce) { getPaddingLength(key, nonce) {
const nonceBuffer = numberToBuffer(nonce, NONCE_LEN, BYTE_ORDER_LE); const nonceBuffer = numberToBuffer(nonce, NONCE_LEN, BYTE_ORDER_LE);
const cipher = crypto.createCipheriv(AeadRandomCipherPreset.cipherName, key, nonceBuffer); const cipher = crypto.createCipheriv(this._cipherName, key, nonceBuffer);
cipher.update(nonceBuffer); cipher.update(nonceBuffer);
cipher.final(); cipher.final();
return cipher.getAuthTag()[0] * AeadRandomCipherPreset.factor; return cipher.getAuthTag()[0] * this._factor;
} }
encrypt(message) { encrypt(message) {
const cipher = crypto.createCipheriv( const cipher = crypto.createCipheriv(
AeadRandomCipherPreset.cipherName, this._cipherName,
this._cipherKey, this._cipherKey,
numberToBuffer(this._cipherNonce, NONCE_LEN, BYTE_ORDER_LE) numberToBuffer(this._cipherNonce, NONCE_LEN, BYTE_ORDER_LE)
); );
@ -245,7 +241,7 @@ export default class AeadRandomCipherPreset extends IPreset {
decrypt(ciphertext, tag) { decrypt(ciphertext, tag) {
const decipher = crypto.createDecipheriv( const decipher = crypto.createDecipheriv(
AeadRandomCipherPreset.cipherName, this._cipherName,
this._decipherKey, this._decipherKey,
numberToBuffer(this._decipherNonce, NONCE_LEN, BYTE_ORDER_LE) numberToBuffer(this._decipherNonce, NONCE_LEN, BYTE_ORDER_LE)
); );

@ -76,15 +76,13 @@ export default class AutoConfPreset extends IPreset {
_header = null; _header = null;
static suites = []; static onCheckParams({suites}) {
static checkParams({suites}) {
if (typeof suites !== 'string' || suites.length < 1) { if (typeof suites !== 'string' || suites.length < 1) {
throw Error('\'suites\' is invalid'); throw Error('\'suites\' is invalid');
} }
} }
static async onInit({suites: uri}) { static async onCache({suites: uri}) {
logger.info(`[auto-conf] loading suites from: ${uri}`); logger.info(`[auto-conf] loading suites from: ${uri}`);
let suites = []; let suites = [];
if (uri.startsWith('http')) { if (uri.startsWith('http')) {
@ -101,7 +99,7 @@ export default class AutoConfPreset extends IPreset {
throw Error(`you must provide at least one suite in ${uri}`); throw Error(`you must provide at least one suite in ${uri}`);
} }
logger.info(`[auto-conf] ${suites.length} suites loaded`); logger.info(`[auto-conf] ${suites.length} suites loaded`);
AutoConfPreset.suites = suites; return {suites};
} }
onDestroy() { onDestroy() {
@ -111,7 +109,7 @@ export default class AutoConfPreset extends IPreset {
createRequestHeader(suites) { createRequestHeader(suites) {
const sid = crypto.randomBytes(2); const sid = crypto.randomBytes(2);
const utc = ntb(getCurrentTimestampInt(), 4, BYTE_ORDER_LE); const utc = ntb(getCurrentTimestampInt(), 4, BYTE_ORDER_LE);
const key = EVP_BytesToKey(Buffer.from(AutoConfPreset.config.key).toString('base64') + hash('md5', sid).toString('base64'), 16, 16); const key = EVP_BytesToKey(Buffer.from(this._config.key).toString('base64') + hash('md5', sid).toString('base64'), 16, 16);
const cipher = crypto.createCipheriv('rc4', key, NOOP); const cipher = crypto.createCipheriv('rc4', key, NOOP);
const enc_utc = cipher.update(utc); const enc_utc = cipher.update(utc);
const request_hmac = hmac('md5', key, Buffer.concat([sid, enc_utc])); const request_hmac = hmac('md5', key, Buffer.concat([sid, enc_utc]));
@ -122,7 +120,7 @@ export default class AutoConfPreset extends IPreset {
} }
encodeChangeSuite({buffer, broadcast, fail}) { encodeChangeSuite({buffer, broadcast, fail}) {
const {suites} = AutoConfPreset; const {suites} = this.getStore();
if (suites.length < 1) { if (suites.length < 1) {
return fail('suites are not initialized properly'); return fail('suites are not initialized properly');
} }
@ -140,7 +138,7 @@ export default class AutoConfPreset extends IPreset {
} }
decodeChangeSuite({buffer, broadcast, fail}) { decodeChangeSuite({buffer, broadcast, fail}) {
const {suites} = AutoConfPreset; const {suites} = this.getStore();
if (suites.length < 1) { if (suites.length < 1) {
return fail('suites are not initialized properly'); return fail('suites are not initialized properly');
} }
@ -149,7 +147,7 @@ export default class AutoConfPreset extends IPreset {
} }
const sid = buffer.slice(0, 2); const sid = buffer.slice(0, 2);
const request_hmac = buffer.slice(6, 22); const request_hmac = buffer.slice(6, 22);
const key = EVP_BytesToKey(Buffer.from(AutoConfPreset.config.KEY).toString('base64') + hash('md5', sid).toString('base64'), 16, 16); const key = EVP_BytesToKey(Buffer.from(this._config.key).toString('base64') + hash('md5', sid).toString('base64'), 16, 16);
const hmac_calc = hmac('md5', key, buffer.slice(0, 6)); const hmac_calc = hmac('md5', key, buffer.slice(0, 6));
if (!hmac_calc.equals(request_hmac)) { if (!hmac_calc.equals(request_hmac)) {
return fail(`unexpected hmac of client request, dump=${dumpHex(buffer)}`); return fail(`unexpected hmac of client request, dump=${dumpHex(buffer)}`);

@ -56,11 +56,11 @@ const DEFAULT_HASH_METHOD = 'sha1';
*/ */
export default class BaseAuthPreset extends IPresetAddressing { export default class BaseAuthPreset extends IPresetAddressing {
static hmacMethod = DEFAULT_HASH_METHOD; _hmacMethod = DEFAULT_HASH_METHOD;
static hmacLen = null; _hmacLen = null;
static hmacKey = null; _hmacKey = null;
_cipher = null; _cipher = null;
@ -78,24 +78,20 @@ export default class BaseAuthPreset extends IPresetAddressing {
_port = null; // buffer _port = null; // buffer
static checkParams({method = DEFAULT_HASH_METHOD}) { static onCheckParams({method = DEFAULT_HASH_METHOD}) {
const methods = Object.keys(HMAC_METHODS); const methods = Object.keys(HMAC_METHODS);
if (!methods.includes(method)) { if (!methods.includes(method)) {
throw Error(`base-auth 'method' must be one of [${methods}]`); throw Error(`base-auth 'method' must be one of [${methods}]`);
} }
} }
static onInit({method = DEFAULT_HASH_METHOD}) { onInit({method = DEFAULT_HASH_METHOD}) {
BaseAuthPreset.hmacMethod = method; const key = EVP_BytesToKey(this._config.key, 16, 16);
BaseAuthPreset.hmacLen = HMAC_METHODS[method]; const iv = hash('md5', Buffer.from(this._config.key + 'base-auth'));
BaseAuthPreset.hmacKey = EVP_BytesToKey(BaseAuthPreset.config.key, 16, 16); this._hmacMethod = method;
} this._hmacLen = HMAC_METHODS[method];
this._hmacKey = key;
constructor() { if (this._config.is_client) {
super();
const {hmacKey: key} = BaseAuthPreset;
const iv = hash('md5', Buffer.from(BaseAuthPreset.config.key + 'base-auth'));
if (BaseAuthPreset.config.is_client) {
this._cipher = crypto.createCipheriv('aes-128-cfb', key, iv); this._cipher = crypto.createCipheriv('aes-128-cfb', key, iv);
} else { } else {
this._decipher = crypto.createDecipheriv('aes-128-cfb', key, iv); this._decipher = crypto.createDecipheriv('aes-128-cfb', key, iv);
@ -111,7 +107,7 @@ export default class BaseAuthPreset extends IPresetAddressing {
} }
onNotified(action) { onNotified(action) {
if (BaseAuthPreset.config.is_client && action.type === CONNECT_TO_REMOTE) { if (this._config.is_client && action.type === CONNECT_TO_REMOTE) {
const {host, port} = action.payload; const {host, port} = action.payload;
this._host = Buffer.from(host); this._host = Buffer.from(host);
this._port = numberToBuffer(port); this._port = numberToBuffer(port);
@ -119,15 +115,14 @@ export default class BaseAuthPreset extends IPresetAddressing {
} }
encodeHeader() { encodeHeader() {
const {hmacMethod, hmacKey} = BaseAuthPreset;
const header = Buffer.concat([numberToBuffer(this._host.length, 1), this._host, this._port]); const header = Buffer.concat([numberToBuffer(this._host.length, 1), this._host, this._port]);
const encHeader = this._cipher.update(header); const encHeader = this._cipher.update(header);
const mac = hmac(hmacMethod, hmacKey, encHeader); const mac = hmac(this._hmacMethod, this._hmacKey, encHeader);
return Buffer.concat([encHeader, mac]); return Buffer.concat([encHeader, mac]);
} }
decodeHeader({buffer, fail}) { decodeHeader({buffer, fail}) {
const {hmacMethod, hmacLen, hmacKey} = BaseAuthPreset; const hmacLen = this._hmacLen;
// minimal length required // minimal length required
if (buffer.length < 31) { if (buffer.length < 31) {
@ -142,7 +137,7 @@ export default class BaseAuthPreset extends IPresetAddressing {
// check hmac // check hmac
const givenHmac = buffer.slice(1 + alen + 2, 1 + alen + 2 + hmacLen); const givenHmac = buffer.slice(1 + alen + 2, 1 + alen + 2 + hmacLen);
const expHmac = hmac(hmacMethod, hmacKey, buffer.slice(0, 1 + alen + 2)); const expHmac = hmac(this._hmacMethod, this._hmacKey, buffer.slice(0, 1 + alen + 2));
if (!givenHmac.equals(expHmac)) { if (!givenHmac.equals(expHmac)) {
return fail(`unexpected HMAC=${givenHmac.toString('hex')} want=${expHmac.toString('hex')} dump=${buffer.slice(0, 60).toString('hex')}`); return fail(`unexpected HMAC=${givenHmac.toString('hex')} want=${expHmac.toString('hex')} dump=${buffer.slice(0, 60).toString('hex')}`);
} }

@ -97,55 +97,60 @@ export const MUX_DATA_FRAME = '@action:mux_data_frame';
export const MUX_CLOSE_CONN = '@action:mux_close_conn'; export const MUX_CLOSE_CONN = '@action:mux_close_conn';
/** /**
*
* @lifecycle * @lifecycle
* static checkParams() -> static onInit() -> constructor() -> ... -> onDestroy() * static onCheckParams()
* Only called once * static onCache()
* constructor()
* onInit()
* ...
* onDestroy()
*
* @note
* static onCheckParams() and static onCache() are called only once since new Hub().
*/ */
export class IPreset { export class IPreset {
/** /**
* will become true after checkParams() * config
* @type {boolean}
*/
static checked = false;
/**
* server config
* @type {Config} * @type {Config}
*/ */
static config = null; _config = null;
/**
* will become true after onInit()
* @type {boolean}
*/
static initialized = false;
/** /**
* check params passed to the preset, if any errors, should throw directly * check params passed to the preset, if any errors, should throw directly
* @param params * @param params
*/ */
static checkParams(params) { static onCheckParams(params) {
} }
/** /**
* you can make some cache in this function * you can make some cache in store or just return something
* you want to put in store, then access store later in other
* hook functions via this.getStore()
* @param params
* @param store
*/
static onCache(params, store) {
// or return something
}
/**
* constructor
* @param config
* @param params * @param params
*/ */
static onInit(params) { constructor({config, params} = {}) {
if (config) {
this._config = config;
}
} }
// properties
/** /**
* return the preset name * constructor alternative to do initialization
* @returns {string} * @param params
*/ */
getName() { onInit(params) {
} }
@ -218,7 +223,7 @@ export class IPreset {
return buffer; return buffer;
} }
// auto-generated methods for convenience, DO NOT implement them! // auto-generated methods, DO NOT implement them!
next(direction, buffer) { next(direction, buffer) {
@ -241,6 +246,13 @@ export class IPreset {
} }
/**
* return store passed to onCache()
*/
getStore() {
}
} }
/** /**
@ -250,23 +262,6 @@ export class IPresetAddressing extends IPreset {
} }
/**
* a class which only have one instance
*/
export class IPresetStatic extends IPreset {
static isInstantiated = false;
constructor() {
super();
if (IPresetStatic.isInstantiated) {
throw Error(`${this.constructor.name} is singleton and can only be instantiated once`);
}
IPresetStatic.isInstantiated = true;
}
}
/** /**
* check if a class is a valid preset class * check if a class is a valid preset class
* @param clazz * @param clazz
@ -278,14 +273,14 @@ export function checkPresetClass(clazz) {
} }
// check require hooks // check require hooks
const requiredMethods = [ const requiredMethods = [
'getName', 'onNotified', 'onDestroy', 'onNotified', 'onDestroy', 'onInit',
'beforeOut', 'beforeIn', 'clientOut', 'serverIn', 'serverOut', 'clientIn', 'beforeOut', 'beforeIn', 'clientOut', 'serverIn', 'serverOut', 'clientIn',
'beforeOutUdp', 'beforeInUdp', 'clientOutUdp', 'serverInUdp', 'serverOutUdp', 'clientInUdp' 'beforeOutUdp', 'beforeInUdp', 'clientOutUdp', 'serverInUdp', 'serverOutUdp', 'clientInUdp'
]; ];
if (requiredMethods.some((method) => typeof clazz.prototype[method] !== 'function')) { if (requiredMethods.some((method) => typeof clazz.prototype[method] !== 'function')) {
return false; return false;
} }
const requiredStaticMethods = ['checkParams', 'onInit']; const requiredStaticMethods = ['onCheckParams', 'onCache'];
if (requiredStaticMethods.some((method) => typeof clazz[method] !== 'function')) { if (requiredStaticMethods.some((method) => typeof clazz[method] !== 'function')) {
return false; return false;
} }

@ -1,9 +1,7 @@
import {checkPresetClass} from './defs'; import {checkPresetClass} from './defs';
// functional // functional
import StatsPreset from './stats';
import TrackerPreset from './tracker'; import TrackerPreset from './tracker';
import AccessControlPreset from './access-control';
import AutoConfPreset from './auto-conf'; import AutoConfPreset from './auto-conf';
import MuxPreset from './mux'; import MuxPreset from './mux';
@ -32,33 +30,9 @@ import ObfsTls12TicketPreset from './obfs-tls1.2-ticket';
// others // others
import AeadRandomCipherPreset from './aead-random-cipher'; import AeadRandomCipherPreset from './aead-random-cipher';
function monkeyPatch(clazz) {
// patch onInit()
clazz.onInit = (function (onInit) {
return function _onInit(...args) {
if (!clazz.initialized) {
onInit(...args);
clazz.initialized = true;
}
};
})(clazz.onInit);
// patch checkParams()
clazz.checkParams = (function (checkParams) {
return function _checkParams(...args) {
if (!clazz.checked) {
checkParams(...args);
clazz.checked = true;
}
};
})(clazz.checkParams);
}
const mapping = { const mapping = {
// functional // functional
'stats': StatsPreset,
'tracker': TrackerPreset, 'tracker': TrackerPreset,
'access-control': AccessControlPreset,
'auto-conf': AutoConfPreset, 'auto-conf': AutoConfPreset,
'mux': MuxPreset, 'mux': MuxPreset,
@ -90,8 +64,6 @@ const mapping = {
const presetClasses = {...mapping}; const presetClasses = {...mapping};
Object.keys(presetClasses).forEach((clazzName) => monkeyPatch(presetClasses[clazzName]));
export function getPresetClassByName(name) { export function getPresetClassByName(name) {
let clazz = presetClasses[name]; let clazz = presetClasses[name];
if (clazz === undefined) { if (clazz === undefined) {

@ -53,8 +53,7 @@ export default class MuxPreset extends IPresetAddressing {
_adBuf = null; _adBuf = null;
constructor() { onInit() {
super();
this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)}); this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)});
this._adBuf.on('data', this.onChunkReceived.bind(this)); this._adBuf.on('data', this.onChunkReceived.bind(this));
} }

@ -63,22 +63,22 @@ function parseFile(file) {
*/ */
export default class ObfsHttpPreset extends IPreset { export default class ObfsHttpPreset extends IPreset {
static pairs = null;
_isHeaderSent = false; _isHeaderSent = false;
_isHeaderRecv = false; _isHeaderRecv = false;
_response = null; _response = null;
static checkParams({file}) { static onCheckParams({file}) {
if (typeof file !== 'string' || file === '') { if (typeof file !== 'string' || file === '') {
throw Error('\'file\' must be a non-empty string'); throw Error('\'file\' must be a non-empty string');
} }
} }
static onInit({file}) { static onCache({file}) {
ObfsHttpPreset.pairs = parseFile(file); return {
pairs: parseFile(file),
};
} }
onDestroy() { onDestroy() {
@ -87,7 +87,7 @@ export default class ObfsHttpPreset extends IPreset {
clientOut({buffer}) { clientOut({buffer}) {
if (!this._isHeaderSent) { if (!this._isHeaderSent) {
const {pairs} = ObfsHttpPreset; const {pairs} = this.getStore();
this._isHeaderSent = true; this._isHeaderSent = true;
const index = crypto.randomBytes(1)[0] % pairs.length; const index = crypto.randomBytes(1)[0] % pairs.length;
const {request} = pairs[index]; const {request} = pairs[index];
@ -99,7 +99,7 @@ export default class ObfsHttpPreset extends IPreset {
serverIn({buffer, fail}) { serverIn({buffer, fail}) {
if (!this._isHeaderRecv) { if (!this._isHeaderRecv) {
const found = ObfsHttpPreset.pairs.find(({request}) => buffer.indexOf(request) === 0); const found = this.getStore().pairs.find(({request}) => buffer.indexOf(request) === 0);
if (found !== undefined) { if (found !== undefined) {
this._isHeaderRecv = true; this._isHeaderRecv = true;
this._response = found.response; this._response = found.response;
@ -123,7 +123,7 @@ export default class ObfsHttpPreset extends IPreset {
clientIn({buffer, fail}) { clientIn({buffer, fail}) {
if (!this._isHeaderRecv) { if (!this._isHeaderRecv) {
const found = ObfsHttpPreset.pairs.find(({response}) => buffer.indexOf(response) === 0); const found = this._config.store.pairs.find(({response}) => buffer.indexOf(response) === 0);
if (found !== undefined) { if (found !== undefined) {
this._isHeaderRecv = true; this._isHeaderRecv = true;
return buffer.slice(found.response.length); return buffer.slice(found.response.length);

@ -43,8 +43,7 @@ export default class ObfsRandomPaddingPreset extends IPreset {
_adBuf = null; _adBuf = null;
constructor() { onInit() {
super();
this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)}); this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)});
this._adBuf.on('data', this.onChunkReceived.bind(this)); this._adBuf.on('data', this.onChunkReceived.bind(this));
} }

@ -78,7 +78,7 @@ export default class ObfsTls12TicketPreset extends IPreset {
_adBuf = null; _adBuf = null;
static checkParams({sni}) { static onCheckParams({sni}) {
if (typeof sni === 'undefined') { if (typeof sni === 'undefined') {
throw Error('\'sni\' must be set'); throw Error('\'sni\' must be set');
} }
@ -90,13 +90,10 @@ export default class ObfsTls12TicketPreset extends IPreset {
} }
} }
constructor({sni}) { onInit({sni}) {
super();
this.onReceiving = this.onReceiving.bind(this);
this.onChunkReceived = this.onChunkReceived.bind(this);
this._sni = Array.isArray(sni) ? sni : [sni]; this._sni = Array.isArray(sni) ? sni : [sni];
this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving}); this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)});
this._adBuf.on('data', this.onChunkReceived); this._adBuf.on('data', this.onChunkReceived.bind(this));
} }
onDestroy() { onDestroy() {

@ -95,19 +95,19 @@ const HKDF_INFO = 'ss-subkey';
*/ */
export default class SsAeadCipherPreset extends IPreset { export default class SsAeadCipherPreset extends IPreset {
static cipherName = ''; _cipherName = '';
static info = Buffer.from(HKDF_INFO); _info = Buffer.from(HKDF_INFO);
static keySize = 0; _keySize = 0;
static saltSize = 0; _saltSize = 0;
static nonceSize = 0; _nonceSize = 0;
static evpKey = null; _evpKey = null;
static isUseLibSodium = false; _isUseLibSodium = false;
_cipherKey = null; _cipherKey = null;
@ -119,25 +119,21 @@ export default class SsAeadCipherPreset extends IPreset {
_adBuf = null; _adBuf = null;
static checkParams({method}) { static onCheckParams({method}) {
const cipherNames = Object.keys(ciphers); const cipherNames = Object.keys(ciphers);
if (!cipherNames.includes(method)) { if (!cipherNames.includes(method)) {
throw Error(`'method' must be one of [${cipherNames}]`); throw Error(`'method' must be one of [${cipherNames}]`);
} }
} }
static onInit({method}) { onInit({method}) {
const [keySize, saltSize, nonceSize] = ciphers[method]; const [keySize, saltSize, nonceSize] = ciphers[method];
SsAeadCipherPreset.cipherName = method; this._cipherName = method;
SsAeadCipherPreset.keySize = keySize; this._keySize = keySize;
SsAeadCipherPreset.saltSize = saltSize; this._saltSize = saltSize;
SsAeadCipherPreset.nonceSize = nonceSize; this._nonceSize = nonceSize;
SsAeadCipherPreset.evpKey = EVP_BytesToKey(SsAeadCipherPreset.config.key, keySize, 16); this._evpKey = EVP_BytesToKey(this._config.key, keySize, 16);
SsAeadCipherPreset.isUseLibSodium = Object.keys(libsodium_functions).includes(method); this._isUseLibSodium = Object.keys(libsodium_functions).includes(method);
}
constructor() {
super();
this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)}); this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)});
this._adBuf.on('data', this.onChunkReceived.bind(this)); this._adBuf.on('data', this.onChunkReceived.bind(this));
} }
@ -156,9 +152,8 @@ export default class SsAeadCipherPreset extends IPreset {
beforeOut({buffer}) { beforeOut({buffer}) {
let salt = null; let salt = null;
if (this._cipherKey === null) { if (this._cipherKey === null) {
const {keySize, saltSize, evpKey, info} = SsAeadCipherPreset; salt = crypto.randomBytes(this._saltSize);
salt = crypto.randomBytes(saltSize); this._cipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, this._evpKey, this._info, this._keySize);
this._cipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, evpKey, info, keySize);
} }
const chunks = getRandomChunks(buffer, MIN_CHUNK_SPLIT_LEN, MAX_CHUNK_SPLIT_LEN).map((chunk) => { const chunks = getRandomChunks(buffer, MIN_CHUNK_SPLIT_LEN, MAX_CHUNK_SPLIT_LEN).map((chunk) => {
const dataLen = numberToBuffer(chunk.length); const dataLen = numberToBuffer(chunk.length);
@ -179,12 +174,12 @@ export default class SsAeadCipherPreset extends IPreset {
onReceiving(buffer, {fail}) { onReceiving(buffer, {fail}) {
if (this._decipherKey === null) { if (this._decipherKey === null) {
const {keySize, saltSize, evpKey, info} = SsAeadCipherPreset; const saltSize = this._saltSize;
if (buffer.length < saltSize) { if (buffer.length < saltSize) {
return; // too short to get salt return; // too short to get salt
} }
const salt = buffer.slice(0, saltSize); const salt = buffer.slice(0, saltSize);
this._decipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, evpKey, info, keySize); this._decipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, this._evpKey, this._info, this._keySize);
return buffer.slice(saltSize); // drop salt return buffer.slice(saltSize); // drop salt
} }
@ -218,12 +213,12 @@ export default class SsAeadCipherPreset extends IPreset {
} }
encrypt(message) { encrypt(message) {
const {isUseLibSodium, cipherName, nonceSize} = SsAeadCipherPreset; const cipherName = this._cipherName;
const cipherKey = this._cipherKey; const cipherKey = this._cipherKey;
const nonce = numberToBuffer(this._cipherNonce, nonceSize, BYTE_ORDER_LE); const nonce = numberToBuffer(this._cipherNonce, this._nonceSize, BYTE_ORDER_LE);
let ciphertext = null; let ciphertext = null;
let tag = null; let tag = null;
if (isUseLibSodium) { if (this._isUseLibSodium) {
const noop = Buffer.alloc(0); const noop = Buffer.alloc(0);
const result = libsodium[libsodium_functions[cipherName][0]]( const result = libsodium[libsodium_functions[cipherName][0]](
message, noop, noop, nonce, cipherKey message, noop, noop, nonce, cipherKey
@ -240,10 +235,10 @@ export default class SsAeadCipherPreset extends IPreset {
} }
decrypt(ciphertext, tag) { decrypt(ciphertext, tag) {
const {isUseLibSodium, cipherName, nonceSize} = SsAeadCipherPreset; const cipherName = this._cipherName;
const decipherKey = this._decipherKey; const decipherKey = this._decipherKey;
const nonce = numberToBuffer(this._decipherNonce, nonceSize, BYTE_ORDER_LE); const nonce = numberToBuffer(this._decipherNonce, this._nonceSize, BYTE_ORDER_LE);
if (isUseLibSodium) { if (this._isUseLibSodium) {
const noop = Buffer.alloc(0); const noop = Buffer.alloc(0);
try { try {
const plaintext = libsodium[libsodium_functions[cipherName][1]]( const plaintext = libsodium[libsodium_functions[cipherName][1]](
@ -270,21 +265,20 @@ export default class SsAeadCipherPreset extends IPreset {
// udp // udp
beforeOutUdp({buffer}) { beforeOutUdp({buffer}) {
const {keySize, saltSize, evpKey, info} = SsAeadCipherPreset; const salt = crypto.randomBytes(this._saltSize);
const salt = crypto.randomBytes(saltSize); this._cipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, this._evpKey, this._info, this._keySize);
this._cipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, evpKey, info, keySize);
this._cipherNonce = 0; this._cipherNonce = 0;
const [ciphertext, tag] = this.encrypt(buffer); const [ciphertext, tag] = this.encrypt(buffer);
return Buffer.concat([salt, ciphertext, tag]); return Buffer.concat([salt, ciphertext, tag]);
} }
beforeInUdp({buffer, fail}) { beforeInUdp({buffer, fail}) {
const {keySize, saltSize, evpKey, info} = SsAeadCipherPreset; const saltSize = this._saltSize;
if (buffer.length < saltSize) { if (buffer.length < saltSize) {
return fail(`too short to get salt, len=${buffer.length} dump=${buffer.toString('hex')}`); return fail(`too short to get salt, len=${buffer.length} dump=${buffer.toString('hex')}`);
} }
const salt = buffer.slice(0, saltSize); const salt = buffer.slice(0, saltSize);
this._decipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, evpKey, info, keySize); this._decipherKey = HKDF(HKDF_HASH_ALGORITHM, salt, this._evpKey, this._info, this._keySize);
this._decipherNonce = 0; this._decipherNonce = 0;
if (buffer.length < saltSize + TAG_SIZE + 1) { if (buffer.length < saltSize + TAG_SIZE + 1) {
return fail(`too short to verify Data, len=${buffer.length} dump=${buffer.toString('hex')}`); return fail(`too short to verify Data, len=${buffer.length} dump=${buffer.toString('hex')}`);

@ -84,7 +84,7 @@ export default class SsBasePreset extends IPresetAddressing {
} }
onNotified(action) { onNotified(action) {
if (SsBasePreset.config.is_client && action.type === CONNECT_TO_REMOTE) { if (this._config.is_client && action.type === CONNECT_TO_REMOTE) {
const {host, port} = action.payload; const {host, port} = action.payload;
const type = getHostType(host); const type = getHostType(host);
this._atyp = type; this._atyp = type;

@ -79,7 +79,15 @@ export default class SsStreamCipherPreset extends IPreset {
_cipher = null; _cipher = null;
_decipher = null; _decipher = null;
static checkParams({method}) { get key() {
return this._key;
}
get iv() {
return this._iv;
}
static onCheckParams({method}) {
if (typeof method !== 'string' || method === '') { if (typeof method !== 'string' || method === '') {
throw Error('\'method\' must be set'); throw Error('\'method\' must be set');
} }
@ -89,22 +97,13 @@ export default class SsStreamCipherPreset extends IPreset {
} }
} }
get key() { onInit({method}) {
return this._key;
}
get iv() {
return this._iv;
}
constructor({method}) {
super();
const [keySize, ivSize] = ciphers[method]; const [keySize, ivSize] = ciphers[method];
const iv = crypto.randomBytes(ivSize); const iv = crypto.randomBytes(ivSize);
this._algorithm = ['rc4-md5', 'rc4-md5-6'].includes(method) ? 'rc4' : method; this._algorithm = ['rc4-md5', 'rc4-md5-6'].includes(method) ? 'rc4' : method;
this._keySize = keySize; this._keySize = keySize;
this._ivSize = ivSize; this._ivSize = ivSize;
this._key = EVP_BytesToKey(SsStreamCipherPreset.config.key, keySize, ivSize); this._key = EVP_BytesToKey(this._config.key, keySize, ivSize);
this._iv = method === 'rc4-md5-6' ? iv.slice(0, 6) : iv; this._iv = method === 'rc4-md5-6' ? iv.slice(0, 6) : iv;
} }

@ -16,8 +16,8 @@ import SsrAuthAes128Preset from './ssr-auth-aes128';
*/ */
export default class SsrAuthAes128Md5Preset extends SsrAuthAes128Preset { export default class SsrAuthAes128Md5Preset extends SsrAuthAes128Preset {
constructor(params) { constructor(props) {
super(params); super(props);
this._hashFunc = 'md5'; this._hashFunc = 'md5';
this._salt = 'auth_aes128_md5'; this._salt = 'auth_aes128_md5';
} }

@ -16,8 +16,8 @@ import SsrAuthAes128Preset from './ssr-auth-aes128';
*/ */
export default class SsrAuthAes128Sha1Preset extends SsrAuthAes128Preset { export default class SsrAuthAes128Sha1Preset extends SsrAuthAes128Preset {
constructor(params) { constructor(props) {
super(params); super(props);
this._hashFunc = 'sha1'; this._hashFunc = 'sha1';
this._salt = 'auth_aes128_sha1'; this._salt = 'auth_aes128_sha1';
} }

@ -82,9 +82,9 @@ const MAX_TIME_DIFF = 30; // seconds
*/ */
export default class SsrAuthAes128Preset extends IPreset { export default class SsrAuthAes128Preset extends IPreset {
static clientId = null; _clientId = null;
static connectionId = null; _connectionId = null;
_userKey = null; _userKey = null;
@ -102,14 +102,9 @@ export default class SsrAuthAes128Preset extends IPreset {
_adBuf = null; _adBuf = null;
static onInit() { onInit() {
SsrAuthAes128Preset.userKey = EVP_BytesToKey(SsrAuthAes128Preset.config.key, 16, 16); this._clientId = crypto.randomBytes(4);
SsrAuthAes128Preset.clientId = crypto.randomBytes(4); this._connectionId = getRandomInt(0, 0x00ffffff);
SsrAuthAes128Preset.connectionId = getRandomInt(0, 0x00ffffff);
}
constructor() {
super();
this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)}); this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)});
this._adBuf.on('data', this.onChunkReceived.bind(this)); this._adBuf.on('data', this.onChunkReceived.bind(this));
} }
@ -125,7 +120,8 @@ export default class SsrAuthAes128Preset extends IPreset {
} }
createRequest(buffer) { createRequest(buffer) {
const {clientId, connectionId} = SsrAuthAes128Preset; const clientId = this._clientId;
const connectionId = this._connectionId;
const userKey = this._userKey = this.readProperty('ss-stream-cipher', 'key'); const userKey = this._userKey = this.readProperty('ss-stream-cipher', 'key');
const iv = this.readProperty('ss-stream-cipher', 'iv'); const iv = this.readProperty('ss-stream-cipher', 'iv');
@ -148,9 +144,9 @@ export default class SsrAuthAes128Preset extends IPreset {
if (connectionId > 0xff000000) { if (connectionId > 0xff000000) {
connection_id = getRandomInt(0, 0x00ffffff); connection_id = getRandomInt(0, 0x00ffffff);
client_id = crypto.randomBytes(4); client_id = crypto.randomBytes(4);
SsrAuthAes128Preset.connectionId = connection_id; this._connectionId = connection_id;
} else { } else {
connection_id = ++SsrAuthAes128Preset.connectionId; connection_id = ++this._connectionId;
} }
const random_bytes_len = getRandomInt(0, buffer.length > 400 ? 512 : 1024); const random_bytes_len = getRandomInt(0, buffer.length > 400 ? 512 : 1024);

@ -16,8 +16,8 @@ import SsrAuthChainPreset from './ssr-auth-chain';
*/ */
export default class SsrAuthChainAPreset extends SsrAuthChainPreset { export default class SsrAuthChainAPreset extends SsrAuthChainPreset {
constructor(params) { constructor(props) {
super(params); super(props);
this._salt = 'auth_chain_a'; this._salt = 'auth_chain_a';
} }

@ -41,8 +41,8 @@ export default class SsrAuthChainBPreset extends SsrAuthChainPreset {
_data_size_list2 = []; _data_size_list2 = [];
constructor(params) { constructor(props) {
super(params); super(props);
this._salt = 'auth_chain_b'; this._salt = 'auth_chain_b';
} }

@ -111,9 +111,9 @@ export function xorshift128plus() {
*/ */
export default class SsrAuthChainPreset extends IPreset { export default class SsrAuthChainPreset extends IPreset {
static clientId = null; _clientId = null;
static connectionId = null; _connectionId = null;
_userKey = null; _userKey = null;
@ -143,13 +143,9 @@ export default class SsrAuthChainPreset extends IPreset {
_adBuf = null; _adBuf = null;
static onInit() { onInit() {
SsrAuthChainPreset.clientId = crypto.randomBytes(4); this._clientId = crypto.randomBytes(4);
SsrAuthChainPreset.connectionId = getRandomInt(0, 0x00ffffff); this._connectionId = getRandomInt(0, 0x00ffffff);
}
constructor() {
super();
this._rngClient = xorshift128plus(); this._rngClient = xorshift128plus();
this._rngServer = xorshift128plus(); this._rngServer = xorshift128plus();
this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)}); this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)});
@ -179,7 +175,8 @@ export default class SsrAuthChainPreset extends IPreset {
} }
createRequest() { createRequest() {
const {clientId, connectionId} = SsrAuthChainPreset; const clientId = this._clientId;
const connectionId = this._connectionId;
const userKey = this._userKey = this.readProperty('ss-stream-cipher', 'key'); const userKey = this._userKey = this.readProperty('ss-stream-cipher', 'key');
const iv = this.readProperty('ss-stream-cipher', 'iv'); const iv = this.readProperty('ss-stream-cipher', 'iv');
@ -203,9 +200,9 @@ export default class SsrAuthChainPreset extends IPreset {
if (connectionId > 0xff000000) { if (connectionId > 0xff000000) {
connection_id = getRandomInt(0, 0x00ffffff); connection_id = getRandomInt(0, 0x00ffffff);
client_id = crypto.randomBytes(4); client_id = crypto.randomBytes(4);
SsrAuthChainPreset.connectionId = connection_id; this._connectionId = connection_id;
} else { } else {
connection_id = ++SsrAuthChainPreset.connectionId; connection_id = ++this._connectionId;
} }
const overhead = ntb(this._overhead, 2, BYTE_ORDER_LE); const overhead = ntb(this._overhead, 2, BYTE_ORDER_LE);
@ -232,17 +229,17 @@ export default class SsrAuthChainPreset extends IPreset {
createChunks(buffer) { createChunks(buffer) {
const userKey = this._userKey; const userKey = this._userKey;
const max_payload_size = SsrAuthChainPreset.config.is_client ? 2800 : (this._tcpMss - this._overhead); const max_payload_size = this._config.is_client ? 2800 : (this._tcpMss - this._overhead);
return getChunks(buffer, max_payload_size).map((payload) => { return getChunks(buffer, max_payload_size).map((payload) => {
let _payload = payload; let _payload = payload;
if (SsrAuthChainPreset.config.is_server && this._encodeChunkId === 1) { if (this._config.is_server && this._encodeChunkId === 1) {
_payload = Buffer.concat([ntb(this._tcpMss, 2, BYTE_ORDER_LE), payload]); _payload = Buffer.concat([ntb(this._tcpMss, 2, BYTE_ORDER_LE), payload]);
} }
const rc4_enc_payload = this._cipher.update(_payload); const rc4_enc_payload = this._cipher.update(_payload);
const hash = SsrAuthChainPreset.config.is_client ? this._lastClientHash : this._lastServerHash; const hash = this._config.is_client ? this._lastClientHash : this._lastServerHash;
const size = rc4_enc_payload.length ^ hash.slice(-2).readUInt16LE(0); const size = rc4_enc_payload.length ^ hash.slice(-2).readUInt16LE(0);
// generate two pieces of random bytes // generate two pieces of random bytes
const rng = SsrAuthChainPreset.config.is_client ? this._rngClient : this._rngServer; const rng = this._config.is_client ? this._rngClient : this._rngServer;
const random_bytes_len = this.getRandomBytesLengthForTcp(hash, _payload.length, rng); const random_bytes_len = this.getRandomBytesLengthForTcp(hash, _payload.length, rng);
const random_bytes = crypto.randomBytes(random_bytes_len); const random_bytes = crypto.randomBytes(random_bytes_len);
const random_divide_pos = random_bytes_len > 0 ? rng.next().mod(8589934609).mod(random_bytes_len).toNumber() : 0; const random_divide_pos = random_bytes_len > 0 ? rng.next().mod(8589934609).mod(random_bytes_len).toNumber() : 0;
@ -253,7 +250,7 @@ export default class SsrAuthChainPreset extends IPreset {
const hmac_key = Buffer.concat([userKey, ntb(this._encodeChunkId, 4, BYTE_ORDER_LE)]); const hmac_key = Buffer.concat([userKey, ntb(this._encodeChunkId, 4, BYTE_ORDER_LE)]);
const chunk_hmac = hmac('md5', hmac_key, chunk); const chunk_hmac = hmac('md5', hmac_key, chunk);
chunk = Buffer.concat([chunk, chunk_hmac.slice(0, 2)]); chunk = Buffer.concat([chunk, chunk_hmac.slice(0, 2)]);
if (SsrAuthChainPreset.config.is_client) { if (this._config.is_client) {
this._lastClientHash = chunk_hmac; this._lastClientHash = chunk_hmac;
} else { } else {
this._lastServerHash = chunk_hmac; this._lastServerHash = chunk_hmac;
@ -350,9 +347,9 @@ export default class SsrAuthChainPreset extends IPreset {
if (buffer.length < 2 || this._adBuf === null) { if (buffer.length < 2 || this._adBuf === null) {
return; // too short to get size return; // too short to get size
} }
const hash = SsrAuthChainPreset.config.is_client ? this._lastServerHash : this._lastClientHash; const hash = this._config.is_client ? this._lastServerHash : this._lastClientHash;
const payload_len = buffer.readUInt16LE(0) ^ hash.readUInt16LE(14); const payload_len = buffer.readUInt16LE(0) ^ hash.readUInt16LE(14);
const rng = SsrAuthChainPreset.config.is_client ? this._rngServer : this._rngClient; const rng = this._config.is_client ? this._rngServer : this._rngClient;
const random_bytes_len = this.getRandomBytesLengthForTcp(hash, payload_len, rng); const random_bytes_len = this.getRandomBytesLengthForTcp(hash, payload_len, rng);
const chunk_size = 2 + random_bytes_len + payload_len + 2; const chunk_size = 2 + random_bytes_len + payload_len + 2;
if (chunk_size >= 4096) { if (chunk_size >= 4096) {
@ -376,9 +373,9 @@ export default class SsrAuthChainPreset extends IPreset {
return fail(`unexpected chunk hmac, chunk=${dumpHex(chunk)}`); return fail(`unexpected chunk hmac, chunk=${dumpHex(chunk)}`);
} }
// drop random_bytes, get encrypted payload // drop random_bytes, get encrypted payload
const hash = SsrAuthChainPreset.config.is_client ? this._lastServerHash : this._lastClientHash; const hash = this._config.is_client ? this._lastServerHash : this._lastClientHash;
const payload_len = chunk.readUInt16LE(0) ^ hash.readUInt16LE(14); const payload_len = chunk.readUInt16LE(0) ^ hash.readUInt16LE(14);
const rng = SsrAuthChainPreset.config.is_client ? this._rngServer : this._rngClient; const rng = this._config.is_client ? this._rngServer : this._rngClient;
const random_bytes_len = this.getRandomBytesLengthForTcp(hash, payload_len, rng); const random_bytes_len = this.getRandomBytesLengthForTcp(hash, payload_len, rng);
let enc_payload = null; let enc_payload = null;
if (random_bytes_len > 0) { if (random_bytes_len > 0) {
@ -390,12 +387,12 @@ export default class SsrAuthChainPreset extends IPreset {
// decrypt payload // decrypt payload
let payload = this._decipher.update(enc_payload); let payload = this._decipher.update(enc_payload);
// update hash // update hash
if (SsrAuthChainPreset.config.is_client) { if (this._config.is_client) {
this._lastServerHash = new_hash; this._lastServerHash = new_hash;
} else { } else {
this._lastClientHash = new_hash; this._lastClientHash = new_hash;
} }
if (SsrAuthChainPreset.config.is_client && this._decodeChunkId === 1) { if (this._config.is_client && this._decodeChunkId === 1) {
this._tcpMss = payload.readUInt16LE(0); this._tcpMss = payload.readUInt16LE(0);
payload = payload.slice(2); payload = payload.slice(2);
} }

@ -95,7 +95,7 @@ export default class TrackerPreset extends IPreset {
if (strs.length > TRACK_MAX_SIZE) { if (strs.length > TRACK_MAX_SIZE) {
strs = strs.slice(0, perSize).concat([' ... ']).concat(strs.slice(-perSize)); strs = strs.slice(0, perSize).concat([' ... ']).concat(strs.slice(-perSize));
} }
const summary = TrackerPreset.config.is_client ? `out/in = ${up}/${dp}, ${ub}b/${db}b` : `in/out = ${dp}/${up}, ${db}b/${ub}b`; const summary = this._config.is_client ? `out/in = ${up}/${dp}, ${ub}b/${db}b` : `in/out = ${dp}/${up}, ${db}b/${ub}b`;
logger.info(`[tracker:${this._transport}] summary(${summary}) abstract(${strs.join(' ')})`); logger.info(`[tracker:${this._transport}] summary(${summary}) abstract(${strs.join(' ')})`);
} }

@ -159,13 +159,8 @@ function createChacha20Poly1305Key(key) {
*/ */
export default class V2rayVmessPreset extends IPresetAddressing { export default class V2rayVmessPreset extends IPresetAddressing {
static uuid = null; _uuid = null;
_security = null;
static security = null;
static userHashCache = [
// {timestamp, authInfo}
];
_atyp = null; _atyp = null;
_host = null; // buffer _host = null; // buffer
@ -192,7 +187,7 @@ export default class V2rayVmessPreset extends IPresetAddressing {
_cipherNonce = 0; _cipherNonce = 0;
_decipherNonce = 0; _decipherNonce = 0;
static checkParams({id, security = 'aes-128-gcm'}) { static onCheckParams({id, security = 'aes-128-gcm'}) {
if (Buffer.from(id.split('-').join(''), 'hex').length !== 16) { if (Buffer.from(id.split('-').join(''), 'hex').length !== 16) {
throw Error('id is not a valid uuid'); throw Error('id is not a valid uuid');
} }
@ -202,17 +197,16 @@ export default class V2rayVmessPreset extends IPresetAddressing {
} }
} }
static onInit({id, security = 'aes-128-gcm'}) { static onCache(_, store) {
V2rayVmessPreset.uuid = Buffer.from(id.split('-').join(''), 'hex'); setInterval(() => V2rayVmessPreset.updateAuthCache(store), 1e3);
if (V2rayVmessPreset.config.is_client) { V2rayVmessPreset.updateAuthCache(store);
V2rayVmessPreset.security = securityTypes[security];
}
setInterval(() => V2rayVmessPreset.updateAuthCache(), 1e3);
V2rayVmessPreset.updateAuthCache();
} }
static updateAuthCache() { static updateAuthCache(store) {
const items = this.userHashCache; const items = store.userHashCache || [
// {timestamp, authInfo},
// ...
];
const now = getCurrentTimestampInt(); const now = getCurrentTimestampInt();
let from = now - TIME_TOLERANCE; let from = now - TIME_TOLERANCE;
const to = now + TIME_TOLERANCE; const to = now + TIME_TOLERANCE;
@ -224,15 +218,18 @@ export default class V2rayVmessPreset extends IPresetAddressing {
} }
for (let ts = from; ts <= to; ++ts) { for (let ts = from; ts <= to; ++ts) {
// account auth info, 16 bytes // account auth info, 16 bytes
const uuid = this.uuid; const uuid = this._uuid;
const authInfo = hmac('md5', uuid, ntb(ts, 8)); const authInfo = hmac('md5', uuid, ntb(ts, 8));
newItems.push({timestamp: ts, authInfo: authInfo}); newItems.push({timestamp: ts, authInfo: authInfo});
} }
this.userHashCache = newItems; store.userHashCache = newItems;
} }
constructor() { onInit({id, security = 'aes-128-gcm'}) {
super(); this._uuid = Buffer.from(id.split('-').join(''), 'hex');
if (this._config.is_client) {
this._security = securityTypes[security];
}
this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)}); this._adBuf = new AdvancedBuffer({getPacketLength: this.onReceiving.bind(this)});
this._adBuf.on('data', this.onChunkReceived.bind(this)); this._adBuf.on('data', this.onChunkReceived.bind(this));
} }
@ -254,7 +251,7 @@ export default class V2rayVmessPreset extends IPresetAddressing {
} }
onNotified(action) { onNotified(action) {
if (V2rayVmessPreset.config.is_client && action.type === CONNECT_TO_REMOTE) { if (this._config.is_client && action.type === CONNECT_TO_REMOTE) {
const {host, port} = action.payload; const {host, port} = action.payload;
const type = getAddrType(host); const type = getAddrType(host);
this._atyp = type; this._atyp = type;
@ -269,7 +266,7 @@ export default class V2rayVmessPreset extends IPresetAddressing {
beforeOut({buffer}) { beforeOut({buffer}) {
if (!this._isHeaderSent) { if (!this._isHeaderSent) {
this._isHeaderSent = true; this._isHeaderSent = true;
const header = V2rayVmessPreset.config.is_client ? this.createRequestHeader() : this.createResponseHeader(); const header = this._config.is_client ? this.createRequestHeader() : this.createResponseHeader();
const chunks = this.getBufferChunks(buffer); const chunks = this.getBufferChunks(buffer);
return Buffer.concat([header, ...chunks]); return Buffer.concat([header, ...chunks]);
} else { } else {
@ -307,7 +304,8 @@ export default class V2rayVmessPreset extends IPresetAddressing {
return fail(`fail to parse request header: ${buffer.toString('hex')}`); return fail(`fail to parse request header: ${buffer.toString('hex')}`);
} }
const {uuid, userHashCache} = V2rayVmessPreset; const uuid = this._uuid;
const {userHashCache} = this.getStore();
// verify auth info // verify auth info
const authInfo = buffer.slice(0, 16); const authInfo = buffer.slice(0, 16);
@ -409,7 +407,7 @@ export default class V2rayVmessPreset extends IPresetAddressing {
return fail('fail to verify request command'); return fail('fail to verify request command');
} }
const data = buffer.slice(16 + plainReqHeader.length + 4); const data = buffer.slice(16 + plainReqHeader.length + 4);
V2rayVmessPreset.security = securityType; this._security = securityType;
this._isBroadCasting = true; this._isBroadCasting = true;
this.broadcast({ this.broadcast({
@ -448,7 +446,9 @@ export default class V2rayVmessPreset extends IPresetAddressing {
this._v = rands[32]; this._v = rands[32];
this._opt = 0x05; this._opt = 0x05;
const {userHashCache, uuid} = V2rayVmessPreset; const uuid = this._uuid;
const {userHashCache} = this.getStore();
const {timestamp, authInfo} = userHashCache[getRandomInt(0, userHashCache.length - 1)]; const {timestamp, authInfo} = userHashCache[getRandomInt(0, userHashCache.length - 1)];
// utc timestamp: Big-Endian, 8 bytes // utc timestamp: Big-Endian, 8 bytes
@ -461,7 +461,7 @@ export default class V2rayVmessPreset extends IPresetAddressing {
let command = Buffer.from([ let command = Buffer.from([
0x01, // Ver 0x01, // Ver
...this._dataEncIV, ...this._dataEncKey, this._v, this._opt, ...this._dataEncIV, ...this._dataEncKey, this._v, this._opt,
paddingLen << 4 | V2rayVmessPreset.security, paddingLen << 4 | this._security,
0x00, // RSV 0x00, // RSV
0x01, // Cmd 0x01, // Cmd
...this._port, this._atyp, ...this._port, this._atyp,
@ -489,7 +489,7 @@ export default class V2rayVmessPreset extends IPresetAddressing {
getBufferChunks(buffer) { getBufferChunks(buffer) {
return getChunks(buffer, 0x3fff).map((chunk) => { return getChunks(buffer, 0x3fff).map((chunk) => {
let _chunk = chunk; let _chunk = chunk;
if ([SECURITY_TYPE_AES_128_GCM, SECURITY_TYPE_CHACHA20_POLY1305].includes(V2rayVmessPreset.security)) { if ([SECURITY_TYPE_AES_128_GCM, SECURITY_TYPE_CHACHA20_POLY1305].includes(this._security)) {
_chunk = Buffer.concat(this.encrypt(_chunk)); _chunk = Buffer.concat(this.encrypt(_chunk));
} }
let _len = _chunk.length; let _len = _chunk.length;
@ -514,7 +514,7 @@ export default class V2rayVmessPreset extends IPresetAddressing {
} }
onChunkReceived(chunk, {next, fail}) { onChunkReceived(chunk, {next, fail}) {
if ([SECURITY_TYPE_AES_128_GCM, SECURITY_TYPE_CHACHA20_POLY1305].includes(V2rayVmessPreset.security)) { if ([SECURITY_TYPE_AES_128_GCM, SECURITY_TYPE_CHACHA20_POLY1305].includes(this._security)) {
const tag = chunk.slice(-16); const tag = chunk.slice(-16);
const data = this.decrypt(chunk.slice(2, -16), tag); const data = this.decrypt(chunk.slice(2, -16), tag);
if (data === null) { if (data === null) {
@ -526,7 +526,7 @@ export default class V2rayVmessPreset extends IPresetAddressing {
} }
encrypt(plaintext) { encrypt(plaintext) {
const {security} = V2rayVmessPreset; const security = this._security;
const nonce = Buffer.concat([ntb(this._cipherNonce), this._dataEncIV.slice(2, 12)]); const nonce = Buffer.concat([ntb(this._cipherNonce), this._dataEncIV.slice(2, 12)]);
let ciphertext = null; let ciphertext = null;
let tag = null; let tag = null;
@ -548,7 +548,7 @@ export default class V2rayVmessPreset extends IPresetAddressing {
} }
decrypt(ciphertext, tag) { decrypt(ciphertext, tag) {
const {security} = V2rayVmessPreset; const security = this._security;
const nonce = Buffer.concat([ntb(this._decipherNonce), this._dataDecIV.slice(2, 12)]); const nonce = Buffer.concat([ntb(this._decipherNonce), this._dataDecIV.slice(2, 12)]);
if (security === SECURITY_TYPE_AES_128_GCM) { if (security === SECURITY_TYPE_AES_128_GCM) {
const decipher = crypto.createDecipheriv('aes-128-gcm', this._dataDecKey, nonce); const decipher = crypto.createDecipheriv('aes-128-gcm', this._dataDecKey, nonce);

@ -5,12 +5,13 @@ import EventEmitter from 'events';
class Bound extends EventEmitter { class Bound extends EventEmitter {
_ctx = null; _ctx = null;
_config = null; _config = null;
constructor({context, config}) { constructor({config, context}) {
super(); super();
this._ctx = context;
this._config = config; this._config = config;
this._ctx = context;
} }
get ctx() { get ctx() {

@ -1,19 +1,15 @@
import EventEmitter from 'events'; import EventEmitter from 'events';
import {getPresetClassByName} from '../../src/presets';
import {PIPE_ENCODE, PIPE_DECODE} from '../../src/constants'; import {PIPE_ENCODE, PIPE_DECODE} from '../../src/constants';
import {Middleware} from '../../src/core/middleware'; import {Middleware} from '../../src/core/middleware';
export class PresetRunner extends EventEmitter { export class PresetRunner extends EventEmitter {
_config = null; _config = null;
constructor({name, params = {}}, config = {}) { constructor({name, params = {}}, config = {}) {
super(); super();
this._config = config; this._config = config;
const clazz = getPresetClassByName(name); this.middleware = new Middleware({config, preset: {name, params}});
clazz.config = config;
clazz.checkParams(params);
this.middleware = new Middleware({name, params}, config);
} }
notify(action) { notify(action) {
@ -29,8 +25,6 @@ export class PresetRunner extends EventEmitter {
data = Buffer.from(data); data = Buffer.from(data);
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.middleware.on('post_1', resolve);
this.middleware.on('post_-1', resolve);
this.middleware.on('fail', reject); this.middleware.on('fail', reject);
this.middleware.on('broadcast', (name, action) => this.emit('broadcast', action)); this.middleware.on('broadcast', (name, action) => this.emit('broadcast', action));
this.middleware.write({ this.middleware.write({
@ -47,8 +41,6 @@ export class PresetRunner extends EventEmitter {
data = Buffer.from(data); data = Buffer.from(data);
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.middleware.on('post_1', resolve);
this.middleware.on('post_-1', resolve);
this.middleware.on('fail', reject); this.middleware.on('fail', reject);
this.middleware.on('broadcast', (name, action) => this.emit('broadcast', action)); this.middleware.on('broadcast', (name, action) => this.emit('broadcast', action));
this.middleware.write({ this.middleware.write({

@ -1,41 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`running on both client and server 1`] = `
Object {
"data": Array [
49,
50,
],
"type": "Buffer",
}
`;
exports[`running on both client and server 2`] = `
Object {
"data": Array [
51,
52,
],
"type": "Buffer",
}
`;
exports[`running on both client and server 3`] = `
Object {
"data": Array [
53,
54,
],
"type": "Buffer",
}
`;
exports[`running on both client and server 4`] = `
Object {
"data": Array [
55,
56,
],
"type": "Buffer",
}
`;

@ -1,40 +0,0 @@
import path from 'path';
import {
PRESET_FAILED,
CONNECT_TO_REMOTE,
CONNECTION_CREATED,
CONNECTION_CLOSED
} from '../../src/presets';
import {PresetRunner, sleep} from '../common';
test('running on both client and server', async () => {
const runner = new PresetRunner({
name: 'access-control',
params: {
acl: path.join(__dirname, 'acl.txt')
}
}, {
is_client: true,
is_server: false
});
await sleep(20);
const actionPayload = {
payload: {
host: 'example.com',
port: 443
}
};
runner.notify({type: CONNECT_TO_REMOTE, ...actionPayload});
runner.notify({type: CONNECTION_CREATED, ...actionPayload});
expect(await runner.forward('12')).toMatchSnapshot();
expect(await runner.forward('34')).toMatchSnapshot();
expect(await runner.backward('56')).toMatchSnapshot();
expect(await runner.backward('78')).toMatchSnapshot();
runner.notify({type: PRESET_FAILED});
runner.notify({type: CONNECTION_CLOSED});
});

@ -1,4 +1,4 @@
import {IPreset, IPresetStatic, checkPresetClass} from '../defs'; import {IPreset, checkPresetClass} from '../../src/presets/defs';
test('IPreset#onNotified', () => { test('IPreset#onNotified', () => {
const preset = new IPreset(); const preset = new IPreset();
@ -10,11 +10,6 @@ test('IPreset#onDestroy', () => {
expect(preset.onDestroy()).toBe(undefined); expect(preset.onDestroy()).toBe(undefined);
}); });
test('IPresetStatic#constructor', () => {
expect(() => new IPresetStatic()).not.toThrow();
expect(() => new IPresetStatic()).toThrow();
});
test('should return false if class is not a function', () => { test('should return false if class is not a function', () => {
expect(checkPresetClass(null)).toBe(false); expect(checkPresetClass(null)).toBe(false);
}); });

@ -1,4 +1,4 @@
import {getPresetClassByName} from '../index'; import {getPresetClassByName} from '../../src/presets';
test('should return a preset class', () => { test('should return a preset class', () => {
expect(getPresetClassByName('ss-base')).toBeDefined(); expect(getPresetClassByName('ss-base')).toBeDefined();

@ -1,51 +0,0 @@
import {CONNECT_TO_REMOTE} from '../../src/presets';
import {PresetRunner} from '../common';
test('running on client', async () => {
const runner = new PresetRunner({
name: 'v2ray-vmess',
params: {
id: 'a3482e88-686a-4a58-8126-99c9df64b7bf',
security: 'aes-128-gcm'
}
}, {
is_client: true,
is_server: false
});
runner.notify({
type: CONNECT_TO_REMOTE,
payload: {
host: 'example.com',
port: 443
}
});
const packet_1 = await runner.forward('12');
const packet_2 = await runner.forward('34');
expect(packet_1.length).toBeGreaterThanOrEqual(50);
expect(packet_2.length).toBeGreaterThanOrEqual(20);
// fail on wrong data
await expect(runner.backward(Buffer.alloc(35))).rejects.toBeDefined();
runner.destroy();
});
test('running on server', async () => {
const runner = new PresetRunner({
name: 'v2ray-vmess',
params: {
id: 'a3482e88-686a-4a58-8126-99c9df64b7bf'
}
}, {
is_client: false,
is_server: true
});
// fail on wrong data
await expect(runner.backward(Buffer.alloc(35))).rejects.toBeDefined();
runner.destroy();
});