'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']); }); } } );