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