added devcontainer

This commit is contained in:
2026-07-10 15:44:09 +02:00
parent 5da43ff0f3
commit 356837ee0a
31 changed files with 4828 additions and 1 deletions
+36
View File
@@ -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
+16
View File
@@ -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"
}
+5
View File
@@ -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
+61
View File
@@ -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))
+45
View File
@@ -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
+92 -1
View File
@@ -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
```
+17
View File
@@ -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
+27
View File
@@ -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
View File
@@ -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 0x170x1a) */
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;
}
}
+34
View File
@@ -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
View File
@@ -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;
}
+83
View File
@@ -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 */
+39
View File
@@ -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);
}
+44
View File
@@ -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
View File
@@ -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;
}
+45
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+73
View File
@@ -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 */
+81
View File
@@ -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
View File
@@ -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
View File
@@ -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 */