From 4052d1880a2f763fe2cb9f6011a45e88bcbc5f21 Mon Sep 17 00:00:00 2001 From: sbruens Date: Fri, 20 Dec 2024 17:01:50 -0500 Subject: [PATCH 1/3] refactor(server): use the new service config format --- src/shadowbox/model/shadowsocks_server.ts | 29 +++++++++++---- src/shadowbox/server/mocks/mocks.ts | 14 ++++---- .../server/outline_shadowsocks_server.ts | 33 ++++++++--------- src/shadowbox/server/server_access_key.ts | 35 ++++++++++++++----- 4 files changed, 74 insertions(+), 37 deletions(-) diff --git a/src/shadowbox/model/shadowsocks_server.ts b/src/shadowbox/model/shadowsocks_server.ts index a3416ef95..60f1a3be9 100644 --- a/src/shadowbox/model/shadowsocks_server.ts +++ b/src/shadowbox/model/shadowsocks_server.ts @@ -13,14 +13,31 @@ // limitations under the License. // Parameters required to identify and authenticate connections to a Shadowsocks server. +export interface ShadowsocksServer { + // Updates the server to accept only the given service configs. + update(config: ShadowsocksConfig): Promise; +} + +/** Represents the overall Shadowsocks configuration with multiple services. */ +export interface ShadowsocksConfig { + services: ShadowsocksService[]; +} + +/* Represents a Shadowsocks service with its listeners and keys. */ +export interface ShadowsocksService { + listeners: ShadowsocksListener[]; + keys: ShadowsocksAccessKey[]; +} + +/* Represents a single listener for a Shadowsocks service. */ +export interface ShadowsocksListener { + type: string; + address: string; +} + +/* Represents an access key for a Shadowsocks service. */ export interface ShadowsocksAccessKey { id: string; - port: number; cipher: string; secret: string; } - -export interface ShadowsocksServer { - // Updates the server to accept only the given access keys. - update(keys: ShadowsocksAccessKey[]): Promise; -} diff --git a/src/shadowbox/server/mocks/mocks.ts b/src/shadowbox/server/mocks/mocks.ts index a99cd5a72..e07ecaea6 100644 --- a/src/shadowbox/server/mocks/mocks.ts +++ b/src/shadowbox/server/mocks/mocks.ts @@ -13,7 +13,7 @@ // limitations under the License. import {PrometheusClient, QueryResultData} from '../../infrastructure/prometheus_scraper'; -import {ShadowsocksAccessKey, ShadowsocksServer} from '../../model/shadowsocks_server'; +import {ShadowsocksAccessKey, ShadowsocksConfig, ShadowsocksServer} from '../../model/shadowsocks_server'; import {TextFile} from '../../infrastructure/text_file'; export class InMemoryFile implements TextFile { @@ -36,15 +36,17 @@ export class InMemoryFile implements TextFile { } export class FakeShadowsocksServer implements ShadowsocksServer { - private accessKeys: ShadowsocksAccessKey[] = []; + private config: ShadowsocksConfig = {services: []}; - update(keys: ShadowsocksAccessKey[]) { - this.accessKeys = keys; + update(config: ShadowsocksConfig) { + this.config = config; return Promise.resolve(); } - getAccessKeys() { - return this.accessKeys; + getAccessKeys(): ShadowsocksAccessKey[] { + return this.config.services.reduce((acc, service) => { + return acc.concat(service.keys); + }, [] as ShadowsocksAccessKey[]); } } diff --git a/src/shadowbox/server/outline_shadowsocks_server.ts b/src/shadowbox/server/outline_shadowsocks_server.ts index e7c403701..1297935e3 100644 --- a/src/shadowbox/server/outline_shadowsocks_server.ts +++ b/src/shadowbox/server/outline_shadowsocks_server.ts @@ -19,14 +19,13 @@ import * as path from 'path'; import * as file from '../infrastructure/file'; import * as logging from '../infrastructure/logging'; -import {ShadowsocksAccessKey, ShadowsocksServer} from '../model/shadowsocks_server'; +import {ShadowsocksAccessKey, ShadowsocksServer, ShadowsocksConfig} from '../model/shadowsocks_server'; // Runs outline-ss-server. export class OutlineShadowsocksServer implements ShadowsocksServer { private ssProcess: child_process.ChildProcess; private ipCountryFilename?: string; private ipAsnFilename?: string; - private isAsnMetricsEnabled = false; private isReplayProtectionEnabled = false; /** @@ -66,10 +65,10 @@ export class OutlineShadowsocksServer implements ShadowsocksServer { } // Promise is resolved after the outline-ss-config config is updated and the SIGHUP sent. - // Keys may not be active yet. + // Listeners and keys may not be active yet. // TODO(fortuna): Make promise resolve when keys are ready. - update(keys: ShadowsocksAccessKey[]): Promise { - return this.writeConfigFile(keys).then(() => { + update(config: ShadowsocksConfig): Promise { + return this.writeConfigFile(config).then(() => { if (!this.ssProcess) { this.start(); return Promise.resolve(); @@ -79,24 +78,26 @@ export class OutlineShadowsocksServer implements ShadowsocksServer { }); } - private writeConfigFile(keys: ShadowsocksAccessKey[]): Promise { + private writeConfigFile(config: ShadowsocksConfig): Promise { return new Promise((resolve, reject) => { - const keysJson = {keys: [] as ShadowsocksAccessKey[]}; - for (const key of keys) { - if (!isAeadCipher(key.cipher)) { - logging.error( - `Cipher ${key.cipher} for access key ${key.id} is not supported: use an AEAD cipher instead.` - ); - continue; + for (const service of config.services) { + const filteredKeys: ShadowsocksAccessKey[] = []; + for (const key of service.keys) { + if (!isAeadCipher(key.cipher)) { + logging.error( + `Cipher ${key.cipher} for access key ${key.id} is not supported: use an AEAD cipher instead.` + ); + continue; + } + filteredKeys.push(key); } - - keysJson.keys.push(key); + service.keys = filteredKeys; } mkdirp.sync(path.dirname(this.configFilename)); try { - file.atomicWriteFileSync(this.configFilename, jsyaml.safeDump(keysJson, {sortKeys: true})); + file.atomicWriteFileSync(this.configFilename, jsyaml.safeDump(config, {sortKeys: true})); resolve(); } catch (error) { reject(error); diff --git a/src/shadowbox/server/server_access_key.ts b/src/shadowbox/server/server_access_key.ts index 2c5224981..93e1927f5 100644 --- a/src/shadowbox/server/server_access_key.ts +++ b/src/shadowbox/server/server_access_key.ts @@ -28,7 +28,7 @@ import { ProxyParams, } from '../model/access_key'; import * as errors from '../model/errors'; -import {ShadowsocksServer} from '../model/shadowsocks_server'; +import {ShadowsocksConfig, ShadowsocksServer, ShadowsocksService} from '../model/shadowsocks_server'; import {PrometheusManagerMetrics} from './manager_metrics'; // The format as json of access keys in the config file. @@ -322,17 +322,34 @@ export class ServerAccessKeyRepository implements AccessKeyRepository { } private updateServer(): Promise { - const serverAccessKeys = this.accessKeys - .filter((key) => !key.reachedDataLimit) - .map((key) => { - return { + const config: ShadowsocksConfig = {services: []} + + // Group access keys by port. + const keysByPort = this.accessKeys.reduce((acc, key) => { + if (!key.reachedDataLimit) { + const port = key.proxyParams.portNumber; + (acc[port] ??= []).push(key); + } + return acc; + }, {} as Record); + + // Create services for each port. + for (const port in keysByPort) { + const service: ShadowsocksService = { + listeners: [ + { type: 'tcp', address: `[::]:${port}` }, + { type: 'udp', address: `[::]:${port}` }, + ], + keys: keysByPort[port].map((key) => ({ id: key.id, - port: key.proxyParams.portNumber, cipher: key.proxyParams.encryptionMethod, secret: key.proxyParams.password, - }; - }); - return this.shadowsocksServer.update(serverAccessKeys); + })), + }; + config.services.push(service); + } + + return this.shadowsocksServer.update(config); } private loadAccessKeys(): AccessKey[] { From 83844894cca253377e8c35a074bc25495478e07a Mon Sep 17 00:00:00 2001 From: sbruens Date: Fri, 20 Dec 2024 17:08:16 -0500 Subject: [PATCH 2/3] Keep the original filter to filter out inactive keys first. --- src/shadowbox/server/server_access_key.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/shadowbox/server/server_access_key.ts b/src/shadowbox/server/server_access_key.ts index 93e1927f5..8063e6dd3 100644 --- a/src/shadowbox/server/server_access_key.ts +++ b/src/shadowbox/server/server_access_key.ts @@ -325,13 +325,13 @@ export class ServerAccessKeyRepository implements AccessKeyRepository { const config: ShadowsocksConfig = {services: []} // Group access keys by port. - const keysByPort = this.accessKeys.reduce((acc, key) => { - if (!key.reachedDataLimit) { + const keysByPort = this.accessKeys + .filter((key) => !key.reachedDataLimit) + .reduce((acc, key) => { const port = key.proxyParams.portNumber; (acc[port] ??= []).push(key); - } - return acc; - }, {} as Record); + return acc; + }, {} as Record); // Create services for each port. for (const port in keysByPort) { From 395260b1cbb6ab6e0a6f982f91336d05531f3594 Mon Sep 17 00:00:00 2001 From: sbruens Date: Tue, 7 Jan 2025 12:10:29 -0500 Subject: [PATCH 3/3] Do the transformation at config writing time. --- src/shadowbox/model/shadowsocks_server.ts | 29 ++------- src/shadowbox/server/mocks/mocks.ts | 14 ++-- .../server/outline_shadowsocks_server.ts | 64 ++++++++++++++----- src/shadowbox/server/server_access_key.ts | 33 +++------- 4 files changed, 67 insertions(+), 73 deletions(-) diff --git a/src/shadowbox/model/shadowsocks_server.ts b/src/shadowbox/model/shadowsocks_server.ts index 60f1a3be9..a3416ef95 100644 --- a/src/shadowbox/model/shadowsocks_server.ts +++ b/src/shadowbox/model/shadowsocks_server.ts @@ -13,31 +13,14 @@ // limitations under the License. // Parameters required to identify and authenticate connections to a Shadowsocks server. -export interface ShadowsocksServer { - // Updates the server to accept only the given service configs. - update(config: ShadowsocksConfig): Promise; -} - -/** Represents the overall Shadowsocks configuration with multiple services. */ -export interface ShadowsocksConfig { - services: ShadowsocksService[]; -} - -/* Represents a Shadowsocks service with its listeners and keys. */ -export interface ShadowsocksService { - listeners: ShadowsocksListener[]; - keys: ShadowsocksAccessKey[]; -} - -/* Represents a single listener for a Shadowsocks service. */ -export interface ShadowsocksListener { - type: string; - address: string; -} - -/* Represents an access key for a Shadowsocks service. */ export interface ShadowsocksAccessKey { id: string; + port: number; cipher: string; secret: string; } + +export interface ShadowsocksServer { + // Updates the server to accept only the given access keys. + update(keys: ShadowsocksAccessKey[]): Promise; +} diff --git a/src/shadowbox/server/mocks/mocks.ts b/src/shadowbox/server/mocks/mocks.ts index e07ecaea6..a99cd5a72 100644 --- a/src/shadowbox/server/mocks/mocks.ts +++ b/src/shadowbox/server/mocks/mocks.ts @@ -13,7 +13,7 @@ // limitations under the License. import {PrometheusClient, QueryResultData} from '../../infrastructure/prometheus_scraper'; -import {ShadowsocksAccessKey, ShadowsocksConfig, ShadowsocksServer} from '../../model/shadowsocks_server'; +import {ShadowsocksAccessKey, ShadowsocksServer} from '../../model/shadowsocks_server'; import {TextFile} from '../../infrastructure/text_file'; export class InMemoryFile implements TextFile { @@ -36,17 +36,15 @@ export class InMemoryFile implements TextFile { } export class FakeShadowsocksServer implements ShadowsocksServer { - private config: ShadowsocksConfig = {services: []}; + private accessKeys: ShadowsocksAccessKey[] = []; - update(config: ShadowsocksConfig) { - this.config = config; + update(keys: ShadowsocksAccessKey[]) { + this.accessKeys = keys; return Promise.resolve(); } - getAccessKeys(): ShadowsocksAccessKey[] { - return this.config.services.reduce((acc, service) => { - return acc.concat(service.keys); - }, [] as ShadowsocksAccessKey[]); + getAccessKeys() { + return this.accessKeys; } } diff --git a/src/shadowbox/server/outline_shadowsocks_server.ts b/src/shadowbox/server/outline_shadowsocks_server.ts index 1297935e3..1abe1b2c0 100644 --- a/src/shadowbox/server/outline_shadowsocks_server.ts +++ b/src/shadowbox/server/outline_shadowsocks_server.ts @@ -19,7 +19,22 @@ import * as path from 'path'; import * as file from '../infrastructure/file'; import * as logging from '../infrastructure/logging'; -import {ShadowsocksAccessKey, ShadowsocksServer, ShadowsocksConfig} from '../model/shadowsocks_server'; +import {ShadowsocksAccessKey, ShadowsocksServer} from '../model/shadowsocks_server'; + +/** Represents an outline-ss-server configuration with multiple services. */ +export interface OutlineSSServerConfig { + services: { + listeners: { + type: string; + address: string; + }[]; + keys: { + id: string; + cipher: string; + secret: string; + }[]; + }[]; +} // Runs outline-ss-server. export class OutlineShadowsocksServer implements ShadowsocksServer { @@ -65,10 +80,10 @@ export class OutlineShadowsocksServer implements ShadowsocksServer { } // Promise is resolved after the outline-ss-config config is updated and the SIGHUP sent. - // Listeners and keys may not be active yet. + // Keys may not be active yet. // TODO(fortuna): Make promise resolve when keys are ready. - update(config: ShadowsocksConfig): Promise { - return this.writeConfigFile(config).then(() => { + update(keys: ShadowsocksAccessKey[]): Promise { + return this.writeConfigFile(keys).then(() => { if (!this.ssProcess) { this.start(); return Promise.resolve(); @@ -78,24 +93,39 @@ export class OutlineShadowsocksServer implements ShadowsocksServer { }); } - private writeConfigFile(config: ShadowsocksConfig): Promise { + private writeConfigFile(keys: ShadowsocksAccessKey[]): Promise { return new Promise((resolve, reject) => { - for (const service of config.services) { - const filteredKeys: ShadowsocksAccessKey[] = []; - for (const key of service.keys) { - if (!isAeadCipher(key.cipher)) { - logging.error( - `Cipher ${key.cipher} for access key ${key.id} is not supported: use an AEAD cipher instead.` - ); - continue; - } - filteredKeys.push(key); + const validKeys: ShadowsocksAccessKey[] = keys.filter((key) => { + if (!isAeadCipher(key.cipher)) { + logging.error( + `Cipher ${key.cipher} for access key ${key.id} is not supported: use an AEAD cipher instead.` + ); + return false; } - service.keys = filteredKeys; + return true; + }); + + const config: OutlineSSServerConfig = {services: []}; + const keysByPort: Record = {}; + for (const key of validKeys) { + (keysByPort[key.port] ??= []).push(key); + } + for (const port in keysByPort) { + const service = { + listeners: [ + {type: 'tcp', address: `[::]:${port}`}, + {type: 'udp', address: `[::]:${port}`}, + ], + keys: keysByPort[port].map((key) => ({ + id: key.id, + cipher: key.cipher, + secret: key.secret, + })), + }; + config.services.push(service); } mkdirp.sync(path.dirname(this.configFilename)); - try { file.atomicWriteFileSync(this.configFilename, jsyaml.safeDump(config, {sortKeys: true})); resolve(); diff --git a/src/shadowbox/server/server_access_key.ts b/src/shadowbox/server/server_access_key.ts index 8063e6dd3..2c5224981 100644 --- a/src/shadowbox/server/server_access_key.ts +++ b/src/shadowbox/server/server_access_key.ts @@ -28,7 +28,7 @@ import { ProxyParams, } from '../model/access_key'; import * as errors from '../model/errors'; -import {ShadowsocksConfig, ShadowsocksServer, ShadowsocksService} from '../model/shadowsocks_server'; +import {ShadowsocksServer} from '../model/shadowsocks_server'; import {PrometheusManagerMetrics} from './manager_metrics'; // The format as json of access keys in the config file. @@ -322,34 +322,17 @@ export class ServerAccessKeyRepository implements AccessKeyRepository { } private updateServer(): Promise { - const config: ShadowsocksConfig = {services: []} - - // Group access keys by port. - const keysByPort = this.accessKeys + const serverAccessKeys = this.accessKeys .filter((key) => !key.reachedDataLimit) - .reduce((acc, key) => { - const port = key.proxyParams.portNumber; - (acc[port] ??= []).push(key); - return acc; - }, {} as Record); - - // Create services for each port. - for (const port in keysByPort) { - const service: ShadowsocksService = { - listeners: [ - { type: 'tcp', address: `[::]:${port}` }, - { type: 'udp', address: `[::]:${port}` }, - ], - keys: keysByPort[port].map((key) => ({ + .map((key) => { + return { id: key.id, + port: key.proxyParams.portNumber, cipher: key.proxyParams.encryptionMethod, secret: key.proxyParams.password, - })), - }; - config.services.push(service); - } - - return this.shadowsocksServer.update(config); + }; + }); + return this.shadowsocksServer.update(serverAccessKeys); } private loadAccessKeys(): AccessKey[] {