Skip to content

Commit

Permalink
Do the transformation at config writing time.
Browse files Browse the repository at this point in the history
  • Loading branch information
sbruens committed Jan 7, 2025
1 parent 8384489 commit 395260b
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 73 deletions.
29 changes: 6 additions & 23 deletions src/shadowbox/model/shadowsocks_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
}

/** 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<void>;
}
14 changes: 6 additions & 8 deletions src/shadowbox/server/mocks/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
}

Expand Down
64 changes: 47 additions & 17 deletions src/shadowbox/server/outline_shadowsocks_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<void> {
return this.writeConfigFile(config).then(() => {
update(keys: ShadowsocksAccessKey[]): Promise<void> {
return this.writeConfigFile(keys).then(() => {
if (!this.ssProcess) {
this.start();
return Promise.resolve();
Expand All @@ -78,24 +93,39 @@ export class OutlineShadowsocksServer implements ShadowsocksServer {
});
}

private writeConfigFile(config: ShadowsocksConfig): Promise<void> {
private writeConfigFile(keys: ShadowsocksAccessKey[]): Promise<void> {
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<number, ShadowsocksAccessKey[]> = {};
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();
Expand Down
33 changes: 8 additions & 25 deletions src/shadowbox/server/server_access_key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -322,34 +322,17 @@ export class ServerAccessKeyRepository implements AccessKeyRepository {
}

private updateServer(): Promise<void> {
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<number, typeof this.accessKeys>);

// 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[] {
Expand Down

0 comments on commit 395260b

Please sign in to comment.