aredn/files/www/cgi-bin/advancednetwork

790 lines
26 KiB
Lua
Executable File

#!/usr/bin/lua
--[[
Part of AREDN -- Used for creating Amateur Radio Emergency Data Networks
Copyright (C) 2023 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(TM) 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
--]]
require("nixio")
require("aredn.hardware")
require("aredn.http")
local html = require("aredn.html")
math.randomseed(os.time())
local base = "/etc/aredn_include/"
local xlink_file = "/etc/config.mesh/xlink"
local default_5_port_layout = { ports = { [1] = "wan", [2] = "lan1", [3] = "lan2", [4] = "lan3", [5] = "lan4" } }
local default_3_port_layout = { ports = { [1] = "lan2", [2] = "lan1", [3] = "wan" } }
local function default_n_port_layout(board_type)
local ports = {}
for _, i in ipairs(nixio.getifaddrs())
do
if i.family == "packet" and i.name:match("^eth") then
ports[#ports + 1] = i.name
end
end
return { ports = ports }
end
local layouts = {
["mikrotik,hap-ac2"] = default_5_port_layout,
["mikrotik,hap-ac3"] = default_5_port_layout,
["glinet,gl-b1300"] = default_3_port_layout,
["qemu"] = default_n_port_layout,
["vmware"] = default_n_port_layout
}
local default_5_port_config = {
{
name = "dtdlink",
vlan = 2,
ports = { lan4 = { tagged = true } },
tagged = true
},
{
name = "lan",
vlan = 3,
ports = { lan1 = { tagged = false }, lan2 = { tagged = false }, lan3 = { tagged = false } },
tagged = false
},
{
name = "wan",
vlan = 1,
ports = { wan = { tagged = false } },
tagged = false
}
}
local default_3_port_config = {
{
name = "dtdlink",
vlan = 2,
ports = { lan2 = { tagged = true } },
tagged = true
},
{
name = "lan",
vlan = 3,
ports = { lan1 = { tagged = false } },
tagged = false
},
{
name = "wan",
vlan = 1,
ports = { wan = { tagged = false } },
tagged = false
}
}
local default_1_port_config = {
{
name = "dtdlink",
vlan = 2,
ports = { eth0 = { tagged = true } },
tagged = true
},
{
name = "lan",
vlan = 3,
ports = { eth0 = { tagged = false } },
tagged = false
},
{
name = "wan",
vlan = 1,
ports = { eth0 = { tagged = true } },
tagged = true
}
}
local default_configs = {
["mikrotik,hap-ac2"] = default_5_port_config,
["mikrotik,hap-ac3"] = default_5_port_config,
["glinet,gl-b1300"] = default_3_port_config,
["qemu"] = default_1_port_config,
["vmware"] = default_1_port_config
}
function read_user_config(network)
local file = base .. network .. ".network.user"
if not nixio.fs.stat(file) then
return nil
end
local config = {
name = network,
vlan = nil,
ports = {},
tagged = false
}
local invlan = false
for line in io.lines(file)
do
if line:match("^config%s+bridge%-vlan") then
invlan = true
elseif line:match("^config") then
invlan = false
elseif invlan then
local m
m = line:match("option%s+vlan%s+'(%d+)'")
if m then
config.vlan = tonumber(m)
if config.vlan == 2 or config.vlan >= 4 then
config.tagged = true
else
config.tagged = false
end
end
m = line:match("list%s+ports%s+'(%S+):u'") or line:match("list%s+ports%s+'(%S+):t'")
if m then
config.ports[m] = {
tagged = config.tagged
}
end
end
end
return config
end
function read_xlink_config()
if not nixio.fs.stat(xlink_file) then
return {}
end
local configs = {}
local config = {}
local type = "none"
for line in io.lines(xlink_file)
do
if line:match("^config%s+bridge%-vlan") then
type = "vlan"
config = {
name = nil,
vlan = nil,
ipaddr = nil,
peer = nil,
weight = 0,
port = nil,
netmask = nil,
mac = ""
}
configs[#configs + 1] = config
elseif line:match("^config%s+interface") then
type = "interface"
config.name = line:match("^config%s+interface%s+'(%S+)'")
elseif type == "vlan" then
local m
m = line:match("option%s+vlan%s+'(%d+)'")
if m then
config.vlan = tonumber(m)
end
m = line:match("list%s+ports%s+'(%S+):t'")
if m then
config.port = m
end
elseif type == "interface" then
local m
m = line:match("option%s+macaddr%s+'(%S+)'")
if m then
config.mac = m
end
m = line:match("option%s+ipaddr%s+'([%d%.]+)'")
if m then
config.ipaddr = m
end
m = line:match("option%s+peer%s+'([%d%.]+)'")
if m then
config.peer = m
end
m = line:match("option%s+weight%s+'([%d]+)'")
if m then
config.weight = tonumber(m)
end
m = line:match("option%s+netmask%s+'([%d%.]+)'")
if m then
config.netmask = m
end
end
end
return configs
end
function write_user_config(config, variables)
local network = config.name
local f = io.open(base .. network .. ".network.user", "w")
f:write("# Generated by advancednetwork\n")
f:write("\nconfig bridge-vlan\n")
f:write("\toption device 'br0'\n")
f:write("\toption vlan '" .. config.vlan .. "'\n")
for name, port in pairs(config.ports)
do
f:write("\tlist ports '" .. name .. (port.tagged and ":t" or ":u") .. "'\n")
end
f:write("\nconfig device\n")
f:write("\toption name 'br-" .. network .. "'\n")
f:write("\toption type 'bridge'\n")
f:write("\toption macaddr '<" .. network .. "_mac>'\n")
f:write("\tlist ports 'br0." .. config.vlan .. "'\n")
f:write("\nconfig interface " .. network .. "\n")
f:write("\toption device 'br-" .. network .. "'\n")
if network == "dtdlink" then
f:write("\toption proto 'static'\n")
f:write("\toption ipaddr '<" .. network .. "_ip>'\n")
f:write("\toption netmask '255.0.0.0'\n")
else
f:write("\toption proto '<" .. network .. "_proto>'\n")
f:write("\toption ipaddr '<" .. network .. "_ip>'\n")
f:write("\toption netmask '<" .. network .. "_mask>'\n")
end
if network == "lan" then
f:write("\toption dns '<wan_dns1> <wan_dns2>'\n")
end
if network == "wan" then
f:write("\toption gateway '<wan_gw>'\n")
end
f:close()
end
function update_legacy_wan_vlan(config)
local lines = {}
for line in io.lines("/etc/config.mesh/_setup")
do
if not line:match("^wan_intf = ") then
lines[#lines + 1] = line
end
end
for name, port in pairs(config.ports)
do
if port.tagged then
local wan_intf = ""
for dev in aredn.hardware.get_board_network_ifname("wan"):gmatch("%S+")
do
wan_intf = wan_intf .. " " .. dev:match("^([^%.]+)") .. "." .. config.vlan
end
if wan_intf ~= "" then
lines[#lines + 1] = "wan_intf =" .. wan_intf
end
break
end
end
local f = io.open("/etc/config.mesh/_setup", "w")
if f then
for _, line in ipairs(lines)
do
f:write(line .. "\n")
end
f:close()
end
end
function write_xlink_config(configs)
local f = io.open(xlink_file, "w")
f:write("# Generated by advancednetwork\n")
for _, config in ipairs(configs)
do
f:write("\nconfig bridge-vlan\n")
f:write("\toption device 'br0'\n")
f:write("\toption vlan '" .. config.vlan .. "'\n")
f:write("\tlist ports '" .. config.port .. ":t'\n")
f:write("\nconfig interface '" .. config.name .. "'\n")
f:write("\toption ifname 'br0." .. config.vlan .. "'\n")
if config.mac == "" then
config.mac = string.gsub("x2:xx:xx:xx:xx:xx", "x", function()
local i = math.random(1, 16)
return string.sub("0123456789ABCDEF", i, i)
end)
end
f:write("\toption macaddr '" .. config.mac .. "'\n")
f:write("\toption proto 'static'\n")
f:write("\toption ipaddr '" .. config.ipaddr .. "'\n")
f:write("\toption weight '" .. config.weight .. "'\n")
if config.netmask and config.netmask ~= "" then
f:write("\toption netmask '" .. config.netmask .. "'\n")
else
f:write("\toption netmask '255.255.255.255'\n")
end
if config.peer and config.peer ~= "" then
f:write("\toption peer '" .. config.peer .. "'\n")
f:write("\nconfig route\n")
f:write("\toption interface '" .. config.name .. "'\n")
f:write("\toption target '" .. config.peer .. "'\n")
end
end
f:close()
end
local get_board_type = aredn.hardware.get_board_type()
local layout = layouts[get_board_type]
if type(layout) == "function" then
layout = layout(get_board_type)
end
local configs = {}
local pending_restart = false
if os.getenv("REQUEST_METHOD") == "POST" then
require('luci.http')
local request = luci.http.Request(nixio.getenv(),
function()
local v = io.read(1024)
if not v then
io.close()
end
return v
end
)
local params = request:formvalue()
if params.op == "save" then
if params.configs then
local variables = {}
for line in io.lines("/etc/config.mesh/_setup")
do
if not (line:match("^%s*#") or line:match("^%s*$")) then
local k, v = line:match("^([^%s]*)%s*=%s*(.*)%s*$")
variables[k] = v
end
end
local configs = luci.jsonc.parse(params.configs)
for _, config in ipairs(configs)
do
write_user_config(config, variables)
if config.name == "wan" then
update_legacy_wan_vlan(config)
end
end
write_xlink_config(luci.jsonc.parse(params.xlinks))
pending_restart = true
end
elseif params.op == "defaults" then
for _, network in ipairs({ "dtdlink", "lan", "wan" })
do
nixio.fs.remove(base .. network .. ".network.user")
end
write_xlink_config({})
pending_restart = true
elseif params.op == "reboot" then
html.reboot()
end
end
local default_config = default_configs[get_board_type]
if type(default_config) == "function" then
default_config = default_config(get_board_type)
end
if default_config then
for _, network in ipairs({ "dtdlink", "lan", "wan" })
do
local config = read_user_config(network)
if not config then
for _, dconfig in ipairs(default_config)
do
if dconfig.name == network then
config = dconfig
break
end
end
end
configs[#configs + 1] = config
end
end
local xlinks = read_xlink_config()
http_header()
html.header("Advanced Network Configuration")
html.print("<body><center>")
html.alert_banner()
html.print([[
<style>
.title {
font-size: 24px;
padding: 5px 0;
text-align: center;
}
.control {
text-align: center;
padding-bottom: 8px;
}
.control input {
margin: 0 4px;
}
.msg {
font-size: 14px;
text-align: center;
font-weight: bold;
padding-bottom: 10px;
}
caption {
font-size: 12px;
font-weight: bold;
padding-bottom: 6px;
}
.ports {
margin: 10px 0 30px 0;
padding: 20px;
border: 1px solid #808080;
}
.ports tr:first-child {
font-variant: all-small-caps;
}
.ports td {
text-align: center;
vertical-align: middle;
}
.ports td:first-child {
text-align: left;
}
.h {
width: 50px;
font-size: 16px;
font-weight: bold;
}
.l {
font-size: 12px;
font-weight: bold;
width: 120px;
padding: 6px 0 2px 0;
}
.v {
font-size: 12px;
color: grey;
}
.v input {
padding-left: 2px;
font-size: 12px;
width: 65px;
border: 1px solid #c0c0c0;
}
.xlinks {
min-width: 610px;
margin: 30px auto;
padding: 20px;
border: 1px solid #808080;
}
.xlinks .h {
height: 30px;
vertical-align: top;
font-variant: all-small-caps;
}
.xlinks td {
width: 100px;
padding-left: 10px;
}
.xlinks td:nth-child(1), .xlinks td:nth-child(4), .xlinks td:nth-child(5) {
width: 40px;
}
.xlinks td:nth-child(6), .xlinks td:nth-child(7) {
width: 20px;
}
.xlinks td input {
width: 100%;
border: 1px solid transparent;
}
.xlinks td input.error {
border-color: red;
}
.xlinks td:nth-child(1) input, .xlinks td:nth-child(4) input {
text-align: right;
}
.xlinks td:nth-child(7) button {
width: 24px;
line-height: 14px;
background-color: #f0f0f0;
border: 1px solid #c0c0c0;
border-radius: 5px;
}
</style>
]])
html.print([[
<script>
const configs = ]] .. luci.jsonc.stringify(configs, true) .. [[;
configs.forEach(config => {
if (Array.isArray(config.ports)) {
config.ports = {}
}
});
function port_change(input, network, port, checked) {
const config = configs.find(c => c.name == network);
if (!config.tagged && checked) {
configs.forEach(config => {
if (!config.tagged && config.ports[port]) {
input.checked = false;
}
});
if (!input.checked) {
return;
}
}
if (checked) {
config.ports[port] = { tagged: config.tagged };
}
else {
delete config.ports[port]
}
}
function wan_vlan_change(input, value) {
const config = configs.find(c => c.name == "wan");
if (isNaN(value) || value < 4) {
if (config.tagged) {
config.tagged = false;
config.vlan = 1;
config.ports = {};
const boxes = document.querySelectorAll("#wan_row td input[type=checkbox]");
for (let i = 0; i < boxes.length; i++) {
boxes[i].checked = false;
}
}
input.value = "";
}
else {
config.tagged = true;
config.vlan = value;
for (port in config.ports) {
config.ports[port].tagged = true;
}
input.value = value;
}
validate();
}
function xlink_add() {
const tbody = document.querySelector("table.xlinks tbody");
const row = document.createElement("tr");
row.innerHTML = "<td><input type='text' value=''></td><td><input type='text' value=''></td><td><input type='text' value=''></td><td><input type='text' value='0'></td>]]
.. (function() local s = "<td" if #layout.ports <= 1 then s = s .. " style='display:none'" end s = s .. "><select>" for pos, port in ipairs(layout.ports) do s = s .. "<option value='" .. port .. "'>" .. pos .. "</option>" end return s .. "</select></td>" end)()
.. (function() local s = "<td><select><option value=''>-</option>" for v = 30, 16, -1 do s = s .. "<option value='" .. v .. "'>/" .. v .. "</option>" end return s .. "</select></td>" end)()
.. [[<td><input type='hidden' value=''><button onclick='xlink_remove(this)'>-</button></td>"
tbody.appendChild(row)
validate();
}
function xlink_remove(button) {
const row = button.parentNode.parentNode;
row.parentNode.removeChild(row)
validate();
}
function generate_xlinks() {
const xlinks = [];
const rows = document.querySelectorAll("table.xlinks tr");
for (let i = 1; i < rows.length; i++) {
const cells = rows[i].querySelectorAll("td input");
const selects = rows[i].querySelectorAll("select");
xlinks.push({
name: "xlink" + (i - 1),
vlan: parseInt(cells[0].value),
ipaddr: cells[1].value,
peer: cells[2].value,
weight: parseInt(cells[3].value),
port: selects[0].value,
netmask: {
"": "255.255.255.255",
"30": "255.255.255.252",
"29": "255.255.255.248",
"28": "255.255.255.240",
"27": "255.255.255.224",
"26": "255.255.255.192",
"25": "255.255.255.128",
"24": "255.255.255.0",
"23": "255.255.254.0",
"22": "255.255.252.0",
"21": "255.255.248.0",
"20": "255.255.240.0",
"19": "255.255.224.0",
"18": "255.255.192.0",
"17": "255.255.128.0",
"16": "255.255.0.0"
}[selects[1].value],
mac: cells[4].value
});
}
return xlinks;
}
function validate() {
let cansave = true;
const wan_config = configs.find(c => c.name == "wan");
let wan_vlan = "__untagged__";
if (wan_config && wan_config.tagged) {
wan_vlan = wan_config.vlan;
}
const rows = document.querySelectorAll("table.xlinks tr");
for (let i = 1; i < rows.length; i++) {
const cells = rows[i].querySelectorAll("td input");
if (/^\d+$/.test(cells[0].value) && parseInt(cells[0].value) > 3 && cells[0].value != wan_vlan) {
cells[0].classList.remove("error");
}
else {
cells[0].classList.add("error");
cansave = false;
}
if (/^\d+\.\d+\.\d+\.\d+$/.test(cells[1].value)) {
cells[1].classList.remove("error");
}
else {
cells[1].classList.add("error");
cansave = false;
}
if (cells[2].value == "" || /^\d+\.\d+\.\d+\.\d+$/.test(cells[2].value)) {
cells[2].classList.remove("error");
}
else {
cells[2].classList.add("error");
cansave = false;
}
if (/^\d+$/.test(cells[3].value)) {
cells[3].classList.remove("error");
}
else {
cells[3].classList.add("error");
cansave = false;
}
if (!cells[1].classList.contains("error") && !cells[1].classList.contains("error")) {
const ip = cells[1].value.split(".");
const peer = cells[2].value.split(".");
const i3 = parseInt(ip[3]);
const p3 = parseInt(peer[3]);
if (i3 == 0 || p3 == 0 || i3 == 255 || p3 == 255) {
cells[1].classList.add("error");
cells[2].classList.add("error");
cansave = false;
}
}
}
if (cansave) {
document.getElementById("save").disabled = null;
}
else {
document.getElementById("save").disabled = "disabled"
}
}
function save_config() {
document.getElementById('configs').value = JSON.stringify(configs);
document.getElementById('xlinks').value = JSON.stringify(generate_xlinks());
}
</script>
]])
html.print("<div>")
-- navbar
html.navbar_admin("advancednetwork")
html.print([[
<div class="control">
<form method="post" action="advancednetwork" enctype="multipart/form-data">
<input type="button" id="save" value="Save Changes" onclick="save_config(); document.getElementById('op').value = 'save'; form.submit()">
<input type="button" value="Default Values" onclick="document.getElementById('op').value = 'defaults'; form.submit()">
<input type="button" value="Reboot" style="font-weight:bold" onclick="document.getElementById('op').value = 'reboot'; form.submit()">
<input type="hidden" name="configs" id="configs" value="">
<input type="hidden" name="xlinks" id="xlinks" value="">
<input type="hidden" name="op" id="op" value="">
</form>
</div>
]])
if nixio.fs.stat("/tmp/reboot-required") then
html.print("<div class='msg'>Reboot is required for changes to take effect</div>")
end
if #layout.ports > 1 then
html.print([[<table class="ports" align="center"><caption>Ports</caption>]])
html.print("<tr>")
html.print("<td></td>")
for pos, port in ipairs(layout.ports)
do
html.print("<td class='h'>" .. pos .. "</td>")
end
html.print("</tr>")
for _, config in ipairs(configs)
do
html.print("<tr id='" .. config.name .. "_row'>")
html.print("<td>")
html.print("<div class='l'>" .. config.name .. "</div>")
if config.name == "wan" then
local value = config.tagged and config.vlan or ""
html.print("<div class='v'>vlan: <input type='text' placeholder='Untagged' onchange='wan_vlan_change(this, parseInt(this.value))' value='" .. value .. "'></div>")
elseif config.name == "dtdlink" then
html.print("<div class='v'>vlan: <span>2</span></div>")
else
html.print("<div class='v'>vlan: <span>Untagged</span></div>")
end
html.print("</td>")
for _, port in ipairs(layout.ports)
do
local checked = config.ports[port] and "checked" or ""
html.print("<td><input type='checkbox' " .. checked .. " onchange='port_change(this, \"" .. config.name .. "\", \"" .. port .. "\", this.checked)'></td>")
end
html.print("</tr>")
end
html.print([[</table>]])
end
html.print([[<table class="xlinks" align="center"><caption>Xlinks</caption>]])
html.print([[<tr class="h"><td>vlan</td><td>ip address</td><td>peer address</td><td>weight</td>]])
if #layout.ports > 1 then
html.print([[<td>&nbsp;port</td>]])
else
html.print([[<td style='display:none'>&nbsp;port</td>]])
end
html.print([[<td>cidr</td>]])
html.print([[<td><button onclick="xlink_add()">+</button></td></tr>]])
for _, xlink in ipairs(xlinks)
do
html.print("<tr id='" .. xlink.name .. "'>")
html.print("<td><input type='text' value='" .. xlink.vlan .. "'></td><td><input type='text' value='" .. xlink.ipaddr .. "'></td><td><input type='text' value='" .. (xlink.peer or "") .. "'></td>")
html.print("<td><input type='text' value='" .. xlink.weight .. "'></td>")
html.print("<td" .. (#layout.ports <= 1 and " style='display:none'" or "") .. "><select>")
for pos, port in ipairs(layout.ports)
do
html.print("<option value='" .. port .. "'" .. (xlink.port == port and " selected" or "") .. ">" .. pos .. "</option>")
end
html.print("</select></td>")
html.print("<td><select>")
html.print("<option value=''>-</option>")
local cidr = netmask_to_cidr(xlink.netmask)
for v = 30, 16, -1
do
html.print("<option value='" .. v .. "'" .. (cidr == v and " selected" or "") .. ">/" .. v .. "</option>")
end
html.print("</select></td>")
html.print("<td><input type='hidden' value='" .. xlink.mac .. "'><button onclick='xlink_remove(this)'>-</button></td>")
html.print("</tr>")
end
html.print([[</table>]])
html.print([[<script>document.querySelector(".xlinks").addEventListener("change", validate);</script>]])
print("</div></center>")
html.footer()
html.print("</body></html>")
http_footer()
if pending_restart then
os.execute("/usr/local/bin/node-setup > /dev/null 2>&1")
os.execute("/usr/local/bin/restart-services.sh > /dev/null 2>&1")
end