'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 = "" + peer.address + ""
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 = "%t ago".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: tls://0.0.0.0:0
or tls://[::]:0
to listen on all interfaces. Choose an acceptable URI tls://
, tcp://
, unix://
or quic://
'));
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']);
});
}
}
);