From 13f62390bfda8cd0ac9215aad685ae82f82a1270 Mon Sep 17 00:00:00 2001
From: Sander Bruens <sbruens@users.noreply.github.com>
Date: Tue, 7 Jan 2025 16:51:47 -0500
Subject: [PATCH] refactor(server): use the new service config format (#1628)

* refactor(server): use the new service config format

* Keep the original filter to filter out inactive keys first.

* Do the transformation at config writing time.
---
 .../server/outline_shadowsocks_server.ts      | 45 ++++++++++++++++---
 1 file changed, 38 insertions(+), 7 deletions(-)

diff --git a/src/shadowbox/server/outline_shadowsocks_server.ts b/src/shadowbox/server/outline_shadowsocks_server.ts
index e7c403701..1abe1b2c0 100644
--- a/src/shadowbox/server/outline_shadowsocks_server.ts
+++ b/src/shadowbox/server/outline_shadowsocks_server.ts
@@ -21,12 +21,26 @@ import * as file from '../infrastructure/file';
 import * as logging from '../infrastructure/logging';
 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 {
   private ssProcess: child_process.ChildProcess;
   private ipCountryFilename?: string;
   private ipAsnFilename?: string;
-  private isAsnMetricsEnabled = false;
   private isReplayProtectionEnabled = false;
 
   /**
@@ -81,22 +95,39 @@ export class OutlineShadowsocksServer implements ShadowsocksServer {
 
   private writeConfigFile(keys: ShadowsocksAccessKey[]): Promise<void> {
     return new Promise((resolve, reject) => {
-      const keysJson = {keys: [] as ShadowsocksAccessKey[]};
-      for (const key of keys) {
+      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.`
           );
-          continue;
+          return false;
         }
+        return true;
+      });
 
-        keysJson.keys.push(key);
+      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(keysJson, {sortKeys: true}));
+        file.atomicWriteFileSync(this.configFilename, jsyaml.safeDump(config, {sortKeys: true}));
         resolve();
       } catch (error) {
         reject(error);