add network json editor written in ucode
authorFelix Fietkau <nbd@nbd.name>
Mon, 22 Aug 2022 20:14:32 +0000 (22:14 +0200)
committerFelix Fietkau <nbd@nbd.name>
Tue, 23 Aug 2022 21:28:31 +0000 (23:28 +0200)
reformat example json to match its output

Signed-off-by: Felix Fietkau <nbd@nbd.name>
examples/net0.json
scripts/unet-cli [new file with mode: 0755]

index 1b969873c77fbeefa3ef7b4757f5f0c29a3c0b2d..3bd0a356658b0892f621277527c585a70ee493d6 100644 (file)
@@ -4,39 +4,58 @@
                "peer-exchange-port": 3458,
                "keepalive": 10
        },
-
        "hosts": {
                "master": {
                        "key": "25sPrbtEtIiANFr00tC5MS2UMfXmHFj/AJyDi4wR8n4=",
                        "endpoint": "192.168.1.3",
-                       "subnet": [ "192.168.3.0/24" ],
-                       "ipaddr": [ "192.168.3.1" ]
+                       "subnet": [
+                               "192.168.3.0/24"
+                       ],
+                       "ipaddr": [
+                               "192.168.3.1"
+                       ]
                },
                "ap1": {
                        "key": "mxQQxpwinlDxy0bp564b25il1oDiaf/a8jkaKQBcjw4=",
-                       "groups": [ "ap" ],
-                       "subnet": [ "192.168.4.0/24" ],
-                       "ipaddr": [ "192.168.4.1" ],
+                       "groups": [
+                               "ap"
+                       ],
+                       "subnet": [
+                               "192.168.4.0/24"
+                       ],
+                       "ipaddr": [
+                               "192.168.4.1"
+                       ],
                        "port": 3457
                },
                "ap2": {
                        "key": "+hiP+1FZci9Hp44gWEPigbsMHMe6De7nnMbVDJFhDjU=",
-                       "groups": [ "ap" ],
-                       "subnet": [ "192.168.5.0/24" ],
-                       "ipaddr": [ "192.168.5.1" ],
+                       "groups": [
+                               "ap"
+                       ],
+                       "subnet": [
+                               "192.168.5.0/24"
+                       ],
+                       "ipaddr": [
+                               "192.168.5.1"
+                       ],
                        "port": 3457
                }
        },
-
        "services": {
                "l2-tunnel": {
                        "type": "vxlan",
                        "config": {
                        },
-                       "members": [ "master", "@ap" ]
+                       "members": [
+                               "master",
+                               "@ap"
+                       ]
                },
                "usteer": {
-                       "members": [ "@ap" ]
+                       "members": [
+                               "@ap"
+                       ]
                }
        }
 }
diff --git a/scripts/unet-cli b/scripts/unet-cli
new file mode 100755 (executable)
index 0000000..35dc233
--- /dev/null
@@ -0,0 +1,414 @@
+#!/usr/bin/env ucode
+
+let fs = require("fs");
+
+let script_dir = sourcepath(0, true);
+if (fs.basename(script_dir) == "scripts") {
+       unet_tool = fs.dirname(script_dir) + "/unet-tool";
+       if (!fs.access(unet_tool, "x")) {
+               warn("unet-tool missing\n");
+               exit(1);
+       }
+} else {
+       unet_tool = "unet-tool";
+}
+
+args = {};
+
+defaults = {
+       port: 51830,
+       pex_port: 51831,
+       keepalive: 10,
+};
+
+function usage() {
+       warn("Usage: ",fs.basename(sourcepath())," [<flags>] <file> <command> [<args>] [<option>=<value> ...]\n",
+            "\n",
+            "Commands:\n",
+            " - create:                                        Create a new network file\n",
+            " - set-config:                                    Change network config parameters\n",
+            " - add-host <name>:                               Add a host\n",
+            " - add-ssh-host <name> <host>:                    Add a remote OpenWrt host via SSH\n",
+            "                                          (<host> can contain SSH options as well)\n",
+            " - set-host <name>:                               Change host settings\n",
+            " - set-ssh-host <name> <host>:                    Update local and remote host settings\n",
+            " - add-service <name>:                            Add a service\n",
+            " - set-service <name>:                            Change service settings\n",
+            " - sign                                           Sign network data\n",
+            "\n",
+            "Flags:\n",
+            " -p:                                              Print modified JSON instead of updating file\n",
+            "\n",
+            "Options:\n",
+            " - config options (create, set-config):\n",
+            "  port=<val>                              set tunnel port (default: ", defaults.port, ")\n",
+            "  pex_port=<val>                          set peer-exchange port (default: ", defaults.pex_port, ")\n",
+            "  keepalive=<val>                         set keepalive interval (seconds, 0: off, default: ", defaults.keepalive,")\n",
+            " host options (add-host, add-ssh-host, set-host):\n",
+            "  key=<val>                               set host public key (required for add-host)\n",
+            "  port=<val>                              set host tunnel port number\n",
+            "  groups=[+|-]<val>[,<val>...]            set/add/remove groups that the host is a member of\n",
+            "  ipaddr=[+|-]<val>[,<val>...]            set/add/remove host ip addresses\n",
+            "  subnet=[+|-]<val>[,<val>...]            set/add/remove host announced subnets\n",
+            "  endpoint=<val>                          set host endpoint address\n",
+            " ssh host options (add-ssh-host, set-ssh-host)\n",
+            "  auth_key=<key>                          use <key> as public auth key on the remote host\n",
+            "  priv_key=<key>                          use <key> as private host key on the remote host (default: generate a new key)\n",
+            "  interface=<name>                        use <name> as interface in /etc/config/network on the remote host\n",
+            "  connect=<val>[,<val>...]                set IP addresses that the host will contact for network updates\n",
+            "  tunnels=<ifname>:<service>[,...]        set active tunnel devices\n",
+            " service options (add-service, set-service):\n",
+            "  type=<val>                              set service type (required for add-service)\n",
+            "  members=[+|-]<val>[,<val>...]           set/add/remove service member hosts/groups\n",
+            " vxlan service options (add-service, set-service):\n",
+            "  id=<val>                                set VXLAN ID\n",
+            "  port=<val>                              set VXLAN port\n",
+            "  mtu=<val>                               set VXLAN device MTU\n",
+            "  forward_ports=[+|-]<val>[,<val>...]     set members allowed to receive broadcast/multicast/unknown-unicast\n",
+            " sign options:\n",
+            "  upload=<ip>[,<ip>...]                   upload signed file to hosts\n",
+            "\n");
+       return 1;
+}
+
+if (length(ARGV) < 2)
+       exit(usage());
+
+file = shift(ARGV);
+command = shift(ARGV);
+
+field_types = {
+       int: function(object, name, val) {
+               object[name] = int(val);
+       },
+       string: function(object, name, val) {
+               object[name] = val;
+       },
+       array: function(object, name, val) {
+               let op = substr(val, 0, 1);
+
+               if (op == "+" || op == "-") {
+                       val = substr(val, 1);
+                       object[name] ??= [];
+               } else {
+                       op = "=";
+                       object[name] = [];
+               }
+
+               let vals = split(val, ",");
+               for (val in vals) {
+                       object[name] = filter(object[name], function(v) {
+                               return v != val
+                       });
+                       if (op != "-")
+                               push(object[name], val);
+               }
+
+               if (!length(object[name]))
+                       delete object[name];
+       },
+};
+
+service_field_types = {
+       vxlan: {
+               id: "int",
+               port: "int",
+               mtu: "int",
+               forward_ports: "array",
+       },
+};
+
+ssh_script = '
+
+set_list() {
+       local field="$1"
+       local val="$2"
+
+       first=1
+       for cur in $val; do
+               if [ -n "$first" ]; then
+                       cmd=set
+               else
+                       cmd=add_list
+               fi
+               uci $cmd "network.$INTERFACE.$field=$cur"
+               first=
+       done
+}
+set_interface_attrs() {
+       [ -n "$AUTH_KEY" ] && uci set "network.$INTERFACE.auth_key=$AUTH_KEY"
+       set_list connect "$CONNECT"
+       set_list tunnels "$TUNNELS"
+}
+
+check_interface() {
+       [ "$(uci -q get "network.$INTERFACE")" = "interface" -a "$(uci -q get "network.$INTERFACE.proto")" = "unet" ] && return 0
+       uci batch <<EOF
+set network.$INTERFACE=interface
+set network.$INTERFACE.proto=unet
+set network.$INTERFACE.device=$INTERFACE
+set network.$INTERFACE.domain=unet
+EOF
+}
+
+check_interface_key() {
+       key="$(uci -q get "network.$INTERFACE.key" | unet-tool -q -H -K -)"
+       [ -n "$key" ] || {
+               uci set "network.$INTERFACE.key=$(unet-tool -G)"
+               key="$(uci get "network.$INTERFACE.key" | unet-tool -H -K -)"
+       }
+       echo "key=$key"
+}
+
+check_interface
+check_interface_key
+set_interface_attrs
+uci commit
+reload_config
+';
+
+args = {};
+print_only = false;
+
+function fetch_args() {
+       for (arg in ARGV) {
+               vals = match(arg, /^(.[[:alnum:]_-]*)=(.*)$/);
+               if (!vals) {
+                       warn("Invalid argument: ", arg, "\n");
+                       exit(1);
+               }
+               args[vals[1]] = vals[2]
+       }
+}
+
+function set_field(typename, object, name, val) {
+       if (!field_types[typename]) {
+               warn("Invalid type ", type, "\n");
+               return;
+       }
+
+       if (type(val) != "string")
+               return;
+
+       if (val == "") {
+               delete object[name];
+               return;
+       }
+
+       field_types[typename](object, name, val);
+}
+
+function set_fields(object, list) {
+       for (f in list)
+               set_field(list[f], object, f, args[f]);
+}
+
+function set_host(name) {
+       let host = net_data.hosts[name];
+
+       set_fields(host, {
+               key: "string",
+               endpoint: "string",
+               port: "int",
+               ipaddr: "array",
+               subnet: "array",
+               groups: "array",
+       });
+}
+
+function set_service(name) {
+       let service = net_data.services[name];
+
+       set_fields(service, {
+               type: "string",
+               members: "array",
+       });
+
+       if (service_field_types[service.type])
+               set_fields(service.config, service_field_types[service.type]);
+}
+
+function sync_ssh_host(host) {
+       let interface = args.interface ?? "unet";
+       let connect = replace(args.connect ?? "", ",", " ");
+       let auth_key = args.auth_key;
+       let tunnels = replace(replace(args.tunnels ?? "", ",", " "), ":", "=");
+
+       if (!auth_key) {
+               let fh = fs.mkstemp();
+               system(unet_tool + " -q -P -K " + file + ".key >&" + fh.fileno());
+               fh.seek();
+               auth_key = fh.read("line");
+               fh.close();
+               auth_key = replace(auth_key, "\n", "");
+               if (auth_key == "") {
+                       warn("Could not read auth key\n");
+                       exit(1);
+               }
+       }
+
+       let fh = fs.mkstemp();
+       fh.write("INTERFACE='" + interface + "'\n");
+       fh.write("CONNECT='" + connect + "'\n");
+       fh.write("AUTH_KEY='" + auth_key + "'\n");
+       fh.write("TUNNELS='" + tunnels + "'\n");
+       fh.write(ssh_script);
+       fh.flush();
+       fh.seek();
+
+       fh2 = fs.mkstemp();
+       system(sprintf("ssh "+host+" sh <&%d >&%d", fh.fileno(), fh2.fileno()));
+       fh.close();
+
+       data = {};
+
+       fh2.seek();
+       while (line = fh2.read("line")) {
+               let vals = match(line, /^(.[[:alnum:]_-]*)=(.*)\n$/);
+               if (!vals) {
+                       warn("Invalid argument: ", arg, "\n");
+                       exit(1);
+               }
+               data[vals[1]] = vals[2]
+       }
+       fh2.close();
+
+       if (!data.key) {
+               warn("Could not read host key from SSH host\n");
+               exit(1);
+       }
+
+       args.key = data.key;
+}
+
+while (substr(ARGV[0], 0, 1) == "-") {
+       opt = shift(ARGV);
+       if (opt == "--")
+               break;
+       else if (opt == "-p")
+               print_only = true;
+       else
+               exit(usage());
+}
+
+if (command == "add-host" || command == "set-host" ||
+    command == "add-ssh-host" || command == "set-ssh-host") {
+       hostname = shift(ARGV);
+       if (!hostname) {
+               warn("Missing host name argument\n");
+               exit(1);
+       }
+}
+
+if (command == "add-ssh-host" || command == "set-ssh-host") {
+       ssh_host = shift(ARGV);
+       if (!ssh_host) {
+               warn("Missing SSH host/user argument\n");
+               exit(1);
+       }
+}
+
+if (command == "add-service" || command == "set-service") {
+       servicename = shift(ARGV);
+       if (!servicename) {
+               warn("Missing service name argument\n");
+               exit(1);
+       }
+}
+
+fetch_args();
+
+if (command == "add-ssh-host" || command == "set-ssh-host") {
+       sync_ssh_host(ssh_host);
+       command = replace(command, "ssh-", "");
+}
+
+if (command == "create") {
+       net_data = {
+               config: {},
+               hosts: {},
+               services: {}
+       };
+} else {
+       fh = fs.open(file);
+       if (!fh) {
+               warn("Could not open input file ", file, "\n");
+               exit(1);
+       }
+       try {
+               net_data = json(fh);
+       } catch(e) {
+               warn("Could not parse input file ", file, "\n");
+               exit(1);
+       }
+}
+
+if (command == "create") {
+       for (key in keys(defaults))
+               args[key] ??= "" + defaults[key];
+       if (!fs.access(file + ".key"))
+               system(unet_tool + " -G > " + file + ".key");
+}
+
+if (command == "sign") {
+       ret = system(unet_tool + " -S -K " + file + ".key -o " + file + ".bin " + file);
+       if (ret != 0)
+               exit(ret);
+
+       if (args.upload) {
+               hosts = split(args.upload, ",");
+               for (host in hosts) {
+                       warn("Uploading " + file + ".bin to " + host + "\n");
+                       ret = system(unet_tool + " -U " + host + " -K "+ file + ".key " + file + ".bin");
+                       if (ret)
+                               warn("Upload failed\n");
+               }
+       }
+       exit(0);
+}
+
+if (command == "create" || command == "set-config") {
+       set_fields(net_data.config, {
+               port: "int",
+               keepalive: "int",
+       });
+       set_field("int", net_data.config, "peer-exchange-port", args.pex_port);
+} else if (command == "add-host") {
+       net_data.hosts[hostname] = {};
+       if (!args.key) {
+               warn("Missing host key\n");
+               exit(1);
+       }
+       set_host(hostname);
+} else if (command == "set-host") {
+       if (!net_data.hosts[hostname]) {
+               warn("Host '", hostname, "' does not exist\n");
+               exit(1);
+       }
+       set_host(hostname);
+} else if (command == "add-service") {
+       net_data.services[servicename] = {
+               config: {},
+               members: [],
+       };
+       if (!args.type) {
+               warn("Missing service type\n");
+               exit(1);
+       }
+       set_service(servicename);
+} else if (command == "set-service") {
+       if (!net_data.services[servicename]) {
+               warn("Service '", servicename, "' does not exist\n");
+               exit(1);
+       }
+       set_service(servicename);
+} else {
+       warn("Unknown command\n");
+       exit(1);
+}
+
+net_data_json = sprintf("%.J\n", net_data);
+if (print_only)
+       print(net_data_json);
+else
+       fs.writefile(file, net_data_json);