#!/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 . 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 --]] 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 ' '\n") end if network == "wan" then f:write("\toption gateway ''\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 '" .. config.name .. "bridge'\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 '" .. config.name .. "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("
") html.alert_banner() html.print([[ ]]) html.print([[ ]]) html.print("
") -- navbar html.navbar_admin("advancednetwork") html.print([[
]]) if nixio.fs.stat("/tmp/reboot-required") then html.print("
Reboot is required for changes to take effect
") end if #layout.ports > 1 then html.print([[]]) html.print("") html.print("") for pos, port in ipairs(layout.ports) do html.print("") end html.print("") for _, config in ipairs(configs) do html.print("") html.print("") for _, port in ipairs(layout.ports) do local checked = config.ports[port] and "checked" or "" html.print("") end html.print("") end html.print([[
Ports
" .. pos .. "
") html.print("
" .. config.name .. "
") if config.name == "wan" then local value = config.tagged and config.vlan or "" html.print("
vlan:
") elseif config.name == "dtdlink" then html.print("
vlan: 2
") else html.print("
vlan: Untagged
") end html.print("
]]) end html.print([[]]) html.print([[]]) if #layout.ports > 1 then html.print([[]]) else html.print([[]]) end html.print([[]]) html.print([[]]) for _, xlink in ipairs(xlinks) do html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") end html.print([[]]) html.print([[]]) print("
") html.footer() html.print("") 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