unet-cli 10 KB

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