#!/opt/lnxall_app/bin/lua -- Created by jiaqiang.ye@lnxall.com -- UCI network with NetworkManager backend module -- 2023/11/03 local invoker = require 'invoker' local nmcli = {} nmcli.BRPFX = 'lxbr_' nmcli.IPV6M = 'disabled' -- for rk3568 gateway, nmcli is so old that it does not support `ipv6.method disabled local nmok, nmver = invoker.invoke(invoker.OUTPUT, "nmcli", "--version") if nmok == 0 and type(nmver) == "string" and (string.find(nmver, "1.14.6", 1, true) or string.find(nmver, "1.10.6", 1, true)) then nmcli.IPV6M = 'ignore' -- ipv6.method should be 'ignore' on rk3568 gateway end nmok, nmver = nil, nil local function del_con_name(conn) local conlen = 0 if type(conn) == "string" then conlen = string.len(conn) end if conlen <= 0 then return false end if conlen >= 4 and conn:sub(1, 3) == "br-" then return false end if conlen >= 7 and conn:sub(1, 6) == "net4g_" then return false end if conn == "lo" then return false end if conn == "docker0" then return false end if conn == "docker1" then return false end if conn == "virtbr0" then return false end return true end -- function to delete all configured connections nmcli["deleteall"] = function () local fmt = string.format local okay, output = invoker.invoke(invoker.OUTPUT + invoker.CLOSEFD, "nmcli", "-m", "tabular", "-t", "con", "show") if okay ~= 0 then io.stderr:write("Error, failed to show NetworkManager connections\n") io.stderr:flush() return false end if type(output) ~= "string" or #output == 0 then return true end for line in string.gmatch(output, "[^\r\n]+") do local con = string.match(line, "^([^:]+)") if del_con_name(con) then io.stdout:write(fmt("Deleting connection: %s...\n", con)) io.stdout:flush() invoker.invoke(invoker.NOSTDIO, "nmcli", "con", "down", con) local res = invoker.invoke(0, "nmcli", "con", "delete", con) if res ~= 0 then io.stderr:write(fmt("Error delete connection: %s.\n", con)) io.stderr:flush() end end end return true end local function netdev_exists(name) if type(name) ~= "string" or #name == 0 then return false end local okay = invoker.invoke(invoker.NOSTDIO, "ip", "link", "show", "dev", name) return okay == 0 end local proto_map = { ["auto"] = "dhcp", ["manual"] = "static" } local function read_wifi_passwd(conn) local fmt = string.format local format = "/etc/NetworkManager/system-connections/%s.nmconnection" local conc = invoker.readfile(fmt(format, conn)) if type(conc) ~= "string" or string.len(conc) == 0 then format = "/etc/sysconfig/network-scripts/ifcfg-%s" conc = invoker.readfile(fmt(format, conn)) if type(conc) ~= "string" or string.len(conc) == 0 then io.stderr:write(fmt("Error, failed to locate connection profile: %s\n", conn)) io.stderr:flush() return nil end end -- process line by line, I think it better for pline in string.gmatch(conc, "[^\r\n]+") do local passwd = string.match(pline, "^psk=(.+)$") if passwd then return passwd end end io.stderr:write(fmt("Warning, no WiFi password found for '%s'\n", conn)) io.stderr:flush() return nil end local function nmcli_coninfo(name) local fmt, mch = string.format, string.match local iflags = invoker.OUTPUT + 0x70000 local okay, output = invoker.invoke(iflags, "nmcli", "con", "show", name) if okay ~= 0 then io.stderr:write(fmt("Error, failed to show NetworkManager connection: %s\n", name)) io.stderr:flush() return nil end if type(output) ~= "string" or string.len(output) == 0 then io.stderr:write(fmt("Error, no information about connection: %s\n", name)) io.stderr:flush() return nil end local regex = "%s:[ \t]+([^ \t\r\n][^\r\n]*)" local netdev = mch(output, fmt(regex, 'connection.interface%-name')) if not netdev_exists(netdev) then io.stderr:write(fmt("Error, no device found for connection: %s\n", name)) io.stderr:flush() nmcli.delete(name) return nil end -- get network type local ntype = mch(output, fmt(regex, "connection.type")) if type(ntype) ~= "string" then io.stderr:write(fmt("Error, network type not found for %s\n", name)) io.stderr:flush() return nil end local proto = mch(output, fmt(regex, 'ipv4.method')) if proto == nil then proto = 'auto' end if proto ~= "auto" and proto ~= "manual" then if type(proto) ~= "string" then proto = "unknown" end io.stderr:write(fmt("Error, invalid ipv4.method for %s: %s\n", name, proto)) io.stderr:flush() return nil end proto = proto_map[proto] local metric = mch(output, fmt(regex, 'ipv4.route%-metric')) if type(metric) == "string" then metric = tonumber(metric) end if type(metric) ~= "number" then metric = -1 end local macaddr = mch(output, fmt(regex, "cloned%-mac%-address")) if type(macaddr) == "string" then macaddr = macaddr:lower() end if type(macaddr) ~= "string" or macaddr == "--" or macaddr == 'permanent' or macaddr == 'preserve' then macaddr = nil end local disabled = true local autocon = mch(output, fmt(regex, "connection.autoconnect")) if autocon == "yes" then disabled = false end -- get configured ipv4 static address, if any local ipmask = mch(output, fmt(regex, "ipv4.addresses")) if ipmask == "--" then ipmask = nil end local dns1, dns2 = nil, nil -- get configured DNS address local dnses = mch(output, fmt(regex, "ipv4.dns")) if type(dnses) == "string" and dnses ~= "--" then if string.find(dnses, ",", 1, true) then dns1, dns2 = mch(dnses, "([%d%.]+),([%d%.]+)") else dns1 = dnses end else dnses = nil end local w_ssid, w_pass, w_enc = nil, nil, nil -- for wifi connection, get the SSID/password/encryption if string.find(ntype, 'wireless', 1, true) then w_pass = read_wifi_passwd(name) w_enc = mch(output, fmt(regex, "key%-mgmt")) if w_enc == nil then w_enc = "none" end w_ssid = mch(output, fmt(regex, "wireless.ssid")) if type(w_ssid) ~= "string" then w_ssid, w_pass, w_enc = nil, nil, nil io.stderr:write(fmt("Error, cannot determine SSID for ''\n", name)) io.stderr:flush() end end local ninfo = { ["iface"] = name, ["netdev"] = netdev, ["macaddr"] = macaddr, proto = proto, disabled = disabled, metric = metric, dns1 = dns1, dns2 = dns2, _ipmask = ipmask, _dnses = dnses, _ntype = ntype, ["ssid"] = w_ssid, ["passwd"] = w_pass, ["wifienc"] = w_enc } if proto == "dhcp" then return ninfo end local ipaddr, mask = nil, nil if type(ipmask) == "string" then ipaddr, mask = mch(ipmask, "([^/]+)/(%d+)") if type(mask) == "string" then mask = tonumber(mask) end end if not invoker.isipv4(ipaddr) or not mask then io.stderr:write(fmt("Error, invalid IPv4 address found for %s/%s", name, netdev)) io.stderr:flush() ipaddr, mask = nil, nil end -- get default gateway local gateway = mch(output, fmt(regex, 'ipv4.gateway')) if not invoker.isipv4(gateway) then gateway = nil end ninfo["ipaddr"] = ipaddr ninfo["netmask"] = mask ninfo["gateway"] = gateway return ninfo end local function wan_new(name, ucfg) local ndev = ucfg["netdev"] invoker.invoke(0, "ip", "addr", "flush", "dev", ndev) local okay = invoker.invoke(0, "nmcli", "con", "add", "con-name", name, "ifname", ndev, "type", "ethernet") if okay ~= 0 then io.stderr:write(string.format("Error, failed add new NM connection: %s\n", name)) io.stderr:flush() return false end local is_dhcp = ucfg["proto"] == "dhcp" local cmds = { "nmcli", "con", "mod", name, "ipv4.method", is_dhcp and 'auto' or 'manual', "ipv6.method", nmcli.IPV6M } local pinfo = invoker.readfile("/proc/cpuinfo") if type(pinfo) ~= "string" then pinfo = "what the fuck" end if is_dhcp and not string.find(pinfo, "Freescale i.MX6", 1, true) then cmds[#cmds + 1] = "ipv4.dhcp-timeout" cmds[#cmds + 1] = "2147483647" end local tmpval = ucfg["macaddr"] if type(tmpval) == "string" then cmds[#cmds + 1] = "ethernet.cloned-mac-address" cmds[#cmds + 1] = tmpval end tmpval = ucfg["metric"] if type(tmpval) == "number" and tmpval >= 0 then cmds[#cmds + 1] = "ipv4.route-metric" cmds[#cmds + 1] = tostring(tmpval) end tmpval = ucfg["disabled"] cmds[#cmds + 1] = "autoconnect" cmds[#cmds + 1] = tmpval and "no" or "yes" if not is_dhcp then if not ucfg["ipaddr"] or not ucfg["netmask"] then io.stderr:write(string.format("Error, no static ipaddr/mask found for %s\n", name)) io.stderr:flush() invoker.invoke(0, "nmcli", "con", "delete", name) return false end cmds[#cmds + 1] = "ipv4.addr" cmds[#cmds + 1] = string.format("%s/%d", ucfg.ipaddr, ucfg.netmask) tmpval = ucfg["gateway"] if type(tmpval) == "string" and #tmpval > 0 then cmds[#cmds + 1] = "ipv4.gateway" cmds[#cmds + 1] = tmpval end end tmpval = ucfg["dns1"] if type(tmpval) == "string" then cmds[#cmds + 1] = "+ipv4.dns" cmds[#cmds + 1] = tmpval end tmpval = ucfg["dns2"] if type(tmpval) == "string" then cmds[#cmds + 1] = "+ipv4.dns" cmds[#cmds + 1] = tmpval end okay = invoker.invoke(0, cmds) if okay ~= 0 then io.stderr:write(string.format("Error, failed to add new connection: %s\n", name)) io.stderr:write(table.concat(cmds, "\n\t")) io.stderr:flush() return false end local updown = ucfg["disabled"] and "down" or "up" invoker.invoke(invoker.NOSTDIO, "nmcli", "-w", "6", "con", updown, name) return true end local function lan_new(lancfg) local lfmt = string.format local okay = invoker.invoke(0, 'nmcli', 'con', 'add', 'type', 'bridge', 'con-name', 'lan', 'ifname', 'br-lan') if not okay then io.stderr:write("Error, failed to create br-lan connection\n") io.stderr:flush() return false end local brmac = lancfg['macaddr']; if type(brmac) ~= "string" or #brmac == 0 then brmac = nil end for _, iface in ipairs(lancfg["netdevs"]) do okay = invoker.invoke(0, 'nmcli', 'con', 'add', 'type', 'bridge-slave', 'con-name', nmcli.BRPFX .. iface, 'ifname', iface, 'autoconnect', 'yes', 'master', 'br-lan', brmac and "ethernet.cloned-mac-address" or nil, brmac) if okay ~= 0 then io.stderr:write(lfmt("Error, failed to bridge network device: %s\n", iface)) io.stderr:flush() return false end end -- disable STP for bridge -- invoker.invoke(0, "nmcli", "con", "modify", "lan", "bridge.stp", "no") local cmds = { "nmcli", "con", "modify", "lan" } cmds[#cmds + 1] = "bridge.stp" cmds[#cmds + 1] = "no" cmds[#cmds + 1] = "ipv4.addr" cmds[#cmds + 1] = lfmt("%s/%d", lancfg["ipaddr"], lancfg["netmask"]) if invoker.isipv4(lancfg['gateway']) then cmds[#cmds + 1] = "ipv4.gateway" cmds[#cmds + 1] = lancfg['gateway'] cmds[#cmds + 1] = "ipv4.route-metric" cmds[#cmds + 1] = tostring(lancfg['metric']) end if type(brmac) == "string" then cmds[#cmds + 1] = "ethernet.cloned-mac-address" cmds[#cmds + 1] = lancfg['macaddr'] end cmds[#cmds + 1] = "ipv4.method" cmds[#cmds + 1] = "manual" cmds[#cmds + 1] = "ipv6.method" cmds[#cmds + 1] = nmcli.IPV6M cmds[#cmds + 1] = "autoconnect" cmds[#cmds + 1] = "yes" okay = invoker.invoke(0, cmds) if okay ~= 0 then io.stderr:write("Error, failed to modify br-lan connection\n") io.stderr:flush() return false end invoker.invoke(invoker.NOSTDIO, "nmcli", "-w", "6", "con", "up", "lan") return true end nmcli["delete"] = function (name) if del_con_name(name) then io.stdout:write(string.format("Deleting interface: %s ...\n", name)) io.stdout:flush() invoker.invoke(invoker.NOSTDIO, "nmcli", "con", "down", name) local okay = invoker.invoke(invoker.NOSTDIO, "nmcli", "con", "delete", name) return okay == 0 end return false end nmcli["update"] = function (ucicfg) local con = ucicfg["iface"] if type(con) ~= "string" or #con == 0 then io.stderr:write("Error, invalid NM connection name found.\n") io.stderr:flush() return false end invoker.invoke(invoker.NOSTDIO, "nmcli", "con", "delete", con) return wan_new(con, ucicfg) end local function bridged_delete(nmcfgs) local dlist = {} for what, _ in pairs(nmcfgs) do if type(what) == "string" and #what > 0x5 and what:sub(1, 5) == nmcli.BRPFX then nmcli.delete(what) dlist[#dlist + 1] = what end end for _, name in ipairs(dlist) do nmcfgs[name] = nil end return true end local function bridged_ifaces(ccfgs) local blist = {} if type(ccfgs) ~= "table" then return blist end for cn, _ in pairs(ccfgs) do if type(cn) == "string" and #cn > 0x5 and cn:sub(1, 5) == nmcli.BRPFX then local ndev = cn:sub(6) blist[ndev] = true end end return blist end nmcli["updatelan"] = function (lancfg, nmcfgs) local nmlan = nmcfgs['lan'] if type(nmlan) ~= "table" then nmcli.delete('lan') bridged_delete(nmcfgs) return lan_new(lancfg), true end local changed = false local iflist = bridged_ifaces(nmcfgs) for _, iface in ipairs(lancfg["netdevs"]) do if not iflist[iface] then io.stdout:write(string.format("INFO: bridge interface: %s...\n", iface)) io.stdout:flush() changed = true local brn = nmcli.BRPFX .. iface invoker.invoke(0, "nmcli", "con", "add", "type", "bridge-slave", "con-name", brn, "ifname", iface, "autoconnect", "yes", "master", "br-lan") invoker.invoke(0, "nothup", "nmcli", "-w", "6", "con", "up", brn) end iflist[iface] = nil nmcfgs[nmcli.BRPFX .. iface] = nil -- avoid deletion end --[[ for iface, _ in pairs(iflist) do changed = true io.stdout:write(string.format("INFO: deleting interface: %s...\n", iface)) io.stdout:flush() nmcli.delete(nmcli.BRPFX .. iface) end ]]-- local okay = 0 if lancfg["ipaddr"] ~= nmlan["ipaddr"] or lancfg["netmask"] ~= nmlan["netmask"] or lancfg["gateway"] ~= nmlan["gateway"] or lancfg['macaddr'] ~= nmlan['macaddr'] or (invoker.isipv4(nmlan["gateway"]) and lancfg["metric"] ~= nmlan["metric"]) then local ipv4 = string.format("%s/%d", lancfg["ipaddr"], lancfg["netmask"]) local lancmd = { "nmcli", "con", "modify", "lan", "ipv4.addr", ipv4, } if invoker.isipv4(lancfg["gateway"]) then lancmd[#lancmd + 1] = "ipv4.gateway" lancmd[#lancmd + 1] = lancfg["gateway"] lancmd[#lancmd + 1] = "ipv4.route-metric" lancmd[#lancmd + 1] = tostring(lancfg["metric"]) elseif invoker.isipv4(nmlan["gateway"]) then lancmd[#lancmd + 1] = "-ipv4.gateway" lancmd[#lancmd + 1] = "" end if type(lancfg['macaddr']) == "string" then lancmd[#lancmd + 1] = "ethernet.cloned-mac-address" lancmd[#lancmd + 1] = lancfg['macaddr'] elseif type(nmlan['macaddr']) == "string" then lancmd[#lancmd + 1] = "-ethernet.cloned-mac-address" lancmd[#lancmd + 1] = "" end lancmd[#lancmd + 1] = "ipv4.route-metric" lancmd[#lancmd + 1] = tostring(lancfg["metric"]) -- print(table.concat(lancmd, " ")) okay = invoker.invoke(0, lancmd) io.stdout:write(string.format("Modify br-lan IPv4 address as %s: %d\n", ipv4, okay)) io.stdout:flush() changed = true end if changed then okay = invoker.invoke(invoker.NOSTDIO, "nmcli", "-w", "6", "con", "up", "lan") end nmcfgs['lan'] = nil -- avoid deletion return okay == 0, changed end local function wifi_keymgmt(encryption) if type(encryption) ~= "string" then return "none" end if encryption == 'none' then return 'none' end if encryption == 'sae' then return 'sae' end -- WPA3 Personal only if encryption == 'owe' then return 'owe' end -- Opportunistic Wireless Encryption if encryption == 'wpa3' then return "wpa-eap-suite-b-192" end -- WPA3 enterprise only if string.find(encryption, "wpa", 1, true) then return "wpa-eap" end -- WPA3/WPA2 Enterprise if string.find(encryption, "wep", 1, true) then return "ieee8021x" end -- Dynamic WEP return "wpa-psk" -- default, WPA2/WPA3 Personal end local function wifiwan_new(wcfg) local okay = invoker.invoke(0, "nmcli", "con", "add", "type", "wifi", "con-name", "wifiwan", "autoconnect", "no", "ipv4.method", "auto", "ipv6.method", nmcli.IPV6M, "ipv4.route-metric", tostring(wcfg['metric']), "ifname", wcfg['netdev'], "ssid", wcfg['ssid']) if okay ~= 0 then io.stderr:write("Error, failed to create wifiwan connection.\n") io.stderr:flush() return false end local enc = wifi_keymgmt(wcfg['wifienc']) local cmds = { "nmcli", "con", "modify", "wifiwan", "wifi.hidden", "true" } if enc ~= "none" then -- cmds[#cmds + 1] = "802-11-wireless-security.auth-alg"; cmds[#cmds + 1] = "open" cmds[#cmds + 1] = "802-11-wireless-security.key-mgmt"; cmds[#cmds + 1] = enc cmds[#cmds + 1] = "802-11-wireless-security.psk"; cmds[#cmds + 1] = wcfg['passwd'] end cmds[#cmds + 1] = "autoconnect"; cmds[#cmds + 1] = "yes" okay = invoker.invoke(0, cmds) if okay ~= 0 then io.stderr:write("Error, failed to modify wifiwan connection.\n") io.stderr:flush() return false end -- invoke nothup instead of nmcli to avoid hang: invoker.invoke(0, "nothup", "nmcli", "-w", "6", "con", "up", "wifiwan") return true end nmcli["wifiwan"] = function(wcfg, nmcfgs) local wifi = nmcfgs['wifiwan'] if type(wifi) ~= "table" then nmcli.delete('wifiwan') return wifiwan_new(wcfg) end nmcfgs['wifiwan'] = nil -- set to nil to avoid deletion local cmds, changed = { "nmcli", "con", "modify", "wifiwan" }, false if wcfg.metric ~= wifi.metric then changed = true cmds[#cmds + 1] = "ipv4.route-metric" cmds[#cmds + 1] = tostring(wcfg.metric) end if wcfg.ssid ~= wifi.ssid then changed = true cmds[#cmds + 1] = "802-11-wireless.ssid" cmds[#cmds + 1] = wcfg.ssid end local enc = wifi_keymgmt(wcfg['wifienc']) if enc ~= wifi['wifienc'] then changed = true if enc == "none" then nmcli.delete('wifiwan') return wifiwan_new(wcfg) end cmds[#cmds + 1] = "802-11-wireless-security.key-mgmt" cmds[#cmds + 1] = enc end if wcfg.passwd ~= wifi['passwd'] then changed = true cmds[#cmds + 1] = "802-11-wireless-security.psk" cmds[#cmds + 1] = wcfg.passwd end if changed then io.stdout:write("INFO: configuration for wifiwan has changed.\n") io.stdout:flush() invoker.invoke(0, cmds) -- carry out the modifications invoker.invoke(0, "nothup", "nmcli", "-w", "6", "con", "up", "wifiwan") end return true end nmcli['wlan'] = function (conname, lcfg, nmcfgs) local lancfg = nmcfgs[conname] if type(lancfg) ~= "table" then invoker.invoke(0, "nmcli", "con", "add", "type", "bridge-slave", "con-name", conname, "ifname", lcfg["netdev"], "autoconnect", "yes", "master", "br-lan") return true end nmcfgs[conname] = nil -- remove connection to avoid deletion if lcfg['netdev'] ~= lancfg['netdev'] then invoker.invoke(0, "nmcli", "con", "modify", conname, "ifname", lcfg['netdev']) invoker.invoke(0, "nmcli", "-w", "6", "con", "up", conname) end return true end -- if the connection does not exist, add it nmcli["getall"] = function () local okay, output = invoker.invoke(invoker.OUTPUT, "nmcli", "-m", "tabular", "-t", "con", "show") if okay ~= 0 then io.stderr:write("Error, failed to show NetworkManager connections\n") io.stderr:flush() return nil end if type(output) ~= "string" or #output == 0 then return nil end local netinfo, ncount = {}, 0 for line in string.gmatch(output, "[^\r\n]+") do local con = string.match(line, "^([^:]+)") if con and #con > 0 then local info = nmcli_coninfo(con) if netinfo[con] then local uuid = string.match(line, "^[^:]+:([^:]+)") if uuid and #uuid > 0 then io.stderr:write("Warning: DELETE DUPLICATED CONNECTION: " .. con .. "\n") io.stderr:flush() invoker.invoke(invoker.NOSTDIO, "nmcli", "con", "del", uuid) end else if info then ncount = ncount + 1 end netinfo[con] = info end end end if ncount > 0 then return netinfo end return nil end return nmcli