Merge pull request #3348 from zhoreeq/luci_app_yggdrasil
luci-app-yggdrasil: Add application for Yggdrasil
This commit is contained in:
commit
046fccdeed
9 changed files with 357 additions and 0 deletions
14
applications/luci-app-yggdrasil/Makefile
Normal file
14
applications/luci-app-yggdrasil/Makefile
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
#
|
||||||
|
# Copyright (C) 2008-2019 The LuCI Team <luci@lists.subsignal.org>
|
||||||
|
#
|
||||||
|
# This is free software, licensed under the Apache License, Version 2.0 .
|
||||||
|
#
|
||||||
|
|
||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
LUCI_TITLE:=LuCI support for Yggdrasil
|
||||||
|
LUCI_DEPENDS:=+yggdrasil
|
||||||
|
|
||||||
|
include ../../luci.mk
|
||||||
|
|
||||||
|
# call BuildPackage - OpenWrt buildroot signature
|
|
@ -0,0 +1,22 @@
|
||||||
|
'use strict';
|
||||||
|
'require form';
|
||||||
|
|
||||||
|
return L.view.extend({
|
||||||
|
render: function() {
|
||||||
|
var m, s, o;
|
||||||
|
|
||||||
|
m = new form.Map('yggdrasil', 'Yggdrasil');
|
||||||
|
|
||||||
|
s = m.section(form.TypedSection, "yggdrasil", _("Encryption keys"));
|
||||||
|
s.anonymous = true;
|
||||||
|
|
||||||
|
s.option(form.Value, "EncryptionPublicKey", _("Encryption public key"));
|
||||||
|
s.option(form.Value, "EncryptionPrivateKey", _("Encryption private key"),
|
||||||
|
_("Keep this private. When compromised, generate a new keypair and IPv6."));
|
||||||
|
s.option(form.Value, "SigningPublicKey", _("Signing public key"));
|
||||||
|
s.option(form.Value, "SigningPrivateKey", _("Signing private key"),
|
||||||
|
_("Keep this private. When compromised, generate a new keypair and IPv6."));
|
||||||
|
|
||||||
|
return m.render();
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,31 @@
|
||||||
|
'use strict';
|
||||||
|
'require form';
|
||||||
|
|
||||||
|
return L.view.extend({
|
||||||
|
render: function() {
|
||||||
|
var m, s, o;
|
||||||
|
|
||||||
|
m = new form.Map('yggdrasil', 'Yggdrasil');
|
||||||
|
|
||||||
|
o = m.section(form.TableSection, "peer", _("Peers"),
|
||||||
|
_("List of connection strings for outbound peer connections in URI format, " +
|
||||||
|
"e.g. tcp://a.b.c.d:e or socks://a.b.c.d:e/f.g.h.i:j. These connections " +
|
||||||
|
"will obey the operating system routing table, therefore you should " +
|
||||||
|
"use this section when you may connect via different interfaces."));
|
||||||
|
o.option(form.Value, "uri", "URI");
|
||||||
|
o.anonymous = true;
|
||||||
|
o.addremove = true;
|
||||||
|
|
||||||
|
o = m.section(form.TableSection, "interface_peer", _("Interface peers"),
|
||||||
|
_("List of connection strings for outbound peer connections in URI format, " +
|
||||||
|
"arranged by source interface, e.g. { \"eth0\": [ tcp://a.b.c.d:e ] }. " +
|
||||||
|
"Note that SOCKS peerings will NOT be affected by this option and should " +
|
||||||
|
"go in the \"Peers\" section instead."));
|
||||||
|
o.option(form.Value, "interface", _("Interface"));
|
||||||
|
o.option(form.Value, "uri", "URI");
|
||||||
|
o.anonymous = true;
|
||||||
|
o.addremove = true;
|
||||||
|
|
||||||
|
return m.render();
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,38 @@
|
||||||
|
'use strict';
|
||||||
|
'require form';
|
||||||
|
|
||||||
|
return L.view.extend({
|
||||||
|
render: function() {
|
||||||
|
var m, s, o;
|
||||||
|
|
||||||
|
m = new form.Map('yggdrasil', 'Yggdrasil');
|
||||||
|
|
||||||
|
s = m.section(form.TypedSection, "yggdrasil", _("Session firewall settings"));
|
||||||
|
s.anonymous = true;
|
||||||
|
|
||||||
|
s.option(form.Flag, "SessionFirewall_Enable", _("Enable session firewall"),
|
||||||
|
_("If disabled, network traffic from any node will be allowed. If enabled, the below rules apply"));
|
||||||
|
s.option(form.Flag, "SessionFirewall_AllowFromDirect", _("Allow from direct"),
|
||||||
|
_("Allow network traffic from directly connected peers"));
|
||||||
|
s.option(form.Flag, "SessionFirewall_AllowFromRemote", _("Allow from remote"),
|
||||||
|
_("Allow network traffic from remote nodes on the network that you are not directly peered with"));
|
||||||
|
s.option(form.Flag, "SessionFirewall_AlwaysAllowOutbound",
|
||||||
|
_("Always allow outbound"), _("Allow outbound network traffic regardless of AllowFromDirect or AllowFromRemote"));
|
||||||
|
|
||||||
|
s = m.section(form.TableSection, "whitelisted_encryption_public_key",
|
||||||
|
_("Whitelisted public keys"),
|
||||||
|
_("Network traffic is always accepted from those peers, regardless of AllowFromDirect or AllowFromRemote"));
|
||||||
|
s.option(form.Value, "key", _("Public key"));
|
||||||
|
s.anonymous = true;
|
||||||
|
s.addremove = true;
|
||||||
|
|
||||||
|
s = m.section(form.TableSection, "blacklisted_encryption_public_key",
|
||||||
|
_("Blacklisted public keys"),
|
||||||
|
_("Network traffic is always rejected from those peers, regardless of AllowFromDirect or AllowFromRemote"));
|
||||||
|
s.option(form.Value, "key", _("Public key"));
|
||||||
|
s.anonymous = true;
|
||||||
|
s.addremove = true;
|
||||||
|
|
||||||
|
return m.render();
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,64 @@
|
||||||
|
'use strict';
|
||||||
|
'require form';
|
||||||
|
|
||||||
|
return L.view.extend({
|
||||||
|
render: function() {
|
||||||
|
var m, s, o;
|
||||||
|
|
||||||
|
m = new form.Map('yggdrasil', 'Yggdrasil');
|
||||||
|
|
||||||
|
s = m.section(form.TypedSection, 'yggdrasil', _('General settings'));
|
||||||
|
s.anonymous = true;
|
||||||
|
|
||||||
|
s.option(form.Value, "IfName", _("Yggdrasil's network interface name"));
|
||||||
|
s.option(form.Value, "LinkLocalTCPPort", _("Link-local TCP port"),
|
||||||
|
_("The port number to be used for the link-local TCP listeners for the "+
|
||||||
|
"configured MulticastInterfaces. This option does not affect listeners" +
|
||||||
|
"specified in the Listen option. Unless you plan to firewall link-local" +
|
||||||
|
"traffic, it is best to leave this as the default value of 0. This " +
|
||||||
|
"option cannot currently be changed by reloading config during runtime."));
|
||||||
|
|
||||||
|
s.option(form.Flag, "NodeInfoPrivacy", _("Enable NodeInfo privacy"),
|
||||||
|
_("By default, nodeinfo contains some defaults including the platform," +
|
||||||
|
" architecture and Yggdrasil version. These can help when surveying" +
|
||||||
|
" the network and diagnosing network routing problems. Enabling" +
|
||||||
|
" nodeinfo privacy prevents this, so that only items specified in" +
|
||||||
|
" \"NodeInfo\" are sent back if specified."));
|
||||||
|
|
||||||
|
o = s.option(form.Value, "NodeInfo", _("NodeInfo"),
|
||||||
|
_("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.validate = function(k, v) {
|
||||||
|
try { JSON.parse(v); return true; } catch (e) { return e.message; }
|
||||||
|
}
|
||||||
|
|
||||||
|
s.option(form.Flag, "IfTAPMode", _("Enable tap mode"));
|
||||||
|
s.option(form.Value, "IfMTU", _("MTU size for the interface"));
|
||||||
|
s.option(form.Value, "SwitchOptions_MaxTotalQueueSize",
|
||||||
|
_("Maximum size of all switch queues combined"));
|
||||||
|
|
||||||
|
o = m.section(form.TableSection, "multicast_interface", _("Multicast interfaces"),
|
||||||
|
_("Regular expressions for which interfaces multicast peer discovery " +
|
||||||
|
"should be enabled on. If none specified, multicast peer discovery is " +
|
||||||
|
"disabled. The default value is .* which uses all interfaces."));
|
||||||
|
o.option(form.Value, "name", _("Interface name"),
|
||||||
|
_("Set .* to multicast on all interfaces"));
|
||||||
|
o.anonymous = true;
|
||||||
|
o.addremove = true;
|
||||||
|
|
||||||
|
o = m.section(form.TableSection, "listen_address", _("Listen addresses"),
|
||||||
|
_("Listen addresses for incoming connections. You will need to add " +
|
||||||
|
"listeners in order to accept incoming peerings from non-local nodes. " +
|
||||||
|
"Multicast peer discovery will work regardless of any listeners set " +
|
||||||
|
"here. Each listener should be specified in URI format as above, e.g. " +
|
||||||
|
"tcp://0.0.0.0:0 or tcp://[::]:0 to listen on all interfaces."));
|
||||||
|
o.option(form.Value, "address",
|
||||||
|
_("Address to listen for incoming connections"),
|
||||||
|
_("e.g. tcp://0.0.0.0:0 or tcp://[::]:0"));
|
||||||
|
o.anonymous = true;
|
||||||
|
o.addremove = true;
|
||||||
|
|
||||||
|
return m.render();
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,113 @@
|
||||||
|
'use strict';
|
||||||
|
'require fs';
|
||||||
|
'require form';
|
||||||
|
|
||||||
|
function init_view() {
|
||||||
|
var view = document.createElement("div");
|
||||||
|
var self_info = document.createElement("div"); self_info.setAttribute("class", "table");
|
||||||
|
|
||||||
|
var table_data = {
|
||||||
|
"IPv6 address": "self-address",
|
||||||
|
"IPv6 subnet": "self-subnet",
|
||||||
|
"Coords": "self-coords",
|
||||||
|
"Public key": "self-boxpubkey",
|
||||||
|
"Build name": "self-buildname",
|
||||||
|
"Build version": "self-version"
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(table_data).forEach(function(k) {
|
||||||
|
var tr = document.createElement("div");
|
||||||
|
tr.setAttribute("class", "tr");
|
||||||
|
var td1 = document.createElement("div"); td1.setAttribute("class", "td left");
|
||||||
|
td1.textContent = k;
|
||||||
|
var td2 = document.createElement("div"); td2.setAttribute("class", "td left");
|
||||||
|
td2.id = table_data[k];
|
||||||
|
|
||||||
|
tr.appendChild(td1); tr.appendChild(td2); self_info.appendChild(tr);
|
||||||
|
});
|
||||||
|
|
||||||
|
var info_title = document.createElement("h2"); info_title.innerText = _("Yggdrasil node status");
|
||||||
|
view.appendChild(info_title);
|
||||||
|
view.appendChild(self_info);
|
||||||
|
var peering_title = document.createElement("h3"); peering_title.innerText = _("Active peers");
|
||||||
|
view.appendChild(peering_title);
|
||||||
|
|
||||||
|
var peerings = document.createElement("table");
|
||||||
|
peerings.setAttribute("class", "table"); peerings.id = "yggdrasil-peerings";
|
||||||
|
var tr = document.createElement("tr");
|
||||||
|
tr.setAttribute("class", "tr table-titles");
|
||||||
|
["Endpoint", "Address", "Proto", "Uptime", "Received", "Transmitted"].forEach(function(t) {
|
||||||
|
var th = document.createElement("th"); th.setAttribute("class", "th nowrap left");
|
||||||
|
th.innerText = t;
|
||||||
|
tr.appendChild(th);
|
||||||
|
});
|
||||||
|
peerings.appendChild(tr);
|
||||||
|
view.appendChild(peerings);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_active_peers() {
|
||||||
|
fs.exec("/usr/sbin/yggdrasilctl", ["-json", "getPeers"]).then(function(res){
|
||||||
|
if (res && res.code === 0) {
|
||||||
|
var peers = JSON.parse(res.stdout.trim())["peers"];
|
||||||
|
var table = document.querySelector('#yggdrasil-peerings');
|
||||||
|
while (table.rows.length > 1) { table.deleteRow(1); }
|
||||||
|
Object.keys(peers).forEach(function(address) {
|
||||||
|
var row = table.insertRow(-1);
|
||||||
|
row.insertCell(-1).textContent = peers[address].endpoint;
|
||||||
|
row.insertCell(-1).textContent = address;
|
||||||
|
row.insertCell(-1).textContent = peers[address].proto;
|
||||||
|
row.insertCell(-1).textContent = '%t'.format(peers[address].uptime);
|
||||||
|
row.insertCell(-1).textContent = '%1024.2mB'.format(peers[address].bytes_recvd);
|
||||||
|
row.insertCell(-1).textContent = '%1024.2mB'.format(peers[address].bytes_sent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return L.view.extend({
|
||||||
|
load: function() {
|
||||||
|
return Promise.all([
|
||||||
|
L.resolveDefault(fs.stat("/usr/sbin/yggdrasilctl"), null),
|
||||||
|
L.resolveDefault(fs.exec("/usr/sbin/yggdrasilctl", ["-json", "getSelf"]), null),
|
||||||
|
L.resolveDefault(fs.exec("/usr/sbin/yggdrasilctl", ["-json", "getPeers"]), null)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
render: function(info) {
|
||||||
|
var view = init_view();
|
||||||
|
|
||||||
|
if (info[0] && info[1] && info[1].code === 0) {
|
||||||
|
var obj = JSON.parse(info[1].stdout.trim())["self"];
|
||||||
|
var peers = JSON.parse(info[2].stdout.trim())["peers"];
|
||||||
|
|
||||||
|
var address = Object.keys(obj)[0];
|
||||||
|
var r = obj[address];
|
||||||
|
view.querySelector('#self-address').innerText = address;
|
||||||
|
view.querySelector('#self-subnet').innerText = r.subnet;
|
||||||
|
view.querySelector('#self-coords').innerText = r.coords;
|
||||||
|
view.querySelector('#self-boxpubkey').innerText = r.box_pub_key;
|
||||||
|
view.querySelector('#self-buildname').innerText = r.build_name;
|
||||||
|
view.querySelector('#self-version').innerText = r.build_version;
|
||||||
|
|
||||||
|
var table = view.querySelector('#yggdrasil-peerings');
|
||||||
|
Object.keys(peers).forEach(function(address) {
|
||||||
|
var row = table.insertRow(-1);
|
||||||
|
row.insertCell(-1).textContent = peers[address].endpoint;
|
||||||
|
row.insertCell(-1).textContent = address;
|
||||||
|
row.insertCell(-1).textContent = peers[address].proto;
|
||||||
|
row.insertCell(-1).textContent = '%t'.format(peers[address].uptime);
|
||||||
|
row.insertCell(-1).textContent = '%1024.2mB'.format(peers[address].bytes_recvd);
|
||||||
|
row.insertCell(-1).textContent = '%1024.2mB'.format(peers[address].bytes_sent);
|
||||||
|
|
||||||
|
});
|
||||||
|
setInterval(update_active_peers, 5000);
|
||||||
|
} else {
|
||||||
|
view.innerHTML = "<h2>Yggdrasil is not running</h2>";
|
||||||
|
}
|
||||||
|
return view;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
|
@ -0,0 +1,49 @@
|
||||||
|
'use strict';
|
||||||
|
'require form';
|
||||||
|
|
||||||
|
return L.view.extend({
|
||||||
|
render: function() {
|
||||||
|
var m, s, o;
|
||||||
|
|
||||||
|
m = new form.Map('yggdrasil', 'Yggdrasil');
|
||||||
|
|
||||||
|
s = m.section(form.TypedSection, "yggdrasil", _("Tunnel Routing"));
|
||||||
|
s.anonymous = true;
|
||||||
|
s.option(form.Flag, "TunnelRouting_Enable", "Enable tunnel routing",
|
||||||
|
_("Allow tunneling non-Yggdrasil traffic over Yggdrasil. This effectively " +
|
||||||
|
"allows you to use Yggdrasil to route to, or to bridge other networks, " +
|
||||||
|
"similar to a VPN tunnel. Tunnelling works between any two nodes and " +
|
||||||
|
"does not require them to be directly peered."));
|
||||||
|
|
||||||
|
o = m.section(form.TableSection, "ipv4_remote_subnet", _("IPv4 remote subnet"),
|
||||||
|
_("IPv4 subnets belonging to remote nodes, mapped to the node's public"));
|
||||||
|
o.option(form.Value, "key", _("Key"), _("Public encryption key"));
|
||||||
|
o.option(form.Value, "subnet", _("Subnet"), _("IPv4 subnet"));
|
||||||
|
o.anonymous = true;
|
||||||
|
o.addremove = true;
|
||||||
|
|
||||||
|
o = m.section(form.TableSection, "ipv4_local_subnet", _("IPv4 local subnet"),
|
||||||
|
_("IPv4 subnets belonging to this node's end of the tunnels. Only traffic " +
|
||||||
|
"from these ranges will be tunnelled."));
|
||||||
|
o.option(form.Value, "subnet", _("Subnet"), _("IPv4 subnet"));
|
||||||
|
o.anonymous = true;
|
||||||
|
o.addremove = true;
|
||||||
|
|
||||||
|
o = m.section(form.TableSection, "ipv6_remote_subnet", _("IPv6 remote subnet"),
|
||||||
|
_("IPv6 subnets belonging to remote nodes, mapped to the node's public"));
|
||||||
|
o.option(form.Value, "key", _("Key"), _("Public encryption key"));
|
||||||
|
o.option(form.Value, "subnet", _("Subnet"), _("IPv6 subnet"));
|
||||||
|
o.anonymous = true;
|
||||||
|
o.addremove = true;
|
||||||
|
|
||||||
|
o = m.section(form.TableSection, "ipv6_local_subnet", _("IPv6 local subnet"),
|
||||||
|
_("IPv6 subnets belonging to this node's end of the tunnels. Only traffic " +
|
||||||
|
"from these ranges (or the Yggdrasil node's IPv6 address/subnet) " +
|
||||||
|
"will be tunnelled."));
|
||||||
|
o.option(form.Value, "subnet", _("Subnet"), _("IPv6 subnet"));
|
||||||
|
o.anonymous = true;
|
||||||
|
o.addremove = true;
|
||||||
|
|
||||||
|
return m.render();
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,16 @@
|
||||||
|
module("luci.controller.yggdrasil", package.seeall)
|
||||||
|
|
||||||
|
function index()
|
||||||
|
if not nixio.fs.access("/etc/config/yggdrasil") then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
entry({"admin", "network", "yggdrasil"}, firstchild(), "Yggdrasil").dependent = true
|
||||||
|
entry({"admin", "network", "yggdrasil", "status"}, view("yggdrasil/status"), _("Status"), 1).leaf = false
|
||||||
|
|
||||||
|
entry({"admin", "network", "yggdrasil", "peers"}, view("yggdrasil/peers"), _("Peers"), 2).leaf = false
|
||||||
|
entry({"admin", "network", "yggdrasil", "settings"}, view("yggdrasil/settings"), _("Settings"), 3).leaf = false
|
||||||
|
entry({"admin", "network", "yggdrasil", "keys"}, view("yggdrasil/keys"), _("Encryption keys"), 4).leaf = false
|
||||||
|
entry({"admin", "network", "yggdrasil", "session_firewall"}, view("yggdrasil/session_firewall"), _("Session firewall"), 5).leaf = false
|
||||||
|
entry({"admin", "network", "yggdrasil", "tunnel_routing"}, view("yggdrasil/tunnel_routing"), _("Tunnel routing"), 6).leaf = false
|
||||||
|
end
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"luci-app-yggdrasil": {
|
||||||
|
"description": "Grant access to LuCI app yggdrasil",
|
||||||
|
"write": {
|
||||||
|
"file": {
|
||||||
|
"/usr/sbin/yggdrasilctl": [ "exec" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue