#!/opt/lnxall_app/bin/lua -- Created by jiaqiang.ye@lnxall.com -- Simple Network Diagnose script -- 2026/02/26 local bit32 = require "bit32" local invoker = require "invoker" local gfmt = string.format local g_chkip = nil local g_portn = nil local function diagnet_usage() io.stdout:write("Usage: diagnet IPv4 [portno]\n") io.stdout:write("Example:\n") io.stdout:write("\tdiagnet 172.18.39.101 1883\n") io.stdout:flush() end local function diagnet_init(arg1, arg2) if not invoker.isipv4(arg1) then if type(arg1) == "string" then io.stderr:write(gfmt("Error, not ipv4-address: %s\n", arg1)) io.stderr:write(gfmt("You might need to execute command line: nslookup %s\n", arg1)) io.stderr:flush() end diagnet_usage() return false end g_chkip = arg1 if type(arg2) == "string" then arg2 = tonumber(arg2) end if type(arg2) == "number" and arg2 >= 1 and arg2 <= 65535 then g_portn = math.floor(arg2) end io.stdout:write(gfmt("Detecting connection to %s, port-number: %s\n", arg1, g_portn and tostring(g_portn) or "NONE")) io.stdout:flush() return true end local function parse_nmask(maskv) if not maskv then return nil end local retval = maskv if type(retval) == "string" then retval = tonumber(retval) end if type(retval) == "number" and retval >= 0 and retval <= 32 then return bit32.bnot(bit32.lshift(0x1, 32 - retval) - 1) end io.stderr:write(gfmt("Error, invalid IPv4 network mask: %s\n", tostring(maskv))) io.stderr:flush() return nil end local function parse_ipval(ipaddr) if not ipaddr then return nil end local a, b, c, d = string.match(ipaddr, "^(%d+)%.(%d+)%.(%d+)%.(%d+)$") if not d then io.stderr:write(gfmt("Error, invalid IPv4 address: %s\n", ipaddr)) io.stderr:flush() return nil end a, b, c, d = tonumber(a), tonumber(b), tonumber(c), tonumber(d) a, b, c = bit32.lshift(a, 24), bit32.lshift(b, 16), bit32.lshift(c, 8) return bit32.bor(a, b, c, d) end local function parse_ipaddr(netdev, outstr, nlist) local ipcnt = 0 for ipaddr in string.gmatch(outstr, "inet%s+([/%d%.]+)%s+") do local ipv4, mval = string.match(ipaddr, "^([%d%.]+)/(%d+)$") if invoker.isipv4(ipv4) and mval then local num = #nlist + 1 nlist[num] = { ["netdev"] = netdev, ["ipstr"] = ipv4, ["ipval"] = parse_ipval(ipv4), ["maskstr"] = mval, ["maskval"] = parse_nmask(mval), } ipcnt = ipcnt + 1 io.stdout:write(gfmt("[INFO] IPv4 address found for %-16s: %s/%s\n", netdev, ipv4, mval)) io.stdout:flush() end end if ipcnt == 0 then io.stderr:write(gfmt("[ERROR] No ipv4 address found for %s\n", netdev)) io.stderr:flush() return false end return true end local function netdev_ipaddr() local netdir = "/sys/class/net" if not invoker.chdir(netdir) then io.stderr:write(gfmt("Error, failed to goto directory: `%s\n", netdir)) io.stderr:flush() return nil end local netdev = invoker.glob("*") if not netdev then io.stderr:write(gfmt("Error, no network device found in `%s\n", netdir)) io.stderr:flush() return nil end local netlist = {} local iflags = bit32.bor(invoker.OUTPUT, invoker.BUFSIZ, 0xf0000) for _, ndev in ipairs(netdev) do local okay, output = invoker.invoke(iflags, "ip", "addr", "show", "dev", ndev) if okay == 0 and type(output) == "string" then parse_ipaddr(ndev, output, netlist) end end if #netlist == 0 then io.stderr:write("[ERROR] no invalid network device found.\n") io.stderr:flush() return nil end return netlist end local function parse_iproute(line) local metric = string.match(line, "%s+metric%s+(%d+)") if metric then metric = tonumber(metric) end if type(metric) ~= "number" then metric = 0 end local src_ip = string.match(line, "%s+src%s+([%d%.]+)") if src_ip and not invoker.isipv4(src_ip) then io.stderr:write(gfmt("Error, invalid src_ip address: %s\n", src_ip)) io.stderr:flush() src_ip = nil end local target_ip, gwip, ndev = nil, nil, nil gwip, ndev = string.match(line, "^default via%s+([%d%.]+)%s+dev%s+([^%s]+)") if ndev and invoker.isipv4(gwip) then return { ["netdev"] = ndev, ["default"] = true, ["gwipstr"] = gwip, ["gwipval"] = parse_ipval(gwip), ["srcipstr"] = src_ip, ["srcipval"] = parse_ipval(src_ip), ["metric"] = metric, } end target_ip, gwip, ndev = string.match(line, "^([/%d%.]+)%s+via%s+([%d%.]+)%s+dev%s+([^%s]+)") if ndev and invoker.isipv4(gwip) then local ipv4, mask = string.match(target_ip, "^([%d%.]+)/(%d+)$") return { ["netdev"] = ndev, ["default"] = false, ["gwipstr"] = gwip, ["gwipval"] = parse_ipval(gwip), ["ipstr"] = ipv4 or target_ip, ["ipval"] = parse_ipval(ipv4 or target_ip), ["maskstr"] = mask or "32", ["maskval"] = parse_nmask(mask or "32"), ["srcipstr"] = src_ip, ["srcipval"] = parse_ipval(src_ip), ["metric"] = metric, } end target_ip, ndev = string.match(line, "^([/%d%.]+)%s+dev%s+([^%s]+)") if ndev and target_ip then local ipv4, mask = string.match(target_ip, "^([%d%.]+)/(%d+)$") return { ["netdev"] = ndev, ["default"] = false, ["ipstr"] = ipv4 or target_ip, ["ipval"] = parse_ipval(ipv4 or target_ip), ["maskstr"] = mask or "32", ["maskval"] = parse_nmask(mask or "32"), ["srcipstr"] = src_ip, ["srcipval"] = parse_ipval(src_ip), ["metric"] = metric, } end io.stderr:write(gfmt("Error, unable to process ip-route line: %s\n", line)) io.stderr:flush() return nil end local function netdev_iproute() local okay, output = invoker.invoke(invoker.OUTPUT, "ip", "route", "show") if okay ~= 0 or type(output) ~= "string" then io.stderr:write("Error, command has failed: `ip route show`\n") io.stderr:flush() return false end local rtab, rnum = {}, 0 for rline in string.gmatch(output, "[^\r\n]+") do local rinfo = parse_iproute(rline) if rinfo then rnum = rnum + 1 rtab[rnum] = rinfo end end if rnum == 0 then io.stderr:write("Error, no valid routing information found.\n") io.stderr:flush() return false end if rnum >= 2 then table.sort(rtab, function(a, b) if a.default and b.default then return a.metric < b.metric end if a.default then return false end if b.default then return true end return a.metric < b.metric end) end return rtab end local function ping_route(rinfo, check_ip, srcip) local cmds = {} cmds[#cmds + 1] = "ping" cmds[#cmds + 1] = "-c3" cmds[#cmds + 1] = "-I" cmds[#cmds + 1] = srcip or rinfo.netdev cmds[#cmds + 1] = check_ip io.stdout:write(gfmt("\nInvoking command: %s\n", table.concat(cmds, " "))) io.stdout:flush() local _, output = invoker.invoke(invoker.OUTPUT, cmds) if type(output) ~= "string" or #output == 0 then io.stderr:write(gfmt("[ERROR] Ping FAILED for %s on: %s\n", check_ip, rinfo.netdev)) io.stderr:flush() return nil end if string.find(output, "100% packet loss", 1, true) or not string.find(output, "packet loss", 1, true) then io.stderr:write(gfmt("[ERROR] Ping FAILED for %s on: %s\n", check_ip, rinfo.netdev)) io.stderr:flush() return nil end io.stdout:write(gfmt("[OKAY] Ping okay on: %s => %s\n", rinfo.netdev, check_ip)) io.stdout:flush() return true end local function dump_rtable(rtab, ninfo) io.stdout:write(gfmt("\tnetwork device: %s\n", rtab.netdev)) if ninfo and ninfo.ipstr then io.stdout:write(gfmt("\tipaddr: %s/%s\n", ninfo.ipstr, ninfo.maskstr or 'xx')) end io.stdout:write(gfmt("\tmetric: %d\n", rtab.metric)) io.stdout:write(gfmt("\tdefault: %s\n", tostring(rtab.default))) io.stdout:write(gfmt("\tgateway ipaddr: %s\n", rtab.gwipstr or "")) io.stdout:write(gfmt("\tsource ipaddr: %s\n", rtab.srcipstr or "")) io.stdout:write(gfmt("\ttarget ipaddr: %s/%s\n", rtab.ipstr or "ANYADDR", rtab.maskstr or "NONE")) io.stdout:flush() end local function try_route(ri, rtab, ninfo) io.stdout:write(gfmt("[%02d] [INFO] connecting %s:%d via route entry:\n", ri, g_chkip, g_portn or 0)) dump_rtable(rtab, ninfo) if rtab.gwipstr then ping_route(rtab, rtab.gwipstr, nil) end local ping_ok, conn_ok = ping_route(rtab, g_chkip, nil), nil if rtab.srcipstr and ping_route(rtab, g_chkip, rtab.srcipstr) then ping_ok = true end if not g_portn then return ping_ok and 1 or 0 end local cmds = {} cmds[#cmds + 1] = "busybox" cmds[#cmds + 1] = "nc" cmds[#cmds + 1] = "-n" cmds[#cmds + 1] = "-zv" if rtab.srcipstr then cmds[#cmds + 1] = "-s" cmds[#cmds + 1] = rtab.srcipstr end cmds[#cmds + 1] = "-w" cmds[#cmds + 1] = "3" cmds[#cmds + 1] = g_chkip cmds[#cmds + 1] = tostring(g_portn) io.stdout:write(gfmt("\nInvoking command: %s\n", table.concat(cmds, " "))) io.stdout:flush() local okay = invoker.invoke(0, cmds) if okay == 0 then io.stdout:write(gfmt("[OKAY] TCP-Check OKAY for %s:%d on network device: %s\n", g_chkip, g_portn, rtab.netdev)) io.stdout:flush() return 1 end io.stderr:write(gfmt("[ERROR] %s:%d on network device: %s\n", g_chkip, g_portn, rtab.netdev)) io.stderr:flush() return 0 end local function bad_route(ri, rtab, ninfo) io.stdout:write(gfmt("[%02d] [ERROR] %s:%d should NOT connect via route entry:\n", ri, g_chkip, g_portn or 0)) dump_rtable(rtab, ninfo) return 0 end local function mainfunc() local sepline = "==========================================================================\n" io.stdout:write(sepline); io.stdout:write(sepline); io.stdout:write(sepline); io.stdout:flush() local nlist = netdev_ipaddr() if not nlist then os.exit(2) end local rlist = netdev_iproute() if not rlist then os.exit(3) end local chkipval, okcnt = parse_ipval(g_chkip), 0 for idx, rinfo in ipairs(rlist) do io.stdout:write(sepline); io.stdout:write(sepline) io.stdout:write(sepline); io.stdout:flush() local nk_info = nil -- find the network information for _, ninfo in ipairs(nlist) do if ninfo.netdev == rinfo.netdev and ninfo.ipstr == rinfo.srcipstr then nk_info = ninfo break end if ninfo.netdev == rinfo.netdev and rinfo.ipval and rinfo.maskval and bit32.band(rinfo.ipval, rinfo.maskval) == bit32.band(ninfo.ipval, rinfo.maskval) then nk_info = ninfo break end if ninfo.netdev == rinfo.netdev and rinfo.srcipval and ninfo.maskval and bit32.band(rinfo.srcipval, ninfo.maskval) == bit32.band(ninfo.ipval, ninfo.maskval) then nk_info = ninfo break end end if not nk_info then for _, ninfo in ipairs(nlist) do if ninfo.netdev == rinfo.netdev and rinfo.gwipval and ninfo.maskval and bit32.band(rinfo.gwipval, ninfo.maskval) == bit32.band(ninfo.ipval, ninfo.maskval) then nk_info = ninfo break end end end if rinfo.default then if nk_info and not rinfo.srcipstr then rinfo.srcipstr, rinfo.srcipval = nk_info.ipstr, nk_info.ipval end okcnt = okcnt + try_route(idx, rinfo, nk_info) elseif rinfo.ipval and rinfo.maskval then if bit32.band(chkipval, rinfo.maskval) == rinfo.ipval then okcnt = okcnt + try_route(idx, rinfo, nk_info) else bad_route(idx, rinfo, nk_info) end else io.stderr:write(gfmt("[%02d] [ERROR] unused route entry:\n", idx)) io.stderr:flush() bad_route(idx, rinfo) end end io.stdout:write(sepline); io.stdout:write(sepline); io.stdout:write(sepline); io.stdout:flush() if okcnt >= 1 then io.stdout:write(gfmt("[OKAY] remote host %s:%d reacheable: %d\n", g_chkip, g_portn or 0, okcnt)) io.stdout:flush() else io.stderr:write(gfmt("[ERROR] remote host %s:%d unreacheable\n", g_chkip, g_portn or 0)) io.stderr:flush() end end if not diagnet_init(arg[1], arg[2]) then os.exit(1) end mainfunc() os.exit(0)