From 30b7618b1277a7cbb189e1863b6d89ce2a52b1b7 Mon Sep 17 00:00:00 2001 From: sbruens Date: Tue, 19 Mar 2024 18:12:35 -0400 Subject: [PATCH 1/8] Update `update_mmdb.sh` script to download ASN database. --- src/shadowbox/scripts/update_mmdb.sh | 58 +++++++++++++++++++++------- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/src/shadowbox/scripts/update_mmdb.sh b/src/shadowbox/scripts/update_mmdb.sh index b283321cb..51327a552 100755 --- a/src/shadowbox/scripts/update_mmdb.sh +++ b/src/shadowbox/scripts/update_mmdb.sh @@ -1,6 +1,20 @@ #!/bin/sh +# +# Copyright 2024 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. -# Download the IP-to-country MMDB database into the same location +# Download the IP-to-country and IP-to-ASN MMDB databases into the same location # used by Alpine's libmaxminddb package. # IP Geolocation by DB-IP (https://db-ip.com) @@ -9,21 +23,35 @@ TMPDIR="$(mktemp -d)" readonly TMPDIR -readonly FILENAME="ip-country.mmdb" - -# We need to make sure that we grab an existing database at install-time -for monthdelta in $(seq 10); do - newdate="$(date --date="-${monthdelta} months" +%Y-%m)" - ADDRESS="https://download.db-ip.com/free/dbip-country-lite-${newdate}.mmdb.gz" - curl --fail --silent "${ADDRESS}" -o "${TMPDIR}/${FILENAME}.gz" > /dev/null && break - if [ "${monthdelta}" -eq '10' ]; then +readonly LIBDIR="/var/lib/libmaxminddb" + +# Downloads a given MMDB database and writes it to the temporary directory. +# @param {string} The database to download. +download_ip_mmdb() { + db=$1 + + for monthdelta in $(seq 0 9); do + newdate="$(date --date="-${monthdelta} months" +%Y-%m)" + address="https://download.db-ip.com/free/db${db}-lite-${newdate}.mmdb.gz" + curl --fail --silent "${address}" -o "${TMPDIR}/${db}.mmdb.gz" > /dev/null && return 0 + done + return 1 +} + +main() { + # We need to make sure that we grab an existing database at install-time + if ! { download_ip_mmdb "ip-country" && download_ip_mmdb "ip-asn"; }; then # A weird exit code on purpose -- we should catch this long before it triggers exit 2 fi -done -gunzip "${TMPDIR}/${FILENAME}.gz" -readonly LIBDIR="/var/lib/libmaxminddb" -mkdir -p "${LIBDIR}" -mv -f "${TMPDIR}/${FILENAME}" "${LIBDIR}" -rmdir "${TMPDIR}" + for filename in "${TMPDIR}"/*; do + gunzip "${filename}" + done + + mkdir -p "${LIBDIR}" + mv -f "${TMPDIR}"/* "${LIBDIR}" + rmdir "${TMPDIR}" +} + +main "$@" From c34f704d1a4f0344d2ecb387deb61b9dad5d587d Mon Sep 17 00:00:00 2001 From: sbruens Date: Tue, 19 Mar 2024 17:46:27 -0400 Subject: [PATCH 2/8] Provide the `ASN` db file to `outline-ss-server`. --- src/shadowbox/server/main.ts | 10 +++++++--- src/shadowbox/server/outline_shadowsocks_server.ts | 13 ++++++++++++- third_party/outline-ss-server/Makefile | 10 +++++----- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/shadowbox/server/main.ts b/src/shadowbox/server/main.ts index ba938eb7a..75a3c3217 100644 --- a/src/shadowbox/server/main.ts +++ b/src/shadowbox/server/main.ts @@ -43,7 +43,8 @@ import { const APP_BASE_DIR = path.join(__dirname, '..'); const DEFAULT_STATE_DIR = '/root/shadowbox/persisted-state'; -const MMDB_LOCATION = '/var/lib/libmaxminddb/ip-country.mmdb'; +const MMDB_LOCATION_COUNTRY = '/var/lib/libmaxminddb/ip-country.mmdb'; +const MMDB_LOCATION_ASN = '/var/lib/libmaxminddb/ip-asn.mmdb'; async function exportPrometheusMetrics(registry: prometheus.Registry, port): Promise { return new Promise((resolve, _) => { @@ -155,8 +156,11 @@ async function main() { verbose, ssMetricsLocation ); - if (fs.existsSync(MMDB_LOCATION)) { - shadowsocksServer.enableCountryMetrics(MMDB_LOCATION); + if (fs.existsSync(MMDB_LOCATION_COUNTRY)) { + shadowsocksServer.enableCountryMetrics(MMDB_LOCATION_COUNTRY); + } + if (fs.existsSync(MMDB_LOCATION_ASN)) { + shadowsocksServer.enableASNMetrics(MMDB_LOCATION_ASN); } const isReplayProtectionEnabled = createRolloutTracker(serverConfig).isRolloutEnabled( diff --git a/src/shadowbox/server/outline_shadowsocks_server.ts b/src/shadowbox/server/outline_shadowsocks_server.ts index da27c874f..a79d9f7e0 100644 --- a/src/shadowbox/server/outline_shadowsocks_server.ts +++ b/src/shadowbox/server/outline_shadowsocks_server.ts @@ -25,6 +25,7 @@ import {ShadowsocksAccessKey, ShadowsocksServer} from '../model/shadowsocks_serv export class OutlineShadowsocksServer implements ShadowsocksServer { private ssProcess: child_process.ChildProcess; private ipCountryFilename = ''; + private ipASNFilename = ''; private isReplayProtectionEnabled = false; // binaryFilename is the location for the outline-ss-server binary. @@ -43,6 +44,13 @@ export class OutlineShadowsocksServer implements ShadowsocksServer { return this; } + // Annotates the Prometheus data metrics with ASN. + // ipASNFilename is the location of the ip-asn.mmdb IP-to-ASN database file. + enableASNMetrics(ipASNFilename: string): OutlineShadowsocksServer { + this.ipASNFilename = ipASNFilename; + return this; + } + enableReplayProtection(): OutlineShadowsocksServer { this.isReplayProtectionEnabled = true; return this; @@ -92,6 +100,9 @@ export class OutlineShadowsocksServer implements ShadowsocksServer { if (this.ipCountryFilename) { commandArguments.push('-ip_country_db', this.ipCountryFilename); } + if (this.ipASNFilename) { + commandArguments.push('-ip_asn_db', this.ipASNFilename); + } if (this.verbose) { commandArguments.push('-verbose'); } @@ -99,7 +110,7 @@ export class OutlineShadowsocksServer implements ShadowsocksServer { commandArguments.push('--replay_history=10000'); } logging.info('======== Starting Outline Shadowsocks Service ========'); - logging.info(`${this.binaryFilename} ${commandArguments.map(a => `"${a}"`).join(' ')}`); + logging.info(`${this.binaryFilename} ${commandArguments.map((a) => `"${a}"`).join(' ')}`); this.ssProcess = child_process.spawn(this.binaryFilename, commandArguments); this.ssProcess.on('error', (error) => { logging.error(`Error spawning outline-ss-server: ${error}`); diff --git a/third_party/outline-ss-server/Makefile b/third_party/outline-ss-server/Makefile index c780a577a..2d8aa3966 100644 --- a/third_party/outline-ss-server/Makefile +++ b/third_party/outline-ss-server/Makefile @@ -1,19 +1,19 @@ -VERSION=1.4.0 +VERSION=1.5.0-beta.2 .PHONY: all all: bin/linux-x86_64/outline-ss-server bin/linux-arm64/outline-ss-server bin/macos-x86_64/outline-ss-server bin/macos-arm64/outline-ss-server bin/linux-x86_64/outline-ss-server: OS=linux -bin/linux-x86_64/outline-ss-server: SHA256=f51bcb6391cca0ae828620c429e698a3b7c409de2374c52f113ca9a525e021a8 +bin/linux-x86_64/outline-ss-server: SHA256=4a720d8febb3d9cadfc175f31028ff2f22a2001ebeeeee269ec9ce302aff6d61 bin/linux-arm64/outline-ss-server: OS=linux -bin/linux-arm64/outline-ss-server: SHA256=14ae581414c9aab04253a385ef1854c003d09f545f6f8a3a55aa987f0c6d3859 +bin/linux-arm64/outline-ss-server: SHA256=769fe32f43e45d990cd65374ed73db00fc8fff6bc356e1b4c62074900e54ddc6 bin/macos-x86_64/outline-ss-server: OS=macos -bin/macos-x86_64/outline-ss-server: SHA256=c85b2e8ae2d48482cbc101e54dcb7eed074a22c14a3a7301993e5f786b34081d +bin/macos-x86_64/outline-ss-server: SHA256=9c8f89394c7f0bc7043c456501655443c57364406ba09f69f74bf913980ebec5 bin/macos-arm64/outline-ss-server: OS=macos -bin/macos-arm64/outline-ss-server: SHA256=9647712a7c732184f98b1e2e7f74281855afed2245ec922c4a24b54f0eb0ce72 +bin/macos-arm64/outline-ss-server: SHA256=8f77803e3dc8c95c2120545b64bc3060ba45bdfa3c09fc5b2536baa9a0840461 TEMPFILE := $(shell mktemp) bin/%/outline-ss-server: From 80a43f81a348ca944bee40f1ac0b298e8679c2b1 Mon Sep 17 00:00:00 2001 From: sbruens Date: Thu, 21 Mar 2024 18:18:05 -0400 Subject: [PATCH 3/8] Add an API to opt-in to the ASN metrics. --- src/shadowbox/README.md | 4 +- src/shadowbox/model/shadowsocks_server.ts | 4 ++ src/shadowbox/server/api.yml | 27 ++++++++ src/shadowbox/server/main.ts | 12 ++-- src/shadowbox/server/manager_service.spec.ts | 28 ++++++++ src/shadowbox/server/manager_service.ts | 39 ++++++++++- src/shadowbox/server/mocks/mocks.ts | 3 + .../server/outline_shadowsocks_server.ts | 66 +++++++++++++------ 8 files changed, 154 insertions(+), 29 deletions(-) diff --git a/src/shadowbox/README.md b/src/shadowbox/README.md index b04bb4d7f..74c831fb7 100644 --- a/src/shadowbox/README.md +++ b/src/shadowbox/README.md @@ -105,9 +105,9 @@ The Outline Server provides a REST API for access key management. If you know th - **Remove an access key:** `curl --insecure -X DELETE $API_URL/access-keys/1` - - **Set a data limit for all access keys:** (e.g. limit outbound data transfer access keys to 1MB over 30 days) `curl --insecure -X PUT -H "Content-Type: application/json" -d '{"limit": {"bytes": 1000}}' $API_URL/experimental/access-key-data-limit` + - **Set a data limit for all access keys:** (e.g. limit outbound data transfer access keys to 1MB over 30 days) `curl --insecure -X PUT -H "Content-Type: application/json" -d '{"limit": {"bytes": 1000}}' $API_URL/server/access-key-data-limit` - - **Remove the access key data limit:** `curl --insecure -X DELETE $API_URL/experimental/access-key-data-limit` + - **Remove the access key data limit:** `curl --insecure -X DELETE $API_URL/server/access-key-data-limit` - **And more...** diff --git a/src/shadowbox/model/shadowsocks_server.ts b/src/shadowbox/model/shadowsocks_server.ts index a3416ef95..70424185c 100644 --- a/src/shadowbox/model/shadowsocks_server.ts +++ b/src/shadowbox/model/shadowsocks_server.ts @@ -21,6 +21,10 @@ export interface ShadowsocksAccessKey { } export interface ShadowsocksServer { + // Annotates the Prometheus data metrics with countries. + isCountryMetricsEnabled: boolean; + // Annotates the Prometheus data metrics with ASN. + isAsnMetricsEnabled: boolean; // Updates the server to accept only the given access keys. update(keys: ShadowsocksAccessKey[]): Promise; } diff --git a/src/shadowbox/server/api.yml b/src/shadowbox/server/api.yml index 13cb55607..c253fa4c4 100644 --- a/src/shadowbox/server/api.yml +++ b/src/shadowbox/server/api.yml @@ -8,6 +8,8 @@ tags: description: Server-level functions - name: Access Key description: Access key functions + - name: Experimental + description: Experimental functions. These are unstable and may disappear. Use with care. servers: - url: https://myserver/SecretPath description: Example URL. Change to your own server. @@ -434,6 +436,29 @@ paths: description: Setting successful '400': description: Invalid request + /experimental/asn-metrics/enabled: + put: + description: Annotates Prometheus data metrics with autonomous system numbers (ASN). + tags: + - Server + - Experimental + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + asnMetricsEnabled: + type: boolean + examples: + '0': + value: '{"asnMetricsEnabled": true}' + responses: + '204': + description: Setting successful + '400': + description: Invalid request /experimental/access-key-data-limit: put: deprecated: true @@ -441,6 +466,7 @@ paths: tags: - Access Key - Limit + - Experimental requestBody: required: true content: @@ -461,6 +487,7 @@ paths: tags: - Access Key - Limit + - Experimental responses: '204': description: Access key limit deleted successfully. diff --git a/src/shadowbox/server/main.ts b/src/shadowbox/server/main.ts index 75a3c3217..f5c6e14c7 100644 --- a/src/shadowbox/server/main.ts +++ b/src/shadowbox/server/main.ts @@ -154,14 +154,11 @@ async function main() { getBinaryFilename('outline-ss-server'), getPersistentFilename('outline-ss-server/config.yml'), verbose, - ssMetricsLocation + ssMetricsLocation, + fs.existsSync(MMDB_LOCATION_COUNTRY) ? MMDB_LOCATION_COUNTRY : undefined, + fs.existsSync(MMDB_LOCATION_ASN) ? MMDB_LOCATION_ASN : undefined ); - if (fs.existsSync(MMDB_LOCATION_COUNTRY)) { - shadowsocksServer.enableCountryMetrics(MMDB_LOCATION_COUNTRY); - } - if (fs.existsSync(MMDB_LOCATION_ASN)) { - shadowsocksServer.enableASNMetrics(MMDB_LOCATION_ASN); - } + shadowsocksServer.isCountryMetricsEnabled = true; const isReplayProtectionEnabled = createRolloutTracker(serverConfig).isRolloutEnabled( 'replay-protection', @@ -234,6 +231,7 @@ async function main() { process.env.SB_DEFAULT_SERVER_NAME || 'Outline Server', serverConfig, accessKeyRepository, + shadowsocksServer, managerMetrics, metricsPublisher ); diff --git a/src/shadowbox/server/manager_service.spec.ts b/src/shadowbox/server/manager_service.spec.ts index 41933030f..5b0788f61 100644 --- a/src/shadowbox/server/manager_service.spec.ts +++ b/src/shadowbox/server/manager_service.spec.ts @@ -24,6 +24,8 @@ import {FakePrometheusClient, FakeShadowsocksServer} from './mocks/mocks'; import {AccessKeyConfigJson, ServerAccessKeyRepository} from './server_access_key'; import {ServerConfigJson} from './server_config'; import {SharedMetricsPublisher} from './shared_metrics'; +import {OutlineShadowsocksServer} from './outline_shadowsocks_server'; +import {ShadowsocksServer} from '../model/shadowsocks_server'; interface ServerInfo { name: string; @@ -1067,6 +1069,25 @@ describe('ShadowsocksManagerService', () => { ); }); }); + describe('enableAsnMetrics', () => { + it('Enables ASN metrics on the Shadowsocks Server', (done) => { + const shadowsocksServer = new FakeShadowsocksServer(); + const service = new ShadowsocksManagerServiceBuilder() + .shadowsocksServer(shadowsocksServer) + .build(); + service.enableAsnMetrics( + {params: {asnMetricsEnabled: true}}, + { + send: (httpCode, _) => { + expect(httpCode).toEqual(204); + expect(shadowsocksServer.isAsnMetricsEnabled).toEqual(true); + responseProcessed = true; + }, + }, + done + ); + }); + }); }); describe('bindService', () => { @@ -1194,6 +1215,7 @@ class ShadowsocksManagerServiceBuilder { private defaultServerName_ = 'default name'; private serverConfig_: JsonConfig = null; private accessKeys_: AccessKeyRepository = null; + private shadowsocksServer_: ShadowsocksServer = null; private managerMetrics_: ManagerMetrics = null; private metricsPublisher_: SharedMetricsPublisher = null; @@ -1212,6 +1234,11 @@ class ShadowsocksManagerServiceBuilder { return this; } + shadowsocksServer(server: ShadowsocksServer) { + this.shadowsocksServer_ = server; + return this; + } + managerMetrics(metrics: ManagerMetrics): ShadowsocksManagerServiceBuilder { this.managerMetrics_ = metrics; return this; @@ -1227,6 +1254,7 @@ class ShadowsocksManagerServiceBuilder { this.defaultServerName_, this.serverConfig_, this.accessKeys_, + this.shadowsocksServer_, this.managerMetrics_, this.metricsPublisher_ ); diff --git a/src/shadowbox/server/manager_service.ts b/src/shadowbox/server/manager_service.ts index 1299b9b55..42d723495 100644 --- a/src/shadowbox/server/manager_service.ts +++ b/src/shadowbox/server/manager_service.ts @@ -27,6 +27,7 @@ import * as version from './version'; import {ManagerMetrics} from './manager_metrics'; import {ServerConfigJson} from './server_config'; import {SharedMetricsPublisher} from './shared_metrics'; +import {ShadowsocksServer} from '../model/shadowsocks_server'; interface AccessKeyJson { // The unique identifier of this access key. @@ -158,6 +159,13 @@ export function bindService( apiServer.get(`${apiPrefix}/metrics/enabled`, service.getShareMetrics.bind(service)); apiServer.put(`${apiPrefix}/metrics/enabled`, service.setShareMetrics.bind(service)); + // Experimental APIs. + + apiServer.put( + `${apiPrefix}/experimental/asn-metrics/enabled`, + service.enableAsnMetrics.bind(service) + ); + // Redirect former experimental APIs apiServer.put( `${apiPrefix}/experimental/access-key-data-limit`, @@ -240,6 +248,7 @@ export class ShadowsocksManagerService { private defaultServerName: string, private serverConfig: JsonConfig, private accessKeys: AccessKeyRepository, + private shadowsocksServer: ShadowsocksServer, private managerMetrics: ManagerMetrics, private metricsPublisher: SharedMetricsPublisher ) {} @@ -621,7 +630,7 @@ export class ShadowsocksManagerService { return next( new restifyErrors.InvalidArgumentError( {statusCode: 400}, - 'Parameter `hours` must be an integer' + 'Parameter `metricsEnabled` must be a boolean' ) ); } @@ -633,4 +642,32 @@ export class ShadowsocksManagerService { res.send(HttpSuccess.NO_CONTENT); next(); } + + public enableAsnMetrics(req: RequestType, res: ResponseType, next: restify.Next): void { + try { + logging.debug(`enableAsnMetrics request ${JSON.stringify(req.params)}`); + const asnMetricsEnabled = req.params.asnMetricsEnabled; + if (asnMetricsEnabled === undefined || asnMetricsEnabled === null) { + return next( + new restifyErrors.MissingParameterError( + {statusCode: 400}, + 'Parameter `asnMetricsEnabled` is missing' + ) + ); + } else if (typeof asnMetricsEnabled !== 'boolean') { + return next( + new restifyErrors.InvalidArgumentError( + {statusCode: 400}, + 'Parameter `asnMetricsEnabled` must be a boolean' + ) + ); + } + this.shadowsocksServer.isAsnMetricsEnabled = asnMetricsEnabled; + res.send(HttpSuccess.NO_CONTENT); + return next(); + } catch (error) { + logging.error(error); + return next(new restifyErrors.InternalServerError()); + } + } } diff --git a/src/shadowbox/server/mocks/mocks.ts b/src/shadowbox/server/mocks/mocks.ts index 248cd55b0..801a40e00 100644 --- a/src/shadowbox/server/mocks/mocks.ts +++ b/src/shadowbox/server/mocks/mocks.ts @@ -38,6 +38,9 @@ export class InMemoryFile implements TextFile { export class FakeShadowsocksServer implements ShadowsocksServer { private accessKeys: ShadowsocksAccessKey[] = []; + isCountryMetricsEnabled = false; + isAsnMetricsEnabled = true; + update(keys: ShadowsocksAccessKey[]) { this.accessKeys = keys; return Promise.resolve(); diff --git a/src/shadowbox/server/outline_shadowsocks_server.ts b/src/shadowbox/server/outline_shadowsocks_server.ts index a79d9f7e0..58463ecb1 100644 --- a/src/shadowbox/server/outline_shadowsocks_server.ts +++ b/src/shadowbox/server/outline_shadowsocks_server.ts @@ -24,31 +24,59 @@ import {ShadowsocksAccessKey, ShadowsocksServer} from '../model/shadowsocks_serv // Runs outline-ss-server. export class OutlineShadowsocksServer implements ShadowsocksServer { private ssProcess: child_process.ChildProcess; - private ipCountryFilename = ''; - private ipASNFilename = ''; + private isCountryMetricsEnabled_ = false; + private isAsnMetricsEnabled_ = false; private isReplayProtectionEnabled = false; - // binaryFilename is the location for the outline-ss-server binary. - // configFilename is the location for the outline-ss-server config. + /** + * @param binaryFilename The location for the outline-ss-server binary. + * @param configFilename The location for the outline-ss-server config. + * @param metricsLocation The location from where to serve the Prometheus data metrics. + * @param verbose Whether to run the server in verbose mode. + * @param ipCountryFilename The location of IP-to-country database file. + * @param ipAsnFilename The location of IP-to-ASN database file. + */ constructor( private readonly binaryFilename: string, private readonly configFilename: string, private readonly verbose: boolean, - private readonly metricsLocation: string + private readonly metricsLocation: string, + private readonly ipCountryFilename?: string, + private readonly ipAsnFilename?: string ) {} - // Annotates the Prometheus data metrics with countries. - // ipCountryFilename is the location of the ip-country.mmdb IP-to-country database file. - enableCountryMetrics(ipCountryFilename: string): OutlineShadowsocksServer { - this.ipCountryFilename = ipCountryFilename; - return this; + // Annotates the Prometheus data metrics with countries. This restarts the + // server if needed. + get isCountryMetricsEnabled(): boolean { + return this.isCountryMetricsEnabled_; } - // Annotates the Prometheus data metrics with ASN. - // ipASNFilename is the location of the ip-asn.mmdb IP-to-ASN database file. - enableASNMetrics(ipASNFilename: string): OutlineShadowsocksServer { - this.ipASNFilename = ipASNFilename; - return this; + set isCountryMetricsEnabled(enable: boolean) { + if (enable && !this.ipCountryFilename) { + throw new Error('Cannot enable country metrics: no country database filename set'); + } + const valueChanged = this.isAsnMetricsEnabled_ != enable; + this.isCountryMetricsEnabled_ = enable; + if (valueChanged && this.ssProcess) { + this.ssProcess.kill('SIGTERM'); + } + } + + // Annotates the Prometheus data metrics with autonomous system numbers (ASN). + // This restarts the server if needed. + get isAsnMetricsEnabled(): boolean { + return this.isAsnMetricsEnabled_; + } + + set isAsnMetricsEnabled(enable: boolean) { + if (enable && !this.ipAsnFilename) { + throw new Error('Cannot enable ASN metrics: no ASN database filename set'); + } + const valueChanged = this.isAsnMetricsEnabled_ != enable; + this.isAsnMetricsEnabled_ = enable; + if (valueChanged && this.ssProcess) { + this.ssProcess.kill('SIGTERM'); + } } enableReplayProtection(): OutlineShadowsocksServer { @@ -97,11 +125,11 @@ export class OutlineShadowsocksServer implements ShadowsocksServer { private start() { const commandArguments = ['-config', this.configFilename, '-metrics', this.metricsLocation]; - if (this.ipCountryFilename) { + if (this.isCountryMetricsEnabled_ && this.ipCountryFilename) { commandArguments.push('-ip_country_db', this.ipCountryFilename); } - if (this.ipASNFilename) { - commandArguments.push('-ip_asn_db', this.ipASNFilename); + if (this.isAsnMetricsEnabled_ && this.ipAsnFilename) { + commandArguments.push('-ip_asn_db', this.ipAsnFilename); } if (this.verbose) { commandArguments.push('-verbose'); @@ -117,7 +145,7 @@ export class OutlineShadowsocksServer implements ShadowsocksServer { }); this.ssProcess.on('exit', (code, signal) => { logging.info(`outline-ss-server has exited with error. Code: ${code}, Signal: ${signal}`); - logging.info(`Restarting`); + logging.info('Restarting'); this.start(); }); // This exposes the outline-ss-server output on the docker logs. From 88344e609c86f54414363f877c41af9a10d744cf Mon Sep 17 00:00:00 2001 From: sbruens Date: Thu, 21 Mar 2024 18:18:53 -0400 Subject: [PATCH 4/8] Remove unused import. --- src/shadowbox/server/manager_service.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/shadowbox/server/manager_service.spec.ts b/src/shadowbox/server/manager_service.spec.ts index 5b0788f61..3aa868c51 100644 --- a/src/shadowbox/server/manager_service.spec.ts +++ b/src/shadowbox/server/manager_service.spec.ts @@ -24,7 +24,6 @@ import {FakePrometheusClient, FakeShadowsocksServer} from './mocks/mocks'; import {AccessKeyConfigJson, ServerAccessKeyRepository} from './server_access_key'; import {ServerConfigJson} from './server_config'; import {SharedMetricsPublisher} from './shared_metrics'; -import {OutlineShadowsocksServer} from './outline_shadowsocks_server'; import {ShadowsocksServer} from '../model/shadowsocks_server'; interface ServerInfo { From 493618558cca16beaca5ea862fdb1139ca3d658a Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 25 Mar 2024 16:24:15 -0400 Subject: [PATCH 5/8] Add the ASN setting to the persisted server config. --- src/shadowbox/model/shadowsocks_server.ts | 5 +- src/shadowbox/server/main.ts | 14 +++-- src/shadowbox/server/manager_service.spec.ts | 24 +++++++- src/shadowbox/server/manager_service.ts | 8 ++- src/shadowbox/server/mocks/mocks.ts | 3 +- .../server/outline_shadowsocks_server.ts | 57 ++++++++----------- src/shadowbox/server/server_config.ts | 6 ++ 7 files changed, 74 insertions(+), 43 deletions(-) diff --git a/src/shadowbox/model/shadowsocks_server.ts b/src/shadowbox/model/shadowsocks_server.ts index 70424185c..a7ea3745e 100644 --- a/src/shadowbox/model/shadowsocks_server.ts +++ b/src/shadowbox/model/shadowsocks_server.ts @@ -21,10 +21,9 @@ export interface ShadowsocksAccessKey { } export interface ShadowsocksServer { - // Annotates the Prometheus data metrics with countries. - isCountryMetricsEnabled: boolean; // Annotates the Prometheus data metrics with ASN. - isAsnMetricsEnabled: boolean; + enableAsnMetrics(enable: boolean); + // Updates the server to accept only the given access keys. update(keys: ShadowsocksAccessKey[]): Promise; } diff --git a/src/shadowbox/server/main.ts b/src/shadowbox/server/main.ts index f5c6e14c7..fe8dbcb53 100644 --- a/src/shadowbox/server/main.ts +++ b/src/shadowbox/server/main.ts @@ -154,11 +154,17 @@ async function main() { getBinaryFilename('outline-ss-server'), getPersistentFilename('outline-ss-server/config.yml'), verbose, - ssMetricsLocation, - fs.existsSync(MMDB_LOCATION_COUNTRY) ? MMDB_LOCATION_COUNTRY : undefined, - fs.existsSync(MMDB_LOCATION_ASN) ? MMDB_LOCATION_ASN : undefined + ssMetricsLocation ); - shadowsocksServer.isCountryMetricsEnabled = true; + if (fs.existsSync(MMDB_LOCATION_COUNTRY)) { + shadowsocksServer.configureCountryMetrics(MMDB_LOCATION_COUNTRY); + } + if (fs.existsSync(MMDB_LOCATION_ASN)) { + shadowsocksServer.configureAsnMetrics(MMDB_LOCATION_ASN); + if (serverConfig.data().experimental?.asnMetricsEnabled) { + shadowsocksServer.enableAsnMetrics(true); + } + } const isReplayProtectionEnabled = createRolloutTracker(serverConfig).isRolloutEnabled( 'replay-protection', diff --git a/src/shadowbox/server/manager_service.spec.ts b/src/shadowbox/server/manager_service.spec.ts index 3aa868c51..52f73a9cf 100644 --- a/src/shadowbox/server/manager_service.spec.ts +++ b/src/shadowbox/server/manager_service.spec.ts @@ -1070,8 +1070,30 @@ describe('ShadowsocksManagerService', () => { }); describe('enableAsnMetrics', () => { it('Enables ASN metrics on the Shadowsocks Server', (done) => { + const serverConfig = new InMemoryConfig({} as ServerConfigJson); + const shadowsocksServer = new FakeShadowsocksServer(); + spyOn(shadowsocksServer, 'enableAsnMetrics'); + const service = new ShadowsocksManagerServiceBuilder() + .serverConfig(serverConfig) + .shadowsocksServer(shadowsocksServer) + .build(); + service.enableAsnMetrics( + {params: {asnMetricsEnabled: true}}, + { + send: (httpCode, _) => { + expect(httpCode).toEqual(204); + expect(shadowsocksServer.enableAsnMetrics).toHaveBeenCalledWith(true); + responseProcessed = true; + }, + }, + done + ); + }); + it('Sets value in the config', (done) => { + const serverConfig = new InMemoryConfig({} as ServerConfigJson); const shadowsocksServer = new FakeShadowsocksServer(); const service = new ShadowsocksManagerServiceBuilder() + .serverConfig(serverConfig) .shadowsocksServer(shadowsocksServer) .build(); service.enableAsnMetrics( @@ -1079,7 +1101,7 @@ describe('ShadowsocksManagerService', () => { { send: (httpCode, _) => { expect(httpCode).toEqual(204); - expect(shadowsocksServer.isAsnMetricsEnabled).toEqual(true); + expect(serverConfig.mostRecentWrite.experimental.asnMetricsEnabled).toBeTrue(); responseProcessed = true; }, }, diff --git a/src/shadowbox/server/manager_service.ts b/src/shadowbox/server/manager_service.ts index 42d723495..12721febf 100644 --- a/src/shadowbox/server/manager_service.ts +++ b/src/shadowbox/server/manager_service.ts @@ -285,6 +285,7 @@ export class ShadowsocksManagerService { accessKeyDataLimit: this.serverConfig.data().accessKeyDataLimit, portForNewAccessKeys: this.serverConfig.data().portForNewAccessKeys, hostnameForAccessKeys: this.serverConfig.data().hostname, + experimental: this.serverConfig.data().experimental, }); next(); } @@ -662,7 +663,12 @@ export class ShadowsocksManagerService { ) ); } - this.shadowsocksServer.isAsnMetricsEnabled = asnMetricsEnabled; + this.shadowsocksServer.enableAsnMetrics(asnMetricsEnabled); + if (this.serverConfig.data().experimental === undefined) { + this.serverConfig.data().experimental = {}; + } + this.serverConfig.data().experimental.asnMetricsEnabled = asnMetricsEnabled; + this.serverConfig.write(); res.send(HttpSuccess.NO_CONTENT); return next(); } catch (error) { diff --git a/src/shadowbox/server/mocks/mocks.ts b/src/shadowbox/server/mocks/mocks.ts index 801a40e00..a27699a68 100644 --- a/src/shadowbox/server/mocks/mocks.ts +++ b/src/shadowbox/server/mocks/mocks.ts @@ -38,8 +38,7 @@ export class InMemoryFile implements TextFile { export class FakeShadowsocksServer implements ShadowsocksServer { private accessKeys: ShadowsocksAccessKey[] = []; - isCountryMetricsEnabled = false; - isAsnMetricsEnabled = true; + enableAsnMetrics(enable: boolean) {} update(keys: ShadowsocksAccessKey[]) { this.accessKeys = keys; diff --git a/src/shadowbox/server/outline_shadowsocks_server.ts b/src/shadowbox/server/outline_shadowsocks_server.ts index 58463ecb1..8ea083f29 100644 --- a/src/shadowbox/server/outline_shadowsocks_server.ts +++ b/src/shadowbox/server/outline_shadowsocks_server.ts @@ -24,56 +24,49 @@ import {ShadowsocksAccessKey, ShadowsocksServer} from '../model/shadowsocks_serv // Runs outline-ss-server. export class OutlineShadowsocksServer implements ShadowsocksServer { private ssProcess: child_process.ChildProcess; - private isCountryMetricsEnabled_ = false; - private isAsnMetricsEnabled_ = false; + private ipCountryFilename?: string; + private ipAsnFilename?: string; + private isAsnMetricsEnabled = false; private isReplayProtectionEnabled = false; /** * @param binaryFilename The location for the outline-ss-server binary. * @param configFilename The location for the outline-ss-server config. - * @param metricsLocation The location from where to serve the Prometheus data metrics. * @param verbose Whether to run the server in verbose mode. - * @param ipCountryFilename The location of IP-to-country database file. - * @param ipAsnFilename The location of IP-to-ASN database file. + * @param metricsLocation The location from where to serve the Prometheus data metrics. */ constructor( private readonly binaryFilename: string, private readonly configFilename: string, private readonly verbose: boolean, - private readonly metricsLocation: string, - private readonly ipCountryFilename?: string, - private readonly ipAsnFilename?: string + private readonly metricsLocation: string ) {} - // Annotates the Prometheus data metrics with countries. This restarts the - // server if needed. - get isCountryMetricsEnabled(): boolean { - return this.isCountryMetricsEnabled_; - } - - set isCountryMetricsEnabled(enable: boolean) { - if (enable && !this.ipCountryFilename) { - throw new Error('Cannot enable country metrics: no country database filename set'); - } - const valueChanged = this.isAsnMetricsEnabled_ != enable; - this.isCountryMetricsEnabled_ = enable; - if (valueChanged && this.ssProcess) { - this.ssProcess.kill('SIGTERM'); - } + /** + * Configures the Shadowsocks Server with country data to annotate Prometheus data metrics. + * @param ipCountryFilename The location of the ip-country.mmdb IP-to-country database file. + */ + configureCountryMetrics(ipCountryFilename: string): OutlineShadowsocksServer { + this.ipCountryFilename = ipCountryFilename; + return this; } - // Annotates the Prometheus data metrics with autonomous system numbers (ASN). - // This restarts the server if needed. - get isAsnMetricsEnabled(): boolean { - return this.isAsnMetricsEnabled_; + /** + * Configures the Shadowsocks Server with ASN data to annotate Prometheus data metrics. + * @param ipAsnFilename The location of the ip-asn.mmdb IP-to-ASN database file. + */ + configureAsnMetrics(ipAsnFilename: string): OutlineShadowsocksServer { + this.ipAsnFilename = ipAsnFilename; + return this; } - set isAsnMetricsEnabled(enable: boolean) { + /** Annotates the Prometheus data metrics with ASN. */ + enableAsnMetrics(enable: boolean) { if (enable && !this.ipAsnFilename) { throw new Error('Cannot enable ASN metrics: no ASN database filename set'); } - const valueChanged = this.isAsnMetricsEnabled_ != enable; - this.isAsnMetricsEnabled_ = enable; + const valueChanged = this.isAsnMetricsEnabled != enable; + this.isAsnMetricsEnabled = enable; if (valueChanged && this.ssProcess) { this.ssProcess.kill('SIGTERM'); } @@ -125,10 +118,10 @@ export class OutlineShadowsocksServer implements ShadowsocksServer { private start() { const commandArguments = ['-config', this.configFilename, '-metrics', this.metricsLocation]; - if (this.isCountryMetricsEnabled_ && this.ipCountryFilename) { + if (this.ipCountryFilename) { commandArguments.push('-ip_country_db', this.ipCountryFilename); } - if (this.isAsnMetricsEnabled_ && this.ipAsnFilename) { + if (this.isAsnMetricsEnabled && this.ipAsnFilename) { commandArguments.push('-ip_asn_db', this.ipAsnFilename); } if (this.verbose) { diff --git a/src/shadowbox/server/server_config.ts b/src/shadowbox/server/server_config.ts index 2e757abeb..e131f5608 100644 --- a/src/shadowbox/server/server_config.ts +++ b/src/shadowbox/server/server_config.ts @@ -37,6 +37,12 @@ export interface ServerConfigJson { hostname?: string; // Default data transfer limit applied to all access keys. accessKeyDataLimit?: DataLimit; + + // Experimental configuration options that are expected to be short-lived. + experimental?: { + // Whether ASN metric annotation for Prometheus is enabled. + asnMetricsEnabled?: boolean; + }; } // Serialized format for rollouts. From 4f895bb24326f013c9edb852e9ebd155b6132678 Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 25 Mar 2024 16:37:42 -0400 Subject: [PATCH 6/8] Continue trying to find other database if 1 of them fails. --- src/shadowbox/scripts/update_mmdb.sh | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/shadowbox/scripts/update_mmdb.sh b/src/shadowbox/scripts/update_mmdb.sh index 51327a552..463280b29 100755 --- a/src/shadowbox/scripts/update_mmdb.sh +++ b/src/shadowbox/scripts/update_mmdb.sh @@ -28,7 +28,7 @@ readonly LIBDIR="/var/lib/libmaxminddb" # Downloads a given MMDB database and writes it to the temporary directory. # @param {string} The database to download. download_ip_mmdb() { - db=$1 + db="$1" for monthdelta in $(seq 0 9); do newdate="$(date --date="-${monthdelta} months" +%Y-%m)" @@ -39,10 +39,18 @@ download_ip_mmdb() { } main() { - # We need to make sure that we grab an existing database at install-time - if ! { download_ip_mmdb "ip-country" && download_ip_mmdb "ip-asn"; }; then - # A weird exit code on purpose -- we should catch this long before it triggers - exit 2 + status_code=0 + # We need to make sure that we grab existing databases at install-time. If + # any fail, we continue to try to fetch other databases and will return a + # weird exit code at the end -- we should catch these failures long before + # they trigger. + if ! download_ip_mmdb "ip-country" ; then + echo "Failed to download IP-country database" + status_code=2 + fi + if ! download_ip_mmdb "ip-asn" ; then + echo "Failed to download IP-ASN database" + status_code=2 fi for filename in "${TMPDIR}"/*; do @@ -52,6 +60,8 @@ main() { mkdir -p "${LIBDIR}" mv -f "${TMPDIR}"/* "${LIBDIR}" rmdir "${TMPDIR}" + + exit "${status_code}" } main "$@" From e1a78dfd1eab2dd05997e6982b7316582e494a8c Mon Sep 17 00:00:00 2001 From: sbruens Date: Mon, 25 Mar 2024 16:38:47 -0400 Subject: [PATCH 7/8] Resolve lint warning --- src/shadowbox/server/mocks/mocks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shadowbox/server/mocks/mocks.ts b/src/shadowbox/server/mocks/mocks.ts index a27699a68..2b133aa98 100644 --- a/src/shadowbox/server/mocks/mocks.ts +++ b/src/shadowbox/server/mocks/mocks.ts @@ -38,7 +38,7 @@ export class InMemoryFile implements TextFile { export class FakeShadowsocksServer implements ShadowsocksServer { private accessKeys: ShadowsocksAccessKey[] = []; - enableAsnMetrics(enable: boolean) {} + enableAsnMetrics(_: boolean) {} update(keys: ShadowsocksAccessKey[]) { this.accessKeys = keys; From fb6f8775e8f5d120fae31fc9ad8db577e77b910b Mon Sep 17 00:00:00 2001 From: sbruens Date: Fri, 29 Mar 2024 09:41:34 -0400 Subject: [PATCH 8/8] Use `outline-ss-server` v1.5.0. --- third_party/outline-ss-server/Makefile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/third_party/outline-ss-server/Makefile b/third_party/outline-ss-server/Makefile index 2d8aa3966..3bc26c30e 100644 --- a/third_party/outline-ss-server/Makefile +++ b/third_party/outline-ss-server/Makefile @@ -1,19 +1,19 @@ -VERSION=1.5.0-beta.2 +VERSION=1.5.0 .PHONY: all all: bin/linux-x86_64/outline-ss-server bin/linux-arm64/outline-ss-server bin/macos-x86_64/outline-ss-server bin/macos-arm64/outline-ss-server bin/linux-x86_64/outline-ss-server: OS=linux -bin/linux-x86_64/outline-ss-server: SHA256=4a720d8febb3d9cadfc175f31028ff2f22a2001ebeeeee269ec9ce302aff6d61 +bin/linux-x86_64/outline-ss-server: SHA256=0c6439242afbea191281404f08ef33490b01d6d0413ccca00004c8a1927de49a bin/linux-arm64/outline-ss-server: OS=linux -bin/linux-arm64/outline-ss-server: SHA256=769fe32f43e45d990cd65374ed73db00fc8fff6bc356e1b4c62074900e54ddc6 +bin/linux-arm64/outline-ss-server: SHA256=a643b28c2a894af6ceb1d309bf742092719877ede85ead6e8cbbc7b64b35a7ab bin/macos-x86_64/outline-ss-server: OS=macos -bin/macos-x86_64/outline-ss-server: SHA256=9c8f89394c7f0bc7043c456501655443c57364406ba09f69f74bf913980ebec5 +bin/macos-x86_64/outline-ss-server: SHA256=f4b034f74701e9dae52bc7c8660e875f81473ef6d535a1470967e887f5beb9c6 bin/macos-arm64/outline-ss-server: OS=macos -bin/macos-arm64/outline-ss-server: SHA256=8f77803e3dc8c95c2120545b64bc3060ba45bdfa3c09fc5b2536baa9a0840461 +bin/macos-arm64/outline-ss-server: SHA256=1f1d1833935ba363a8c468cd61e90d42de7f16e7332346b3c80f389c914192d3 TEMPFILE := $(shell mktemp) bin/%/outline-ss-server: