From 5330c24b7785e32ed13496ff7e475269793390da Mon Sep 17 00:00:00 2001 From: NiLuJe Date: Tue, 21 Feb 2023 00:46:19 +0100 Subject: [PATCH] Device: Handle network info data gathering ourselves (#10139) i.e., we now query routes, interfaces, wireless extensions & ping ourselves, dropping the dependency on specific CLI tools altogether. --- base | 2 +- frontend/device/generic/device.lua | 397 +++++++++++++++++++++--- frontend/device/sdl/device.lua | 16 +- frontend/ui/network/manager.lua | 36 --- frontend/ui/network/networklistener.lua | 3 + 5 files changed, 358 insertions(+), 96 deletions(-) diff --git a/base b/base index 96b2a16e6..4d3944628 160000 --- a/base +++ b/base @@ -1 +1 @@ -Subproject commit 96b2a16e685a75de1b7fcfe30b9dd19d54f5f306 +Subproject commit 4d39446285161ab3b3041264049f763f74b28711 diff --git a/frontend/device/generic/device.lua b/frontend/device/generic/device.lua index 8dae21b3c..8d938d912 100644 --- a/frontend/device/generic/device.lua +++ b/frontend/device/generic/device.lua @@ -7,18 +7,20 @@ This module defines stubs for common methods. local DataStorage = require("datastorage") local Geom = require("ui/geometry") local logger = require("logger") +local ffi = require("ffi") +local time = require("ui/time") local util = require("util") local _ = require("gettext") local ffiUtil = require("ffi/util") +local C = ffi.C local T = ffiUtil.template +-- We'll need a bunch of stuff for getifaddrs & co in Device:retrieveNetworkInfo +require("ffi/posix_h") + local function yes() return true end local function no() return false end -local function isCommand(s) - return os.execute("command -v "..s.." >/dev/null") == 0 -end - local Device = { screen_saver_mode = false, screen_saver_lock = false, @@ -500,57 +502,360 @@ function Device:exit() require("ffi/input"):closeAll() end -function Device:retrieveNetworkInfo() - -- NOTE: This sed monstrosity is tailored for the busybox implementation of ifconfig - local std_out = io.popen("ifconfig | " .. - "sed -n " .. - "-e 's/ \\+$//g' " .. - "-e 's/ \\+/ /g' " .. - "-e 's/ \\?inet6\\? addr: \\?\\([^ ]\\+\\) .*$/IP: \\1/p' " .. - "-e 's/Link encap:Ethernet[[:blank:]]*HWaddr \\(.*\\)/\\1/p'", - "r") - if std_out then - local result = std_out:read("*all") - std_out:close() - std_out = io.popen('2>/dev/null iwconfig | grep ESSID | cut -d\\" -f2') - if std_out then - local ssid = std_out:read("*l") - std_out:close() - result = result .. "SSID: " .. ssid .. "\n" - end - -- iproute2 tools may not always be available, fall back to net-tools if necessary - if isCommand("ip") then - std_out = io.popen([[ip r | grep default | tail -n 1 | cut -d ' ' -f 3]], "r") - else - std_out = io.popen([[route -n | awk '$4 == "UG" {print $2}' | tail -n 1]], "r") - end - local default_gw - if std_out then - default_gw = std_out:read("*l") - std_out:close() - if default_gw == "" then - default_gw = nil +-- Lifted from busybox's libbb/inet_cksum.c +local function inet_cksum(ptr, nleft) + local addr = ffi.new("const uint16_t *", ptr) + + local sum = ffi.new("unsigned int", 0) + while nleft > 1 do + sum = sum + addr[0] + addr = addr + 1 + nleft = nleft - 2 + end + + if nleft == 1 then + local u8p = ffi.cast("uint8_t *", addr) + sum = sum + u8p[0] + end + + sum = bit.rshift(sum, 16) + bit.band(sum, 0xFFFF) + sum = sum + bit.rshift(sum, 16) + + sum = bit.bnot(sum) + return ffi.cast("uint16_t", sum) +end + +function Device:ping4(ip) + -- Try an unprivileged ICMP socket first + -- NOTE: This is disabled by default, barring custom distro setup during init, c.f., sysctl net.ipv4.ping_group_range + -- It also requires Linux 3.0+ (https://github.com/torvalds/linux/commit/c319b4d76b9e583a5d88d6bf190e079c4e43213d) + local socket, socket_type + socket = C.socket(C.AF_INET, bit.bor(C.SOCK_DGRAM, C.SOCK_NONBLOCK, C.SOCK_CLOEXEC), C.IPPROTO_ICMP) + if socket == -1 then + local errno = ffi.errno() + logger.dbg("Device:ping4: unprivileged ICMP socket:", ffi.string(C.strerror(errno))) + + -- Try a raw socket + socket = C.socket(C.AF_INET, bit.bor(C.SOCK_RAW, C.SOCK_NONBLOCK, C.SOCK_CLOEXEC), C.IPPROTO_ICMP) + if socket == -1 then + errno = ffi.errno() + if errno == C.EPERM then + logger.dbg("Device:ping4: Opening a RAW ICMP socket requires CAP_NET_RAW capabilities!") + else + logger.dbg("Device:ping4: Raw ICMP socket:", ffi.string(C.strerror(errno))) end - end - if default_gw then - result = result .. T(_("Default gateway: %1"), default_gw) .. "\n" - -- NOTE: No -w flag available in the old busybox build used on Legacy Kindles (K4 included)... - local pingok + --- Fall-back to the ping CLI tool, in the hope that it's setuid... if self:isKindle() and self:hasDPad() then - pingok = os.execute("ping -q -c1 " .. default_gw .. " > /dev/null") + -- NOTE: No -w flag available in the old busybox build used on Legacy Kindles (K4 included)... + return os.execute("ping -q -c1 " .. ip .. " > /dev/null") == 0 else - pingok = os.execute("ping -q -c1 -w2 " .. default_gw .. " > /dev/null") + return os.execute("ping -q -c1 -w2 " .. ip .. " > /dev/null") == 0 end - if pingok == 0 then - result = result .. _("Gateway ping successful") + else + socket_type = C.SOCK_RAW + end + else + socket_type = C.SOCK_DGRAM + end + + -- c.f., busybox's networking/ping.c + local DEFDATALEN = 56 -- 64 - 8 + local MAXIPLEN = 60 + local MAXICMPLEN = 76 + + -- Base the id on our PID (like busybox) + local myid = ffi.cast("uint16_t", C.getpid()) + myid = C.htons(myid) + + -- Setup the packet + local packet = ffi.new("char[?]", DEFDATALEN + MAXIPLEN + MAXICMPLEN) + local pkt = ffi.cast("struct icmphdr *", packet) + pkt.type = C.ICMP_ECHO + pkt.un.echo.id = myid + pkt.un.echo.sequence = C.htons(1) + pkt.checksum = inet_cksum(ffi.cast("const void *", pkt), ffi.sizeof(packet)) + + -- Set the destination address + local addr = ffi.new("struct sockaddr_in") + addr.sin_family = C.AF_INET + local in_addr = ffi.new("struct in_addr") + if C.inet_aton(ip, in_addr) == 0 then + logger.err("Device:ping4: Invalid address:", ip) + C.close(socket) + return false + end + addr.sin_addr = in_addr + addr.sin_port = 0 + + -- Send the ping + local start_time = time.now() + if C.sendto(socket, packet, DEFDATALEN + C.ICMP_MINLEN, 0, ffi.cast("struct sockaddr*", addr), ffi.sizeof(addr)) == - 1 then + local errno = ffi.errno() + logger.err("Device:ping4: sendto:", ffi.string(C.strerror(errno))) + C.close(socket) + return false + end + + -- We'll poll to make timing out easier on us (busybox uses a SIGALRM :s) + local pfd = ffi.new("struct pollfd") + pfd.fd = socket + pfd.events = C.POLLIN + local timeout = 2000 + + -- Wait for a response + while true do + local poll_num = C.poll(pfd, 1, timeout) + -- Slice the timeout in two on every retry, ensuring we'll bail definitively after 4s... + timeout = bit.rshift(timeout, 1) + if poll_num == -1 then + local errno = ffi.errno() + if errno ~= C.EINTR then + logger.err("Device:ping4: poll:", ffi.string(C.strerror(errno))) + C.close(socket) + return false + end + elseif poll_num > 0 then + local c = C.recv(socket, packet, ffi.sizeof(packet), 0) + if c == -1 then + local errno = ffi.errno() + if errno ~= C.EINTR then + logger.err("Device:ping4: recv:", ffi.string(C.strerror(errno))) + C.close(socket) + return false + end else - result = result .. _("Gateway ping FAILED") + -- Do some minimal verification of the reply's validity. + -- This is mostly based on busybox's ping, + -- with some extra inspiration from iputils's ping, especially as far as SOCK_DGRAM is concerned. + local iphdr = ffi.cast("struct iphdr *", packet) -- ip + icmp + local hlen + if socket_type == C.SOCK_RAW then + hlen = bit.lshift(iphdr.ihl, 2) + if c < (hlen + 8) or iphdr.ihl < 5 then + -- Packet too short (we don't use recvfrom, so we can't log where it's from ;o)) + logger.dbg("Device:ping4: received a short packet") + goto continue + end + else + hlen = 0 + end + -- Skip ip hdr to get at the ICMP part + local icp = ffi.cast("struct icmphdr *", packet + hlen) + -- Check that we got a *reply* to *our* ping + -- NOTE: The reply's ident is defined by the kernel for SOCK_DGRAM, so we can't do anything with it! + if icp.type == C.ICMP_ECHOREPLY and + (socket_type == C.SOCK_DGRAM or icp.un.echo.id == myid) then + break + end + end + else + local end_time = time.now() + logger.info("Device:ping4: timed out waiting for a response from", ip) + C.close(socket) + return false, end_time - start_time + end + ::continue:: + end + local end_time = time.now() + + -- If we got this far, we've got a reply to our ping in time! + C.close(socket) + return true, end_time - start_time +end + +function Device:getDefaultRoute(interface) + local fd = io.open("/proc/net/route", "re") + if not fd then + return + end + + local gateway + local l = 1 + for line in fd:lines() do + -- Skip the first line (header) + if l > 1 then + local fields = {} + for field in line:gmatch("%S+") do + table.insert(fields, field) + end + -- Check the requested interface or anything that isn't lo + if (interface and fields[1] == interface) or (not interface and fields[1] ~= "lo") then + -- We're looking for something that's up & a gateway + if bit.band(fields[4], C.RTF_UP) ~= 0 and bit.band(fields[4], C.RTF_GATEWAY) ~= 0 then + -- Handle the conversion from network endianness hex string into a human-readable numeric form + local sockaddr_in = ffi.new("struct sockaddr_in") + sockaddr_in.sin_family = C.AF_INET + sockaddr_in.sin_addr.s_addr = tonumber(fields[3], 16) + local host = ffi.new("char[?]", C.NI_MAXHOST) + local s = C.getnameinfo(ffi.cast("struct sockaddr *", sockaddr_in), + ffi.sizeof("struct sockaddr_in"), + host, C.NI_MAXHOST, + nil, 0, + C.NI_NUMERICHOST) + if s ~= 0 then + logger.err("Device:getDefaultRoute: getnameinfo:", ffi.string(C.gai_strerror(s))) + break + else + gateway = ffi.string(host) + -- If we specified an interface, we're done. + -- If we didn't, we'll just keep the last gateway in the routing table... + if interface then + break + end + end + end end + end + l = l + 1 + end + fd:close() + + return gateway +end + +function Device:retrieveNetworkInfo() + -- We're going to need a random socket for the network & wireless ioctls... + local socket = C.socket(C.PF_INET, C.SOCK_DGRAM, C.IPPROTO_IP); + if socket == -1 then + local errno = ffi.errno() + logger.err("Device:retrieveNetworkInfo: socket:", ffi.string(C.strerror(errno))) + return + end + + local ifaddr = ffi.new("struct ifaddrs *[1]") + if C.getifaddrs(ifaddr) == -1 then + local errno = ffi.errno() + logger.err("Device:retrieveNetworkInfo: getifaddrs:", ffi.string(C.strerror(errno))) + return false + end + + -- Build a string rope to format the results + local results = {} + local interfaces = {} + local prev_ifname, default_gw + + -- Loop over all the network interfaces + local ifa = ifaddr[0] + while ifa ~= nil do + -- Skip over loopback or downed interfaces + if ifa.ifa_addr ~= nil and + bit.band(ifa.ifa_flags, C.IFF_UP) ~= 0 and + bit.band(ifa.ifa_flags, C.IFF_LOOPBACK) == 0 then + local family = ifa.ifa_addr.sa_family + if family == C.AF_INET or family == C.AF_INET6 then + local host = ffi.new("char[?]", C.NI_MAXHOST) + local s = C.getnameinfo(ifa.ifa_addr, + family == C.AF_INET and ffi.sizeof("struct sockaddr_in") or ffi.sizeof("struct sockaddr_in6"), + host, C.NI_MAXHOST, + nil, 0, + C.NI_NUMERICHOST) + if s ~= 0 then + logger.err("Device:retrieveNetworkInfo: getnameinfo:", ffi.string(C.gai_strerror(s))) + else + -- Only print the ifname once + local ifname = ffi.string(ifa.ifa_name) + if not interfaces[ifname] then + if prev_ifname and ifname ~= prev_ifname then + -- Add a linebreak between interfaces + table.insert(results, "") + end + prev_ifname = ifname + table.insert(results, T(_("Interface: %1"), ifname)) + interfaces[ifname] = true + -- Get its MAC address + local ifr = ffi.new("struct ifreq") + ffi.copy(ifr.ifr_ifrn.ifrn_name, ifa.ifa_name, C.IFNAMSIZ) + if C.ioctl(socket, C.SIOCGIFHWADDR, ifr) == -1 then + local errno = ffi.errno() + logger.err("Device:retrieveNetworkInfo: SIOCGIFHWADDR ioctl:", ffi.string(C.strerror(errno))) + else + local mac = string.format("%02X:%02X:%02X:%02X:%02X:%02X", + bit.band(ifr.ifr_ifru.ifru_hwaddr.sa_data[0], 0xFF), + bit.band(ifr.ifr_ifru.ifru_hwaddr.sa_data[1], 0xFF), + bit.band(ifr.ifr_ifru.ifru_hwaddr.sa_data[2], 0xFF), + bit.band(ifr.ifr_ifru.ifru_hwaddr.sa_data[3], 0xFF), + bit.band(ifr.ifr_ifru.ifru_hwaddr.sa_data[4], 0xFF), + bit.band(ifr.ifr_ifru.ifru_hwaddr.sa_data[5], 0xFF)) + table.insert(results, T(_("MAC: %1"), mac)) + end + + -- Check if it's a wireless interface (c.f., wireless-tools) + local iwr = ffi.new("struct iwreq") + ffi.copy(iwr.ifr_ifrn.ifrn_name, ifa.ifa_name, C.IFNAMSIZ) + if C.ioctl(socket, C.SIOCGIWNAME, iwr) ~= -1 then + interfaces[ifname] = "wireless" + -- Get its ESSID + local essid = ffi.new("char[?]", C.IW_ESSID_MAX_SIZE + 1) + iwr.u.essid.pointer = ffi.cast("caddr_t", essid) + iwr.u.essid.length = C.IW_ESSID_MAX_SIZE + 1 + iwr.u.essid.flags = 0 + if C.ioctl(socket, C.SIOCGIWESSID, iwr) == -1 then + local errno = ffi.errno() + logger.err("Device:retrieveNetworkInfo: SIOCGIWESSID ioctl:", ffi.string(C.strerror(errno))) + else + local essid_on = iwr.u.data.flags + if essid_on ~= 0 then + local token_index = bit.band(essid_on, C.IW_ENCODE_INDEX) + if token_index > 1 then + table.insert(results, T(_("SSID: \"%1\" [%2]"), ffi.string(essid), token_index)) + else + table.insert(results, T(_("SSID: \"%1\""), ffi.string(essid))) + end + else + table.insert(results, _("SSID: off/any")) + end + end + end + end + + if family == C.AF_INET then + table.insert(results, T(_("IP: %1"), ffi.string(host))) + local gw = self:getDefaultRoute(ifname) + if gw then + table.insert(results, T(_("Default gateway: %1"), gw)) + -- If that's a wireless interface, use *that* one for the ping test + if interfaces[ifname] == "wireless" then + default_gw = gw + end + end + else + table.insert(results, T(_("IPv6: %1"), ffi.string(host))) + --- @todo: Build an IPv6 variant of getDefaultRoute that parses /proc/net/ipv6_route + end + end + end + end + ifa = ifa.ifa_next + end + C.freeifaddrs(ifaddr[0]) + C.close(socket) + + if prev_ifname then + table.insert(results, "") + end + -- Only ping a single gateway (if we found a wireless interface earlier, we've kept its gateway address around) + if not default_gw then + -- If not, we'll simply use the last one in the list... + default_gw = self:getDefaultRoute() + end + if default_gw then + local ok, rtt = self:ping4(default_gw) + if ok then + rtt = string.format("%.3f", rtt * 1/1000) -- i.e., time.to_ms w/o flooring + table.insert(results, _("Gateway ping successful")) + table.insert(results, T(_("RTT: %1 ms"), rtt)) else - result = result .. _("No default gateway to ping") + table.insert(results, _("Gateway ping FAILED")) + if rtt then + rtt = string.format("%.1f", time.to_s(rtt)) + table.insert(results, T(_("Timed out after %1 s"), rtt)) + end end - return result + else + table.insert(results, _("No default gateway to ping")) end + + return table.concat(results, "\n") end function Device:setTime(hour, min) diff --git a/frontend/device/sdl/device.lua b/frontend/device/sdl/device.lua index b29177336..56476fcbb 100644 --- a/frontend/device/sdl/device.lua +++ b/frontend/device/sdl/device.lua @@ -370,19 +370,9 @@ function Device:initNetworkManager(NetworkMgr) function NetworkMgr:isWifiOn() return true end function NetworkMgr:isConnected() -- Pull the default gateway first, so we don't even try to ping anything if there isn't one... - local default_gw, std_out - if isCommand("ip") then - std_out = io.popen([[ip r | grep default | tail -n 1 | cut -d ' ' -f 3]], "r") - else - std_out = io.popen([[route -n | awk '$4 == "UG" {print $2}' | tail -n 1]], "r") - end - - if std_out then - default_gw = std_out:read("*l") - std_out:close() - if not default_gw or default_gw == "" then - return false - end + local default_gw = Device:getDefaultRoute() + if not default_gw then + return false end return 0 == os.execute("ping -c1 -w2 " .. default_gw .. " > /dev/null") end diff --git a/frontend/ui/network/manager.lua b/frontend/ui/network/manager.lua index 5432c4569..34b492bae 100644 --- a/frontend/ui/network/manager.lua +++ b/frontend/ui/network/manager.lua @@ -214,42 +214,6 @@ function NetworkMgr:ifHasAnAddress() return ok end ---[[ --- This would be the aforementioned Linux ioctl approach -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -int main(int argc, char *argv[]) { - // Querying IPv6 would require a different in6_ifreq struct and more hoop-jumping... - struct ifreq ifr; - struct sockaddr_in sai; - strncpy(ifr.ifr_name, *++argv, IFNAMSIZ); - - int fd = socket(PF_INET, SOCK_DGRAM, 0); - ioctl(fd, SIOCGIFADDR, &ifr); - close(fd); - - /* - // inet_ntoa is deprecated - memcpy(&sai, &ifr.ifr_addr, sizeof(sai)); - printf("ifr.ifr_addr: %s\n", inet_ntoa(sai.sin_addr)); - */ - char host[NI_MAXHOST]; - int s = getnameinfo(&ifr.ifr_addr, sizeof(struct sockaddr_in), host, NI_MAXHOST, NULL, 0, NI_NUMERICHOST); - printf("ifr.ifr_addr: %s\n", host); - - return EXIT_SUCCESS; -} ---]] - function NetworkMgr:toggleWifiOn(complete_callback, long_press) local toggle_im = InfoMessage:new{ text = _("Turning on Wi-Fi…"), diff --git a/frontend/ui/network/networklistener.lua b/frontend/ui/network/networklistener.lua index 87c4b6f56..522980fc3 100644 --- a/frontend/ui/network/networklistener.lua +++ b/frontend/ui/network/networklistener.lua @@ -2,6 +2,7 @@ local BD = require("ui/bidi") local Device = require("device") local Event = require("ui/event") local EventListener = require("ui/widget/eventlistener") +local Font = require("ui/font") local InfoMessage = require("ui/widget/infomessage") local NetworkMgr = require("ui/network/manager") local UIManager = require("ui/uimanager") @@ -240,6 +241,8 @@ function NetworkListener:onShowNetworkInfo() if Device.retrieveNetworkInfo then UIManager:show(InfoMessage:new{ text = Device:retrieveNetworkInfo(), + -- IPv6 addresses are *loooooong*! + face = Font:getFace("x_smallinfofont"), }) else UIManager:show(InfoMessage:new{