|
|
@@ -0,0 +1,237 @@
|
|
|
+-- client2server-minimal.lua - Unified DHCP + WiFi + SSID monitoring via ubus
|
|
|
+local cfg = {
|
|
|
+ server_url = "http://163.245.193.47:3843",
|
|
|
+ router_id = "Guayabal",
|
|
|
+ check_interval = 10,
|
|
|
+ state_file = "/tmp/client2server_state",
|
|
|
+}
|
|
|
+
|
|
|
+-------------------------------------------------
|
|
|
+-- Utilities
|
|
|
+-------------------------------------------------
|
|
|
+local function log_info(msg)
|
|
|
+ os.execute('logger -t client2server -p user.info "' .. msg .. '"')
|
|
|
+end
|
|
|
+
|
|
|
+local function http_post(event_type, data)
|
|
|
+ local json = '{"router_id":"' .. cfg.router_id .. '","event":"' .. event_type .. '","data":' .. data .. '}'
|
|
|
+ local f = io.popen("curl -s -X POST '" .. cfg.server_url .. "/api/events' -H 'Content-Type: application/json' -d '" .. json .. "'", "r")
|
|
|
+ local result = f:read("*a")
|
|
|
+ f:close()
|
|
|
+ return result
|
|
|
+end
|
|
|
+
|
|
|
+local function send_event(event_type, data)
|
|
|
+ log_info("Event: " .. event_type)
|
|
|
+ local resp = http_post(event_type, data)
|
|
|
+ if resp and resp:match("OK") then
|
|
|
+ log_info("OK")
|
|
|
+ else
|
|
|
+ log_info("Fail: " .. (resp or "nil"):sub(1, 50))
|
|
|
+ end
|
|
|
+end
|
|
|
+
|
|
|
+-------------------------------------------------
|
|
|
+-- State Management
|
|
|
+-------------------------------------------------
|
|
|
+local function load_state()
|
|
|
+ local f = io.open(cfg.state_file, "r")
|
|
|
+ if f then
|
|
|
+ local content = f:read("*a")
|
|
|
+ f:close()
|
|
|
+ local clients = {}
|
|
|
+ local ssid_enabled = {}
|
|
|
+ local dhcp = {}
|
|
|
+ for line in content:gmatch("[^\n]+") do
|
|
|
+ local t, v = line:match("^([^:]+):(.+)$")
|
|
|
+ if t == "client" then clients[v] = true
|
|
|
+ elseif t == "ssid" then ssid_enabled[v] = true
|
|
|
+ elseif t == "dhcp" then
|
|
|
+ local mac, ip = v:match("^(.+)->(.+)$")
|
|
|
+ if mac and ip then dhcp[mac] = {ip=ip, mac=mac} end
|
|
|
+ end
|
|
|
+ end
|
|
|
+ return { clients = clients, ssid_enabled = ssid_enabled, dhcp = dhcp }
|
|
|
+ end
|
|
|
+ return { clients = {}, ssid_enabled = {}, dhcp = {} }
|
|
|
+end
|
|
|
+
|
|
|
+local function save_state(state)
|
|
|
+ local f = io.open(cfg.state_file, "w")
|
|
|
+ if f then
|
|
|
+ for mac in pairs(state.clients) do
|
|
|
+ f:write("client:" .. mac .. "\n")
|
|
|
+ end
|
|
|
+ for iface in pairs(state.ssid_enabled) do
|
|
|
+ f:write("ssid:" .. iface .. "\n")
|
|
|
+ end
|
|
|
+ for mac, info in pairs(state.dhcp) do
|
|
|
+ f:write("dhcp:" .. mac .. "->" .. info.ip .. "\n")
|
|
|
+ end
|
|
|
+ f:close()
|
|
|
+ end
|
|
|
+end
|
|
|
+
|
|
|
+-------------------------------------------------
|
|
|
+-- WiFi: Get ALL hostapd interfaces
|
|
|
+-------------------------------------------------
|
|
|
+local function get_hostapd_interfaces()
|
|
|
+ local f = io.popen("ubus list | grep hostapd")
|
|
|
+ local interfaces = {}
|
|
|
+ if f then
|
|
|
+ for line in f:lines() do
|
|
|
+ table.insert(interfaces, line)
|
|
|
+ end
|
|
|
+ f:close()
|
|
|
+ end
|
|
|
+ return interfaces
|
|
|
+end
|
|
|
+
|
|
|
+-- Get clients from ALL interfaces
|
|
|
+local function get_wifi_clients()
|
|
|
+ local clients = {}
|
|
|
+ local interfaces = get_hostapd_interfaces()
|
|
|
+ for _, iface in ipairs(interfaces) do
|
|
|
+ local f = io.popen("ubus call " .. iface .. " get_clients 2>/dev/null")
|
|
|
+ if f then
|
|
|
+ local result = f:read("*a")
|
|
|
+ f:close()
|
|
|
+ for mac in result:gmatch('"([a-fA-F0-9:]+)"') do
|
|
|
+ clients[mac] = true
|
|
|
+ end
|
|
|
+ end
|
|
|
+ end
|
|
|
+ return clients
|
|
|
+end
|
|
|
+
|
|
|
+-- Get SSID status for ALL interfaces
|
|
|
+local function get_ssid_status()
|
|
|
+ local status = {}
|
|
|
+ local interfaces = get_hostapd_interfaces()
|
|
|
+ for _, iface in ipairs(interfaces) do
|
|
|
+ local f = io.popen("ubus call " .. iface .. " get_status 2>/dev/null")
|
|
|
+ if f then
|
|
|
+ local result = f:read("*a")
|
|
|
+ f:close()
|
|
|
+ local enabled = result:match('"enabled":%s*(%a+)')
|
|
|
+ if enabled == "true" then
|
|
|
+ status[iface] = true
|
|
|
+ end
|
|
|
+ end
|
|
|
+ end
|
|
|
+ return status
|
|
|
+end
|
|
|
+
|
|
|
+-------------------------------------------------
|
|
|
+-- DHCP: Get leases via ubus
|
|
|
+-------------------------------------------------
|
|
|
+local function get_dhcp_leases()
|
|
|
+ local leases = {}
|
|
|
+
|
|
|
+ -- Try ubus first
|
|
|
+ local f = io.popen("ubus call dhcp ipv4leases 2>/dev/null")
|
|
|
+ if f then
|
|
|
+ local result = f:read("*a")
|
|
|
+ f:close()
|
|
|
+ if result:match('"leases"') then
|
|
|
+ for ip, mac in result:gmatch('"ip":"([%d%.]+)"[^}]*"mac":"([a-fA-F0-9:]+)"') do
|
|
|
+ leases[mac] = {ip=ip, mac=mac}
|
|
|
+ end
|
|
|
+ return leases
|
|
|
+ end
|
|
|
+ end
|
|
|
+
|
|
|
+ -- Fallback: read lease file
|
|
|
+ f = io.popen("cat /tmp/dhcp.leases 2>/dev/null")
|
|
|
+ if f then
|
|
|
+ for line in f:lines() do
|
|
|
+ local e, mac, ip, h, c = line:match("^(%S+) (%S+) (%S+) (%S+) (%S+)")
|
|
|
+ if mac then leases[mac] = {ip=ip, mac=mac, hostname=h} end
|
|
|
+ end
|
|
|
+ f:close()
|
|
|
+ end
|
|
|
+ return leases
|
|
|
+end
|
|
|
+
|
|
|
+-------------------------------------------------
|
|
|
+-- Main Loop
|
|
|
+-------------------------------------------------
|
|
|
+function main()
|
|
|
+ log_info("Starting client2server-minimal...")
|
|
|
+ log_info("Router: " .. cfg.router_id)
|
|
|
+
|
|
|
+ local state = load_state()
|
|
|
+
|
|
|
+ -- Initial event
|
|
|
+ local leases = get_dhcp_leases()
|
|
|
+ local count = 0
|
|
|
+ for _ in pairs(leases) do count = count + 1 end
|
|
|
+ log_info("DHCP leases: " .. count)
|
|
|
+ send_event("router_online", '{"lease_count":' .. count .. '}')
|
|
|
+
|
|
|
+ while true do
|
|
|
+ os.execute("sleep " .. cfg.check_interval)
|
|
|
+
|
|
|
+ -------------------------------------------------
|
|
|
+ -- WiFi Clients
|
|
|
+ -------------------------------------------------
|
|
|
+ local current_clients = get_wifi_clients()
|
|
|
+
|
|
|
+ for mac in pairs(current_clients) do
|
|
|
+ if not state.clients[mac] then
|
|
|
+ log_info("WiFi CONNECTED: " .. mac)
|
|
|
+ send_event("wifi_connected", '{"mac":"' .. mac .. '"}')
|
|
|
+ end
|
|
|
+ end
|
|
|
+ for mac in pairs(state.clients) do
|
|
|
+ if not current_clients[mac] then
|
|
|
+ log_info("WiFi DISCONNECTED: " .. mac)
|
|
|
+ send_event("wifi_disconnected", '{"mac":"' .. mac .. '"}')
|
|
|
+ end
|
|
|
+ end
|
|
|
+ state.clients = current_clients
|
|
|
+
|
|
|
+ -------------------------------------------------
|
|
|
+ -- SSID Status
|
|
|
+ -------------------------------------------------
|
|
|
+ local current_ssid = get_ssid_status()
|
|
|
+
|
|
|
+ for iface in pairs(current_ssid) do
|
|
|
+ if not state.ssid_enabled[iface] then
|
|
|
+ log_info("SSID ENABLED: " .. iface)
|
|
|
+ send_event("ssid_enabled", '{"interface":"' .. iface .. '"}')
|
|
|
+ end
|
|
|
+ end
|
|
|
+ for iface in pairs(state.ssid_enabled) do
|
|
|
+ if not current_ssid[iface] then
|
|
|
+ log_info("SSID DISABLED: " .. iface)
|
|
|
+ send_event("ssid_disabled", '{"interface":"' .. iface .. '"}')
|
|
|
+ end
|
|
|
+ end
|
|
|
+ state.ssid_enabled = current_ssid
|
|
|
+
|
|
|
+ -------------------------------------------------
|
|
|
+ -- DHCP Leases
|
|
|
+ -------------------------------------------------
|
|
|
+ local current_dhcp = get_dhcp_leases()
|
|
|
+
|
|
|
+ for mac, info in pairs(current_dhcp) do
|
|
|
+ if not state.dhcp[mac] then
|
|
|
+ log_info("DHCP NEW: " .. mac .. " -> " .. info.ip)
|
|
|
+ send_event("dhcp_new", '{"mac":"' .. mac .. '","ip":"' .. info.ip .. '"}')
|
|
|
+ end
|
|
|
+ end
|
|
|
+ for mac, info in pairs(state.dhcp) do
|
|
|
+ if not current_dhcp[mac] then
|
|
|
+ log_info("DHCP RELEASE: " .. mac .. " -> " .. info.ip)
|
|
|
+ send_event("dhcp_release", '{"mac":"' .. mac .. '","ip":"' .. info.ip .. '"}')
|
|
|
+ end
|
|
|
+ end
|
|
|
+ state.dhcp = current_dhcp
|
|
|
+
|
|
|
+ -- Save state
|
|
|
+ save_state(state)
|
|
|
+ end
|
|
|
+end
|
|
|
+
|
|
|
+main()
|