added devcontainer
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
FROM debian:trixie
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
clang \
|
||||
flex \
|
||||
bison \
|
||||
g++ \
|
||||
gawk \
|
||||
gettext \
|
||||
git \
|
||||
libncurses-dev \
|
||||
libssl-dev \
|
||||
python3 \
|
||||
rsync \
|
||||
unzip \
|
||||
zlib1g-dev \
|
||||
file \
|
||||
wget \
|
||||
patch \
|
||||
time \
|
||||
sudo \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN adduser openwrt --disabled-password --gecos "" && \
|
||||
echo "openwrt ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
||||
|
||||
USER openwrt
|
||||
WORKDIR /home/openwrt
|
||||
|
||||
RUN git clone https://github.com/openwrt/openwrt.git && \
|
||||
mkdir -p /home/openwrt/openwrt/package/OpenUniFi
|
||||
|
||||
WORKDIR /home/openwrt/openwrt/package/OpenUniFi
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "OpenUniFi Development",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"remoteUser": "openwrt",
|
||||
|
||||
// 1. Overrides the default volume mount path
|
||||
"workspaceMount": "source=${localWorkspaceFolder},target=/home/openwrt/openwrt/package/OpenUniFi,type=bind,consistency=cached",
|
||||
|
||||
// 2. Tells VS Code to open this directory when the container starts
|
||||
"workspaceFolder": "/home/openwrt/openwrt/package/OpenUniFi",
|
||||
|
||||
// 3. Runs OpenWrt configuration routines after mounting your package
|
||||
"postCreateCommand": "cd /home/openwrt/openwrt && ./scripts/feeds update -a && ./scripts/feeds install -a && cp .devcontainer/openwrt.config .config || true && make defconfig"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
CONFIG_TARGET_mpc85xx=y
|
||||
CONFIG_TARGET_mpc85xx_p1020=y
|
||||
CONFIG_TARGET_mpc85xx_p1020_DEVICE_hpe_msm460=y
|
||||
CONFIG_PACKAGE_kmod-tun=m
|
||||
CONFIG_PACKAGE_openuf=m
|
||||
@@ -0,0 +1,61 @@
|
||||
# openuf — OpenWrt SDK package Makefile
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME := openuf
|
||||
PKG_VERSION := 0.4.0
|
||||
PKG_RELEASE := 1
|
||||
|
||||
PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/openuf
|
||||
SECTION := net
|
||||
CATEGORY := Network
|
||||
TITLE := openUF — UniFi bridge daemon for OpenWrt
|
||||
DEPENDS := +libmbedtls +libuci +libjson-c +kmod-tun
|
||||
URL := https://github.com/openuf/openuf
|
||||
endef
|
||||
|
||||
define Package/openuf/description
|
||||
Emulates a UniFi U6 IW access point, allowing OpenWrt to be managed
|
||||
by a UniFi Network controller. Supports adoption, WiFi config push
|
||||
(band steering, fast roaming, WPA3, PMF), client reporting, CPU/RAM
|
||||
stats, and LLDP topology.
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
mkdir -p $(PKG_BUILD_DIR)
|
||||
$(CP) ./src/* $(PKG_BUILD_DIR)/
|
||||
endef
|
||||
|
||||
TARGET_CFLAGS += -I$(STAGING_DIR)/usr/include -DENABLE_LOGGING=1
|
||||
TARGET_LDFLAGS += -lmbedtls -lmbedcrypto -luci -ljson-c
|
||||
|
||||
define Build/Compile
|
||||
$(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_LDFLAGS) \
|
||||
-o $(PKG_BUILD_DIR)/openuf \
|
||||
$(PKG_BUILD_DIR)/main.c \
|
||||
$(PKG_BUILD_DIR)/config.c \
|
||||
$(PKG_BUILD_DIR)/state.c \
|
||||
$(PKG_BUILD_DIR)/crypto.c \
|
||||
$(PKG_BUILD_DIR)/http.c \
|
||||
$(PKG_BUILD_DIR)/announce.c \
|
||||
$(PKG_BUILD_DIR)/inform.c \
|
||||
$(PKG_BUILD_DIR)/wlan.c \
|
||||
$(PKG_BUILD_DIR)/sysinfo.c \
|
||||
$(PKG_BUILD_DIR)/clients.c \
|
||||
$(PKG_BUILD_DIR)/lldp.c \
|
||||
$(PKG_BUILD_DIR)/models.c
|
||||
endef
|
||||
|
||||
define Package/openuf/install
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/openuf $(1)/usr/sbin/openuf
|
||||
$(INSTALL_DIR) $(1)/etc/openuf
|
||||
$(INSTALL_CONF) ./files/openuf.conf $(1)/etc/openuf/openuf.conf
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) ./files/openuf.init $(1)/etc/init.d/openuf
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,openuf))
|
||||
@@ -0,0 +1,45 @@
|
||||
# openuf — Makefile para compilar directamente en el dispositivo
|
||||
#
|
||||
# Requisitos:
|
||||
# opkg install gcc make \
|
||||
# libmbedtls-dev libuci-dev libjson-c-dev \
|
||||
# lldpd (opcional, para leer vecinos LLDP)
|
||||
#
|
||||
# Uso:
|
||||
# make -f Makefile.standalone
|
||||
# make -f Makefile.standalone install
|
||||
|
||||
CC = gcc
|
||||
CFLAGS = -Wall -Wextra -O2 -I/usr/include -DENABLE_LOGGING=1
|
||||
LDFLAGS = -lmbedtls -lmbedcrypto -luci -ljson-c
|
||||
|
||||
SRCS = src/main.c \
|
||||
src/config.c \
|
||||
src/state.c \
|
||||
src/crypto.c \
|
||||
src/http.c \
|
||||
src/announce.c \
|
||||
src/inform.c \
|
||||
src/wlan.c \
|
||||
src/sysinfo.c \
|
||||
src/clients.c \
|
||||
src/lldp.c \
|
||||
src/models.c
|
||||
|
||||
TARGET = openuf
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(SRCS)
|
||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||
|
||||
install: $(TARGET)
|
||||
install -m 755 $(TARGET) /usr/sbin/openuf
|
||||
[ -f /etc/openuf/openuf.conf ] || install -D -m 644 files/openuf.conf /etc/openuf/openuf.conf
|
||||
install -m 755 files/openuf.init /etc/init.d/openuf
|
||||
# /etc/init.d/openuf enable
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET)
|
||||
|
||||
.PHONY: all install clean
|
||||
@@ -1,2 +1,93 @@
|
||||
# OpenUF
|
||||
# openUF — C
|
||||
|
||||
Daemon that makes an OpenWrt router appear as a **UniFi U6 InWall** to the UniFi Network controller.
|
||||
|
||||
## Implemented Features
|
||||
|
||||
| Feature | Description | Implementation |
|
||||
| --- | --- | --- |
|
||||
| **L2 Discovery** | UDP broadcast + multicast every 10s | `announce.c` → port 10001 |
|
||||
| **Adoption** | AES-128-CBC handshake with the controller | `inform.c` → `handle_response()` |
|
||||
| **WiFi Config** | Creates WiFi networks from the controller via UCI | `wlan.c` → `wlan_apply_config()` |
|
||||
| **Band Steering** | 802.11k/v Neighbor Reports + BSS Transition | `wlan.c` → `apply_vap()` |
|
||||
| **Fast Roaming** | 802.11r FT with mobility_domain derived from MAC | `wlan.c` → `apply_vap()` |
|
||||
| **WPA3 / PMF** | SAE, SAE-mixed, 802.11w 0/1/2 | `wlan.c` → `sec_to_uci()` |
|
||||
| **WiFi Clients** | MAC, signal, bitrate, bytes per VAP | `clients.c` → `iw station dump` |
|
||||
| **Wired Clients** | MACs from bridge FDB | `clients.c` → `bridge fdb` |
|
||||
| **CPU / RAM** | Real-time usage | `sysinfo.c` → `/proc/stat` + `/proc/meminfo` |
|
||||
| **Interfaces** | Speed, duplex, rx/tx counters | `sysinfo.c` → `/proc/net/dev` |
|
||||
| **Channel / RF** | Channel utilization, noise, tx_power | `sysinfo.c` → `iw survey dump` |
|
||||
| **LLDP Send** | Custom frames via AF_PACKET raw socket | `lldp.c` → `lldp_send_frame()` |
|
||||
| **LLDP Read** | Neighbors for UniFi topology | `lldp.c` → `lldpctl -f json` |
|
||||
|
||||
## Quick Installation
|
||||
|
||||
```sh
|
||||
# On the OpenWrt device:
|
||||
opkg update
|
||||
opkg install gcc make libmbedtls-dev libuci-dev libjson-c-dev
|
||||
|
||||
# Compile and install
|
||||
make -f Makefile.standalone install
|
||||
|
||||
# Configure
|
||||
vi /etc/openuf/openuf.conf # adjust controller_ip and lan_if
|
||||
|
||||
# Start
|
||||
/etc/init.d/openuf start
|
||||
/etc/init.d/openuf enable # start on boot
|
||||
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
```ini
|
||||
controller_ip = 192.168.1.1 # UniFi controller IP
|
||||
lan_if = br-lan # LAN interface (for MAC and IP)
|
||||
ufmodel = u6-inwall # emulated model
|
||||
inform_interval = 10 # seconds between informs
|
||||
enable_announce = 1
|
||||
enable_inform = 1
|
||||
|
||||
```
|
||||
|
||||
## U6 InWall Model
|
||||
|
||||
A **U6 IW** is emulated because it has 5 GbE ports, which covers most OpenWrt routers. The model reports:
|
||||
|
||||
* 5 ethernet ports (eth0-eth4)
|
||||
* 2.4 GHz WiFi 6 Radio (HE/802.11ax)
|
||||
* 5 GHz WiFi 6 Radio (HE/802.11ax)
|
||||
|
||||
## LLDP
|
||||
|
||||
For visual topology in UniFi:
|
||||
|
||||
```sh
|
||||
opkg install lldpd
|
||||
/etc/init.d/lldpd start
|
||||
/etc/init.d/lldpd enable
|
||||
|
||||
```
|
||||
|
||||
openuf sends its own LLDP frames even without lldpd (raw socket).
|
||||
With lldpd installed, it also reports upstream neighbors (switches).
|
||||
|
||||
## Adoption
|
||||
|
||||
The process is automatic:
|
||||
|
||||
1. The AP appears as "Pending" in UniFi.
|
||||
2. Click on "Adopt" → the controller sends a new key.
|
||||
3. The AP applies the key and becomes "Connected".
|
||||
4. The controller pushes the WiFi configuration (SSIDs, channels, etc.).
|
||||
|
||||
To reset: `rm /etc/openuf/state.json && reboot`
|
||||
|
||||
## Dependencies
|
||||
|
||||
```sh
|
||||
opkg install libmbedtls libuci libjson-c
|
||||
opkg install lldpd # optional, for topology
|
||||
|
||||
```
|
||||
@@ -0,0 +1,17 @@
|
||||
# openuf — configuration
|
||||
#
|
||||
# controller_ip: UniFi controller IP address or hostname
|
||||
# lan_if: primary LAN interface (used for the AP MAC and IP)
|
||||
# ufmodel: emulated model (u6-inwall recommended)
|
||||
# inform_interval: seconds between inform requests (minimum 5)
|
||||
# enable_announce: 1=enable UDP discovery (port 10001)
|
||||
# enable_inform: 1=enable HTTP inform requests (adoption and telemetry)
|
||||
# enable_logging: 1=enable logging to /var/log/openuf.log
|
||||
|
||||
controller_ip = 10.10.10.1
|
||||
lan_if = br-lan
|
||||
ufmodel = uapg2-ac-lr
|
||||
inform_interval = 10
|
||||
enable_announce = 1
|
||||
enable_inform = 1
|
||||
enable_logging = 1
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# openuf init script (procd)
|
||||
|
||||
USE_PROCD=1
|
||||
START=95
|
||||
STOP=10
|
||||
|
||||
CONF=/etc/openuf/openuf.conf
|
||||
PROG=/usr/sbin/openuf
|
||||
|
||||
start_service() {
|
||||
procd_open_instance
|
||||
procd_set_param command "$PROG" -c "$CONF"
|
||||
procd_set_param respawn 3600 5 0
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
killall openuf 2>/dev/null
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
stop_service
|
||||
start_service
|
||||
}
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
* openuf - announce.c
|
||||
*
|
||||
* Implementa el protocolo de descubrimiento UDP de UniFi (puerto 10001).
|
||||
*
|
||||
* ── Destinos ─────────────────────────────────────────────────────────
|
||||
* El protocolo especifica que los paquetes de anuncio se envían a DOS destinos:
|
||||
* 1. Broadcast: 255.255.255.255:10001
|
||||
* 2. Multicast: 233.89.188.1:10001 ← requerido para redes con multicast
|
||||
*
|
||||
* El controlador UniFi escucha en ambas direcciones.
|
||||
* Usar sólo broadcast puede fallar en redes donde el broadcast está filtrado.
|
||||
*
|
||||
* ── Formato del paquete ──────────────────────────────────────────────
|
||||
* Header: [0x02][0x06][0x00][total_payload_len] (4 bytes fijos)
|
||||
* TLVs: [type:1][len_hi:1][len_lo:1][value:len]
|
||||
*
|
||||
* ── Modelo U6 InWall ─────────────────────────────────────────────────
|
||||
* Se emula este modelo específicamente porque:
|
||||
* - Tiene 5 puertos GbE (eth0-eth4): cubre la mayoría de routers OpenWrt
|
||||
* - Soporta WiFi 6 (802.11ax) en 2.4 GHz y 5 GHz
|
||||
* - Tiene PoE passthrough (útil para redes de campus)
|
||||
* - Es un modelo actual y bien soportado por el controlador
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
|
||||
#include "announce.h"
|
||||
#include "config.h"
|
||||
|
||||
#ifdef ENABLE_LOGGING
|
||||
#include <stdio.h>
|
||||
extern FILE *log_fp;
|
||||
#define LOG(fmt, ...) do { if (log_fp) { fprintf(log_fp, "[%s] " fmt "\n", __func__, ##__VA_ARGS__); fflush(log_fp); } } while(0)
|
||||
#else
|
||||
#define LOG(fmt, ...) do {} while(0)
|
||||
#endif
|
||||
|
||||
/* ─── Packet type constants ─────────────────────────────────────── */
|
||||
#define PKT_TYPE_HW_ADDR 0x01
|
||||
#define PKT_TYPE_IP_ADDR 0x02
|
||||
#define PKT_TYPE_FWVER_VERBOSE 0x03
|
||||
#define PKT_TYPE_UPTIME 0x0a
|
||||
#define PKT_TYPE_HOSTNAME 0x0b
|
||||
#define PKT_TYPE_PLATFORM 0x0c
|
||||
#define PKT_TYPE_INC_COUNTER 0x12
|
||||
#define PKT_TYPE_HW_ADDR2 0x13
|
||||
#define PKT_TYPE_PLATFORM2 0x15
|
||||
#define PKT_TYPE_FWVER_SHORT 0x16
|
||||
#define PKT_TYPE_FWVER_FACTORY 0x1b
|
||||
|
||||
/* Fixed capability blob (types 0x17–0x1a) */
|
||||
static const unsigned char PKT_BLOB[] = {
|
||||
0x17, 0x00, 0x01, 0x01,
|
||||
0x18, 0x00, 0x01, 0x00,
|
||||
0x19, 0x00, 0x01, 0x01,
|
||||
0x1a, 0x00, 0x01, 0x00,
|
||||
};
|
||||
|
||||
/* ─── TLV helpers ───────────────────────────────────────────────── */
|
||||
static int tlv_append(unsigned char *pkt, int pos, int max,
|
||||
uint8_t type, const unsigned char *val, int vlen)
|
||||
{
|
||||
if (pos + 3 + vlen > max) return pos;
|
||||
pkt[pos++] = type;
|
||||
pkt[pos++] = (vlen >> 8) & 0xff;
|
||||
pkt[pos++] = vlen & 0xff;
|
||||
memcpy(pkt + pos, val, vlen);
|
||||
return pos + vlen;
|
||||
}
|
||||
|
||||
static int tlv_str(unsigned char *pkt, int pos, int max,
|
||||
uint8_t type, const char *str)
|
||||
{
|
||||
return tlv_append(pkt, pos, max, type,
|
||||
(const unsigned char *)str, strlen(str));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* ─── parse_mac ─────────────────────────────────────────────────── */
|
||||
static void parse_mac(const char *s, unsigned char out[6])
|
||||
{
|
||||
unsigned int b[6] = {0};
|
||||
sscanf(s, "%x:%x:%x:%x:%x:%x",
|
||||
&b[0], &b[1], &b[2], &b[3], &b[4], &b[5]);
|
||||
for (int i = 0; i < 6; i++) out[i] = (unsigned char)b[i];
|
||||
}
|
||||
|
||||
static void parse_ip(const char *s, unsigned char out[4])
|
||||
{
|
||||
unsigned int b[4] = {0};
|
||||
sscanf(s, "%u.%u.%u.%u", &b[0], &b[1], &b[2], &b[3]);
|
||||
for (int i = 0; i < 4; i++) out[i] = (unsigned char)b[i];
|
||||
}
|
||||
|
||||
/* ─── announce_init ─────────────────────────────────────────────── */
|
||||
int announce_init(announce_ctx_t *ctx,
|
||||
const uf_model_t *m,
|
||||
const char *mac_str,
|
||||
const char *ip_str)
|
||||
{
|
||||
memset(ctx, 0, sizeof(*ctx));
|
||||
ctx->sockfd = -1;
|
||||
ctx->sockfd_mcast = -1;
|
||||
|
||||
unsigned char mac[6], ip[4];
|
||||
parse_mac(mac_str, mac);
|
||||
parse_ip(ip_str, ip);
|
||||
|
||||
unsigned char *p = ctx->pkt;
|
||||
int pos = 0;
|
||||
int max = (int)sizeof(ctx->pkt);
|
||||
|
||||
/* Packet header: version=2, reserved=6, flags=0, len (filled later) */
|
||||
p[pos++] = 0x02;
|
||||
p[pos++] = 0x06;
|
||||
p[pos++] = 0x00;
|
||||
p[pos++] = 0x00; /* total_payload_len – patched at end */
|
||||
|
||||
/* IP_ADDR TLV: mac(6) + ip(4) */
|
||||
{
|
||||
unsigned char val[10];
|
||||
memcpy(val, mac, 6);
|
||||
memcpy(val + 6, ip, 4);
|
||||
pos = tlv_append(p, pos, max, PKT_TYPE_IP_ADDR, val, 10);
|
||||
}
|
||||
|
||||
/* HW_ADDR: mac */
|
||||
pos = tlv_append(p, pos, max, PKT_TYPE_HW_ADDR, mac, 6);
|
||||
|
||||
/* UPTIME: 4-byte BE – record offset for patching */
|
||||
{
|
||||
unsigned char u4[4] = {0, 0, 0, 10};
|
||||
ctx->uptime_offset = pos + 3; /* offset of the value bytes */
|
||||
pos = tlv_append(p, pos, max, PKT_TYPE_UPTIME, u4, 4);
|
||||
}
|
||||
|
||||
/* HOSTNAME */
|
||||
pos = tlv_str(p, pos, max, PKT_TYPE_HOSTNAME, m->display_name);
|
||||
|
||||
/* PLATFORM */
|
||||
pos = tlv_str(p, pos, max, PKT_TYPE_PLATFORM, m->platform);
|
||||
|
||||
/* FWVER_VERBOSE: "<pre><ver>-<version>.<buildtime>" */
|
||||
{
|
||||
char fwv[128];
|
||||
snprintf(fwv, sizeof(fwv), "%s%s-openUF-%s.%s",
|
||||
m->fw_pre, m->fw_ver, OPENUF_VERSION, m->fw_buildtime);
|
||||
pos = tlv_str(p, pos, max, PKT_TYPE_FWVER_VERBOSE, fwv);
|
||||
}
|
||||
|
||||
/* FWVER_SHORT: "<ver>-openUF-<version>" */
|
||||
{
|
||||
char fws[64];
|
||||
snprintf(fws, sizeof(fws), "%s-openUF-%s", m->fw_ver, OPENUF_VERSION);
|
||||
pos = tlv_str(p, pos, max, PKT_TYPE_FWVER_SHORT, fws);
|
||||
}
|
||||
|
||||
/* PLATFORM2 */
|
||||
pos = tlv_str(p, pos, max, PKT_TYPE_PLATFORM2, m->platform);
|
||||
|
||||
/* Capability blob */
|
||||
memcpy(p + pos, PKT_BLOB, sizeof(PKT_BLOB));
|
||||
pos += sizeof(PKT_BLOB);
|
||||
|
||||
/* HW_ADDR2: mac */
|
||||
pos = tlv_append(p, pos, max, PKT_TYPE_HW_ADDR2, mac, 6);
|
||||
|
||||
/* INC_COUNTER: 4 bytes – record offset for patching */
|
||||
{
|
||||
unsigned char c4[4] = {0, 0, 0, 0};
|
||||
ctx->ctr_offset = pos + 3;
|
||||
pos = tlv_append(p, pos, max, PKT_TYPE_INC_COUNTER, c4, 4);
|
||||
}
|
||||
|
||||
/* FWVER_FACTORY */
|
||||
pos = tlv_str(p, pos, max, PKT_TYPE_FWVER_FACTORY, m->fw_factoryver);
|
||||
|
||||
/* Patch total payload length (byte 3 = total - 4 header bytes) */
|
||||
p[3] = (unsigned char)((pos - 4) & 0xff);
|
||||
|
||||
ctx->pkt_len = pos;
|
||||
ctx->counter = 0;
|
||||
ctx->uptime = 10;
|
||||
|
||||
/* ── Socket para broadcast 255.255.255.255 ─────────────────── */
|
||||
ctx->sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (ctx->sockfd < 0) {
|
||||
perror("[openuf] announce socket");
|
||||
return -1;
|
||||
}
|
||||
int on = 1;
|
||||
setsockopt(ctx->sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
|
||||
/* Bind a puerto efímero — OpenWrt no permite setpeername() a broadcast */
|
||||
struct sockaddr_in bind_addr = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_addr.s_addr = INADDR_ANY,
|
||||
.sin_port = 0,
|
||||
};
|
||||
bind(ctx->sockfd, (struct sockaddr *)&bind_addr, sizeof(bind_addr));
|
||||
|
||||
/* ── Socket para multicast 233.89.188.1 ────────────────────── */
|
||||
/* El controlador UniFi también escucha en este grupo multicast.
|
||||
* Esto es necesario cuando broadcast está filtrado en la red. */
|
||||
ctx->sockfd_mcast = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (ctx->sockfd_mcast >= 0) {
|
||||
int ttl = 1; /* TTL=1: no cruzar router */
|
||||
setsockopt(ctx->sockfd_mcast, IPPROTO_IP, IP_MULTICAST_TTL,
|
||||
&ttl, sizeof(ttl));
|
||||
int loop = 0;
|
||||
setsockopt(ctx->sockfd_mcast, IPPROTO_IP, IP_MULTICAST_LOOP,
|
||||
&loop, sizeof(loop));
|
||||
bind(ctx->sockfd_mcast, (struct sockaddr *)&bind_addr, sizeof(bind_addr));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ─── announce_send ─────────────────────────────────────────────── */
|
||||
int announce_send(announce_ctx_t *ctx)
|
||||
{
|
||||
ctx->counter++;
|
||||
ctx->uptime += 10;
|
||||
|
||||
/* Patch counter and uptime in the packet buffer */
|
||||
put32be(ctx->pkt + ctx->ctr_offset, ctx->counter);
|
||||
put32be(ctx->pkt + ctx->uptime_offset, ctx->uptime);
|
||||
|
||||
int ret = 0;
|
||||
|
||||
/* ── Envío 1: Broadcast 255.255.255.255:10001 ─────────────── */
|
||||
struct sockaddr_in dest_bcast = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_port = htons(ANNOUNCE_PORT),
|
||||
.sin_addr.s_addr = INADDR_BROADCAST,
|
||||
};
|
||||
if (sendto(ctx->sockfd, ctx->pkt, ctx->pkt_len, 0,
|
||||
(struct sockaddr *)&dest_bcast, sizeof(dest_bcast)) < 0) {
|
||||
perror("[openuf] announce sendto broadcast");
|
||||
ret = -1;
|
||||
}
|
||||
|
||||
/* ── Envío 2: Multicast 233.89.188.1:10001 ────────────────── */
|
||||
if (ctx->sockfd_mcast >= 0) {
|
||||
struct sockaddr_in dest_mcast = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_port = htons(ANNOUNCE_PORT),
|
||||
};
|
||||
inet_pton(AF_INET, "233.89.188.1", &dest_mcast.sin_addr);
|
||||
if (sendto(ctx->sockfd_mcast, ctx->pkt, ctx->pkt_len, 0,
|
||||
(struct sockaddr *)&dest_mcast, sizeof(dest_mcast)) < 0) {
|
||||
/* No es error crítico — algunos kernels no tienen ruta multicast */
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* ─── announce_close ────────────────────────────────────────────── */
|
||||
void announce_close(announce_ctx_t *ctx)
|
||||
{
|
||||
if (ctx->sockfd >= 0) {
|
||||
close(ctx->sockfd);
|
||||
ctx->sockfd = -1;
|
||||
}
|
||||
if (ctx->sockfd_mcast >= 0) {
|
||||
close(ctx->sockfd_mcast);
|
||||
ctx->sockfd_mcast = -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifndef OPENUF_ANNOUNCE_H
|
||||
#define OPENUF_ANNOUNCE_H
|
||||
|
||||
#include "ufmodel.h"
|
||||
|
||||
/* Announce context – keeps mutable state between sends */
|
||||
typedef struct {
|
||||
int sockfd; /* socket broadcast */
|
||||
int sockfd_mcast; /* socket multicast 233.89.188.1 */
|
||||
unsigned char pkt[512];
|
||||
int pkt_len;
|
||||
int ctr_offset; /* byte offset of counter field in pkt */
|
||||
int uptime_offset;
|
||||
uint32_t counter;
|
||||
uint32_t uptime;
|
||||
} announce_ctx_t;
|
||||
|
||||
/* Build the static part of the announce packet and open the UDP socket.
|
||||
* mac_str : "aa:bb:cc:dd:ee:ff"
|
||||
* ip_str : "192.168.1.x"
|
||||
* Returns 0 on success. */
|
||||
int announce_init(announce_ctx_t *ctx,
|
||||
const uf_model_t *model,
|
||||
const char *mac_str,
|
||||
const char *ip_str);
|
||||
|
||||
/* Send one announce burst to 255.255.255.255:10001.
|
||||
* Increments counter and uptime. Returns 0 on success. */
|
||||
int announce_send(announce_ctx_t *ctx);
|
||||
|
||||
/* Close the socket */
|
||||
void announce_close(announce_ctx_t *ctx);
|
||||
|
||||
#endif /* OPENUF_ANNOUNCE_H */
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* openuf - clients.c
|
||||
*
|
||||
* Enumera clientes para el payload inform → sta_table.
|
||||
*
|
||||
* ── Parseo de iw dev station dump ───────────────────────────────────
|
||||
*
|
||||
* La salida tiene bloques por cliente:
|
||||
*
|
||||
* Station aa:bb:cc:dd:ee:ff (on wlan0)
|
||||
* inactive time: 120 ms
|
||||
* rx bytes: 2000000
|
||||
* rx packets: 2000
|
||||
* tx bytes: 5000000
|
||||
* tx packets: 5000
|
||||
* signal: -62 [-62, -65] dBm
|
||||
* tx bitrate: 144.4 MBit/s MCS 15
|
||||
* rx bitrate: 108.0 MBit/s
|
||||
* connected time: 1800 seconds
|
||||
*
|
||||
* Detectamos el inicio de cada cliente con "Station XX:XX:..." y
|
||||
* rellenamos los campos hasta encontrar el siguiente cliente.
|
||||
*
|
||||
* ── ARP: /proc/net/arp ─────────────────────────────────────────────
|
||||
*
|
||||
* IP HW type Flags HW addr Mask Device
|
||||
* 192.168.1.x 0x1 0x2 aa:bb:cc:dd:ee:ff * br-lan
|
||||
*
|
||||
* Flags 0x2 = entrada completa (reachable).
|
||||
* Flags 0x0 = incompleta (no responde ARP), ignorar.
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <json-c/json.h>
|
||||
|
||||
#include "clients.h"
|
||||
|
||||
/* ─── Normalizar MAC a minúsculas ─────────────────────────────────── */
|
||||
static void mac_lower(const char *src, char *dst, size_t sz)
|
||||
{
|
||||
for (size_t i = 0; src[i] && i < sz-1; i++)
|
||||
dst[i] = tolower((unsigned char)src[i]);
|
||||
dst[strlen(src) < sz ? strlen(src) : sz-1] = '\0';
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
/proc/net/arp — MAC → IP
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
int clients_mac_to_ip(const char *mac, char *ip_out, size_t sz)
|
||||
{
|
||||
ip_out[0] = '\0';
|
||||
FILE *f = fopen("/proc/net/arp", "r");
|
||||
if (!f) return -1;
|
||||
|
||||
char line[256];
|
||||
fgets(line, sizeof(line), f); /* skip header */
|
||||
|
||||
char ml[32] = {0};
|
||||
mac_lower(mac, ml, sizeof(ml));
|
||||
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
char ip[64], hw_type[16], flags[16], hw[32], mask[16], dev[32];
|
||||
if (sscanf(line, "%63s %15s %15s %31s %15s %31s",
|
||||
ip, hw_type, flags, hw, mask, dev) != 6) continue;
|
||||
if (strcmp(flags, "0x2") != 0) continue;
|
||||
char hl[32] = {0};
|
||||
mac_lower(hw, hl, sizeof(hl));
|
||||
if (strcmp(ml, hl) == 0) {
|
||||
strncpy(ip_out, ip, sz-1);
|
||||
fclose(f); return 0;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
/tmp/dhcp.leases — MAC → hostname
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
int clients_mac_to_hostname(const char *mac, char *out, size_t sz)
|
||||
{
|
||||
out[0] = '\0';
|
||||
static const char *files[] = {
|
||||
"/tmp/dhcp.leases",
|
||||
"/var/lib/misc/dnsmasq.leases",
|
||||
NULL
|
||||
};
|
||||
|
||||
char ml[32] = {0};
|
||||
mac_lower(mac, ml, sizeof(ml));
|
||||
|
||||
for (int fi = 0; files[fi]; fi++) {
|
||||
FILE *f = fopen(files[fi], "r");
|
||||
if (!f) continue;
|
||||
char line[256];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
long ts;
|
||||
char lm[32], lip[64], lh[64], lcid[64];
|
||||
if (sscanf(line, "%ld %31s %63s %63s %63s",
|
||||
&ts, lm, lip, lh, lcid) < 4) continue;
|
||||
char ll[32] = {0};
|
||||
mac_lower(lm, ll, sizeof(ll));
|
||||
if (strcmp(ml, ll) == 0 && strcmp(lh, "*") != 0) {
|
||||
strncpy(out, lh, sz-1);
|
||||
fclose(f); return 0;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ─── Parsear tasa de bits "144.4 MBit/s ..." → kbps ───────────── */
|
||||
static long parse_rate_kbps(const char *s)
|
||||
{
|
||||
float r = 0;
|
||||
sscanf(s, "%f MBit/s", &r);
|
||||
return (long)(r * 1000.0f);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
iw dev <iface> station dump → array sta_info_t
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
int clients_read_wifi(const char *wlan_iface,
|
||||
const char *radio_band,
|
||||
int channel,
|
||||
sta_info_t *out,
|
||||
int max_out)
|
||||
{
|
||||
char cmd[128];
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"iw dev %s station dump 2>/dev/null", wlan_iface);
|
||||
FILE *p = popen(cmd, "r");
|
||||
if (!p) return 0;
|
||||
|
||||
int count = 0;
|
||||
sta_info_t *cur = NULL;
|
||||
char line[256];
|
||||
|
||||
while (fgets(line, sizeof(line), p)) {
|
||||
line[strcspn(line, "\r\n")] = '\0';
|
||||
|
||||
/* ── Nueva estación ──────────────────────────────────────── */
|
||||
char mac[32], on_iface[32];
|
||||
if (sscanf(line, "Station %31s (on %31[^)])", mac, on_iface) == 2) {
|
||||
if (count >= max_out) break;
|
||||
cur = &out[count++];
|
||||
memset(cur, 0, sizeof(*cur));
|
||||
strncpy(cur->mac, mac, sizeof(cur->mac)-1);
|
||||
strncpy(cur->vap_name, wlan_iface, sizeof(cur->vap_name)-1);
|
||||
strncpy(cur->radio, radio_band, sizeof(cur->radio)-1);
|
||||
cur->channel = channel;
|
||||
cur->noise = -95;
|
||||
continue;
|
||||
}
|
||||
if (!cur) continue;
|
||||
|
||||
/* ── Contadores ──────────────────────────────────────────── */
|
||||
long long llv;
|
||||
if (sscanf(line, " rx bytes: %lld", &llv) == 1) { cur->rx_bytes = llv; continue; }
|
||||
if (sscanf(line, " tx bytes: %lld", &llv) == 1) { cur->tx_bytes = llv; continue; }
|
||||
if (sscanf(line, " rx packets: %lld", &llv) == 1) { cur->rx_packets = llv; continue; }
|
||||
if (sscanf(line, " tx packets: %lld", &llv) == 1) { cur->tx_packets = llv; continue; }
|
||||
|
||||
/* ── Señal ───────────────────────────────────────────────── */
|
||||
int sig;
|
||||
if (sscanf(line, " signal: %d", &sig) == 1) { cur->signal = sig; continue; }
|
||||
|
||||
/* ── Bitrate ─────────────────────────────────────────────── */
|
||||
char rest[128];
|
||||
if (sscanf(line, " tx bitrate: %127[^\n]", rest) == 1) {
|
||||
cur->tx_rate = parse_rate_kbps(rest); continue;
|
||||
}
|
||||
if (sscanf(line, " rx bitrate: %127[^\n]", rest) == 1) {
|
||||
cur->rx_rate = parse_rate_kbps(rest); continue;
|
||||
}
|
||||
|
||||
/* ── Tiempo conectado ────────────────────────────────────── */
|
||||
int upt;
|
||||
if (sscanf(line, " connected time: %d seconds", &upt) == 1) {
|
||||
cur->uptime = upt; continue;
|
||||
}
|
||||
}
|
||||
pclose(p);
|
||||
|
||||
/* ── Enriquecer: IP, hostname, rssi, CCQ ─────────────────────── */
|
||||
for (int i = 0; i < count; i++) {
|
||||
sta_info_t *s = &out[i];
|
||||
clients_mac_to_ip(s->mac, s->ip, sizeof(s->ip));
|
||||
clients_mac_to_hostname(s->mac, s->hostname, sizeof(s->hostname));
|
||||
if (!s->hostname[0])
|
||||
strncpy(s->hostname, s->mac, sizeof(s->hostname)-1);
|
||||
|
||||
/* RSN = SNR estimado (signal - noise) */
|
||||
s->rssi = s->signal - s->noise;
|
||||
if (s->rssi < 0) s->rssi = 0;
|
||||
|
||||
/* CCQ: métrica 0-1000
|
||||
* -50 dBm → 1000 (excelente)
|
||||
* -90 dBm → 0 (muy malo)
|
||||
* fórmula lineal: (signal + 90) * 25, limitado 0-1000 */
|
||||
int ccq = (s->signal + 90) * 25;
|
||||
s->ccq = (ccq < 0) ? 0 : (ccq > 1000) ? 1000 : ccq;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
Construir JSON sta_table para un VAP
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
|
||||
El JSON array resultante se anida dentro de vap_table[i].sta_table
|
||||
en el payload inform. Ejemplo de entrada:
|
||||
{
|
||||
"mac": "aa:bb:cc:dd:ee:ff",
|
||||
"ip": "192.168.1.100",
|
||||
"hostname": "mi-movil",
|
||||
"signal": -62,
|
||||
"rssi": 33,
|
||||
"noise": -95,
|
||||
"tx_rate": 144000,
|
||||
"rx_rate": 108000,
|
||||
"tx_bytes": 5000000,
|
||||
"rx_bytes": 2000000,
|
||||
"tx_packets": 5000,
|
||||
"rx_packets": 2000,
|
||||
"uptime": 1800,
|
||||
"radio": "ng",
|
||||
"channel": 6,
|
||||
"vap_name": "ath0",
|
||||
"is_11r": false,
|
||||
"ccq": 700
|
||||
}
|
||||
*/
|
||||
struct json_object *clients_build_sta_table(const char *wlan_iface,
|
||||
const char *radio_band,
|
||||
int channel,
|
||||
const char *vap_name)
|
||||
{
|
||||
sta_info_t stas[MAX_STA];
|
||||
int n = clients_read_wifi(wlan_iface, radio_band, channel,
|
||||
stas, MAX_STA);
|
||||
|
||||
struct json_object *arr = json_object_new_array();
|
||||
for (int i = 0; i < n; i++) {
|
||||
sta_info_t *s = &stas[i];
|
||||
struct json_object *o = json_object_new_object();
|
||||
json_object_object_add(o, "mac", json_object_new_string(s->mac));
|
||||
json_object_object_add(o, "ip", json_object_new_string(s->ip));
|
||||
json_object_object_add(o, "hostname", json_object_new_string(s->hostname));
|
||||
json_object_object_add(o, "signal", json_object_new_int(s->signal));
|
||||
json_object_object_add(o, "rssi", json_object_new_int(s->rssi));
|
||||
json_object_object_add(o, "noise", json_object_new_int(s->noise));
|
||||
json_object_object_add(o, "tx_rate", json_object_new_int64(s->tx_rate));
|
||||
json_object_object_add(o, "rx_rate", json_object_new_int64(s->rx_rate));
|
||||
json_object_object_add(o, "tx_bytes", json_object_new_int64(s->tx_bytes));
|
||||
json_object_object_add(o, "rx_bytes", json_object_new_int64(s->rx_bytes));
|
||||
json_object_object_add(o, "tx_packets", json_object_new_int64(s->tx_packets));
|
||||
json_object_object_add(o, "rx_packets", json_object_new_int64(s->rx_packets));
|
||||
json_object_object_add(o, "uptime", json_object_new_int(s->uptime));
|
||||
json_object_object_add(o, "radio", json_object_new_string(s->radio));
|
||||
json_object_object_add(o, "channel", json_object_new_int(s->channel));
|
||||
json_object_object_add(o, "vap_name", json_object_new_string(
|
||||
vap_name ? vap_name : wlan_iface));
|
||||
json_object_object_add(o, "is_11r", json_object_new_boolean(s->is_11r));
|
||||
json_object_object_add(o, "ccq", json_object_new_int(s->ccq));
|
||||
json_object_object_add(o, "idletime", json_object_new_int(0));
|
||||
json_object_array_add(arr, o);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
#ifndef OPENUF_CLIENTS_H
|
||||
#define OPENUF_CLIENTS_H
|
||||
|
||||
/*
|
||||
* openuf - clients.h
|
||||
*
|
||||
* Enumera clientes conectados (WiFi y ethernet) para el sta_table
|
||||
* del payload inform.
|
||||
*
|
||||
* ── WiFi: iw dev <iface> station dump ────────────────────────────
|
||||
*
|
||||
* Por cada cliente asociado devuelve:
|
||||
* MAC, señal (dBm), tx/rx bitrate (MBit/s), tx/rx bytes,
|
||||
* tx/rx packets, connected time (segundos)
|
||||
*
|
||||
* ── IP del cliente: /proc/net/arp ───────────────────────────────
|
||||
*
|
||||
* Cruce MAC → IP. Solo entradas completas (flags=0x2).
|
||||
*
|
||||
* ── Hostname: /tmp/dhcp.leases (dnsmasq) ────────────────────────
|
||||
*
|
||||
* Formato: timestamp MAC IP hostname client-id
|
||||
*
|
||||
* ── Ethernet: bridge fdb show ───────────────────────────────────
|
||||
*
|
||||
* MACs dinámicas (no permanent, no multicast) en el bridge.
|
||||
*
|
||||
* ── CCQ (Client Connection Quality) ─────────────────────────────
|
||||
*
|
||||
* Métrica 0-1000 basada en RSSI. El controlador la muestra
|
||||
* como barra de calidad de señal del cliente.
|
||||
* CCQ = clamp((signal + 90) * 25, 0, 1000)
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <json-c/json.h>
|
||||
|
||||
#define MAX_STA 128
|
||||
|
||||
typedef struct {
|
||||
char mac[32];
|
||||
char ip[64];
|
||||
char hostname[64];
|
||||
int signal; /* RSSI dBm (negativo) */
|
||||
int noise; /* dBm */
|
||||
int rssi; /* SNR ≈ signal - noise */
|
||||
long tx_rate; /* kbps */
|
||||
long rx_rate;
|
||||
long long tx_bytes;
|
||||
long long rx_bytes;
|
||||
long long tx_packets;
|
||||
long long rx_packets;
|
||||
int uptime; /* segundos conectado */
|
||||
char radio[8]; /* "ng" / "na" / "6g" */
|
||||
int channel;
|
||||
char vap_name[32];
|
||||
bool is_11r;
|
||||
int ccq;
|
||||
bool is_wired;
|
||||
} sta_info_t;
|
||||
|
||||
/* Lee clientes WiFi de una interfaz. Devuelve nº de clientes. */
|
||||
int clients_read_wifi(const char *wlan_iface,
|
||||
const char *radio_band,
|
||||
int channel,
|
||||
sta_info_t *out,
|
||||
int max_out);
|
||||
|
||||
/* Construye JSON array sta_table para un VAP.
|
||||
* El caller debe liberar con json_object_put(). */
|
||||
struct json_object *clients_build_sta_table(const char *wlan_iface,
|
||||
const char *radio_band,
|
||||
int channel,
|
||||
const char *vap_name);
|
||||
|
||||
/* Busca IP en /proc/net/arp dado un MAC. */
|
||||
int clients_mac_to_ip(const char *mac, char *ip_out, size_t sz);
|
||||
|
||||
/* Busca hostname en /tmp/dhcp.leases dado un MAC. */
|
||||
int clients_mac_to_hostname(const char *mac, char *out, size_t sz);
|
||||
|
||||
#endif /* OPENUF_CLIENTS_H */
|
||||
@@ -0,0 +1,39 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "config.h"
|
||||
|
||||
void config_load(openuf_config_t *cfg)
|
||||
{
|
||||
/* Defaults */
|
||||
strncpy(cfg->controller_ip, DEFAULT_CONTROLLER_IP, sizeof(cfg->controller_ip) - 1);
|
||||
strncpy(cfg->lan_if, DEFAULT_LAN_IF, sizeof(cfg->lan_if) - 1);
|
||||
strncpy(cfg->ufmodel, DEFAULT_UFMODEL, sizeof(cfg->ufmodel) - 1);
|
||||
cfg->inform_interval = DEFAULT_INFORM_INTERVAL;
|
||||
cfg->enable_announce = 1;
|
||||
cfg->enable_inform = 1;
|
||||
cfg->enable_logging = 1;
|
||||
|
||||
FILE *f = fopen(OPENUF_CONF_FILE, "r");
|
||||
if (!f) return;
|
||||
|
||||
char line[256];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
/* Strip newline */
|
||||
line[strcspn(line, "\r\n")] = '\0';
|
||||
/* Skip comments / empty */
|
||||
if (line[0] == '#' || line[0] == '\0') continue;
|
||||
|
||||
char key[64] = {0}, val[192] = {0};
|
||||
if (sscanf(line, " %63[^= ] = %191s", key, val) != 2) continue;
|
||||
|
||||
if (!strcmp(key, "controller_ip")) strncpy(cfg->controller_ip, val, sizeof(cfg->controller_ip) - 1);
|
||||
else if (!strcmp(key, "lan_if")) strncpy(cfg->lan_if, val, sizeof(cfg->lan_if) - 1);
|
||||
else if (!strcmp(key, "ufmodel")) strncpy(cfg->ufmodel, val, sizeof(cfg->ufmodel) - 1);
|
||||
else if (!strcmp(key, "inform_interval")) cfg->inform_interval = atoi(val);
|
||||
else if (!strcmp(key, "enable_announce")) cfg->enable_announce = atoi(val);
|
||||
else if (!strcmp(key, "enable_inform")) cfg->enable_inform = atoi(val);
|
||||
else if (!strcmp(key, "enable_logging")) cfg->enable_logging = atoi(val);
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#ifndef OPENUF_CONFIG_H
|
||||
#define OPENUF_CONFIG_H
|
||||
|
||||
/* ─── Build-time defaults (override with /etc/openuf/openuf.conf) ─── */
|
||||
#define OPENUF_VERSION "0.3-C"
|
||||
#define OPENUF_STATE_FILE "/etc/openuf/state.json"
|
||||
#define OPENUF_CONF_FILE "/etc/openuf/openuf.conf"
|
||||
|
||||
#ifndef ENABLE_LOGGING
|
||||
#define ENABLE_LOGGING 1
|
||||
#endif
|
||||
#define DEFAULT_CONTROLLER_IP "10.10.10.1"
|
||||
#define DEFAULT_LAN_IF "br-lan"
|
||||
#define DEFAULT_UFMODEL "uapg2-ac-lr"
|
||||
#define DEFAULT_INFORM_INTERVAL 10
|
||||
#define ANNOUNCE_INTERVAL 10
|
||||
#define ANNOUNCE_PORT 10001
|
||||
#define INFORM_PORT 8080
|
||||
#define INFORM_PATH "/inform"
|
||||
#define DEFAULT_AUTH_KEY "ba86f2bbe107c7c57eb5f2690775c712"
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
#include <stdio.h>
|
||||
extern FILE *log_fp;
|
||||
#define LOG(fmt, ...) do { if (log_fp) { fprintf(log_fp, "[%s] " fmt "\n", __func__, ##__VA_ARGS__); fflush(log_fp); } } while(0)
|
||||
#else
|
||||
#define LOG(fmt, ...) do {} while(0)
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
char controller_ip[64];
|
||||
char lan_if[32];
|
||||
char ufmodel[32]; /* "u6-inwall" | "u6-lite" */
|
||||
int inform_interval;
|
||||
int enable_announce;
|
||||
int enable_inform;
|
||||
int enable_logging;
|
||||
} openuf_config_t;
|
||||
|
||||
/* Parse /etc/openuf/openuf.conf (simple key=value).
|
||||
* Fills *cfg with defaults first, then overrides from file. */
|
||||
void config_load(openuf_config_t *cfg);
|
||||
|
||||
#endif /* OPENUF_CONFIG_H */
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <mbedtls/aes.h>
|
||||
#include <mbedtls/gcm.h>
|
||||
#include <mbedtls/entropy.h>
|
||||
#include <mbedtls/ctr_drbg.h>
|
||||
|
||||
#include "crypto.h"
|
||||
|
||||
/* ─── Hex / binary helpers ──────────────────────────────────────── */
|
||||
void crypto_hex2bin(const char *hex, unsigned char *bin, size_t bin_len)
|
||||
{
|
||||
for (size_t i = 0; i < bin_len; i++) {
|
||||
unsigned int b = 0;
|
||||
sscanf(hex + i * 2, "%02x", &b);
|
||||
bin[i] = (unsigned char)b;
|
||||
}
|
||||
}
|
||||
|
||||
void crypto_bin2hex(const unsigned char *bin, size_t bin_len, char *hex_out)
|
||||
{
|
||||
for (size_t i = 0; i < bin_len; i++)
|
||||
sprintf(hex_out + i * 2, "%02x", bin[i]);
|
||||
hex_out[bin_len * 2] = '\0';
|
||||
}
|
||||
|
||||
/* ─── Random hex ────────────────────────────────────────────────── */
|
||||
int crypto_random_hex(unsigned char *hex_out, int nbytes)
|
||||
{
|
||||
mbedtls_entropy_context entropy;
|
||||
mbedtls_ctr_drbg_context ctr_drbg;
|
||||
unsigned char buf[64];
|
||||
int ret;
|
||||
|
||||
if (nbytes > 64) return -1;
|
||||
|
||||
mbedtls_entropy_init(&entropy);
|
||||
mbedtls_ctr_drbg_init(&ctr_drbg);
|
||||
|
||||
ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy,
|
||||
(const unsigned char *)"openuf", 6);
|
||||
if (ret != 0) goto out;
|
||||
|
||||
ret = mbedtls_ctr_drbg_random(&ctr_drbg, buf, nbytes);
|
||||
if (ret != 0) goto out;
|
||||
|
||||
crypto_bin2hex(buf, nbytes, (char *)hex_out);
|
||||
ret = 0;
|
||||
out:
|
||||
mbedtls_ctr_drbg_free(&ctr_drbg);
|
||||
mbedtls_entropy_free(&entropy);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* ─── AES-128-CBC encrypt (PKCS#7 padding) ──────────────────────── */
|
||||
int crypto_encrypt(const char *key_hex, const char *iv_hex,
|
||||
const unsigned char *in, size_t in_len,
|
||||
unsigned char *out)
|
||||
{
|
||||
unsigned char key[16], iv[16];
|
||||
crypto_hex2bin(key_hex, key, 16);
|
||||
crypto_hex2bin(iv_hex, iv, 16);
|
||||
|
||||
/* PKCS#7: pad to next 16-byte block */
|
||||
size_t pad = 16 - (in_len % 16);
|
||||
size_t padded = in_len + pad;
|
||||
|
||||
unsigned char *tmp = malloc(padded);
|
||||
if (!tmp) return -1;
|
||||
memcpy(tmp, in, in_len);
|
||||
memset(tmp + in_len, (unsigned char)pad, pad);
|
||||
|
||||
mbedtls_aes_context ctx;
|
||||
mbedtls_aes_init(&ctx);
|
||||
if (mbedtls_aes_setkey_enc(&ctx, key, 128) != 0) {
|
||||
mbedtls_aes_free(&ctx); free(tmp); return -1;
|
||||
}
|
||||
|
||||
/* iv is modified in place by CBC – use a copy */
|
||||
unsigned char iv_copy[16];
|
||||
memcpy(iv_copy, iv, 16);
|
||||
|
||||
int ret = mbedtls_aes_crypt_cbc(&ctx, MBEDTLS_AES_ENCRYPT,
|
||||
padded, iv_copy, tmp, out);
|
||||
mbedtls_aes_free(&ctx);
|
||||
free(tmp);
|
||||
return (ret == 0) ? (int)padded : -1;
|
||||
}
|
||||
|
||||
/* ─── AES-128-CBC decrypt (PKCS#7 unpadding) ────────────────────── */
|
||||
int crypto_decrypt(const char *key_hex, const char *iv_hex,
|
||||
const unsigned char *in, size_t in_len,
|
||||
unsigned char *out)
|
||||
{
|
||||
if (in_len == 0 || in_len % 16 != 0) return -1;
|
||||
|
||||
unsigned char key[16], iv[16];
|
||||
crypto_hex2bin(key_hex, key, 16);
|
||||
crypto_hex2bin(iv_hex, iv, 16);
|
||||
|
||||
unsigned char iv_copy[16];
|
||||
memcpy(iv_copy, iv, 16);
|
||||
|
||||
mbedtls_aes_context ctx;
|
||||
mbedtls_aes_init(&ctx);
|
||||
if (mbedtls_aes_setkey_dec(&ctx, key, 128) != 0) {
|
||||
mbedtls_aes_free(&ctx); return -1;
|
||||
}
|
||||
|
||||
int ret = mbedtls_aes_crypt_cbc(&ctx, MBEDTLS_AES_DECRYPT,
|
||||
in_len, iv_copy, in, out);
|
||||
mbedtls_aes_free(&ctx);
|
||||
if (ret != 0) return -1;
|
||||
|
||||
/* Remove PKCS#7 padding */
|
||||
unsigned char pad = out[in_len - 1];
|
||||
if (pad == 0 || pad > 16) return -1;
|
||||
return (int)(in_len - pad);
|
||||
}
|
||||
|
||||
int crypto_gcm_encrypt(const char *key_hex, const char *iv_hex,
|
||||
const unsigned char *aad, size_t aad_len,
|
||||
const unsigned char *in, size_t in_len,
|
||||
unsigned char *out, unsigned char tag[16])
|
||||
{
|
||||
unsigned char key[16], iv[16];
|
||||
crypto_hex2bin(key_hex, key, sizeof(key));
|
||||
crypto_hex2bin(iv_hex, iv, sizeof(iv));
|
||||
|
||||
mbedtls_gcm_context ctx;
|
||||
mbedtls_gcm_init(&ctx);
|
||||
int ret = mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES,
|
||||
key, 128);
|
||||
if (ret == 0)
|
||||
ret = mbedtls_gcm_crypt_and_tag(&ctx, MBEDTLS_GCM_ENCRYPT,
|
||||
in_len, iv, sizeof(iv),
|
||||
aad, aad_len, in, out, 16, tag);
|
||||
mbedtls_gcm_free(&ctx);
|
||||
return ret == 0 ? (int)in_len : -1;
|
||||
}
|
||||
|
||||
int crypto_gcm_decrypt(const char *key_hex, const char *iv_hex,
|
||||
const unsigned char *aad, size_t aad_len,
|
||||
const unsigned char *in, size_t in_len,
|
||||
const unsigned char tag[16], unsigned char *out)
|
||||
{
|
||||
unsigned char key[16], iv[16];
|
||||
crypto_hex2bin(key_hex, key, sizeof(key));
|
||||
crypto_hex2bin(iv_hex, iv, sizeof(iv));
|
||||
|
||||
mbedtls_gcm_context ctx;
|
||||
mbedtls_gcm_init(&ctx);
|
||||
int ret = mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES,
|
||||
key, 128);
|
||||
if (ret == 0)
|
||||
ret = mbedtls_gcm_auth_decrypt(&ctx, in_len, iv, sizeof(iv),
|
||||
aad, aad_len, tag, 16, in, out);
|
||||
mbedtls_gcm_free(&ctx);
|
||||
return ret == 0 ? (int)in_len : -1;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#ifndef OPENUF_CRYPTO_H
|
||||
#define OPENUF_CRYPTO_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/*
|
||||
* AES-128-CBC helpers using mbedTLS.
|
||||
*
|
||||
* All keys and IVs are passed as 32-char hex strings (16 bytes).
|
||||
* All in/out buffers are raw binary.
|
||||
*/
|
||||
|
||||
/* Generate random bytes, return as hex string.
|
||||
* hex_out must be at least nbytes*2+1 bytes. */
|
||||
int crypto_random_hex(unsigned char *hex_out, int nbytes);
|
||||
|
||||
/* AES-128-CBC encrypt.
|
||||
* out must be >= in_len + 16 (PKCS#7 padded to block boundary).
|
||||
* Returns ciphertext length, or -1 on error. */
|
||||
int crypto_encrypt(const char *key_hex, const char *iv_hex,
|
||||
const unsigned char *in, size_t in_len,
|
||||
unsigned char *out);
|
||||
|
||||
/* AES-128-CBC decrypt.
|
||||
* out must be >= in_len bytes.
|
||||
* Returns plaintext length (PKCS#7 unpadded), or -1 on error. */
|
||||
int crypto_decrypt(const char *key_hex, const char *iv_hex,
|
||||
const unsigned char *in, size_t in_len,
|
||||
unsigned char *out);
|
||||
|
||||
/* AES-128-GCM with a 16-byte authentication tag. */
|
||||
int crypto_gcm_encrypt(const char *key_hex, const char *iv_hex,
|
||||
const unsigned char *aad, size_t aad_len,
|
||||
const unsigned char *in, size_t in_len,
|
||||
unsigned char *out, unsigned char tag[16]);
|
||||
int crypto_gcm_decrypt(const char *key_hex, const char *iv_hex,
|
||||
const unsigned char *aad, size_t aad_len,
|
||||
const unsigned char *in, size_t in_len,
|
||||
const unsigned char tag[16], unsigned char *out);
|
||||
|
||||
/* Hex <-> binary conversions */
|
||||
void crypto_hex2bin(const char *hex, unsigned char *bin, size_t bin_len);
|
||||
void crypto_bin2hex(const unsigned char *bin, size_t bin_len, char *hex_out);
|
||||
|
||||
#endif /* OPENUF_CRYPTO_H */
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* openuf - http.c
|
||||
*
|
||||
* Tiny HTTP/1.0 POST over raw TCP. Avoids libcurl dependency.
|
||||
* Handles chunked responses by reading until connection close.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <netdb.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <netinet/in.h>
|
||||
|
||||
#include "http.h"
|
||||
|
||||
#define RECV_CHUNK 4096
|
||||
|
||||
/* Parse "http://host:port/path" into components */
|
||||
static int parse_url(const char *url,
|
||||
char *host, size_t host_sz,
|
||||
int *port,
|
||||
char *path, size_t path_sz)
|
||||
{
|
||||
*port = 80;
|
||||
|
||||
/* skip "http://" */
|
||||
const char *p = url;
|
||||
if (strncmp(p, "http://", 7) == 0) p += 7;
|
||||
else if (strncmp(p, "https://", 8) == 0) {
|
||||
p += 8; *port = 443;
|
||||
}
|
||||
|
||||
/* find end of host[:port] section */
|
||||
const char *slash = strchr(p, '/');
|
||||
size_t hp_len = slash ? (size_t)(slash - p) : strlen(p);
|
||||
|
||||
/* split host and port */
|
||||
const char *colon = memchr(p, ':', hp_len);
|
||||
if (colon) {
|
||||
size_t hlen = (size_t)(colon - p);
|
||||
if (hlen >= host_sz) return -1;
|
||||
memcpy(host, p, hlen);
|
||||
host[hlen] = '\0';
|
||||
*port = atoi(colon + 1);
|
||||
} else {
|
||||
if (hp_len >= host_sz) return -1;
|
||||
memcpy(host, p, hp_len);
|
||||
host[hp_len] = '\0';
|
||||
}
|
||||
|
||||
/* path */
|
||||
if (slash)
|
||||
snprintf(path, path_sz, "%s", slash);
|
||||
else
|
||||
snprintf(path, path_sz, "/");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int http_post(const char *url,
|
||||
const char *content_type,
|
||||
const unsigned char *body, size_t body_len,
|
||||
unsigned char **resp_out, size_t *resp_len)
|
||||
{
|
||||
char host[128], path[256];
|
||||
int port;
|
||||
|
||||
*resp_out = NULL;
|
||||
*resp_len = 0;
|
||||
|
||||
if (parse_url(url, host, sizeof(host), &port, path, sizeof(path)) != 0)
|
||||
return -1;
|
||||
|
||||
/* Resolve host */
|
||||
struct hostent *he = gethostbyname(host);
|
||||
if (!he) return -1;
|
||||
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
|
||||
/* 10-second connect timeout */
|
||||
struct timeval tv = { .tv_sec = 10, .tv_usec = 0 };
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
struct sockaddr_in sa = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_port = htons((uint16_t)port),
|
||||
};
|
||||
memcpy(&sa.sin_addr, he->h_addr_list[0], he->h_length);
|
||||
|
||||
if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) != 0) {
|
||||
close(fd); return -1;
|
||||
}
|
||||
|
||||
/* Build request */
|
||||
char hdr[512];
|
||||
int hdr_len = snprintf(hdr, sizeof(hdr),
|
||||
"POST %s HTTP/1.0\r\n"
|
||||
"Host: %s:%d\r\n"
|
||||
"Content-Type: %s\r\n"
|
||||
"Content-Length: %zu\r\n"
|
||||
"User-Agent: AirControl Agent v1.0\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n",
|
||||
path, host, port, content_type, body_len);
|
||||
|
||||
if (write(fd, hdr, hdr_len) != hdr_len ||
|
||||
write(fd, body, body_len) != (ssize_t)body_len) {
|
||||
close(fd); return -1;
|
||||
}
|
||||
|
||||
/* Read full response */
|
||||
size_t total = 0, cap = RECV_CHUNK;
|
||||
unsigned char *buf = malloc(cap);
|
||||
if (!buf) { close(fd); return -1; }
|
||||
|
||||
ssize_t n;
|
||||
while ((n = read(fd, buf + total, cap - total)) > 0) {
|
||||
total += n;
|
||||
if (total >= cap) {
|
||||
cap *= 2;
|
||||
unsigned char *nb = realloc(buf, cap);
|
||||
if (!nb) { free(buf); close(fd); return -1; }
|
||||
buf = nb;
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
|
||||
if (total < 12) { free(buf); return -1; }
|
||||
|
||||
/* Parse HTTP status line */
|
||||
int status = 0;
|
||||
sscanf((char *)buf, "HTTP/%*s %d", &status);
|
||||
|
||||
/* Find body (after \r\n\r\n) */
|
||||
unsigned char *body_start = (unsigned char *)memmem(buf, total,
|
||||
"\r\n\r\n", 4);
|
||||
if (!body_start) { free(buf); return status; }
|
||||
body_start += 4;
|
||||
|
||||
size_t body_sz = total - (size_t)(body_start - buf);
|
||||
*resp_out = malloc(body_sz + 1);
|
||||
if (*resp_out) {
|
||||
memcpy(*resp_out, body_start, body_sz);
|
||||
(*resp_out)[body_sz] = '\0';
|
||||
*resp_len = body_sz;
|
||||
}
|
||||
free(buf);
|
||||
return status;
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
#ifndef OPENUF_HTTP_H
|
||||
#define OPENUF_HTTP_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/*
|
||||
* Minimal HTTP/1.0 POST client (raw TCP sockets, no libcurl).
|
||||
*
|
||||
* Posts 'body' of 'body_len' bytes to the given URL.
|
||||
* Allocates *resp_out (caller must free) and sets *resp_len.
|
||||
* Returns HTTP status code (200, etc.) or -1 on error.
|
||||
*/
|
||||
int http_post(const char *url,
|
||||
const char *content_type,
|
||||
const unsigned char *body, size_t body_len,
|
||||
unsigned char **resp_out, size_t *resp_len);
|
||||
|
||||
#endif /* OPENUF_HTTP_H */
|
||||
+1039
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
#ifndef OPENUF_INFORM_H
|
||||
#define OPENUF_INFORM_H
|
||||
|
||||
#include "state.h"
|
||||
#include "ufmodel.h"
|
||||
|
||||
/*
|
||||
* UniFi Inform protocol constants
|
||||
*
|
||||
* Binary packet layout (big-endian):
|
||||
* [4] Magic "TNBU"
|
||||
* [4] Packet version = 0
|
||||
* [6] Device MAC
|
||||
* [2] Flags (0x0001 = encrypted)
|
||||
* [16] AES-CBC IV
|
||||
* [4] Data version = 1
|
||||
* [4] Payload length
|
||||
* [N] AES-128-CBC encrypted JSON payload
|
||||
*/
|
||||
|
||||
#define INFORM_MAGIC "TNBU"
|
||||
#define INFORM_PKT_VERSION 0
|
||||
#define INFORM_DATA_VERSION 1
|
||||
#define INFORM_FLAG_ENCRYPTED 0x0001
|
||||
#define INFORM_FLAG_GCM 0x0008
|
||||
|
||||
/* Send one inform cycle.
|
||||
* Updates *st in place (adopted flag, auth key, inform_url, cfgversion).
|
||||
* Returns 0 on success, -1 on error (sets err_out[0..127]). */
|
||||
int inform_send(openuf_state_t *st,
|
||||
const uf_model_t *model,
|
||||
long uptime,
|
||||
char *err_out);
|
||||
|
||||
#endif /* OPENUF_INFORM_H */
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
* openuf - lldp.c
|
||||
*
|
||||
* LLDP completo: envío de frames propios + lectura de vecinos.
|
||||
*
|
||||
* ── Construcción del frame ────────────────────────────────────────
|
||||
*
|
||||
* Los TLVs LLDP tienen cabecera de 2 bytes:
|
||||
* bit 15..9 → tipo (7 bits)
|
||||
* bit 8..0 → longitud (9 bits, max 511 bytes)
|
||||
*
|
||||
* uint16_t header_be = (type << 9) | (len & 0x1ff)
|
||||
*
|
||||
* Ejemplo: Chassis ID TLV (type=1), 7 bytes de valor:
|
||||
* header = (1 << 9) | 7 = 0x0207
|
||||
* → bytes: 0x02 0x07 [subtype=4] [MAC 6 bytes]
|
||||
*
|
||||
* ── Envío con AF_PACKET ───────────────────────────────────────────
|
||||
*
|
||||
* 1. socket(AF_PACKET, SOCK_RAW, htons(0x88cc))
|
||||
* 2. ioctl(SIOCGIFINDEX) → ifindex
|
||||
* 3. Construir frame completo en buffer
|
||||
* 4. sendto() con sockaddr_ll
|
||||
*
|
||||
* Sin CAP_NET_RAW (no root) → socket() devuelve EPERM.
|
||||
* Lo ignoramos silenciosamente (LLDP es opcional).
|
||||
*
|
||||
* ── Lectura de vecinos con lldpctl ───────────────────────────────
|
||||
*
|
||||
* lldpctl -f json retorna:
|
||||
* {
|
||||
* "lldp": {
|
||||
* "interface": [
|
||||
* {
|
||||
* "name": "eth0",
|
||||
* "chassis": {
|
||||
* "id": {"type":"mac", "value":"aa:bb:..."},
|
||||
* "name": {"value":"switch1"},
|
||||
* "descr":{"value":"Cisco Catalyst 2960"}
|
||||
* },
|
||||
* "port": {
|
||||
* "id": {"type":"ifname", "value":"Gi1/0/3"},
|
||||
* "descr":{"value":"to-AP"}
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <errno.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <net/if.h>
|
||||
#include <netinet/in.h>
|
||||
#include <linux/if_ether.h>
|
||||
#include <linux/if_packet.h>
|
||||
#include <json-c/json.h>
|
||||
|
||||
#include "lldp.h"
|
||||
|
||||
/* ─── Constantes ────────────────────────────────────────────────── */
|
||||
static const uint8_t LLDP_DST[6] = {0x01,0x80,0xc2,0x00,0x00,0x0e};
|
||||
#define LLDP_ETHERTYPE 0x88cc
|
||||
#define CAP_WLAN_AP 0x0040
|
||||
|
||||
/* ─── Escribir TLV en buffer ────────────────────────────────────── */
|
||||
static int tlv_write(uint8_t *buf, int pos, int maxlen,
|
||||
int type, const uint8_t *val, int vlen)
|
||||
{
|
||||
if (pos + 2 + vlen > maxlen) return pos;
|
||||
uint16_t hdr = (uint16_t)((type << 9) | (vlen & 0x1ff));
|
||||
buf[pos++] = (hdr >> 8) & 0xff;
|
||||
buf[pos++] = hdr & 0xff;
|
||||
if (val && vlen > 0) { memcpy(buf+pos, val, vlen); pos += vlen; }
|
||||
return pos;
|
||||
}
|
||||
|
||||
static int tlv_str(uint8_t *buf, int pos, int maxlen,
|
||||
int type, const char *str)
|
||||
{
|
||||
return tlv_write(buf, pos, maxlen, type,
|
||||
(const uint8_t*)str, (int)strlen(str));
|
||||
}
|
||||
|
||||
/* ─── Parsear MAC "aa:bb:cc:dd:ee:ff" → bytes ──────────────────── */
|
||||
static void parse_mac(const char *s, uint8_t out[6])
|
||||
{
|
||||
unsigned int b[6]={0};
|
||||
sscanf(s,"%x:%x:%x:%x:%x:%x",&b[0],&b[1],&b[2],&b[3],&b[4],&b[5]);
|
||||
for(int i=0;i<6;i++) out[i]=(uint8_t)b[i];
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
lldp_send_frame
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
int lldp_send_frame(const char *ifname,
|
||||
const char *mac_str,
|
||||
const char *hostname,
|
||||
const char *model_desc,
|
||||
int ttl)
|
||||
{
|
||||
/* Socket raw — requiere root */
|
||||
int fd = socket(AF_PACKET, SOCK_RAW, htons(LLDP_ETHERTYPE));
|
||||
if (fd < 0) return -1; /* EPERM sin root → silencioso */
|
||||
|
||||
struct ifreq ifr;
|
||||
memset(&ifr, 0, sizeof(ifr));
|
||||
strncpy(ifr.ifr_name, ifname, IFNAMSIZ-1);
|
||||
if (ioctl(fd, SIOCGIFINDEX, &ifr) < 0) { close(fd); return -1; }
|
||||
int ifindex = ifr.ifr_ifindex;
|
||||
|
||||
uint8_t src[6];
|
||||
parse_mac(mac_str, src);
|
||||
|
||||
uint8_t frame[1518];
|
||||
int pos = 0;
|
||||
|
||||
/* Ethernet header */
|
||||
memcpy(frame, LLDP_DST, 6); pos += 6; /* dst */
|
||||
memcpy(frame+6, src, 6); pos += 6; /* src */
|
||||
frame[pos++] = 0x88;
|
||||
frame[pos++] = 0xcc; /* EtherType 0x88cc */
|
||||
|
||||
/* TLV: Chassis ID (type=1): subtype=4(MAC) + MAC */
|
||||
{
|
||||
uint8_t v[7]; v[0]=4; memcpy(v+1,src,6);
|
||||
pos = tlv_write(frame, pos, sizeof(frame), 1, v, 7);
|
||||
}
|
||||
|
||||
/* TLV: Port ID (type=2): subtype=5(ifname) + nombre */
|
||||
{
|
||||
size_t nlen = strlen(ifname);
|
||||
uint8_t v[64]; v[0]=5; memcpy(v+1,ifname,nlen);
|
||||
pos = tlv_write(frame, pos, sizeof(frame), 2, v, (int)nlen+1);
|
||||
}
|
||||
|
||||
/* TLV: TTL (type=3): uint16 BE */
|
||||
{
|
||||
uint8_t v[2] = {(uint8_t)(ttl>>8),(uint8_t)(ttl&0xff)};
|
||||
pos = tlv_write(frame, pos, sizeof(frame), 3, v, 2);
|
||||
}
|
||||
|
||||
/* TLV: System Name (type=5) */
|
||||
if (hostname && hostname[0])
|
||||
pos = tlv_str(frame, pos, sizeof(frame), 5, hostname);
|
||||
|
||||
/* TLV: System Description (type=6) */
|
||||
if (model_desc && model_desc[0])
|
||||
pos = tlv_str(frame, pos, sizeof(frame), 6, model_desc);
|
||||
|
||||
/* TLV: System Capabilities (type=7): caps + enabled (WLAN AP) */
|
||||
{
|
||||
uint8_t v[4] = {
|
||||
0x00, (uint8_t)(CAP_WLAN_AP >> 8),
|
||||
0x00, (uint8_t)(CAP_WLAN_AP & 0xff)
|
||||
};
|
||||
/* Corregir: CAP_WLAN_AP = 0x0040, un solo byte basta */
|
||||
v[1] = 0x00; v[0] = 0x00;
|
||||
/* bit 6 de los 16 bits de capabilities */
|
||||
uint16_t cap = CAP_WLAN_AP;
|
||||
v[0] = (cap >> 8) & 0xff; v[1] = cap & 0xff;
|
||||
v[2] = v[0]; v[3] = v[1]; /* enabled = same */
|
||||
pos = tlv_write(frame, pos, sizeof(frame), 7, v, 4);
|
||||
}
|
||||
|
||||
/* TLV: End (type=0, len=0) */
|
||||
pos = tlv_write(frame, pos, sizeof(frame), 0, NULL, 0);
|
||||
|
||||
struct sockaddr_ll sa;
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.sll_family = AF_PACKET;
|
||||
sa.sll_ifindex = ifindex;
|
||||
sa.sll_halen = ETH_ALEN;
|
||||
memcpy(sa.sll_addr, LLDP_DST, 6);
|
||||
|
||||
ssize_t sent = sendto(fd, frame, pos, 0,
|
||||
(struct sockaddr*)&sa, sizeof(sa));
|
||||
close(fd);
|
||||
return (sent > 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
lldp_available
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
bool lldp_available(void)
|
||||
{
|
||||
return (access("/usr/sbin/lldpctl", X_OK) == 0 ||
|
||||
access("/usr/bin/lldpctl", X_OK) == 0);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
lldp_read_neighbors — parsea JSON de lldpctl
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
|
||||
Navega: root → "lldp" → "interface" (array) → cada vecino.
|
||||
Por cada vecino extrae: chassis.id, chassis.name, chassis.descr,
|
||||
port.id, port.descr, y el nombre de la interfaz local.
|
||||
|
||||
El resultado se incluye en lldp_table[] del payload inform.
|
||||
El controlador lo usa para dibujar las líneas de conexión en
|
||||
la topología visual (qué switch/puerto conecta a este AP).
|
||||
*/
|
||||
struct json_object *lldp_read_neighbors(void)
|
||||
{
|
||||
struct json_object *result = json_object_new_array();
|
||||
if (!lldp_available()) return result;
|
||||
|
||||
FILE *p = popen("lldpctl -f json 2>/dev/null", "r");
|
||||
if (!p) return result;
|
||||
|
||||
/* Leer toda la salida (limitado a 16KB) */
|
||||
char buf[16384] = {0};
|
||||
size_t total = 0, n;
|
||||
char tmp[1024];
|
||||
while ((n = fread(tmp, 1, sizeof(tmp), p)) > 0 &&
|
||||
total + n < sizeof(buf)-1) {
|
||||
memcpy(buf+total, tmp, n); total += n;
|
||||
}
|
||||
pclose(p);
|
||||
if (!total) return result;
|
||||
|
||||
struct json_object *root = json_tokener_parse(buf);
|
||||
if (!root) return result;
|
||||
|
||||
/* Navegar: root.lldp.interface[] */
|
||||
struct json_object *lldp_o, *iface_arr;
|
||||
if (!json_object_object_get_ex(root, "lldp", &lldp_o)) goto done;
|
||||
if (!json_object_object_get_ex(lldp_o, "interface", &iface_arr)) goto done;
|
||||
if (!json_object_is_type(iface_arr, json_type_array)) goto done;
|
||||
|
||||
int ni = json_object_array_length(iface_arr);
|
||||
for (int i = 0; i < ni; i++) {
|
||||
struct json_object *iface = json_object_array_get_idx(iface_arr, i);
|
||||
if (!iface) continue;
|
||||
|
||||
/* Puerto local */
|
||||
struct json_object *tmp_o;
|
||||
const char *local_port = "";
|
||||
if (json_object_object_get_ex(iface, "name", &tmp_o))
|
||||
local_port = json_object_get_string(tmp_o);
|
||||
|
||||
/* Chassis */
|
||||
const char *chassis_id="", *sys_name="", *sys_desc="";
|
||||
struct json_object *chassis;
|
||||
if (json_object_object_get_ex(iface, "chassis", &chassis)) {
|
||||
struct json_object *cid, *cname, *cdescr;
|
||||
if (json_object_object_get_ex(chassis, "id", &cid)) {
|
||||
struct json_object *cv;
|
||||
if (json_object_object_get_ex(cid, "value", &cv))
|
||||
chassis_id = json_object_get_string(cv);
|
||||
}
|
||||
if (json_object_object_get_ex(chassis, "name", &cname)) {
|
||||
struct json_object *cv;
|
||||
if (json_object_object_get_ex(cname, "value", &cv))
|
||||
sys_name = json_object_get_string(cv);
|
||||
}
|
||||
if (json_object_object_get_ex(chassis, "descr", &cdescr)) {
|
||||
struct json_object *cv;
|
||||
if (json_object_object_get_ex(cdescr, "value", &cv))
|
||||
sys_desc = json_object_get_string(cv);
|
||||
}
|
||||
}
|
||||
|
||||
/* Port */
|
||||
const char *port_id="", *port_desc="";
|
||||
struct json_object *port;
|
||||
if (json_object_object_get_ex(iface, "port", &port)) {
|
||||
struct json_object *pid, *pdesc;
|
||||
if (json_object_object_get_ex(port, "id", &pid)) {
|
||||
struct json_object *pv;
|
||||
if (json_object_object_get_ex(pid, "value", &pv))
|
||||
port_id = json_object_get_string(pv);
|
||||
}
|
||||
if (json_object_object_get_ex(port, "descr", &pdesc)) {
|
||||
struct json_object *pv;
|
||||
if (json_object_object_get_ex(pdesc, "value", &pv))
|
||||
port_desc = json_object_get_string(pv);
|
||||
}
|
||||
}
|
||||
|
||||
struct json_object *e = json_object_new_object();
|
||||
json_object_object_add(e, "local_port", json_object_new_string(local_port));
|
||||
json_object_object_add(e, "chassis_id", json_object_new_string(chassis_id));
|
||||
json_object_object_add(e, "port_id", json_object_new_string(port_id));
|
||||
json_object_object_add(e, "sys_name", json_object_new_string(sys_name));
|
||||
json_object_object_add(e, "sys_desc", json_object_new_string(sys_desc));
|
||||
json_object_object_add(e, "port_desc", json_object_new_string(port_desc));
|
||||
json_object_object_add(e, "port_table", json_object_new_array());
|
||||
json_object_array_add(result, e);
|
||||
}
|
||||
|
||||
done:
|
||||
json_object_put(root);
|
||||
return result;
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
#ifndef OPENUF_LLDP_H
|
||||
#define OPENUF_LLDP_H
|
||||
|
||||
/*
|
||||
* openuf - lldp.h
|
||||
*
|
||||
* LLDP (Link Layer Discovery Protocol — IEEE 802.1AB)
|
||||
*
|
||||
* ── ENVÍO de frames LLDP propios ────────────────────────────────
|
||||
*
|
||||
* El AP transmite frames LLDP por cada puerto ethernet.
|
||||
* Esto permite al switch upstream registrar al AP como vecino,
|
||||
* y al controlador UniFi construir el mapa de topología visual.
|
||||
*
|
||||
* Frame Ethernet:
|
||||
* dst = 01:80:c2:00:00:0e (multicast LLDP estándar)
|
||||
* src = MAC del AP
|
||||
* type = 0x88cc
|
||||
*
|
||||
* Payload (TLVs encadenados):
|
||||
* Header TLV = [type:7bits | len_hi:1bit][len_lo:8bits]
|
||||
*
|
||||
* TLV type=1 Chassis ID subtype=4(MAC), value=MAC[6]
|
||||
* TLV type=2 Port ID subtype=5(ifname), value="eth0"
|
||||
* TLV type=3 TTL value=uint16_BE
|
||||
* TLV type=5 System Name value=hostname
|
||||
* TLV type=6 System Desc value="modelo versión"
|
||||
* TLV type=7 Capabilities cap=0x0040(WLAN-AP), en=0x0040
|
||||
* TLV type=0 End of LLDPDU len=0
|
||||
*
|
||||
* ── LECTURA de vecinos: lldpctl -f json ─────────────────────────
|
||||
*
|
||||
* Si lldpd está instalado, leemos los vecinos detectados
|
||||
* y los incluimos en lldp_table del payload inform.
|
||||
*
|
||||
* lldp_table en el JSON inform:
|
||||
* [{
|
||||
* "local_port": "eth0",
|
||||
* "chassis_id": "aa:bb:cc:...",
|
||||
* "port_id": "Gi1/0/3",
|
||||
* "sys_name": "switch-piso1",
|
||||
* "sys_desc": "Cisco Catalyst 2960",
|
||||
* "port_desc": "to-AP"
|
||||
* }]
|
||||
*
|
||||
* ── SIN lldpd ───────────────────────────────────────────────────
|
||||
*
|
||||
* lldp_send_frame() funciona sin lldpd (usa raw socket directo).
|
||||
* lldp_read_neighbors() retorna array vacío si no hay lldpctl.
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <json-c/json.h>
|
||||
|
||||
/* Envía un frame LLDP por raw socket AF_PACKET.
|
||||
* Requiere ejecutar como root (CAP_NET_RAW).
|
||||
* Devuelve 0 si ok, -1 si error (sin root → error silencioso). */
|
||||
int lldp_send_frame(const char *ifname,
|
||||
const char *mac_str,
|
||||
const char *hostname,
|
||||
const char *model_desc,
|
||||
int ttl);
|
||||
|
||||
/* Lee vecinos LLDP de lldpctl y retorna JSON array lldp_table.
|
||||
* Si lldpctl no está, retorna array vacío (no falla).
|
||||
* Caller libera con json_object_put(). */
|
||||
struct json_object *lldp_read_neighbors(void);
|
||||
|
||||
/* true si lldpctl está instalado */
|
||||
bool lldp_available(void);
|
||||
|
||||
#endif /* OPENUF_LLDP_H */
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* openuf - main.c
|
||||
*
|
||||
* Daemon principal. Bucle con tres tareas:
|
||||
* 1. Announce – UDP broadcast+multicast cada 10s (descubrimiento L2)
|
||||
* 2. Inform – HTTP POST cifrado cada 10s (adopción + telemetría)
|
||||
* 3. LLDP – Raw frame L2 cada 30s (topología visual en UniFi)
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <net/if.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "state.h"
|
||||
#include "ufmodel.h"
|
||||
#include "announce.h"
|
||||
#include "inform.h"
|
||||
#include "lldp.h"
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
FILE *log_fp = NULL;
|
||||
#endif
|
||||
|
||||
#define LLDP_INTERVAL 30 /* seconds between LLDP frames */
|
||||
#define LLDP_TTL 120 /* LLDP record lifetime in seconds */
|
||||
|
||||
static int get_mac(const char *iface, char *out, size_t sz)
|
||||
{
|
||||
char path[128];
|
||||
snprintf(path, sizeof(path), "/sys/class/net/%s/address", iface);
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return -1;
|
||||
char buf[32] = {0};
|
||||
fgets(buf, sizeof(buf), f);
|
||||
fclose(f);
|
||||
buf[strcspn(buf, "\r\n")] = '\0';
|
||||
if (strlen(buf) < 11) return -1;
|
||||
strncpy(out, buf, sz-1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int get_ip(const char *iface, char *out, size_t sz)
|
||||
{
|
||||
int fd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
struct ifreq ifr;
|
||||
memset(&ifr, 0, sizeof(ifr));
|
||||
strncpy(ifr.ifr_name, iface, IFNAMSIZ-1);
|
||||
int ret = -1;
|
||||
if (ioctl(fd, SIOCGIFADDR, &ifr) == 0) {
|
||||
struct sockaddr_in *sa = (struct sockaddr_in *)&ifr.ifr_addr;
|
||||
strncpy(out, inet_ntoa(sa->sin_addr), sz-1);
|
||||
ret = 0;
|
||||
}
|
||||
close(fd);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
for (int i = 1; i < argc-1; i++)
|
||||
if (!strcmp(argv[i], "-c"))
|
||||
setenv("OPENUF_CONF", argv[i+1], 1);
|
||||
|
||||
openuf_config_t cfg;
|
||||
config_load(&cfg);
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
if (cfg.enable_logging) {
|
||||
log_fp = fopen("/var/log/openuf.log", "a");
|
||||
if (log_fp) {
|
||||
LOG("Logging enabled");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
const uf_model_t *model = ufmodel_find(cfg.ufmodel);
|
||||
|
||||
char mac_str[32] = "00:00:00:00:00:00";
|
||||
char ip_str[64] = "192.168.1.1";
|
||||
|
||||
if (get_mac(cfg.lan_if, mac_str, sizeof(mac_str)) != 0)
|
||||
get_mac("eth0", mac_str, sizeof(mac_str));
|
||||
if (get_ip(cfg.lan_if, ip_str, sizeof(ip_str)) != 0)
|
||||
get_ip("eth0", ip_str, sizeof(ip_str));
|
||||
|
||||
openuf_state_t state;
|
||||
state_load(&state);
|
||||
strncpy(state.mac, mac_str, sizeof(state.mac)-1);
|
||||
strncpy(state.ip, ip_str, sizeof(state.ip)-1);
|
||||
if (!state.hostname[0])
|
||||
strncpy(state.hostname, model->display_name, sizeof(state.hostname)-1);
|
||||
|
||||
/* Log initial state */
|
||||
LOG("Initial device state: adopted=%d, authkey=%.8s...", state.adopted,
|
||||
state.authkey[0] ? state.authkey : "DEFAULT");
|
||||
|
||||
if (!state.adopted || !state.inform_url[0])
|
||||
snprintf(state.inform_url, sizeof(state.inform_url),
|
||||
"http://%s:%d%s", cfg.controller_ip, INFORM_PORT, INFORM_PATH);
|
||||
state_save(&state);
|
||||
|
||||
printf("[openuf] Starting model=%-8s MAC=%s IP=%s\n",
|
||||
model->model, mac_str, ip_str);
|
||||
printf("[openuf] Controller: %s\n", state.inform_url);
|
||||
printf("[openuf] Adopted: %s\n", state.adopted ? "yes" : "no");
|
||||
printf("[openuf] LLDP available: %s\n",
|
||||
lldp_available() ? "yes (lldpd)" : "no (transmit only)");
|
||||
fflush(stdout);
|
||||
|
||||
LOG("Daemon started");
|
||||
|
||||
/* ── Announce socket ────────────────────────────────────────── */
|
||||
announce_ctx_t ann;
|
||||
if (cfg.enable_announce) {
|
||||
if (announce_init(&ann, model, mac_str, ip_str) != 0) {
|
||||
LOG("Failed to initialize announce service");
|
||||
cfg.enable_announce = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Descripción LLDP del dispositivo ───────────────────────── */
|
||||
char lldp_desc[128];
|
||||
snprintf(lldp_desc, sizeof(lldp_desc),
|
||||
"%s %s%s (openuf)",
|
||||
model->model_display, model->fw_pre, model->fw_ver);
|
||||
|
||||
/* ── Bucle principal ─────────────────────────────────────────── */
|
||||
time_t start_time = time(NULL);
|
||||
time_t last_announce = 0;
|
||||
time_t last_inform = 0;
|
||||
time_t last_lldp = 0;
|
||||
|
||||
printf("[openuf] Main loop started\n");
|
||||
fflush(stdout);
|
||||
|
||||
while (1) {
|
||||
time_t now = time(NULL);
|
||||
|
||||
/* Announce L2/UDP */
|
||||
if (cfg.enable_announce &&
|
||||
(now - last_announce) >= ANNOUNCE_INTERVAL) {
|
||||
LOG("Sending announce");
|
||||
int announce_rc = announce_send(&ann);
|
||||
LOG("Announce completed with result=%d", announce_rc);
|
||||
last_announce = now;
|
||||
}
|
||||
|
||||
/* LLDP frames por cada interfaz ethernet */
|
||||
if ((now - last_lldp) >= LLDP_INTERVAL) {
|
||||
LOG("Sending LLDP frames");
|
||||
for (int i = 0; i < model->port_table_len; i++) {
|
||||
const char *iface = model->port_table[i].ifname;
|
||||
/* Leer MAC real de la interfaz si disponible */
|
||||
char iface_mac[32];
|
||||
if (get_mac(iface, iface_mac, sizeof(iface_mac)) != 0)
|
||||
strncpy(iface_mac, mac_str, sizeof(iface_mac)-1);
|
||||
int lldp_rc = lldp_send_frame(iface, iface_mac,
|
||||
state.hostname, lldp_desc,
|
||||
LLDP_TTL);
|
||||
LOG("LLDP frame interface=%s mac=%s result=%d",
|
||||
iface, iface_mac, lldp_rc);
|
||||
}
|
||||
last_lldp = now;
|
||||
}
|
||||
|
||||
/* Inform HTTP POST */
|
||||
if (cfg.enable_inform &&
|
||||
(now - last_inform) >= cfg.inform_interval) {
|
||||
last_inform = now;
|
||||
LOG("Sending inform");
|
||||
|
||||
/* Actualizar IP en cada ciclo */
|
||||
char new_ip[64] = {0};
|
||||
if (get_ip(cfg.lan_if, new_ip, sizeof(new_ip)) == 0 ||
|
||||
get_ip("eth0", new_ip, sizeof(new_ip)) == 0)
|
||||
strncpy(state.ip, new_ip, sizeof(state.ip)-1);
|
||||
|
||||
long uptime = (long)(now - start_time);
|
||||
char err[128] = {0};
|
||||
if (inform_send(&state, model, uptime, err) != 0) {
|
||||
LOG("inform error: %s", err);
|
||||
}
|
||||
}
|
||||
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
announce_close(&ann);
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
if (log_fp) {
|
||||
LOG("Shutting down");
|
||||
fclose(log_fp);
|
||||
}
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* openuf - models.c
|
||||
*
|
||||
* Descriptores de hardware para los modelos emulados.
|
||||
*
|
||||
* ── Por qué U6 InWall como modelo principal ─────────────────────────
|
||||
*
|
||||
* Se elige U6 IW porque:
|
||||
* • 5 puertos GbE (eth0-eth4) → cubre la mayoría de routers OpenWrt
|
||||
* con 4 LAN + 1 WAN convertidos a puertos independientes
|
||||
* • WiFi 6 (802.11ax) en 2.4 GHz y 5 GHz
|
||||
* • Sin PoE uplink (el router alimenta por adaptador)
|
||||
* • Firmware string reconocido por UniFi Network 7.x+
|
||||
*
|
||||
* Los 5 puertos se mapean así en un router típico OpenWrt:
|
||||
* eth0 → Puerto 1 (WAN físico, reconfigurado como LAN)
|
||||
* eth1 → Puerto 2
|
||||
* eth2 → Puerto 3
|
||||
* eth3 → Puerto 4
|
||||
* eth4 → Puerto 5 (o CPU en SoCs sin eth4 físico)
|
||||
*/
|
||||
|
||||
#include "ufmodel.h"
|
||||
#include <string.h>
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
U6 InWall — 5 puertos GbE + WiFi 6 (2.4+5 GHz)
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
static const uf_radio_t u6iw_radios[] = {
|
||||
/* name radio ch ht min max nss pwr caps ant he */
|
||||
{ "wifi0", "ng", 6, "HT40", 5, 23, 2, 20, 4, 0, true },
|
||||
{ "wifi1", "na", 36, "HT80", 5, 23, 2, 20, 7, 0, true },
|
||||
};
|
||||
|
||||
/* 5 puertos: puerto 0 es uplink, 1-4 son LAN */
|
||||
static const uf_port_t u6iw_ports[] = {
|
||||
/* ifname name idx poe_caps media speed up uplink duplex */
|
||||
{ "eth0", "eth0", 0, 255, "GE", 1000, false, true, true },
|
||||
{ "eth1", "eth1", 1, 0, "GE", 1000, false, false, true },
|
||||
{ "eth2", "eth2", 2, 0, "GE", 1000, false, false, true },
|
||||
{ "eth3", "eth3", 3, 0, "GE", 1000, false, false, true },
|
||||
{ "eth4", "eth4", 4, 4, "GE", 1000, false, false, true },
|
||||
};
|
||||
|
||||
static const uf_eth_entry_t u6iw_eth[] = {
|
||||
{ "eth0", 5 },
|
||||
};
|
||||
|
||||
static const uf_radio_map_t u6iw_rmap[] = {
|
||||
{ "ng", "radio0" },
|
||||
{ "na", "radio1" },
|
||||
};
|
||||
|
||||
const uf_model_t model_u6inwall = {
|
||||
.model = "U6IW",
|
||||
.model_display = "U6 IW",
|
||||
.display_name = "U6-IW",
|
||||
.platform = "U6IW",
|
||||
.board_rev = 3,
|
||||
.has_eth1 = true,
|
||||
.fw_pre = "U6IW.mt7622_5_4.v",
|
||||
.fw_ver = "6.6.55.14430",
|
||||
.fw_buildtime = "230901.1200",
|
||||
.fw_factoryver = "6.6.55.14430",
|
||||
.radio_table = u6iw_radios,
|
||||
.radio_table_len = 2,
|
||||
.port_table = u6iw_ports,
|
||||
.port_table_len = 5,
|
||||
.ethernet_table = u6iw_eth,
|
||||
.ethernet_table_len = 1,
|
||||
.radio_map = u6iw_rmap,
|
||||
.radio_map_len = 2,
|
||||
};
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
U6 Lite — 1 puerto GbE + WiFi 6 (2.4+5 GHz)
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
static const uf_radio_t u6lite_radios[] = {
|
||||
{ "wifi0", "ng", 6, "HT40", 5, 23, 2, 20, 4, 0, true },
|
||||
{ "wifi1", "na", 36, "HT80", 5, 23, 2, 20, 7, 0, true },
|
||||
};
|
||||
static const uf_port_t u6lite_ports[] = {
|
||||
{ "eth0", "eth0", 0, 255, "GE", 1000, false, true, true },
|
||||
};
|
||||
static const uf_eth_entry_t u6lite_eth[] = { { "eth0", 1 } };
|
||||
static const uf_radio_map_t u6lite_rmap[] = {
|
||||
{ "ng", "radio0" }, { "na", "radio1" },
|
||||
};
|
||||
const uf_model_t model_u6lite = {
|
||||
.model="U6LITE", .model_display="U6 Lite", .display_name="U6-Lite",
|
||||
.platform="U6LITE", .board_rev=3, .has_eth1=false,
|
||||
.fw_pre="U6LITE.mt7622_5_4.v", .fw_ver="6.6.55.14430",
|
||||
.fw_buildtime="230901.1200", .fw_factoryver="6.6.55.14430",
|
||||
.radio_table=u6lite_radios, .radio_table_len=2,
|
||||
.port_table=u6lite_ports, .port_table_len=1,
|
||||
.ethernet_table=u6lite_eth, .ethernet_table_len=1,
|
||||
.radio_map=u6lite_rmap, .radio_map_len=2,
|
||||
};
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
UAP Gen 1 — 1 puerto Fast Ethernet + WiFi N 2.4 GHz
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
static const uf_radio_t uapg1_radios[] = {
|
||||
{ "wifi0", "ng", 6, "HT20", 5, 23, 2, 20, 4, 0, false },
|
||||
};
|
||||
static const uf_port_t uapg1_ports[] = {
|
||||
{ "eth0", "eth0", 0, 255, "GE", 100, false, true, true },
|
||||
};
|
||||
static const uf_eth_entry_t uapg1_eth[] = { { "eth0", 1 } };
|
||||
static const uf_radio_map_t uapg1_rmap[] = { { "ng", "radio0" } };
|
||||
const uf_model_t model_uapg1 = {
|
||||
.model="BZ2", .model_display="UAP", .display_name="UAP",
|
||||
.platform="BZ2", .board_rev=1, .has_eth1=false,
|
||||
.fw_pre="BZ2.ar7240.v", .fw_ver="6.6.55.14430",
|
||||
.fw_buildtime="230901.1200", .fw_factoryver="6.6.55.14430",
|
||||
.radio_table=uapg1_radios, .radio_table_len=1,
|
||||
.port_table=uapg1_ports, .port_table_len=1,
|
||||
.ethernet_table=uapg1_eth, .ethernet_table_len=1,
|
||||
.radio_map=uapg1_rmap, .radio_map_len=1,
|
||||
};
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
UAP Gen 1 LR
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
static const uf_radio_t uapg1lr_radios[] = {
|
||||
{ "wifi0", "ng", 6, "HT20", 5, 23, 2, 22, 4, 0, false },
|
||||
};
|
||||
static const uf_port_t uapg1lr_ports[] = {
|
||||
{ "eth0", "eth0", 0, 255, "GE", 100, false, true, true },
|
||||
};
|
||||
static const uf_eth_entry_t uapg1lr_eth[] = { { "eth0", 1 } };
|
||||
static const uf_radio_map_t uapg1lr_rmap[] = { { "ng", "radio0" } };
|
||||
const uf_model_t model_uapg1lr = {
|
||||
.model="BZ2LR", .model_display="UAP-LR", .display_name="UAP-LR",
|
||||
.platform="BZ2LR", .board_rev=1, .has_eth1=false,
|
||||
.fw_pre="BZ2LR.ar7240.v", .fw_ver="6.6.55.14430",
|
||||
.fw_buildtime="230901.1200", .fw_factoryver="6.6.55.14430",
|
||||
.radio_table=uapg1lr_radios, .radio_table_len=1,
|
||||
.port_table=uapg1lr_ports, .port_table_len=1,
|
||||
.ethernet_table=uapg1lr_eth, .ethernet_table_len=1,
|
||||
.radio_map=uapg1lr_rmap, .radio_map_len=1,
|
||||
};
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
UAP AC LR — 1 puerto GbE + WiFi AC dual-band
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
static const uf_radio_t uapg2aclr_radios[] = {
|
||||
{ "wifi0", "ng", 6, "HT40", 5, 23, 2, 20, 4, 0, false },
|
||||
{ "wifi1", "na", 36, "HT80", 5, 23, 2, 20, 7, 0, false },
|
||||
};
|
||||
static const uf_port_t uapg2aclr_ports[] = {
|
||||
{ "eth0", "eth0", 0, 255, "GE", 1000, false, true, true },
|
||||
};
|
||||
static const uf_eth_entry_t uapg2aclr_eth[] = { { "eth0", 1 } };
|
||||
static const uf_radio_map_t uapg2aclr_rmap[] = {
|
||||
{ "ng", "radio0" }, { "na", "radio1" },
|
||||
};
|
||||
const uf_model_t model_uapg2aclr = {
|
||||
.model="U2IW", .model_display="UAP-AC-LR", .display_name="UAP-AC-LR",
|
||||
.platform="U2IW", .board_rev=2, .has_eth1=false,
|
||||
.fw_pre="U2IW.qca956x.v", .fw_ver="6.7.54.15663",
|
||||
.fw_buildtime="260615.1200", .fw_factoryver="6.7.54.15663",
|
||||
.radio_table=uapg2aclr_radios, .radio_table_len=2,
|
||||
.port_table=uapg2aclr_ports, .port_table_len=1,
|
||||
.ethernet_table=uapg2aclr_eth, .ethernet_table_len=1,
|
||||
.radio_map=uapg2aclr_rmap, .radio_map_len=2,
|
||||
};
|
||||
|
||||
/* ─── Registro de modelos ─────────────────────────────────────── */
|
||||
static const uf_model_t *all_models[] = {
|
||||
&model_u6inwall,
|
||||
&model_u6lite,
|
||||
&model_uapg1,
|
||||
&model_uapg1lr,
|
||||
&model_uapg2aclr,
|
||||
NULL
|
||||
};
|
||||
|
||||
const uf_model_t *ufmodel_find(const char *name)
|
||||
{
|
||||
if (!name) return &model_u6inwall;
|
||||
for (int i = 0; all_models[i]; i++) {
|
||||
const uf_model_t *m = all_models[i];
|
||||
if (!strcasecmp(name, m->model) ||
|
||||
!strcasecmp(name, m->model_display) ||
|
||||
!strcasecmp(name, m->display_name) ||
|
||||
!strcasecmp(name, m->platform))
|
||||
return m;
|
||||
}
|
||||
/* Aliases de configuración */
|
||||
if (!strcasecmp(name, "u6-inwall") ||
|
||||
!strcasecmp(name, "u6iw")) return &model_u6inwall;
|
||||
if (!strcasecmp(name, "u6-lite")) return &model_u6lite;
|
||||
if (!strcasecmp(name, "uapg1")) return &model_uapg1;
|
||||
if (!strcasecmp(name, "uapg1-lr")) return &model_uapg1lr;
|
||||
if (!strcasecmp(name, "uapg2-ac-lr")) return &model_uapg2aclr;
|
||||
|
||||
return &model_u6inwall; /* default */
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <json-c/json.h>
|
||||
#include "state.h"
|
||||
#include "config.h"
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
#include <stdio.h>
|
||||
extern FILE *log_fp;
|
||||
#define LOG(fmt, ...) do { if (log_fp) { fprintf(log_fp, "[%s] " fmt "\n", __func__, ##__VA_ARGS__); fflush(log_fp); } } while(0)
|
||||
#else
|
||||
#define LOG(fmt, ...) do {} while(0)
|
||||
#endif
|
||||
|
||||
static void state_defaults(openuf_state_t *st)
|
||||
{
|
||||
memset(st, 0, sizeof(*st));
|
||||
st->adopted = false;
|
||||
strncpy(st->authkey, DEFAULT_AUTH_KEY, sizeof(st->authkey) - 1);
|
||||
strncpy(st->cfgversion, "0", sizeof(st->cfgversion) - 1);
|
||||
st->config_applied = false;
|
||||
st->config_schema = 0;
|
||||
st->use_aes_gcm = false;
|
||||
}
|
||||
|
||||
void state_load(openuf_state_t *st)
|
||||
{
|
||||
state_defaults(st);
|
||||
|
||||
FILE *f = fopen(OPENUF_STATE_FILE, "r");
|
||||
if (!f) {
|
||||
LOG("State file not found, using defaults");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Read whole file */
|
||||
fseek(f, 0, SEEK_END);
|
||||
long sz = ftell(f);
|
||||
rewind(f);
|
||||
if (sz <= 0 || sz > 4096) { fclose(f); return; }
|
||||
|
||||
char *buf = malloc(sz + 1);
|
||||
if (!buf) { fclose(f); return; }
|
||||
fread(buf, 1, sz, f);
|
||||
buf[sz] = '\0';
|
||||
fclose(f);
|
||||
|
||||
struct json_object *root = json_tokener_parse(buf);
|
||||
free(buf);
|
||||
if (!root) {
|
||||
LOG("Failed to parse state file");
|
||||
return;
|
||||
}
|
||||
|
||||
struct json_object *v;
|
||||
#define LOAD_STR(field, key) \
|
||||
if (json_object_object_get_ex(root, key, &v) && json_object_is_type(v, json_type_string)) \
|
||||
strncpy(st->field, json_object_get_string(v), sizeof(st->field) - 1)
|
||||
#define LOAD_BOOL(field, key) \
|
||||
if (json_object_object_get_ex(root, key, &v)) \
|
||||
st->field = json_object_get_boolean(v)
|
||||
#define LOAD_INT(field, key) \
|
||||
if (json_object_object_get_ex(root, key, &v)) \
|
||||
st->field = json_object_get_int(v)
|
||||
|
||||
LOAD_BOOL(adopted, "adopted");
|
||||
LOAD_STR (authkey, "authkey");
|
||||
LOAD_STR (inform_url, "inform_url");
|
||||
LOAD_STR (cfgversion, "cfgversion");
|
||||
LOAD_BOOL(config_applied, "config_applied");
|
||||
LOAD_INT (config_schema, "config_schema");
|
||||
LOAD_BOOL(use_aes_gcm, "use_aes_gcm");
|
||||
LOAD_STR (mac, "mac");
|
||||
LOAD_STR (ip, "ip");
|
||||
LOAD_STR (hostname, "hostname");
|
||||
|
||||
/*
|
||||
* Older versions stored cfgversion from setparam before applying the
|
||||
* corresponding setstate. Force one provisioning request when migrating.
|
||||
*/
|
||||
if (!st->config_applied ||
|
||||
st->config_schema < OPENUF_CONFIG_SCHEMA) {
|
||||
st->config_applied = false;
|
||||
strncpy(st->cfgversion, "0", sizeof(st->cfgversion) - 1);
|
||||
}
|
||||
|
||||
/* CRITICAL: If not adopted, force DEFAULT_AUTH_KEY */
|
||||
if (!st->adopted) {
|
||||
LOG("Device not adopted - resetting authkey to DEFAULT");
|
||||
strncpy(st->authkey, DEFAULT_AUTH_KEY, sizeof(st->authkey) - 1);
|
||||
}
|
||||
|
||||
LOG("State loaded: adopted=%d, authkey=%.8s..., inform_url=%s, aes_gcm=%d",
|
||||
st->adopted, st->authkey[0] ? st->authkey : "DEFAULT",
|
||||
st->inform_url, st->use_aes_gcm);
|
||||
|
||||
json_object_put(root);
|
||||
}
|
||||
|
||||
int state_save(const openuf_state_t *st)
|
||||
{
|
||||
/* Ensure directory exists */
|
||||
mkdir("/etc/openuf", 0755);
|
||||
|
||||
LOG("Saving state: adopted=%d, authkey=%.8s..., inform_url=%s, aes_gcm=%d",
|
||||
st->adopted, st->authkey[0] ? st->authkey : "DEFAULT",
|
||||
st->inform_url, st->use_aes_gcm);
|
||||
|
||||
struct json_object *root = json_object_new_object();
|
||||
json_object_object_add(root, "adopted", json_object_new_boolean(st->adopted));
|
||||
json_object_object_add(root, "authkey", json_object_new_string(st->authkey));
|
||||
json_object_object_add(root, "inform_url", json_object_new_string(st->inform_url));
|
||||
json_object_object_add(root, "cfgversion", json_object_new_string(st->cfgversion));
|
||||
json_object_object_add(root, "config_applied", json_object_new_boolean(st->config_applied));
|
||||
json_object_object_add(root, "config_schema", json_object_new_int(st->config_schema));
|
||||
json_object_object_add(root, "use_aes_gcm", json_object_new_boolean(st->use_aes_gcm));
|
||||
json_object_object_add(root, "mac", json_object_new_string(st->mac));
|
||||
json_object_object_add(root, "ip", json_object_new_string(st->ip));
|
||||
json_object_object_add(root, "hostname", json_object_new_string(st->hostname));
|
||||
|
||||
const char *s = json_object_to_json_string_ext(root, JSON_C_TO_STRING_PRETTY);
|
||||
|
||||
FILE *f = fopen(OPENUF_STATE_FILE, "w");
|
||||
if (!f) {
|
||||
LOG("Failed to open state file for writing");
|
||||
json_object_put(root);
|
||||
return -1;
|
||||
}
|
||||
fputs(s, f);
|
||||
fclose(f);
|
||||
json_object_put(root);
|
||||
LOG("State saved successfully");
|
||||
return 0;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
#ifndef OPENUF_STATE_H
|
||||
#define OPENUF_STATE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#define OPENUF_CONFIG_SCHEMA 3
|
||||
|
||||
typedef struct {
|
||||
bool adopted;
|
||||
char authkey[64];
|
||||
char inform_url[256];
|
||||
char cfgversion[32];
|
||||
bool config_applied;
|
||||
int config_schema;
|
||||
bool use_aes_gcm;
|
||||
char mac[32];
|
||||
char ip[64];
|
||||
char hostname[64];
|
||||
} openuf_state_t;
|
||||
|
||||
/* Load state from OPENUF_STATE_FILE. Fills defaults if file missing. */
|
||||
void state_load(openuf_state_t *st);
|
||||
|
||||
/* Persist state to OPENUF_STATE_FILE (creates /etc/openuf/ if needed). */
|
||||
int state_save(const openuf_state_t *st);
|
||||
|
||||
#endif /* OPENUF_STATE_H */
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
* openuf - sysinfo.c
|
||||
*
|
||||
* Lee estadísticas del sistema para el payload inform.
|
||||
*
|
||||
* ── CPU: /proc/stat ──────────────────────────────────────────────────
|
||||
*
|
||||
* Formato: cpu user nice system idle iowait irq softirq steal
|
||||
*
|
||||
* El uso se calcula con dos snapshots separados en el tiempo:
|
||||
* activo = user + nice + system + irq + softirq + steal
|
||||
* total = activo + idle + iowait
|
||||
* uso % = (Δactivo / Δtotal) × 100
|
||||
*
|
||||
* ── Memoria: /proc/meminfo ───────────────────────────────────────────
|
||||
*
|
||||
* MemTotal, MemFree, Buffers, Cached
|
||||
* used = total - free - buffers - cached
|
||||
*
|
||||
* ── Interfaces: /proc/net/dev + /sys/class/net/<iface>/ ─────────────
|
||||
*
|
||||
* /proc/net/dev → contadores acumulados rx/tx
|
||||
* /sys/class/net/speed → velocidad negociada (Mbps)
|
||||
* /sys/class/net/duplex → "full" / "half"
|
||||
* /sys/class/net/operstate → "up" / "down" / "unknown"
|
||||
* /sys/class/net/address → MAC
|
||||
* ioctl SIOCGIFADDR → IP
|
||||
*
|
||||
* ── Radio: iw dev <iface> info + survey dump ─────────────────────────
|
||||
*
|
||||
* info: canal actual, potencia TX
|
||||
* survey dump: active/busy/tx/rx time → calcular % utilización
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <stdbool.h>
|
||||
#include <net/if.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
|
||||
#include "sysinfo.h"
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
Memoria
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
int sysinfo_mem(mem_stats_t *out)
|
||||
{
|
||||
memset(out, 0, sizeof(*out));
|
||||
FILE *f = fopen("/proc/meminfo", "r");
|
||||
if (!f) return -1;
|
||||
|
||||
char line[128];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
long val = 0;
|
||||
if (sscanf(line, "MemTotal: %ld kB", &val) == 1) out->total_kb = val;
|
||||
else if (sscanf(line, "MemFree: %ld kB", &val) == 1) out->free_kb = val;
|
||||
else if (sscanf(line, "Buffers: %ld kB", &val) == 1) out->buffer_kb = val;
|
||||
else if (sscanf(line, "Cached: %ld kB", &val) == 1) out->cached_kb = val;
|
||||
}
|
||||
fclose(f);
|
||||
return (out->total_kb > 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
CPU
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
typedef struct {
|
||||
unsigned long long user, nice, sys, idle, iowait, irq, softirq, steal;
|
||||
} cpu_snap_t;
|
||||
|
||||
static cpu_snap_t g_prev = {0};
|
||||
static int g_valid = 0;
|
||||
|
||||
static int read_cpu(cpu_snap_t *s)
|
||||
{
|
||||
FILE *f = fopen("/proc/stat", "r");
|
||||
if (!f) return -1;
|
||||
int r = fscanf(f, "cpu %llu %llu %llu %llu %llu %llu %llu %llu",
|
||||
&s->user, &s->nice, &s->sys, &s->idle,
|
||||
&s->iowait, &s->irq, &s->softirq, &s->steal);
|
||||
fclose(f);
|
||||
return (r >= 4) ? 0 : -1;
|
||||
}
|
||||
|
||||
int sysinfo_cpu_percent(void)
|
||||
{
|
||||
cpu_snap_t cur;
|
||||
if (read_cpu(&cur) != 0) return 0;
|
||||
|
||||
if (!g_valid) { g_prev = cur; g_valid = 1; return 0; }
|
||||
|
||||
unsigned long long da = (cur.user - g_prev.user)
|
||||
+ (cur.nice - g_prev.nice)
|
||||
+ (cur.sys - g_prev.sys)
|
||||
+ (cur.irq - g_prev.irq)
|
||||
+ (cur.softirq - g_prev.softirq)
|
||||
+ (cur.steal - g_prev.steal);
|
||||
unsigned long long di = (cur.idle - g_prev.idle)
|
||||
+ (cur.iowait - g_prev.iowait);
|
||||
unsigned long long dt = da + di;
|
||||
g_prev = cur;
|
||||
return (dt == 0) ? 0 : (int)((da * 100) / dt);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
Interfaz de red
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
static int read_sysfs_str(const char *iface, const char *file,
|
||||
char *out, size_t sz)
|
||||
{
|
||||
char path[128];
|
||||
snprintf(path, sizeof(path), "/sys/class/net/%s/%s", iface, file);
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return -1;
|
||||
char buf[64] = {0};
|
||||
fgets(buf, sizeof(buf), f);
|
||||
fclose(f);
|
||||
buf[strcspn(buf, "\r\n")] = '\0';
|
||||
strncpy(out, buf, sz - 1);
|
||||
return (strlen(out) > 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
static int read_sysfs_int(const char *iface, const char *file)
|
||||
{
|
||||
char buf[32] = {0};
|
||||
if (read_sysfs_str(iface, file, buf, sizeof(buf)) != 0) return -1;
|
||||
int v = -1; sscanf(buf, "%d", &v); return v;
|
||||
}
|
||||
|
||||
static void read_ip_ioctl(const char *iface, char *out, size_t sz)
|
||||
{
|
||||
int fd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (fd < 0) return;
|
||||
struct ifreq ifr;
|
||||
memset(&ifr, 0, sizeof(ifr));
|
||||
strncpy(ifr.ifr_name, iface, IFNAMSIZ - 1);
|
||||
if (ioctl(fd, SIOCGIFADDR, &ifr) == 0) {
|
||||
struct sockaddr_in *sa = (struct sockaddr_in *)&ifr.ifr_addr;
|
||||
strncpy(out, inet_ntoa(sa->sin_addr), sz - 1);
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
|
||||
int sysinfo_iface(const char *ifname, iface_stats_t *out)
|
||||
{
|
||||
memset(out, 0, sizeof(*out));
|
||||
strncpy(out->name, ifname, sizeof(out->name) - 1);
|
||||
|
||||
/* MAC, operstate, speed, duplex */
|
||||
read_sysfs_str(ifname, "address", out->mac, sizeof(out->mac));
|
||||
char opstate[32] = {0};
|
||||
read_sysfs_str(ifname, "operstate", opstate, sizeof(opstate));
|
||||
out->up = (strcmp(opstate, "up") == 0 || strcmp(opstate, "unknown") == 0);
|
||||
int sp = read_sysfs_int(ifname, "speed");
|
||||
out->speed = (sp > 0) ? sp : 1000;
|
||||
|
||||
char dup[16] = {0};
|
||||
read_sysfs_str(ifname, "duplex", dup, sizeof(dup));
|
||||
out->full_duplex = (strncmp(dup, "full", 4) == 0);
|
||||
|
||||
/* IP */
|
||||
read_ip_ioctl(ifname, out->ip, sizeof(out->ip));
|
||||
|
||||
/* Contadores de /proc/net/dev */
|
||||
FILE *f = fopen("/proc/net/dev", "r");
|
||||
if (!f) return 0;
|
||||
|
||||
char line[512];
|
||||
fgets(line, sizeof(line), f); /* skip header lines */
|
||||
fgets(line, sizeof(line), f);
|
||||
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
char *colon = strchr(line, ':');
|
||||
if (!colon) continue;
|
||||
|
||||
/* Extraer nombre de interfaz (puede tener espacios al inicio) */
|
||||
size_t end = colon - line;
|
||||
while (end > 0 && line[end-1] == ' ') end--;
|
||||
size_t start = 0;
|
||||
while (start < end && line[start] == ' ') start++;
|
||||
char name[32] = {0};
|
||||
size_t nlen = end - start;
|
||||
if (nlen >= sizeof(name)) continue;
|
||||
strncpy(name, line + start, nlen);
|
||||
|
||||
if (strcmp(name, ifname) != 0) continue;
|
||||
|
||||
long long rb,rp,re,rd,rf,rframe,rcomp,rmulti;
|
||||
long long tb,tp,te,td,tf,tcol,tcomp,tcarr;
|
||||
sscanf(colon+1,
|
||||
"%lld %lld %lld %lld %lld %lld %lld %lld"
|
||||
" %lld %lld %lld %lld %lld %lld %lld %lld",
|
||||
&rb,&rp,&re,&rd,&rf,&rframe,&rcomp,&rmulti,
|
||||
&tb,&tp,&te,&td,&tf,&tcol,&tcomp,&tcarr);
|
||||
out->rx_bytes = rb; out->rx_packets = rp;
|
||||
out->rx_errors = re; out->rx_dropped = rd;
|
||||
out->rx_multicast= rmulti;
|
||||
out->tx_bytes = tb; out->tx_packets = tp;
|
||||
out->tx_errors = te; out->tx_dropped = td;
|
||||
break;
|
||||
}
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
Radio WiFi
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
|
||||
1. iw dev wlan0 info → canal y potencia
|
||||
Ejemplo:
|
||||
Interface wlan0
|
||||
channel 6 (2437 MHz), width: 20 MHz
|
||||
txpower 20.00 dBm
|
||||
|
||||
2. iw dev wlan0 survey dump → utilización del canal
|
||||
Buscamos el bloque con "[in use]":
|
||||
frequency: 2437 MHz [in use]
|
||||
channel active time: 12345 ms
|
||||
channel busy time: 987 ms
|
||||
channel transmit time: 456 ms
|
||||
channel receive time: 321 ms
|
||||
|
||||
Calculamos:
|
||||
cu_total = busy/active × 100
|
||||
cu_self_tx = transmit/active × 100
|
||||
cu_self_rx = receive/active × 100
|
||||
*/
|
||||
int sysinfo_radio(const char *iface, radio_stats_t *out)
|
||||
{
|
||||
memset(out, 0, sizeof(*out));
|
||||
strncpy(out->iface, iface, sizeof(out->iface) - 1);
|
||||
out->noise = -95;
|
||||
|
||||
char cmd[128];
|
||||
|
||||
/* iw dev <iface> info */
|
||||
snprintf(cmd, sizeof(cmd), "iw dev %s info 2>/dev/null", iface);
|
||||
FILE *p = popen(cmd, "r");
|
||||
if (!p) return -1;
|
||||
|
||||
char line[256];
|
||||
while (fgets(line, sizeof(line), p)) {
|
||||
int ch; float mhz;
|
||||
if (sscanf(line, " channel %d (%f MHz)", &ch, &mhz) == 2)
|
||||
out->channel = ch;
|
||||
float tp;
|
||||
if (sscanf(line, " txpower %f dBm", &tp) == 1)
|
||||
out->tx_power = (int)tp;
|
||||
}
|
||||
pclose(p);
|
||||
|
||||
/* iw dev <iface> survey dump */
|
||||
snprintf(cmd, sizeof(cmd), "iw dev %s survey dump 2>/dev/null", iface);
|
||||
p = popen(cmd, "r");
|
||||
if (!p) return 0;
|
||||
|
||||
long long active=0, busy=0, tx_t=0, rx_t=0;
|
||||
int in_use = 0;
|
||||
while (fgets(line, sizeof(line), p)) {
|
||||
if (strstr(line, "[in use]")) {
|
||||
in_use = 1; active=busy=tx_t=rx_t=0; continue;
|
||||
}
|
||||
if (!in_use) continue;
|
||||
/* Nueva frecuencia sin [in use] resetea el bloque */
|
||||
if (strstr(line, "frequency:") && !strstr(line, "[in use]")) {
|
||||
in_use = 0; continue;
|
||||
}
|
||||
float noise; long long val;
|
||||
if (sscanf(line, " noise: %f dBm", &noise) == 1) out->noise = (int)noise;
|
||||
if (sscanf(line, " channel active time: %lld ms", &val) == 1) active = val;
|
||||
if (sscanf(line, " channel busy time: %lld ms", &val) == 1) busy = val;
|
||||
if (sscanf(line, " channel transmit time: %lld ms", &val) == 1) tx_t = val;
|
||||
if (sscanf(line, " channel receive time: %lld ms", &val) == 1) rx_t = val;
|
||||
}
|
||||
pclose(p);
|
||||
|
||||
if (active > 0) {
|
||||
out->cu_total = (int)(busy * 100 / active);
|
||||
out->cu_self_tx = (int)(tx_t * 100 / active);
|
||||
out->cu_self_rx = (int)(rx_t * 100 / active);
|
||||
}
|
||||
|
||||
/* Número de clientes asociados */
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"iw dev %s station dump 2>/dev/null | grep -c '^Station'",
|
||||
iface);
|
||||
p = popen(cmd, "r");
|
||||
if (p) { fscanf(p, "%d", &out->num_sta); pclose(p); }
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#ifndef OPENUF_SYSINFO_H
|
||||
#define OPENUF_SYSINFO_H
|
||||
|
||||
/*
|
||||
* openuf - sysinfo.h
|
||||
*
|
||||
* Lee estadísticas del sistema (CPU, RAM, interfaces, radios).
|
||||
* Todas las lecturas son del kernel Linux directamente:
|
||||
*
|
||||
* /proc/stat → uso CPU (deltas entre dos snapshots)
|
||||
* /proc/meminfo → memoria total/libre/buffer/cache
|
||||
* /proc/net/dev → contadores rx/tx por interfaz
|
||||
* /sys/class/net/ → speed, duplex, operstate, MAC
|
||||
* iw dev <if> info → canal actual, potencia TX
|
||||
* iw dev <if> survey dump → utilización del canal
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/* ── Memoria ─────────────────────────────────────────────────────── */
|
||||
typedef struct {
|
||||
long total_kb;
|
||||
long free_kb;
|
||||
long buffer_kb;
|
||||
long cached_kb;
|
||||
} mem_stats_t;
|
||||
|
||||
int sysinfo_mem(mem_stats_t *out);
|
||||
|
||||
/* ── CPU ─────────────────────────────────────────────────────────── */
|
||||
/* Retorna % uso CPU (0-100). Primera llamada retorna 0 (toma snapshot).
|
||||
* Las siguientes calculan el delta respecto a la anterior.
|
||||
* Con intervalo de 10s da un buen promedio de uso. */
|
||||
int sysinfo_cpu_percent(void);
|
||||
|
||||
/* ── Interfaz de red ─────────────────────────────────────────────── */
|
||||
typedef struct {
|
||||
char name[32];
|
||||
char mac[32];
|
||||
char ip[64];
|
||||
bool up;
|
||||
int speed; /* Mbps: 10/100/1000; -1 si no disponible */
|
||||
bool full_duplex;
|
||||
long long rx_bytes;
|
||||
long long tx_bytes;
|
||||
long long rx_packets;
|
||||
long long tx_packets;
|
||||
long long rx_errors;
|
||||
long long tx_errors;
|
||||
long long rx_dropped;
|
||||
long long tx_dropped;
|
||||
long long rx_multicast;
|
||||
} iface_stats_t;
|
||||
|
||||
int sysinfo_iface(const char *ifname, iface_stats_t *out);
|
||||
|
||||
/* ── Radio WiFi ─────────────────────────────────────────────────── */
|
||||
typedef struct {
|
||||
char name[32];
|
||||
char iface[32];
|
||||
int channel;
|
||||
int tx_power;
|
||||
int cu_total; /* % uso canal total */
|
||||
int cu_self_tx; /* % tiempo transmitiendo */
|
||||
int cu_self_rx; /* % tiempo recibiendo */
|
||||
int num_sta;
|
||||
int noise; /* dBm */
|
||||
} radio_stats_t;
|
||||
|
||||
/* iface: "wlan0", "wlan1" */
|
||||
int sysinfo_radio(const char *iface, radio_stats_t *out);
|
||||
|
||||
#endif /* OPENUF_SYSINFO_H */
|
||||
@@ -0,0 +1,81 @@
|
||||
#ifndef OPENUF_UFMODEL_H
|
||||
#define OPENUF_UFMODEL_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/* ─── Radio entry ─────────────────────────────────────────────────── */
|
||||
typedef struct {
|
||||
const char *name; /* "wifi0", "wifi1" */
|
||||
const char *radio; /* "ng" (2.4 GHz) | "na" (5 GHz) | "6g" */
|
||||
int channel;
|
||||
const char *ht; /* "HT20", "HT40", "HT80" */
|
||||
int min_txpower;
|
||||
int max_txpower;
|
||||
int nss;
|
||||
int tx_power;
|
||||
int radio_caps;
|
||||
int antenna_gain;
|
||||
bool he_enabled;
|
||||
} uf_radio_t;
|
||||
|
||||
/* ─── Ethernet port entry ─────────────────────────────────────────── */
|
||||
typedef struct {
|
||||
const char *ifname;
|
||||
const char *name;
|
||||
int port_idx;
|
||||
int poe_caps;
|
||||
const char *media; /* "GE" */
|
||||
int speed;
|
||||
bool up;
|
||||
bool is_uplink;
|
||||
bool full_duplex;
|
||||
} uf_port_t;
|
||||
|
||||
/* ─── Ethernet table entry ────────────────────────────────────────── */
|
||||
typedef struct {
|
||||
const char *name;
|
||||
int num_port;
|
||||
} uf_eth_entry_t;
|
||||
|
||||
/* ─── Radio map entry (band → OpenWrt device) ─────────────────────── */
|
||||
typedef struct {
|
||||
const char *band; /* "ng", "na", "6g" */
|
||||
const char *device; /* "radio0", "radio1" */
|
||||
} uf_radio_map_t;
|
||||
|
||||
/* ─── Full model descriptor ───────────────────────────────────────── */
|
||||
typedef struct {
|
||||
const char *model; /* "U6IW", "U6LITE" */
|
||||
const char *model_display; /* "U6 IW" */
|
||||
const char *display_name; /* "U6-IW" */
|
||||
const char *platform; /* used in announce PKT_PLATFORM */
|
||||
int board_rev;
|
||||
bool has_eth1;
|
||||
|
||||
/* Firmware strings */
|
||||
const char *fw_pre; /* "U6IW.mt7622_5_4.v" */
|
||||
const char *fw_ver; /* "6.6.55.14430" */
|
||||
const char *fw_buildtime; /* "230901.1200" */
|
||||
const char *fw_factoryver;
|
||||
|
||||
/* Tables */
|
||||
const uf_radio_t *radio_table;
|
||||
int radio_table_len;
|
||||
const uf_port_t *port_table;
|
||||
int port_table_len;
|
||||
const uf_eth_entry_t *ethernet_table;
|
||||
int ethernet_table_len;
|
||||
const uf_radio_map_t *radio_map;
|
||||
int radio_map_len;
|
||||
} uf_model_t;
|
||||
|
||||
/* ─── Model registry ──────────────────────────────────────────────── */
|
||||
const uf_model_t *ufmodel_find(const char *name);
|
||||
|
||||
extern const uf_model_t model_u6inwall;
|
||||
extern const uf_model_t model_u6lite;
|
||||
extern const uf_model_t model_uapg1;
|
||||
extern const uf_model_t model_uapg1lr;
|
||||
extern const uf_model_t model_uapg2aclr;
|
||||
|
||||
#endif /* OPENUF_UFMODEL_H */
|
||||
+885
@@ -0,0 +1,885 @@
|
||||
/*
|
||||
* openuf - wlan.c
|
||||
*
|
||||
* Traduce la configuración WiFi del controlador UniFi en settings
|
||||
* UCI de OpenWrt usando libuci directamente (sin shell).
|
||||
*
|
||||
* ── CÓMO SE APLICA LA CONFIGURACIÓN ────────────────────────────────
|
||||
*
|
||||
* El controlador envía "setstate" con:
|
||||
* radio_table[] → configuración de las radios (canal, potencia, HT)
|
||||
* vap_table[] → configuración de las redes WiFi (SSID, clave, roaming...)
|
||||
*
|
||||
* Este módulo:
|
||||
* 1. Borra todas las wifi-iface UCI con prefijo "openuf_"
|
||||
* 2. Aplica radio_table → wireless.<device>.channel/txpower/htmode
|
||||
* 3. Crea nuevas wifi-iface por cada VAP con su configuración
|
||||
* 4. Ejecuta "wifi reload" para aplicar sin reiniciar
|
||||
*
|
||||
* ── MAPEO DE SEGURIDAD ──────────────────────────────────────────────
|
||||
*
|
||||
* UniFi OpenWrt UCI Descripción
|
||||
* ─────────────────────────────────────────────
|
||||
* open none Sin contraseña
|
||||
* wpapsk psk WPA Personal
|
||||
* wpa2psk psk2 WPA2 Personal
|
||||
* wpapskwpa2psk psk-mixed WPA/WPA2 mixto
|
||||
* wpa3 sae WPA3 Personal
|
||||
* wpa3transition sae-mixed WPA2+WPA3 transición
|
||||
* wpa2enterprise wpa2 WPA2 Enterprise (RADIUS)
|
||||
* wpa3enterprise wpa3 WPA3 Enterprise
|
||||
*
|
||||
* ── BAND STEERING (802.11k/v) ──────────────────────────────────────
|
||||
*
|
||||
* Cuando UniFi activa band_steering, configuramos en UCI:
|
||||
* ieee80211k = 1 → Neighbor Reports (AP informa a cliente de otros APs)
|
||||
* ieee80211v = 1 → BSS Transition Management (AP puede pedir que el
|
||||
* cliente se mueva a otro AP/radio)
|
||||
* rrm_neighbor_report = 1
|
||||
* bss_transition = 1
|
||||
*
|
||||
* El hostapd de OpenWrt usa estos flags para implementar 802.11k/v.
|
||||
* Band steering real requiere lógica adicional (daemon externo o
|
||||
* script que monitoriza RSSI y envía BTM Request).
|
||||
*
|
||||
* ── FAST ROAMING (802.11r) ─────────────────────────────────────────
|
||||
*
|
||||
* Cuando UniFi activa fast_roaming_enabled:
|
||||
* ieee80211r = 1 → FT (Fast BSS Transition)
|
||||
* ft_over_ds = 1 → FT sobre Distribution System (más compatible)
|
||||
* mobility_domain = XXXX → Mismo dominio en todos los APs del site
|
||||
* ft_psk_generate_local = 1 → PSK sin servidor FT externo
|
||||
*
|
||||
* El mobility_domain se deriva de los primeros 2 bytes del MAC del AP.
|
||||
* Todos los APs del mismo site deben usar el mismo mobility_domain.
|
||||
*
|
||||
* ── PMF (Protected Management Frames / 802.11w) ─────────────────────
|
||||
*
|
||||
* pmf_mode → ieee80211w:
|
||||
* "disabled" → 0 (sin PMF)
|
||||
* "optional" → 1 (PMF opcional, compatible con clientes sin PMF)
|
||||
* "required" → 2 (PMF obligatorio, solo clientes con PMF)
|
||||
*
|
||||
* WPA3 siempre requiere PMF=2.
|
||||
*
|
||||
* ── LECTURA DE VAPs DESDE UCI ───────────────────────────────────────
|
||||
*
|
||||
* wlan_get_vap_table() itera todas las wifi-iface de /etc/config/wireless
|
||||
* que tengan prefijo "openuf_" y construye el JSON vap_table para
|
||||
* incluirlo en el payload inform.
|
||||
*
|
||||
* Para cada VAP leemos: ssid, device, bssid, encryption, key, disabled
|
||||
* y los traducimos al formato que espera el controlador.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <uci.h>
|
||||
#include <json-c/json.h>
|
||||
|
||||
#include "wlan.h"
|
||||
#include "ufmodel.h"
|
||||
|
||||
/* ─── Mapeo de seguridad UniFi → OpenWrt UCI ────────────────────── */
|
||||
static const char *sec_to_uci(const char *uf)
|
||||
{
|
||||
if (!uf || !strcmp(uf,"open")) return "none";
|
||||
if (!strcmp(uf,"wpapsk")) return "psk";
|
||||
if (!strcmp(uf,"wpa2psk")) return "psk2";
|
||||
if (!strcmp(uf,"wpapskwpa2psk")) return "psk-mixed";
|
||||
if (!strcmp(uf,"wpa3")) return "sae";
|
||||
if (!strcmp(uf,"wpa3transition")) return "sae-mixed";
|
||||
if (!strcmp(uf,"wpa2enterprise")) return "wpa2";
|
||||
if (!strcmp(uf,"wpa3enterprise")) return "wpa3";
|
||||
return "psk2"; /* default */
|
||||
}
|
||||
|
||||
/* Mapeo inverso: UCI → UniFi (para wlan_get_vap_table) */
|
||||
static const char *sec_to_unifi(const char *uci)
|
||||
{
|
||||
if (!uci || !strcmp(uci,"none")) return "open";
|
||||
if (!strcmp(uci,"psk")) return "wpapsk";
|
||||
if (!strcmp(uci,"psk2")) return "wpa2psk";
|
||||
if (!strcmp(uci,"psk-mixed")) return "wpapskwpa2psk";
|
||||
if (!strcmp(uci,"sae")) return "wpa3";
|
||||
if (!strcmp(uci,"sae-mixed")) return "wpa3transition";
|
||||
if (!strcmp(uci,"wpa2")) return "wpa2enterprise";
|
||||
if (!strcmp(uci,"wpa3")) return "wpa3enterprise";
|
||||
return "wpa2psk";
|
||||
}
|
||||
|
||||
/* ─── Nombre de sección UCI seguro (máx 15 chars) ──────────────── */
|
||||
static void safe_section_name(const char *ssid, char *out, size_t sz)
|
||||
{
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; ssid[i] && j < sz-1 && j < 15; i++) {
|
||||
char c = ssid[i];
|
||||
if ((c>='a'&&c<='z')||(c>='A'&&c<='Z')||
|
||||
(c>='0'&&c<='9')||c=='_'||c=='-')
|
||||
out[j++] = c;
|
||||
else
|
||||
out[j++] = '_';
|
||||
}
|
||||
out[j] = '\0';
|
||||
}
|
||||
|
||||
/* ─── libuci: set un valor en wireless ─────────────────────────── */
|
||||
static int uci_set_val(struct uci_context *ctx,
|
||||
const char *path, const char *val)
|
||||
{
|
||||
struct uci_ptr ptr;
|
||||
char *p = malloc(strlen(path) + strlen(val) + 2);
|
||||
if (!p) return -1;
|
||||
sprintf(p, "%s=%s", path, val);
|
||||
int ret = uci_lookup_ptr(ctx, &ptr, p, true);
|
||||
free(p);
|
||||
if (ret != UCI_OK) return -1;
|
||||
return (uci_set(ctx, &ptr) == UCI_OK) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Wrapper que formatea path y value en printf style */
|
||||
#define UCI_SET(ctx, pkg, sec, opt, val) do { \
|
||||
char _path[256]; \
|
||||
snprintf(_path, sizeof(_path), "%s.%s.%s", pkg, sec, opt); \
|
||||
uci_set_val(ctx, _path, val); \
|
||||
} while(0)
|
||||
|
||||
#define UCI_SET_INT(ctx, pkg, sec, opt, ival) do { \
|
||||
char _v[32]; snprintf(_v, sizeof(_v), "%d", ival); \
|
||||
UCI_SET(ctx, pkg, sec, opt, _v); \
|
||||
} while(0)
|
||||
|
||||
/* ─── Encontrar/crear sección UCI ──────────────────────────────── */
|
||||
static int uci_ensure_section(struct uci_context *ctx,
|
||||
struct uci_package *pkg,
|
||||
const char *sec_name,
|
||||
const char *sec_type)
|
||||
{
|
||||
struct uci_element *e;
|
||||
uci_foreach_element(&pkg->sections, e) {
|
||||
struct uci_section *s = uci_to_section(e);
|
||||
if (!strcmp(s->e.name, sec_name) && !strcmp(s->type, sec_type))
|
||||
return 0; /* ya existe */
|
||||
}
|
||||
/* Create a named section: wireless.<name>=<type>. */
|
||||
char *p = malloc(strlen(pkg->e.name) + strlen(sec_name) +
|
||||
strlen(sec_type) + 3);
|
||||
if (!p) return -1;
|
||||
sprintf(p, "%s.%s=%s", pkg->e.name, sec_name, sec_type);
|
||||
struct uci_ptr ptr;
|
||||
int ret = uci_lookup_ptr(ctx, &ptr, p, true);
|
||||
if (ret == UCI_OK)
|
||||
ret = uci_set(ctx, &ptr);
|
||||
free(p);
|
||||
return ret == UCI_OK ? 0 : -1;
|
||||
}
|
||||
|
||||
static int ensure_vlan_network(int vid)
|
||||
{
|
||||
struct uci_context *ctx = uci_alloc_context();
|
||||
if (!ctx) return -1;
|
||||
|
||||
struct uci_package *pkg = NULL;
|
||||
if (uci_load(ctx, "network", &pkg) != UCI_OK) {
|
||||
uci_free_context(ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char device_section[48], interface_section[32];
|
||||
char device_name[32], vid_string[16];
|
||||
snprintf(device_section, sizeof(device_section),
|
||||
"openuf_vlan%d", vid);
|
||||
snprintf(interface_section, sizeof(interface_section),
|
||||
"vlan%d", vid);
|
||||
snprintf(device_name, sizeof(device_name), "br-lan.%d", vid);
|
||||
snprintf(vid_string, sizeof(vid_string), "%d", vid);
|
||||
|
||||
int ok = uci_ensure_section(ctx, pkg, device_section, "device") == 0 &&
|
||||
uci_ensure_section(ctx, pkg, interface_section, "interface") == 0;
|
||||
if (ok) {
|
||||
UCI_SET(ctx, "network", device_section, "type", "8021q");
|
||||
UCI_SET(ctx, "network", device_section, "ifname", "br-lan");
|
||||
UCI_SET(ctx, "network", device_section, "vid", vid_string);
|
||||
UCI_SET(ctx, "network", device_section, "name", device_name);
|
||||
UCI_SET(ctx, "network", interface_section, "proto", "none");
|
||||
UCI_SET(ctx, "network", interface_section, "device", device_name);
|
||||
ok = uci_commit(ctx, &pkg, false) == UCI_OK;
|
||||
}
|
||||
|
||||
uci_unload(ctx, pkg);
|
||||
uci_free_context(ctx);
|
||||
if (ok)
|
||||
printf("[openuf] Configured VLAN %d as network '%s' on br-lan\n",
|
||||
vid, interface_section);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
wlan_clear — remove all VAPs before applying controller ownership
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
void wlan_clear(void)
|
||||
{
|
||||
struct uci_context *ctx = uci_alloc_context();
|
||||
if (!ctx) return;
|
||||
|
||||
struct uci_package *pkg = NULL;
|
||||
if (uci_load(ctx, "wireless", &pkg) != UCI_OK) {
|
||||
uci_free_context(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Recopilar secciones a eliminar (no modificar durante iteración) */
|
||||
char *to_del[64];
|
||||
int ndel = 0;
|
||||
struct uci_element *e;
|
||||
uci_foreach_element(&pkg->sections, e) {
|
||||
struct uci_section *s = uci_to_section(e);
|
||||
if (!strcmp(s->type, "wifi-iface") && ndel < 64) {
|
||||
to_del[ndel++] = strdup(s->e.name);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < ndel; i++) {
|
||||
struct uci_ptr ptr;
|
||||
char path[128];
|
||||
snprintf(path, sizeof(path), "wireless.%s", to_del[i]);
|
||||
if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK)
|
||||
uci_delete(ctx, &ptr);
|
||||
free(to_del[i]);
|
||||
}
|
||||
|
||||
if (ndel > 0) {
|
||||
uci_commit(ctx, &pkg, false);
|
||||
printf("[openuf] wlan_clear: removed %d existing VAPs\n", ndel);
|
||||
}
|
||||
|
||||
uci_unload(ctx, pkg);
|
||||
uci_free_context(ctx);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
wlan_apply_radio — aplicar config de radio (canal, HT, potencia)
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
|
||||
Lectura de parámetros del JSON del controlador:
|
||||
channel → wireless.<device>.channel
|
||||
ht → wireless.<device>.htmode ("HT20" / "HT40" / "HT80" / "HE80")
|
||||
tx_power → wireless.<device>.txpower
|
||||
min_rssi → no se mapea a UCI (requiere daemon externo)
|
||||
*/
|
||||
void wlan_apply_radio(struct json_object *radio_json,
|
||||
const char *device_name)
|
||||
{
|
||||
struct uci_context *ctx = uci_alloc_context();
|
||||
if (!ctx) return;
|
||||
|
||||
struct uci_package *pkg = NULL;
|
||||
if (uci_load(ctx, "wireless", &pkg) != UCI_OK) {
|
||||
uci_free_context(ctx); return;
|
||||
}
|
||||
|
||||
struct json_object *v;
|
||||
char path[256];
|
||||
|
||||
/* Map UniFi band names to OpenWrt mac80211 band names. */
|
||||
if (json_object_object_get_ex(radio_json, "radio", &v)) {
|
||||
const char *radio = json_object_get_string(v);
|
||||
const char *band = !strcmp(radio, "ng") ? "2g" :
|
||||
!strcmp(radio, "na") ? "5g" :
|
||||
!strcmp(radio, "6g") ? "6g" : NULL;
|
||||
if (band) {
|
||||
snprintf(path, sizeof(path), "wireless.%s.band=%s",
|
||||
device_name, band);
|
||||
struct uci_ptr ptr;
|
||||
if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK)
|
||||
uci_set(ctx, &ptr);
|
||||
}
|
||||
}
|
||||
|
||||
#define RP(key, uci_opt) \
|
||||
if (json_object_object_get_ex(radio_json, key, &v)) { \
|
||||
snprintf(path, sizeof(path), "wireless.%s.%s=%s", \
|
||||
device_name, uci_opt, json_object_get_string(v)); \
|
||||
struct uci_ptr ptr; \
|
||||
if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK) \
|
||||
uci_set(ctx, &ptr); \
|
||||
}
|
||||
|
||||
RP("ht", "htmode");
|
||||
|
||||
/* Canal: 0 = auto en UniFi */
|
||||
if (json_object_object_get_ex(radio_json, "channel", &v)) {
|
||||
int ch = json_object_get_int(v);
|
||||
if (ch == 0) {
|
||||
snprintf(path, sizeof(path), "wireless.%s.channel=auto", device_name);
|
||||
} else {
|
||||
snprintf(path, sizeof(path), "wireless.%s.channel=%d", device_name, ch);
|
||||
}
|
||||
struct uci_ptr ptr;
|
||||
if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK)
|
||||
uci_set(ctx, &ptr);
|
||||
}
|
||||
|
||||
/* tx_power */
|
||||
if (json_object_object_get_ex(radio_json, "tx_power", &v)) {
|
||||
snprintf(path, sizeof(path), "wireless.%s.txpower=%d",
|
||||
device_name, json_object_get_int(v));
|
||||
struct uci_ptr ptr;
|
||||
if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK)
|
||||
uci_set(ctx, &ptr);
|
||||
}
|
||||
|
||||
/* Habilitar el radio */
|
||||
snprintf(path, sizeof(path), "wireless.%s.disabled=0", device_name);
|
||||
struct uci_ptr ptr;
|
||||
if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK)
|
||||
uci_set(ctx, &ptr);
|
||||
|
||||
#undef RP
|
||||
|
||||
uci_commit(ctx, &pkg, false);
|
||||
uci_unload(ctx, pkg);
|
||||
uci_free_context(ctx);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
Crear una VAP (wifi-iface UCI) desde un JSON VAP del controlador
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
|
||||
Parámetros del controlador que leemos y cómo los mapeamos:
|
||||
|
||||
essid → wireless.openuf_X.ssid
|
||||
x_passphrase → wireless.openuf_X.key
|
||||
security → wireless.openuf_X.encryption (via sec_to_uci)
|
||||
hide_ssid → wireless.openuf_X.hidden
|
||||
guest_policy → wireless.openuf_X.isolate (aislamiento de clientes)
|
||||
fast_roaming_enabled → ieee80211r, ft_over_ds, mobility_domain, ft_psk_generate_local
|
||||
band_steering → ieee80211k, ieee80211v, rrm_neighbor_report, bss_transition
|
||||
pmf_mode → ieee80211w (0/1/2)
|
||||
wpa3_support → añadir "sae-mixed" si WPA2+WPA3
|
||||
uapsd → uapsd (U-APSD power saving)
|
||||
vlan_id → wireless.openuf_X.vlan_id (si ≠ 0)
|
||||
*/
|
||||
static int apply_vap(struct uci_context *ctx,
|
||||
struct uci_package *pkg,
|
||||
struct json_object *vap_json,
|
||||
const char *device_name,
|
||||
const char *mac_str,
|
||||
int vap_idx)
|
||||
{
|
||||
struct json_object *v;
|
||||
const char *essid = "";
|
||||
const char *security = "wpa2psk";
|
||||
const char *pass = "";
|
||||
|
||||
if (json_object_object_get_ex(vap_json, "essid", &v)) essid = json_object_get_string(v);
|
||||
if (json_object_object_get_ex(vap_json, "security", &v)) security = json_object_get_string(v);
|
||||
if (json_object_object_get_ex(vap_json, "x_passphrase",&v)) pass = json_object_get_string(v);
|
||||
|
||||
/* Nombre de sección: openuf_<idx>_<ssid_safe> */
|
||||
char safe[16] = {0};
|
||||
safe_section_name(essid, safe, sizeof(safe));
|
||||
char sec_name[48];
|
||||
snprintf(sec_name, sizeof(sec_name), "openuf_%d_%s", vap_idx, safe);
|
||||
|
||||
if (uci_ensure_section(ctx, pkg, sec_name, "wifi-iface") != 0) {
|
||||
printf("[openuf] Failed to create VAP section '%s'\n", sec_name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
UCI_SET(ctx, "wireless", sec_name, "device", device_name);
|
||||
UCI_SET(ctx, "wireless", sec_name, "mode", "ap");
|
||||
UCI_SET(ctx, "wireless", sec_name, "ssid", essid);
|
||||
UCI_SET(ctx, "wireless", sec_name, "network", "lan");
|
||||
UCI_SET(ctx, "wireless", sec_name, "encryption", sec_to_uci(security));
|
||||
|
||||
/* Contraseña */
|
||||
if (pass && pass[0] && strcmp(security,"open") != 0)
|
||||
UCI_SET(ctx, "wireless", sec_name, "key", pass);
|
||||
|
||||
/* SSID oculto */
|
||||
int hidden = 0;
|
||||
if (json_object_object_get_ex(vap_json, "hide_ssid", &v))
|
||||
hidden = json_object_get_boolean(v) ? 1 : 0;
|
||||
UCI_SET_INT(ctx, "wireless", sec_name, "hidden", hidden);
|
||||
|
||||
/* Aislamiento de clientes (guest network) */
|
||||
int isolate = 0;
|
||||
if (json_object_object_get_ex(vap_json, "guest_policy", &v))
|
||||
isolate = json_object_get_boolean(v) ? 1 : 0;
|
||||
UCI_SET_INT(ctx, "wireless", sec_name, "isolate", isolate);
|
||||
|
||||
/* U-APSD (ahorro de energía para clientes móviles) */
|
||||
int uapsd = 1;
|
||||
if (json_object_object_get_ex(vap_json, "uapsd", &v))
|
||||
uapsd = json_object_get_boolean(v) ? 1 : 0;
|
||||
UCI_SET_INT(ctx, "wireless", sec_name, "uapsd", uapsd);
|
||||
|
||||
/* ── PMF (Protected Management Frames / 802.11w) ──────────────
|
||||
* "disabled" → 0, "optional" → 1, "required" → 2
|
||||
* WPA3 (sae/sae-mixed) siempre requiere ieee80211w=2 */
|
||||
int pmf = 0;
|
||||
if (json_object_object_get_ex(vap_json, "pmf_mode", &v)) {
|
||||
const char *pm = json_object_get_string(v);
|
||||
if (!strcmp(pm, "optional")) pmf = 1;
|
||||
if (!strcmp(pm, "required")) pmf = 2;
|
||||
}
|
||||
/* WPA3 obliga PMF=2 */
|
||||
if (!strcmp(security,"wpa3") || !strcmp(security,"wpa3transition") ||
|
||||
!strcmp(security,"wpa3enterprise"))
|
||||
pmf = 2;
|
||||
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211w", pmf);
|
||||
|
||||
/* ── Fast Roaming (802.11r FT) ────────────────────────────────
|
||||
* Permite que los clientes se muevan entre APs sin re-autenticación
|
||||
* completa. El handshake FT sólo tarda ~50ms vs ~200-300ms normal. */
|
||||
int ft = 0;
|
||||
if (json_object_object_get_ex(vap_json, "fast_roaming_enabled", &v))
|
||||
ft = json_object_get_boolean(v) ? 1 : 0;
|
||||
if (ft) {
|
||||
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211r", 1);
|
||||
UCI_SET_INT(ctx, "wireless", sec_name, "ft_over_ds", 1);
|
||||
UCI_SET_INT(ctx, "wireless", sec_name, "ft_psk_generate_local", 1);
|
||||
/* mobility_domain: derivar de MAC del AP (2 bytes) */
|
||||
char mdomain[8] = {0};
|
||||
if (mac_str && strlen(mac_str) >= 5) {
|
||||
/* Usar bytes 0 y 1 de la MAC como dominio */
|
||||
char b0[3]={mac_str[0],mac_str[1],0};
|
||||
char b1[3]={mac_str[3],mac_str[4],0};
|
||||
unsigned int v0=0,v1=0;
|
||||
sscanf(b0,"%x",&v0); sscanf(b1,"%x",&v1);
|
||||
snprintf(mdomain, sizeof(mdomain), "%02x%02x", v0, v1);
|
||||
} else {
|
||||
strcpy(mdomain, "1234");
|
||||
}
|
||||
UCI_SET(ctx, "wireless", sec_name, "mobility_domain", mdomain);
|
||||
} else {
|
||||
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211r", 0);
|
||||
}
|
||||
|
||||
/* ── Band Steering (802.11k/v) ────────────────────────────────
|
||||
* 802.11k: Neighbor Reports → el AP informa al cliente qué otros
|
||||
* APs existen para facilitar el roaming.
|
||||
* 802.11v: BSS Transition Management → el AP puede "sugerir" al
|
||||
* cliente que se mueva a otro AP con mejor señal. */
|
||||
int band_steer = 0;
|
||||
if (json_object_object_get_ex(vap_json, "band_steering", &v))
|
||||
band_steer = json_object_get_boolean(v) ? 1 : 0;
|
||||
if (band_steer) {
|
||||
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211k", 1);
|
||||
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211v", 1);
|
||||
UCI_SET_INT(ctx, "wireless", sec_name, "rrm_neighbor_report", 1);
|
||||
UCI_SET_INT(ctx, "wireless", sec_name, "bss_transition", 1);
|
||||
} else {
|
||||
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211k", 0);
|
||||
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211v", 0);
|
||||
}
|
||||
|
||||
/* ── VLAN ──────────────────────────────────────────────────────
|
||||
* Si vlan_id ≠ 0, configurar la interfaz con VLAN tagging. */
|
||||
if (json_object_object_get_ex(vap_json, "vlan_id", &v)) {
|
||||
int vid = json_object_get_int(v);
|
||||
if (vid > 0) {
|
||||
if (ensure_vlan_network(vid) != 0) {
|
||||
printf("[openuf] Failed to configure VLAN network %d\n", vid);
|
||||
return -1;
|
||||
}
|
||||
UCI_SET_INT(ctx, "wireless", sec_name, "vlan_id", vid);
|
||||
/* Establecer network a vlanXXX si existe */
|
||||
char vlan_net[32];
|
||||
snprintf(vlan_net, sizeof(vlan_net), "vlan%d", vid);
|
||||
UCI_SET(ctx, "wireless", sec_name, "network", vlan_net);
|
||||
}
|
||||
}
|
||||
|
||||
printf("[openuf] VAP '%s' → %s enc=%s ft=%d bs=%d pmf=%d\n",
|
||||
essid, sec_name, sec_to_uci(security), ft, band_steer, pmf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
wlan_apply_config — aplicar configuración completa del controlador
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
|
||||
Llamado desde inform.c → handle_response() cuando _type=="setstate".
|
||||
config_json es el JSON completo del controlador.
|
||||
|
||||
Proceso:
|
||||
1. Eliminar VAPs antiguas (prefijo openuf_)
|
||||
2. Aplicar radio_table (canal, potencia, htmode) por radio
|
||||
3. Crear una VAP por cada entrada en vap_table
|
||||
4. Hacer commit UCI
|
||||
5. Ejecutar "wifi reload" para aplicar sin reiniciar el AP
|
||||
*/
|
||||
int wlan_apply_config(struct json_object *config_json,
|
||||
const uf_model_t *model)
|
||||
{
|
||||
struct json_object *rt_arr = NULL, *vt_arr = NULL, *v;
|
||||
json_object_object_get_ex(config_json, "radio_table", &rt_arr);
|
||||
json_object_object_get_ex(config_json, "vap_table", &vt_arr);
|
||||
|
||||
/* Obtener MAC del AP para mobility_domain */
|
||||
char mac_str[32] = "00:00:00:00:00:00";
|
||||
{
|
||||
char path[128];
|
||||
snprintf(path, sizeof(path), "/sys/class/net/eth0/address");
|
||||
FILE *f = fopen(path, "r");
|
||||
if (f) { fgets(mac_str, sizeof(mac_str), f); fclose(f); }
|
||||
mac_str[strcspn(mac_str, "\r\n")] = '\0';
|
||||
}
|
||||
|
||||
/* 1. Limpiar VAPs antiguas */
|
||||
wlan_clear();
|
||||
|
||||
/* 2. Aplicar radio_table */
|
||||
if (rt_arr && json_object_is_type(rt_arr, json_type_array)) {
|
||||
int nr = json_object_array_length(rt_arr);
|
||||
for (int i = 0; i < nr; i++) {
|
||||
struct json_object *r = json_object_array_get_idx(rt_arr, i);
|
||||
if (!r) continue;
|
||||
/* Buscar el device UCI correspondiente a esta banda */
|
||||
const char *radio_band = "";
|
||||
if (json_object_object_get_ex(r, "radio", &v))
|
||||
radio_band = json_object_get_string(v);
|
||||
const char *device_name = "radio0";
|
||||
for (int j = 0; j < model->radio_map_len; j++) {
|
||||
if (!strcmp(model->radio_map[j].band, radio_band)) {
|
||||
device_name = model->radio_map[j].device;
|
||||
break;
|
||||
}
|
||||
}
|
||||
wlan_apply_radio(r, device_name);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Load the package after the per-radio commits, otherwise this context
|
||||
* contains a stale copy that can overwrite those changes on commit.
|
||||
*/
|
||||
struct uci_context *ctx = uci_alloc_context();
|
||||
if (!ctx) {
|
||||
printf("[openuf] Failed to allocate UCI context\n");
|
||||
return -1;
|
||||
}
|
||||
struct uci_package *pkg = NULL;
|
||||
if (uci_load(ctx, "wireless", &pkg) != UCI_OK) {
|
||||
char *uci_error = NULL;
|
||||
uci_get_errorstr(ctx, &uci_error, "wireless");
|
||||
printf("[openuf] Failed to load UCI wireless configuration: %s\n",
|
||||
uci_error ? uci_error : "unknown UCI error");
|
||||
free(uci_error);
|
||||
uci_free_context(ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Once UniFi provisioning owns Wi-Fi, disable OpenWrt's generated
|
||||
* default VAPs. Leaving them enabled keeps broadcasting "OpenWrt"
|
||||
* alongside the controller-managed SSIDs.
|
||||
*/
|
||||
int disabled_defaults = 0;
|
||||
struct uci_element *default_element;
|
||||
uci_foreach_element(&pkg->sections, default_element) {
|
||||
struct uci_section *section = uci_to_section(default_element);
|
||||
if (!strcmp(section->type, "wifi-iface") &&
|
||||
!strncmp(section->e.name, "default_radio", 13)) {
|
||||
UCI_SET(ctx, "wireless", section->e.name, "disabled", "1");
|
||||
disabled_defaults++;
|
||||
}
|
||||
}
|
||||
if (disabled_defaults)
|
||||
printf("[openuf] Disabled %d default OpenWrt VAPs\n",
|
||||
disabled_defaults);
|
||||
|
||||
/* 3. Crear VAPs */
|
||||
if (vt_arr && json_object_is_type(vt_arr, json_type_array)) {
|
||||
int nv = json_object_array_length(vt_arr);
|
||||
for (int i = 0; i < nv; i++) {
|
||||
struct json_object *vap = json_object_array_get_idx(vt_arr, i);
|
||||
if (!vap) continue;
|
||||
|
||||
/* Buscar device UCI para este VAP */
|
||||
const char *radio_band = "ng";
|
||||
if (json_object_object_get_ex(vap, "radio", &v))
|
||||
radio_band = json_object_get_string(v);
|
||||
const char *device_name = "radio0";
|
||||
for (int j = 0; j < model->radio_map_len; j++) {
|
||||
if (!strcmp(model->radio_map[j].band, radio_band)) {
|
||||
device_name = model->radio_map[j].device;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (apply_vap(ctx, pkg, vap, device_name, mac_str, i) != 0) {
|
||||
uci_unload(ctx, pkg);
|
||||
uci_free_context(ctx);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 4. Commit UCI */
|
||||
if (uci_commit(ctx, &pkg, false) != UCI_OK) {
|
||||
char *uci_error = NULL;
|
||||
uci_get_errorstr(ctx, &uci_error, "wireless");
|
||||
printf("[openuf] Failed to commit UCI wireless configuration: %s\n",
|
||||
uci_error ? uci_error : "unknown UCI error");
|
||||
free(uci_error);
|
||||
uci_unload(ctx, pkg);
|
||||
uci_free_context(ctx);
|
||||
return -1;
|
||||
}
|
||||
uci_unload(ctx, pkg);
|
||||
uci_free_context(ctx);
|
||||
|
||||
/* 5. Aplicar cambios sin reiniciar (wifi reload recarga hostapd) */
|
||||
printf("[openuf] Running wifi reload...\n");
|
||||
system("ubus call network reload >/dev/null 2>&1");
|
||||
system("wifi reload 2>/dev/null &");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int system_cfg_get(const char *cfg, const char *key,
|
||||
char *out, size_t out_size)
|
||||
{
|
||||
size_t key_len = strlen(key);
|
||||
const char *line = cfg;
|
||||
|
||||
while (line && *line) {
|
||||
const char *end = strchr(line, '\n');
|
||||
size_t line_len = end ? (size_t)(end - line) : strlen(line);
|
||||
if (line_len > key_len && !strncmp(line, key, key_len) &&
|
||||
line[key_len] == '=') {
|
||||
size_t value_len = line_len - key_len - 1;
|
||||
if (value_len >= out_size) value_len = out_size - 1;
|
||||
memcpy(out, line + key_len + 1, value_len);
|
||||
out[value_len] = '\0';
|
||||
return 1;
|
||||
}
|
||||
line = end ? end + 1 : NULL;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int wlan_apply_system_cfg(const char *system_cfg,
|
||||
const uf_model_t *model)
|
||||
{
|
||||
if (!system_cfg || !system_cfg[0])
|
||||
return -1;
|
||||
|
||||
struct json_object *root = json_object_new_object();
|
||||
struct json_object *radios = json_object_new_array();
|
||||
struct json_object *vaps = json_object_new_array();
|
||||
char key[64], value[256];
|
||||
|
||||
for (int i = 1; i <= 4; i++) {
|
||||
snprintf(key, sizeof(key), "radio.%d.ieee_mode", i);
|
||||
if (!system_cfg_get(system_cfg, key, value, sizeof(value)))
|
||||
continue;
|
||||
|
||||
struct json_object *radio = json_object_new_object();
|
||||
const char *band = strstr(value, "11na") ? "na" : "ng";
|
||||
json_object_object_add(radio, "radio",
|
||||
json_object_new_string(band));
|
||||
|
||||
const char *ht = strstr(value, "ht80") ? "HT80" :
|
||||
strstr(value, "ht40") ? "HT40" : "HT20";
|
||||
json_object_object_add(radio, "ht", json_object_new_string(ht));
|
||||
|
||||
snprintf(key, sizeof(key), "radio.%d.channel", i);
|
||||
if (system_cfg_get(system_cfg, key, value, sizeof(value)))
|
||||
json_object_object_add(radio, "channel",
|
||||
json_object_new_int(!strcmp(value, "auto") ? 0 : atoi(value)));
|
||||
|
||||
snprintf(key, sizeof(key), "radio.%d.txpower", i);
|
||||
if (system_cfg_get(system_cfg, key, value, sizeof(value)) &&
|
||||
strcmp(value, "auto"))
|
||||
json_object_object_add(radio, "tx_power",
|
||||
json_object_new_int(atoi(value)));
|
||||
json_object_array_add(radios, radio);
|
||||
}
|
||||
|
||||
for (int i = 1; i <= 32; i++) {
|
||||
snprintf(key, sizeof(key), "aaa.%d.ssid", i);
|
||||
if (!system_cfg_get(system_cfg, key, value, sizeof(value)))
|
||||
continue;
|
||||
|
||||
struct json_object *vap = json_object_new_object();
|
||||
json_object_object_add(vap, "essid",
|
||||
json_object_new_string(value));
|
||||
|
||||
snprintf(key, sizeof(key), "aaa.%d.status", i);
|
||||
if (system_cfg_get(system_cfg, key, value, sizeof(value)) &&
|
||||
strcmp(value, "enabled")) {
|
||||
json_object_put(vap);
|
||||
continue;
|
||||
}
|
||||
|
||||
snprintf(key, sizeof(key), "wireless.%d.parent", i);
|
||||
const char *band = "ng";
|
||||
if (system_cfg_get(system_cfg, key, value, sizeof(value)) &&
|
||||
!strcmp(value, "wifi1"))
|
||||
band = "na";
|
||||
json_object_object_add(vap, "radio", json_object_new_string(band));
|
||||
|
||||
snprintf(key, sizeof(key), "aaa.%d.wpa.psk", i);
|
||||
if (system_cfg_get(system_cfg, key, value, sizeof(value))) {
|
||||
json_object_object_add(vap, "security",
|
||||
json_object_new_string("wpa2psk"));
|
||||
json_object_object_add(vap, "x_passphrase",
|
||||
json_object_new_string(value));
|
||||
} else {
|
||||
json_object_object_add(vap, "security",
|
||||
json_object_new_string("open"));
|
||||
}
|
||||
|
||||
snprintf(key, sizeof(key), "aaa.%d.hide_ssid", i);
|
||||
if (system_cfg_get(system_cfg, key, value, sizeof(value)))
|
||||
json_object_object_add(vap, "hide_ssid",
|
||||
json_object_new_boolean(!strcmp(value, "true")));
|
||||
|
||||
snprintf(key, sizeof(key), "aaa.%d.ft.status", i);
|
||||
if (system_cfg_get(system_cfg, key, value, sizeof(value)))
|
||||
json_object_object_add(vap, "fast_roaming_enabled",
|
||||
json_object_new_boolean(!strcmp(value, "enabled")));
|
||||
|
||||
snprintf(key, sizeof(key), "aaa.%d.pmf.mode", i);
|
||||
if (system_cfg_get(system_cfg, key, value, sizeof(value))) {
|
||||
const char *pmf = !strcmp(value, "2") ? "required" :
|
||||
!strcmp(value, "1") ? "optional" : "disabled";
|
||||
json_object_object_add(vap, "pmf_mode",
|
||||
json_object_new_string(pmf));
|
||||
}
|
||||
|
||||
snprintf(key, sizeof(key), "aaa.%d.br.devname", i);
|
||||
if (system_cfg_get(system_cfg, key, value, sizeof(value))) {
|
||||
const char *dot = strrchr(value, '.');
|
||||
if (dot && atoi(dot + 1) > 0)
|
||||
json_object_object_add(vap, "vlan_id",
|
||||
json_object_new_int(atoi(dot + 1)));
|
||||
}
|
||||
json_object_array_add(vaps, vap);
|
||||
}
|
||||
|
||||
json_object_object_add(root, "radio_table", radios);
|
||||
json_object_object_add(root, "vap_table", vaps);
|
||||
printf("[openuf] Parsed legacy system_cfg: %zu radios, %zu VAPs\n",
|
||||
json_object_array_length(radios), json_object_array_length(vaps));
|
||||
int result = wlan_apply_config(root, model);
|
||||
json_object_put(root);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
wlan_get_vap_table — leer VAPs activas desde UCI
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
|
||||
Itera todas las wifi-iface con prefijo "openuf_" en /etc/config/wireless
|
||||
y construye el JSON vap_table para incluir en el payload inform.
|
||||
|
||||
Campos que leemos de UCI → campos en el JSON:
|
||||
ssid → essid
|
||||
device → (usado para buscar radio y BSSID)
|
||||
encryption → security (via sec_to_unifi)
|
||||
hidden → hide_ssid
|
||||
ieee80211r → fast_roaming_enabled
|
||||
ieee80211k → band_steering
|
||||
ieee80211w → pmf_mode ("disabled"/"optional"/"required")
|
||||
disabled → up (inverso)
|
||||
|
||||
También intentamos leer el BSSID real de la interfaz wlan
|
||||
desde /sys/class/net/<iface>/address.
|
||||
*/
|
||||
struct json_object *wlan_get_vap_table(const uf_model_t *model)
|
||||
{
|
||||
struct json_object *arr = json_object_new_array();
|
||||
|
||||
struct uci_context *ctx = uci_alloc_context();
|
||||
if (!ctx) return arr;
|
||||
|
||||
struct uci_package *pkg = NULL;
|
||||
if (uci_load(ctx, "wireless", &pkg) != UCI_OK) {
|
||||
uci_free_context(ctx);
|
||||
return arr;
|
||||
}
|
||||
|
||||
struct uci_element *e;
|
||||
uci_foreach_element(&pkg->sections, e) {
|
||||
struct uci_section *sec = uci_to_section(e);
|
||||
if (strcmp(sec->type, "wifi-iface") != 0) continue;
|
||||
/* Solo reportar VAPs gestionadas por openuf */
|
||||
if (strncmp(sec->e.name, "openuf_", 7) != 0) continue;
|
||||
|
||||
#define UCI_GET(opt) uci_lookup_option_string(ctx, sec, opt)
|
||||
|
||||
const char *ssid = UCI_GET("ssid");
|
||||
const char *device = UCI_GET("device");
|
||||
const char *enc = UCI_GET("encryption");
|
||||
const char *dis = UCI_GET("disabled");
|
||||
const char *r11 = UCI_GET("ieee80211r");
|
||||
const char *k11 = UCI_GET("ieee80211k");
|
||||
const char *w11 = UCI_GET("ieee80211w");
|
||||
const char *hidden = UCI_GET("hidden");
|
||||
|
||||
if (!ssid) ssid = "";
|
||||
if (!device) device = "radio0";
|
||||
|
||||
/* Banda de este radio */
|
||||
const char *radio_band = "ng";
|
||||
for (int j = 0; j < model->radio_map_len; j++) {
|
||||
if (!strcmp(model->radio_map[j].device, device)) {
|
||||
radio_band = model->radio_map[j].band;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Nombre de la interfaz wlan (wlan0 para radio0, etc.) */
|
||||
char wlan_iface[32] = "wlan0";
|
||||
int ridx = 0;
|
||||
sscanf(device, "radio%d", &ridx);
|
||||
snprintf(wlan_iface, sizeof(wlan_iface), "wlan%d", ridx);
|
||||
|
||||
/* Leer BSSID real desde sysfs */
|
||||
char bssid[32] = "00:00:00:00:00:00";
|
||||
{
|
||||
char path[128];
|
||||
snprintf(path, sizeof(path), "/sys/class/net/%s/address", wlan_iface);
|
||||
FILE *f = fopen(path, "r");
|
||||
if (f) {
|
||||
fgets(bssid, sizeof(bssid), f); fclose(f);
|
||||
bssid[strcspn(bssid, "\r\n")] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
/* PMF: ieee80211w → "disabled"/"optional"/"required" */
|
||||
const char *pmf = "disabled";
|
||||
if (w11) {
|
||||
if (!strcmp(w11,"1")) pmf = "optional";
|
||||
if (!strcmp(w11,"2")) pmf = "required";
|
||||
}
|
||||
|
||||
bool ft_on = (r11 && !strcmp(r11,"1"));
|
||||
bool bs_on = (k11 && !strcmp(k11,"1"));
|
||||
bool hid = (hidden && !strcmp(hidden,"1"));
|
||||
bool up = !(dis && !strcmp(dis,"1"));
|
||||
|
||||
struct json_object *o = json_object_new_object();
|
||||
json_object_object_add(o, "essid", json_object_new_string(ssid));
|
||||
json_object_object_add(o, "bssid", json_object_new_string(bssid));
|
||||
json_object_object_add(o, "name", json_object_new_string(sec->e.name));
|
||||
json_object_object_add(o, "radio", json_object_new_string(radio_band));
|
||||
json_object_object_add(o, "security", json_object_new_string(sec_to_unifi(enc)));
|
||||
json_object_object_add(o, "up", json_object_new_boolean(up));
|
||||
json_object_object_add(o, "hide_ssid", json_object_new_boolean(hid));
|
||||
json_object_object_add(o, "fast_roaming_enabled",json_object_new_boolean(ft_on));
|
||||
json_object_object_add(o, "band_steering", json_object_new_boolean(bs_on));
|
||||
json_object_object_add(o, "pmf_mode", json_object_new_string(pmf));
|
||||
json_object_object_add(o, "num_sta", json_object_new_int(0));
|
||||
json_object_array_add(arr, o);
|
||||
#undef UCI_GET
|
||||
}
|
||||
|
||||
uci_unload(ctx, pkg);
|
||||
uci_free_context(ctx);
|
||||
return arr;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
#ifndef OPENUF_WLAN_H
|
||||
#define OPENUF_WLAN_H
|
||||
|
||||
#include <json-c/json.h>
|
||||
#include "ufmodel.h"
|
||||
|
||||
/*
|
||||
* Translate UniFi WLAN/VAP config into OpenWrt UCI wireless settings.
|
||||
* All openuf-managed interfaces are named openuf_NN_<ssid> so they
|
||||
* can be safely removed on re-provision.
|
||||
*/
|
||||
|
||||
/* Remove all UCI wifi-iface sections whose name starts with "openuf_" */
|
||||
void wlan_clear(void);
|
||||
|
||||
/* Apply radio-level settings from a UniFi radio_table entry.
|
||||
* radio_json : JSON object with fields: channel, ht, tx_power
|
||||
* device_name: OpenWrt radio device ("radio0", "radio1") */
|
||||
void wlan_apply_radio(struct json_object *radio_json,
|
||||
const char *device_name);
|
||||
|
||||
/* Apply full config pushed by controller (setstate).
|
||||
* config_json: decoded setstate JSON object
|
||||
* model : model descriptor for radio_map lookup */
|
||||
int wlan_apply_config(struct json_object *config_json,
|
||||
const uf_model_t *model);
|
||||
|
||||
/* Apply the legacy newline-separated system_cfg format. */
|
||||
int wlan_apply_system_cfg(const char *system_cfg,
|
||||
const uf_model_t *model);
|
||||
|
||||
/* Build vap_table JSON array from current UCI state.
|
||||
* Caller owns returned json_object. */
|
||||
struct json_object *wlan_get_vap_table(const uf_model_t *model);
|
||||
|
||||
#endif /* OPENUF_WLAN_H */
|
||||
Reference in New Issue
Block a user