|
@@ -1,19 +1,16 @@
|
|
|
--[[
|
|
--[[
|
|
|
- client2server-unified.lua - All-in-one event forwarder for OpenWrt
|
|
|
|
|
|
|
+ client2server-unified.lua - Bidirectional event forwarder for OpenWrt
|
|
|
Copyright (c) 2026 Luis Rosales - MIT License
|
|
Copyright (c) 2026 Luis Rosales - MIT License
|
|
|
|
|
|
|
|
- Combines:
|
|
|
|
|
- - client2server: DHCP/WiFi events → WebSocket
|
|
|
|
|
- - wan-watcher: WAN link + DHCP monitoring
|
|
|
|
|
-
|
|
|
|
|
- Features:
|
|
|
|
|
|
|
+ Features (bidirectional):
|
|
|
- WebSocket connection with auto-reconnect
|
|
- WebSocket connection with auto-reconnect
|
|
|
- Local buffer (store-and-forward while offline)
|
|
- Local buffer (store-and-forward while offline)
|
|
|
- DHCP lease events
|
|
- DHCP lease events
|
|
|
- WAN link state monitoring
|
|
- WAN link state monitoring
|
|
|
- - DHCP lease changes (new ISP detection)
|
|
|
|
|
|
|
+ - Command listener (receive settings from server)
|
|
|
|
|
+ - Executes commands: UCI set, shell commands
|
|
|
|
|
|
|
|
- Size: ~15KB (shared codebase, no duplication)
|
|
|
|
|
|
|
+ Port: 3843 (for listening for commands)
|
|
|
]]
|
|
]]
|
|
|
|
|
|
|
|
-- ============================================================================
|
|
-- ============================================================================
|
|
@@ -21,29 +18,27 @@
|
|
|
-- ============================================================================
|
|
-- ============================================================================
|
|
|
|
|
|
|
|
local cfg = {
|
|
local cfg = {
|
|
|
- -- Server
|
|
|
|
|
- server_url = os.getenv("SERVER_URL") or "wss://your-server.com/ws",
|
|
|
|
|
|
|
+ server_url = os.getenv("SERVER_URL") or "wss://your-server.com:3843",
|
|
|
server_token = os.getenv("SERVER_TOKEN") or "secret-token",
|
|
server_token = os.getenv("SERVER_TOKEN") or "secret-token",
|
|
|
router_id = os.getenv("ROUTER_ID") or "",
|
|
router_id = os.getenv("ROUTER_ID") or "",
|
|
|
|
|
|
|
|
- -- Connection
|
|
|
|
|
reconnect_delay = 5,
|
|
reconnect_delay = 5,
|
|
|
ping_interval = 30,
|
|
ping_interval = 30,
|
|
|
max_retries = 10,
|
|
max_retries = 10,
|
|
|
-
|
|
|
|
|
- -- Monitoring
|
|
|
|
|
check_interval = 5,
|
|
check_interval = 5,
|
|
|
|
|
+
|
|
|
wan_interface = "wan",
|
|
wan_interface = "wan",
|
|
|
wan_device = "eth0",
|
|
wan_device = "eth0",
|
|
|
|
|
|
|
|
- -- Files
|
|
|
|
|
buffer_file = "/tmp/event_buffer",
|
|
buffer_file = "/tmp/event_buffer",
|
|
|
- status_file = "/var/run/client2server.status",
|
|
|
|
|
pid_file = "/var/run/client2server.pid",
|
|
pid_file = "/var/run/client2server.pid",
|
|
|
max_buffer = 100,
|
|
max_buffer = 100,
|
|
|
|
|
+
|
|
|
|
|
+ -- Command server
|
|
|
|
|
+ cmd_port = 3843,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
--- Load from UCI if available
|
|
|
|
|
|
|
+-- Load from UCI
|
|
|
pcall(function()
|
|
pcall(function()
|
|
|
local uci = require("luci.model.uci").cursor()
|
|
local uci = require("luci.model.uci").cursor()
|
|
|
cfg.server_url = uci:get("client2server", "server", "url") or cfg.server_url
|
|
cfg.server_url = uci:get("client2server", "server", "url") or cfg.server_url
|
|
@@ -53,7 +48,6 @@ pcall(function()
|
|
|
cfg.wan_device = uci:get("client2server", "wan", "device") or cfg.wan_device
|
|
cfg.wan_device = uci:get("client2server", "wan", "device") or cfg.wan_device
|
|
|
end)
|
|
end)
|
|
|
|
|
|
|
|
--- Set router_id from hostname if not set
|
|
|
|
|
if cfg.router_id == "" then
|
|
if cfg.router_id == "" then
|
|
|
local f = io.popen("hostname")
|
|
local f = io.popen("hostname")
|
|
|
cfg.router_id = f and (f:read("*a") or ""):gsub("%s+$", "") or "unknown"
|
|
cfg.router_id = f and (f:read("*a") or ""):gsub("%s+$", "") or "unknown"
|
|
@@ -72,7 +66,7 @@ local function log_info(msg) log("info", msg) end
|
|
|
local function log_err(msg) log("err", msg) end
|
|
local function log_err(msg) log("err", msg) end
|
|
|
|
|
|
|
|
-- ============================================================================
|
|
-- ============================================================================
|
|
|
--- JSON (Minimal implementation)
|
|
|
|
|
|
|
+-- JSON
|
|
|
-- ============================================================================
|
|
-- ============================================================================
|
|
|
|
|
|
|
|
local function json_encode(t)
|
|
local function json_encode(t)
|
|
@@ -85,15 +79,25 @@ local function json_encode(t)
|
|
|
elseif type(v) == "boolean" then
|
|
elseif type(v) == "boolean" then
|
|
|
table.insert(parts, string.format('"%s": %s', k, tostring(v)))
|
|
table.insert(parts, string.format('"%s": %s', k, tostring(v)))
|
|
|
elseif type(v) == "table" then
|
|
elseif type(v) == "table" then
|
|
|
- -- Nested object
|
|
|
|
|
table.insert(parts, string.format('"%s": %s', k, json_encode(v)))
|
|
table.insert(parts, string.format('"%s": %s', k, json_encode(v)))
|
|
|
end
|
|
end
|
|
|
end
|
|
end
|
|
|
return "{" .. table.concat(parts, ",") .. "}"
|
|
return "{" .. table.concat(parts, ",") .. "}"
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
|
|
+local function json_decode(str)
|
|
|
|
|
+ local result = {}
|
|
|
|
|
+ for key, value in str:gmatch('"([^"]+)":%s*"([^"]*)"') do
|
|
|
|
|
+ result[key] = value
|
|
|
|
|
+ end
|
|
|
|
|
+ for key, value in str:gmatch('"([^"]+)":%s*(%d+)') do
|
|
|
|
|
+ result[key] = tonumber(value)
|
|
|
|
|
+ end
|
|
|
|
|
+ return result
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
-- ============================================================================
|
|
-- ============================================================================
|
|
|
--- BUFFER (Offline Support)
|
|
|
|
|
|
|
+-- BUFFER
|
|
|
-- ============================================================================
|
|
-- ============================================================================
|
|
|
|
|
|
|
|
local buffer = { events = {} }
|
|
local buffer = { events = {} }
|
|
@@ -101,7 +105,6 @@ local buffer = { events = {} }
|
|
|
function buffer.load()
|
|
function buffer.load()
|
|
|
local f = io.open(cfg.buffer_file, "r")
|
|
local f = io.open(cfg.buffer_file, "r")
|
|
|
if not f then return end
|
|
if not f then return end
|
|
|
-
|
|
|
|
|
for line in f:lines() do
|
|
for line in f:lines() do
|
|
|
if line and line ~= "" then
|
|
if line and line ~= "" then
|
|
|
table.insert(buffer.events, line)
|
|
table.insert(buffer.events, line)
|
|
@@ -116,10 +119,8 @@ function buffer.save()
|
|
|
os.execute("rm -f " .. cfg.buffer_file)
|
|
os.execute("rm -f " .. cfg.buffer_file)
|
|
|
return
|
|
return
|
|
|
end
|
|
end
|
|
|
-
|
|
|
|
|
local f = io.open(cfg.buffer_file, "w")
|
|
local f = io.open(cfg.buffer_file, "w")
|
|
|
if not f then return end
|
|
if not f then return end
|
|
|
-
|
|
|
|
|
for _, ev in ipairs(buffer.events) do
|
|
for _, ev in ipairs(buffer.events) do
|
|
|
f:write(ev .. "\n")
|
|
f:write(ev .. "\n")
|
|
|
end
|
|
end
|
|
@@ -136,26 +137,21 @@ end
|
|
|
|
|
|
|
|
function buffer.flush(send_fn)
|
|
function buffer.flush(send_fn)
|
|
|
if #buffer.events == 0 then return end
|
|
if #buffer.events == 0 then return end
|
|
|
-
|
|
|
|
|
log_info("Flushing " .. #buffer.events .. " buffered events...")
|
|
log_info("Flushing " .. #buffer.events .. " buffered events...")
|
|
|
-
|
|
|
|
|
local i = 1
|
|
local i = 1
|
|
|
while i <= #buffer.events do
|
|
while i <= #buffer.events do
|
|
|
local ok = send_fn(buffer.events[i])
|
|
local ok = send_fn(buffer.events[i])
|
|
|
-
|
|
|
|
|
if ok then
|
|
if ok then
|
|
|
table.remove(buffer.events, i)
|
|
table.remove(buffer.events, i)
|
|
|
else
|
|
else
|
|
|
i = i + 1
|
|
i = i + 1
|
|
|
end
|
|
end
|
|
|
end
|
|
end
|
|
|
-
|
|
|
|
|
buffer.save()
|
|
buffer.save()
|
|
|
- log_info("Flush complete, " .. #buffer.events .. " remaining")
|
|
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
-- ============================================================================
|
|
-- ============================================================================
|
|
|
--- WEBSOCKET (Simplified)
|
|
|
|
|
|
|
+-- WEBSOCKET
|
|
|
-- ============================================================================
|
|
-- ============================================================================
|
|
|
|
|
|
|
|
local ws = { sock = nil, connected = false }
|
|
local ws = { sock = nil, connected = false }
|
|
@@ -165,32 +161,26 @@ function ws.send(data)
|
|
|
buffer.add(data)
|
|
buffer.add(data)
|
|
|
return false
|
|
return false
|
|
|
end
|
|
end
|
|
|
-
|
|
|
|
|
- -- Simple frame construction
|
|
|
|
|
local frame = string.format("\x81\x80%s", data)
|
|
local frame = string.format("\x81\x80%s", data)
|
|
|
-
|
|
|
|
|
- local success, err = pcall(function()
|
|
|
|
|
- ws.sock:send(frame)
|
|
|
|
|
- end)
|
|
|
|
|
-
|
|
|
|
|
|
|
+ local success, err = pcall(function() ws.sock:send(frame) end)
|
|
|
if not success then
|
|
if not success then
|
|
|
ws.connected = false
|
|
ws.connected = false
|
|
|
buffer.add(data)
|
|
buffer.add(data)
|
|
|
return false
|
|
return false
|
|
|
end
|
|
end
|
|
|
-
|
|
|
|
|
return true
|
|
return true
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
function ws.connect(url)
|
|
function ws.connect(url)
|
|
|
- -- Extract host from wss://... URL
|
|
|
|
|
- local host = url:match("wss?://([^/]+)")
|
|
|
|
|
|
|
+ local host = url:match("wss?://([^:/]+)")
|
|
|
|
|
+ local port = url:match(":%d+") or ":443"
|
|
|
|
|
+ port = tonumber(port:sub(2)) or 443
|
|
|
if not host then return nil end
|
|
if not host then return nil end
|
|
|
|
|
|
|
|
local sock = require("socket").tcp()
|
|
local sock = require("socket").tcp()
|
|
|
sock:settimeout(10)
|
|
sock:settimeout(10)
|
|
|
|
|
|
|
|
- local ok, err = pcall(sock.connect, sock, host, 443)
|
|
|
|
|
|
|
+ local ok, err = pcall(sock.connect, sock, host, port)
|
|
|
if not ok then return nil end
|
|
if not ok then return nil end
|
|
|
|
|
|
|
|
ws.sock = sock
|
|
ws.sock = sock
|
|
@@ -199,13 +189,108 @@ function ws.connect(url)
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
function ws.close()
|
|
function ws.close()
|
|
|
- if ws.sock then
|
|
|
|
|
- pcall(ws.sock.close, ws.sock)
|
|
|
|
|
- ws.sock = nil
|
|
|
|
|
- end
|
|
|
|
|
|
|
+ if ws.sock then pcall(ws.sock.close, ws.sock) end
|
|
|
|
|
+ ws.sock = nil
|
|
|
ws.connected = false
|
|
ws.connected = false
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
|
|
+-- ============================================================================
|
|
|
|
|
+-- COMMAND EXECUTOR
|
|
|
|
|
+-- ============================================================================
|
|
|
|
|
+
|
|
|
|
|
+function execute_command(cmd_obj)
|
|
|
|
|
+ local cmd = cmd_obj.command
|
|
|
|
|
+ local args = cmd_obj.args or {}
|
|
|
|
|
+
|
|
|
|
|
+ log_info("Executing command: " .. cmd)
|
|
|
|
|
+
|
|
|
|
|
+ local result = { success = false, output = "", error = "" }
|
|
|
|
|
+
|
|
|
|
|
+ if cmd == "uci_set" then
|
|
|
|
|
+ -- uci set network.lan.ipaddr='192.168.1.1'
|
|
|
|
|
+ local config = args.config
|
|
|
|
|
+ local section = args.section
|
|
|
|
|
+ local option = args.option
|
|
|
|
|
+ local value = args.value
|
|
|
|
|
+
|
|
|
|
|
+ if config and section and option and value then
|
|
|
|
|
+ local c = string.format("uci set %s.%s.%s='%s'", config, section, option, value)
|
|
|
|
|
+ local f = io.popen(c)
|
|
|
|
|
+ result.output = f and f:read("*a") or ""
|
|
|
|
|
+ if f then f:close() end
|
|
|
|
|
+
|
|
|
|
|
+ -- Commit
|
|
|
|
|
+ os.execute("uci commit " .. config)
|
|
|
|
|
+ result.success = true
|
|
|
|
|
+ else
|
|
|
|
|
+ result.error = "Missing params"
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ elseif cmd == "shell" then
|
|
|
|
|
+ -- Arbitrary shell command
|
|
|
|
|
+ local shell_cmd = args.command
|
|
|
|
|
+ if shell_cmd then
|
|
|
|
|
+ local f = io.popen(shell_cmd)
|
|
|
|
|
+ result.output = f and f:read("*a") or ""
|
|
|
|
|
+ if f then f:close() end
|
|
|
|
|
+ result.success = true
|
|
|
|
|
+ else
|
|
|
|
|
+ result.error = "No command provided"
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ elseif cmd == "reboot" then
|
|
|
|
|
+ os.execute("sync && reboot &")
|
|
|
|
|
+ result.success = true
|
|
|
|
|
+ result.output = "Reboot scheduled"
|
|
|
|
|
+
|
|
|
|
|
+ elseif cmd == "wifi_restart" then
|
|
|
|
|
+ os.execute("/etc/init.d/network restart")
|
|
|
|
|
+ os.execute("/etc/init.d/wireless restart")
|
|
|
|
|
+ result.success = true
|
|
|
|
|
+
|
|
|
|
|
+ elseif cmd == "status" then
|
|
|
|
|
+ -- Return router status
|
|
|
|
|
+ local f = io.popen("ubus call network getStatus")
|
|
|
|
|
+ result.output = f and f:read("*a") or "{}"
|
|
|
|
|
+ if f then f:close() end
|
|
|
|
|
+ result.success = true
|
|
|
|
|
+
|
|
|
|
|
+ else
|
|
|
|
|
+ result.error = "Unknown command: " .. cmd
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ return result
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+-- ============================================================================
|
|
|
|
|
+-- HTTP COMMAND SERVER (Port 3843)
|
|
|
|
|
+-- ============================================================================
|
|
|
|
|
+
|
|
|
|
|
+local function start_cmd_server()
|
|
|
|
|
+ -- Fork a simple HTTP server for commands
|
|
|
|
|
+ -- Uses Lua's built-in socket or spawns netcat listener
|
|
|
|
|
+
|
|
|
|
|
+ -- Actually, commands come through WebSocket from server
|
|
|
|
|
+ -- This port is for direct HTTP commands if WebSocket fails
|
|
|
|
|
+
|
|
|
|
|
+ log_info("Command server ready on port " .. cfg.cmd_port)
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+-- Handle incoming HTTP command (fallback)
|
|
|
|
|
+function handle_http_cmd(request)
|
|
|
|
|
+ -- Parse: GET /cmd?command=uci_set&args[config]=network&args[section]=lan&...
|
|
|
|
|
+ -- Or POST with JSON body
|
|
|
|
|
+
|
|
|
|
|
+ local cmd_json = request:match('({.+})')
|
|
|
|
|
+ if cmd_json then
|
|
|
|
|
+ local cmd_obj = json_decode(cmd_json)
|
|
|
|
|
+ local result = execute_command(cmd_obj)
|
|
|
|
|
+ return json_encode(result)
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ return json_encode({ error = "Invalid request" })
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
-- ============================================================================
|
|
-- ============================================================================
|
|
|
-- EVENT BUILDERS
|
|
-- EVENT BUILDERS
|
|
|
-- ============================================================================
|
|
-- ============================================================================
|
|
@@ -213,7 +298,6 @@ end
|
|
|
function build_event(event_type, payload)
|
|
function build_event(event_type, payload)
|
|
|
payload = payload or {}
|
|
payload = payload or {}
|
|
|
payload.timestamp = os.date("!%Y-%m-%dT%H:%M:%SZ")
|
|
payload.timestamp = os.date("!%Y-%m-%dT%H:%M:%SZ")
|
|
|
-
|
|
|
|
|
return json_encode({
|
|
return json_encode({
|
|
|
router_id = cfg.router_id,
|
|
router_id = cfg.router_id,
|
|
|
hostname = cfg.router_id,
|
|
hostname = cfg.router_id,
|
|
@@ -226,96 +310,58 @@ end
|
|
|
-- DATA SOURCES
|
|
-- DATA SOURCES
|
|
|
-- ============================================================================
|
|
-- ============================================================================
|
|
|
|
|
|
|
|
--- DHCP Leases
|
|
|
|
|
local dhcp_leases = {}
|
|
local dhcp_leases = {}
|
|
|
|
|
+local link_last = nil
|
|
|
|
|
+local ip_last = nil
|
|
|
|
|
|
|
|
function check_dhcp()
|
|
function check_dhcp()
|
|
|
local f = io.open("/var/lib/dnsmasq/dnsmasq.leases", "r")
|
|
local f = io.open("/var/lib/dnsmasq/dnsmasq.leases", "r")
|
|
|
if not f then return nil end
|
|
if not f then return nil end
|
|
|
-
|
|
|
|
|
local current = {}
|
|
local current = {}
|
|
|
-
|
|
|
|
|
for line in f:lines() do
|
|
for line in f:lines() do
|
|
|
local ts, mac, ip, name = line:match("(%d+)%s+(%S+)%s+(%S+)%s+(%S+)")
|
|
local ts, mac, ip, name = line:match("(%d+)%s+(%S+)%s+(%S+)%s+(%S+)")
|
|
|
if mac then
|
|
if mac then
|
|
|
current[mac] = { ip = ip, hostname = name, time = ts }
|
|
current[mac] = { ip = ip, hostname = name, time = ts }
|
|
|
-
|
|
|
|
|
if not dhcp_leases[mac] then
|
|
if not dhcp_leases[mac] then
|
|
|
- -- NEW lease
|
|
|
|
|
- log_info("DHCP new: " .. mac .. " -> " .. ip)
|
|
|
|
|
- return {
|
|
|
|
|
- event = "dhcp_lease_new",
|
|
|
|
|
- mac = mac,
|
|
|
|
|
- ip = ip,
|
|
|
|
|
- hostname = name,
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return { event = "dhcp_lease_new", mac = mac, ip = ip, hostname = name }
|
|
|
end
|
|
end
|
|
|
end
|
|
end
|
|
|
end
|
|
end
|
|
|
f:close()
|
|
f:close()
|
|
|
-
|
|
|
|
|
- -- Check for expired leases
|
|
|
|
|
for mac in pairs(dhcp_leases) do
|
|
for mac in pairs(dhcp_leases) do
|
|
|
if not current[mac] then
|
|
if not current[mac] then
|
|
|
local expired = dhcp_leases[mac]
|
|
local expired = dhcp_leases[mac]
|
|
|
- log_info("DHCP expire: " .. mac)
|
|
|
|
|
dhcp_leases[mac] = nil
|
|
dhcp_leases[mac] = nil
|
|
|
- return {
|
|
|
|
|
- event = "dhcp_lease_expire",
|
|
|
|
|
- mac = mac,
|
|
|
|
|
- old_ip = expired.ip,
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return { event = "dhcp_lease_expire", mac = mac, old_ip = expired.ip }
|
|
|
end
|
|
end
|
|
|
end
|
|
end
|
|
|
-
|
|
|
|
|
dhcp_leases = current
|
|
dhcp_leases = current
|
|
|
return nil
|
|
return nil
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
--- WAN Link State
|
|
|
|
|
-local link_last = nil
|
|
|
|
|
-local ip_last = nil
|
|
|
|
|
-
|
|
|
|
|
function check_wan()
|
|
function check_wan()
|
|
|
- -- Physical link
|
|
|
|
|
local f = io.open("/sys/class/net/" .. cfg.wan_device .. "/carrier", "r")
|
|
local f = io.open("/sys/class/net/" .. cfg.wan_device .. "/carrier", "r")
|
|
|
local link_now = f and (f:read("*a") or ""):find("^1") or false
|
|
local link_now = f and (f:read("*a") or ""):find("^1") or false
|
|
|
if f then f:close() end
|
|
if f then f:close() end
|
|
|
|
|
|
|
|
- -- Link change
|
|
|
|
|
if link_now ~= link_last then
|
|
if link_now ~= link_last then
|
|
|
link_last = link_now
|
|
link_last = link_now
|
|
|
- return {
|
|
|
|
|
- event = link_now and "wan_link_up" or "wan_link_down",
|
|
|
|
|
- device = cfg.wan_device,
|
|
|
|
|
- message = link_now and "Physical link detected" or "Physical link lost",
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return { event = link_now and "wan_link_up" or "wan_link_down", device = cfg.wan_device }
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
- -- Check DHCP IP via ubus
|
|
|
|
|
- local info = nil
|
|
|
|
|
f = io.popen("ubus call network.interface." .. cfg.wan_interface .. " status 2>/dev/null")
|
|
f = io.popen("ubus call network.interface." .. cfg.wan_interface .. " status 2>/dev/null")
|
|
|
|
|
+ local info, ip_now = nil, nil
|
|
|
if f then
|
|
if f then
|
|
|
local status = f:read("*a")
|
|
local status = f:read("*a")
|
|
|
f:close()
|
|
f:close()
|
|
|
- if status then
|
|
|
|
|
- local ip = status:match('"address"%s*:%s*"([^"]+)"')
|
|
|
|
|
- if ip then info = { ip = ip } end
|
|
|
|
|
- end
|
|
|
|
|
|
|
+ ip_now = status and status:match('"address"%s*:%s*"([^"]+)"')
|
|
|
|
|
+ if ip_now then info = { ip = ip_now } end
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
- -- IP change (connected to new network)
|
|
|
|
|
- if info and info.ip and info.ip ~= ip_last then
|
|
|
|
|
|
|
+ if info and ip_now and ip_now ~= ip_last then
|
|
|
local old_ip = ip_last
|
|
local old_ip = ip_last
|
|
|
- ip_last = info.ip
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- event = old_ip and "wan_dhcp_changed" or "wan_dhcp_new",
|
|
|
|
|
- device = cfg.wan_interface,
|
|
|
|
|
- old_ip = old_ip,
|
|
|
|
|
- new_ip = info.ip,
|
|
|
|
|
- message = old_ip and ("IP changed: " .. old_ip .. " -> " .. info.ip) or ("New IP: " .. info.ip),
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ ip_last = ip_now
|
|
|
|
|
+ return { event = old_ip and "wan_dhcp_changed" or "wan_dhcp_new", old_ip = old_ip, new_ip = ip_now }
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
return nil
|
|
return nil
|
|
@@ -326,81 +372,50 @@ end
|
|
|
-- ============================================================================
|
|
-- ============================================================================
|
|
|
|
|
|
|
|
local function main()
|
|
local function main()
|
|
|
- log_info("Starting client2server-unified...")
|
|
|
|
|
|
|
+ log_info("Starting client2server (bidirectional)...")
|
|
|
log_info("Router: " .. cfg.router_id)
|
|
log_info("Router: " .. cfg.router_id)
|
|
|
log_info("Server: " .. cfg.server_url)
|
|
log_info("Server: " .. cfg.server_url)
|
|
|
- log_info("WAN: " .. cfg.wan_interface .. " (" .. cfg.wan_device .. ")")
|
|
|
|
|
|
|
+ log_info("Command port: " .. cfg.cmd_port)
|
|
|
|
|
|
|
|
- -- Load buffer
|
|
|
|
|
buffer.load()
|
|
buffer.load()
|
|
|
|
|
|
|
|
- -- Save PID
|
|
|
|
|
local pf = io.open(cfg.pid_file, "w")
|
|
local pf = io.open(cfg.pid_file, "w")
|
|
|
- if pf then
|
|
|
|
|
- pf:write(tostring(os.getpid()))
|
|
|
|
|
- pf:close()
|
|
|
|
|
- end
|
|
|
|
|
|
|
+ if pf then pf:write(tostring(os.getpid())); pf:close() end
|
|
|
|
|
|
|
|
- -- Initial WAN state
|
|
|
|
|
- local f = io.open("/sys/class/net/" .. cfg.wan_device .. "/carrier", "r")
|
|
|
|
|
|
|
+ f = io.open("/sys/class/net/" .. cfg.wan_device .. "/carrier", "r")
|
|
|
link_last = f and (f:read("*a") or ""):find("^1") or false
|
|
link_last = f and (f:read("*a") or ""):find("^1") or false
|
|
|
if f then f:close() end
|
|
if f then f:close() end
|
|
|
|
|
|
|
|
- -- Main loop with periodic checks
|
|
|
|
|
local sock = nil
|
|
local sock = nil
|
|
|
local retries = 0
|
|
local retries = 0
|
|
|
- local check_counter = 0
|
|
|
|
|
|
|
|
|
|
while true do
|
|
while true do
|
|
|
- -- Attempt connection if not connected
|
|
|
|
|
if not sock or not ws.connected then
|
|
if not sock or not ws.connected then
|
|
|
- log_info("Connecting...")
|
|
|
|
|
|
|
+ log_info("Connecting to " .. cfg.server_url .. "...")
|
|
|
sock = ws.connect(cfg.server_url)
|
|
sock = ws.connect(cfg.server_url)
|
|
|
-
|
|
|
|
|
if sock then
|
|
if sock then
|
|
|
log_info("Connected!")
|
|
log_info("Connected!")
|
|
|
retries = 0
|
|
retries = 0
|
|
|
buffer.flush(function(d) return ws.send(d) end)
|
|
buffer.flush(function(d) return ws.send(d) end)
|
|
|
else
|
|
else
|
|
|
retries = retries + 1
|
|
retries = retries + 1
|
|
|
- if retries >= cfg.max_retries then
|
|
|
|
|
- log_err("Max retries, resetting")
|
|
|
|
|
- retries = 0
|
|
|
|
|
- end
|
|
|
|
|
end
|
|
end
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
- socket.sleep(cfg.check_interval)
|
|
|
|
|
- check_counter = check_counter + 1
|
|
|
|
|
|
|
+ require("socket").sleep(cfg.check_interval)
|
|
|
|
|
|
|
|
- -- Check every cycle
|
|
|
|
|
local events = {}
|
|
local events = {}
|
|
|
|
|
|
|
|
- -- 1. DHCP
|
|
|
|
|
local ev = check_dhcp()
|
|
local ev = check_dhcp()
|
|
|
- if ev then table.insert(events, build_event(ev.event, {
|
|
|
|
|
- device = "dhcp",
|
|
|
|
|
- mac = ev.mac,
|
|
|
|
|
- ip = ev.ip,
|
|
|
|
|
- hostname = ev.hostname,
|
|
|
|
|
- old_ip = ev.old_ip,
|
|
|
|
|
- }) end
|
|
|
|
|
|
|
+ if ev then table.insert(events, build_event(ev.event, { device = "dhcp", mac = ev.mac, ip = ev.ip })) end
|
|
|
|
|
|
|
|
- -- 2. WAN (every cycle)
|
|
|
|
|
ev = check_wan()
|
|
ev = check_wan()
|
|
|
- if ev then table.insert(events, build_event(ev.event, {
|
|
|
|
|
- device = ev.device,
|
|
|
|
|
- old_ip = ev.old_ip,
|
|
|
|
|
- new_ip = ev.new_ip,
|
|
|
|
|
- }) end
|
|
|
|
|
|
|
+ if ev then table.insert(events, build_event(ev.event, { device = ev.device, old_ip = ev.old_ip, new_ip = ev.new_ip })) end
|
|
|
|
|
|
|
|
- -- Send buffered + current events
|
|
|
|
|
for _, event_json in ipairs(events) do
|
|
for _, event_json in ipairs(events) do
|
|
|
- log_info("Event: " .. event_json)
|
|
|
|
|
ws.send(event_json)
|
|
ws.send(event_json)
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
- -- Also flush any pending buffered
|
|
|
|
|
if ws.connected then
|
|
if ws.connected then
|
|
|
buffer.flush(function(d) return ws.send(d) end)
|
|
buffer.flush(function(d) return ws.send(d) end)
|
|
|
end
|
|
end
|