#!/opt/lnxall_app/bin/lua -- Created by jiaqiang.ye@lnxall.com -- Simple UCI-confugured, NetworkManager-based utility -- 2023/11/02 -- load external Lua modules local bit = require 'bit' local uci = require 'uci' local posix = require 'posix' local nmcli = require 'nmcli' local invoker = require 'invoker' local BDHCPD_SVR = 'bdhcpd.service' local HOSTAPD_SVR = 'lnxhostapd.service' local BDHCPD_CFG = "/etc/udhcpd.conf" local HOSTAPD_CFG = "/etc/hostapd.conf" -- rename function local gfmt = string.format local g_changed_lan = false local function setup_env() local env_list = { "LANG", "LC_ADDRESS", "LC_IDENTIFICATION", "LC_MEASUREMENT", "LC_MONETARY", "LC_NAME", "LC_NUMERIC", "LC_PAPER", "LC_TELEPHONE", "LC_TIME", "LC_ALL" } for _, envn in ipairs(env_list) do posix.setenv(envn) end -- override PATH to call nothup: posix.setenv('PATH', '/opt/lnxall_app/bin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin', 1) -- finally, lock a temporary file local okay, errmsg = invoker.waitlock('/tmp/.netuci-lock') if not okay then if type(errmsg) ~= "string" then errmsg = "unknown error" end io.stderr:write(gfmt("Error, failed to lock file for netuci: %s\n", errmsg)) io.stderr:flush() end return true end local function has_netdev(ndev) if type(ndev) ~= "string" then return false end if string.len(ndev) == 0 then return false end return invoker.invoke(invoker.NOSTDIO, "ip", "link", "show", "dev", ndev) == 0 end local function check_proto(prot) if type(prot) ~= "string" then return false end if prot == "dhcp" or prot == "static" then return true end return false end local function check_macaddr(maddr) if type(maddr) ~= "string" then return nil end if string.len(maddr) ~= 17 then return nil end local addrs = {} for hex in string.gmatch(maddr, "%x+") do local hexnum = tonumber("0x" .. hex) if hexnum and hexnum < 256 then addrs[#addrs + 1] = hexnum end end if #addrs ~= 6 then return nil end return gfmt('%02x:%02x:%02x:%02x:%02x:%02x', addrs[1], addrs[2], addrs[3], addrs[4], addrs[5], addrs[6]) end local function down_all_wlan(w0, w1, w2) if w0 then invoker.invoke(invoker.NOSTDIO, "ip", "link", "set", "dev", w0, "down") end if w1 then invoker.invoke(invoker.NOSTDIO, "ip", "link", "set", "dev", w1, "down") end if w2 then invoker.invoke(invoker.NOSTDIO, "ip", "link", "set", "dev", w2, "down") end end local function dhcp_addrp(ipv4, nmask) local ips = 0 for ipe in string.gmatch(ipv4, "%d+") do ips = ips * 256 + tonumber(ipe) end if type(nmask) ~= "number" or nmask == 0 or nmask >= 32 then io.stderr:write(gfmt("Error, invalid DHCP server netmask: %s\n", type(nmask))) io.stderr:flush() return "192.168.1." end local mask = bit.bnot(bit.lshift(0x1, 32 - nmask) - 0x1) ips = bit.band(ips, mask, 0xFFFFFFFF) return string.format("%d.%d.%d.", bit.band(0xFF, bit.rshift(ips, 24)), bit.band(0xFF, bit.rshift(ips, 16)), bit.band(0xFF, bit.rshift(ips, 8))) end local function start_hostapd_server(wlan) local cfgs = {} cfgs[#cfgs + 1] = "interface=" .. wlan['netdev'] cfgs[#cfgs + 1] = "bridge=br-lan" cfgs[#cfgs + 1] = "driver=nl80211" cfgs[#cfgs + 1] = "logger_syslog=-1" cfgs[#cfgs + 1] = "logger_syslog_level=3" local ctrl_i = "/var/run/hostapd" cfgs[#cfgs + 1] = "ctrl_interface=" .. ctrl_i cfgs[#cfgs + 1] = "ctrl_interface_group=0" cfgs[#cfgs + 1] = "hw_mode=g" cfgs[#cfgs + 1] = "channel=" .. tostring(wlan['channel']) if wlan['passwd'] then cfgs[#cfgs + 1] = "wpa=2" cfgs[#cfgs + 1] = "auth_algs=3" cfgs[#cfgs + 1] = "wpa_key_mgmt=WPA-PSK" cfgs[#cfgs + 1] = "wpa_passphrase=" .. wlan['passwd'] else cfgs[#cfgs + 1] = "auth_algs=1" end local ssid = wlan['ssid'] if ssid == 'lnxall_def_SN' then local fini = invoker.readfile("/app/config/fac.ini") if type(fini) == "string" then local gwsn = string.match(fini, "sn%s*=%s*([^\r\n%s]+)") if gwsn then ssid = 'lnxall_def_' .. gwsn end end end cfgs[#cfgs + 1] = 'ssid=' .. ssid cfgs[#cfgs + 1] = "\n" cfgs = table.concat(cfgs, "\n") local oldcfg = invoker.readfile(HOSTAPD_CFG) if oldcfg ~= cfgs then local filp = io.open(HOSTAPD_CFG, "wb") if filp then filp:write(cfgs) filp:close(); filp = nil end invoker.invoke(invoker.NOSTDIO, "rm", "-rf", string.format("%s/*", ctrl_i)) invoker.invoke(invoker.NOSTDIO, "systemctl", "restart", HOSTAPD_SVR) else invoker.invoke(invoker.NOSTDIO, "systemctl", g_changed_lan and "restart" or "start", HOSTAPD_SVR) end invoker.invoke(invoker.NOSTDIO, "systemctl", "enable", HOSTAPD_SVR) end local function stop_hostapd_server() invoker.invoke(invoker.NOSTDIO, "systemctl", "stop", HOSTAPD_SVR) invoker.invoke(invoker.NOSTDIO, "systemctl", "disable", HOSTAPD_SVR) end local function start_dhcp_server(lcfg) local lfile = '/run/udhcpd.leases' if posix.access(lfile) ~= 0 then invoker.invoke(invoker.NOSTDIO, "touch", lfile) end local ucfg, addrp = {}, dhcp_addrp(lcfg.ipaddr, lcfg.netmask) ucfg[#ucfg + 1] = gfmt("start %s150", addrp) ucfg[#ucfg + 1] = gfmt("end %s200", addrp) ucfg[#ucfg + 1] = "interface br-lan" ucfg[#ucfg + 1] = gfmt("opt dns %s %s", lcfg.dns1 or '8.8.8.8', lcfg.dns2 or '223.5.5.5') ucfg[#ucfg + 1] = gfmt("option subnet %s0", dhcp_addrp('255.255.255.255', lcfg.netmask)) ucfg[#ucfg + 1] = gfmt("opt router %s", lcfg.ipaddr) ucfg[#ucfg + 1] = "option lease 7200 # default: 2 hours" ucfg[#ucfg + 1] = "pidfile /run/udhcpd.pid" ucfg[#ucfg + 1] = gfmt("lease_file %s", lfile) ucfg[#ucfg + 1] = "\n" ucfg = table.concat(ucfg, "\n") if ucfg == invoker.readfile(BDHCPD_CFG) then io.stdout:write("INFO: busybox based DHCP server configuration not changed.\n") io.stdout:flush() invoker.invoke(invoker.NOSTDIO, 'systemctl', g_changed_lan and 'restart' or 'start', BDHCPD_SVR) invoker.invoke(invoker.NOSTDIO, 'systemctl', 'enable', BDHCPD_SVR) return true end local filp = io.open(BDHCPD_CFG, "wb") if not filp then io.stderr:write("Error, failed to open DHCP server config!\n") io.stderr:flush() return false end filp:write(ucfg) filp:close(); filp = nil -- restart DHCP service invoker.invoke(invoker.NOSTDIO, 'systemctl', 'enable', BDHCPD_SVR) invoker.invoke(invoker.NOSTDIO, 'systemctl', 'restart', BDHCPD_SVR) return true end local function stop_dhcp_server() invoker.invoke(invoker.NOSTDIO, 'systemctl', 'stop', BDHCPD_SVR) invoker.invoke(invoker.NOSTDIO, 'systemctl', 'disable', BDHCPD_SVR) posix.unlink(BDHCPD_CFG); posix.sync() return true end -- check whether there is a network device used more than once local function filter_netdev(wcfgs, lcfg) local nlist, okay = {}, true for _, wcfg in pairs(wcfgs) do local netdev = wcfg["netdev"] if type(netdev) ~= "string" then io.stderr:write(gfmt("Error, invalid type of network device: %s\n", type(netdev))) io.stderr:flush() elseif not wcfg['disabled'] and nlist[netdev] then okay = false io.stderr:write(gfmt("Error, network device used twice: %s\n", netdev)) io.stderr:flush() elseif not wcfg['disabled'] then nlist[netdev] = true -- wcfg["iface"] end end local lndevs = type(lcfg) == "table" and lcfg["netdevs"] or nil if type(lndevs) == "table" then for _, ndev in ipairs(lndevs) do local wcfg = wcfgs[ndev] if nlist[ndev] and type(wcfg) == "table" and not wcfg['disabled'] then okay = false io.stderr:write(gfmt("Error, network device used many times: %s\n", ndev)) io.stderr:flush() else nlist[ndev] = true end end end nlist = nil return okay end local function load_wancfg(uhdl, name) local iface = uhdl:get('network', name) if type(iface) ~= "string" or iface ~= "interface" then -- io.stderr:write(gfmt("INFO: WAN interface not found: %s\n", name)) -- io.stderr:flush() return nil end local netdev = uhdl:get('network', name, 'device') if not has_netdev(netdev) then io.stderr:write(gfmt("Warning: WAN device not found for %s\n", name)) io.stderr:flush() return nil end local proto = uhdl:get('network', name, 'proto') if not check_proto(proto) then io.stderr:write(gfmt("ERROR: invalid protocol for %s/%s\n", name, netdev)) io.stderr:flush() return nil end local is_dhcpv6 = false local enable = uhdl:get('network', name, 'dhcpv6') if type(enable) == "number" and enable == 1 then is_dhcpv6 = true elseif type(enable) == "string" and enable == "1" then is_dhcpv6 = true end local disabled = false -- interface disabled ? local tmpval = uhdl:get('network', name, 'disabled') if type(tmpval) == "number" and tmpval == 1 then disabled = true elseif type(tmpval) == "string" and tmpval == "1" then disabled = true end tmpval = uhdl:get('network', name, 'macaddr') local macaddr = check_macaddr(tmpval) if tmpval and not macaddr then io.stderr:write(gfmt("ERROR: invalid macaddr for %s/%s", name, netdev)) io.stderr:flush() return nil end local metric = 0 -- if not specified, default to zero tmpval = uhdl:get('network', name, 'metric') if type(tmpval) == "string" then tmpval = tonumber(tmpval) end if type(tmpval) == "number" and tmpval >= 0 then metric = math.floor(tmpval) end local dns1, dns2 = nil, nil local dns = uhdl:get('network', name, 'dns') if type(dns) == "table" then if invoker.isipv4(dns[1]) then dns1 = dns[1] end if invoker.isipv4(dns[2]) then dns2 = dns[2] end end local wcfg = { ["iface"] = name, ["netdev"] = netdev, ["macaddr"] = macaddr, ["proto"] = proto, ["disabled"] = disabled, ["metric"] = metric, dns1 = dns1, dns2 = dns2, ["dhcpv6"] = is_dhcpv6 } if proto == "dhcp" then return wcfg end local ipaddr = uhdl:get('network', name, 'ipaddr') if type(ipaddr) ~= "string" or string.len(ipaddr) == 0 then io.stderr:write(gfmt("ERROR: invalid static IP address for %s/%s\n", name, netdev)) io.stderr:flush() return nil end if not invoker.isipv4(ipaddr) then io.stderr:write(gfmt("ERROR: invalid static IP address: %s\n", ipaddr)) io.stderr:flush() return nil end local netmask = uhdl:get('network', name, 'netmask') netmask = invoker.ismaskv4(netmask) if not netmask then io.stderr:write(gfmt("Warning, invalid netmask specified for %s/%s, using default 255.255.255.0\n", name, netdev)) io.stderr:flush() netmask = invoker.ismaskv4('255.255.255.0') end local gwip = uhdl:get('network', name, 'gateway') if type(gwip) ~= "string" then gwip = nil elseif not invoker.isipv4(gwip) then io.stderr:write(gfmt("Error, invalid gateway IP address for %s/%s\n", name, netdev)) io.stderr:flush() return nil end wcfg["ipaddr"] = ipaddr wcfg["netmask"] = netmask wcfg["gateway"] = gwip return wcfg end local function load_wancfgs(path) if not path then path = '/etc/config' end local xuci = uci.cursor(path) local ifaces = { 'wan', 'wan1', 'wan2', 'wan3', 'wan4', 'wwan' } local wcfgs, count = {}, 0 for _, iface in ipairs(ifaces) do local cfg = load_wancfg(xuci, iface) if cfg then count = count + 1 wcfgs[iface] = cfg end end xuci:close() xuci = nil if count > 0 then return wcfgs end return nil end local function load_lancfg(path) if not path then path = '/etc/config' end local landev = '@device[0]' local xuci = uci.cursor(path) local ltype = xuci:get('network', landev, 'type') if ltype ~= "bridge" then local l_type = type(ltype) if l_type ~= "string" then ltype = l_type end io.stderr:write(gfmt("Error, invalid br-lan type: %s\n", ltype)) io.stderr:flush() xuci:close(); xuci = nil return nil end local ports = xuci:get('network', landev, 'ports') if type(ports) ~= "table" or #ports == 0 then ports = {} io.stderr:write("Warning, no ports specified for br-lan\n") io.stderr:flush() -- xuci:close(); xuci = nil -- return nil end local ndevs, idx = {}, 0 while true do idx = idx + 1 local nports = ports[idx] if type(nports) ~= "string" then break end for port in string.gmatch(nports, "[^%s]+") do if has_netdev(port) then ndevs[#ndevs + 1] = port else io.stderr:write(gfmt("Error, network device not found for br-lan: %s\n", port)) io.stderr:flush() end end end if #ndevs == 0 then io.stderr:write("Warning, no valid network device found for br-lan.\n") io.stderr:flush() -- xuci:close(); xuci = nil -- return nil end local tmpval = xuci:get('network', 'lan', 'device') if tmpval ~= "br-lan" then io.stderr:write("Error, invalid device specified for lan.\n") io.stderr:flush() xuci:close(); xuci = nil return nil end tmpval = xuci:get('network', 'lan', 'proto') if tmpval ~= "static" then io.stderr:write("Error, invalid protocol for lan, must be static.\n") io.stderr:flush() xuci:close(); xuci = nil return nil end local ipaddr = xuci:get('network', 'lan', 'ipaddr') if not invoker.isipv4(ipaddr) then io.stderr:write("Error, invalid IPv4 address specified for lan.\n") io.stderr:flush() xuci:close(); xuci = nil return nil end local netmask = xuci:get('network', 'lan', 'netmask') netmask = invoker.ismaskv4(netmask) if not netmask or netmask > 24 then io.stderr:write("Error, invalid netmask specified for lan.\n") io.stderr:flush() xuci:close(); xuci = nil return nil end local dns1, dns2 = nil, nil local dns = xuci:get('network', 'lan', 'dns') if type(dns) == "table" then if invoker.isipv4(dns[1]) then dns1 = dns[1] end if invoker.isipv4(dns[2]) then dns2 = dns[2] end end local disabled = xuci:get('network', 'lan', 'disabled') if disabled == "1" or disabled == 1 then disabled = true else disabled = false end local langw, lanmtr = xuci:get('network', 'lan', 'gateway'), xuci:get('network', 'lan', 'metric') if invoker.isipv4(langw) then if type(lanmtr) == "string" and #lanmtr > 0 then lanmtr = tonumber(lanmtr) end if type(lanmtr) == "number" and lanmtr >= 0 then lanmtr = math.floor(lanmtr) else lanmtr = 200 end else langw, lanmtr = nil, 999 end local tmpval = xuci:get('network', 'lan', 'macaddr') local macaddr = check_macaddr(tmpval) if tmpval and not macaddr then io.stderr:write(gfmt("ERROR: invalid macaddr for br-lan: %s\n", tmpval)) io.stderr:flush() end xuci:close(); xuci = nil return { ["ipaddr"] = ipaddr, ["netmask"] = netmask, dns1 = dns1, dns2 = dns2, ["netdevs"] = ndevs, ["disabled"] = disabled, ["gateway"] = langw, ["metric"] = lanmtr, ["macaddr"] = macaddr } end local function load_wificfg(xuci, iface) local dis = xuci:get('wireless', iface, 'disabled') if dis == "1" or dis == 1 then dis = true else dis = false end local netw = xuci:get('wireless', iface, 'network') if netw ~= 'lan' and netw ~= 'wifiwan' then if type(netw) == "string" then io.stderr:write(gfmt("Error, invalid network option for wireless '%s'\n", iface)) io.stderr:flush() end return nil end local mode = xuci:get('wireless', iface, 'mode') if (netw == 'lan' and mode ~= 'ap') or (netw == 'wifiwan' and mode ~= 'sta') then io.stderr:write(gfmt("Error, unsupported mode for wireless '%s'\n", iface)) io.stderr:flush() return nil end local ssid = xuci:get('wireless', iface, 'ssid') if type(ssid) ~= "string" or string.len(ssid) == 0 then io.stderr:write(gfmt("Error, SSID option not specified for '%s'\n", iface)) io.stderr:flush() return nil end local enc = xuci:get('wireless', iface, 'encryption') if type(enc) ~= "string" then io.stderr:write(gfmt("Error, encryption not specified for SSID '%s'\n", ssid)) io.stderr:flush() return nil end local chan = xuci:get('wireless', iface, 'channel') if type(chan) == "string" then if chan == "auto" then chan = 0 else chan = tonumber(chan) end end if type(chan) ~= "number" then chan = 11 end local passwd = nil if enc ~= 'none' then passwd = xuci:get('wireless', iface, 'key') if type(passwd) ~= "string" or #passwd == 0 then io.stderr:write(gfmt("Error, invalid password specified for SSID '%s'\n", ssid)) io.stderr:flush() return nil end end return { ['network'] = netw, ['mode'] = mode, ssid = ssid, ['wifienc'] = enc, ['passwd'] = passwd, ['disabled'] = dis, ['channel'] = chan } end local function load_wificfgs(path) if not path then path = '/etc/config' end local wuci = uci.cursor(path) local ifaces = { 'radio0', 'radio1' } local wcfgs, count = {}, 0 for _, iface in ipairs(ifaces) do local cfg = load_wificfg(wuci, iface) if cfg then count = count + 1 wcfgs[cfg['network']] = cfg end end wuci:close(); wuci = nil if count > 0 then return wcfgs end return nil end local function dump_wancfgs(wcfgs) for _, wcfg in pairs(wcfgs) do io.stdout:write(gfmt("Interface: %s =>\n", wcfg.iface)) io.stdout:write(gfmt(" device: %s\n", wcfg.netdev)) io.stdout:write(gfmt(" protocol: %s\n", wcfg.proto)) io.stdout:write(gfmt(" disabled: %s\n", wcfg.disabled and 'true' or 'false')) io.stdout:write(gfmt(" metric: %d\n", wcfg.metric)) io.stdout:write(gfmt(" macaddr: %s\n", wcfg.macaddr or '')) io.stdout:write(gfmt(" disabled: %s\n", wcfg.disabled and '1' or '0')) if wcfg.proto == 'static' then io.stdout:write(gfmt(" ipaddr: %s\n", wcfg.ipaddr)) io.stdout:write(gfmt(" netmask: %d\n", wcfg.netmask)) io.stdout:write(gfmt(" gateway: %s\n", wcfg.gateway or '')) end io.stdout:write("==============================\n") io.stdout:flush() end end local function config_changed(ucicfg, nmcfg) local fields = { "netdev", "macaddr", "proto", "disabled", "metric", "dns1", "dns2", "ipaddr", "netmask", "gateway", "dhcpv6" } for _, field in ipairs(fields) do local uval, nval = ucicfg[field], nmcfg[field] if uval ~= nval then io.stderr:write(gfmt("INFO: %s has changed for %s: %s <=> %s\n", field, ucicfg["iface"], tostring(uval), tostring(nval))) io.stderr:flush() return true end end if ucicfg['proto'] == 'dhcp' and type(nmcfg['_ipmask']) == "string" then return true end return false end local function wifi_bringup(wcfgs, nmcfgs) -- load wifiwan configuration from `/etc/config/network local nuci = uci.cursor('/etc/config') local wanen, wifiwan = true, 'wifiwan' local wdev, wdevok = nuci:get('network', wifiwan, 'device'), true if type(wdev) ~= "string" or not has_netdev(wdev) then if type(wdev) == "string" then io.stderr:write(gfmt("Error, wifiwan device not found: %s\n", wdev)) io.stderr:flush() end wdevok = false -- wireless network device is not available end local disabled = nuci:get('network', wifiwan, 'disabled') if disabled == "1" or disabled == 1 then wanen = false -- wifiwan is disabled end if wanen and wdevok then -- okay, wifiwan is enabled, we have work to do. local metric = nuci:get('network', wifiwan, 'metric') if metric == nil then metric = 0 end if type(metric) == "string" then metric = tonumber(metric) end if type(metric) ~= "number" then metric = 30 io.stderr:write(gfmt("Error, invalid metric value for '%s'\n", wifiwan)) io.stderr:flush() end -- proto should be dhcp for wifiwan local proto = nuci:get('network', wifiwan, 'proto') if proto ~= 'dhcp' then io.stderr:write("Error, only dhcp protocol is supported by wifiwan\n") io.stderr:flush() end local wan = wcfgs['wifiwan'] if type(wan) == "table" and not wan['disabled'] then wan['netdev'] = wdev wan['metric'] = metric nmcli.wifiwan(wan, nmcfgs) stop_hostapd_server() return true end end if not wdevok then stop_hostapd_server() down_all_wlan("wlan0", "wlan1", "wlan2") return false end local nmcon = nmcli.BRPFX .. wdev -- connection name of wlan local lancfg = nmcfgs[nmcon] -- realtime connection state -- setup wireless AP configuration local wlan = wcfgs['lan'] if type(wlan) ~= 'table' or wlan['disabled'] then if lancfg then -- previous wlan connection exists nmcli.delete(nmcon) -- delete wlan connection end stop_hostapd_server() down_all_wlan("wlan0", "wlan1", "wlan2") return true end wlan['netdev'] = wdev -- set WiFi network device -- call nmcli module method for the dirty work nmcli.wlan(nmcon, wlan, nmcfgs) start_hostapd_server(wlan) return true end local function mainfunc() local wancfgs = load_wancfgs() if not wancfgs then io.stderr:write("Error, no valid configurations found.\n") io.stderr:flush() return 1 end local lancfg = load_lancfg() local wificfgs = load_wificfgs(); if not lancfg then io.stderr:write("Warning: no br-lan configuration found, disabled\n") io.stderr:flush() end if not filter_netdev(wancfgs, lancfg) then return 2 end dump_wancfgs(wancfgs) print('***************************************************') local nmcfgs = nmcli.getall() -- if nmcfgs then dump_wancfgs(nmcfgs) end if not nmcfgs then nmcfgs = {} end -- empty list -- compare the configurations to detect which one has changed for concfg, ccfg in pairs(wancfgs) do local changed = true local nmcfg, changed = nmcfgs[concfg], true if type(nmcfg) == "table" then changed = config_changed(ccfg, nmcfg) end if changed then nmcli.update(ccfg, nmcfg) end nmcfgs[concfg] = nil end -- check and carry out LAN configuration if lancfg and not lancfg['disabled'] then local _, changed = nmcli.updatelan(lancfg, nmcfgs) if changed then g_changed_lan = true end start_dhcp_server(lancfg) else stop_dhcp_server() end -- determine and setup wifiwan if wificfgs then wifi_bringup(wificfgs, nmcfgs) else stop_hostapd_server() down_all_wlan("wlan0", "wlan1", "wlan2") end -- for any extra connections, just DELETION. -- this means that you SHOULD delete any connection -- you want to keep from `nmcfgs for concfg, _ in pairs(nmcfgs) do nmcli.delete(concfg) end return 0 end local function delay_msec(mdelay) local tnow = invoker.uptime() local hnow = tnow + math.floor(mdelay / 1000) + 1 while tnow < hnow do invoker.waitsec(hnow - tnow) tnow = invoker.uptime() end posix.sync() end setup_env() local rval = mainfunc() if type(rval) ~= "number" then rval = 1 end delay_msec(3000) os.exit(rval)