#!/opt/lnxall_app/bin/lua -- Created by jiaqiang.ye@lnxall.com -- Simple 4G DNS resolve service -- 2024/09/26 local bit32 = require "bit32" local cjson = require "cjson" local posix = require "posix" local invoker = require "invoker" local gfmt = string.format local DNS_TIMEOUT = 8000 local g_dns = { [1] = "8.8.8.8", [2] = "114.114.114.114", } local g_dns1 = { [1] = "219.141.136.10", [2] = "202.96.134.133", } local g_gooddns = {} -- map of accessible DNS servers local g_hostip = {} -- hostname to IP address mapping local g_host_file = "/app/config/dns4g_resolv.json" local g_modem_file = "/app/config/modefor4g.json" local g_start_time = invoker.uptime() local g_hostnames = {} posix.setenv("PATH", "/opt/lnxall_app/bin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin", true) local function load_json_file(file) if posix.access(file) ~= 0 then io.stderr:write(gfmt("Warning, file not found: %s\n", file)) io.stderr:flush() return nil end local fh = io.open(file, "rb") if not fh then io.stderr:write(gfmt("Error, failed to open: %s\n", file)) io.stderr:flush() return nil end local fdat = fh:read("*a") fh:close(); fh = nil if type(fdat) ~= "string" or #fdat == 0 then io.stderr:write(gfmt("Warning, failed to read file: %s\n", file)) io.stderr:flush() return nil end local okay, json = pcall(cjson.decode, fdat) if not okay then io.stderr:write(gfmt("Error, failed to decode as JSON: %s\n", file)) io.stderr:flush() return nil end return json end local function load_deb4g_config() local jdat = load_json_file(g_modem_file) if type(jdat) ~= "table" then return false end local ipt, idx = jdat["ipw"], 0 if type(ipt) ~= "table" then ipt = {} end while true do idx = idx + 1 local hn = ipt[idx] if hn == nil then break end if invoker.isipv4(hn) or invoker.isipv6(hn) then io.stderr:write(gfmt("INFO: Ignore IP address from IPW: %s\n", hn)) io.stderr:flush() elseif type(hn) == "string" and #hn > 0 then if g_hostnames[hn] then io.stdout:write(gfmt("INFO: Ignoring duplicated hostname from IPW: %s\n", hn)) else g_hostnames[hn] = -1 io.stdout:write(gfmt("INFO: appending hostname from IPW: %s\n", hn)) end io.stdout:flush() end end ipt, idx = jdat["ips"], 0 if type(ipt) ~= "table" then ipt = {} end while true do idx = idx + 1 local hn = ipt[idx] if hn == nil then break end if invoker.isipv4(hn) or invoker.isipv6(hn) then io.stderr:write(gfmt("INFO: Ignore IP address from IPS: %s\n", hn)) io.stderr:flush() elseif type(hn) == "string" and #hn > 0 then if g_hostnames[hn] then io.stdout:write(gfmt("INFO: Ignoring duplicated hostname from IPS: %s\n", hn)) else g_hostnames[hn] = -1 io.stdout:write(gfmt("INFO: appending hostname from IPS: %s\n", hn)) end io.stdout:flush() end end return true end local function load_dns4g_resolv() local jdat = load_json_file(g_host_file) if type(jdat) ~= "table" then return false end for host_n, host_p in pairs(jdat) do if type(host_n) == "string" and type(host_p) == "number" and not g_hostnames[host_n] then g_hostnames[host_n] = math.floor(host_p) io.stdout:write(gfmt("INFO: appending hostname: %s => %s\n", host_n, tostring(g_hostnames[host_n]))) io.stdout:flush() end end return true end local function dns_resolv(hostn, dnssvr, show) if type(hostn) ~= "string" or #hostn == 0 then io.stderr:write("Error, invalid hostname specified.\n") io.stderr:flush() return false end if not invoker.isipv4(dnssvr) then io.stderr:write("Error, invalid DNS server IP specified.\n") io.stderr:flush() return false end local iflags = invoker.NOWAIT + invoker.OUTPUT + invoker.NOSTDIO local pid, dfd = invoker.invoke(iflags, "nslookup", hostn, dnssvr) if not pid then io.stderr:write("Error, failed to call nslookup.\n") io.stderr:flush() return false end local ts = invoker.uptimsec() local te, exited = ts, false ts = ts + DNS_TIMEOUT while te < ts do if invoker.waitpid(pid, true) then exited = true break end invoker.msleep(300) te = invoker.uptimsec() end if not exited then invoker.kill(pid, 9) invoker.msleep(100) invoker.kill(pid, 9) invoker.waitpid(pid, false) end local output = invoker.readfd(dfd, 16384) posix.close(dfd); dfd = -1 if type(output) ~= "string" or #output == 0 then if show then io.stderr:write(gfmt("Error, failed to resolve hostname: %s\n", hostn)) io.stderr:flush() end return false end local haveh, ipaddrs = false, {} for line in string.gmatch(output, "[^\r\n]+") do if not haveh then local regex = "Name:%s+" .. hostn if string.find(line, regex) then haveh = true end else local ip = string.match(line, "Address[^:]*:%s+([%d%.]+)") if ip and invoker.isipv4(ip) then ipaddrs[#ipaddrs + 1] = ip if show then io.stdout:write(gfmt("Resolved via %s: %s => %s\n", dnssvr, hostn, ip)) io.stdout:flush() end end end end if not ipaddrs[1] then if show then io.stderr:write(gfmt("Error, failed to resolve host '%s'\n", hostn)) io.stderr:flush() end return false end if not g_gooddns[dnssvr] then g_gooddns[dnssvr] = true io.stdout:write(gfmt("DNS server %s is good.\n", dnssvr)) io.stdout:flush() end if g_hostip[hostn] ~= ipaddrs[1] then g_hostip[hostn] = ipaddrs[1] end return true end local function check_host(hostname, portno) local ip = g_hostip[hostname] if not ip then ip = hostname end local _, output = invoker.invoke(invoker.OUTPUT, "ping", "-c3", "-w4", ip) if type(output) ~= "string" then return false end if not string.find(output, "packet loss", 1, true) then return false end if string.find(output, "100% packet loss", 1, true) then if portno and portno > 0 and invoker.tcpcheck(ip, portno, 3000) then return true end return false end return true end local function check_hosts(lastnok) for host, pno in pairs(g_hostnames) do if check_host(host, pno) then lastnok[host] = nil else lastnok[host] = true end end end local function check_simiccid() io.stdout:write("Waiting for SIMCARD ICCID file...\n") io.stdout:flush() while true do local id = invoker.readfile("/tmp/simcard_iccid") if id and #id >= 8 and string.find(id, "%d+") then break end id = invoker.readfile("/run/sim_iccid.txt") if id and #id >= 8 and string.find(id, "%d+") then break end if invoker.uptime() < 240 then invoker.waitsec(15) else invoker.waitsec(180) end end io.stdout:write("Found ICCID, continue 4G DNS checking.\n") io.stdout:flush() return true end local function dns1_good() local dns = g_dns1[1] if g_gooddns[dns] then return true end dns = g_dns1[2] if g_gooddns[dns] then return true end return false end local function next_dns(count) local i = bit32.band(count, 0x1) + 1 if i == 1 then local dns1 = g_dns1[1] if g_gooddns[dns1] then return dns1 end return g_dns[i] end local dns2 = g_dns1[2] if g_gooddns[dns2] then return dns2 end return g_dns[i] end local function mainfunc(dns1, dns2) local iflags = bit32.bor(invoker.OUTPUT, invoker.TRIMEND) if invoker.isipv4(dns1) then g_dns[1] = dns1 else local okay, ipv4 = invoker.invoke(iflags, "uci", "get", "dnsconfig.@[0].dns1") if okay == 0 and invoker.isipv4(ipv4) then g_dns[1] = ipv4 end end if invoker.isipv4(dns2) then g_dns[2] = dns2 else local okay, ipv4 = invoker.invoke(iflags, "uci", "get", "dnsconfig.@[0].dns2") if okay == 0 and invoker.isipv4(ipv4) then g_dns[2] = ipv4 end end io.stdout:write(gfmt("DNS servers used for appliation: %s, %s, %s, %s\n\n", g_dns[1], g_dns[2], g_dns1[1], g_dns1[2])) io.stdout:flush() check_simiccid() for host, _ in pairs(g_hostnames) do dns_resolv(host, g_dns[2], true) end local errlist, round = {}, 0 while true do local iternum = 0 check_hosts(errlist) local rend = invoker.uptime() local rbegin = rend + 180 while rend < rbegin do iternum = iternum + 1 local dns, num = next_dns(round), 0 for host, _ in pairs(errlist) do num = num + 1 io.stdout:write(gfmt("Resolving hostname round: %d, %d => %s...\n", round, iternum, host)) io.stdout:flush() dns_resolv(host, dns, false) end if num <= 0 then invoker.waitsec(rbegin - rend) else invoker.waitsec(15) end rend = invoker.uptime() end round = round + 1 -- resolve all hostnames if (invoker.uptime() - g_start_time) >= 3600 or dns1_good() then local idx = bit32.band(round, 0x1) + 1 local dns = dns1_good() and g_dns1[idx] or g_dns[idx] for host, _ in pairs(g_hostnames) do dns_resolv(host, dns) end else local idx = bit32.rshift(bit32.band(round, 0x2), 1) + 1 for host, _ in pairs(g_hostnames) do if bit32.band(round, 0x1) == 0 then dns_resolv(host, g_dns[idx]) else dns_resolv(host, g_dns1[idx]) end end end end end invoker.setname("DNS4GCHECK") load_dns4g_resolv() load_deb4g_config() mainfunc(arg[1], arg[2])