#! /usr/bin/lua --[[ Part of AREDN® -- Used for creating Amateur Radio Emergency Data Networks Copyright (C) 2021-2023 Tim Wilkinson Orignal Perl Copyright (C) 2015 Conrad Lara 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.utils") require('aredn.info') require("aredn.hardware") require("iwinfo") require("aredn.services") -- helpers start function is_null(v) if not v or v == "" or v == 0 or v == "0" then return true else return false end end function is_notnull(v) return not is_null(v) end local function h2s(hex) local s = "" if hex then for i = 1,#hex,2 do local p = hex:sub(i, i+1) if p:match("[0-9a-f][0-9a-f]") then s = s .. string.char(tonumber(p, 16)) else s = s .. p end end end return s end local function tablesize(t) local len = 0 for _ in pairs(t) do len = len + 1 end return len end local function get_subtable(container, key) local subtable = container[key] if not subtable then subtable = {} container[key] = subtable end return subtable end -- helpers end local FORCED = "force" local UNFORCED = "onrequest" local c = uci.cursor() local cm = uci.cursor("/etc/config.mesh") local ieee80211 = "/sys/class/ieee80211/" local lanintf = aredn.hardware.get_board_network_ifname("lan") local node = aredn.info.get_nvram("node") local tactical = aredn.info.get_nvram("tactical") local mac2 = aredn.info.get_nvram("mac2") local dtdmac = aredn.info.get_nvram("dtdmac") local wifi_mon_enable = false local deleteme = {} local cfg = { lan_intf = lanintf, wan_intf = aredn.hardware.get_board_network_ifname("wan"), bridge_network_config = "", lan_network_config = "", wan_network_config = "", dtdlink_network_config = "", wifi_network_config = "", olsrd_dtd_interface_mode = "ether", tun_network_config = "", wireguard_network_config = "", dtdlink_interfaces = "\tlist network 'dtdlink'", vpn_interfaces = "", olsrd_pollrate = "0.05", tun_devices_config = "", hello_interval = "4.0", tc_interval = "10.0", mid_interval = "10.0", hna_interval = "10.0" } -- Track the changes so we can make better decissions about what to restart/reboot local changes = { reboot = false, system = false, manager = false, network = false, olsr = false, dnsmasq = false, firewall = false, tunnels = false, wireless = false, localservices = false, wpad = false } function valid_config(config) remove_all("/tmp/uci_validate") nixio.fs.mkdir("/tmp/uci_validate") write_all("/tmp/uci_validate/config", config) local r = os.execute("/sbin/uci -c /tmp/uci_validate import dummy < /tmp/uci_validate/config") remove_all("/tmp/uci_validate") return r == 0 and true or false end function is_nat_mode() return is_null(cfg.dmz_mode) end function is_dmz_mode() return is_notnull(cfg.dmz_mode) and cfg.dmz_mode ~= "1" end function is_altnet_mode() return cfg.dmz_mode == "1" end function expand_vars(lines) local nlines = {} for line in lines:gmatch("([^\n]*\n?)") do local inc = line:match("^include%s+(%S+)%s*") if inc then if nixio.fs.stat(inc) then line = expand_vars(read_all(inc)) if not valid_config(line) then print("Invalid config fragment: " .. inc) os.exit(1) end else line = nil end elseif line:match("^[^#]") then for parm in line:gmatch("<([^%s]*)>") do if deleteme[parm] then line = nil elseif parm == "NODE" then line = line:gsub("", node) elseif parm == "MAC2" then line = line:gsub("", mac2) elseif parm == "DTDMAC" then line = line:gsub("", dtdmac) elseif cfg[parm] then line = line:gsub("<" .. parm .. ">", cfg[parm]) else line = nil end end end if line then nlines[#nlines + 1] = line end end return table.concat(nlines, "") end -- load the verify the selected configuration for line in io.lines("/etc/config.mesh/_setup") do if not (line:match("^%s*#") or line:match("^%s*$")) then line = line:gsub("", node):gsub("", mac2):gsub("", dtdmac) local k, v = line:match("^([^%s]*)%s*=%s*(.*)%s*$") cfg[k] = v end end if cfg.wifi_enable == "1" then if not cfg.wifi_intf or cfg.wifi_intf == "" then cfg.wifi_intf = aredn.hardware.get_board_network_ifname("wifi"):match("^(%S+)") end local mtu = c:get("aredn", "@lqm[0]", "mtu") mtu = tonumber(mtu) if mtu and mtu >= 256 and mtu <= 1500 then cfg.wifi_mtu = mtu end else cfg.wifi_intf = "br-nomesh" end -- Supernode options local is_supernode = (cm:get("aredn", "@supernode[0]", "enable") == "1") if is_supernode then cfg.olsrd_dtd_interface_mode = "isolated" cfg.olsrd_pollrate = "0.01" end -- delete some config lines if necessary if cfg.wan_proto == "dhcp" then deleteme.wan_ip = true deleteme.wan_gw = true deleteme.wan_mask = true end if is_dmz_mode() or is_altnet_mode() or cfg.wan_proto ~= "disabled" then deleteme.lan_gw = true end -- lan_dhcp sense is inverted in the dhcp config file -- and it is a checkbox so it may not be defined - this fixes that if cfg.lan_dhcp == "1" then cfg.lan_dhcp = 0 else cfg.lan_dhcp = 1 end -- handle possible remote syslog local remote_log = cm:get("aredn", "@remotelog[0]", "url") or "" local proto, ip, port = remote_log:match("^(.+)://(%d+%.%d+%.%d+%.%d+):(%d+)$") port = tonumber(port) if proto and (proto == "tcp" or proto == "udp") and (port > 0 and port < 65536) and validate_ip(ip) then cfg.remote_log_ip = ip cfg.remote_log_port = port cfg.remote_log_proto = proto else deleteme.remote_log_ip = true deleteme.remote_log_port = true deleteme.remote_log_proto = true end -- verify that we have all the variables we need for file in nixio.fs.glob("/etc/config.mesh/*") do for line in io.lines(file) do if line:match("^[^#]") then for parm in line:gmatch("<([^%s]*)>") do if parm:upper() == parm then -- nvram variable if aredn.info.get_nvram(parm:lower()) == "" then print ("nv parameter '" .. parm .. "' in file '" .. file .. "' does not exist") return -1 end else if not cfg[parm] and not deleteme[parm] then print ("parameter '" .. parm .. "' in file '" .. file .. "' does not exist") return -1 end end end end end end -- sensible dmz_mode default if is_null(cfg.dmz_mode) then cfg.dmz_mode = "0" end -- switch to dmz values if needed if is_dmz_mode() then cfg.lan_ip = cfg.dmz_lan_ip cfg.lan_mask = cfg.dmz_lan_mask cfg.dhcp_start = cfg.dmz_dhcp_start cfg.dhcp_end = cfg.dmz_dhcp_end cfg.dhcp_limit = cfg.dmz_dhcp_limit end -- select ports and dhcp files based on mode local portfile = "/etc/config.mesh/_setup.ports" local dhcpfile = "/etc/config.mesh/_setup.dhcp" local dhcptagsfile = "/etc/config.mesh/_setup.dhcptags" local dhcpoptionsfile = "/etc/config.mesh/_setup.dhcpoptions" local aliasfile = "/etc/config.mesh/aliases" local servfile = "/etc/config.mesh/_setup.services" if is_nat_mode() then portfile = portfile .. ".nat" dhcpfile = dhcpfile .. ".nat" dhcptagsfile = dhcptagsfile .. ".nat" dhcpoptionsfile = dhcpoptionsfile .. ".nat" aliasfile = aliasfile .. ".nat" servfile = servfile .. ".nat" else portfile = portfile .. ".dmz" dhcpfile = dhcpfile .. ".dmz" dhcptagsfile = dhcptagsfile .. ".dmz" dhcpoptionsfile = dhcpoptionsfile .. ".dmz" aliasfile = aliasfile .. ".dmz" servfile = servfile .. ".dmz" end -- check for old aliases file, copy it to .dmz and create symlink -- just in case anyone is already using the file for some script or something if not nixio.fs.readlink("/etc/config.mesh/aliases") then if nixio.fs.stat("/etc/config.mesh/aliases") then filecopy("/etc/config.mesh/aliases", "/etc/config.mesh/aliases.dmz") os.remove("/etc/config.mesh/aliases") else io.open("/etc/config.mesh/aliases.dmz", "a"):close() end nixio.fs.symlink("aliases.dmz", "/etc/config.mesh/aliases") end -- generate the new school bridge configuration if nixio.fs.stat("/etc/aredn_include/bridge.network.user") then cfg.bridge_network_config = expand_vars(read_all("/etc/aredn_include/bridge.network.user")) if not valid_config(cfg.bridge_network_config) then print("Invalid config fragment: /etc/aredn_include/bridge.network.user") os.exit(1) end else local list = {} for _, net in ipairs({ "lan", "wan", "dtdlink" }) do local ports = aredn.hardware.get_board_network_ifname(net):split(" ") for _, port in ipairs(ports) do list[port:gsub("%..*$", "")] = true end end local config = "config device\n option name 'br0'\n option type 'bridge'\n option vlan_filtering '1'\n" for port, _ in pairs(list) do config = config .. " list ports '" .. port .. "'\n" end cfg.bridge_network_config = config end -- generate the network configurations for _, net in ipairs({ "lan", "wan", "dtdlink", "wifi" }) do local wireless = false local config = "" -- user override if nixio.fs.stat("/etc/aredn_include/" .. net .. ".network.user") then if nixio.fs.stat("/etc/aredn_include/fixedmac." .. net) then for line in io.lines("/etc/aredn_include/fixedmac." .. net) do local m = line:match("option%s+macaddr%s+(%S+)") if m then cfg[net .. "_mac"] = m end end end config = expand_vars(read_all("/etc/aredn_include/" .. net .. ".network.user")) if not valid_config(config) then print("Invalid config fragment: /etc/aredn_include/" .. net .. ".network.user") os.exit(1) end else -- generate a complete config local vlan = nil local ports = aredn.hardware.get_board_network_ifname(net):split(" ") if net == "lan" then -- If the LAN has be given a vlan to use (usually by a switch) we use that here, -- otherwise we just use '3' local lvlan = ports[1]:match("%.(%d+)$") if lvlan then vlan = lvlan .. ":t" else vlan = "3:u" end elseif net == "wan" then -- wifi can be used for the WAN (we become a wifi client) if cfg.wifi3_enable == "1" then -- WAN uses wifi wireless = true ports = { "wlan0" } for devname in nixio.fs.dir(ieee80211) do local hwmode = "11g" if iwinfo.nl80211.freqlist(devname)[1].mhz > 5000 then hwmode="11a" end if hwmode == cfg.wifi3_hwmode then ports = { "wlan" .. devname:match("^phy(%d+)$") } break end end else -- If the WAN has been given a vlan to use (usually 1) we use that here. local wvlan = ports[1]:match("%.(%d+)$") if wvlan then vlan = wvlan .. ":t" else vlan = "4:u" end -- handle wan vlan override for line in io.lines("/etc/config.mesh/_setup") do local wport, wvlan = line:match("^wan_intf = (%w+%.)(%d+)") if wvlan then vlan = wvlan .. ":t" ports = { wport .. wvlan } break end end end elseif net == "dtdlink" then -- Always vlan 2 vlan = "2:t" cfg.dtdlink_proto = "static" cfg.dtdlink_mask = "255.0.0.0" elseif net == "wifi" then wireless = true ports = { cfg.wifi_intf } end if vlan then -- new school vlan configuration local v, t = vlan:match("(.*):(.*)") config = config .. "\nconfig bridge-vlan\n option device 'br0'\n option vlan '" .. v .. "'\n" for _, port in ipairs(ports) do config = config .. " list ports '" .. port:gsub("%..*$", "") .. ":" .. t .. "'\n" end config = config .. "\n" ports = { "br0." .. v } end local proto = cfg[net .. "_proto"] or "" local ipaddr = cfg[net .. "_ip"] or "" local netmask = cfg[net .. "_mask"] or "" local mtu = cfg[net .. "_mtu"] or "" local dns1 = "" local dns2 = "" if net == "lan" then dns1 = cfg.wan_dns1 or "" dns2 = cfg.wan_dns2 or "" end local gateway = cfg[net .. "_gw"] or "" config = config .. "config device\n" if not wireless then config = config .. " option name 'br-" .. net .. "'\n option type 'bridge'\n" elseif cfg.wifi_enable ~= "1" then netmask = "255.255.255.255" end if nixio.fs.stat("/etc/aredn_include/fixedmac." .. net) then config = config .. read_all("/etc/aredn_include/fixedmac." .. net) end if not wireless then if #ports == 0 then config = config .. " option bridge_empty '1'\n" else for _, port in ipairs(ports) do config = config .. " list ports '" .. port .. "'\n" end end else config = config .. " option name '" .. ports[1] .. "'\n" if not ports[1]:match("^wlan") then config = config .. " option type 'bridge'\n option bridge_empty '1'\n" end end config = config .. "\nconfig interface " .. net .. "\n" if wireless then config = config .. " option device '" .. ports[1] .. "'\n" else config = config .. " option device 'br-" .. net .. "'\n" end if proto ~= "" then config = config .. " option proto '" .. proto .. "'\n" end if mtu ~= "" then config = config .. " option mtu '" .. mtu .. "'\n" end if ipaddr ~= "" then config = config .. " option ipaddr '" .. ipaddr .. "'\n" end if netmask ~= "" then config = config .. " option netmask '" .. netmask .. "'\n" end if dns1 ~= "" or dns2 ~= "" then config = config .. " option dns '" .. (dns1 or "") .. (dns1 and dns2 and " " or "") .. (dns2 or "") .. "'\n" end if gateway ~= "" then config = config .. " option gateway '" .. gateway .. "'\n" end end cfg[net .. "_network_config"] = config end -- Generate the tunnel configurations local tun_port = cm:get("vtun", "@options[0]", "port") if tun_port then cfg.tun_network_config = cfg.tun_network_config .. "config options\n\toption port '" .. tun_port .. "'\n\n" end local tun_start = cm:get("vtun", "@network[0]", "start") local tun_dns = cm:get("vtun", "@network[0]", "dns") if tun_start or tun_dns then cfg.tun_network_config = cfg.tun_network_config .. "config network\n" if tun_start then cfg.tun_network_config = cfg.tun_network_config .. "\toption start '" .. tun_start .. "'\n" end if tun_dns then cfg.tun_network_config = cfg.tun_network_config .. "\toption dns '" .. tun_dns .. "'\n" end cfg.tun_network_config = cfg.tun_network_config .. "\n" end local def_tun_weight = tonumber(cm:get("aredn", "@tunnel[0]", "weight") or 1) or 0 local is_supernode = cm:get("aredn", "@supernode[0]", "enable") == "1" if is_supernode then def_tun_weight = 0 end local tun_weights = {} local vtunclients = 0 cm:foreach("vtun", "client", function(s) if s.enabled == "1" then cfg.tun_network_config = cfg.tun_network_config .. string.format("config client\n\toption enabled '1'\n\toption node '%s'\n\toption passwd '%s'\n\toption clientip '%s'\n\toption serverip '%s'\n\toption netip '%s'\n\n", s.node:upper(), s.passwd, s.clientip, s.serverip, s.netip) local w = s.weight or def_tun_weight if not tun_weights[w] then tun_weights[w] = {} end table.insert(tun_weights[w], string.format("tun%d", 50 + vtunclients)) vtunclients = vtunclients + 1 end end ) local wgclients = 0 cm:foreach("wireguard", "client", function(s) if s.enabled == "1" then local server_priv, _, _, client_pub = s.key:match("^(.+=)(.+=)(.+=)(.+=)$") local addr, port = s.clientip:match("^(%d+%.%d+%.%d+%.%d+):(%d+)$") cfg.wireguard_network_config = cfg.wireguard_network_config .. string.format("config interface 'wgc%d'\n\toption proto 'wireguard'\n\toption private_key '%s'\n\toption nohostroute '1'\n\toption listen_port '%s'\n\tlist addresses '%s'\n\n", wgclients, server_priv, port, addr) cfg.wireguard_network_config = cfg.wireguard_network_config .. string.format("config wireguard_wgc%d\n\toption public_key '%s'\n\toption persistent_keepalive '25'\n\tlist allowed_ips '0.0.0.0/0'\n\n", wgclients, client_pub) local w = s.weight or def_tun_weight if not tun_weights[w] then tun_weights[w] = {} end table.insert(tun_weights[w], string.format("wgc%d", wgclients)) wgclients = wgclients + 1 end end ) local vtunservers = 0 local wgservers = 0 local vtunclients_roundup = 10 * math.ceil(vtunclients / 10) cm:foreach("vtun", "server", function(s) if s.enabled == "1" then if s.netip:match(":") then local server_pub, client_priv, client_pub = s.passwd:match("^(.+=)(.+=)(.+=)$") local abc, d, p = s.netip:match("^(%d+%.%d+%.%d+)%.(%d+):(%d+)") d = tonumber(d) + 1 cfg.wireguard_network_config = cfg.wireguard_network_config .. string.format("config interface 'wgs%d'\n\toption proto 'wireguard'\n\toption private_key '%s'\n\toption nohostroute '1'\n\tlist addresses '%s'\n\n", wgservers, client_priv, (abc .. "." .. d)) cfg.wireguard_network_config = cfg.wireguard_network_config .. string.format("config wireguard_wgs%d\n\toption public_key '%s'\n\toption endpoint_host '%s'\n\toption endpoint_port '%s'\n\toption persistent_keepalive '25'\n\tlist allowed_ips '0.0.0.0/0'\n\n", wgservers, server_pub, s.host, p) local w = s.weight or def_tun_weight if not tun_weights[w] then tun_weights[w] = {} end table.insert(tun_weights[w], string.format("wgs%d", wgservers)) wgservers = wgservers + 1 else cfg.tun_network_config = cfg.tun_network_config .. string.format("config server\n\toption enabled '1'\n\toption host '%s'\n\toption node '%s'\n\toption passwd '%s'\n\toption clientip '%s'\n\toption serverip '%s'\n\toption netip '%s'\n\n", s.host, s.node:upper(), s.passwd, s.clientip, s.serverip, s.netip) local w = s.weight or def_tun_weight if not tun_weights[w] then tun_weights[w] = {} end table.insert(tun_weights[w], string.format("tun%d", 50 + vtunclients_roundup + vtunservers)) vtunservers = vtunservers + 1 end end end ) -- Create the tunnel devices we need local maxclients = 0 local maxservers = 0 -- No tunnel devices without the vtund app if nixio.fs.stat("/usr/sbin/vtund") then maxclients = 10 * math.ceil(vtunclients / 10) maxservers = 10 * math.ceil(vtunservers / 10) end for dev = 50, 50 + maxclients + maxservers - 1 do cfg.tun_devices_config = cfg.tun_devices_config .. string.format("config interface 'tun%d'\n\toption ifname 'tun%d'\n\toption proto 'none'\n\n", dev, dev) end -- Not wireguard devices without the wg app if not nixio.fs.stat("/usr/bin/wg") then wgclients = 0 wgservers = 0 end -- Put the tunnels into the vpn zone local vpnzone = false if maxclients + maxservers + wgclients + wgservers > 0 then vpnzone = true for i = 50, 50 + maxclients + maxservers - 1 do cfg.vpn_interfaces = cfg.vpn_interfaces .. "\tlist network 'tun" .. i .. "'\n" end for i = 0, wgclients-1 do cfg.vpn_interfaces = cfg.vpn_interfaces .. "\tlist network 'wgc" .. i .. "'\n" end for i = 0, wgservers-1 do cfg.vpn_interfaces = cfg.vpn_interfaces .. "\tlist network 'wgs" .. i .. "'\n" end end -- Put xlinks into dtdlink zone if nixio.fs.stat("/etc/config.mesh/xlink") then uci.cursor("/etc/config.mesh"):foreach("xlink", "interface", function(section) cfg.dtdlink_interfaces = cfg.dtdlink_interfaces .. "\n\tlist network '" .. section[".name"] .. "'" end ) end remove_all("/tmp/new_config") nixio.fs.mkdir("/tmp/new_config") for file in nixio.fs.glob("/etc/config.mesh/*") do local bfile = nixio.fs.basename(file) if bfile == "vtun" or bfile == "wireguard" or bfile == "xlink" or bfile == "olsrd" or bfile == "firewall.user" or bfile:match("^_setup") or bfile:match("^aliases") then -- Dont copy these else local f = io.open("/tmp/new_config/" .. bfile, "w") if f then f:write(expand_vars(read_all(file))) f:close() end end end -- Tunnels write_all("/tmp/new_config/vtun", expand_vars("")) local nc = uci.cursor("/tmp/new_config") -- append to firewall local fw = io.open("/tmp/new_config/firewall", "a") if fw then if not is_nat_mode() then fw:write("\nconfig forwarding\n option src wifi\n option dest lan\n") fw:write("\nconfig forwarding\n option src dtdlink\n option dest lan\n") if vpnzone then fw:write("\nconfig forwarding\n option src vpn\n option dest lan\n") end end if nc:get("aredn", "@wan[0]", "olsrd_gw") == "1" then fw:write("\nconfig forwarding\n option src wifi\n option dest wan\n") fw:write("\nconfig forwarding\n option src dtdlink\n option dest wan\n") if vpnzone then fw:write("\nconfig forwarding\n option src vpn\n option dest wan\n") end end if nixio.fs.access(portfile) then for line in io.lines(portfile) do if not (line:match("^%s*#") or line:match("^%s*$")) then local dip = line:match("dmz_ip = (%w+)") if dip and is_dmz_mode() then fw:write("\nconfig redirect\n option src wifi\n option proto tcp\n option src_dip " .. cfg.wifi_ip .. "\n option dest_ip " .. dip .. "\n") fw:write("\nconfig redirect\n option src wifi\n option proto udp\n option src_dip " .. cfg.wifi_ip .. "\n option dest_ip " .. dip .. "\n") else local intf, type, oport, host, iport, enable = line:match("(.*):(.*):(.*):(.*):(.*):(.*)") if enable == "1" then local match = "option src_dport " .. oport .. "\n" if type == "tcp" then match = match .. " option proto tcp\n" elseif type == "udp" then match = match .. " option proto udp\n" end -- uci the host and then -- set the inside port unless the rule uses an outside port range host = "option dest_ip " .. host .. "\n" if not oport:match("-") then host = host .. " option dest_port " .. iport .. "\n" end if is_dmz_mode() and intf == "both" then intf = "wan" end if intf == "both" then fw:write("\nconfig redirect\n option src wifi\n option dest lan\n " .. match .. " option src_dip " .. cfg.wifi_ip .. "\n " .. host .. "\n") fw:write("\nconfig redirect\n option src dtdlink\n option dest lan\n " .. match .. " option src_dip " .. cfg.wifi_ip .. "\n " .. host .. "\n") if vpnzone then fw:write("\nconfig redirect\n option src vpn\n option dest lan\n " .. match .. " option src_dip " .. cfg.wifi_ip .. "\n " .. host .. "\n") end fw:write("config redirect\n option src wan\n option dest lan\n " .. match .. " " .. host .. "\n") elseif intf == "wifi" and is_nat_mode() then fw:write("\nconfig redirect\n option src dtdlink\n option dest lan\n " .. match .. " option src_dip " .. cfg.wifi_ip .. "\n " .. host .. "\n") fw:write("\nconfig redirect\n option src wifi\n option dest lan\n " .. match .. " option src_dip " .. cfg.wifi_ip .. "\n " .. host .. "\n") if vpnzone then fw:write("\nconfig redirect\n option src vpn\n option dest lan\n " .. match .. " option src_dip " .. cfg.wifi_ip .. "\n " .. host .. "\n") end elseif intf == "wan" then fw:write("\nconfig redirect\n option src dtdlink\n option dest lan\n " .. match .. " option src_dip " .. cfg.wifi_ip .. "\n " .. host .. "\n") fw:write("config redirect\n option src wan\n option dest lan\n " .. match .. " " .. host .. "\n") end end end end end end fw:close(); end -- setup nat if is_nat_mode() then -- zone[0] = lan, zone[1] = wan, zone[2] = wifi, zone[3] = dtdlink, zone[4] = vpn local masq_src = cfg.lan_ip .. "/" .. netmask_to_cidr(cfg.lan_mask) for z = 2, 4 do nc:set("firewall", "@zone[" .. z .. "]", "masq", "1") nc:set("firewall", "@zone[" .. z .. "]", "masq_src", masq_src) end nc:commit("firewall") end -- setup node lan dhcp local function load_dhcp_tags(dhcptagsfile) local dhcp_tags = {} for line in io.lines(dhcptagsfile) do if not (line:match("^%s*#") or line:match("^%s*$")) then local name, condition, pattern = line:match("(%S+)%s+(%S+)%s+(.*)") if pattern then table.insert(get_subtable(dhcp_tags, condition), {name = name, pattern = pattern}) end end end return dhcp_tags end local function load_dhcp_options(dhcpoptionsfile) local dhcp_options = {} if nixio.fs.access(dhcpoptionsfile) then for line in io.lines(dhcpoptionsfile) do if not (line:match("^%s*#") or line:match("^%s*$")) then local tag, force, opt_num, opt_val = line:match("(%S*)%s+(%w+)%s+(%d+)%s+(.*)") if opt_val then local by_tag = get_subtable(dhcp_options, tag) if tag == "" and force == FORCED then force = UNFORCED -- force is unsupported for untagged options end table.insert(get_subtable(by_tag, force), {num = opt_num, val = opt_val}) end end end end return dhcp_options end local function option_item(tag, option) local parts = {} if tag ~= "" then table.insert(parts, "tag:" .. tag) end table.insert(parts, option.num) table.insert(parts, option.val) return table.concat(parts, ",") end local function create_classifying_section(condition, cond_list) for i, props in ipairs(cond_list) do local secname = condition local pat = props.pattern if (condition == "subscriberid") then secname = "subscrid" pat = '"' .. pat:gsub('"', '\\"') .. '"' print(props.pattern, "->", pat) end nc:add("dhcp", secname) local section_ref = string.format("@%s[%d]", secname, i-1) nc:set("dhcp", section_ref, "networkid", props.name) nc:set("dhcp", section_ref, condition, pat) end end local function create_tag_section(tag, force, optlist) if tag ~= "" then nc:set("dhcp", tag, "tag") if force == FORCED then nc:set("dhcp", tag, "force", 1) end local options = {} for _, option in ipairs(optlist) do table.insert(options, option_item("", option)) -- tag is on section end nc:set("dhcp", tag, "dhcp_option", options) end end do local dhcp_option_list = {} if nc:get("aredn", "@wan[0]", "lan_dhcp_route") == "1" or nc:get("aredn", "@wan[0]", "lan_dhcp_defaultroute") == "1" then -- Provide stateless routes and default route table.insert(dhcp_option_list, "121,10.0.0.0/8," .. cfg.lan_ip .. ",0.0.0.0/0," .. cfg.lan_ip) table.insert(dhcp_option_list, "249,10.0.0.0/8," .. cfg.lan_ip .. ",0.0.0.0/0," .. cfg.lan_ip) else -- Provide stateless routes to the mesh, and a blank default route (option 3 has no values) to -- suppress default route being sent table.insert(dhcp_option_list, "121,10.0.0.0/8," .. cfg.lan_ip) table.insert(dhcp_option_list, "249,10.0.0.0/8," .. cfg.lan_ip) table.insert(dhcp_option_list, "3") end local advanced_options = load_dhcp_options(dhcpoptionsfile) if nixio.fs.access(dhcptagsfile) then for condition, cond_list in pairs(load_dhcp_tags(dhcptagsfile)) do create_classifying_section(condition, cond_list) end end for tag, forcelist in pairs(advanced_options) do if forcelist then if tag == "" -- tag-section name cannot be empty or tablesize(forcelist) > 1 -- section name must be unique then -- place unforced options in the anonymous section for _, option in ipairs(forcelist[UNFORCED]) do table.insert(dhcp_option_list, option_item(tag, option)) end forcelist[UNFORCED] = nil end for force, optlist in pairs(forcelist) do create_tag_section(tag, force, optlist) end end end if #dhcp_option_list > 0 then nc:set("dhcp", "@dhcp[0]", "dhcp_option", dhcp_option_list) end end nc:commit("dhcp") -- generate the wireless config file local config = "" local ifacecount = aredn.hardware.get_radio_count() local devpaths = {} for dev = 0, ifacecount - 1 do local devname = "phy" .. dev local radio = "radio" .. dev local wlan = "wlan" .. dev local devpath = nixio.fs.realpath(ieee80211 .. nixio.fs.readlink(ieee80211 .. devname)):match("^/sys/devices/(.*)/ieee802.*$") if devpath:match("^platform.*/pci.*") then devpath = devpath:match("^platform/(.*)") end local devpathc = devpaths[devpath] or 0 devpaths[devpath] = devpathc + 1 if devpathc > 0 then devpath = devpath .. "+" .. devpathc end local is_mesh_rf = false local htmode = "HT20" local disabled = "0" local chanbw = nil local country = nil local channel = nil local distance = nil local hwmode = "11g" if iwinfo.nl80211.freqlist(devname)[1].mhz > 5000 then hwmode="11a" end local network = nil local mode = nil local ssid = nil local encryption = nil local key = nil if wlan == cfg.wifi_intf then -- mesh RF adhoc configuration is_mesh_rf = true channel = cfg.wifi_channel chanbw = cfg.wifi_chanbw country = "HX" distance = cfg.wifi_distance ssid = cfg.wifi_ssid .. "-" .. chanbw .. "-v3" mode = "adhoc" encryption = "none" network = "wifi" elseif cfg.wifi2_enable == "1" and (ifacecount == 1 or (ifacecount > 1 and hwmode == cfg.wifi2_hwmode)) then -- lan AP interface channel = cfg.wifi2_channel ssid = h2s(cfg.wifi2_ssid) mode = "ap" encryption = cfg.wifi2_encryption key = h2s(cfg.wifi2_key) network = "lan" elseif cfg.wifi3_enable == "1" and (ifacecount == 1 or (ifacecount > 1 and hwmode == cfg.wifi3_hwmode)) then -- wan client ssid = h2s(cfg.wifi3_ssid) mode = "sta" if cfg.wifi3_key and cfg.wifi3_key ~= "" then encryption = "psk2" key = h2s(cfg.wifi3_key) else encryption = "none" end network = "wan" htmode = nil else disabled = "1" end config = config .. "config wifi-device '" .. radio .. "'\n option type 'mac80211'\n" config = config .. " option disabled '" .. disabled .. "'\n" if channel then config = config .. " option channel '" .. channel .. "'\n" end if chanbw then config = config .. " option chanbw '" .. chanbw .. "'\n" end if country then config = config .. " option country '" .. country .. "'\n" end if distance then config = config .. " option distance '" .. distance .. "'\n" end config = config .. " option hwmode '" .. hwmode .. "'\n" if htmode then config = config .. " option htmode '" .. htmode .. "'\n" end config = config .. " option path '" .. devpath .. "'\n\n" config = config .. "config wifi-iface\n" config = config .. " option ifname '" .. wlan .. "'\n" config = config .. " option device '" .. radio .. "'\n" if network then config = config .. " option network '" .. network .. "'\n" end if mode then config = config .. " option mode '" .. mode .. "'\n" end if ssid then config = config .. " option ssid '" .. ssid .. "'\n" end if encryption then config = config .. " option encryption '" .. encryption .. "'\n" end if key then config = config .. " option key '" .. key .. "'\n" end config = config .. "\n" if is_mesh_rf and wifi_mon_enable then config = config .. "config wifi-iface\n" config = config .. " option ifname '" .. wlan .. "-1'\n" config = config .. " option device '" .. radio .. "'\n" config = config .. " option network 'wifi_mon'\n option mode 'monitor'\n\n" end end write_all("/tmp/new_config/wireless", config) -- indicate whether lan is running in dmz mode nc:set("aredn", "@dmz[0]", "mode", cfg.dmz_mode) nc:commit("aredn") -- generate the host and ethers files local h = io.open("/etc/hosts", "w") local e = io.open("/etc/ethers", "w") if h and e then h:write("# automatically generated file - do not edit\n") h:write("# use /etc/hosts.user for custom entries\n") h:write("127.0.0.1\tlocalhost\n") if is_notnull(cfg.wifi_ip) then h:write(cfg.lan_ip .. "\tlocalnode\n") h:write(cfg.wifi_ip .. "\t" .. node .. " " .. tactical .. "\n") else h:write(cfg.lan_ip .. "\tlocalnode " .. node .. " " .. tactical .. "\n") end if is_notnull(cfg.dtdlink_ip) then h:write(cfg.dtdlink_ip .. "\tdtdlink." .. node .. ".local.mesh dtdlink." .. node .."\n") end if is_nat_mode() then h:write(decimal_to_ip(ip_to_decimal(cfg.lan_ip) + 1) .. "\tlocalap\n") end e:write("# automatically generated file - do not edit\n") e:write("# use /etc/ethers.user for custom entries\n") local netaddr = nixio.bit.band(ip_to_decimal(cfg.lan_ip), ip_to_decimal(cfg.lan_mask)) if nixio.fs.access(dhcpfile) then for line in io.lines(dhcpfile) do if not (line:match("^%s*#") or line:match("^%s*$")) then local mac, ip, host, noprop = line:match("(%S+)%s+(%S+)%s+(%S+)%s*(%S*)") if mac and ip and host and noprop then ip = decimal_to_ip(netaddr + ip) if validate_same_subnet(ip, cfg.lan_ip, cfg.lan_mask) and validate_ip_netmask(ip, cfg.lan_mask) then h:write(ip .. "\t" .. host .. " " .. noprop .. "\n") e:write(mac .. "\t" .. ip .. "\n") end end end end end -- aliases need to ba added to /etc/hosts or they will now show up on the localnode -- nor will the services thehy offer -- also add a comment to the hosts file so we can display the aliases differently if needed local f = io.open(aliasfile, "r") if f then for line in f:lines() do if not (line:match("^%s*#") or line:match("^%s*$")) then local ip, host = line:match("(%S+)%s+(%S+)") if ip then if host:match("%.") and not host:match("%.local%.mesh$") then host = host .. ".local.mesh" end h:write(ip .. "\t" .. host .. " #ALIAS\n") end end end f:close() end h:write("\n") if nixio.fs.access("/etc/hosts.user", "r") then for line in io.lines("/etc/hosts.user") do h:write(line .. "\n") end end if nixio.fs.access("/etc/ethers.user", "r") then for line in io.lines("/etc/ethers.user") do e:write(line .. "\n") end end h:close() e:close() end -- generate olsrd.conf if nixio.fs.access("/etc/config.mesh/olsrd", "r") then local of = io.open("/tmp/new_config/olsrd", "w") if of then for line in io.lines("/etc/config.mesh/olsrd") do if line:match("") then if is_null(cfg.olsrd_bridge) then line = line:gsub("", '"wifi" "lan"') else line = line:gsub("", '"lan"') end elseif line:match("^[^#]") then for parm in line:gmatch("<([^%s]*)>") do line = line:gsub("<" .. parm .. ">", cfg[parm]) end end of:write(line .. "\n") end if is_dmz_mode() then local a, b, c, d = cfg.dmz_lan_ip:match("(.*)%.(.*)%.(.*)%.(.*)") of:write(string.format("\nconfig Hna4\n\toption netaddr %s.%s.%s.%d\n\toption netmask 255.255.255.%d\n\n", a, b, c, d - 1, nixio.bit.band(255 * 2 ^ cfg.dmz_mode, 255))) end if is_altnet_mode() then local a, b, c, d = cfg.lan_ip:match("(.*)%.(.*)%.(.*)%.(.*)") of:write(string.format("\nconfig Hna4\n\toption netaddr %s.%s.%s.%d\n\toption netmask %s\n\n", a, b, c, d - 1, cfg.lan_mask)) end if cfg.wifi_enable ~= "1" and is_notnull(cfg.wifi_ip) then of:write(string.format("config Hna4\n\toption netaddr %s\n\toption netmask 255.255.255.255\n\n", cfg.wifi_ip)) end if is_supernode then of:write("config Hna4\n\toption netaddr 10.0.0.0\n\toption netmask 255.0.0.0\n\n") local altnetwork = nc:get("aredn", "@supernode[0]", "altnetwork") local altnetmask = nc:get("aredn", "@supernode[0]", "altnetmask") if altnetwork and altnetmask then of:write("config Hna4\n\toption netaddr " .. altnetwork .. "\n\toption netmask " .. altnetmask .. "\n\n") end end if nixio.fs.stat("/etc/config.mesh/xlink") then uci.cursor("/etc/config.mesh"):foreach("xlink", "interface", function(section) if section.netmask ~= "255.255.255.255" then local addr = decimal_to_ip(nixio.bit.band(ip_to_decimal(section.ipaddr), ip_to_decimal(section.netmask))) of:write(string.format("config Hna4\n\toption netaddr %s\n\toption netmask %s\n\n", addr, section.netmask)) end end ) end if nc:get("aredn", "@wan[0]", "olsrd_gw") == "1" then of:write("config LoadPlugin\n\toption library 'olsrd_dyn_gw.so.0.5'\n\toption Interval '60'\n\tlist Ping '8.8.8.8'\n\tlist Ping '8.8.4.4'\n\n") end of:write("config LoadPlugin 'nameservice'\n\toption library 'olsrd_nameservice.so.0.4'\n\toption interval '30'\n\toption timeout '300'\n\toption sighup_pid_file '/var/run/dnsmasq/dnsmasq.pid'\n\toption name_change_script '/usr/local/bin/olsrd-namechange'\n") aredn.services.reset_validation() local names, hosts, services = aredn.services.get() for _, name in ipairs(names) do of:write("\tlist name '" .. name .. "'\n") end for _, host in ipairs(hosts) do if host.host ~= "" then of:write("\tlist hosts '" .. host.ip .. " " .. host.host .. "'\n") end end for _, service in ipairs(services) do of:write("\tlist service '" .. service .. "'\n") end of:write("\n") -- add all the tunnel interfaces if vtunclients + vtunservers + wgclients + wgservers > 0 then for weight, ifaces in pairs(tun_weights) do of:write("\nconfig Interface\n") for _, iface in ipairs(ifaces) do of:write("\tlist interface '" .. iface .. "'\n") end of:write("\toption Ip4Broadcast '255.255.255.255'\n") weight = tonumber(weight) if weight < 1 then of:write("\toption Mode 'ether'\n") elseif weight > 1 then of:write("\toption LinkQualityMult 'default " .. (1 / weight) .. "'\n") end of:write("\toption HelloInterval '" .. cfg.hello_interval .. "'\n") of:write("\toption TcInterval '" .. cfg.tc_interval .. "'\n") of:write("\toption MidInterval '" .. cfg.mid_interval .. "'\n") of:write("\toption HnaInterval '" .. cfg.hna_interval .. "'\n") end end nc:set("aredn", "@tunnel[0]", "maxclients", maxclients) nc:set("aredn", "@tunnel[0]", "maxservers", maxservers) nc:commit("aredn") -- add xlink interfaces if nixio.fs.stat("/etc/config.mesh/xlink") then uci.cursor("/etc/config.mesh"):foreach("xlink", "interface", function(section) of:write("\nconfig Interface\n\tlist interface '" .. section[".name"] .. "'\n") if section.peer then of:write("\toption Ip4Broadcast '" .. section.peer .. "'\n") else of:write("\toption Ip4Broadcast '255.255.255.255'\n") end local weight = tonumber(section.weight or 0) if weight then if weight > 1 then of:write("\toption LinkQualityMult 'default " .. (1 / weight) .. "'\n") elseif weight < 1 then of:write("\toption Mode 'ether'\n") end else of:write("\toption Mode 'ether'\n") end of:write("\toption HelloInterval '" .. cfg.hello_interval .. "'\n") of:write("\toption TcInterval '" .. cfg.tc_interval .. "'\n") of:write("\toption MidInterval '" .. cfg.mid_interval .. "'\n") of:write("\toption HnaInterval '" .. cfg.hna_interval .. "'\n") end ) end -- OLSRD user extras if nixio.fs.stat("/etc/aredn_include/olsrd.user") then of:write("\n") of:write(expand_vars(read_all("/etc/aredn_include/olsrd.user"))) end of:close() end end -- Update user firewall local _, diff = filecopy("/etc/config.mesh/firewall.user", "/etc/firewall.user", true) if diff then changes.firewall = true end -- Update services script local sf = io.open("/tmp/local_services", "w") if sf then sf:write("#!/bin/sh\n") if cfg.wifi_proto ~= "disabled" then local wifi_channel = tonumber(cfg.wifi_channel) if is_null(cfg.wifi_txpower) or tonumber(cfg.wifi_txpower) > aredn.hardware.wifi_maxpower(cfg.wifi_intf, wifi_channel) then cfg.wifi_txpower = aredn.hardware.wifi_maxpower(cfg.wifi_intf, wifi_channel) elseif tonumber(cfg.wifi_txpower) < 1 then cfg.wifi_txpower = 1 end if cfg.wifi_enable == "1" then sf:write("/usr/sbin/iw dev " .. cfg.wifi_intf .. " set txpower fixed " .. cfg.wifi_txpower .. "00\n") end if is_notnull(cfg.aprs_lat) and is_notnull(cfg.aprs_lon) then nc:set("aredn", "@location[0]", "lat", cfg.aprs_lat) nc:set("aredn", "@location[0]", "lon", cfg.aprs_lon) nc:commit("aredn") end end sf:close() local _, diff = filecopy("/tmp/local_services", "/etc/local/services", true) if diff then changes.localservices = true end os.remove("/tmp/local_services") nixio.fs.chmod("/etc/local/services", "777") end --- --- Make it official --- -- Handle special cases local config_special = { dmz_mode = c:get("aredn", "@dmz[0]", "mode"), lqm_enable = c:get("aredn", "@lqm[0]", "enable"), tunnel_weight = c:get("aredn", "@tunnel[0]", "weight"), supernode_enable = c:get("aredn", "@supernode[0]", "enable"), watchdog_enable = c:get("aredn", "@watchdog[0]", "enable"), watchdog_pings = c:get("aredn", "@watchdog[0]", "ping_addresses"), watchdog_daily = c:get("aredn", "@watchdog[0]", "daily"), web_access = c:get("aredn", "@wan[0]", "web_access"), ssh_access = c:get("aredn", "@wan[0]", "ssh_access"), telnet_access = c:get("aredn", "@wan[0]", "telnet_access"), wifi_mode_0 = c:get("wireless", "@wifi-iface[0]", "mode"), wifi_mode_1 = c:get("wireless", "@wifi-iface[1]", "mode"), wifi_channel_0 = c:get("wireless", "@wifi-device[0]", "channel"), wifi_channel_1 = c:get("wireless", "@wifi-device[1]", "channel") } local nfiles = {} for file in nixio.fs.glob("/tmp/new_config/*") do nfiles[nixio.fs.basename(file)] = true end -- Remove files we no longer need for file in nixio.fs.glob("/etc/config/*") do if not nfiles[nixio.fs.basename(file)] then nixio.fs.remove(file) changes.reboot = true end end for file, _ in pairs(nfiles) do local ffile = "/tmp/new_config/" .. file local _, diff = filecopy(ffile, "/etc/config/" .. file, true) if diff then if file == "system" then changes.log = true changes.system = true elseif file == "aredn" then local oc = uci:cursor() if oc:get("aredn", "@dmz[0]", "mode") ~= config_special.dmz_mode then changes.reboot = true end if oc:get("aredn", "@lqm[0]", "enable") ~= config_special.lqm_enable then changes.manager = true end if oc:get("aredn", "@tunnel[0]", "weight") ~= config_special.tunnel_weight then changes.olsrd = true end if oc:get("aredn", "@supernode[0]", "enable") ~= config_special.supernode_enable then changes.reboot = true end if oc:get("aredn", "@watchdog[0]", "enable") ~= config_special.watchdog_enable then changes.reboot = true end if oc:get("aredn", "@watchdog[0]", "ping_addresses") ~= config_special.watchdog_pings then changes.manager = true end if oc:get("aredn", "@watchdog[0]", "daily") ~= config_special.watchdog_daily then changes.manager = true end if oc:get("aredn", "@wan[0]", "web_access") ~= config_special.web_access or oc:get("aredn", "@wan[0]", "ssh_access") ~= config_special.ssh_access or oc:get("aredn", "@wan[0]", "telnet_access") ~= config_special.telnet_access then changes.firewall = true end elseif file == "network" then changes.network = true changes.tunnels = true -- restarting network devices requires tunnels to restart elseif file == "dhcp" then changes.dnsmasq = true elseif file == "olsrd" then changes.olsrd = true elseif file == "firewall" then changes.firewall = true elseif file == "wireless" then local oc = uci:cursor() if oc:get("wireless", "@wifi-device[0]", "channel") ~= config_special.wifi_channel_0 or oc:get("wireless", "@wifi-device[1]", "channel") ~= config_special.wifi_channel_1 then changes.manager = true end if oc:get("wireless", "@wifi-iface[0]", "mode") ~= config_special.wifi_mode_0 or oc:get("wireless", "@wifi-iface[1]", "mode") ~= config_special.wifi_mode_1 then -- Only start the hostapd (etc) if we need to. This doesn't change what is currently running -- only what automatically runs in the future if oc:get("wireless", "@wifi-iface[0]", "mode") == "ap" or oc:get("wireless", "@wifi-iface[1]", "mode") == "ap" then os.execute("/etc/init.d/wpad enable > /dev/null 2>&1") else os.execute("/etc/init.d/wpad disable > /dev/null 2>&1") end changes.reboot = true else changes.wireless = true end elseif file == "vtun" then changes.tunnels = true else changes.reboot = true end end nixio.fs.remove(ffile) end nixio.fs.rmdir("/tmp/new_config") aredn.info.set_nvram("config", "mesh") aredn.info.set_nvram("node", node) aredn.info.set_nvram("tactical", tactical) -- Set file flags for whichever parts of the system require a restart/reboot for k, v in pairs(changes) do if v then nixio.fs.mkdir("/tmp/reboot-required") io.open("/tmp/reboot-required/" .. k, "w"):close() end end return 0