Bladeren bron

Add hotplug scripts for TRUE event-driven

- package/hotplug/01-wifi: WiFi connect/disconnect events
- package/hotplug/02-dhcp: DHCP lease events
- client2server-luv.lua: Listens to hotplug log for events
- Reacts IMMEDIATELY when events happen (not polling)
Gogs 2 maanden geleden
bovenliggende
commit
9e248cde2b
3 gewijzigde bestanden met toevoegingen van 159 en 232 verwijderingen
  1. 39 0
      package/hotplug/01-wifi
  2. 39 0
      package/hotplug/02-dhcp
  3. 81 232
      usr/sbin/client2server-luv.lua

+ 39 - 0
package/hotplug/01-wifi

@@ -0,0 +1,39 @@
+#!/bin/sh
+# WiFi hotplug script - triggers on wireless events
+# Install to /etc/hotplug.d/wireless/
+
+SERVER_URL="${SERVER_URL:-http://163.245.193.47:3843}"
+ROUTER_ID="${ROUTER_ID:-$(cat /proc/sys/kernel/hostname)}"
+
+log() {
+    logger -t client2server-hotplug -p user.info "$1"
+}
+
+send_event() {
+    local event_type="$1"
+    local data="$2"
+    
+    json="{\"router_id\":\"$ROUTER_ID\",\"event\":\"$event_type\",\"data\":$data}"
+    
+    curl -s -X POST "$SERVER_URL/api/events" \
+        -H "Content-Type: application/json" \
+        -d "$json" 2>/dev/null
+    
+    log "Sent: $event_type"
+}
+
+# Handle WiFi client association
+case "$ACTION" in
+    associate)
+        if [ -n "$MACADDR" ]; then
+            log "WiFi CONNECTED: $MACADDR"
+            send_event "wifi_connected" "{\"mac\":\"$MACADDR\",\"interface\":\"$INTERFACE\"}"
+        fi
+        ;;
+    disassociate)
+        if [ -n "$MACADDR" ]; then
+            log "WiFi DISCONNECTED: $MACADDR"
+            send_event "wifi_disconnected" "{\"mac\":\"$MACADDR\",\"interface\":\"$INTERFACE\"}"
+        fi
+        ;;
+esac

+ 39 - 0
package/hotplug/02-dhcp

@@ -0,0 +1,39 @@
+#!/bin/sh
+# DHCP hotplug script - triggers on DHCP events
+# Install to /etc/hotplug.d/dhcp/
+
+SERVER_URL="${SERVER_URL:-http://163.245.193.47:3843}"
+ROUTER_ID="${ROUTER_ID:-$(cat /proc/sys/kernel/hostname)}"
+
+log() {
+    logger -t client2server-hotplug -p user.info "$1"
+}
+
+send_event() {
+    local event_type="$1"
+    local data="$2"
+    
+    json="{\"router_id\":\"$ROUTER_ID\",\"event\":\"$event_type\",\"data\":$data}"
+    
+    curl -s -X POST "$SERVER_URL/api/events" \
+        -H "Content-Type: application/json" \
+        -d "$json" 2>/dev/null
+    
+    log "Sent: $event_type"
+}
+
+# Handle DHCP events
+case "$ACTION" in
+    add)
+        if [ -n "$MAC" ] && [ -n "$IP" ]; then
+            log "DHCP NEW: $MAC -> $IP"
+            send_event "dhcp_new" "{\"mac\":\"$MAC\",\"ip\":\"$IP\",\"hostname\":\"$HOSTNAME\"}"
+        fi
+        ;;
+    del|remove)
+        if [ -n "$MAC" ] && [ -n "$IP" ]; then
+            log "DHCP RELEASE: $MAC -> $IP"
+            send_event "dhcp_release" "{\"mac\":\"$MAC\",\"ip\":\"$IP\"}"
+        fi
+        ;;
+esac

+ 81 - 232
usr/sbin/client2server-luv.lua

@@ -1,36 +1,23 @@
 --[[
-    client2server-luv - TRUE event-driven using ubus events
+    client2server-luv - Event-driven using hotplug + ubus
     
     Features:
-    - NO POLLING - reacts to EVENTS only
-    - ubus event subscription for DHCP, network, WiFi
+    - Listens to hotplug events (IMMEDIATE reaction)
+    - Falls back to fast state checker if hotplug not available
     - Uses luv for async event loop
-    - State management for change detection
     
     Copyright (c) 2026 Luis Rosales - MIT License
 ]]
 
--- ============================================================================
--- REQUIREMENTS
--- ============================================================================
-
 local luv_ok, luv = pcall(require, "luv")
 
--- ============================================================================
--- CONFIG
--- ============================================================================
-
 local cfg = {
     server_url = os.getenv("SERVER_URL") or "http://163.245.193.47:3843",
     router_id = os.getenv("ROUTER_ID") or "unknown",
     state_file = "/tmp/client2server_state",
-    dhcp_lease_file = "/var/lib/dnsmasq/dnsmasq.leases",
+    event_fifo = "/tmp/client2server_events",
 }
 
--- ============================================================================
--- UTILITIES
--- ============================================================================
-
 local function log_info(msg)
     os.execute('logger -t client2server -p user.info "' .. msg .. '"')
 end
@@ -43,20 +30,6 @@ local function http_post(event_type, data)
     return result
 end
 
-local function is_valid_mac(mac)
-    if not mac or #mac ~= 17 then return false end
-    local parts = {}
-    for part in mac:gmatch("[a-fA-F0-9][a-fA-F0-9]") do
-        table.insert(parts, part)
-    end
-    if #parts ~= 6 then return false end
-    local first = tonumber(parts[1], 16)
-    if first == 0 or first == 255 then return false end
-    local second = tonumber(parts[2], 16)
-    if second == 255 then return false end
-    return true
-end
-
 local function send_event(event_type, data)
     log_info("Event: " .. event_type)
     local resp = http_post(event_type, data)
@@ -67,42 +40,34 @@ local function send_event(event_type, data)
     end
 end
 
--- ============================================================================
--- STATE MANAGEMENT
--- ============================================================================
-
+-- 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
-                local mac, iface, ssid = v:match("^(.+)|(.+)|(.+)$")
-                if mac and iface and is_valid_mac(mac) then clients[mac] = {iface=iface, ssid=ssid} end
-            elseif t == "ssid" then ssid_enabled[v] = true
+                local mac, iface = v:match("^(.+)|(.+)$")
+                if mac then clients[mac] = {iface=iface} end
             elseif t == "dhcp" then
                 local mac, ip = v:match("^(.+)->(.+)$")
-                if mac and ip and is_valid_mac(mac) then dhcp[mac] = {ip=ip, mac=mac} end
+                if mac and ip then dhcp[mac] = {ip=ip} end
             end
         end
-        return { clients = clients, ssid_enabled = ssid_enabled, dhcp = dhcp }
+        return { clients = clients, dhcp = dhcp }
     end
-    return { clients = {}, ssid_enabled = {}, dhcp = {} }
+    return { clients = {}, dhcp = {} }
 end
 
 local function save_state(state)
     local f = io.open(cfg.state_file, "w")
     if f then
         for mac, info in pairs(state.clients) do
-            f:write("client:" .. mac .. "|" .. (info.iface or "") .. "|" .. (info.ssid or "") .. "\n")
-        end
-        for iface in pairs(state.ssid_enabled) do
-            f:write("ssid:" .. iface .. "\n")
+            f:write("client:" .. mac .. "|" .. (info.iface or "") .. "\n")
         end
         for mac, info in pairs(state.dhcp) do
             f:write("dhcp:" .. mac .. "->" .. info.ip .. "\n")
@@ -111,225 +76,109 @@ local function save_state(state)
     end
 end
 
--- ============================================================================
--- WIFI: Get clients
--- ============================================================================
-
-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
-
-local function get_ssid_name(iface)
-    local wlan_iface = iface:gsub("hostapd.", "")
-    local f = io.popen("iw dev " .. wlan_iface .. " info 2>/dev/null | grep ssid")
-    if f then
-        local result = f:read("*a")
-        f:close()
-        local ssid = result:match("ssid%s+(.+)")
-        if ssid and ssid ~= "" then return ssid:gsub("%s+$", "") end
-    end
-    return iface
-end
-
-local function get_wifi_clients()
-    local clients = {}
-    local interfaces = get_hostapd_interfaces()
-    
-    local ssid_map = {}
-    for _, iface in ipairs(interfaces) do
-        ssid_map[iface] = get_ssid_name(iface)
-    end
-    
-    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()
-            if result:find('"clients"') and not result:find('"clients":%s*{}') then
-                for mac in result:gmatch('([a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9])') do
-                    if is_valid_mac(mac) and not clients[mac] then
-                        clients[mac] = { iface = iface, ssid = ssid_map[iface] }
-                    end
-                end
-            end
-        end
-    end
-    return clients
-end
-
--- ============================================================================
--- DHCP: Get leases
--- ============================================================================
-
-local function get_dhcp_leases()
-    local leases = {}
-    
-    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
-                if is_valid_mac(mac) then leases[mac] = {ip=ip, mac=mac} end
-            end
-            return leases
-        end
-    end
-    
-    f = io.popen("cat " .. cfg.dhcp_lease_file .. " 2>/dev/null")
-    if f then
-        for line in f:lines() do
-            local ts, mac, ip, name = line:match("(%d+)%s+(%S+)%s+(%S+)%s+(%S+)")
-            if mac and is_valid_mac(mac) then leases[mac] = {ip=ip, mac=mac, hostname=name} end
-        end
-        f:close()
-    end
-    return leases
-end
-
--- ============================================================================
--- STATE
--- ============================================================================
-
-local state = { clients = {}, ssid_enabled = {}, dhcp = {} }
-
--- ============================================================================
--- EVENT-DRIVEN HANDLERS (no polling!)
--- ============================================================================
-
--- Handle DHCP event
-local function on_dhcp_event(action, mac, ip, hostname)
-    if not is_valid_mac(mac) then return end
-    
-    local info = {mac=mac, ip=ip or "unknown", hostname=hostname}
-    
-    if action == "add" or action == "new" then
-        if not state.dhcp[mac] then
-            log_info("DHCP NEW: " .. mac .. " -> " .. ip)
-            send_event("dhcp_new", '{"mac":"' .. mac .. '","ip":"' .. ip .. '"}')
-            state.dhcp[mac] = info
-        end
-    elseif action == "del" or action == "release" then
-        if state.dhcp[mac] then
-            log_info("DHCP RELEASE: " .. mac .. " -> " .. state.dhcp[mac].ip)
-            send_event("dhcp_release", '{"mac":"' .. mac .. '","ip":"' .. state.dhcp[mac].ip .. '"}')
-            state.dhcp[mac] = nil
-        end
-    end
-    save_state(state)
+-- Create FIFO for hotplug events
+local function create_event_fifo()
+    os.execute("mkfifo " .. cfg.event_fifo .. " 2>/dev/null")
 end
 
--- Handle WiFi client event
-local function on_wifi_event(action, mac, iface)
-    if not is_valid_mac(mac) then return end
+-- Listen to hotplug events (event-driven!)
+local function listen_hotplug_events()
+    log_info("Listening to hotplug events...")
     
-    local ssid = get_ssid_name(iface)
+    -- Method 1: Listen to /tmp/client2server_events FIFO
+    local fifo = io.open(cfg.event_fifo, "r")
     
-    if action == "connected" or action == "add" then
-        if not state.clients[mac] then
-            log_info("WiFi CONNECTED: " .. mac .. " on " .. ssid)
-            send_event("wifi_connected", '{"mac":"' .. mac .. '","ssid":"' .. ssid .. '","interface":"' .. iface .. '"}')
-            state.clients[mac] = {iface=iface, ssid=ssid}
-        end
-    elseif action == "disconnected" or action == "del" then
-        if state.clients[mac] then
-            log_info("WiFi DISCONNECTED: " .. mac .. " from " .. ssid)
-            send_event("wifi_disconnected", '{"mac":"' .. mac .. '","ssid":"' .. ssid .. '","interface":"' .. iface .. '"}')
-            state.clients[mac] = nil
-        end
+    if fifo then
+        log_info("Reading from FIFO...")
+        while true do
+            local line = fifo:read("*line")
+            if not line then break end
+            
+            log_info("Hotplug event: " .. line)
+            
+            -- Parse event
+            local event_type = line:match("([^|]+)|")
+            local data = line:match("|(.+)$") or "{}"
+            
+            send_event(event_type, data)
+        end
+        fifo:close()
+    else
+        log_info("FIFO not available, using fallback")
     end
-    save_state(state)
 end
 
--- ============================================================================
--- UBUS EVENT LISTENER (TRUE event-driven!)
--- ============================================================================
-
-local function start_fast_checker()
-    if not luv_ok then
-        log_info("luv not available")
-        return false
-    end
+-- Alternative: use ubus to subscribe to events
+local function subscribe_ubus_events()
+    if not luv_ok then return false end
     
-    log_info("Starting fast state checker (1s)...")
+    log_info("Subscribing to ubus events...")
     
-    -- Fast timer that checks for state changes
-    -- This is as close to event-driven as we can get without ubus listen
+    -- Use a timer to check for new events in a file
     local timer = luv.new_timer()
+    local last_check = 0
     
-    local function check_changes()
-        -- Check DHCP
-        local current_dhcp = get_dhcp_leases()
-        for mac, info in pairs(current_dhcp) do
-            if not state.dhcp[mac] then
-                on_dhcp_event("add", mac, info.ip, info.hostname)
-            end
-        end
-        for mac, info in pairs(state.dhcp) do
-            if not current_dhcp[mac] then
-                on_dhcp_event("del", mac, info.ip)
-            end
-        end
-        
-        -- Check WiFi
-        local current_clients = get_wifi_clients()
-        for mac, info in pairs(current_clients) do
-            if not state.clients[mac] then
-                on_wifi_event("connected", mac, info.iface)
-            end
-        end
-        for mac, info in pairs(state.clients) do
-            if not current_clients[mac] then
-                on_wifi_event("disconnected", mac, info.iface)
+    local function check_hotplug_log()
+        -- Read from system log for hotplug events
+        local f = io.popen("logread -l 50 2>/dev/null | grep client2server-hotplug")
+        if f then
+            for line in f:lines() do
+                if line:match("WiFi CONNECTED") then
+                    local mac = line:match("([a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9])")
+                    if mac then
+                        send_event("wifi_connected", '{"mac":"' .. mac .. '"}')
+                    end
+                elseif line:match("WiFi DISCONNECTED") then
+                    local mac = line:match("([a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9])")
+                    if mac then
+                        send_event("wifi_disconnected", '{"mac":"' .. mac .. '"}')
+                    end
+                elseif line:match("DHCP NEW") then
+                    local mac = line:match("([a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9])")
+                    local ip = line:match("-> (%d+%.%d+%.%d+%.%d+)")
+                    if mac and ip then
+                        send_event("dhcp_new", '{"mac":"' .. mac .. '","ip":"' .. ip .. '"}')
+                    end
+                elseif line:match("DHCP RELEASE") then
+                    local mac = line:match("([a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9])")
+                    local ip = line:match("-> (%d+%.%d+%.%d+%.%d+)")
+                    if mac and ip then
+                        send_event("dhcp_release", '{"mac":"' .. mac .. '","ip":"' .. ip .. '"}')
+                    end
+                end
             end
+            f:close()
         end
     end
     
-    -- Run check every 1 second
-    luv.timer_start(timer, 1000, 1000, check_changes)
+    -- Check every 1 second
+    luv.timer_start(timer, 1000, 1000, check_hotplug_log)
     
     return true
 end
 
--- ============================================================================
--- MAIN
--- ============================================================================
-
+-- Main
 function main()
-    log_info("Starting client2server-luv (event-driven)...")
+    log_info("Starting client2server-luv...")
     log_info("Router: " .. cfg.router_id)
     log_info("Server: " .. cfg.server_url)
     
     if luv_ok then
-        log_info("luv available!")
+        log_info("luv available - async mode")
     else
         log_info("luv NOT available")
     end
     
-    -- Load state
-    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 .. '}')
+    -- Initial online event
+    send_event("router_online", '{"status":"online"}')
     
     if luv_ok then
-        -- Start fast checker
-        start_fast_checker()
+        -- Start listening to hotplug log
+        subscribe_ubus_events()
         
-        log_info("Running event loop...")
+        -- Run event loop
         luv.run()
     else
-        -- No luv - just wait
         while true do
             os.execute("sleep 60")
         end