From: Felix Fietkau Date: Wed, 12 Feb 2025 19:01:09 +0000 (+0100) Subject: unetd: add cli module X-Git-Url: http://git.lede-project.org./?a=commitdiff_plain;h=f7c5a2b7d4e0e10fe7b6f49a56ac6ca94a7deed4;p=openwrt%2Fstaging%2Fstintel.git unetd: add cli module This vastly simplifies creating and managing unet networks. It also adds support for the unetd protocol for onboarding new nodes over the network. Signed-off-by: Felix Fietkau --- diff --git a/package/network/services/unetd/Makefile b/package/network/services/unetd/Makefile index 6923c68873..94ae13dc48 100644 --- a/package/network/services/unetd/Makefile +++ b/package/network/services/unetd/Makefile @@ -80,6 +80,7 @@ endef define Package/unetd/install $(INSTALL_DIR) \ + $(1)/usr/share/ucode/cli/modules \ $(1)/etc/unetd \ $(1)/lib/bpf \ $(1)/etc/init.d \ @@ -92,6 +93,7 @@ define Package/unetd/install $(PKG_INSTALL_DIR)/usr/sbin/unet-tool \ $(1)/usr/sbin/ $(if $(CONFIG_UNETD_VXLAN_SUPPORT),$(INSTALL_DATA) $(PKG_BUILD_DIR)/mss-bpf.o $(1)/lib/bpf/mss.o) + $(INSTALL_DATA) ./files/unet.uc $(1)/usr/share/ucode/cli/modules $(INSTALL_BIN) ./files/unetd.init $(1)/etc/init.d/unetd $(INSTALL_BIN) ./files/unetd.sh $(1)/lib/netifd/proto endef diff --git a/package/network/services/unetd/files/unet.uc b/package/network/services/unetd/files/unet.uc new file mode 100644 index 0000000000..b884e23f60 --- /dev/null +++ b/package/network/services/unetd/files/unet.uc @@ -0,0 +1,1226 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2025 Felix Fietkau +'use strict'; + +import { readfile, writefile, mkstemp, mkdir, unlink } from "fs"; +import { time_format } from "cli.utils"; +import * as editor from "cli.object-editor"; +import * as rtnl from "rtnl"; +import * as uci from "uci"; + +const supported_service_types = [ + "vxlan", "uconfig", "unetacl", +]; + +function get_networks() +{ + let ret = []; + + uci.cursor().foreach("network", "interface", (s) => { + if (s.proto != "unet") + return; + push(ret, s[".name"]); + }); + + return ret; +} + +function get_network_status() +{ + let data = model.ubus.call("unetd", "network_get"); + if (!data) + return {}; + + return data.networks; +} + +function network_get_string_file(str) +{ + let f = mkstemp(); + f.write(str); + f.flush(); + return f; +} + +function network_get_file_string(f) +{ + f.seek(); + let str = trim(f.read("all")); + f.close(); + return str; +} + +function __network_get_pubkey(pw_file, salt, rounds) +{ + pw_file.seek(); + + let pubkey_file = mkstemp(); + if (system(`unet-tool -P -s ${rounds},${salt} <&${pw_file.fileno()} >&${pubkey_file.fileno()}`)) + return ctx.command_failed("Failed to generate public key"); + + pubkey_file.seek(); + let pubkey = trim(pubkey_file.read("all")); + pubkey_file.close(); + + return pubkey; +} + +function network_get_pubkey(pw_file, network) +{ + return __network_get_pubkey(pw_file, network.config.salt, network.config.rounds); +} + +function __network_fetch_password(ctx, named, confirm) +{ + if (named.password) + return true; + + if (!model.cb.getpass) { + if (ctx.invalid_argument) + ctx.invalid_argument("Could not get network config password"); + return; + } + + let pw = model.cb.getpass("Network config password: "); + if (length(pw) < 12) { + if (ctx.invalid_argument) + ctx.invalid_argument("Password must be at least 12 characters long"); + return; + } + + if (confirm) { + let pw2 = model.cb.getpass("Confirm config password: "); + if (pw != pw2) { + if (ctx.invalid_argument) + ctx.invalid_argument("Password mismatch"); + return; + } + } + + named.password = pw; + + return true; +} + +function network_fetch_password(ctx, named, confirm) +{ + if (ctx.data.netdata) + named.password ??= ctx.data.netdata.password; + + if (!__network_fetch_password(ctx, named, confirm)) + return; + + let pw_file = network_get_string_file(named.password); + + return pw_file; +} + +function network_sign_data(ctx, name, network, pw_file, upload) +{ + let rounds = network.config.rounds; + let salt = network.config.salt; + + mkdir("/etc/unetd", 0700); + let json_file = "/etc/unetd/" + name + ".json"; + let bin_file = "/etc/unetd/" + name + ".bin"; + if (upload) + bin_file += "." + time(); + writefile(json_file, sprintf("%.J\n", network)); + + pw_file.seek(); + let ret = system(`unet-tool -S -s ${rounds},${salt} -o "${bin_file}" "${json_file}" <&${pw_file.fileno()}`); + unlink(json_file); + if (ret) { + if (ctx.command_failed) + ctx.command_failed("Failed to sign network configuration"); + return false; + } + + if (!upload) + return true; + + ret = system(`unet-tool -U 127.0.0.1 "${bin_file}"`); + unlink(bin_file); + if (ret) { + if (ctx.command_failed) + ctx.command_failed("Failed to upload network configuration"); + return false; + } + + pw_file.close(); + return true; +} + +function network_create_uci(name, iface) +{ + let cur = uci.cursor(); + cur.set("network", name, "interface"); + for (let key, val in iface) + cur.set("network", name, key, val); + cur.commit(); + + system("reload_config"); +} + +const config_editor = { + change_cb: function(ctx, argv) { + ctx.data.netdata.changed = true; + }, + add: { + help: "Add configuration parameter value", + }, + set: { + help: "Set configuration parameters", + }, + remove: { + help: "Remove configuration parameter value", + }, + named_args: { + port: { + help: "wireguard port", + default: 51830, + required: true, + args: { + type: "int", + min: 1, + max: 65535, + } + }, + "unet-port": { + help: "unet protocol port", + default: 51831, + required: true, + attribute: "peer-exchange-port", + args: { + type: "int", + min: 1, + max: 65535, + } + }, + keepalive: { + help: "keepalive interval (seconds)", + default: 10, + args: { + type: "int", + min: 0, + } + }, + "stun-server": { + help: "STUN server", + multiple: true, + args: { + type: "host", + } + } + } +}; + +const UnetConfigEdit = editor.new(config_editor); + +const iface_editor = { + change_cb: function(ctx, argv) { + ctx.data.netdata.iface_changed = true; + }, + add: { + help: "Add interface parameter value", + }, + set: { + help: "Set interface parameters", + }, + remove: { + help: "Remove interface parameter value", + }, + named_args: { + metric: { + help: "Interface metric", + allow_empty: true, + default: 100, + args: { + type: "int", + } + }, + zone: { + help: "Firewall zone", + allow_empty: true, + default: "lan", + args: { + type: "string", + } + }, + domain: { + help: "Local DNS domain for unet hosts", + default: "unet", + allow_empty: true, + args: { + type: "host" + } + }, + "local-network": { + help: "Local network interface for discovering peers", + default: [ "lan" ], + attribute: "local_network", + allow_empty: true, + multiple: true, + args: { + type: "string", + } + }, + connect: { + help: "Connect to remote IP or broadcast address", + allow_empty: true, + multiple: true, + args: { + type: "string", + }, + }, + }, +}; + +const network_local_args = { + ...iface_editor.named_args, + network: { + help: "network name", + default: "unet", + required: true, + args: { + type: "string", + } + }, +}; + +const UnetIfaceEdit = editor.new(iface_editor); + +function network_create(ctx, argv, named) { + ctx.apply_defaults(); + + if (!named.network || index(named.network, "/") >= 0) + return ctx.error("Invalid network name: %s", named.network); + + let pw_file = network_fetch_password(ctx, named, true); + if (!pw_file) + return; + + let salt = readfile("/dev/urandom", 16); + if (length(salt) != 16) + return ctx.unknown_error(); + + salt = map(split(salt, ""), (v) => ord(v)); + salt = join("", map(salt, (v) => sprintf("%02x", v))); + let rounds = 10000; + + let network = { + config: { + salt, rounds, + }, + hosts: {}, + }; + for (let name, spec in config_editor.named_args) { + let val = named[name]; + if (val == null) + continue; + name = spec.attribute ?? name; + network.config[name] = val; + } + + let pubkey = network_get_pubkey(pw_file, network); + + let hostkey_file = mkstemp(); + if (system(`unet-tool -G >&${hostkey_file.fileno()}`)) + return ctx.command_failed("Failed to generate host key"); + + hostkey_file.seek(); + let host_pubkey_file = mkstemp(); + if (system(`unet-tool -H -K - <&${hostkey_file.fileno()} >&${host_pubkey_file.fileno()}`)) + return ctx.command_failed("Failed to generate host public key"); + + let host_key = network_get_file_string(hostkey_file); + let host_pubkey = network_get_file_string(host_pubkey_file); + network.config.id = pubkey; + + network.hosts[named.host] = { + key: host_pubkey, + }; + + if (!network_sign_data(ctx, named.network, network, pw_file)) + return; + + network_create_uci(named.network, { + proto: "unet", + metric: named.metric, + zone: named.zone, + domain: named.domain, + key: host_key, + auth_key: pubkey, + local_network: named["local-network"], + connect: named["connect"], + }); + + return ctx.ok("Created network "+ named.network); +} + +function network_delete(ctx, argv) { + let name = argv[0]; + let cur = uci.cursor(); + if (!cur.delete("network", name)) + return ctx.command_failed("Command failed"); + + cur.commit(); + system("reload_config"); + return ctx.ok("Network deleted"); +} + +function network_iface_save(ctx) +{ + let netdata = ctx.data.netdata; + let network = ctx.data.network; + let changed; + + if (!netdata.iface_changed) + return; + + let cur = uci.cursor(); + let iface_orig = cur.get_all("network", network); + for (let name, val in netdata.iface) { + if (iface_orig[name] == val) + continue; + + if (val == null) + cur.delete("network", network, name); + else + cur.set("network", network, name, val); + changed = true; + } + + if (changed) + cur.commit(); + + netdata.iface_changed = false; + + return changed; +} + +function network_apply(ctx, argv, named) +{ + let name = ctx.data.network; + let netdata = ctx.data.netdata; + let data = netdata.json; + + if (!netdata.changed) + return; + + let pw_file = network_fetch_password(ctx, named); + if (!pw_file) + return; + + let id = network_get_pubkey(pw_file, data); + if (id != data.config.id) { + pw_file.close(); + return ctx.invalid_argument("Invalid password"); + } + + if (!network_sign_data(ctx, name, data, pw_file, true)) + return; + + netdata.changed = false; + return true; +} + +function __network_enroll_cancel(model, ctx) +{ + let req = ctx.data.enroll; + if (!req) + return false; + + req.sub.remove(); + model.ubus.call("unetd", "enroll_stop"); + delete ctx.data.enroll; + return true; +} + +function network_enroll_accept(ctx, argv, named) +{ + let req = ctx.data.enroll; + let id = argv[0]; + if (!req || !id) + return ctx.invalid_argument(); + + let peer = req.peers[id]; + if (!peer) + return ctx.invalid_argument("Session not found: %s", id); + + model.ubus.call("unetd", "enroll_accept", { + session: id + }); + + return ctx.ok("Network peer accepted"); +} + +function network_handle_enroll_update(model, ctx, msg) +{ + let invite = ctx.data.enroll; + if (!invite) + return; + + let data = msg.data; + let peer = invite.peers[data.session]; + let ret; + + if (!peer) + model.status_msg("New device detected at " + data.address + ", session id " + data.session); + + peer ??= {}; + if (data.accepted && !peer.accepted) + model.status_msg("Accepted peer at " + data.address + ", session id " + data.session); + if (!data.accepted) + data.confirmed = false; + + if (data.confirmed && !peer.confirmed) { + model.status_msg("Confirmed peer at " + data.address + ", session id " + data.session); + ret = data; + } + + invite.peers[data.session] = data; + + return ret; +} + +function network_invite_peer_update(model, ctx, msg) +{ + let name = ctx.data.network; + let netdata = ctx.data.netdata; + let invite = ctx.data.enroll; + if (!invite) + return; + + let data = network_handle_enroll_update(model, ctx, msg); + if (!data) + return; + + netdata.json.hosts[invite.name] ??= {}; + netdata.json.hosts[invite.name].key = data.enroll_key; + netdata.changed = true; + + let pw_file = network_get_string_file(netdata.password); + if (network_sign_data(ctx, name, netdata.json, pw_file, true)) { + netdata.changed = false; + model.status_msg("Updated configuration"); + } + + __network_enroll_cancel(model, ctx); +} + +function network_invite(ctx, argv, named) +{ + let network = ctx.data.network; + let netdata = ctx.data.netdata; + let data = netdata.json; + + let pw_file = network_fetch_password(ctx, named); + if (!pw_file) + return; + + let id = network_get_pubkey(pw_file, data); + pw_file.close(); + if (id != data.config.id) + return ctx.invalid_argument("Invalid password"); + + netdata.password = named.password; + let invite = { + name: argv[0], + peers: {}, + }; + + invite.sub = model.ubus.subscriber((msg) => { + if (msg.type == "enroll_peer_update") + network_invite_peer_update(ctx.model, ctx, msg); + else if (msg.type == "enroll_timeout") + __network_enroll_cancel(ctx.model, ctx); + }); + + let req = { + network, + timeout: named.timeout, + }; + + if (named["access-key"]) { + req.enroll_secret = named["access-key"]; + req.enroll_auto = true; + } + + if (named.connect) + req.connect = named.connect; + + invite.sub.subscribe("unetd"); + model.ubus.call("unetd", "enroll_start", req); + ctx.data.enroll = invite; + + return ctx.ok("Invite started"); +} + +function network_join_peer_update(model, ctx, msg) +{ + let joinreq = ctx.data.enroll; + let name = joinreq.name; + + let data = network_handle_enroll_update(model, ctx, msg); + if (!data) + return; + + let iface = { + proto: "unet", + metric: joinreq.metric, + zone: joinreq.zone, + domain: joinreq.domain, + connect: joinreq.connect, + local_network: joinreq.local_network, + key: data.local_key, + auth_key: data.enroll_key, + }; + + if (joinreq.connect) + iface.connect = joinreq.connect; + + network_create_uci(name, iface); + + model.status_msg("Configuration added for interface " + name); + + __network_enroll_cancel(model, ctx); +} + +function resolve_network_broadcast_addr(list, net) +{ + let data = model.ubus.call("network.interface." + net, "status"); + if (!data) + return; + + let dev = data.l3_device; + if (!dev) + return; + + let req = rtnl.request(rtnl.const.RTM_GETADDR, rtnl.const.NLM_F_DUMP); + for (let addr in req) + if (addr.family == 2 && addr.dev == dev && addr.broadcast) + push(list, addr.broadcast); +} + +function network_join(ctx, argv, named) +{ + __network_enroll_cancel(model, ctx); + ctx.apply_defaults(); + + let data = { + name: named.network, + metric: named.metric, + zone: named.zone, + domain: named.domain, + connect: named.connect, + local_network: named["local-network"], + peers: {}, + }; + + let req = { + timeout: named.timeout, + }; + + if (named["access-key"]) { + req.enroll_secret = named["access-key"]; + req.enroll_auto = true; + } + + if (data.connect) + req.connect = [ ...data.connect ]; + if (length(data.local_network) > 0) { + req.connect ??= []; + for (let net in data.local_network) + resolve_network_broadcast_addr(req.connect, net); + } + + data.sub = model.ubus.subscriber((msg) => { + if (msg.type == "enroll_peer_update") + network_join_peer_update(ctx.model, ctx, msg); + else if (msg.type == "enroll_timeout") + __network_enroll_cancel(ctx.model, ctx); + }); + data.sub.subscribe("unetd"); + model.ubus.call("unetd", "enroll_start", req); + + ctx.data.enroll = data; + + return ctx.ok("Join request started"); +} + +function network_edit_exit_hook() +{ + let ctx = this; + let netdata = ctx.data.netdata; + + network_iface_save(ctx); + __network_enroll_cancel(model, ctx); + if (!netdata.changed) + return true; + + if (!model.cb.poll_key) + return true; + + let key = model.poll_key(['c', 'r', 'a'], `You have uncommitted changes. [a]pply, [r]evert or [c]ancel? `); + if (!key) + return true; + + switch (key) { + case 'c': + warn("cancel\n"); + return false; + case 'r': + warn("revert\n"); + return true; + case 'a': + warn("apply\n"); + break; + } + + let name = ctx.data.network; + let data = netdata.json; + + let pw_file = network_fetch_password(ctx, {}); + if (!pw_file) + return; + + let id = network_get_pubkey(pw_file, data); + if (id != data.config.id) { + warn("Invalid password\n"); + return false; + } + + if (!network_sign_data(ctx, name, data, pw_file, true)) { + warn("Failed to apply network configuration\n"); + return false; + } + + return true; +} + +function network_edit(ctx, argv) { + let network = argv[0]; + if (!network) { + network = "unet"; + if (!get_network_status()[network]) + return ctx.invalid_argument('no valid network name provided'); + } + + let iface_data = uci.cursor().get_all("network", network); + for (let name in keys(iface_data)) + if (substr(name, 0, 1) == ".") + delete iface_data[name]; + + let json_file = mkstemp(); + if (system(`unet-tool -T -b /etc/unetd/${network}.bin >&${json_file.fileno()}`)) + return; + + let json_data; + try { + json_data = network_get_file_string(json_file); + json_data = json(json_data); + } catch (e) { + json_data = null; + } + + if (!json_data) + return; + + let netdata = { + json: json_data, + iface: iface_data, + changed: false, + }; + + json_data.hosts ??= {}; + json_data.services ??= {}; + + ctx.add_hook("exit", network_edit_exit_hook); + + return ctx.set('edit "' + network + '"', { + network, netdata, + object_edit: json_data, + }); +} + +const network_args = [ + { + name: "network", + help: "Network name", + type: "enum", + value: () => get_networks(), + required: true, + } +]; + +const network_status_args = [ + { + name: "network", + help: "Network name", + type: "enum", + value: () => keys(get_network_status()) + } +]; + +const network_sign_args = { + password: { + help: "Network configuration password", + no_complete: true, + args: { + type: "string", + min: 12, + } + }, +}; + +const network_config_args = editor.object_create_params(UnetConfigEdit); + +const network_create_args = { + ...network_sign_args, + ...network_config_args, + ...network_local_args, + host: { + help: "local host name", + default: "main", + required: true, + args: { + type: "string", + } + }, +}; + +const network_invite_name_arg = [ + { + name: "name", + help: "Name of the invited device", + type: "string", + } +]; + +const network_enroll_args = { + "access-key": { + help: "Access key for allowing the device into the network", + args: { + type: "string", + } + }, + timeout: { + help: "Timeout for invite", + required: true, + default: 120, + args: { + type: "int", + } + }, +}; + +const enroll_accept_arg = [{ + name: "session_id", + help: "Session id of the network peer", + type: "string", + required: true, + type: "enum", + value: (ctx) => keys(ctx.data.enroll.peers), +}]; + +const network_join_args = { + ...network_enroll_args, + ...network_local_args, +}; + +const network_invite_args = { + ...network_enroll_args, + ...network_sign_args, +}; + +const host_editor = { + change_cb: function(ctx, argv) { + ctx.data.netdata.changed = true; + }, + named_args: { + name: { + help: "Host name", + get: (ctx) => ctx.data.name, + set: (ctx, val) => { + let name = ctx.data.name; + let hosts = ctx.data.netdata.json.hosts; + hosts[val] = hosts[name]; + delete hosts[name]; + ctx.data.name = val; + }, + change_only: true, + args: { + type: "string", + } + }, + key: { + help: "Wireguard key", + required: true, + args: { + type: "string", + } + }, + port: { + help: "Wireguard port", + args: { + type: "int", + min: 1, + max: 65535, + } + }, + "unet-port": { + help: "unet protocol port (0: wireguard only)", + args: { + type: "int", + min: 0, + max: 65535, + } + }, + endpoint: { + help: "Wireguard endpoint IP address", + args: { + type: "string", + } + }, + ipaddr: { + help: "IP address", + multiple: true, + args: { + type: "ipv4", + } + }, + subnet: { + help: "IP subnet", + multiple: true, + args: { + type: "cidr4", + } + }, + gateway: { + help: "Other host to be used as gateway", + args: { + type: "enum", + value: function(ctx, argv) { + return filter(keys(ctx.data.netdata.json.hosts), + (v) => v != ctx.data.name); + } + } + }, + group: { + help: "Host group membership", + attribute: "groups", + multiple: true, + args: { + type: "enum", + no_validate: true, + value: function(ctx) { + let groups = {}; + for (let name, host in ctx.data.netdata.json.hosts) + for (let group in host.groups) + groups[group] = true; + return keys(groups); + } + } + } + }, +}; + +const UnetHostEdit = editor.new(host_editor); + +function is_vxlan_service(ctx, argv, named, spec) +{ + let type = named.type; + if (ctx.data.edit) + type ??= ctx.data.edit.type; + + return type == "vxlan"; +} + +const service_editor = { + change_cb: function(ctx, argv) { + ctx.data.netdata.changed = true; + }, + named_args: { + type: { + help: "Service type", + required: true, + args: { + type: "enum", + no_validate: true, + value: supported_service_types, + } + }, + member: { + help: "Service member", + attribute: "members", + multiple: true, + args: { + type: "enum", + value: (ctx) => [ "@all", ...keys(ctx.data.netdata.json.hosts) ] + } + }, + "vxlan-id": { + help: "VXLAN ID", + attribute: "id", + available: is_vxlan_service, + args: { + type: "int", + min: 0, + max: (1 << 24) - 1, + } + }, + "vxlan-port": { + help: "VXLAN port", + attribute: "port", + available: is_vxlan_service, + args: { + type: "int", + min: 1, + max: 65535, + } + }, + "vxlan-mtu": { + help: "VXLAN tunnel MTU", + attribute: "mtu", + available: is_vxlan_service, + args: { + type: "int", + min: 1280, + max: 9000, + } + }, + "vxlan-forwarding-port": { + help: "Member allowed to receive broad-/multicast and unknown unicast", + attribute: "forward_ports", + available: is_vxlan_service, + multiple: true, + args: { + type: "enum", + value: (ctx) => keys(ctx.data.netdata.json.hosts) + } + }, + } +}; + +const UnetServiceEdit = editor.new(service_editor); + +const edit_create_destroy = { + change_cb: function(ctx, argv) { + ctx.data.netdata.changed = true; + }, + types: { + host: { + node_name: "UnetHostEdit", + node: UnetHostEdit, + object: "hosts", + }, + service: { + node_name: "UnetServiceEdit", + node: UnetServiceEdit, + object: "services", + }, + }, +}; + +let UnetEdit = { + config: { + help: "Edit network global configuration", + select_node: "UnetConfigEdit", + select: function(ctx) { + return ctx.set("config", { + edit: ctx.data.object_edit.config, + }); + } + }, + iface: { + help: "Edit interface configuration", + select_node: "UnetIfaceEdit", + select: function(ctx) { + return ctx.set("iface", { + edit: ctx.data.netdata.iface, + }); + } + }, + accept: { + help: "Accept invited network peer", + args: enroll_accept_arg, + available: (ctx) => ctx.data.enroll && length(ctx.data.enroll.peers) > 0, + call: network_enroll_accept, + }, + invite: { + help: "Invite another device to the network", + args: network_invite_name_arg, + named_args: network_invite_args, + call: network_invite, + }, + cancel: { + help: "Cancel device invitation", + available: (ctx) => ctx.data.enroll, + call: function(ctx) { + __network_enroll_cancel(model, ctx); + return ctx.ok("Invitation cancelled"); + } + }, + dump: { + help: "Show network json data", + call: function(ctx) { + return ctx.json("Network data", ctx.data.netdata.json); + } + }, + save: { + help: "Save network data to json file", + args: [ + { + name: "file", + help: "Destination path", + type: "path", + required: true, + new_path: true, + }, + ], + call: function(ctx, argv) { + if (!writefile(argv[0], sprintf("%.J\n", ctx.data.netdata.json))) + return ctx.command_failed("Could not write to %s", argv[0]); + + return ctx.ok("Configuration saved to "+argv[0]); + } + }, + restore: { + help: "Restore network data from json file", + args: [ + { + name: "file", + help: "Source path", + type: "path", + required: true, + }, + ], + call: function(ctx, argv) { + let config, data; + try { + data = json(readfile(argv[0])); + config = data.config; + } catch (e) { + return ctx.command_failed("Could not read JSON data from %s", argv[0]); + } + + if (!config) + return ctx.command_failed("Invalid network json file"); + + let json = ctx.data.netdata.json; + let prev_config = {}; + for (let field in [ "salt", "rounds", "id" ]) { + prev_config[field] = json.config[field]; + delete config[field]; + } + + ctx.data.netdata.changed = true; + data.config = { ...prev_config, ...config }; + ctx.data.netdata.json = data; + + return ctx.ok("Configuration restored from "+argv[0]); + } + }, + apply: { + help: "Apply changes", + named_args: network_sign_args, + call: function(ctx, argv, named) { + let netdata = ctx.data.netdata; + + let changed = network_iface_save(ctx); + if (network_apply(ctx, argv, named)) + changed = true; + + if (!changed) + return ctx.ok("No changes"); + + return ctx.ok("Changes applied"); + } + } +}; +editor.edit_create_destroy(edit_create_destroy, UnetEdit); + +const Unet = { + status: { + help: "Show unet network information", + args: network_status_args, + call: function(ctx, argv) { + let name = argv[0]; + let status = get_network_status(); + if (!status) + return ctx.command_failed(); + + if (!name) + return ctx.list("Networks", keys(status)); + + status = status[name]; + if (!status) + return ctx.not_found(); + + let data = {}; + for (let name, host in status.peers) { + let cur = []; + + data[`Host '${name}'`] = cur; + push(cur, [ "State", host.connected ? "connected" : "disconnected" ]); + if (!host.connected) + continue; + + if (host.endpoint) + push(cur, [ "IP address", host.endpoint ]); + + push(cur, [ "Idle time", time_format(host.idle) ]); + push(cur, [ "Sent bytes", host.tx_bytes ]); + push(cur, [ "Received bytes", host.rx_bytes ]); + push(cur, [ "Last handshake", time_format(host.last_handshake_sec) + " ago" ]); + } + return ctx.multi_table("Status of network " + name, data); + } + }, + join: { + help: "Join existing network", + named_args: network_join_args, + call: network_join, + }, + accept: { + help: "Accept network peer", + args: enroll_accept_arg, + available: (ctx) => ctx.data.enroll && length(ctx.data.enroll.peers) > 0, + call: network_enroll_accept, + }, + cancel: { + help: "Cancel join request", + available: (ctx) => ctx.data.enroll, + call: function(ctx) { + __network_enroll_cancel(model, ctx); + return ctx.ok("Join request cancelled"); + }, + }, + create: { + help: "Create network", + named_args: network_create_args, + call: network_create, + }, + delete: { + help: "Delete network", + args: network_args, + call: network_delete, + }, + edit: { + help: "Edit network", + args: network_status_args, + no_subcommands: true, + select_node: "UnetEdit", + select: network_edit, + }, +}; + +const Root = { + unet: { + help: "unetd network management", + select_node: "Unet", + } +}; + +model.add_nodes({ Root, Unet, UnetEdit, UnetConfigEdit, UnetIfaceEdit, UnetHostEdit, UnetServiceEdit });