3191e9117c235d37d3015e17be5cc9063b25604a
[project/unetd.git] / scripts / unet-cli
1 #!/usr/bin/env ucode
2 'use strict';
3
4 import { access, basename, dirname, mkstemp, open, writefile, popen } from 'fs';
5
6 function assert(cond, message) {
7 if (!cond) {
8 warn(message, "\n");
9 exit(1);
10 }
11
12 return true;
13 }
14
15 let unet_tool = "unet-tool";
16 let script_dir = sourcepath(0, true);
17
18 if (basename(script_dir) == "scripts") {
19 unet_tool = `${dirname(script_dir)}/unet-tool`;
20 assert(access(unet_tool, "x"), "unet-tool missing");
21 }
22
23 let args = {};
24
25 const defaults = {
26 port: 51830,
27 pex_port: 51831,
28 keepalive: 10,
29 };
30
31 const usage_message = `
32 Usage: ${basename(sourcepath())} [<flags>] <file> <command> [<args>] [<option>=<value> ...]
33
34 Commands:
35 - create: Create a new network file
36 - set-config: Change network config parameters
37 - add-host <name>: Add a host
38 - add-ssh-host <name> <host>: Add a remote OpenWrt host via SSH
39 (<host> can contain SSH options as well)
40 - set-host <name>: Change host settings
41 - set-ssh-host <name> <host>: Update local and remote host settings
42 - add-service <name>: Add a service
43 - set-service <name>: Change service settings
44 - sign Sign network data
45
46 Flags:
47 -p: Print modified JSON instead of updating file
48
49 Options:
50 - config options (create, set-config):
51 port=<val> set tunnel port (default: ${defaults.port})
52 pex_port=<val> set peer-exchange port (default: ${defaults.pex_port}, 0: disabled)
53 keepalive=<val> set keepalive interval (seconds, 0: off, default: ${defaults.keepalive})
54 stun=[+|-]<host:port>[,<host:port>...] set/add/remove STUN servers
55 - host options (add-host, add-ssh-host, set-host):
56 key=<val> set host public key (required for add-host)
57 port=<val> set host tunnel port number
58 pex_port=<val> set host peer-exchange port (default: network pex_port, 0: disabled)
59 groups=[+|-]<val>[,<val>...] set/add/remove groups that the host is a member of
60 ipaddr=[+|-]<val>[,<val>...] set/add/remove host ip addresses
61 subnet=[+|-]<val>[,<val>...] set/add/remove host announced subnets
62 endpoint=<val> set host endpoint address
63 gateway=<name> set host gateway (using name of other host)
64 - ssh host options (add-ssh-host, set-ssh-host)
65 auth_key=<key> use <key> as public auth key on the remote host
66 priv_key=<key> use <key> as private host key on the remote host (default: generate a new key)
67 interface=<name> use <name> as interface in /etc/config/network on the remote host
68 domain=<name> use <name> as hosts file domain on the remote host (default: unet)
69 connect=<val>[,<val>...] set IP addresses that the host will contact for network updates
70 tunnels=<ifname>:<service>[,...] set active tunnel devices
71 dht=0|1 set DHT enabled
72 - service options (add-service, set-service):
73 type=<val> set service type (required for add-service)
74 members=[+|-]<val>[,<val>...] set/add/remove service member hosts/groups
75 - vxlan service options (add-service, set-service):
76 id=<val> set VXLAN ID
77 port=<val> set VXLAN port
78 mtu=<val> set VXLAN device MTU
79 forward_ports=[+|-]<val>[,<val>...] set members allowed to receive broadcast/multicast/unknown-unicast
80 - sign options:
81 upload=<ip>[,<ip>...] upload signed file to hosts
82
83 `;
84
85 function usage() {
86 warn(usage_message);
87 return 1;
88 }
89
90 if (length(ARGV) < 2)
91 exit(usage());
92
93 let file = shift(ARGV);
94 let command = shift(ARGV);
95
96 const field_types = {
97 int: function(object, name, val) {
98 object[name] = int(val);
99 },
100 string: function(object, name, val) {
101 object[name] = val;
102 },
103 array: function(object, name, val) {
104 let op = substr(val, 0, 1);
105
106 if (op == "+" || op == "-") {
107 val = substr(val, 1);
108 object[name] ??= [];
109 } else {
110 op = "=";
111 object[name] = [];
112 }
113
114 let vals = split(val, ",");
115 for (val in vals) {
116 object[name] = filter(object[name], function(v) {
117 return v != val
118 });
119 if (op != "-")
120 push(object[name], val);
121 }
122
123 if (!length(object[name]))
124 delete object[name];
125 },
126 };
127
128 const service_field_types = {
129 vxlan: {
130 id: "int",
131 port: "int",
132 mtu: "int",
133 forward_ports: "array",
134 },
135 };
136
137 const ssh_script = `
138
139 set_list() {
140 local field="$1"
141 local val="$2"
142
143 first=1
144 for cur in $val; do
145 if [ -n "$first" ]; then
146 cmd=set
147 else
148 cmd=add_list
149 fi
150 uci $cmd "network.$INTERFACE.$field=$cur"
151 first=
152 done
153 }
154 set_interface_attrs() {
155 [ -n "$AUTH_KEY" ] && uci set "network.$INTERFACE.auth_key=$AUTH_KEY"
156 [ -n "$DHT" ] && uci set "network.$INTERFACE.dht=$DHT"
157 set_list connect "$CONNECT"
158 set_list tunnels "$TUNNELS"
159 uci set "network.$INTERFACE.domain=$DOMAIN"
160 }
161
162 check_interface() {
163 [ "$(uci -q get "network.$INTERFACE")" = "interface" -a "$(uci -q get "network.$INTERFACE.proto")" = "unet" ] && return 0
164 uci batch <<EOF
165 set network.$INTERFACE=interface
166 set network.$INTERFACE.proto=unet
167 set network.$INTERFACE.device=$INTERFACE
168 EOF
169 }
170
171 check_interface_key() {
172 key="$(uci -q get "network.$INTERFACE.key" | unet-tool -q -H -K -)"
173 [ -n "$key" ] || {
174 uci set "network.$INTERFACE.key=$(unet-tool -G)"
175 key="$(uci get "network.$INTERFACE.key" | unet-tool -H -K -)"
176 }
177 echo "key=$key"
178 }
179
180 check_interface
181 check_interface_key
182 set_interface_attrs
183 uci commit
184 reload_config
185 ifup $INTERFACE
186 `;
187
188 let print_only = false;
189
190 function fetch_args() {
191 for (let arg in ARGV) {
192 let vals = match(arg, /^(.[[:alnum:]_-]*)=(.*)$/);
193 assert(vals, `Invalid argument: ${arg}`);
194 args[vals[1]] = vals[2]
195 }
196 }
197
198 function set_field(typename, object, name, val) {
199 if (!field_types[typename]) {
200 warn(`Invalid type ${type}\n`);
201 return;
202 }
203
204 if (type(val) != "string")
205 return;
206
207 if (val == "") {
208 delete object[name];
209 return;
210 }
211
212 field_types[typename](object, name, val);
213 }
214
215 function set_fields(object, list) {
216 for (let f in list)
217 set_field(list[f], object, f, args[f]);
218 }
219
220 function set_host(host) {
221 set_fields(host, {
222 key: "string",
223 endpoint: "string",
224 gateway: "string",
225 port: "int",
226 ipaddr: "array",
227 subnet: "array",
228 groups: "array",
229 });
230 set_field("int", host, "peer-exchange-port", args.pex_port);
231 }
232
233 function set_service(service) {
234 set_fields(service, {
235 type: "string",
236 members: "array",
237 });
238
239 if (service_field_types[service.type])
240 set_fields(service.config, service_field_types[service.type]);
241 }
242
243 function sync_ssh_host(host) {
244 let interface = args.interface ?? "unet";
245 let connect = replace(args.connect ?? "", ",", " ");
246 let auth_key = args.auth_key;
247 let tunnels = replace(replace(args.tunnels ?? "", ",", " "), ":", "=");
248 let domain = args.domain ?? "unet";
249 let dht;
250
251 if (args.dht == "1" || args.dht == "0")
252 dht = args.dht;
253 else
254 dht = "";
255
256 if (!auth_key) {
257 let fh = mkstemp();
258 system(`${unet_tool} -q -P -K ${file}.key >&${fh.fileno()}`);
259 fh.seek();
260 auth_key = fh.read("line");
261 fh.close();
262 auth_key = replace(auth_key, "\n", "");
263 if (auth_key == "") {
264 warn("Could not read auth key\n");
265 exit(1);
266 }
267 }
268
269 let fh = mkstemp();
270 fh.write(`INTERFACE='${interface}'\n`);
271 fh.write(`CONNECT='${connect}'\n`);
272 fh.write(`AUTH_KEY='${auth_key}'\n`);
273 fh.write(`TUNNELS='${tunnels}'\n`);
274 fh.write(`DOMAIN='${domain}'\n`);
275 fh.write(`DHT='${dht}'\n`);
276 fh.write(ssh_script);
277 fh.flush();
278 fh.seek();
279
280 let fh2 = mkstemp();
281 system(`ssh ${host} sh <&${fh.fileno()} >&${fh2.fileno()}`);
282 fh.close();
283
284 let data = {}, line;
285
286 fh2.seek();
287 while (line = fh2.read("line")) {
288 let vals = match(line, /^(.[[:alnum:]_-]*)=(.*)\n$/);
289 assert(vals, `Invalid argument: ${line}`);
290 data[vals[1]] = vals[2]
291 }
292 fh2.close();
293
294 assert(data.key, "Could not read host key from SSH host");
295
296 args.key = data.key;
297 }
298
299 while (substr(ARGV[0], 0, 1) == "-") {
300 let opt = shift(ARGV);
301 if (opt == "--")
302 break;
303 else if (opt == "-p")
304 print_only = true;
305 else
306 exit(usage());
307 }
308
309 let hostname, ssh_host, servicename;
310
311 if (command in [ "add-host", "set-host", "add-ssh-host", "set-ssh-host" ]) {
312 hostname = shift(ARGV);
313 assert(hostname, "Missing host name argument");
314 }
315
316 if (command in [ "add-ssh-host", "set-ssh-host" ]) {
317 ssh_host = shift(ARGV);
318 assert(ssh_host, "Missing SSH host/user argument");
319 }
320
321 if (command in [ "add-service", "set-service" ]) {
322 servicename = shift(ARGV);
323 assert(servicename, "Missing service name argument");
324 }
325
326 fetch_args();
327
328 if (command in [ "add-ssh-host", "set-ssh-host" ]) {
329 sync_ssh_host(ssh_host);
330 command = replace(command, "ssh-", "");
331 }
332
333 let net_data;
334
335 if (command == "create") {
336 net_data = {
337 config: {},
338 hosts: {},
339 services: {}
340 };
341 } else {
342 let fh = open(file);
343 assert(fh, `Could not open input file ${file}`);
344
345 try {
346 net_data = json(fh);
347 } catch(e) {
348 assert(false, `Could not parse input file ${file}`);
349 }
350 }
351
352 if (command == "create") {
353 for (let key, val in defaults)
354 args[key] ??= `${val}`;
355 if (!access(`${file}.key`))
356 system(`${unet_tool} -G > ${file}.key`);
357 net_data.config.id = trim(popen(`unet-tool -P -K ${file}.key`).read("all"));
358 }
359
360 if (command == "sign") {
361 let ret = system(`${unet_tool} -S -K ${file}.key -o ${file}.bin ${file}`);
362 if (ret != 0)
363 exit(ret);
364
365 if (args.upload) {
366 for (let host in split(args.upload, ",")) {
367 warn(`Uploading ${file}.bin to ${host}\n`);
368 ret = system(`${unet_tool} -U ${host} -K ${file}.key ${file}.bin`);
369 if (ret)
370 warn("Upload failed\n");
371 }
372 }
373 exit(0);
374 }
375
376 switch (command) {
377 case 'create':
378 case 'set-config':
379 set_fields(net_data.config, {
380 port: "int",
381 keepalive: "int",
382 });
383 set_field("int", net_data.config, "peer-exchange-port", args.pex_port);
384 set_field("array", net_data.config, "stun-servers", args.stun);
385 break;
386
387 case 'add-host':
388 net_data.hosts[hostname] = {};
389 assert(args.key, "Missing host key");
390 set_host(net_data.hosts[hostname]);
391 break;
392
393 case 'set-host':
394 assert(net_data.hosts[hostname], `Host '${hostname}' does not exist`);
395 set_host(net_data.hosts[hostname]);
396 break;
397
398 case 'add-service':
399 net_data.services[servicename] = {
400 config: {},
401 members: [],
402 };
403 assert(args.type, "Missing service type");
404 set_service(net_data.services[servicename]);
405 break;
406
407 case 'set-service':
408 assert(net_data.services[servicename], `Service '${servicename}' does not exist`);
409 set_service(net_data.services[servicename]);
410 break;
411
412 default:
413 assert(false, "Unknown command");
414 }
415
416 const net_data_json = sprintf("%.J\n", net_data);
417
418 if (print_only)
419 print(net_data_json);
420 else
421 writefile(file, net_data_json);