@opensecurity/zonzon-cli 0.1.1 → 0.1.2
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.js +215 -222
- package/package.json +1 -1
- package/tsconfig.json +1 -7
package/dist/cli.js
CHANGED
|
@@ -1,257 +1,250 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
}
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
import { parseArgs } from "util";
|
|
6
|
+
import { randomBytes } from "crypto";
|
|
7
|
+
import { DevDnsServer, DnsHandler, HttpHandler, SniProxyService, validateServerConfig, audit } from "@opensecurity/zonzon-core";
|
|
8
|
+
import { ControlPlane } from "@opensecurity/zonzon-control-plane";
|
|
9
|
+
const CONFIG_DIR = path.join(os.homedir(), ".zonzon");
|
|
10
|
+
const DEFAULT_CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
11
|
+
function ensureConfigDir() {
|
|
12
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
13
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
37
14
|
}
|
|
38
|
-
return { config, port, httpPort, watch, help };
|
|
39
15
|
}
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
}
|
|
16
|
+
function loadConfig(configPath) {
|
|
17
|
+
if (!fs.existsSync(configPath)) {
|
|
18
|
+
return {};
|
|
51
19
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
}
|
|
20
|
+
try {
|
|
21
|
+
const fileContents = fs.readFileSync(configPath, "utf8");
|
|
22
|
+
return JSON.parse(fileContents) || {};
|
|
78
23
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
audit.error(`Failed to parse configuration file at ${configPath}: ${err.message}`);
|
|
26
|
+
process.exit(1);
|
|
93
27
|
}
|
|
94
|
-
return config;
|
|
95
28
|
}
|
|
96
|
-
function
|
|
97
|
-
|
|
29
|
+
function saveConfig(configPath, data) {
|
|
30
|
+
ensureConfigDir();
|
|
98
31
|
try {
|
|
99
|
-
|
|
32
|
+
const jsonStr = JSON.stringify(data, null, 2);
|
|
33
|
+
fs.writeFileSync(configPath, jsonStr, { encoding: "utf8", mode: 0o600 });
|
|
100
34
|
}
|
|
101
35
|
catch (err) {
|
|
102
|
-
audit.error(`
|
|
36
|
+
audit.error(`Failed to write configuration file at ${configPath}: ${err.message}`);
|
|
103
37
|
process.exit(1);
|
|
104
38
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
39
|
+
}
|
|
40
|
+
function setDeepValue(obj, pathStr, value) {
|
|
41
|
+
const parts = pathStr.split(".");
|
|
42
|
+
const last = parts.pop();
|
|
43
|
+
let current = obj;
|
|
44
|
+
for (const part of parts) {
|
|
45
|
+
if (!current[part] || typeof current[part] !== "object") {
|
|
46
|
+
current[part] = {};
|
|
112
47
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
48
|
+
current = current[part];
|
|
49
|
+
}
|
|
50
|
+
if (value === "true")
|
|
51
|
+
current[last] = true;
|
|
52
|
+
else if (value === "false")
|
|
53
|
+
current[last] = false;
|
|
54
|
+
else if (!isNaN(Number(value)))
|
|
55
|
+
current[last] = Number(value);
|
|
56
|
+
else
|
|
57
|
+
current[last] = value;
|
|
58
|
+
}
|
|
59
|
+
function printUsage() {
|
|
60
|
+
console.log(`
|
|
61
|
+
zonzon core engine (v0.1.0)
|
|
62
|
+
Usage: zonzon <command> [options]
|
|
63
|
+
|
|
64
|
+
Commands:
|
|
65
|
+
init Initialize the default configuration file at ~/.zonzon/config.json
|
|
66
|
+
start Boot the routing engine and control plane
|
|
67
|
+
config Manage configuration state
|
|
68
|
+
|
|
69
|
+
Config Commands:
|
|
70
|
+
zonzon config view Print the current configuration
|
|
71
|
+
zonzon config set <key> <value> Set a configuration value using dot notation
|
|
72
|
+
Example: zonzon config set port 53
|
|
73
|
+
Example: zonzon config set controlPlane.port 8081
|
|
74
|
+
|
|
75
|
+
Global Options:
|
|
76
|
+
--config, -c Override path to configuration file (default: ~/.zonzon/config.json)
|
|
77
|
+
`);
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
async function handleInit(configPath) {
|
|
81
|
+
if (fs.existsSync(configPath)) {
|
|
82
|
+
audit.error(`Configuration already exists at ${configPath}`);
|
|
116
83
|
process.exit(1);
|
|
117
84
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
85
|
+
const defaultConf = {
|
|
86
|
+
port: 53,
|
|
87
|
+
fallbackDns: "1.1.1.1",
|
|
88
|
+
maxTcpConnections: 100,
|
|
89
|
+
tcpIdleTimeoutMs: 30000,
|
|
90
|
+
controlPlane: {
|
|
91
|
+
enabled: true,
|
|
92
|
+
port: 8080
|
|
93
|
+
},
|
|
94
|
+
firewall: {
|
|
95
|
+
defaultPolicy: "deny",
|
|
96
|
+
allowlist_ips: ["127.0.0.1"]
|
|
97
|
+
},
|
|
98
|
+
hosts: {}
|
|
99
|
+
};
|
|
100
|
+
saveConfig(configPath, defaultConf);
|
|
101
|
+
audit.system(`Initialized secure default configuration at ${configPath}`);
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
async function handleConfig(configPath, args) {
|
|
105
|
+
const subCmd = args[0];
|
|
106
|
+
if (subCmd === "view") {
|
|
107
|
+
if (!fs.existsSync(configPath)) {
|
|
108
|
+
audit.error(`No configuration found at ${configPath}. Run 'zonzon init' first.`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
const fileContents = fs.readFileSync(configPath, "utf8");
|
|
112
|
+
console.log(fileContents);
|
|
113
|
+
process.exit(0);
|
|
123
114
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
115
|
+
if (subCmd === "set") {
|
|
116
|
+
const key = args[1];
|
|
117
|
+
const value = args[2];
|
|
118
|
+
if (!key || value === undefined) {
|
|
119
|
+
audit.error("Usage: zonzon config set <key> <value>");
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
const currentConfig = loadConfig(configPath);
|
|
123
|
+
setDeepValue(currentConfig, key, value);
|
|
124
|
+
saveConfig(configPath, currentConfig);
|
|
125
|
+
audit.system(`Updated configuration: ${key} = ${value}`);
|
|
126
|
+
process.exit(0);
|
|
127
127
|
}
|
|
128
|
-
|
|
128
|
+
printUsage();
|
|
129
129
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
async function startEngine(configPath, portOverride, cpPortOverride) {
|
|
131
|
+
const rawConfig = loadConfig(configPath);
|
|
132
|
+
if (portOverride) {
|
|
133
|
+
rawConfig.port = parseInt(portOverride, 10);
|
|
134
|
+
}
|
|
135
|
+
if (cpPortOverride) {
|
|
136
|
+
if (!rawConfig.controlPlane)
|
|
137
|
+
rawConfig.controlPlane = {};
|
|
138
|
+
rawConfig.controlPlane.port = parseInt(cpPortOverride, 10);
|
|
139
|
+
}
|
|
140
|
+
let config;
|
|
138
141
|
try {
|
|
139
|
-
|
|
140
|
-
dnsHandler.stop(),
|
|
141
|
-
httpHandler.stop(),
|
|
142
|
-
sniHandler.stop()
|
|
143
|
-
]);
|
|
144
|
-
clearTimeout(forceExit);
|
|
145
|
-
process.exit(0);
|
|
142
|
+
config = validateServerConfig(rawConfig);
|
|
146
143
|
}
|
|
147
144
|
catch (err) {
|
|
148
|
-
|
|
145
|
+
audit.error(`Configuration Schema Violation: ${err.message}`);
|
|
149
146
|
process.exit(1);
|
|
150
147
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
let
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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;
|
|
148
|
+
const dnsServer = new DevDnsServer(config);
|
|
149
|
+
const dnsHandler = new DnsHandler(dnsServer, config);
|
|
150
|
+
const httpHandler = new HttpHandler(dnsServer, config, 80);
|
|
151
|
+
const sniProxy = new SniProxyService(config, 443);
|
|
152
|
+
const isCpEnabled = config.controlPlane?.enabled !== false;
|
|
153
|
+
let controlPlane = null;
|
|
154
|
+
let isEphemeralKey = false;
|
|
155
|
+
let activeApiKey = "";
|
|
156
|
+
if (isCpEnabled) {
|
|
157
|
+
activeApiKey = config.controlPlane?.apiKey || "";
|
|
158
|
+
if (!activeApiKey) {
|
|
159
|
+
activeApiKey = randomBytes(32).toString("hex");
|
|
160
|
+
isEphemeralKey = true;
|
|
161
|
+
}
|
|
162
|
+
const blindIndexSalt = randomBytes(16).toString("hex");
|
|
163
|
+
const cpPort = config.controlPlane?.port || 8080;
|
|
164
|
+
controlPlane = new ControlPlane({
|
|
165
|
+
port: cpPort,
|
|
166
|
+
apiKey: activeApiKey,
|
|
167
|
+
blindIndexSalt: blindIndexSalt,
|
|
168
|
+
initialConfig: config,
|
|
169
|
+
});
|
|
170
|
+
controlPlane.subscribe((newConfig) => {
|
|
171
|
+
audit.system("Applying dynamic configuration update from Control Plane...");
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const shutdown = async () => {
|
|
175
|
+
audit.system("Initiating graceful shutdown sequence...");
|
|
176
|
+
await dnsHandler.stop();
|
|
177
|
+
await httpHandler.stop();
|
|
178
|
+
await sniProxy.stop();
|
|
179
|
+
if (controlPlane) {
|
|
180
|
+
await controlPlane.stop();
|
|
206
181
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
182
|
+
process.exit(0);
|
|
183
|
+
};
|
|
184
|
+
process.on("SIGINT", shutdown);
|
|
185
|
+
process.on("SIGTERM", shutdown);
|
|
186
|
+
try {
|
|
187
|
+
await dnsHandler.start();
|
|
188
|
+
audit.system(`DNS Listener actively enforcing Zero-Trust boundaries on port ${config.port}`);
|
|
189
|
+
await httpHandler.start();
|
|
190
|
+
audit.system(`HTTP L7 Sandbox Router active on port 80`);
|
|
191
|
+
await sniProxy.start();
|
|
192
|
+
audit.system(`SNI Proxy active on port 443`);
|
|
193
|
+
if (controlPlane) {
|
|
194
|
+
await controlPlane.start();
|
|
195
|
+
if (isEphemeralKey) {
|
|
196
|
+
audit.system(`[SECURITY] Generated Ephemeral API Key for this session: ${activeApiKey}`);
|
|
197
|
+
audit.system(`[SECURITY] Do not lose this key. It will not be shown again.`);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
audit.system(`[SECURITY] Control Plane using static API Key from configuration.`);
|
|
222
201
|
}
|
|
223
|
-
throw err;
|
|
224
202
|
}
|
|
203
|
+
audit.system("Initialization complete. Awaiting connections...");
|
|
225
204
|
}
|
|
226
|
-
|
|
227
|
-
|
|
205
|
+
catch (err) {
|
|
206
|
+
audit.error(`Fatal bind error during initialization: ${err.message}`);
|
|
207
|
+
await shutdown();
|
|
228
208
|
}
|
|
229
209
|
}
|
|
230
210
|
async function main() {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
211
|
+
const { values, positionals } = parseArgs({
|
|
212
|
+
options: {
|
|
213
|
+
config: {
|
|
214
|
+
type: "string",
|
|
215
|
+
short: "c",
|
|
216
|
+
},
|
|
217
|
+
port: {
|
|
218
|
+
type: "string",
|
|
219
|
+
short: "p",
|
|
220
|
+
},
|
|
221
|
+
"cp-port": {
|
|
222
|
+
type: "string",
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
strict: false,
|
|
226
|
+
allowPositionals: true
|
|
227
|
+
});
|
|
228
|
+
const command = positionals[0];
|
|
229
|
+
const configPath = values.config
|
|
230
|
+
? path.resolve(process.cwd(), values.config)
|
|
231
|
+
: DEFAULT_CONFIG_PATH;
|
|
232
|
+
switch (command) {
|
|
233
|
+
case "init":
|
|
234
|
+
await handleInit(configPath);
|
|
235
|
+
break;
|
|
236
|
+
case "config":
|
|
237
|
+
await handleConfig(configPath, positionals.slice(1));
|
|
238
|
+
break;
|
|
239
|
+
case "start":
|
|
240
|
+
await startEngine(configPath, values.port, values["cp-port"]);
|
|
241
|
+
break;
|
|
242
|
+
default:
|
|
243
|
+
printUsage();
|
|
244
|
+
break;
|
|
251
245
|
}
|
|
252
|
-
await startServer(configPath, args);
|
|
253
246
|
}
|
|
254
247
|
main().catch((err) => {
|
|
255
|
-
audit.error(`
|
|
248
|
+
audit.error(`Unhandled execution fault: ${err.message}`);
|
|
256
249
|
process.exit(1);
|
|
257
250
|
});
|
package/package.json
CHANGED
package/tsconfig.json
CHANGED
|
@@ -2,13 +2,7 @@
|
|
|
2
2
|
"extends": "../../tsconfig.base.json",
|
|
3
3
|
"compilerOptions": {
|
|
4
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
|
-
}
|
|
5
|
+
"rootDir": "./src"
|
|
12
6
|
},
|
|
13
7
|
"references": [
|
|
14
8
|
{ "path": "../core" },
|