/* * openuf - inform.c * * UniFi Inform Protocol — full implementation. * * ── HOW IT WORKS ───────────────────────────────────────────────────── * * Every 10 seconds the AP makes an HTTP POST to http://:8080/inform * with a binary TNBU packet containing JSON encrypted with AES-128-CBC. * * The controller responds with another TNBU packet. The AP decrypts, parses * the JSON, and executes the action (_type). * * ── TNBU BINARY PACKET ─────────────────────────────────────────────── * * Offset Bytes Field * ------ ----- ----- * 0 4 Magic "TNBU" * 4 4 Packet version (=0), uint32 BE * 8 6 AP MAC address * 14 2 Flags: bit0=encrypted, bit1=zlib * 16 16 AES IV (when encrypted) * 32 4 Data version (=1), uint32 BE * 36 4 Payload length, uint32 BE * 40 N JSON payload, encrypted with AES-128-CBC * * ── HOW PARAMETERS ARE READ ────────────────────────────────────────── * * CPU: sysinfo_cpu_percent() → /proc/stat (delta across 2 calls) * RAM: sysinfo_mem() → /proc/meminfo * Interfaces: sysinfo_iface() → /proc/net/dev + /sys/class/net/ * Radios: sysinfo_radio() → iw dev info + survey * UCI VAPs: wlan_get_vap_table() → libuci wireless.* * WiFi clients: clients_build_sta_table() → iw dev station dump * IP clients: clients_mac_to_ip() → /proc/net/arp * Client names: clients_mac_to_hostname() → /tmp/dhcp.leases * LLDP neighbors: lldp_read_neighbors() → lldpctl -f json * * ── ADOPTION CYCLE ─────────────────────────────────────────────────── * * 1. AP sends inform with key=DEFAULT, default=true, state=1 * 2. Controller responds: {_type:"cmd", cmd:"set-adopt", * key:"new32hexkey", uri:"http://..."} * 3. AP saves the new key + URL to state.json, adopted=true * 4. AP sends inform with the new key, state=4, default=false * 5. Controller responds: {_type:"setstate", radio_table:[...], vap_table:[...]} * 6. AP applies WiFi config via wlan_apply_config() → libuci → wifi reload */ #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" /* ─── Big-endian helpers ────────────────────────────────────────── */ static void put32be(unsigned char *p, uint32_t v) { p[0]=(v>>24)&0xff; p[1]=(v>>16)&0xff; p[2]=(v>> 8)&0xff; p[3]=v&0xff; } static void put16be(unsigned char *p, uint16_t v) { p[0]=(v>>8)&0xff; p[1]=v&0xff; } static uint32_t get32be(const unsigned char *p) { return ((uint32_t)p[0]<<24)|((uint32_t)p[1]<<16)| ((uint32_t)p[2]<<8)|(uint32_t)p[3]; } static uint16_t get16be(const unsigned char *p) { return ((uint16_t)p[0]<<8)|(uint16_t)p[1]; } static int valid_authkey(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; } /* ═══════════════════════════════════════════════════════════════════ 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 struct json_object *build_sys_stats(void) { struct json_object *o = json_object_new_object(); mem_stats_t mem; if (sysinfo_mem(&mem) == 0) { long used_kb = mem.total_kb - mem.free_kb - mem.buffer_kb - mem.cached_kb; if (used_kb < 0) used_kb = 0; json_object_object_add(o, "mem_total", json_object_new_int64(mem.total_kb * 1024LL)); json_object_object_add(o, "mem_used", json_object_new_int64(used_kb * 1024LL)); json_object_object_add(o, "mem_buffer", json_object_new_int64(mem.buffer_kb * 1024LL)); } else { json_object_object_add(o, "mem_total", json_object_new_int(0)); json_object_object_add(o, "mem_used", json_object_new_int(0)); json_object_object_add(o, "mem_buffer", json_object_new_int(0)); } /* CPU — delta relative to the previous call (every ~10s gives a good average) */ json_object_object_add(o, "cpu", json_object_new_int(sysinfo_cpu_percent())); return o; } /* ═══════════════════════════════════════════════════════════════════ if_table — network interface statistics ═══════════════════════════════════════════════════════════════════ All Ethernet ports on the model are reported. /proc/net/dev is read for counters, and /sys/class/net// for speed, duplex, and link status. */ static struct json_object *build_if_table(const uf_model_t *m, const openuf_state_t *st) { struct json_object *arr = json_object_new_array(); for (int i = 0; i < m->port_table_len; i++) { const char *ifname = m->port_table[i].ifname; iface_stats_t stats; sysinfo_iface(ifname, &stats); struct json_object *o = json_object_new_object(); json_object_object_add(o, "name", json_object_new_string(ifname)); json_object_object_add(o, "mac", json_object_new_string(stats.mac[0] ? stats.mac : st->mac)); json_object_object_add(o, "ip", json_object_new_string(stats.ip[0] ? stats.ip : st->ip)); json_object_object_add(o, "up", json_object_new_boolean(stats.up)); json_object_object_add(o, "speed", json_object_new_int(stats.speed > 0 ? stats.speed : 1000)); json_object_object_add(o, "full_duplex", json_object_new_boolean(stats.full_duplex)); json_object_object_add(o, "num_port", json_object_new_int(1)); json_object_object_add(o, "rx_bytes", json_object_new_int64(stats.rx_bytes)); json_object_object_add(o, "tx_bytes", json_object_new_int64(stats.tx_bytes)); json_object_object_add(o, "rx_packets", json_object_new_int64(stats.rx_packets)); json_object_object_add(o, "tx_packets", json_object_new_int64(stats.tx_packets)); json_object_object_add(o, "rx_errors", json_object_new_int64(stats.rx_errors)); json_object_object_add(o, "tx_errors", json_object_new_int64(stats.tx_errors)); json_object_object_add(o, "rx_dropped", json_object_new_int64(stats.rx_dropped)); json_object_object_add(o, "tx_dropped", json_object_new_int64(stats.tx_dropped)); json_object_object_add(o, "rx_multicast", json_object_new_int64(stats.rx_multicast)); json_object_array_add(arr, o); } return arr; } /* ═══════════════════════════════════════════════════════════════════ radio_table — static definition of the radio hardware ═══════════════════════════════════════════════════════════════════ Describes the physical capabilities of each radio to the controller. The controller uses this to know which frequencies and modes it supports. */ static void build_radio_table(struct json_object *root, const uf_model_t *m) { struct json_object *arr = json_object_new_array(); for (int i = 0; i < m->radio_table_len; i++) { const uf_radio_t *r = &m->radio_table[i]; struct json_object *o = json_object_new_object(); json_object_object_add(o, "name", json_object_new_string(r->name)); json_object_object_add(o, "radio", json_object_new_string(r->radio)); json_object_object_add(o, "channel", json_object_new_int(r->channel)); json_object_object_add(o, "ht", json_object_new_string(r->ht)); json_object_object_add(o, "min_txpower", json_object_new_int(r->min_txpower)); json_object_object_add(o, "max_txpower", json_object_new_int(r->max_txpower)); json_object_object_add(o, "nss", json_object_new_int(r->nss)); json_object_object_add(o, "tx_power", json_object_new_int(r->tx_power)); json_object_object_add(o, "radio_caps", json_object_new_int(r->radio_caps)); json_object_object_add(o, "antenna_gain", json_object_new_int(r->antenna_gain)); json_object_object_add(o, "he_enabled", json_object_new_boolean(r->he_enabled)); json_object_object_add(o, "builtin_antenna", json_object_new_boolean(true)); json_object_object_add(o, "builtin_ant_gain", json_object_new_int(0)); json_object_array_add(arr, o); } json_object_object_add(root, "radio_table", arr); } /* ═══════════════════════════════════════════════════════════════════ radio_table_stats — dynamic channel statistics ═══════════════════════════════════════════════════════════════════ Channel utilization is read in real time using: iw dev wlan0 survey dump → active/busy/tx/rx time iw dev wlan0 info → current channel, power The controller displays this data in the RF view. */ static struct json_object *build_radio_table_stats(const uf_model_t *m) { struct json_object *arr = json_object_new_array(); for (int i = 0; i < m->radio_map_len; i++) { const uf_radio_map_t *rm = &m->radio_map[i]; const char *device = wlan_device_for_band(m, rm->band); if (!device) device = rm->device; /* Map "radio0" → "wlan0" by OpenWrt convention */ char wlan_iface[32]; int ridx = 0; sscanf(device, "radio%d", &ridx); snprintf(wlan_iface, sizeof(wlan_iface), "wlan%d", ridx); /* Radio name in the static table */ const char *radio_name = (i < m->radio_table_len) ? m->radio_table[i].name : wlan_iface; int default_ch = (i < m->radio_table_len) ? m->radio_table[i].channel : 6; int default_pwr = (i < m->radio_table_len) ? m->radio_table[i].tx_power : 20; radio_stats_t rs; if (sysinfo_radio(wlan_iface, &rs) != 0) { memset(&rs, 0, sizeof(rs)); rs.noise = -95; } struct json_object *o = json_object_new_object(); json_object_object_add(o, "name", json_object_new_string(radio_name)); json_object_object_add(o, "channel", json_object_new_int(rs.channel ? rs.channel : default_ch)); json_object_object_add(o, "tx_power", json_object_new_int(rs.tx_power ? rs.tx_power : default_pwr)); json_object_object_add(o, "cu_self_tx", json_object_new_int(rs.cu_self_tx)); json_object_object_add(o, "cu_self_rx", json_object_new_int(rs.cu_self_rx)); json_object_object_add(o, "cu_total", json_object_new_int(rs.cu_total)); json_object_object_add(o, "num_sta", json_object_new_int(rs.num_sta)); json_object_object_add(o, "noise", json_object_new_int(rs.noise)); json_object_array_add(arr, o); } return arr; } /* ═══════════════════════════════════════════════════════════════════ port_table — Real/actual status of the ethernet ports ═══════════════════════════════════════════════════════════════════ /sys/class/net//speed and operstate are read to reflect the actual status of each port on the controller. */ static void build_port_table(struct json_object *root, const uf_model_t *m) { struct json_object *arr = json_object_new_array(); for (int i = 0; i < m->port_table_len; i++) { const uf_port_t *pt = &m->port_table[i]; iface_stats_t stats; sysinfo_iface(pt->ifname, &stats); struct json_object *o = json_object_new_object(); json_object_object_add(o, "ifname", json_object_new_string(pt->ifname)); json_object_object_add(o, "name", json_object_new_string(pt->name)); json_object_object_add(o, "port_idx", json_object_new_int(pt->port_idx)); json_object_object_add(o, "poe_caps", json_object_new_int(pt->poe_caps)); json_object_object_add(o, "media", json_object_new_string(pt->media)); json_object_object_add(o, "speed", json_object_new_int(stats.speed > 0 ? stats.speed : pt->speed)); json_object_object_add(o, "up", json_object_new_boolean(stats.up)); json_object_object_add(o, "is_uplink", json_object_new_boolean(pt->is_uplink)); json_object_object_add(o, "full_duplex", json_object_new_boolean(stats.full_duplex)); json_object_object_add(o, "rx_bytes", json_object_new_int64(stats.rx_bytes)); json_object_object_add(o, "tx_bytes", json_object_new_int64(stats.tx_bytes)); json_object_array_add(arr, o); } json_object_object_add(root, "port_table", arr); } static void build_eth_table(struct json_object *root, const uf_model_t *m) { struct json_object *arr = json_object_new_array(); for (int i = 0; i < m->ethernet_table_len; i++) { const uf_eth_entry_t *e = &m->ethernet_table[i]; struct json_object *o = json_object_new_object(); json_object_object_add(o, "name", json_object_new_string(e->name)); json_object_object_add(o, "num_port", json_object_new_int(e->num_port)); json_object_array_add(arr, o); } json_object_object_add(root, "ethernet_table", arr); } /* ═══════════════════════════════════════════════════════════════════ vap_table — active VAPs with connected clients (sta_table) ═══════════════════════════════════════════════════════════════════ For each active VAP in UCI: 1. Interface statistics for the wlan are read with sysinfo_iface() 2. The current channel is obtained with sysinfo_radio() 3. Clients are enumerated with clients_build_sta_table() → iw dev wlan0 station dump (signal, bitrate, bytes, uptime) → /proc/net/arp (MAC → IP) → /tmp/dhcp.leases (MAC → hostname) The nested sta_table is what the controller uses to: - Display clients on the dashboard - Calculate per-client statistics - Draw the network topology */ static struct json_object *build_vap_table(const uf_model_t *m) { /* Get list of VAPs from UCI */ struct json_object *uci_vaps = wlan_get_vap_table(m); int nvaps = json_object_array_length(uci_vaps); struct json_object *arr = json_object_new_array(); for (int i = 0; i < nvaps; i++) { struct json_object *vap = json_object_array_get_idx(uci_vaps, i); struct json_object *v; const char *essid = ""; const char *vap_name = ""; const char *radio = "ng"; const char *bssid = "00:00:00:00:00:00"; const char *vap_id = NULL; const char *ifname = NULL; int vlan_id = 0; int is_11r = 0; int band_steering = 0; int handoff_suggestions = 0; if (json_object_object_get_ex(vap, "essid", &v)) essid = json_object_get_string(v); if (json_object_object_get_ex(vap, "name", &v)) vap_name = json_object_get_string(v); if (json_object_object_get_ex(vap, "radio", &v)) radio = json_object_get_string(v); if (json_object_object_get_ex(vap, "bssid", &v)) bssid = json_object_get_string(v); if (json_object_object_get_ex(vap, "id", &v)) vap_id = json_object_get_string(v); if (json_object_object_get_ex(vap, "ifname", &v)) ifname = json_object_get_string(v); if (json_object_object_get_ex(vap, "vlan_id", &v)) vlan_id = json_object_get_int(v); if (json_object_object_get_ex(vap, "fast_roaming_enabled", &v)) is_11r = json_object_get_boolean(v); if (json_object_object_get_ex(vap, "band_steering", &v)) band_steering = json_object_get_boolean(v); if (json_object_object_get_ex(vap, "handoff_suggestions", &v)) handoff_suggestions = json_object_get_boolean(v); /* Map band → wlan interface and current channel */ char wlan_iface[32] = "phy0-ap0"; if (ifname && ifname[0]) snprintf(wlan_iface, sizeof(wlan_iface), "%s", ifname); int channel = 6; for (int j = 0; j < m->radio_map_len; j++) { if (strcmp(m->radio_map[j].band, radio) == 0) { int idx = 0; const char *device = wlan_device_for_band( m, m->radio_map[j].band); if (!device) device = m->radio_map[j].device; sscanf(device, "radio%d", &idx); if (!ifname || !ifname[0]) snprintf(wlan_iface, sizeof(wlan_iface), "phy%d-ap0", idx); radio_stats_t rs; if (sysinfo_radio(wlan_iface, &rs) == 0 && rs.channel) channel = rs.channel; else if (idx < m->radio_table_len) channel = m->radio_table[idx].channel; break; } } /* Wireless interface statistics */ iface_stats_t iface_st; sysinfo_iface(wlan_iface, &iface_st); /* Clients connected to this VAP */ struct json_object *sta_tbl = clients_build_sta_table(wlan_iface, radio, channel, vap_name, vlan_id, is_11r); int num_sta = json_object_array_length(sta_tbl); /* Calculate tx_power of the corresponding radio */ int tx_pwr = 20; radio_stats_t rs2; if (sysinfo_radio(wlan_iface, &rs2) == 0 && rs2.tx_power) tx_pwr = rs2.tx_power; struct json_object *o = json_object_new_object(); json_object_object_add(o, "essid", json_object_new_string(essid)); json_object_object_add(o, "bssid", json_object_new_string(bssid)); json_object_object_add(o, "name", json_object_new_string(vap_name)); json_object_object_add(o, "radio", json_object_new_string(radio)); if (vlan_id > 0) json_object_object_add(o, "vlan_id", json_object_new_int(vlan_id)); json_object_object_add(o, "up", json_object_new_boolean(iface_st.up)); json_object_object_add(o, "channel", json_object_new_int(channel)); json_object_object_add(o, "tx_power", json_object_new_int(tx_pwr)); json_object_object_add(o, "band_steering", json_object_new_boolean(band_steering)); json_object_object_add(o, "bss_transition", json_object_new_boolean(handoff_suggestions)); json_object_object_add(o, "handoff_suggestions", json_object_new_boolean(handoff_suggestions)); json_object_object_add(o, "num_sta", json_object_new_int(num_sta)); json_object_object_add(o, "rx_bytes", json_object_new_int64(iface_st.rx_bytes)); json_object_object_add(o, "tx_bytes", json_object_new_int64(iface_st.tx_bytes)); json_object_object_add(o, "rx_packets", json_object_new_int64(iface_st.rx_packets)); json_object_object_add(o, "tx_packets", json_object_new_int64(iface_st.tx_packets)); json_object_object_add(o, "rx_errors", json_object_new_int64(iface_st.rx_errors)); json_object_object_add(o, "tx_errors", json_object_new_int64(iface_st.tx_errors)); json_object_object_add(o, "rx_dropped", json_object_new_int64(iface_st.rx_dropped)); json_object_object_add(o, "tx_dropped", json_object_new_int64(iface_st.tx_dropped)); /* Only controller-issued ObjectIds are valid in this field. */ if (vap_id) json_object_object_add(o, "id", json_object_new_string(vap_id)); json_object_object_add(o, "usage", json_object_new_string("user")); json_object_object_add(o, "ccq", json_object_new_int(0)); /* Nested sta_table — clients of THIS VAP */ json_object_object_add(o, "sta_table", sta_tbl); json_object_array_add(arr, o); } json_object_put(uci_vaps); return arr; } /* Build the device-level station table UniFi uses for client ownership. */ static struct json_object *collect_sta_table(struct json_object *vap_table) { struct json_object *all = json_object_new_array(); int vap_count = json_object_array_length(vap_table); for (int i = 0; i < vap_count; i++) { struct json_object *vap = json_object_array_get_idx(vap_table, i); struct json_object *stations; if (!json_object_object_get_ex(vap, "sta_table", &stations) || !json_object_is_type(stations, json_type_array)) continue; int count = json_object_array_length(stations); for (int j = 0; j < count; j++) json_object_array_add(all, json_object_get( json_object_array_get_idx(stations, j))); } return all; } /* ═══════════════════════════════════════════════════════════════════ build_payload — Complete assembly of the inform JSON ═══════════════════════════════════════════════════════════════════ */ static char *build_payload(const openuf_state_t *st, const uf_model_t *m, long uptime) { /* MAC without colons → serial (uppercase) */ char mac_clean[32] = {0}; { const char *s = st->mac; int j = 0; for (int i = 0; s[i] && j < 12; i++) if (s[i] != ':') { char c = s[i]; if (c >= 'a' && c <= 'f') c -= 32; mac_clean[j++] = c; } } char fw_version[64]; snprintf(fw_version, sizeof(fw_version), "%s%s", m->fw_pre, m->fw_ver); char inform_url_buf[256]; if (st->inform_url[0]) strncpy(inform_url_buf, st->inform_url, sizeof(inform_url_buf)-1); else snprintf(inform_url_buf, sizeof(inform_url_buf), "http://unifi:%d%s", INFORM_PORT, INFORM_PATH); struct json_object *root = json_object_new_object(); /* ── Device identity ──────────────────────────────── */ json_object_object_add(root, "mac", json_object_new_string(st->mac)); json_object_object_add(root, "serial", json_object_new_string(mac_clean)); json_object_object_add(root, "model", json_object_new_string(m->model)); json_object_object_add(root, "model_display", json_object_new_string(m->model_display)); json_object_object_add(root, "display_name", json_object_new_string(m->display_name)); json_object_object_add(root, "board_rev", json_object_new_int(m->board_rev)); json_object_object_add(root, "version", json_object_new_string(fw_version)); json_object_object_add(root, "bootrom_version", json_object_new_string("openuf-v0.4")); json_object_object_add(root, "required_version", json_object_new_string("2.4.4")); json_object_object_add(root, "ip", json_object_new_string(st->ip)); json_object_object_add(root, "hostname", json_object_new_string(st->hostname[0] ? st->hostname : m->display_name)); json_object_object_add(root, "inform_url", json_object_new_string(inform_url_buf)); json_object_object_add(root, "uptime", json_object_new_int64(uptime)); json_object_object_add(root, "time", json_object_new_int64((long long)uptime)); json_object_object_add(root, "state", json_object_new_int(st->adopted ? 4 : 1)); json_object_object_add(root, "default", json_object_new_boolean(!st->adopted)); json_object_object_add(root, "cfgversion", json_object_new_string(st->cfgversion)); json_object_object_add(root, "x_authkey", json_object_new_string(st->adopted ? st->authkey : DEFAULT_AUTH_KEY)); json_object_object_add(root, "_default_key", json_object_new_boolean(!st->adopted)); json_object_object_add(root, "has_eth1", json_object_new_boolean(m->has_eth1)); json_object_object_add(root, "isolated", json_object_new_boolean(false)); json_object_object_add(root, "locating", json_object_new_boolean(false)); json_object_object_add(root, "uplink", json_object_new_string("eth0")); json_object_object_add(root, "country_code", json_object_new_int(0)); /* ── CPU + RAM ──────────────────────────────────────────────── */ json_object_object_add(root, "sys_stats", build_sys_stats()); /* ── Ethernet interfaces with real counters ──────────────── */ json_object_object_add(root, "if_table", build_if_table(m, st)); /* ── Radio capabilities (static, from the model) ─────────────── */ build_radio_table(root, m); /* ── Real-time channel utilization ────────────────────── */ json_object_object_add(root, "radio_table_stats", build_radio_table_stats(m)); /* ── Ethernet ports with actual status ───────────────────────── */ build_port_table(root, m); build_eth_table(root, m); /* Publish both per-VAP and device-level station views. */ struct json_object *vap_table = build_vap_table(m); struct json_object *sta_table = collect_sta_table(vap_table); int station_count = json_object_array_length(sta_table); json_object_object_add(root, "vap_table", vap_table); json_object_object_add(root, "sta_table", sta_table); /* ── LLDP neighbors for visual topology ─────────────────────── */ json_object_object_add(root, "lldp_table", lldp_read_neighbors()); /* Global counters */ json_object_object_add(root, "bytes_r", json_object_new_int(0)); json_object_object_add(root, "bytes_d", json_object_new_int(0)); json_object_object_add(root, "num_sta", json_object_new_int(station_count)); const char *s = json_object_to_json_string(root); /* Log shows what authkey is actually in the payload */ LOG("Payload state=%d, default=%s, adopted=%d, cfgversion=%s, config_applied=%d, x_authkey=%.8s...", st->adopted ? 4 : 1, !st->adopted ? "true" : "false", st->adopted, st->cfgversion, st->config_applied, st->authkey[0] ? st->authkey : "DEFAULT"); char *copy = strdup(s); json_object_put(root); return copy; } /* ═══════════════════════════════════════════════════════════════════ TNBU binary packet ═══════════════════════════════════════════════════════════════════ */ static unsigned char *build_packet(const char *mac_hex, const char *key_hex, const char *payload, int use_aes_gcm, size_t *out_len) { unsigned char iv_hex[33] = {0}; if (crypto_random_hex(iv_hex, 16) != 0) return NULL; unsigned char mac_bin[6]; crypto_hex2bin(mac_hex, mac_bin, 6); size_t pl_len = strlen(payload); size_t body_len = use_aes_gcm ? pl_len + 16 : pl_len + (16 - (pl_len % 16)); size_t pkt_len = 40 + body_len; unsigned char *pkt = malloc(pkt_len); if (!pkt) return NULL; unsigned char *p = pkt; memcpy(p, INFORM_MAGIC, 4); p += 4; put32be(p, INFORM_PKT_VERSION); p += 4; memcpy(p, mac_bin, 6); p += 6; put16be(p, INFORM_FLAG_ENCRYPTED | (use_aes_gcm ? INFORM_FLAG_GCM : 0)); p += 2; unsigned char iv_bin[16]; crypto_hex2bin((char *)iv_hex, iv_bin, 16); memcpy(p, iv_bin, 16); p += 16; put32be(p, INFORM_DATA_VERSION); p += 4; put32be(p, (uint32_t)body_len); p += 4; int enc_len; if (use_aes_gcm) { unsigned char tag[16]; enc_len = crypto_gcm_encrypt(key_hex, (char *)iv_hex, pkt, 40, (const unsigned char *)payload, pl_len, p, tag); if (enc_len >= 0) memcpy(p + enc_len, tag, sizeof(tag)); } else { enc_len = crypto_encrypt(key_hex, (char *)iv_hex, (const unsigned char *)payload, pl_len, p); } if (enc_len < 0) { free(pkt); return NULL; } *out_len = pkt_len; return pkt; } /* ═══════════════════════════════════════════════════════════════════ Parse binary response from the controller ═══════════════════════════════════════════════════════════════════ */ static char *parse_packet(const unsigned char *data, size_t data_len, const char *key_hex) { if (data_len < 40) return NULL; if (memcmp(data, INFORM_MAGIC, 4) != 0) return NULL; uint16_t flags = get16be(data + 14); const unsigned char *iv_bin = data + 16; uint32_t body_len = get32be(data + 36); const unsigned char *body = data + 40; if (40 + body_len > data_len) return NULL; if ((flags & INFORM_FLAG_GCM) != 0) { if (body_len < 16) return NULL; size_t cipher_len = body_len - 16; char iv_hex[33]; crypto_bin2hex(iv_bin, 16, iv_hex); unsigned char *plain = malloc(cipher_len + 1); if (!plain) return NULL; int pl = crypto_gcm_decrypt(key_hex, iv_hex, data, 40, body, cipher_len, body + cipher_len, plain); if (pl < 0) { free(plain); return NULL; } plain[pl] = '\0'; return (char *)plain; } if (flags & INFORM_FLAG_ENCRYPTED) { char iv_hex[33]; crypto_bin2hex(iv_bin, 16, iv_hex); unsigned char *plain = malloc(body_len + 1); if (!plain) return NULL; int pl = crypto_decrypt(key_hex, iv_hex, body, body_len, plain); if (pl < 0) { free(plain); return NULL; } plain[pl] = '\0'; return (char *)plain; } char *copy = malloc(body_len + 1); if (!copy) return NULL; memcpy(copy, body, body_len); copy[body_len] = '\0'; return copy; } /* ═══════════════════════════════════════════════════════════════════ Process JSON command from the controller ═══════════════════════════════════════════════════════════════════ _type == "noop" → do nothing _type == "cmd" → set-adopt / reboot / reset / locate _type == "setstate" → apply radio_table + vap_table via UCI _type == "setparam" → change a single parameter */ static void 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)) type = json_object_get_string(v); LOG("Handling response type: %s", type); /* ── noop ────────────────────────────────────────────────────── */ if (!strcmp(type, "noop")) { strcpy(action_out, "noop"); 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 (valid_authkey(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 (!valid_authkey(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") && valid_authkey(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"); system("reboot &"); } else if (!strcmp(cmd, "reset")) { strcpy(action_out, "reset"); system("rm -f " OPENUF_STATE_FILE); system("reboot &"); } 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) { printf("[openuf] Applying controller WiFi configuration...\n"); 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); } /* ═══════════════════════════════════════════════════════════════════ inform_send — main public function ═══════════════════════════════════════════════════════════════════ */ int inform_send(openuf_state_t *st, const uf_model_t *model, long uptime, char *err_out) { if (!st->inform_url[0]) { LOG("No inform_url set"); strncpy(err_out, "no inform_url", 127); return -1; } const char *key_hex = (st->authkey[0]) ? st->authkey : DEFAULT_AUTH_KEY; /* CRITICAL: When not adopted, ALWAYS use DEFAULT_AUTH_KEY */ if (!st->adopted && st->authkey[0] && strcmp(st->authkey, DEFAULT_AUTH_KEY) != 0) { LOG("WARNING: Device not adopted but has custom authkey! Using DEFAULT instead!"); key_hex = DEFAULT_AUTH_KEY; } LOG("Sending inform: adopted=%d, authkey=%.8s..., inform_url=%s", st->adopted, key_hex, st->inform_url); /* MAC without colons */ char mac_hex[32] = {0}; { const char *s = st->mac; int j = 0; for (int i = 0; s[i] && j < 12; i++) if (s[i] != ':') mac_hex[j++] = s[i]; } char *payload = build_payload(st, model, uptime); if (!payload) { strncpy(err_out, "build_payload OOM", 127); return -1; } LOG("Built payload, length: %zu", strlen(payload)); unsigned char *resp_body = NULL; size_t resp_len = 0; int status = -1; int selected_gcm = st->use_aes_gcm; /* * A controller remembers the negotiated cipher. If local state was * created before use_aes_gcm was persisted, it rejects CBC with HTTP 400 * and cannot send another setparam. Retry once with the other cipher. */ for (int attempt = 0; attempt < 2; attempt++) { size_t pkt_len = 0; unsigned char *pkt = build_packet(mac_hex, key_hex, payload, selected_gcm, &pkt_len); if (!pkt) { free(payload); strncpy(err_out, "build_packet failed", 127); return -1; } LOG("Built packet, length: %zu, cipher: %s", pkt_len, selected_gcm ? "AES-GCM" : "AES-CBC"); status = http_post(st->inform_url, "application/x-binary-data", pkt, pkt_len, &resp_body, &resp_len); free(pkt); LOG("HTTP POST to %s, status: %d, response length: %zu", st->inform_url, status, resp_len); if (status != 400 || !st->adopted || attempt != 0) break; free(resp_body); resp_body = NULL; resp_len = 0; selected_gcm = !selected_gcm; LOG("Controller rejected %s; retrying once with %s", selected_gcm ? "AES-CBC" : "AES-GCM", selected_gcm ? "AES-GCM" : "AES-CBC"); } free(payload); if (status < 0) { snprintf(err_out, 127, "HTTP connect failed"); return -1; } if (status != 200) { snprintf(err_out, 127, "HTTP %d", status); free(resp_body); return -1; } if (st->use_aes_gcm != selected_gcm) { st->use_aes_gcm = selected_gcm; state_save(st); LOG("Recovered cipher state; persisted aes_gcm=%d", st->use_aes_gcm); } if (!resp_body || resp_len == 0) { LOG("No response body"); free(resp_body); return 0; } char *resp_json = parse_packet(resp_body, resp_len, key_hex); free(resp_body); if (!resp_json) { LOG("Failed to parse response packet"); snprintf(err_out, 127, "parse_packet failed"); return -1; } LOG("Parsed response JSON, length=%zu", strlen(resp_json)); struct json_object *resp_obj = json_tokener_parse(resp_json); free(resp_json); if (!resp_obj) { LOG("Failed to parse JSON"); snprintf(err_out, 127, "JSON parse failed"); return -1; } struct json_object *response_type; if (json_object_object_get_ex(resp_obj, "_type", &response_type)) LOG("Parsed response type: %s", json_object_get_string(response_type)); char action[64] = "noop"; handle_response(st, model, resp_obj, action); json_object_put(resp_obj); LOG("Response action: %s", action); if (strcmp(action, "noop") != 0) printf("[openuf] Action: %s\n", action); return 0; }