/* * Handle controller commands, adoption state changes, provisioning results, * firmware-version spoofing, and protocol-safe response diagnostics. */ #include #include #include #include #include #include #include "inform.h" #include "crypto.h" #include "http.h" #include "wlan.h" #include "state.h" #include "config.h" #include "sysinfo.h" #include "clients.h" #include "lldp.h" #include "inform_internal.h" static int inform_valid_auth_key(const char *key) { if (!key || strlen(key) != 32) return 0; for (size_t i = 0; i < 32; i++) if (!isxdigit((unsigned char)key[i])) return 0; return 1; } static int protocol_debug_level; void inform_set_debug_level(int level) { protocol_debug_level = level < 0 ? 0 : level > 2 ? 2 : level; } int inform_debug_level(void) { return protocol_debug_level; } static int inform_debug_system_cfg_key(const char *key) { if (!key || (strncmp(key, "aaa.", 4) && strncmp(key, "wireless.", 9))) return 0; return strstr(key, ".ssid") || strstr(key, ".id") || strstr(key, ".vap_ind") || strstr(key, ".parent"); } void inform_log_controller_response(struct json_object *response, const char *raw_json) { if (protocol_debug_level <= 0 || !response) return; LOG("Protocol debug: decrypted controller response fields follow"); json_object_object_foreach(response, key, value) { LOG("Protocol field: %s type=%s", key, json_type_to_name(json_object_get_type(value))); } struct json_object *system_cfg_object; if (json_object_object_get_ex(response, "system_cfg", &system_cfg_object)) { const char *system_cfg = json_object_get_string(system_cfg_object); char *copy = system_cfg ? strdup(system_cfg) : NULL; if (copy) { char *save = NULL; for (char *line = strtok_r(copy, "\n", &save); line; line = strtok_r(NULL, "\n", &save)) { line[strcspn(line, "\r")] = '\0'; char *equals = strchr(line, '='); if (!equals) continue; *equals = '\0'; if (inform_debug_system_cfg_key(line)) LOG("Protocol system_cfg: %s=%s", line, equals + 1); } free(copy); } } if (protocol_debug_level >= 2) { LOG("WARNING: full decrypted response may contain credentials"); LOG("Protocol response JSON: %s", raw_json ? raw_json : ""); } } /* ═══════════════════════════════════════════════════════════════════ sys_stats — CPU and memory of the system ═══════════════════════════════════════════════════════════════════ The controller shows CPU and RAM in the device view. We read /proc/stat and /proc/meminfo directly. */ static void reboot_openwrt(void) { LOG("Controller requested an OpenWrt reboot"); int status = system("/sbin/reboot"); if (status != 0) LOG("OpenWrt reboot command failed with status=%d", status); } /* ═══════════════════════════════════════════════════════════════════ Process JSON command from the controller ═══════════════════════════════════════════════════════════════════ _type == "noop" → do nothing _type == "reboot" → reboot OpenWrt _type == "cmd" → set-adopt / legacy reboot / reset / locate _type == "setstate" → apply radio_table + vap_table via UCI _type == "setparam" → change a single parameter */ void inform_handle_response(openuf_state_t *st, const uf_model_t *model, struct json_object *resp, char *action_out) { struct json_object *v; const char *type = "noop"; if (json_object_object_get_ex(resp, "_type", &v) && json_object_is_type(v, json_type_string)) type = json_object_get_string(v); LOG("Handling response type: %s", type); /* An OpenWrt host cannot install UniFi firmware. Acknowledge the * controller's request by reporting its target version from now on. */ if (!strcmp(type, "upgrade")) { if (json_object_object_get_ex(resp, "version", &v) && json_object_is_type(v, json_type_string)) { const char *version = json_object_get_string(v); size_t len = strlen(version); if (len > 0 && len < sizeof(st->firmware_version)) { snprintf(st->firmware_version, sizeof(st->firmware_version), "%s", version); state_save(st); LOG("Firmware upgrade spoofed; now reporting version=%s", st->firmware_version); strcpy(action_out, "upgrade-spoofed"); return; } } LOG("Ignoring upgrade response without a valid version"); strcpy(action_out, "upgrade-invalid"); return; } /* ── noop ────────────────────────────────────────────────────── */ if (!strcmp(type, "noop")) { strcpy(action_out, "noop"); return; } /* Modern controllers send reboot as a top-level response type rather * than wrapping it in {"_type":"cmd","cmd":"reboot"}. */ if (!strcmp(type, "reboot")) { strcpy(action_out, "reboot"); reboot_openwrt(); return; } /* ── setparam ────────────────────────────────────────────────── */ if (!strcmp(type, "setparam")) { int received_adoption_key = 0; int applied_system_cfg = 0; /* First parse mgmt_cfg used by modern controllers. */ if (json_object_object_get_ex(resp, "mgmt_cfg", &v)) { const char *mgmt_cfg = json_object_get_string(v); LOG("Parsing mgmt_cfg: %s", mgmt_cfg); /* Parse newline-separated key=value pairs. */ char cfg_copy[2048]; strncpy(cfg_copy, mgmt_cfg, sizeof(cfg_copy)-1); cfg_copy[sizeof(cfg_copy)-1] = '\0'; char *line = strtok(cfg_copy, "\n"); while (line) { char *eq = strchr(line, '='); if (eq) { *eq = '\0'; const char *key = line; const char *val = eq + 1; if (!strcmp(key, "authkey")) LOG("mgmt_cfg param: authkey = %.8s...", val); else LOG("mgmt_cfg param: %s = %s", key, val); if (!strcmp(key, "authkey")) { if (inform_valid_auth_key(val) && strcmp(st->authkey, val) != 0) { int replacing_key = st->authkey[0] && strcmp(st->authkey, DEFAULT_AUTH_KEY) != 0; strncpy(st->authkey, val, sizeof(st->authkey)-1); st->authkey[sizeof(st->authkey)-1] = '\0'; received_adoption_key = 1; LOG("%s device key from setparam", replacing_key ? "Replaced" : "Accepted"); } else if (!inform_valid_auth_key(val)) { LOG("Ignoring invalid authkey from setparam"); } } else if (!strcmp(key, "cfgversion")) { /* * This is the version the controller wants, not proof * that its setstate has been applied locally. */ LOG("Controller requested cfgversion=%s; currently applied=%s", val, st->cfgversion); } else if (!strcmp(key, "use_aes_gcm")) { st->use_aes_gcm = !strcmp(val, "true") || !strcmp(val, "1"); LOG("AES-GCM %s for subsequent inform packets", st->use_aes_gcm ? "enabled" : "disabled"); } else if (!strcmp(key, "mgmt_url")) { /* Could save mgmt_url for future use */ } /* Other management parameters are currently informational. */ } line = strtok(NULL, "\n"); } } struct json_object *system_cfg_obj; if (json_object_object_get_ex(resp, "system_cfg", &system_cfg_obj)) { const char *system_cfg = json_object_get_string(system_cfg_obj); LOG("Applying legacy system_cfg, length=%zu", strlen(system_cfg)); if (wlan_apply_system_cfg(system_cfg, model) == 0) { applied_system_cfg = 1; st->config_applied = true; st->config_schema = OPENUF_CONFIG_SCHEMA; if (json_object_object_get_ex(resp, "cfgversion", &v)) snprintf(st->cfgversion, sizeof(st->cfgversion), "%s", json_object_get_string(v)); LOG("Legacy system_cfg applied successfully, cfgversion=%s", st->cfgversion); } else { st->config_applied = false; strncpy(st->cfgversion, "0", sizeof(st->cfgversion) - 1); LOG("Legacy system_cfg failed; requesting provisioning retry"); } } /* Fall back to the direct key/value format used by older controllers. */ if (json_object_object_get_ex(resp, "key", &v)) { const char *key = json_object_get_string(v); struct json_object *val_o; if (json_object_object_get_ex(resp, "value", &val_o)) { const char *val = json_object_get_string(val_o); LOG("setparam key=%s val=%s", key, val); if (!strcmp(key, "inform_url")) strncpy(st->inform_url, val, sizeof(st->inform_url)-1); else if (!strcmp(key, "authkey") && inform_valid_auth_key(val) && strcmp(st->authkey, val) != 0) { int replacing_key = st->authkey[0] && strcmp(st->authkey, DEFAULT_AUTH_KEY) != 0; strncpy(st->authkey, val, sizeof(st->authkey)-1); st->authkey[sizeof(st->authkey)-1] = '\0'; received_adoption_key = 1; LOG("%s device key from direct setparam", replacing_key ? "Replaced" : "Accepted"); } } } /* * Modern controllers complete adoption by returning the per-device * key in setparam. Mark the device adopted before its next inform so * both the payload and packet encryption switch to that key. */ if (received_adoption_key) { st->adopted = true; LOG("Adoption completed through setparam; next inform will use the controller key"); } state_save(st); LOG("State saved after setparam"); strcpy(action_out, applied_system_cfg ? "provisioned" : received_adoption_key ? "adopted" : "setparam"); return; } /* ── cmd ─────────────────────────────────────────────────────── */ if (!strcmp(type, "cmd")) { const char *cmd = ""; if (json_object_object_get_ex(resp, "cmd", &v)) cmd = json_object_get_string(v); if (!strcmp(cmd, "set-adopt") || !strcmp(cmd, "adopt")) { if (json_object_object_get_ex(resp, "uri", &v)) strncpy(st->inform_url, json_object_get_string(v), sizeof(st->inform_url)-1); if (json_object_object_get_ex(resp, "key", &v)) strncpy(st->authkey, json_object_get_string(v), sizeof(st->authkey)-1); st->adopted = true; state_save(st); strcpy(action_out, "adopted"); LOG("Adopted successfully. Key: %.8s...", st->authkey); } else if (!strcmp(cmd, "reboot")) { strcpy(action_out, "reboot"); reboot_openwrt(); } else if (!strcmp(cmd, "reset")) { strcpy(action_out, "reset"); system("rm -f " OPENUF_STATE_FILE); reboot_openwrt(); } else if (!strcmp(cmd, "locate")) { /* Blink LED — on OpenWrt: echo 1 > /sys/class/leds/.../trigger */ strcpy(action_out, "locate"); } else { snprintf(action_out, 64, "cmd:%s", cmd); } return; } /* ── setstate — WiFi configuration from the controller ──────────── */ if (!strcmp(type, "setstate")) { if (json_object_object_get_ex(resp, "cfgversion", &v)) snprintf(st->cfgversion, sizeof(st->cfgversion), "%s", json_object_get_string(v)); struct json_object *rt = NULL, *vt = NULL; json_object_object_get_ex(resp, "radio_table", &rt); json_object_object_get_ex(resp, "vap_table", &vt); int apply_ok = 0; if (rt || vt) { LOGF(stdout, "Applying controller WiFi configuration..."); apply_ok = wlan_apply_config(resp, model) == 0; } else { LOG("setstate contained neither radio_table nor vap_table"); } st->config_applied = apply_ok; if (apply_ok) st->config_schema = OPENUF_CONFIG_SCHEMA; if (!apply_ok) { strncpy(st->cfgversion, "0", sizeof(st->cfgversion) - 1); LOG("WiFi configuration failed; cfgversion reset so the controller retries"); } state_save(st); strcpy(action_out, apply_ok ? "setstate" : "setstate-failed"); return; } snprintf(action_out, 64, "unknown:%s", type); }