|
|
@@ -1,353 +0,0 @@
|
|
|
--- 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
|
|
|
-
|
|
|
--- Validate MAC address (proper format with valid hex)
|
|
|
-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
|
|
|
- -- First byte: not 00 or FF
|
|
|
- local first = tonumber(parts[1], 16)
|
|
|
- if first == 0 or first == 255 then return false end
|
|
|
- -- Second byte: not FF (multicast)
|
|
|
- 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)
|
|
|
- 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
|
|
|
- local mac, iface, ssid = v:match("^(.+)|(.+)|(.+)$")
|
|
|
- -- Validate MAC when loading from state
|
|
|
- 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
|
|
|
- elseif t == "dhcp" then
|
|
|
- local mac, ip = v:match("^(.+)->(.+)$")
|
|
|
- -- Validate MAC for DHCP too
|
|
|
- if mac and ip and is_valid_mac(mac) 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, 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")
|
|
|
- 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 + SSID names
|
|
|
--------------------------------------------------
|
|
|
-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 SSID name for an interface via iw dev (most reliable)
|
|
|
-local function get_ssid_name(iface)
|
|
|
- -- Map hostapd interface to wlan interface
|
|
|
- local wlan_map = {
|
|
|
- ["hostapd.wlan0-1"] = "wlan0-1",
|
|
|
- ["hostapd.wlan0-2"] = "wlan0-2",
|
|
|
- }
|
|
|
- local wlan_iface = wlan_map[iface] or iface
|
|
|
-
|
|
|
- -- Use iw dev to get exact SSID
|
|
|
- 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+$", "") -- trim
|
|
|
- end
|
|
|
- end
|
|
|
-
|
|
|
- -- Fallback: try iw dev without specific interface
|
|
|
- f = io.popen("iw dev 2>/dev/null")
|
|
|
- if f then
|
|
|
- local result = f:read("*a")
|
|
|
- f:close()
|
|
|
- -- Extract SSIDs from output
|
|
|
- for line in result:gmatch("[^\n]+") do
|
|
|
- local ssid = line:match("ssid%s+(.+)")
|
|
|
- if ssid and ssid ~= "" then
|
|
|
- return ssid
|
|
|
- end
|
|
|
- end
|
|
|
- end
|
|
|
- return iface
|
|
|
-end
|
|
|
-
|
|
|
--- Get all SSIDs mapped to interfaces
|
|
|
-local function get_all_ssids()
|
|
|
- local ssids = {}
|
|
|
- local interfaces = get_hostapd_interfaces()
|
|
|
- for _, iface in ipairs(interfaces) do
|
|
|
- local ssid = get_ssid_name(iface)
|
|
|
- ssids[iface] = { ssid = ssid }
|
|
|
- end
|
|
|
- return ssids
|
|
|
-end
|
|
|
-
|
|
|
--- Get all SSID statuses via iw
|
|
|
-local function get_all_ssids()
|
|
|
- local ssids = {}
|
|
|
- local f = io.popen("iw dev 2>/dev/null")
|
|
|
- if f then
|
|
|
- local result = f:read("*a")
|
|
|
- f:close()
|
|
|
- local current_iface = nil
|
|
|
- for line in result:gmatch("[^\n]+") do
|
|
|
- -- Detect interface from "Interface wlanX-1"
|
|
|
- local iface = line:match("Interface%s+(wlan%d-%d)")
|
|
|
- if iface then
|
|
|
- current_iface = iface
|
|
|
- end
|
|
|
- -- Extract SSID
|
|
|
- local ssid = line:match("ssid%s+(.+)")
|
|
|
- if ssid and current_iface then
|
|
|
- local hostapd_iface = "hostapd." .. current_iface
|
|
|
- ssids[hostapd_iface] = { enabled = true, ssid = ssid }
|
|
|
- end
|
|
|
- end
|
|
|
- end
|
|
|
- return ssids
|
|
|
-end
|
|
|
-
|
|
|
--- Get clients from ALL interfaces WITH their SSID
|
|
|
--- Validate MAC address (proper format with valid hex)
|
|
|
-local function get_wifi_clients()
|
|
|
- local clients = {}
|
|
|
- local interfaces = get_hostapd_interfaces()
|
|
|
-
|
|
|
- -- Get SSID mapping first
|
|
|
- local ssid_map = {}
|
|
|
- for _, iface in ipairs(interfaces) do
|
|
|
- ssid_map[iface] = get_ssid_name(iface)
|
|
|
- end
|
|
|
-
|
|
|
- -- Query each interface separately
|
|
|
- local count = 0
|
|
|
- for _, iface in ipairs(interfaces) do
|
|
|
- log_info("Querying: " .. iface)
|
|
|
- local f = io.popen("ubus call " .. iface .. " get_clients 2>/dev/null")
|
|
|
- if f then
|
|
|
- local result = f:read("*a")
|
|
|
- f:close()
|
|
|
- log_info(iface .. " result: " .. result:sub(1, 100))
|
|
|
- -- Check if there are actually clients (result contains "clients": {})
|
|
|
- if result:find('"clients"') and not result:find('"clients":%s*{}') then
|
|
|
- local mac_count = 0
|
|
|
- -- Extract MAC addresses - match pattern for valid MACs
|
|
|
- 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
|
|
|
- -- Validate MAC before adding
|
|
|
- if is_valid_mac(mac) and not clients[mac] then
|
|
|
- clients[mac] = { iface = iface, ssid = ssid_map[iface] }
|
|
|
- mac_count = mac_count + 1
|
|
|
- end
|
|
|
- end
|
|
|
- log_info(iface .. " clients: " .. mac_count)
|
|
|
- count = count + mac_count
|
|
|
- end
|
|
|
- end
|
|
|
- end
|
|
|
- log_info("Total WiFi clients: " .. count)
|
|
|
-
|
|
|
- return clients
|
|
|
-end
|
|
|
-
|
|
|
--- Get SSID status for ALL interfaces
|
|
|
-local function get_ssid_status()
|
|
|
- local status = {}
|
|
|
- local ssids = get_all_ssids()
|
|
|
- for iface, info in pairs(ssids) do
|
|
|
- if info.enabled then
|
|
|
- status[iface] = true
|
|
|
- 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
|
|
|
- -- Validate MAC before adding
|
|
|
- if is_valid_mac(mac) then leases[mac] = {ip=ip, mac=mac} end
|
|
|
- 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+)")
|
|
|
- -- Validate MAC before adding
|
|
|
- if mac and is_valid_mac(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, info in pairs(current_clients) do
|
|
|
- if not state.clients[mac] then
|
|
|
- log_info("WiFi CONNECTED: " .. mac .. " on " .. info.ssid)
|
|
|
- send_event("wifi_connected", '{"mac":"' .. mac .. '","ssid":"' .. info.ssid .. '","interface":"' .. info.iface .. '"}')
|
|
|
- end
|
|
|
- end
|
|
|
- for mac, info in pairs(state.clients) do
|
|
|
- if not current_clients[mac] then
|
|
|
- log_info("WiFi DISCONNECTED: " .. mac .. " from " .. info.ssid)
|
|
|
- send_event("wifi_disconnected", '{"mac":"' .. mac .. '","ssid":"' .. info.ssid .. '","interface":"' .. info.iface .. '"}')
|
|
|
- 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
|
|
|
- local ssid = get_ssid_name(iface)
|
|
|
- log_info("SSID ENABLED: " .. ssid .. " (" .. iface .. ")")
|
|
|
- send_event("ssid_enabled", '{"interface":"' .. iface .. '","ssid":"' .. ssid .. '"}')
|
|
|
- end
|
|
|
- end
|
|
|
- for iface in pairs(state.ssid_enabled) do
|
|
|
- if not current_ssid[iface] then
|
|
|
- local ssid = get_ssid_name(iface)
|
|
|
- log_info("SSID DISABLED: " .. ssid .. " (" .. iface .. ")")
|
|
|
- send_event("ssid_disabled", '{"interface":"' .. iface .. '","ssid":"' .. ssid .. '"}')
|
|
|
- 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()
|