aredn/files/app/main/status/e/ports-and-xlinks.ut

479 lines
22 KiB
Plaintext
Raw Normal View History

{%
/*
* Part of AREDN® -- Used for creating Amateur Radio Emergency Data Networks
* Copyright (C) 2024 Tim Wilkinson
* See Contributors file for additional contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
function loadPorts(type, def)
{
const f = fs.open(`/etc/aredn_include/${type}.network.user`);
if (!f) {
return def;
}
const r = { vlan: null, ports: {} };
let pcount = 0;
for (let l = f.read("line"); length(l); l = f.read("line")) {
let m = match(l, /list ports '(.+):([ut])'/);
if (m) {
r.ports[m[1]] = true;
if (m[2] === "u") {
r.vlan = 0;
}
pcount++;
}
m = match(l, /option vlan '(\d+)'/);
if (m) {
r.vlan = int(m[1]);
}
}
if (pcount === 0 && ((type === "wan" && r.vlan === 1) || (type === "lan" && r.vlan === 3))) {
r.vlan = 0;
}
f.close();
return r;
}
if (request.env.REQUEST_METHOD === "PUT") {
configuration.prepareChanges();
if ("xlinks" in request.args) {
const xlinks = {};
const xs = json(request.args.xlinks);
for (let i = 0; i < length(xs); i++) {
xlinks[xs[i].name] = xs[i];
}
let defport = "eth0";
const ports = hardware.getEthernetPorts();
if (length(ports) > 0) {
defport = ports[0].k;
}
uciMesh.foreach("xlink", "interface", x => {
const ux = xlinks[x[".name"]];
if (ux) {
uciMesh.set("xlink", `${ux.name}bridge`, "vlan", ux.vlan);
uciMesh.set("xlink", `${ux.name}bridge`, "ports", [ `${ux.port || defport}:t` ]);
uciMesh.commit("xlink");
uciMesh.set("xlink", ux.name, "ifname", `br0.${ux.vlan}`);
uciMesh.set("xlink", ux.name, "ipaddr", ux.ipaddr);
if (uciMesh.get("xlink", ux.name, "peer") !== ux.peer) {
uciMesh.set("xlink", ux.name, "peer", ux.peer);
uciMesh.commit("xlink");
uciMesh.set("xlink", ux.name, "peer", ux.peer);
if (ux.peer) {
uciMesh.set("xlink", `${ux.name}route`, "route");
uciMesh.set("xlink", `${ux.name}route`, "interface", ux.name);
uciMesh.set("xlink", `${ux.name}route`, "target", ux.peer);
}
else {
uciMesh.delete("xlink", `${name}route`);
}
uciMesh.commit("xlink");
}
uciMesh.set("xlink", ux.name, "weight", ux.weight);
uciMesh.set("xlink", ux.name, "netmask", network.CIDRToNetmask(ux.cidr));
delete xlinks[ux.name];
}
else {
const name = x[".name"];
uciMesh.delete("xlink", name);
uciMesh.delete("xlink", `${name}bridge`);
uciMesh.delete("xlink", `${name}route`);
}
uciMesh.commit("xlink");
});
for (let name in xlinks) {
const ux = xlinks[name];
uciMesh.set("xlink", `${ux.name}bridge`, "bridge-vlan");
uciMesh.set("xlink", `${ux.name}bridge`, "device", "br0");
uciMesh.set("xlink", `${ux.name}bridge`, "vlan", ux.vlan);
uciMesh.set("xlink", `${ux.name}bridge`, "ports", [ `${ux.port || defport}:t` ]);
uciMesh.commit("xlink");
uciMesh.set("xlink", name, "interface");
uciMesh.set("xlink", name, "ifname", `br0.${ux.vlan}`);
uciMesh.set("xlink", name, "ipaddr", ux.ipaddr);
uciMesh.set("xlink", name, "weight", ux.weight);
uciMesh.set("xlink", name, "netmask", network.CIDRToNetmask(ux.cidr));
if (ux.peer) {
uciMesh.set("xlink", name, "peer", ux.peer);
uciMesh.set("xlink", `${ux.name}route`, "route");
uciMesh.set("xlink", `${ux.name}route`, "interface", ux.name);
uciMesh.set("xlink", `${ux.name}route`, "target", ux.peer);
uciMesh.commit("xlink");
}
uciMesh.set("xlink", name, "proto", "static");
uciMesh.set("xlink", name, "macaddr", replace("x2:xx:xx:xx:xx:xx", "x", _ => sprintf("%X",math.rand()&15)));
uciMesh.commit("xlink");
}
delete request.args.xlinks;
}
const k = keys(request.args);
if (length(k) > 0) {
if (length(hardware.getEthernetPorts()) > 1) {
const defnet = hardware.getDefaultNetworkConfiguration();
const nets = {
wan: loadPorts("wan", defnet.wan),
lan: loadPorts("lan", defnet.lan),
dtdlink: loadPorts("dtdlink", defnet.dtdlink)
};
for (let i = 0; i < length(k); i++) {
if (k[i] === "port_wan_vlan") {
nets.wan.vlan = int(request.args[k[i]] || 0);
}
else {
const ptp = split(k[i], "_");
const net = nets[ptp[1]];
if (net) {
if (request.args[k[i]] === "on") {
net.ports[ptp[2]] = true;
}
else {
delete net.ports[ptp[2]];
}
}
}
}
if (nets.wan.vlan === 0) {
for (let p in nets.wan.ports) {
if (nets.lan.ports[p]) {
delete nets.wan.ports[p];
}
}
}
function savePort(type, c)
{
const f = fs.open(`/etc/aredn_include/${type}.network.user`, "w");
if (f) {
f.write(`# Generated by advancednetwork
config bridge-vlan
option device 'br0'
option vlan '${c.vlan ? c.vlan : type == "lan" ? 3 : 1}'
${join("\n", map(keys(c.ports), p => "\tlist ports '" + p + (c.vlan ? ":t'" : ":u'")))}
config device
option name 'br-${type}'
option type 'bridge'
option macaddr '<${type}_mac>'
list ports 'br0.${c.vlan ? c.vlan : type == "lan" ? 3 : 1}'
config interface '${type}'
option device 'br-${type}'
${type === "dtdlink" ? "\toption proto 'static'" : "\toption proto '<" + type + "_proto>'"}
option ipaddr '<${type}_ip>'
${type === "dtdlink" ? "\toption netmask '255.0.0.0'" : "\toption netmask '<" + type + "_mask>'"}
${type === "wan" ? "\toption gateway '<wan_gw>'" : ""}${type === "lan" ? "\toption dns '<wan_dns1> <wan_dns2>'" : ""}
`);
f.close();
}
}
savePort("wan", nets.wan);
savePort("lan", nets.lan);
savePort("dtdlink", nets.dtdlink);
}
}
print(_R("changes"));
return;
}
if (request.env.REQUEST_METHOD === "DELETE") {
configuration.revertModalChanges();
print(_R("changes"));
return;
}
let haveports = false;
let wan_vlan = 0;
const ports = hardware.getEthernetPorts();
if (length(ports) > 1) {
haveports = true;
const defnet = hardware.getDefaultNetworkConfiguration();
const wan = loadPorts("wan", defnet.wan);
const lan = loadPorts("lan", defnet.lan);
const dtdlink = loadPorts("dtdlink", defnet.dtdlink);
for (let i = 0; i < length(ports); i++) {
const v = {
name: ports[i].k,
display: ports[i].d,
info: hardware.getEthernetPortInfo(ports[i].k),
dtdlink: dtdlink.ports[ports[i].k],
lan: lan.ports[ports[i].k],
wan: wan.ports[ports[i].k]
};
ports[i] = v;
}
wan_vlan = wan.vlan;
}
const xlinks = [];
uciMesh.foreach("xlink", "interface", x => {
push(xlinks, {
name: x[".name"],
vlan: int(split(x.ifname, ".")[1]),
ipaddr: x.ipaddr,
peer: x.peer || "",
weight: int(x.weight),
port: match(uciMesh.get("xlink", `${x[".name"]}bridge`, "ports")[0], /^(.+):/)[1],
cidr: network.netmaskToCIDR(x.netmask)
});
});
%}
<div class="dialog">
{{_R("dialog-header", `${haveports ? "Ports &amp; " : ""}XLinks`)}}
<div>
{% if (haveports) { %}
<div class="ports">
<div>
<table>
<thead>
<tr>
<td></td>
{%
for (let i = 0; i < length(ports); i++) {
const p = ports[i];
print(`<td ${p.info.active ? 'class="active"' : ''}><div>${p.display}</div></td>`);
}
%}
</tr>
</thead>
<tbody>
<tr>
<td><div>dtd</div><div>vlan: 2</div></td>
{%
for (let i = 0; i < length(ports); i++) {
const p = ports[i];
print(`<td><input hx-put="${request.env.REQUEST_URI}" type="checkbox" name="port_dtdlink_${p.name}" ${p.dtdlink ? "checked" : ""} hx-vals='js:{port_dtdlink_${p.name}:htmx.find("[name=port_dtdlink_${p.name}]").checked ? "on" : "off"}'></td>`);
}
%}
</tr>
<tr>
<td><div>lan</div><div>vlan: untagged</div></td>
{%
for (let i = 0; i < length(ports); i++) {
const p = ports[i];
print(`<td><input hx-put="${request.env.REQUEST_URI}" type="checkbox" name="port_lan_${p.name}" ${p.lan ? "checked" : ""} hx-vals='js:{port_lan_${p.name}:htmx.find("[name=port_lan_${p.name}]").checked ? "on" : "off"}'></td>`);
}
%}
</tr>
<tr>
<td><div>wan</div><div>vlan: <input hx-put="{{request.env.REQUEST_URI}}" type="text" name="port_wan_vlan" hx-include=".ports [name^='port_wan_']" size="5" placeholder="untagged" pattern="([14-9]|[1-9][0-9]{1,2}|[1-3][0-9]{3}|40[0-8][0-9]|409[0-4])" value="{{wan_vlan != 0 ? wan_vlan : ''}}"></div></td>
{%
for (let i = 0; i < length(ports); i++) {
const p = ports[i];
print(`<td><input hx-put="${request.env.REQUEST_URI}" type="checkbox" name="port_wan_${p.name}" ${p.wan ? "checked" : ""} hx-include="previous [name='port_wan_vlan']" hx-vals='js:{port_wan_${p.name}:htmx.find("[name=port_wan_${p.name}]").checked ? "on" : "off"}'></td>`);
}
%}
</tr>
</tbody>
</table>
</div>
</div>
{{_H("<p>AREDN nodes have three primary networks; DTD, LAN and WAN (shown above in the left column). You can modify the default assignment
of these networks to ports (shown across the top) using the checkboxes at the intersection of a network and a port. You can also choose the
VLAN to assign to the WAN network. Networks can be assigned to multiple ports, or no ports.
Note that on some devices, ports may have names like <i>WAN</i> or <i>LAN</i>. These are just arbitrary names given by the manufacturer
and you are not forced to assign networks of the same name to these ports.
<p>Active network ports, where a cable is present and attached to another device, are shown in green.")}}
<hr>
{% } %}
<div class="xlinks">
<div>
<div class="cols">
<div>
<div class="o">XLinks</div>
<div class="m">Inter-device links across non-AREDN networks</div>
</div>
<button class="add">+</button>
</div>
</div>
<div id="xlink-list">
<div class="cols xlink-label">
<div style="white-space:nowrap">
<div>vlan</div>
<div>ip a&zwnj;ddress</div>
<div>peer a&zwnj;ddress</div>
<div>cidr</div>
<div>weight</div>
{% if (haveports) { %}
<div>port</div>
{% } %}
</div>
<div></div>
</div>
{% for (let i = 0; i < length(xlinks); i++) {
const x = xlinks[i];
%}
<div class="cols xlink" data-name="{{x.name}}">
<div>
<input name="vlan" type="text" size="5" required pattern="([4-9]|[1-9][0-9]{1,2}|[1-3][0-9]{3}|40[0-8][0-9]|409[0-5])" placeholder="VLan" value="{{x.vlan}}">
<input name="ipaddr" type="text" size="25" required pattern="{{constants.patIP}}" placeholder="IP A&zwnj;ddress" value="{{x.ipaddr}}">
<input name="peer" type="text" size="25" pattern="{{constants.patIP}}" placeholder="IP A&zwnj;ddress" value="{{x.peer}}">
<select name="cidr">
<option value="32" {{x.cidr === 32 ? "selected" : ""}}>PtP</option>
<option value="31" {{x.cidr === 31 ? "selected" : ""}}>/ 31</option>
<option value="30" {{x.cidr === 30 ? "selected" : ""}}>/ 30</option>
<option value="29" {{x.cidr === 29 ? "selected" : ""}}>/ 29</option>
<option value="28" {{x.cidr === 28 ? "selected" : ""}}>/ 28</option>
<option value="27" {{x.cidr === 27 ? "selected" : ""}}>/ 27</option>
<option value="26" {{x.cidr === 26 ? "selected" : ""}}>/ 26</option>
<option value="25" {{x.cidr === 25 ? "selected" : ""}}>/ 25</option>
<option value="24" {{x.cidr === 24 ? "selected" : ""}}>/ 24</option>
<option value="23" {{x.cidr === 23 ? "selected" : ""}}>/ 23</option>
<option value="22" {{x.cidr === 22 ? "selected" : ""}}>/ 22</option>
<option value="21" {{x.cidr === 21 ? "selected" : ""}}>/ 21</option>
<option value="20" {{x.cidr === 20 ? "selected" : ""}}>/ 20</option>
<option value="19" {{x.cidr === 19 ? "selected" : ""}}>/ 19</option>
<option value="18" {{x.cidr === 18 ? "selected" : ""}}>/ 18</option>
<option value="17" {{x.cidr === 17 ? "selected" : ""}}>/ 17</option>
<option value="16" {{x.cidr === 16 ? "selected" : ""}}>/ 16</option>
</select>
<input name="weight" type="text" size="2" required pattern="([0-9]|[1-9][0-9]|100)" placeholder="Wt" value="{{x.weight}}">
{% if (haveports) { %}
<select name="port">
{%
for (let i = 0; i < length(ports); i++) {
const p = ports[i];
print(`<option value="${p.name}" ${x.port === p.name ? "selected" : ""}>${p.display}</option>`);
}
%}
</select>
{% } %}
</div>
<button>-</button>
</div>
{% } %}
</div>
</div>
{{_H("<p>XLinks provide a way of routing AREDN traffic across external non-AREDN networks. Each is created with a specific VLAN,
IP address for both ends, a weight of how likely to link is to be used, and an optional network size and port (on multi-port
devices). Think of xlinks as extra dtds between devices. How xlink traffic is routed once it leaves the node is dependent
on the non-AREDN network, which allows for the greatest flexibility.")}}
</div>
{{_R("dialog-footer")}}
<script>
(function(){
{{_R("open")}}
const patIP = "((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\\.?\\b){4}";
const xlinks = {{sprintf("%J", xlinks)}};
function update()
{
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
swap: "none",
values: { xlinks: JSON.stringify(xlinks) }
});
}
htmx.on("#ctrl-modal .dialog #xlink-list", "change", event => {
const target = htmx.closest(event.target, "[data-name]");
const xlink = xlinks.find(x => x.name == target.dataset.name);
const vlan = htmx.find(target, "[name=vlan]");
const ipaddr = htmx.find(target, "[name=ipaddr]");
const peer = htmx.find(target, "[name=peer]");
const weight = htmx.find(target, "[name=weight]");
const port = htmx.find(target, "[name=port]");
const cidr = htmx.find(target, "[name=cidr]");
if (vlan.validity.valid && ipaddr.validity.valid && peer.validity.valid && weight.validity.valid) {
xlink.vlan = parseInt(vlan.value);
xlink.ipaddr = ipaddr.value;
xlink.peer = peer.value;
xlink.weight = parseInt(weight.value);
xlink.port = port ? port.value : null;
xlink.cidr = parseInt(cidr.value);
update();
}
});
htmx.on("#ctrl-modal .dialog #xlink-list", "click", event => {
if (event.target.nodeName === "BUTTON") {
const target = htmx.closest(event.target, "[data-name]");
const xlink = xlinks.findIndex(x => x.name == target.dataset.name);
htmx.remove(target);
xlinks.splice(xlink, 1);
update();
}
});
htmx.on("#ctrl-modal .dialog .xlinks .add", "click", event => {
let name;
for (let i = 0;; i++) {
name = `xlink${i}`;
if (!htmx.find(`#ctrl-modal .dialog #xlink-list .xlink[data-name=${name}]`)) {
break;
}
}
const x = {
name: name,
vlan: "",
ipaddr: "",
peer: "",
weight: 0,
port: null,
cidr: 32
};
xlinks.push(x);
const div = document.createElement("div");
const ls = htmx.find("#ctrl-modal .dialog #xlink-list");
div.innerHTML = `<div class="cols xlink" data-name="${x.name}">
<div>
<input name="vlan" type="text" size="5" required pattern="([4-9]|[1-9][0-9]{1,2}|[1-3][0-9]{3}|40[0-8][0-9]|409[0-5])" placeholder="VLan">
<input name="ipaddr" type="text" size="25" required pattern="${patIP}" placeholder="IP A&zwnj;ddress">
<input name="peer" type="text" size="25" pattern="${patIP}" placeholder="IP A&zwnj;ddress">
<select name="cidr">
<option value="32">PtP</option>
<option value="31">/ 31</option>
<option value="30">/ 30</option>
<option value="29">/ 29</option>
<option value="28">/ 28</option>
<option value="27">/ 27</option>
<option value="26">/ 26</option>
<option value="25">/ 25</option>
<option value="24">/ 24</option>
<option value="23">/ 23</option>
<option value="22">/ 22</option>
<option value="21">/ 21</option>
<option value="20">/ 20</option>
<option value="19">/ 19</option>
<option value="18">/ 18</option>
<option value="17">/ 17</option>
<option value="16">/ 16</option>
</select>
<input name="weight" type="text" size="2" required pattern="([0-9]|[1-9][0-9]|100)" placeholder="Wt">
{% if (haveports) { %}<select name="port">
{%
for (let i = 0; i < length(ports); i++) {
const p = ports[i];
print(`<option value="${p.name}">${p.display}</option>`);
}
%}
</select>{% } %}
</div>
<button>-</button>
</div>`;
ls.appendChild(div.firstChild);
});
})();
</script>
</div>