Merge pull request #6689 from yggdrasil-openwrt/yggdrasil-2023-11-11
authorJo-Philipp Wich <jo@mein.io>
Thu, 30 Nov 2023 23:37:22 +0000 (00:37 +0100)
committerGitHub <noreply@github.com>
Thu, 30 Nov 2023 23:37:22 +0000 (00:37 +0100)
luci-proto-yggdrasil: yggdrasil now supported by netifd

protocols/luci-proto-yggdrasil/Makefile [new file with mode: 0644]
protocols/luci-proto-yggdrasil/htdocs/luci-static/resources/protocol/yggdrasil.js [new file with mode: 0644]
protocols/luci-proto-yggdrasil/root/usr/libexec/rpcd/luci.yggdrasil [new file with mode: 0755]
protocols/luci-proto-yggdrasil/root/usr/share/rpcd/acl.d/luci-proto-yggdrasil.json [new file with mode: 0644]

diff --git a/protocols/luci-proto-yggdrasil/Makefile b/protocols/luci-proto-yggdrasil/Makefile
new file mode 100644 (file)
index 0000000..ecd20fb
--- /dev/null
@@ -0,0 +1,18 @@
+#
+# Copyright (C) 2023 kulupu.io development team (turretkeeper@kulupu.io)
+#
+# This is free software, licensed under the Apache License, Version 2.0 .
+#
+
+include $(TOPDIR)/rules.mk
+
+LUCI_TITLE:=Support for Yggdrasil Network
+LUCI_DEPENDS:=+yggdrasil
+LUCI_PKGARCH:=all
+PKG_VERSION:=1.0.0
+
+PKG_PROVIDES:=luci-proto-yggdrasil
+
+include ../../luci.mk
+
+# call BuildPackage - OpenWrt buildroot signature
diff --git a/protocols/luci-proto-yggdrasil/htdocs/luci-static/resources/protocol/yggdrasil.js b/protocols/luci-proto-yggdrasil/htdocs/luci-static/resources/protocol/yggdrasil.js
new file mode 100644 (file)
index 0000000..849242a
--- /dev/null
@@ -0,0 +1,271 @@
+'use strict';
+'require form';
+'require network';
+'require rpc';
+'require tools.widgets as widgets';
+'require uci';
+'require ui';
+network.registerPatternVirtual(/^yggdrasil-.+$/);
+
+function validatePrivateKey(section_id,value) {
+       if (value.length == 0) {
+               return true;
+       };
+       if (!value.match(/^([0-9a-fA-F]){128}$/)) {
+               if (value != "auto") {
+                       return _('Invalid private key string %s').format(value);
+               }
+               return true;
+       }
+       return true;
+};
+
+function validatePublicKey(section_id,value) {
+       if (value.length == 0) {
+               return true;
+       };
+       if (!value.match(/^([0-9a-fA-F]){64}$/))
+               return _('Invalid public key string %s').format(value);
+       return true;
+};
+
+function validateYggdrasilListenUri(section_id,value) {
+       if (value.length == 0) {
+               return true;
+       };
+       if (!value.match(/^(tls|tcp|unix|quic):\/\//))
+               return _('Unsupported URI scheme in %s').format(value);
+       return true;
+};
+
+function validateYggdrasilPeerUri(section_id,value) {
+       if (!value.match(/^(tls|tcp|unix|quic|socks|sockstls):\/\//))
+               return _('URI scheme %s not supported').format(value);
+       return true;
+};
+
+var cbiKeyPairGenerate = form.DummyValue.extend({
+       cfgvalue: function(section_id, value) {
+               return E('button', {
+                       'class':'btn',
+                       'click':ui.createHandlerFn(this, function(section_id,ev) {
+                               var prv = this.section.getUIElement(section_id,'private_key'),
+                                       pub = this.section.getUIElement(section_id,'public_key'),
+                                       map = this.map;
+
+                               return generateKey().then(function(keypair){
+                                       prv.setValue(keypair.priv);
+                                       pub.setValue(keypair.pub);
+                                       map.save(null,true);
+                               });
+                       },section_id)
+               },[_('Generate new key pair')]);
+       }
+});
+
+function updateActivePeers(ifname) {
+       getPeers(ifname).then(function(peers){
+               var table = document.querySelector('#yggdrasil-active-peerings-' + ifname);
+               if (table) {
+                       while (table.rows.length > 1) { table.deleteRow(1); }
+                       peers.forEach(function(peer) {
+                               var row = table.insertRow(-1);
+                               row.style.fontSize = "xx-small";
+                               if (!peer.up) {
+                                       row.style.opacity = "66%";
+                               }
+                               var cell = row.insertCell(-1)
+                               cell.className = "td"
+                               cell.textContent = peer.remote;
+
+                               cell = row.insertCell(-1)
+                               cell.className = "td"
+                               cell.textContent = peer.up ? "Up" : "Down";
+
+                               cell = row.insertCell(-1)
+                               cell.className = "td"
+                               cell.textContent = peer.inbound ? "In" : "Out";
+
+                               cell = row.insertCell(-1)
+                               cell.className = "td"
+                               cell.innerHTML = "<u style='cursor: default'>" + peer.address + "</u>"
+                               cell.dataToggle = "tooltip";
+                               cell.title = "Key: " + peer.key;
+
+                               cell = row.insertCell(-1)
+                               cell.className = "td"
+                               cell.textContent = '%t'.format(peer.uptime);
+
+                               cell = row.insertCell(-1)
+                               cell.className = "td"
+                               cell.textContent = '%.2mB'.format(peer.bytes_recvd);
+
+                               cell = row.insertCell(-1)
+                               cell.className = "td"
+                               cell.textContent = '%.2mB'.format(peer.bytes_sent);
+
+                               cell = row.insertCell(-1)
+                               cell.className = "td"
+                               cell.textContent = peer.priority;
+
+                               cell = row.insertCell(-1)
+                               cell.className = "td"
+                               if (!peer.up) {
+                                       cell.innerHTML = "<u style='cursor: default'>%t ago</u>".format(peer.last_error_time)
+                                       cell.dataToggle = "tooltip"
+                                       cell.title = peer.last_error
+                               } else {
+                                       cell.innerHTML = "-"
+                               }
+                       });
+                       setTimeout(updateActivePeers.bind(this, ifname), 5000);
+               }
+       });
+}
+
+var cbiActivePeers = form.DummyValue.extend({
+       cfgvalue: function(section_id, value) {
+               updateActivePeers(this.option);
+               return E('table', {
+                       'class': 'table',
+                       'id': 'yggdrasil-active-peerings-' + this.option,
+               },[
+                       E('tr', {'class': 'tr'}, [
+                               E('th', {'class': 'th'}, _('URI')),
+                               E('th', {'class': 'th'}, _('State')),
+                               E('th', {'class': 'th'}, _('Dir')),
+                               E('th', {'class': 'th'}, _('IP Address')),
+                               E('th', {'class': 'th'}, _('Uptime')),
+                               E('th', {'class': 'th'}, _('RX')),
+                               E('th', {'class': 'th'}, _('TX')),
+                               E('th', {'class': 'th'}, _('Priority')),
+                               E('th', {'class': 'th'}, _('Last Error')),
+                       ])
+               ]);
+       }
+});
+
+var generateKey = rpc.declare({
+       object:'luci.yggdrasil',
+       method:'generateKeyPair',
+       expect:{keys:{}}
+});
+
+var getPeers = rpc.declare({
+       object:'luci.yggdrasil',
+       method:'getPeers',
+       params:['interface'],
+       expect:{peers:[]}
+});
+
+return network.registerProtocol('yggdrasil',
+       {
+               getI18n: function() {
+                       return _('Yggdrasil Network');
+               },
+               getIfname: function() {
+                       return this._ubus('l3_device') || this.sid;
+               },
+               getType: function() {
+                       return "tunnel";
+               },
+               getOpkgPackage: function() {
+                       return 'yggdrasil';
+               },
+               isFloating: function() {
+                       return true;
+               },
+               isVirtual: function() {
+                       return true;
+               },
+               getDevices: function() {
+                       return null;
+               },
+               containsDevice: function(ifname) {
+                       return(network.getIfnameOf(ifname)==this.getIfname());
+               },
+               renderFormOptions: function(s) {
+                       var o, ss;
+                       o=s.taboption('general',form.Value,'private_key',_('Private key'),_('The private key for your Yggdrasil node'));
+                       o.optional=false;
+                       o.password=true;
+                       o.validate=validatePrivateKey;
+
+                       o=s.taboption('general',form.Value,'public_key',_('Public key'),_('The public key for your Yggdrasil node'));
+                       o.optional=true;
+                       o.validate=validatePublicKey;
+
+                       s.taboption('general',cbiKeyPairGenerate,'_gen_server_keypair',' ');
+
+                       o=s.taboption('advanced',form.Value,'mtu',_('MTU'),_('A default MTU of 65535 is set by Yggdrasil. It is recomended to utilize the default.'));
+                       o.optional=true;
+                       o.placeholder=65535;
+                       o.datatype='range(1280, 65535)';
+
+                       o=s.taboption('general',form.TextValue,'node_info',_('Node info'),_('Optional node info. This must be a { "key": "value", ... } map or set as null. This is entirely optional but, if set, is visible to the whole network on request.'));
+                       o.optional=true;
+                       o.placeholder="{}";
+
+                       o=s.taboption('general',form.Flag,'node_info_privacy',_('Node info privacy'),_('Enable node info privacy so that only items specified in "Node info" are sent back. Otherwise defaults including the platform, architecture and Yggdrasil version are included.'));
+                       o.default=o.disabled;
+
+                       try {
+                               s.tab('peers',_('Peers'));
+                       } catch(e) {};
+                       o=s.taboption('peers', form.SectionValue, '_active', form.NamedSection, this.sid, "interface", _("Active peers"))
+                       ss=o.subsection;
+                       ss.option(cbiActivePeers, this.sid);
+
+                       o=s.taboption('peers', form.SectionValue, '_listen', form.NamedSection, this.sid, "interface", _("Listen for peers"))
+                       ss=o.subsection;
+
+                       o=ss.option(form.DynamicList,'listen_address',_('Listen addresses'), _('Add listeners in order to accept incoming peerings from non-local nodes. Multicast peer discovery works regardless of listeners set here. URI Format: <code>tls://0.0.0.0:0</code> or <code>tls://[::]:0</code> to listen on all interfaces. Choose an acceptable URI <code>tls://</code>, <code>tcp://</code>, <code>unix://</code> or <code>quic://</code>'));
+                       o.placeholder="tls://0.0.0.0:0"
+                       o.validate=validateYggdrasilListenUri;
+
+                       o=s.taboption('peers',form.DynamicList,'allowed_public_key',_('Accept from public keys'),_('If empty, all incoming connections will be allowed (default). This does not affect outgoing peerings, nor link-local peers discovered via multicast.'));
+                       o.validate=validatePublicKey;
+
+                       o=s.taboption('peers', form.SectionValue, '_peers', form.TableSection, 'yggdrasil_%s_peer'.format(this.sid), _("Peer addresses"))
+                       ss=o.subsection;
+                       ss.addremove=true;
+                       ss.anonymous=true;
+                       ss.addbtntitle=_("Add peer address");
+
+                       o=ss.option(form.Value,"address",_("Peer URI"));
+                       o.placeholder="tls://0.0.0.0:0"
+                       o.validate=validateYggdrasilPeerUri;
+                       ss.option(widgets.NetworkSelect,"interface",_("Peer interface"));
+
+                       o=s.taboption('peers', form.SectionValue, '_interfaces', form.TableSection, 'yggdrasil_%s_interface'.format(this.sid), _("Multicast rules"))
+                       ss=o.subsection;
+                       ss.addbtntitle=_("Add multicast rule");
+                       ss.addremove=true;
+                       ss.anonymous=true;
+
+                       o=ss.option(widgets.DeviceSelect,"interface",_("Devices"));
+                       o.multiple=true;
+
+                       ss.option(form.Flag,"beacon",_("Send multicast beacon"));
+
+                       ss.option(form.Flag,"listen",_("Listen to multicast beacons"));
+
+                       o=ss.option(form.Value,"port",_("Port"));
+                       o.optional=true;
+                       o.datatype='range(1, 65535)';
+
+                       o=ss.option(form.Value,"password",_("Password"));
+                       o.optional=true;
+
+                       return;
+               },
+               deleteConfiguration: function() {
+                       uci.sections('network', 'yggdrasil_%s_interface'.format(this.sid), function(s) {
+                               uci.remove('network', s['.name']);
+                       });
+                       uci.sections('network', 'yggdrasil_%s_peer'.format(this.sid), function(s) {
+                               uci.remove('network', s['.name']);
+                       });
+               }
+       }
+);
diff --git a/protocols/luci-proto-yggdrasil/root/usr/libexec/rpcd/luci.yggdrasil b/protocols/luci-proto-yggdrasil/root/usr/libexec/rpcd/luci.yggdrasil
new file mode 100755 (executable)
index 0000000..35d6627
--- /dev/null
@@ -0,0 +1,36 @@
+#!/bin/sh
+
+. /usr/share/libubox/jshn.sh
+
+case "$1" in
+       list)
+               json_init
+               json_add_object "generateKeyPair"
+               json_close_object
+               json_add_object "getPeers"
+               json_add_string "interface"
+               json_close_object
+               json_dump
+       ;;
+       call)
+               case "$2" in
+                       generateKeyPair)
+                               json_load "$(yggdrasil -genconf -json)"
+                               json_get_vars PrivateKey
+                               json_cleanup
+                               json_init
+                               json_add_object "keys"
+                               json_add_string "priv" "$PrivateKey"
+                               json_add_string "pub" "${PrivateKey:64}"
+                               json_close_object
+                               json_dump
+                       ;;
+                       getPeers)
+                               read -r input
+                               json_load "$input"
+                               json_get_vars interface
+                               yggdrasilctl -endpoint="unix:///tmp/yggdrasil/${interface}.sock" -json getPeers
+                       ;;
+               esac
+       ;;
+esac
diff --git a/protocols/luci-proto-yggdrasil/root/usr/share/rpcd/acl.d/luci-proto-yggdrasil.json b/protocols/luci-proto-yggdrasil/root/usr/share/rpcd/acl.d/luci-proto-yggdrasil.json
new file mode 100644 (file)
index 0000000..0351d86
--- /dev/null
@@ -0,0 +1,10 @@
+{
+       "luci-proto-yggdrasil": {
+               "description": "Grant access to LuCI Yggdrasil procedures",
+               "write": {
+                       "ubus": {
+                               "luci.yggdrasil": [ "generateKeyPair", "getPeers" ]
+                       }
+               }
+       }
+}