5 import { access, basename, dirname, mkstemp, open, writefile } from 'fs';
7 function assert(cond, message) {
16 let unet_tool = "unet-tool";
17 let script_dir = sourcepath(0, true);
19 if (basename(script_dir) == "scripts") {
20 unet_tool = `${dirname(script_dir)}/unet-tool`;
21 assert(access(unet_tool, "x"), "unet-tool missing");
32 const usage_message = `
33 Usage: ${basename(sourcepath())} [<flags>] <file> <command> [<args>] [<option>=<value> ...]
36 - create: Create a new network file
37 - set-config: Change network config parameters
38 - add-host <name>: Add a host
39 - add-ssh-host <name> <host>: Add a remote OpenWrt host via SSH
40 (<host> can contain SSH options as well)
41 - set-host <name>: Change host settings
42 - set-ssh-host <name> <host>: Update local and remote host settings
43 - add-service <name>: Add a service
44 - set-service <name>: Change service settings
45 - sign Sign network data
48 -p: Print modified JSON instead of updating file
51 - config options (create, set-config):
52 port=<val> set tunnel port (default: ${defaults.port})
53 pex_port=<val> set peer-exchange port (default: ${defaults.pex_port}, 0: disabled)
54 keepalive=<val> set keepalive interval (seconds, 0: off, default: ${defaults.keepalive})
55 stun=[+|-]<host:port>[,<host:port>...] set/add/remove STUN servers
56 - host options (add-host, add-ssh-host, set-host):
57 key=<val> set host public key (required for add-host)
58 port=<val> set host tunnel port number
59 pex_port=<val> set host peer-exchange port (default: network pex_port, 0: disabled)
60 groups=[+|-]<val>[,<val>...] set/add/remove groups that the host is a member of
61 ipaddr=[+|-]<val>[,<val>...] set/add/remove host ip addresses
62 subnet=[+|-]<val>[,<val>...] set/add/remove host announced subnets
63 endpoint=<val> set host endpoint address
64 gateway=<name> set host gateway (using name of other host)
65 - ssh host options (add-ssh-host, set-ssh-host)
66 auth_key=<key> use <key> as public auth key on the remote host
67 priv_key=<key> use <key> as private host key on the remote host (default: generate a new key)
68 interface=<name> use <name> as interface in /etc/config/network on the remote host
69 domain=<name> use <name> as hosts file domain on the remote host (default: unet)
70 connect=<val>[,<val>...] set IP addresses that the host will contact for network updates
71 tunnels=<ifname>:<service>[,...] set active tunnel devices
72 dht=0|1 set DHT enabled
73 - service options (add-service, set-service):
74 type=<val> set service type (required for add-service)
75 members=[+|-]<val>[,<val>...] set/add/remove service member hosts/groups
76 - vxlan service options (add-service, set-service):
78 port=<val> set VXLAN port
79 mtu=<val> set VXLAN device MTU
80 forward_ports=[+|-]<val>[,<val>...] set members allowed to receive broadcast/multicast/unknown-unicast
82 upload=<ip>[,<ip>...] upload signed file to hosts
94 let file = shift(ARGV);
95 let command = shift(ARGV);
98 int: function(object, name, val) {
99 object[name] = int(val);
101 string: function(object, name, val) {
104 array: function(object, name, val) {
105 let op = substr(val, 0, 1);
107 if (op == "+" || op == "-") {
108 val = substr(val, 1);
115 let vals = split(val, ",");
117 object[name] = filter(object[name], function(v) {
121 push(object[name], val);
124 if (!length(object[name]))
129 const service_field_types = {
134 forward_ports: "array",
146 if [ -n "$first" ]; then
151 uci $cmd "network.$INTERFACE.$field=$cur"
155 set_interface_attrs() {
156 [ -n "$AUTH_KEY" ] && uci set "network.$INTERFACE.auth_key=$AUTH_KEY"
157 [ -n "$DHT" ] && uci set "network.$INTERFACE.dht=$DHT"
158 set_list connect "$CONNECT"
159 set_list tunnels "$TUNNELS"
160 uci set "network.$INTERFACE.domain=$DOMAIN"
164 [ "$(uci -q get "network.$INTERFACE")" = "interface" -a "$(uci -q get "network.$INTERFACE.proto")" = "unet" ] && return 0
166 set network.$INTERFACE=interface
167 set network.$INTERFACE.proto=unet
168 set network.$INTERFACE.device=$INTERFACE
172 check_interface_key() {
173 key="$(uci -q get "network.$INTERFACE.key" | unet-tool -q -H -K -)"
175 uci set "network.$INTERFACE.key=$(unet-tool -G)"
176 key="$(uci get "network.$INTERFACE.key" | unet-tool -H -K -)"
189 let print_only = false;
191 function fetch_args() {
192 for (let arg in ARGV) {
193 let vals = match(arg, /^(.[[:alnum:]_-]*)=(.*)$/);
194 assert(vals, `Invalid argument: ${arg}`);
195 args[vals[1]] = vals[2]
199 function set_field(typename, object, name, val) {
200 if (!field_types[typename]) {
201 warn(`Invalid type ${type}\n`);
205 if (type(val) != "string")
213 field_types[typename](object, name, val);
216 function set_fields(object, list) {
218 set_field(list[f], object, f, args[f]);
221 function set_host(host) {
231 set_field("int", host, "peer-exchange-port", args.pex_port);
234 function set_service(service) {
235 set_fields(service, {
240 if (service_field_types[service.type])
241 set_fields(service.config, service_field_types[service.type]);
244 function sync_ssh_host(host) {
245 let interface = args.interface ?? "unet";
246 let connect = replace(args.connect ?? "", ",", " ");
247 let auth_key = args.auth_key;
248 let tunnels = replace(replace(args.tunnels ?? "", ",", " "), ":", "=");
249 let domain = args.domain ?? "unet";
252 if (args.dht == "1" || args.dht == "0")
259 system(`${unet_tool} -q -P -K ${file}.key >&${fh.fileno()}`);
261 auth_key = fh.read("line");
263 auth_key = replace(auth_key, "\n", "");
264 if (auth_key == "") {
265 warn("Could not read auth key\n");
271 fh.write(`INTERFACE='${interface}'\n`);
272 fh.write(`CONNECT='${connect}'\n`);
273 fh.write(`AUTH_KEY='${auth_key}'\n`);
274 fh.write(`TUNNELS='${tunnels}'\n`);
275 fh.write(`DOMAIN='${domain}'\n`);
276 fh.write(`DHT='${dht}'\n`);
277 fh.write(ssh_script);
282 system(`ssh ${host} sh <&${fh.fileno()} >&${fh2.fileno()}`);
288 while (line = fh2.read("line")) {
289 let vals = match(line, /^(.[[:alnum:]_-]*)=(.*)\n$/);
290 assert(vals, `Invalid argument: ${line}`);
291 data[vals[1]] = vals[2]
295 assert(data.key, "Could not read host key from SSH host");
300 while (substr(ARGV[0], 0, 1) == "-") {
301 let opt = shift(ARGV);
304 else if (opt == "-p")
310 let hostname, ssh_host, servicename;
312 if (command in [ "add-host", "set-host", "add-ssh-host", "set-ssh-host" ]) {
313 hostname = shift(ARGV);
314 assert(hostname, "Missing host name argument");
317 if (command in [ "add-ssh-host", "set-ssh-host" ]) {
318 ssh_host = shift(ARGV);
319 assert(ssh_host, "Missing SSH host/user argument");
322 if (command in [ "add-service", "set-service" ]) {
323 servicename = shift(ARGV);
324 assert(servicename, "Missing service name argument");
329 if (command in [ "add-ssh-host", "set-ssh-host" ]) {
330 sync_ssh_host(ssh_host);
331 command = replace(command, "ssh-", "");
336 if (command == "create") {
344 assert(fh, `Could not open input file ${file}`);
349 assert(false, `Could not parse input file ${file}`);
353 if (command == "create") {
354 for (let key, val in defaults)
355 args[key] ??= `${val}`;
356 if (!access(`${file}.key`))
357 system(`${unet_tool} -G > ${file}.key`);
360 if (command == "sign") {
361 let ret = system(`${unet_tool} -S -K ${file}.key -o ${file}.bin ${file}`);
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`);
370 warn("Upload failed\n");
379 set_fields(net_data.config, {
383 set_field("int", net_data.config, "peer-exchange-port", args.pex_port);
384 set_field("array", net_data.config, "stun-servers", args.stun);
388 net_data.hosts[hostname] = {};
389 assert(args.key, "Missing host key");
390 set_host(net_data.hosts[hostname]);
394 assert(net_data.hosts[hostname], `Host '${hostname}' does not exist`);
395 set_host(net_data.hosts[hostname]);
399 net_data.services[servicename] = {
403 assert(args.type, "Missing service type");
404 set_service(net_data.services[servicename]);
408 assert(net_data.services[servicename], `Service '${servicename}' does not exist`);
409 set_service(net_data.services[servicename]);
413 assert(false, "Unknown command");
416 const net_data_json = sprintf("%.J\n", net_data);
419 print(net_data_json);
421 writefile(file, net_data_json);