@opensecurity/zonzon-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +257 -0
- package/package.json +33 -0
- package/src/cli.ts +284 -0
- package/tsconfig.json +19 -0
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, existsSync, watchFile } from "fs";
|
|
3
|
+
import { resolve, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { parse as parseYaml } from "yaml";
|
|
6
|
+
import { validateServerConfig, DevDnsServer, DnsHandler, HttpHandler, SniProxyService, audit } from "zonzon-core";
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
function parseArgs() {
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
let config = "";
|
|
11
|
+
let port;
|
|
12
|
+
let httpPort;
|
|
13
|
+
let watch = false;
|
|
14
|
+
let help = false;
|
|
15
|
+
for (let i = 0; i < args.length; i++) {
|
|
16
|
+
switch (args[i]) {
|
|
17
|
+
case "--config":
|
|
18
|
+
case "-c":
|
|
19
|
+
config = args[++i] || "";
|
|
20
|
+
break;
|
|
21
|
+
case "--port":
|
|
22
|
+
case "-p":
|
|
23
|
+
port = Number(args[++i]);
|
|
24
|
+
break;
|
|
25
|
+
case "--http-port":
|
|
26
|
+
httpPort = Number(args[++i]);
|
|
27
|
+
break;
|
|
28
|
+
case "--watch":
|
|
29
|
+
case "-w":
|
|
30
|
+
watch = true;
|
|
31
|
+
break;
|
|
32
|
+
case "--help":
|
|
33
|
+
case "-h":
|
|
34
|
+
help = true;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { config, port, httpPort, watch, help };
|
|
39
|
+
}
|
|
40
|
+
function findDefaultConfig() {
|
|
41
|
+
const candidates = [
|
|
42
|
+
resolve(process.cwd(), "config/hosts.yaml"),
|
|
43
|
+
resolve(process.cwd(), "config/hosts.json"),
|
|
44
|
+
resolve(__dirname, "../../../config/hosts.yaml"),
|
|
45
|
+
resolve(__dirname, "../../../config/hosts.json"),
|
|
46
|
+
];
|
|
47
|
+
for (const candidate of candidates) {
|
|
48
|
+
if (existsSync(candidate)) {
|
|
49
|
+
return candidate;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
function printUsage() {
|
|
55
|
+
console.log(`
|
|
56
|
+
zonzon CLI
|
|
57
|
+
|
|
58
|
+
Options:
|
|
59
|
+
-c, --config <path> Path to configuration file
|
|
60
|
+
-p, --port <number> DNS port
|
|
61
|
+
--http-port <num> HTTP proxy/redirect port
|
|
62
|
+
-w, --watch Watch config file
|
|
63
|
+
-h, --help Show this help message
|
|
64
|
+
`);
|
|
65
|
+
}
|
|
66
|
+
function mergeEnvConfig(config) {
|
|
67
|
+
if (process.env.ZONZON_PORT)
|
|
68
|
+
config.port = parseInt(process.env.ZONZON_PORT, 10);
|
|
69
|
+
if (process.env.ZONZON_FALLBACK_DNS)
|
|
70
|
+
config.fallbackDns = process.env.ZONZON_FALLBACK_DNS;
|
|
71
|
+
if (!config.firewall)
|
|
72
|
+
config.firewall = { defaultPolicy: "deny" };
|
|
73
|
+
if (process.env.ZONZON_DEFAULT_POLICY) {
|
|
74
|
+
const policy = process.env.ZONZON_DEFAULT_POLICY.toLowerCase();
|
|
75
|
+
if (policy === "allow" || policy === "deny") {
|
|
76
|
+
config.firewall.defaultPolicy = policy;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const listMappings = [
|
|
80
|
+
{ env: "ZONZON_FIREWALL_ALLOWLIST_DOMAINS", key: "allowlist_domains" },
|
|
81
|
+
{ env: "ZONZON_FIREWALL_BLOCKLIST_DOMAINS", key: "blocklist_domains" },
|
|
82
|
+
{ env: "ZONZON_FIREWALL_ALLOWLIST_RANGES", key: "allowlist_ranges" },
|
|
83
|
+
{ env: "ZONZON_FIREWALL_BLOCKLIST_RANGES", key: "blocklist_ranges" },
|
|
84
|
+
{ env: "ZONZON_FIREWALL_ALLOWLIST_IPS", key: "allowlist_ips" },
|
|
85
|
+
{ env: "ZONZON_FIREWALL_BLOCKLIST_IPS", key: "blocklist_ips" }
|
|
86
|
+
];
|
|
87
|
+
for (const mapping of listMappings) {
|
|
88
|
+
const envVal = process.env[mapping.env];
|
|
89
|
+
if (envVal) {
|
|
90
|
+
const items = envVal.split(",").map(i => i.trim()).filter(Boolean);
|
|
91
|
+
config.firewall[mapping.key] = [...(config.firewall[mapping.key] || []), ...items];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return config;
|
|
95
|
+
}
|
|
96
|
+
function loadConfig(path) {
|
|
97
|
+
let raw;
|
|
98
|
+
try {
|
|
99
|
+
raw = readFileSync(path, "utf-8");
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
audit.error(`Error loading config file '${path}'`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
if (path.endsWith(".yaml") || path.endsWith(".yml")) {
|
|
108
|
+
parsed = parseYaml(raw);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
parsed = JSON.parse(raw);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
audit.error(`Error parsing config file`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
let validated;
|
|
119
|
+
try {
|
|
120
|
+
validated = validateServerConfig(parsed);
|
|
121
|
+
validated = mergeEnvConfig(validated);
|
|
122
|
+
validated = validateServerConfig(validated);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
audit.error(`Configuration validation error`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
return validated;
|
|
129
|
+
}
|
|
130
|
+
let isShuttingDown = false;
|
|
131
|
+
async function gracefulShutdown(dnsHandler, httpHandler, sniHandler) {
|
|
132
|
+
if (isShuttingDown)
|
|
133
|
+
return;
|
|
134
|
+
isShuttingDown = true;
|
|
135
|
+
const forceExit = setTimeout(() => {
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}, 5000);
|
|
138
|
+
try {
|
|
139
|
+
await Promise.all([
|
|
140
|
+
dnsHandler.stop(),
|
|
141
|
+
httpHandler.stop(),
|
|
142
|
+
sniHandler.stop()
|
|
143
|
+
]);
|
|
144
|
+
clearTimeout(forceExit);
|
|
145
|
+
process.exit(0);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
clearTimeout(forceExit);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function startServer(configPath, args) {
|
|
153
|
+
let config = loadConfig(configPath);
|
|
154
|
+
const effectivePort = args.port ?? config.port;
|
|
155
|
+
config = { ...config, port: effectivePort };
|
|
156
|
+
const httpPort = args.httpPort || 80;
|
|
157
|
+
const httpsPort = 443;
|
|
158
|
+
let dnsHandler;
|
|
159
|
+
let httpHandler;
|
|
160
|
+
let sniHandler;
|
|
161
|
+
const portsToTry = effectivePort === 53 ? [53, 10053] : [effectivePort];
|
|
162
|
+
let started = false;
|
|
163
|
+
for (const attemptPort of portsToTry) {
|
|
164
|
+
try {
|
|
165
|
+
const portConfig = { ...config, port: attemptPort };
|
|
166
|
+
const dnsServer = new DevDnsServer(portConfig);
|
|
167
|
+
dnsHandler = new DnsHandler(dnsServer, portConfig);
|
|
168
|
+
const actualHttpPort = (attemptPort !== effectivePort && !args.httpPort) ? 8080 : httpPort;
|
|
169
|
+
const actualHttpsPort = (attemptPort !== effectivePort && !args.httpPort) ? 8443 : httpsPort;
|
|
170
|
+
httpHandler = new HttpHandler(dnsServer, config, actualHttpPort);
|
|
171
|
+
sniHandler = new SniProxyService(config, actualHttpsPort);
|
|
172
|
+
await dnsHandler.start();
|
|
173
|
+
await httpHandler.start();
|
|
174
|
+
await sniHandler.start();
|
|
175
|
+
started = true;
|
|
176
|
+
audit.system(`System active: DNS=${attemptPort} | HTTP=${actualHttpPort} | HTTPS=${actualHttpsPort}`);
|
|
177
|
+
const filePath = resolve(configPath);
|
|
178
|
+
let prevContent = "";
|
|
179
|
+
try {
|
|
180
|
+
prevContent = readFileSync(filePath, { encoding: "utf-8" });
|
|
181
|
+
}
|
|
182
|
+
catch { }
|
|
183
|
+
watchFile(filePath, async () => {
|
|
184
|
+
if (isShuttingDown)
|
|
185
|
+
return;
|
|
186
|
+
try {
|
|
187
|
+
const content = readFileSync(filePath, { encoding: "utf-8" });
|
|
188
|
+
if (content !== prevContent) {
|
|
189
|
+
prevContent = content;
|
|
190
|
+
if (dnsHandler && httpHandler && sniHandler) {
|
|
191
|
+
await Promise.all([dnsHandler.stop(), httpHandler.stop(), sniHandler.stop()]);
|
|
192
|
+
startServer(filePath, args);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
const handleExit = () => {
|
|
200
|
+
if (dnsHandler && httpHandler && sniHandler)
|
|
201
|
+
gracefulShutdown(dnsHandler, httpHandler, sniHandler);
|
|
202
|
+
};
|
|
203
|
+
process.on("SIGINT", handleExit);
|
|
204
|
+
process.on("SIGTERM", handleExit);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
if (dnsHandler)
|
|
209
|
+
await dnsHandler.stop().catch(() => { });
|
|
210
|
+
if (httpHandler)
|
|
211
|
+
await httpHandler.stop().catch(() => { });
|
|
212
|
+
if (sniHandler)
|
|
213
|
+
await sniHandler.stop().catch(() => { });
|
|
214
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
215
|
+
if (message.includes("EACCES") || message.includes("EADDRINUSE")) {
|
|
216
|
+
if (attemptPort === effectivePort && effectivePort === 53) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
else if (attemptPort === 10053) {
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (!started) {
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async function main() {
|
|
231
|
+
const args = parseArgs();
|
|
232
|
+
if (args.help) {
|
|
233
|
+
printUsage();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
let configPath;
|
|
237
|
+
if (args.config) {
|
|
238
|
+
configPath = resolve(args.config);
|
|
239
|
+
if (!existsSync(configPath)) {
|
|
240
|
+
audit.error(`Config file not found at '${configPath}'`);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
const autoPath = findDefaultConfig();
|
|
246
|
+
if (!autoPath) {
|
|
247
|
+
audit.error("No configuration file found");
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
configPath = autoPath;
|
|
251
|
+
}
|
|
252
|
+
await startServer(configPath, args);
|
|
253
|
+
}
|
|
254
|
+
main().catch((err) => {
|
|
255
|
+
audit.error(`Uncaught Exception: ${err instanceof Error ? err.message : String(err)}`);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opensecurity/zonzon-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Command line interface and entrypoint for zonzon",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "Lucian BLETAN <neuraluc@gmail.com>",
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"bin": {
|
|
9
|
+
"zonzon": "./dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/opensecurity/zonzon.git",
|
|
14
|
+
"directory": "packages/cli"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/opensecurity/zonzon/tree/main/packages/cli#readme",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"cli",
|
|
19
|
+
"server",
|
|
20
|
+
"daemon"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -b",
|
|
27
|
+
"start": "node ./dist/cli.js start"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@opensecurity/zonzon-core": "*",
|
|
31
|
+
"@opensecurity/zonzon-control-plane": "*"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import { parseArgs } from "util";
|
|
7
|
+
import { randomBytes } from "crypto";
|
|
8
|
+
import {
|
|
9
|
+
DevDnsServer,
|
|
10
|
+
DnsHandler,
|
|
11
|
+
HttpHandler,
|
|
12
|
+
SniProxyService,
|
|
13
|
+
ServerConfig,
|
|
14
|
+
validateServerConfig,
|
|
15
|
+
audit
|
|
16
|
+
} from "@opensecurity/zonzon-core";
|
|
17
|
+
import { ControlPlane } from "@opensecurity/zonzon-control-plane";
|
|
18
|
+
|
|
19
|
+
const CONFIG_DIR = path.join(os.homedir(), ".zonzon");
|
|
20
|
+
const DEFAULT_CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
21
|
+
|
|
22
|
+
function ensureConfigDir() {
|
|
23
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
24
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadConfig(configPath: string): any {
|
|
29
|
+
if (!fs.existsSync(configPath)) {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const fileContents = fs.readFileSync(configPath, "utf8");
|
|
34
|
+
return JSON.parse(fileContents) || {};
|
|
35
|
+
} catch (err: any) {
|
|
36
|
+
audit.error(`Failed to parse configuration file at ${configPath}: ${err.message}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function saveConfig(configPath: string, data: any): void {
|
|
42
|
+
ensureConfigDir();
|
|
43
|
+
try {
|
|
44
|
+
const jsonStr = JSON.stringify(data, null, 2);
|
|
45
|
+
fs.writeFileSync(configPath, jsonStr, { encoding: "utf8", mode: 0o600 });
|
|
46
|
+
} catch (err: any) {
|
|
47
|
+
audit.error(`Failed to write configuration file at ${configPath}: ${err.message}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function setDeepValue(obj: any, pathStr: string, value: any): void {
|
|
53
|
+
const parts = pathStr.split(".");
|
|
54
|
+
const last = parts.pop()!;
|
|
55
|
+
let current = obj;
|
|
56
|
+
for (const part of parts) {
|
|
57
|
+
if (!current[part] || typeof current[part] !== "object") {
|
|
58
|
+
current[part] = {};
|
|
59
|
+
}
|
|
60
|
+
current = current[part];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (value === "true") current[last] = true;
|
|
64
|
+
else if (value === "false") current[last] = false;
|
|
65
|
+
else if (!isNaN(Number(value))) current[last] = Number(value);
|
|
66
|
+
else current[last] = value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function printUsage() {
|
|
70
|
+
console.log(`
|
|
71
|
+
zonzon core engine (v0.1.0)
|
|
72
|
+
Usage: zonzon <command> [options]
|
|
73
|
+
|
|
74
|
+
Commands:
|
|
75
|
+
init Initialize the default configuration file at ~/.zonzon/config.json
|
|
76
|
+
start Boot the routing engine and control plane
|
|
77
|
+
config Manage configuration state
|
|
78
|
+
|
|
79
|
+
Config Commands:
|
|
80
|
+
zonzon config view Print the current configuration
|
|
81
|
+
zonzon config set <key> <value> Set a configuration value using dot notation
|
|
82
|
+
Example: zonzon config set port 53
|
|
83
|
+
Example: zonzon config set controlPlane.port 8081
|
|
84
|
+
|
|
85
|
+
Global Options:
|
|
86
|
+
--config, -c Override path to configuration file (default: ~/.zonzon/config.json)
|
|
87
|
+
`);
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function handleInit(configPath: string) {
|
|
92
|
+
if (fs.existsSync(configPath)) {
|
|
93
|
+
audit.error(`Configuration already exists at ${configPath}`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const defaultConf = {
|
|
98
|
+
port: 53,
|
|
99
|
+
fallbackDns: "1.1.1.1",
|
|
100
|
+
maxTcpConnections: 100,
|
|
101
|
+
tcpIdleTimeoutMs: 30000,
|
|
102
|
+
controlPlane: {
|
|
103
|
+
enabled: true,
|
|
104
|
+
port: 8080
|
|
105
|
+
},
|
|
106
|
+
firewall: {
|
|
107
|
+
defaultPolicy: "deny",
|
|
108
|
+
allowlist_ips: ["127.0.0.1"]
|
|
109
|
+
},
|
|
110
|
+
hosts: {}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
saveConfig(configPath, defaultConf);
|
|
114
|
+
audit.system(`Initialized secure default configuration at ${configPath}`);
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function handleConfig(configPath: string, args: string[]) {
|
|
119
|
+
const subCmd = args[0];
|
|
120
|
+
if (subCmd === "view") {
|
|
121
|
+
if (!fs.existsSync(configPath)) {
|
|
122
|
+
audit.error(`No configuration found at ${configPath}. Run 'zonzon init' first.`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
const fileContents = fs.readFileSync(configPath, "utf8");
|
|
126
|
+
console.log(fileContents);
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (subCmd === "set") {
|
|
131
|
+
const key = args[1];
|
|
132
|
+
const value = args[2];
|
|
133
|
+
if (!key || value === undefined) {
|
|
134
|
+
audit.error("Usage: zonzon config set <key> <value>");
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const currentConfig = loadConfig(configPath);
|
|
139
|
+
setDeepValue(currentConfig, key, value);
|
|
140
|
+
saveConfig(configPath, currentConfig);
|
|
141
|
+
audit.system(`Updated configuration: ${key} = ${value}`);
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
printUsage();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function startEngine(configPath: string, portOverride?: string, cpPortOverride?: string) {
|
|
149
|
+
const rawConfig = loadConfig(configPath);
|
|
150
|
+
|
|
151
|
+
if (portOverride) {
|
|
152
|
+
rawConfig.port = parseInt(portOverride, 10);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (cpPortOverride) {
|
|
156
|
+
if (!rawConfig.controlPlane) rawConfig.controlPlane = {};
|
|
157
|
+
rawConfig.controlPlane.port = parseInt(cpPortOverride, 10);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let config: ServerConfig;
|
|
161
|
+
try {
|
|
162
|
+
config = validateServerConfig(rawConfig);
|
|
163
|
+
} catch (err: any) {
|
|
164
|
+
audit.error(`Configuration Schema Violation: ${err.message}`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const dnsServer = new DevDnsServer(config);
|
|
169
|
+
const dnsHandler = new DnsHandler(dnsServer, config);
|
|
170
|
+
const httpHandler = new HttpHandler(dnsServer, config, 80);
|
|
171
|
+
const sniProxy = new SniProxyService(config, 443);
|
|
172
|
+
|
|
173
|
+
const isCpEnabled = config.controlPlane?.enabled !== false;
|
|
174
|
+
let controlPlane: ControlPlane | null = null;
|
|
175
|
+
let isEphemeralKey = false;
|
|
176
|
+
let activeApiKey = "";
|
|
177
|
+
|
|
178
|
+
if (isCpEnabled) {
|
|
179
|
+
activeApiKey = config.controlPlane?.apiKey || "";
|
|
180
|
+
if (!activeApiKey) {
|
|
181
|
+
activeApiKey = randomBytes(32).toString("hex");
|
|
182
|
+
isEphemeralKey = true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const blindIndexSalt = randomBytes(16).toString("hex");
|
|
186
|
+
const cpPort = config.controlPlane?.port || 8080;
|
|
187
|
+
|
|
188
|
+
controlPlane = new ControlPlane({
|
|
189
|
+
port: cpPort,
|
|
190
|
+
apiKey: activeApiKey,
|
|
191
|
+
blindIndexSalt: blindIndexSalt,
|
|
192
|
+
initialConfig: config,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
controlPlane.subscribe((newConfig) => {
|
|
196
|
+
audit.system("Applying dynamic configuration update from Control Plane...");
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const shutdown = async () => {
|
|
201
|
+
audit.system("Initiating graceful shutdown sequence...");
|
|
202
|
+
await dnsHandler.stop();
|
|
203
|
+
await httpHandler.stop();
|
|
204
|
+
await sniProxy.stop();
|
|
205
|
+
if (controlPlane) {
|
|
206
|
+
await controlPlane.stop();
|
|
207
|
+
}
|
|
208
|
+
process.exit(0);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
process.on("SIGINT", shutdown);
|
|
212
|
+
process.on("SIGTERM", shutdown);
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
await dnsHandler.start();
|
|
216
|
+
audit.system(`DNS Listener actively enforcing Zero-Trust boundaries on port ${config.port}`);
|
|
217
|
+
|
|
218
|
+
await httpHandler.start();
|
|
219
|
+
audit.system(`HTTP L7 Sandbox Router active on port 80`);
|
|
220
|
+
|
|
221
|
+
await sniProxy.start();
|
|
222
|
+
audit.system(`SNI Proxy active on port 443`);
|
|
223
|
+
|
|
224
|
+
if (controlPlane) {
|
|
225
|
+
await controlPlane.start();
|
|
226
|
+
if (isEphemeralKey) {
|
|
227
|
+
audit.system(`[SECURITY] Generated Ephemeral API Key for this session: ${activeApiKey}`);
|
|
228
|
+
audit.system(`[SECURITY] Do not lose this key. It will not be shown again.`);
|
|
229
|
+
} else {
|
|
230
|
+
audit.system(`[SECURITY] Control Plane using static API Key from configuration.`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
audit.system("Initialization complete. Awaiting connections...");
|
|
235
|
+
} catch (err: any) {
|
|
236
|
+
audit.error(`Fatal bind error during initialization: ${err.message}`);
|
|
237
|
+
await shutdown();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function main() {
|
|
242
|
+
const { values, positionals } = parseArgs({
|
|
243
|
+
options: {
|
|
244
|
+
config: {
|
|
245
|
+
type: "string",
|
|
246
|
+
short: "c",
|
|
247
|
+
},
|
|
248
|
+
port: {
|
|
249
|
+
type: "string",
|
|
250
|
+
short: "p",
|
|
251
|
+
},
|
|
252
|
+
"cp-port": {
|
|
253
|
+
type: "string",
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
strict: false,
|
|
257
|
+
allowPositionals: true
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const command = positionals[0];
|
|
261
|
+
const configPath = values.config
|
|
262
|
+
? path.resolve(process.cwd(), values.config as string)
|
|
263
|
+
: DEFAULT_CONFIG_PATH;
|
|
264
|
+
|
|
265
|
+
switch (command) {
|
|
266
|
+
case "init":
|
|
267
|
+
await handleInit(configPath);
|
|
268
|
+
break;
|
|
269
|
+
case "config":
|
|
270
|
+
await handleConfig(configPath, positionals.slice(1));
|
|
271
|
+
break;
|
|
272
|
+
case "start":
|
|
273
|
+
await startEngine(configPath, values.port as string, values["cp-port"] as string);
|
|
274
|
+
break;
|
|
275
|
+
default:
|
|
276
|
+
printUsage();
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
main().catch((err) => {
|
|
282
|
+
audit.error(`Unhandled execution fault: ${err.message}`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"baseUrl": ".",
|
|
7
|
+
"ignoreDeprecations": "6.0",
|
|
8
|
+
"paths": {
|
|
9
|
+
"@opensecurity/zonzon-core": ["../core/src/index.ts"],
|
|
10
|
+
"@opensecurity/zonzon-control-plane": ["../control-plane/src/index.ts"]
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"references": [
|
|
14
|
+
{ "path": "../core" },
|
|
15
|
+
{ "path": "../control-plane" }
|
|
16
|
+
],
|
|
17
|
+
"include": ["src/**/*.ts"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|