summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJo-Philipp Wich <jo@mein.io>2023-12-01 00:37:22 +0100
committerGitHub <noreply@github.com>2023-12-01 00:37:22 +0100
commitab3f8254753d18935b1f6d8d015709613b3990a1 (patch)
tree83d49eb3b8e47f282091bba968bb95a26aa68d20
parenta6753d44e7bcff305f789e182031fb5f089a8836 (diff)
parentc9786be7518929f076ad9a1abe190b2573851128 (diff)
Merge pull request #6689 from yggdrasil-openwrt/yggdrasil-2023-11-11
luci-proto-yggdrasil: yggdrasil now supported by netifd
-rw-r--r--protocols/luci-proto-yggdrasil/Makefile18
-rw-r--r--protocols/luci-proto-yggdrasil/htdocs/luci-static/resources/protocol/yggdrasil.js271
-rwxr-xr-xprotocols/luci-proto-yggdrasil/root/usr/libexec/rpcd/luci.yggdrasil36
-rw-r--r--protocols/luci-proto-yggdrasil/root/usr/share/rpcd/acl.d/luci-proto-yggdrasil.json10
4 files changed, 335 insertions, 0 deletions
diff --git a/protocols/luci-proto-yggdrasil/Makefile b/protocols/luci-proto-yggdrasil/Makefile
new file mode 100644
index 0000000000..ecd20fb655
--- /dev/null
+++ b/protocols/luci-proto-yggdrasil/Makefile
@@ -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
index 0000000000..849242abff
--- /dev/null
+++ b/protocols/luci-proto-yggdrasil/htdocs/luci-static/resources/protocol/yggdrasil.js
@@ -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
index 0000000000..35d6627be7
--- /dev/null
+++ b/protocols/luci-proto-yggdrasil/root/usr/libexec/rpcd/luci.yggdrasil
@@ -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
index 0000000000..0351d8610d
--- /dev/null
+++ b/protocols/luci-proto-yggdrasil/root/usr/share/rpcd/acl.d/luci-proto-yggdrasil.json
@@ -0,0 +1,10 @@
+{
+ "luci-proto-yggdrasil": {
+ "description": "Grant access to LuCI Yggdrasil procedures",
+ "write": {
+ "ubus": {
+ "luci.yggdrasil": [ "generateKeyPair", "getPeers" ]
+ }
+ }
+ }
+}