Merge pull request #6689 from yggdrasil-openwrt/yggdrasil-2023-11-11
luci-proto-yggdrasil: yggdrasil now supported by netifd
This commit is contained in:
commit
ab3f825475
4 changed files with 335 additions and 0 deletions
18
protocols/luci-proto-yggdrasil/Makefile
Normal file
18
protocols/luci-proto-yggdrasil/Makefile
Normal file
|
@ -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
|
|
@ -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']);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
36
protocols/luci-proto-yggdrasil/root/usr/libexec/rpcd/luci.yggdrasil
Executable file
36
protocols/luci-proto-yggdrasil/root/usr/libexec/rpcd/luci.yggdrasil
Executable file
|
@ -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
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"luci-proto-yggdrasil": {
|
||||
"description": "Grant access to LuCI Yggdrasil procedures",
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.yggdrasil": [ "generateKeyPair", "getPeers" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue