Compare commits

..
16 Commits
Author SHA1 Message Date
KodaandKoda c7687fd35d added log Timestamps (#42)
Generate and publish developer Wiki / publish-wiki (push) Canceled after 0s
Generate and publish developer Wiki / validate-wiki (push) Canceled after 9s
Build and publish release / create-release (push) Canceled after 0s
Build and publish release / build (push) Canceled after 0s
Build and publish release / publish-release (push) Canceled after 0s
Build and publish release / metadata (push) Canceled after 10s
Reviewed-on: #42
Co-authored-by: Koda YeenBean <n122330@gmail.com>
2026-08-07 14:55:38 +01:00
KodaandKoda 308803053c fixing release pipeline (#40)
Generate and publish developer Wiki / validate-wiki (push) Successful in 21s
Build and publish release / metadata (push) Successful in 45s
Generate and publish developer Wiki / publish-wiki (push) Successful in 12s
Build and publish release / create-release (push) Successful in 55s
Build and publish release / build (push) Failing after 23m29s
Build and publish release / publish-release (push) Skipped
Reviewed-on: #40
Co-authored-by: Koda YeenBean <n122330@gmail.com>
2026-07-20 00:03:28 +01:00
KodaandKoda 21dda2e618 fixing wiki generation some more (#39)
Generate and publish developer Wiki / validate-wiki (push) Successful in 7s
Build and publish release / create-release (push) Successful in 1m4s
Build and publish release / build (push) Failing after 31m41s
Build and publish release / publish-release (push) Skipped
Generate and publish developer Wiki / publish-wiki (push) Successful in 11s
Build and publish release / metadata (push) Successful in 50s
Reviewed-on: #39
Co-authored-by: Koda YeenBean <n122330@gmail.com>
2026-07-19 22:48:38 +01:00
KodaandKoda 4f30a031bb fixing wiki generation (#38)
Build and publish release / metadata (push) Canceled after 0s
Build and publish release / create-release (push) Canceled after 0s
Build and publish release / build (push) Canceled after 0s
Build and publish release / publish-release (push) Canceled after 0s
Generate and publish developer Wiki / generated-wiki (push) Failing after 43s
Reviewed-on: #38
Co-authored-by: Koda YeenBean <n122330@gmail.com>
2026-07-19 22:34:26 +01:00
KodaandKoda 8bf3e03de8 move wiki to wiki section (#37)
Build and publish release / build (push) Canceled after 0s
Build and publish release / publish-release (push) Canceled after 0s
Build and publish release / metadata (push) Canceled after 0s
Build and publish release / create-release (push) Canceled after 0s
Reviewed-on: #37
Co-authored-by: Koda YeenBean <n122330@gmail.com>
2026-07-19 22:05:05 +01:00
KodaandKoda 3fcd99af1f refactor #1 + adding documentation (#36)
Build and publish release / metadata (push) Successful in 44s
Build and publish release / create-release (push) Successful in 49s
Build and publish release / publish-release (push) Canceled after 0s
Build and publish release / build (push) Canceled after 7m44s
Reviewed-on: #36
Co-authored-by: Koda YeenBean <n122330@gmail.com>
2026-07-19 21:26:28 +01:00
KodaandKoda db81260d7f fixing release pipeline (#35)
Build and publish release / metadata (push) Successful in 38s
Build and publish release / create-release (push) Successful in 50s
Build and publish release / build (push) Failing after 31m10s
Build and publish release / publish-release (push) Skipped
Reviewed-on: #35
Co-authored-by: Koda YeenBean <n122330@gmail.com>
2026-07-15 22:48:18 +01:00
KodaandKoda f4d433d8ae fixing release pipeline (#34)
Build and publish release / metadata (push) Successful in 48s
Build and publish release / create-release (push) Successful in 45s
Build and publish release / build (push) Failing after 22m11s
Build and publish release / publish-release (push) Skipped
Reviewed-on: #34
Co-authored-by: Koda YeenBean <n122330@gmail.com>
2026-07-15 22:16:29 +01:00
KodaandKoda 2cee24db86 fixing release pipeline (#33)
Build and publish release / metadata (push) Successful in 40s
Build and publish release / create-release (push) Successful in 52s
Build and publish release / build (push) Failing after 23m41s
Build and publish release / publish-release (push) Skipped
Reviewed-on: #33
Co-authored-by: Koda YeenBean <n122330@gmail.com>
2026-07-15 21:40:24 +01:00
Koda b8f6f31f4d fixing release pipeline again, but like for real this time (#32)
Build and publish release / metadata (push) Successful in 36s
Build and publish release / create-release (push) Successful in 42s
Build and publish release / build (push) Failing after 23m31s
Build and publish release / publish-release (push) Skipped
Reviewed-on: #32
2026-07-15 20:49:09 +01:00
Koda c406d20ac2 fixing release pipeline again (#31)
Build and publish release / metadata (push) Successful in 34s
Build and publish release / create-release (push) Successful in 50s
Build and publish release / build (push) Failing after 4m34s
Build and publish release / publish-release (push) Skipped
Reviewed-on: #31
2026-07-15 20:37:15 +01:00
Koda 208b49b512 fixing release pipeline (#30)
Build and publish release / metadata (push) Successful in 35s
Build and publish release / create-release (push) Successful in 53s
Build and publish release / build (push) Failing after 10m49s
Build and publish release / publish-release (push) Skipped
Reviewed-on: #30
2026-07-15 20:14:27 +01:00
Koda a0ee657a02 Added Release Pipeline (#29)
Build and publish release / metadata (push) Successful in 42s
Build and publish release / create-release (push) Successful in 55s
Build and publish release / build (push) Failing after 4m11s
Build and publish release / publish-release (push) Skipped
Reviewed-on: #29
2026-07-15 19:39:51 +01:00
Koda 37c286e756 enable wifi version steering (#28)
Reviewed-on: #28
2026-07-14 23:38:46 +01:00
Koda b52a0a165d adding more metric reporting (#27)
Reviewed-on: #27
2026-07-14 23:09:21 +01:00
Koda b014e67753 reboot action reboots Accesspoint (#26)
Reviewed-on: #26
2026-07-14 21:17:59 +01:00
37 changed files with 4946 additions and 3118 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"openwrt_release": "25.12.1",
"version": { "major": 0, "minor": 4 },
"targets": [
{ "architecture": "x64", "target": "x86", "subtarget": "64" },
{ "architecture": "x86", "target": "x86", "subtarget": "generic" },
{ "architecture": "arm64", "target": "armsr", "subtarget": "armv8" },
{ "architecture": "arm32", "target": "armsr", "subtarget": "armv7" },
{ "architecture": "mpc85xx", "target": "mpc85xx", "subtarget": "p1020" }
]
}
+75
View File
@@ -0,0 +1,75 @@
name: Generate and publish developer Wiki
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: openunifi-wiki
cancel-in-progress: false
permissions:
code: read
jobs:
validate-wiki:
runs-on: ubuntu-latest
steps:
- name: Check out source repository
uses: actions/checkout@v4
with:
fetch-depth: 2
path: source
- name: Check patch whitespace
run: git -C source diff --check HEAD^ HEAD
- name: Validate documentation generator
run: |
mkdir -p "$RUNNER_TEMP/openuf-wiki"
python3 source/scripts/docs/generate-wiki.py --output "$RUNNER_TEMP/openuf-wiki"
python3 source/scripts/docs/generate-wiki.py --check --output "$RUNNER_TEMP/openuf-wiki"
publish-wiki:
if: gitea.event_name != 'pull_request'
needs: validate-wiki
runs-on: ubuntu-latest
permissions:
code: read
wiki: write
steps:
- name: Check out source repository
uses: actions/checkout@v4
with:
path: source
- name: Clone Wiki Git repository
env:
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_SERVER_URL: ${{ gitea.server_url }}
run: |
set -eu
repository_owner=${GITEA_REPOSITORY%%/*}
repository_name=${GITEA_REPOSITORY##*/}
wiki_name=$(printf '%s' "$repository_name" | tr '[:upper:]' '[:lower:]')
wiki_url="${GITEA_SERVER_URL%/}/${repository_owner}/${wiki_name}.wiki.git"
git clone -- "$wiki_url" wiki
- name: Generate and verify Gitea Wiki pages
run: |
python3 source/scripts/docs/generate-wiki.py --output wiki
python3 source/scripts/docs/generate-wiki.py --check --output wiki
- name: Publish changed Wiki pages
working-directory: wiki
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -eu
git config user.name "openUF documentation bot"
git config user.email "actions@openuf.invalid"
git add -- Home.md Developer-Guide.md Call-Graph.md Runtime-Flow.md _Sidebar.md
if git diff --cached --quiet; then
echo "Gitea Wiki is already current"
exit 0
fi
git commit -m "docs: update generated developer wiki"
basic_auth=$(printf 'x-access-token:%s' "$GITEA_TOKEN" | base64 | tr -d '\n')
git -c http.extraHeader="Authorization: Basic $basic_auth" push origin HEAD
+112
View File
@@ -0,0 +1,112 @@
name: Build and publish release
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: openunifi-release
cancel-in-progress: false
permissions:
code: read
releases: write
jobs:
metadata:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.release.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Validate documentation generator
run: |
mkdir -p "$RUNNER_TEMP/openuf-wiki"
python3 ./scripts/docs/generate-wiki.py --output "$RUNNER_TEMP/openuf-wiki"
python3 ./scripts/docs/generate-wiki.py --check --output "$RUNNER_TEMP/openuf-wiki"
- name: Install metadata dependencies
run: sudo apt-get update && sudo apt-get install --yes jq
- name: Read release configuration
id: release
shell: bash
run: |
set -euo pipefail
major="$(jq -er '.version.major | select(type == "number" and floor == . and . >= 0)' .gitea/release-targets.json)"
minor="$(jq -er '.version.minor | select(type == "number" and floor == . and . >= 0)' .gitea/release-targets.json)"
tag_prefix="v${major}.${minor}."
latest_patch=-1
while IFS= read -r tag; do
candidate="${tag#"$tag_prefix"}"
if [[ $candidate =~ ^(0|[1-9][0-9]*)$ ]] && (( candidate > latest_patch )); then
latest_patch=$candidate
fi
done < <(git tag --list "${tag_prefix}*")
patch=$((latest_patch + 1))
version="${major}.${minor}.${patch}"
jq -e '
(.targets | length > 0) and
([.targets[].architecture] | length == (unique | length)) and
all(.targets[]; (.architecture | type == "string" and test("^[A-Za-z0-9_-]+$")))
' .gitea/release-targets.json >/dev/null
printf 'version=%s\n' "$version" >>"$GITHUB_OUTPUT"
create-release:
needs: metadata
runs-on: ubuntu-latest
outputs:
release-id: ${{ steps.create.outputs.release-id }}
steps:
- uses: actions/checkout@v4
- name: Install release dependencies
run: sudo apt-get update && sudo apt-get install --yes curl jq
- name: Create draft release
id: create
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
VERSION: ${{ needs.metadata.outputs.version }}
run: |
release_id="$(scripts/ci/gitea-release.sh create)"
printf 'release-id=%s\n' "$release_id" >>"$GITHUB_OUTPUT"
build:
needs: [metadata, create-release]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install SDK build dependencies
shell: bash
run: |
sudo apt-get update
packages=(build-essential clang flex bison g++ gawk gettext git libncurses-dev libssl-dev python3 rsync unzip zlib1g-dev file wget patch time curl jq zstd)
case "$(uname -m)" in
aarch64|arm64|armv7l|armv8l)
packages+=(qemu-user-static)
;;
esac
sudo apt-get install --yes "${packages[@]}"
- name: Build and upload configured packages
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
RELEASE_ID: ${{ needs.create-release.outputs.release-id }}
VERSION: ${{ needs.metadata.outputs.version }}
shell: bash
run: |
set -euo pipefail
while IFS= read -r architecture; do
scripts/ci/build-release.sh .gitea/release-targets.json "$architecture" "$VERSION" dist
ASSET="dist/openUniFi-${architecture}-${VERSION}.apk" scripts/ci/gitea-release.sh upload
done < <(jq -r '.targets[].architecture' .gitea/release-targets.json)
publish-release:
needs: [metadata, create-release, build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Publish completed release
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
RELEASE_ID: ${{ needs.create-release.outputs.release-id }}
run: scripts/ci/gitea-release.sh publish
+19 -3
View File
@@ -19,16 +19,18 @@ threading or event framework; the main loop runs once per second.
| Lifecycle and scheduling | `src/main.c` | Load config/state, choose model, run announce/inform/LLDP timers | | Lifecycle and scheduling | `src/main.c` | Load config/state, choose model, run announce/inform/LLDP timers |
| User configuration | `src/config.[ch]`, `files/openuf.conf` | Parse static daemon settings and defaults | | User configuration | `src/config.[ch]`, `files/openuf.conf` | Parse static daemon settings and defaults |
| Persistent controller state | `src/state.[ch]` | Read/write `/etc/openuf/state.json` | | Persistent controller state | `src/state.[ch]` | Read/write `/etc/openuf/state.json` |
| Inform/adoption protocol | `src/inform.[ch]` | Build telemetry, encode TNBU packets, handle controller commands | | Inform exchange | `src/inform/inform.c`, `src/inform.h` | Orchestrate payload, TNBU, HTTP, and response stages |
| Inform internals | `src/inform/{payload,packet,response}.c` | Build telemetry, encode TNBU packets, handle commands |
| Transport and encryption | `src/http.[ch]`, `src/crypto.[ch]` | Raw HTTP/1.0 client and AES-CBC/AES-GCM helpers | | Transport and encryption | `src/http.[ch]`, `src/crypto.[ch]` | Raw HTTP/1.0 client and AES-CBC/AES-GCM helpers |
| WiFi provisioning | `src/wlan.[ch]` | Translate controller config to UCI and report VAP state | | WiFi provisioning | `src/wlan/{provision,legacy,radio,uci}.c`, `src/wlan.h` | Translate controller config to UCI |
| WiFi telemetry/helpers | `src/wlan/{telemetry,common}.c` | Report VAP state and share translations |
| Device telemetry | `src/sysinfo.[ch]`, `src/clients.[ch]` | Read proc/sysfs/`iw`/bridge/dnsmasq data | | Device telemetry | `src/sysinfo.[ch]`, `src/clients.[ch]` | Read proc/sysfs/`iw`/bridge/dnsmasq data |
| Discovery/topology | `src/announce.[ch]`, `src/lldp.[ch]` | UniFi UDP announce and LLDP send/read | | Discovery/topology | `src/announce.[ch]`, `src/lldp.[ch]` | UniFi UDP announce and LLDP send/read |
| Emulated hardware | `src/models.c`, `src/ufmodel.h` | Model registry, radios, ports, and band-to-radio mapping | | Emulated hardware | `src/models.c`, `src/ufmodel.h` | Model registry, radios, ports, and band-to-radio mapping |
| Packaging/service | `Makefile`, `files/openuf.init` | OpenWrt package recipe and procd service | | Packaging/service | `Makefile`, `files/openuf.init` | OpenWrt package recipe and procd service |
Runtime flow: `main()` -> `inform_send()` -> build telemetry -> encrypt -> Runtime flow: `main()` -> `inform_send()` -> build telemetry -> encrypt ->
`http_post()` -> decrypt response -> `handle_response()` -> optionally `http_post()` -> decrypt response -> `inform_handle_response()` -> optionally
`wlan_apply_config()`/`wlan_apply_system_cfg()` and `state_save()`. `wlan_apply_config()`/`wlan_apply_system_cfg()` and `state_save()`.
## Important invariants ## Important invariants
@@ -70,6 +72,15 @@ make package/OpenUniFi/compile
`Makefile.standalone` is for compiling on an OpenWrt device with development `Makefile.standalone` is for compiling on an OpenWrt device with development
packages installed; it is not a host-side substitute for the cross-build. packages installed; it is not a host-side substitute for the cross-build.
Architecture pages for the Gitea Wiki are generated from the recursive C source
tree into a separate checkout. After source or architecture changes, run:
```sh
./scripts/docs/generate-wiki.py --output /path/to/openunifi.wiki
./scripts/docs/generate-wiki.py --check --output /path/to/openunifi.wiki
```
There is currently no automated test suite. For documentation-only changes, There is currently no automated test suite. For documentation-only changes,
check paths and commands against the source and package Makefile. For C changes, check paths and commands against the source and package Makefile. For C changes,
the cross-build is the minimum verification; report any device/controller the cross-build is the minimum verification; report any device/controller
@@ -103,6 +114,11 @@ behavior that still needs manual testing.
- Ordinary read-only commands may fail with the same `bwrap` namespace error. - Ordinary read-only commands may fail with the same `bwrap` namespace error.
When the command is safe and required, rerun it using the environment's normal When the command is safe and required, rerun it using the environment's normal
escalation/approval mechanism instead of investigating the namespace setup. escalation/approval mechanism instead of investigating the namespace setup.
- OpenWrt's release SDK host tools and preload libraries are x86-64. When using
them through QEMU on an ARM runner, never export the SDK's fakeroot library as
host `LD_PRELOAD`; carry it in a private variable and pass it to the bundled
x86 loader with `--preload`. Validate changes with an emulated build through
mbedTLS packaging, where a broken handoff appears as a `library/...` dependency.
Agents must preserve useful environment knowledge for later agents. When a new Agents must preserve useful environment knowledge for later agents. When a new
pitfall or workaround is verified to be deterministic or repeatedly encountered, pitfall or workaround is verified to be deterministic or repeatedly encountered,
+15 -14
View File
@@ -26,27 +26,28 @@ endef
define Build/Prepare define Build/Prepare
mkdir -p $(PKG_BUILD_DIR) mkdir -p $(PKG_BUILD_DIR)
$(CP) ./src/* $(PKG_BUILD_DIR)/ $(CP) ./src/. $(PKG_BUILD_DIR)/
endef endef
TARGET_CFLAGS += -I$(STAGING_DIR)/usr/include -DENABLE_LOGGING=1 OPENUF_SRCS := \
main.c config.c state.c crypto.c http.c announce.c \
inform/inform.c inform/payload.c inform/packet.c inform/response.c \
wlan/common.c wlan/uci.c wlan/radio.c wlan/provision.c \
wlan/legacy.c wlan/telemetry.c \
sysinfo.c clients.c lldp.c models.c
TARGET_CFLAGS += \
-I$(STAGING_DIR)/usr/include \
-I$(PKG_BUILD_DIR) \
-I$(PKG_BUILD_DIR)/inform \
-I$(PKG_BUILD_DIR)/wlan \
-DENABLE_LOGGING=1
TARGET_LDFLAGS += -lmbedtls -lmbedcrypto -luci -ljson-c TARGET_LDFLAGS += -lmbedtls -lmbedcrypto -luci -ljson-c
define Build/Compile define Build/Compile
$(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_LDFLAGS) \ $(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_LDFLAGS) \
-o $(PKG_BUILD_DIR)/openuf \ -o $(PKG_BUILD_DIR)/openuf \
$(PKG_BUILD_DIR)/main.c \ $(addprefix $(PKG_BUILD_DIR)/,$(OPENUF_SRCS))
$(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 endef
define Package/openuf/install define Package/openuf/install
+11 -3
View File
@@ -10,7 +10,7 @@
# make -f Makefile.standalone install # make -f Makefile.standalone install
CC = gcc CC = gcc
CFLAGS = -Wall -Wextra -O2 -I/usr/include -DENABLE_LOGGING=1 CFLAGS = -Wall -Wextra -O2 -I/usr/include -Isrc -Isrc/inform -Isrc/wlan -DENABLE_LOGGING=1
LDFLAGS = -lmbedtls -lmbedcrypto -luci -ljson-c LDFLAGS = -lmbedtls -lmbedcrypto -luci -ljson-c
SRCS = src/main.c \ SRCS = src/main.c \
@@ -19,8 +19,16 @@ SRCS = src/main.c \
src/crypto.c \ src/crypto.c \
src/http.c \ src/http.c \
src/announce.c \ src/announce.c \
src/inform.c \ src/inform/inform.c \
src/wlan.c \ src/inform/payload.c \
src/inform/packet.c \
src/inform/response.c \
src/wlan/common.c \
src/wlan/uci.c \
src/wlan/radio.c \
src/wlan/provision.c \
src/wlan/legacy.c \
src/wlan/telemetry.c \
src/sysinfo.c \ src/sysinfo.c \
src/clients.c \ src/clients.c \
src/lldp.c \ src/lldp.c \
+73 -8
View File
@@ -7,17 +7,17 @@ Daemon that makes an OpenWrt router appear as a UniFi AP to UniFi Network contro
| Feature | Description | Implementation | | Feature | Description | Implementation |
| --- | --- | --- | | --- | --- | --- |
| **L2 Discovery** | UDP broadcast + multicast every 10s | `announce.c` → port 10001 | | **L2 Discovery** | UDP broadcast + multicast every 10s | `announce.c` → port 10001 |
| **Adoption** | AES-128-CBC handshake with the controller | `inform.c``handle_response()` | | **Adoption** | AES-128-CBC/GCM handshake with the controller | `inform/response.c``inform_handle_response()` |
| **Firmware spoofing** | Persists and reports the target version requested by an upgrade | `inform.c``handle_response()` | | **Remote reboot** | Reboots OpenWrt when requested by the controller | `inform/response.c``inform_handle_response()` |
| **WiFi Config** | Creates WiFi networks from the controller via UCI | `wlan.c``wlan_apply_config()` | | **Firmware spoofing** | Persists and reports the target version requested by an upgrade | `inform/response.c``inform_handle_response()` |
| **Band Steering** | 802.11k/v Neighbor Reports + BSS Transition | `wlan.c``apply_vap()` | | **WiFi Config** | Creates WiFi networks from the controller via UCI | `wlan/provision.c``wlan_apply_config()` |
| **Fast Roaming** | 802.11r FT with mobility_domain derived from MAC | `wlan.c``apply_vap()` | | **Band Steering** | 802.11k/v Neighbor Reports + BSS Transition | `wlan/provision.c` and `wlan/uci.c` |
| **WPA3 / PMF** | SAE, SAE-mixed, 802.11w 0/1/2 | `wlan.c``sec_to_uci()` | | **Fast Roaming** | 802.11r FT with a stable SSID mobility domain | `wlan/provision.c``apply_vap()` |
| **WPA3 / PMF** | SAE, SAE-mixed, 802.11w 0/1/2 | `wlan/common.c` and `wlan/provision.c` |
| **WiFi Clients** | MAC, signal, bitrate, bytes per VAP | `clients.c``iw station dump` | | **WiFi Clients** | MAC, signal, bitrate, bytes per VAP | `clients.c``iw station dump` |
| **Wired Clients** | MACs from bridge FDB | `clients.c``bridge fdb` | | **Wired Clients** | MACs from bridge FDB | `clients.c``bridge fdb` |
| **CPU / RAM** | Real-time usage | `sysinfo.c``/proc/stat` + `/proc/meminfo` | | **CPU / RAM** | Real-time usage | `sysinfo.c``/proc/stat` + `/proc/meminfo` |
| **Interfaces** | Speed, duplex, rx/tx counters | `sysinfo.c``/proc/net/dev` | | **Interfaces** | Speed, duplex, rx/tx counters | `sysinfo.c``/proc/net/dev` |
| **Wired Uplink** | Link state, speed, duplex, and counters for topology | `inform.c``uplink` |
| **Channel / RF** | Channel utilization, noise, tx_power | `sysinfo.c``iw survey dump` | | **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 Send** | Custom frames via AF_PACKET raw socket | `lldp.c``lldp_send_frame()` |
| **LLDP Read** | Neighbors for UniFi topology | `lldp.c``lldpctl -f json` | | **LLDP Read** | Neighbors for UniFi topology | `lldp.c``lldpctl -f json` |
@@ -46,6 +46,71 @@ make -f Makefile.standalone
Contributors and AI agents should read [`AGENTS.md`](AGENTS.md) for the concise Contributors and AI agents should read [`AGENTS.md`](AGENTS.md) for the concise
architecture map, invariants, and validation checklist. architecture map, invariants, and validation checklist.
## Developer documentation and Gitea Wiki
The publish-ready pages live in the separate `Koda/openunifi.wiki` repository.
They include a developer guide, a generated Mermaid function-call graph, and
runtime flow charts. Check out that repository, then refresh it after source or
architecture changes:
```shell
./scripts/docs/generate-wiki.py --output /path/to/openunifi.wiki
```
Use the same tool as a linter to reject stale pages, oversized C modules,
missing documented entry points, or known non-English comment fragments:
```shell
./scripts/docs/generate-wiki.py --check --output /path/to/openunifi.wiki
```
The generator uses only the Python 3 standard library, and the publisher uses
POSIX shell utilities and Git. Neither downloads or executes an x86-only helper,
so both run natively on ARM and x86 build agents.
Publishing is an explicit authenticated step: `./scripts/docs/publish-wiki.sh`.
The script derives the lowercase `.wiki.git` repository URL from `origin`,
checks it out before generating, and accepts an explicit URL override. No
credentials are stored in this repository. The documentation workflow clones the
Wiki Git endpoint directly because it is not a normal API repository. Its
publishing job requests Gitea's `wiki: write` permission for `GITEA_TOKEN` and
passes the token through a transient HTTP authentication header. Repository
Actions settings must allow Wiki write access because Gitea clamps requested
job permissions to the configured maximum.
## Automated releases
Every push to `main` runs `.gitea/workflows/release.yaml`. It builds with pinned
OpenWrt SDKs and publishes `openUniFi-<architecture>-<version>.apk` for x64,
x86, arm64, arm32, and mpc85xx. The workflow uses Gitea's built-in
`GITEA_TOKEN`; repository Actions settings must allow it `write` access to
releases. A failed target leaves the release as a draft rather than publishing
an incomplete set.
Targets and the pinned OpenWrt release live in `.gitea/release-targets.json`.
Add an object with a unique `architecture` label and a valid OpenWrt
`target`/`subtarget` pair to extend the build list; no workflow change is
required. The generic ARM entries select ARMv7 and ARMv8 ABIs; use a
device-specific OpenWrt target when necessary.
OpenWrt publishes its release SDKs with x86-64 host tools, even when the target
is ARM or PowerPC. On an ARM build agent, the workflow installs
`qemu-user-static` and the build script runs the SDK host tools explicitly
through `qemu-x86_64-static`; privileged `binfmt_misc` registration is not
required. Native x86-64 agents skip emulation and remain faster.
Releases use the next available `MAJOR.MINOR.PATCH` tag. `MAJOR` and `MINOR`
live in the release config; the workflow finds the highest existing
`vMAJOR.MINOR.PATCH` tag and increments its patch, starting at `0` when a new
major or minor line is introduced. For example, changing the config from `0.4`
to `0.5` makes the next release `0.5.0`. Release runs are serialized so two
merges cannot select the same patch version.
Alternatives are Conventional Commits with a semantic-release tool (best when
not every merge must release), or a manually maintained `VERSION` file bumped
in every pull request (simple, but a forgotten bump blocks the release). Change
`major` for incompatible changes and `minor` for compatible features.
## Changing Compiler Settings ## Changing Compiler Settings
```shell ```shell
@@ -76,7 +141,7 @@ apk del wpad-basic-mbedtls && apk add wpad-mbedtls && /etc/init.d/network restar
apk -r del openuf apk -r del openuf
# Install new version # Install new version
apk add openuf-0.4.0-r1.apk --allow-untrusted apk add openuf-0.4.0-r3.apk --allow-untrusted
# Configure # Configure
vi /etc/openuf/openuf.conf vi /etc/openuf/openuf.conf
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 4 ]]; then
echo "usage: $0 CONFIG ARCHITECTURE VERSION OUTPUT_DIR" >&2
exit 2
fi
config=$1
architecture=$2
version=$3
output_dir=$4
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
[[ $architecture =~ ^[a-zA-Z0-9_-]+$ ]] || { echo "invalid architecture name: $architecture" >&2; exit 2; }
[[ $version =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || { echo "invalid semantic version: $version" >&2; exit 2; }
matches=$(jq --arg architecture "$architecture" '[.targets[] | select(.architecture == $architecture)] | length' "$config")
[[ $matches -eq 1 ]] || { echo "architecture must occur exactly once in $config: $architecture" >&2; exit 2; }
openwrt_release=$(jq -er '.openwrt_release' "$config")
target=$(jq -er --arg architecture "$architecture" '.targets[] | select(.architecture == $architecture) | .target' "$config")
subtarget=$(jq -er --arg architecture "$architecture" '.targets[] | select(.architecture == $architecture) | .subtarget' "$config")
download_base="https://downloads.openwrt.org/releases/${openwrt_release}/targets/${target}/${subtarget}"
sdk_archive=$(curl -fsSL "${download_base}/" | grep -o "openwrt-sdk-${openwrt_release}-[^\"]*\.tar\.zst" | head -n 1)
[[ -n $sdk_archive ]] || { echo "no SDK found at $download_base" >&2; exit 1; }
work_dir="${RUNNER_TEMP:-/tmp}/openunifi-${architecture}-${version}"
rm -rf -- "$work_dir"
mkdir -p -- "$work_dir" "$repo_root/$output_dir"
curl -fL --retry 3 --output "$work_dir/$sdk_archive" "$download_base/$sdk_archive"
expected_sha=$(curl -fsSL "$download_base/sha256sums" | awk -v archive="$sdk_archive" '{ name=$2; sub(/^\*/, "", name); if (name == archive) { print $1; exit } }')
[[ -n $expected_sha ]] || { echo "no checksum found for $sdk_archive" >&2; exit 1; }
printf '%s %s\n' "$expected_sha" "$work_dir/$sdk_archive" | sha256sum --check
tar --zstd -xf "$work_dir/$sdk_archive" -C "$work_dir"
sdk_dir=$(find "$work_dir" -mindepth 1 -maxdepth 1 -type d -name 'openwrt-sdk-*' -print -quit)
[[ -n $sdk_dir ]] || { echo "SDK directory was not extracted" >&2; exit 1; }
cd "$sdk_dir"
# OpenWrt publishes relocatable SDKs whose host tools are x86-64 binaries.
# On ARM runners, invoke the SDK's bundled x86-64 loader explicitly through
# QEMU. This works without privileged binfmt_misc registration on the host.
build_host=$(uname -m)
case $build_host in
x86_64|amd64)
;;
aarch64|arm64|armv7l|armv8l)
qemu_x86_64=$(command -v qemu-x86_64-static || command -v qemu-x86_64 || true)
[[ -n $qemu_x86_64 ]] || {
echo "ARM build host requires qemu-x86_64-static (install qemu-user-static)" >&2
exit 1
}
mapfile -d '' sdk_wrappers < <(
find staging_dir -type f -exec grep -IlZ 'ld-linux-x86-64\.so\.2' {} +
)
[[ ${#sdk_wrappers[@]} -gt 0 ]] || {
echo "no relocatable x86-64 SDK wrappers found" >&2
exit 1
}
sed -i -E \
-e 's|^export LD_PRELOAD=.*(\$dir/.*runas\.so)"$|sdk_preload="${SDK_GUEST_LD_PRELOAD:+$SDK_GUEST_LD_PRELOAD:}\1"\nunset LD_PRELOAD SDK_GUEST_LD_PRELOAD|' \
-e "s|^exec (\"\\\$dir/.*ld-linux-x86-64\\.so\\.2\")|exec ${qemu_x86_64} \\1 --preload \"\\\$sdk_preload\"|" \
"${sdk_wrappers[@]}"
# fakeroot normally exports its x86-64 preload before invoking a wrapped
# command. Carry it in a private variable so ARM-native env/bash never try
# to load it; the wrapper above passes it directly to the guest loader.
fakeroot_script=staging_dir/host/bin/fakeroot
sed -i 's/LD_PRELOAD="$FAKEROOT_LIB"/SDK_GUEST_LD_PRELOAD="$FAKEROOT_LIB"/g' "$fakeroot_script"
grep -q 'SDK_GUEST_LD_PRELOAD="$FAKEROOT_LIB"' "$fakeroot_script"
staging_dir/host/bin/sed --version >/dev/null
;;
*)
echo "unsupported build host architecture: $build_host" >&2
exit 1
;;
esac
# Install the package definitions needed by openuf. The feeds tool recursively
# installs their transitive package definitions; OpenWrt's build graph then
# compiles that dependency closure before openuf.
./scripts/feeds update base packages
./scripts/feeds install mbedtls uci usteer
# openuf needs the mbedTLS libraries, not its optional example/utility
# executables. Under x86 user emulation on ARM, linking those large programs is
# unreliable and unnecessary, so keep the library build and omit the programs.
mbedtls_makefile=feeds/base_root/package/libs/mbedtls/Makefile
[[ -f $mbedtls_makefile ]] || {
echo "mbedTLS feed Makefile not found: $mbedtls_makefile" >&2
exit 1
}
sed -i 's/-DENABLE_PROGRAMS:Bool=ON/-DENABLE_PROGRAMS:Bool=OFF/' "$mbedtls_makefile"
grep -q -- '-DENABLE_PROGRAMS:Bool=OFF' "$mbedtls_makefile"
mkdir -p package/openuf
(cd "$repo_root" && tar --exclude=.git --exclude="$output_dir" -cf - .) | (cd package/openuf && tar -xf -)
sed -i -E "s/^(PKG_VERSION[[:space:]]*:?=[[:space:]]*).*/\\1${version}/" package/openuf/Makefile
# Release SDKs remember the package set used to create the SDK. Neutralize
# those baked-in defaults so this job builds openuf and its dependency closure,
# rather than every package and kernel module available for the target.
sed -i -E '/^config PACKAGE_/,/^$/ s/^([[:space:]]*)default [ym]$/\1default n/' Config-build.in
for symbol in TARGET_MULTI_PROFILE TARGET_ALL_PROFILES TARGET_PER_DEVICE_ROOTFS ALL_NONSHARED ALL_KMODS ALL BUILDBOT; do
sed -i -E "/^config ${symbol}$/,/^$/ s/^([[:space:]]*)default y$/\\1default n/" Config.in Config-build.in
done
printf '%s\n' \
'# CONFIG_ALL is not set' \
'# CONFIG_ALL_KMODS is not set' \
'# CONFIG_ALL_NONSHARED is not set' \
'# CONFIG_TARGET_MULTI_PROFILE is not set' \
'# CONFIG_TARGET_ALL_PROFILES is not set' \
'CONFIG_PACKAGE_openuf=m' >.config
make defconfig
build_jobs=$(nproc)
case $build_host in
aarch64|arm64|armv7l|armv8l)
(( build_jobs <= 2 )) || build_jobs=2
make -j"$build_jobs" package/openuf/compile V=sc
;;
*)
if ! make -j"$build_jobs" package/openuf/compile; then
echo "parallel build failed; retrying serially with verbose diagnostics" >&2
make -j1 package/openuf/compile V=sc
fi
;;
esac
mapfile -t packages < <(find bin/packages -type f -name "openuf-${version}-*.apk")
if [[ ${#packages[@]} -ne 1 ]]; then
echo "expected one openuf package, found ${#packages[@]}" >&2
printf '%s\n' "${packages[@]}" >&2
exit 1
fi
asset="$repo_root/$output_dir/openUniFi-${architecture}-${version}.apk"
cp -- "${packages[0]}" "$asset"
sha256sum "$asset"
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
action=${1:-}
: "${GITEA_TOKEN:?GITEA_TOKEN is required}"
: "${GITHUB_SERVER_URL:?GITHUB_SERVER_URL is required}"
: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
api="${GITHUB_SERVER_URL%/}/api/v1/repos/${GITHUB_REPOSITORY}"
auth_header="Authorization: token ${GITEA_TOKEN}"
case "$action" in
create)
: "${VERSION:?VERSION is required}"
tag="v${VERSION}"
if response=$(curl -fsS -H "$auth_header" "$api/releases/tags/$tag" 2>/dev/null); then
jq -er '.id' <<<"$response"
exit 0
fi
openwrt_release=$(jq -er '.openwrt_release' .gitea/release-targets.json)
payload=$(jq -n --arg tag "$tag" --arg version "$VERSION" --arg sha "${GITHUB_SHA:-main}" --arg openwrt "$openwrt_release" '{tag_name: $tag, target_commitish: $sha, name: ("openUniFi " + $version), body: ("Automated multi-architecture build for OpenWrt " + $openwrt + "."), draft: true, prerelease: false}')
curl -fsS -X POST -H "$auth_header" -H 'Content-Type: application/json' --data "$payload" "$api/releases" | jq -er '.id'
;;
upload)
: "${RELEASE_ID:?RELEASE_ID is required}"
: "${ASSET:?ASSET is required}"
[[ -f $ASSET ]] || { echo "asset not found: $ASSET" >&2; exit 1; }
asset_name=$(basename -- "$ASSET")
old_asset_id=$(curl -fsS -H "$auth_header" "$api/releases/$RELEASE_ID/assets" | jq -r --arg name "$asset_name" '.[] | select(.name == $name) | .id' | head -n 1)
if [[ -n $old_asset_id ]]; then
curl -fsS -X DELETE -H "$auth_header" "$api/releases/$RELEASE_ID/assets/$old_asset_id"
fi
curl -fsS -X POST -H "$auth_header" -F "attachment=@${ASSET}" "$api/releases/$RELEASE_ID/assets?name=$asset_name" >/dev/null
;;
publish)
: "${RELEASE_ID:?RELEASE_ID is required}"
curl -fsS -X PATCH -H "$auth_header" -H 'Content-Type: application/json' --data '{"draft":false}' "$api/releases/$RELEASE_ID" >/dev/null
;;
*)
echo "usage: $0 {create|upload|publish}" >&2
exit 2
;;
esac
+589
View File
@@ -0,0 +1,589 @@
#!/usr/bin/env python3
"""Generate and lint the openUF Gitea Wiki architecture documentation."""
from __future__ import annotations
import argparse
import dataclasses
import re
import sys
from pathlib import Path
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
SOURCE_ROOT = REPOSITORY_ROOT / "src"
MAX_IMPLEMENTATION_LINES = 900
CONTROL_WORDS = {
"for", "if", "return", "sizeof", "switch", "while", "do",
"case", "defined", "typeof", "alignof",
}
NON_ENGLISH_COMMENT_WORDS = {
"algunos", "configuración", "configuramos", "controlador", "cuando",
"contraseña", "desde", "descripción", "envía", "lectura", "modelo",
"obligatorio", "opcional", "para", "puerto", "requiere", "siempre",
"sin", "tienen", "traduce",
}
FUNCTION_PATTERN = re.compile(
r"(?m)^[ \t]*"
r"(?P<prefix>(?:static\s+)?(?:inline\s+)?(?:const\s+)?"
r"(?:(?:struct|enum)\s+[A-Za-z_]\w*\s*\*?\s*|"
r"[A-Za-z_]\w*(?:\s+|\s*\*+\s*))+?)"
r"(?P<name>[A-Za-z_]\w*)\s*"
r"\((?P<parameters>[^;{}]*)\)\s*\{"
)
@dataclasses.dataclass(frozen=True)
class Function:
name: str
relative_file: str
line: int
body: str
is_static: bool
@property
def node_id(self) -> str:
raw = f"{self.relative_file}_{self.name}"
return "function_" + re.sub(r"[^A-Za-z0-9_]", "_", raw)
MODULE_DESCRIPTIONS = {
"main.c": "Daemon lifecycle and one-second scheduler.",
"config.c": "Static daemon configuration parser and defaults.",
"state.c": "Persistent adoption and controller state.",
"models.c": "Emulated hardware model registry.",
"announce.c": "UniFi layer-2 UDP discovery.",
"lldp.c": "LLDP frame transmission and neighbor collection.",
"http.c": "Minimal HTTP/1.0 transport.",
"crypto.c": "AES-CBC, AES-GCM, and encoding helpers.",
"sysinfo.c": "Kernel and nl80211 device telemetry.",
"clients.c": "Wireless and bridge client telemetry.",
"inform/inform.c": "One inform request/response exchange.",
"inform/payload.c": "Inform JSON telemetry assembly.",
"inform/packet.c": "TNBU binary envelope codec.",
"inform/response.c": "Controller command and provisioning dispatch.",
"wlan/common.c": "Shared Wi-Fi translations and validators.",
"wlan/uci.c": "Reusable UCI, VLAN, steering, and cleanup operations.",
"wlan/radio.c": "Runtime radio mapping and radio settings.",
"wlan/provision.c": "Modern setstate Wi-Fi provisioning.",
"wlan/legacy.c": "Legacy system_cfg translation.",
"wlan/telemetry.c": "Managed VAP telemetry from UCI and nl80211.",
}
def mask_comments_and_literals(source: str) -> str:
"""Replace comments and literals with spaces while preserving newlines."""
output = list(source)
index = 0
state = "code"
while index < len(source):
current = source[index]
following = source[index + 1] if index + 1 < len(source) else ""
if state == "code" and current == "/" and following == "*":
output[index] = output[index + 1] = " "
state = "block_comment"
index += 2
continue
if state == "code" and current == "/" and following == "/":
output[index] = output[index + 1] = " "
state = "line_comment"
index += 2
continue
if state == "code" and current in {'"', "'"}:
output[index] = " "
state = "string" if current == '"' else "character"
index += 1
continue
if state == "block_comment":
if current == "*" and following == "/":
output[index] = output[index + 1] = " "
state = "code"
index += 2
continue
if current != "\n":
output[index] = " "
elif state == "line_comment":
if current == "\n":
state = "code"
else:
output[index] = " "
elif state in {"string", "character"}:
if current == "\\" and following:
output[index] = " "
if following != "\n":
output[index + 1] = " "
index += 2
continue
terminator = '"' if state == "string" else "'"
if current == terminator:
state = "code"
if current != "\n":
output[index] = " "
index += 1
return "".join(output)
def matching_brace(source: str, opening_brace: int) -> int:
depth = 0
for index in range(opening_brace, len(source)):
if source[index] == "{":
depth += 1
elif source[index] == "}":
depth -= 1
if depth == 0:
return index
raise ValueError(f"unmatched opening brace at byte {opening_brace}")
def discover_functions() -> list[Function]:
functions: list[Function] = []
for path in sorted(SOURCE_ROOT.rglob("*.c")):
source = path.read_text(encoding="utf-8")
masked = mask_comments_and_literals(source)
relative_file = path.relative_to(SOURCE_ROOT).as_posix()
for match in FUNCTION_PATTERN.finditer(masked):
name = match.group("name")
if name in CONTROL_WORDS:
continue
opening_brace = match.end() - 1
try:
closing_brace = matching_brace(masked, opening_brace)
except ValueError as error:
raise ValueError(
f"cannot parse {relative_file}:{name}: {error}") from error
functions.append(Function(
name=name,
relative_file=relative_file,
line=source.count("\n", 0, match.start()) + 1,
body=masked[opening_brace + 1:closing_brace],
is_static="static" in match.group("prefix").split(),
))
return functions
def resolve_calls(functions: list[Function]) -> dict[Function, list[Function]]:
by_name: dict[str, list[Function]] = {}
for function in functions:
by_name.setdefault(function.name, []).append(function)
result: dict[Function, list[Function]] = {}
for caller in functions:
callees: set[Function] = set()
for name in re.findall(r"\b([A-Za-z_]\w*)\s*\(", caller.body):
candidates = by_name.get(name, [])
local_candidates = [
candidate for candidate in candidates
if candidate.relative_file == caller.relative_file
]
if local_candidates:
callees.update(local_candidates)
elif len(candidates) == 1:
callees.add(candidates[0])
else:
callees.update(
candidate for candidate in candidates
if not candidate.is_static
)
result[caller] = sorted(
callees, key=lambda item: (item.relative_file, item.line, item.name)
)
return result
def module_rows() -> str:
rows = []
for path in sorted(SOURCE_ROOT.rglob("*.c")):
relative = path.relative_to(SOURCE_ROOT).as_posix()
description = MODULE_DESCRIPTIONS.get(relative, "Implementation module.")
rows.append(f"| `src/{relative}` | {description} |")
return "\n".join(rows)
def developer_guide() -> str:
return f"""# Developer Guide
This guide describes the current openUF implementation. The daemon is a
single-process, single-threaded OpenWrt service. Its main loop wakes once per
second and schedules discovery, LLDP, and controller inform work.
## Safety first
openUF runs as root. It can replace managed Wi-Fi configuration, create VLAN
devices, send raw Ethernet frames, reboot the device, and persist adoption
credentials. Do not run the daemon itself on a development workstation.
Compile it with the OpenWrt toolchain and perform runtime tests on a disposable
OpenWrt access point.
Protocol debug level 2 can log decrypted credentials. Never enable it by
default, attach those logs to issues, or commit controller payload captures.
## Source layout
| Module | Responsibility |
| --- | --- |
{module_rows()}
Public contracts remain in `src/*.h`. The `src/inform/` and `src/wlan/`
directories contain private implementation units; their `*_internal.h` files
are not stable interfaces for other subsystems.
## Runtime architecture
`main()` loads static configuration, persistent controller state, and the
emulated model. It discovers the LAN MAC/IP and an initial controller from the
default route when no explicit controller is configured. The main loop then
schedules:
1. `announce_send()` for UniFi UDP discovery.
2. `lldp_send_frame()` for each model Ethernet port.
3. `inform_send()` for adoption, telemetry, commands, and provisioning.
See [[Runtime Flow|Runtime-Flow]] for the decision flow and
[[Function Call Graph|Call-Graph]] for generated caller/callee relationships.
## Inform and adoption invariants
- The TNBU layout, big-endian fields, flag meanings, authenticated header, and
cipher selection are controller compatibility constraints.
- An unadopted device always encrypts with `DEFAULT_AUTH_KEY`, even if stale
state contains another key.
- The controller response is decoded before command dispatch. Adoption can be
completed through either legacy `set-adopt` or modern `setparam` data.
- `cfgversion` records configuration that was successfully applied locally.
Failed provisioning resets it to `"0"` so the controller retries.
- Increase `OPENUF_CONFIG_SCHEMA` only when an existing persisted
configuration must be reapplied after an upgrade.
## Wi-Fi ownership and provisioning
Only UCI `wifi-iface` sections prefixed with `openuf_` belong to this daemon.
Cleanup must preserve every unrelated section. Controller VAP ObjectIds are
stored in `openuf_vap_id` so client topology remains stable across reprovision.
Modern `setstate` data is applied by `wlan_apply_config()`. Legacy
newline-separated `system_cfg` data is first translated to the same JSON shape
by `wlan_apply_system_cfg()`, keeping one provisioning path responsible for UCI
commits and radio startup.
Model band and port assumptions belong in `models.c`. The runtime radio mapper
may resolve a model band to a different local PHY, but protocol and telemetry
code must not invent model-specific mappings.
## Common development tasks
### Add telemetry
1. Add a bounded reader to `sysinfo.c`, `clients.c`, or another focused module.
2. Add the controller field in `inform/payload.c`.
3. Document units, fallback behavior, ownership, and any sensitive content.
4. Regenerate this Wiki and cross-build the package.
### Add a controller command
1. Extend dispatch in `inform/response.c`.
2. Validate controller values before changing state or invoking a command.
3. Save state only after a coherent transition.
4. Preserve adoption-key and `cfgversion` behavior.
### Add a Wi-Fi setting
1. Parse controller aliases in `wlan/provision.c` or `wlan/legacy.c`.
2. Put reusable UCI work in `wlan/uci.c` and translations in `wlan/common.c`.
3. Report the effective value from `wlan/telemetry.c` when the controller
expects it in `vap_table`.
4. Test on device; a successful cross-build cannot validate netifd/hostapd
behavior.
### Add or change a model
Update `ufmodel.h`, the complete model entry in `models.c`, and every affected
telemetry or WLAN consumer together. Do not scatter model checks across the
protocol implementation.
## Build and validation
From the OpenWrt root:
```sh
make package/OpenUniFi/compile
```
Use `V=s` for detailed compiler diagnostics. If a clean package rebuild is
needed, clean only this package:
```sh
make package/OpenUniFi/clean
make package/OpenUniFi/compile
```
`Makefile.standalone` is only for compiling directly on an OpenWrt device with
development packages installed. It is not a host-side test substitute.
## Documentation workflow
Check out the separate `openunifi.wiki` repository, then generate pages after
changing C code or architecture:
```sh
./scripts/docs/generate-wiki.py --output /path/to/openunifi.wiki
```
Run the linter-style drift and structure check in CI or before committing:
```sh
./scripts/docs/generate-wiki.py --check --output /path/to/openunifi.wiki
```
The generator uses only the Python 3 standard library. The publisher uses POSIX
shell utilities and Git, so neither path depends on the runner CPU architecture.
The checker parses C functions recursively, rebuilds internal call edges,
validates required runtime entry points, rejects known non-English comment
fragments, and limits each implementation unit to
{MAX_IMPLEMENTATION_LINES} lines.
## Publishing to the Gitea Wiki
Gitea stores this documentation in the separate lowercase
`openunifi.wiki` repository. Run `scripts/docs/publish-wiki.sh` from a trusted
machine with suitable credentials. It derives that repository from `origin`;
an explicit URL may be passed when needed. The publisher checks out the Wiki
repository before generating, updates only the generated Markdown files,
commits changed pages, and pushes them back to the Wiki repository.
"""
def home_page() -> str:
return """# openUF Developer Wiki
This Wiki is generated from the current source tree and maintained in the
separate `openunifi.wiki` repository.
- [[Developer Guide|Developer-Guide]] — architecture, invariants, extension
points, build validation, and Wiki publishing.
- [[Runtime Flow|Runtime-Flow]] — lifecycle, inform, response, and Wi-Fi
provisioning flow charts.
- [[Function Call Graph|Call-Graph]] — generated internal caller/callee graph
and searchable function table.
Run `./scripts/docs/generate-wiki.py --check --output /path/to/openunifi.wiki`
from the source checkout to verify these pages are current.
"""
def call_graph_page(functions: list[Function], calls: dict[Function, list[Function]]) -> str:
lines = [
"# Function Call Graph", "",
"Generated from `src/**/*.c` by `scripts/docs/generate-wiki.py`.", "",
"```mermaid", "flowchart LR",
]
by_file: dict[str, list[Function]] = {}
for function in functions:
by_file.setdefault(function.relative_file, []).append(function)
for index, (relative_file, file_functions) in enumerate(sorted(by_file.items())):
lines.append(f' subgraph module_{index}["src/{relative_file}"]')
for function in file_functions:
lines.append(f' {function.node_id}["{function.name}()"]')
lines.append(" end")
for caller in functions:
for callee in calls[caller]:
lines.append(f" {caller.node_id} --> {callee.node_id}")
lines.extend(["```", "", "## Caller/callee index", "",
"| Source | Caller | Internal callees |",
"| --- | --- | --- |"])
for caller in functions:
callees = ", ".join(
f"`{callee.name}()`" for callee in calls[caller]
) or ""
lines.append(
f"| `src/{caller.relative_file}:{caller.line}` | "
f"`{caller.name}()` | {callees} |"
)
return "\n".join(lines) + "\n"
def runtime_flow_page() -> str:
return """# Runtime Flow
The flow is generated alongside the call graph. Function names are validated
against the current C sources so renamed or removed stages fail documentation
checks.
## Daemon scheduler
```mermaid
flowchart TD
start([main]) --> config[config_load]
config --> state[state_load]
state --> model[ufmodel_find]
model --> identity[Read LAN MAC, IP, and default gateway]
identity --> persist[state_save]
persist --> announceInit[announce_init when enabled]
announceInit --> loop{One-second main loop}
loop -->|announce due| announce[announce_send]
announce --> loop
loop -->|LLDP due| ports{For every model port}
ports --> lldp[lldp_send_frame]
lldp --> loop
loop -->|inform due| refresh[Refresh controller URL and device IP]
refresh --> inform[inform_send]
inform --> loop
loop -->|nothing due| sleep[sleep 1 second]
sleep --> loop
```
## Inform exchange
```mermaid
flowchart TD
send([inform_send]) --> url{inform URL available?}
url -->|no| error[Return an error]
url -->|yes| key{Device adopted?}
key -->|no| defaultKey[Use DEFAULT_AUTH_KEY]
key -->|yes| stateKey[Use persisted device key]
defaultKey --> payload[inform_build_payload]
stateKey --> payload
payload --> packet[inform_packet_build]
packet --> post[http_post]
post --> status{HTTP 200?}
status -->|no, first attempt| retry[Retry with alternate CBC or GCM cipher]
retry --> packet
status -->|no, final attempt| error
status -->|yes| decode[inform_packet_parse]
decode --> json[Parse controller JSON]
json --> handle[inform_handle_response]
```
## Controller response and provisioning
```mermaid
flowchart TD
response([inform_handle_response]) --> type{_type}
type -->|noop| done[No state change]
type -->|upgrade| version[Persist requested firmware version]
type -->|reboot or reset| reboot[reboot_openwrt]
type -->|cmd adopt| adopt[Persist key, URL, and adopted state]
type -->|setparam| params[Parse management parameters]
params --> legacy{system_cfg present?}
legacy -->|yes| legacyApply[wlan_apply_system_cfg]
legacyApply --> normalize[Build radio_table and vap_table JSON]
normalize --> apply[wlan_apply_config]
type -->|setstate| apply
apply --> ids[wlan_ensure_vap_ids]
ids --> clear[wlan_clear managed openuf_ VAPs]
clear --> radios[wlan_apply_radio for each band]
radios --> vaps[Create controller VAP sections]
vaps --> commit[Commit UCI and configure usteer]
commit --> start[Start and verify radios sequentially]
start --> result{Apply succeeded?}
result -->|yes| saved[Save cfgversion and config schema]
result -->|no| retryConfig[Reset cfgversion to 0]
```
"""
def sidebar() -> str:
return """- [[Home]]
- [[Developer Guide|Developer-Guide]]
- [[Runtime Flow|Runtime-Flow]]
- [[Function Call Graph|Call-Graph]]
"""
def validate_source(functions: list[Function]) -> list[str]:
errors: list[str] = []
names = {function.name for function in functions}
required = {
"main", "config_load", "state_load", "state_save", "ufmodel_find",
"announce_init", "announce_send", "lldp_send_frame", "inform_send",
"inform_build_payload", "inform_packet_build", "http_post",
"inform_packet_parse", "inform_handle_response", "reboot_openwrt",
"wlan_apply_system_cfg", "wlan_apply_config", "wlan_ensure_vap_ids",
"wlan_clear", "wlan_apply_radio",
}
missing = sorted(required - names)
if missing:
errors.append("runtime documentation references missing functions: " +
", ".join(missing))
for path in sorted(SOURCE_ROOT.rglob("*.c")):
source = path.read_text(encoding="utf-8")
line_count = source.count("\n") + 1
if line_count > MAX_IMPLEMENTATION_LINES:
errors.append(
f"{path.relative_to(REPOSITORY_ROOT)} has {line_count} lines; "
f"split modules above {MAX_IMPLEMENTATION_LINES} lines"
)
comments = "\n".join(re.findall(r"/\*.*?\*/|//[^\n]*", source,
flags=re.DOTALL)).lower()
found = sorted(
word for word in NON_ENGLISH_COMMENT_WORDS
if re.search(rf"\b{re.escape(word)}\b", comments)
)
if found:
errors.append(
f"{path.relative_to(REPOSITORY_ROOT)} contains non-English "
f"comment words: {', '.join(found)}"
)
return errors
def render_pages(functions: list[Function]) -> dict[str, str]:
calls = resolve_calls(functions)
return {
"Home.md": home_page(),
"Developer-Guide.md": developer_guide(),
"Call-Graph.md": call_graph_page(functions, calls),
"Runtime-Flow.md": runtime_flow_page(),
"_Sidebar.md": sidebar(),
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true",
help="fail if generated pages or source checks are stale")
parser.add_argument(
"--output", type=Path, required=True, metavar="WIKI_CHECKOUT",
help="path to the checked-out openunifi.wiki repository",
)
arguments = parser.parse_args()
functions = discover_functions()
errors = validate_source(functions)
pages = render_pages(functions)
output = arguments.output.resolve()
if arguments.check:
for filename, expected in pages.items():
path = output / filename
if not path.exists():
errors.append(f"missing generated page: {path}")
elif path.read_text(encoding="utf-8") != expected:
errors.append(f"generated page is stale: {path}")
else:
output.mkdir(parents=True, exist_ok=True)
for filename, content in pages.items():
(output / filename).write_text(content, encoding="utf-8")
if errors:
for error in errors:
print(f"documentation error: {error}", file=sys.stderr)
if arguments.check:
print(
"run ./scripts/docs/generate-wiki.py --output "
f"{output} to refresh pages",
file=sys.stderr,
)
return 1
action = "verified" if arguments.check else "generated"
print(f"{action} {len(pages)} Wiki pages from {len(functions)} C functions")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env sh
set -eu
if [ "$#" -gt 1 ]; then
echo "usage: $0 [WIKI_REPOSITORY_URL]" >&2
exit 2
fi
script_directory=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
repository_root=$(CDPATH= cd -- "$script_directory/../.." && pwd)
wiki_repository_url=${1:-}
temporary_directory=$(mktemp -d "${TMPDIR:-/tmp}/openuf-wiki.XXXXXX")
if [ -z "$wiki_repository_url" ]; then
origin_url=$(git -C "$repository_root" config --get remote.origin.url || true)
if [ -z "$origin_url" ]; then
echo "no origin remote found; pass the Gitea Wiki repository URL" >&2
exit 2
fi
source_repository_url=${origin_url%.git}
repository_parent=${source_repository_url%/*}
repository_name=${source_repository_url##*/}
wiki_repository_name=$(
printf '%s' "$repository_name" | tr '[:upper:]' '[:lower:]'
)
wiki_repository_url="$repository_parent/$wiki_repository_name.wiki.git"
fi
cleanup() {
rm -rf -- "$temporary_directory"
}
trap cleanup EXIT HUP INT TERM
git clone -- "$wiki_repository_url" "$temporary_directory/wiki"
"$script_directory/generate-wiki.py" --output "$temporary_directory/wiki"
"$script_directory/generate-wiki.py" --check \
--output "$temporary_directory/wiki"
cd "$temporary_directory/wiki"
git add -- Home.md Developer-Guide.md Call-Graph.md Runtime-Flow.md _Sidebar.md
if git diff --cached --quiet; then
echo "Gitea Wiki is already current"
exit 0
fi
git -c user.name="openUF documentation bot" \
-c user.email="actions@openuf.invalid" \
commit -m "docs: update generated developer wiki"
git push origin HEAD
+7 -6
View File
@@ -27,6 +27,7 @@
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include <unistd.h> #include <unistd.h>
#include <errno.h>
#include <arpa/inet.h> #include <arpa/inet.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <netinet/in.h> #include <netinet/in.h>
@@ -37,7 +38,7 @@
#ifdef ENABLE_LOGGING #ifdef ENABLE_LOGGING
#include <stdio.h> #include <stdio.h>
extern FILE *log_fp; 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) #define LOG(fmt, ...) do { if (log_fp) openuf_log_emit(log_fp, __func__, fmt, ##__VA_ARGS__); } while(0)
#else #else
#define LOG(fmt, ...) do {} while(0) #define LOG(fmt, ...) do {} while(0)
#endif #endif
@@ -196,15 +197,15 @@ int announce_init(announce_ctx_t *ctx,
ctx->counter = 0; ctx->counter = 0;
ctx->uptime = 10; ctx->uptime = 10;
/* ── Socket para broadcast 255.255.255.255 ─────────────────── */ /* ── Socket for broadcast 255.255.255.255 ─────────────────── */
ctx->sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); ctx->sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (ctx->sockfd < 0) { if (ctx->sockfd < 0) {
perror("[openuf] announce socket"); LOGF(stderr, "announce socket: %s", strerror(errno));
return -1; return -1;
} }
int on = 1; int on = 1;
setsockopt(ctx->sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)); setsockopt(ctx->sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
/* Bind a puerto efímero — OpenWrt no permite setpeername() a broadcast */ /* Bind an ephemeral port; OpenWrt does not allow broadcast setpeername(). */
struct sockaddr_in bind_addr = { struct sockaddr_in bind_addr = {
.sin_family = AF_INET, .sin_family = AF_INET,
.sin_addr.s_addr = INADDR_ANY, .sin_addr.s_addr = INADDR_ANY,
@@ -249,7 +250,7 @@ int announce_send(announce_ctx_t *ctx)
}; };
if (sendto(ctx->sockfd, ctx->pkt, ctx->pkt_len, 0, if (sendto(ctx->sockfd, ctx->pkt, ctx->pkt_len, 0,
(struct sockaddr *)&dest_bcast, sizeof(dest_bcast)) < 0) { (struct sockaddr *)&dest_bcast, sizeof(dest_bcast)) < 0) {
perror("[openuf] announce sendto broadcast"); LOGF(stderr, "announce sendto broadcast: %s", strerror(errno));
ret = -1; ret = -1;
} }
@@ -262,7 +263,7 @@ int announce_send(announce_ctx_t *ctx)
inet_pton(AF_INET, "233.89.188.1", &dest_mcast.sin_addr); inet_pton(AF_INET, "233.89.188.1", &dest_mcast.sin_addr);
if (sendto(ctx->sockfd_mcast, ctx->pkt, ctx->pkt_len, 0, if (sendto(ctx->sockfd_mcast, ctx->pkt, ctx->pkt_len, 0,
(struct sockaddr *)&dest_mcast, sizeof(dest_mcast)) < 0) { (struct sockaddr *)&dest_mcast, sizeof(dest_mcast)) < 0) {
/* No es error crítico — algunos kernels no tienen ruta multicast */ /* This is nonfatal; some kernels have no multicast route. */
} }
} }
+11
View File
@@ -167,6 +167,11 @@ int clients_read_wifi(const char *wlan_iface,
if (sscanf(line, " tx bytes: %lld", &llv) == 1) { cur->tx_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, " rx packets: %lld", &llv) == 1) { cur->rx_packets = llv; continue; }
if (sscanf(line, " tx packets: %lld", &llv) == 1) { cur->tx_packets = llv; continue; } if (sscanf(line, " tx packets: %lld", &llv) == 1) { cur->tx_packets = llv; continue; }
if (sscanf(line, " tx retries: %lld", &llv) == 1) { cur->tx_retries = llv; continue; }
if (sscanf(line, " tx failed: %lld", &llv) == 1) { cur->tx_failed = llv; continue; }
if (sscanf(line, " rx drop misc: %lld", &llv) == 1) { cur->rx_dropped = llv; continue; }
if (sscanf(line, " tx duration: %lld us", &llv) == 1) { cur->tx_duration = llv; continue; }
if (sscanf(line, " rx duration: %lld us", &llv) == 1) { cur->rx_duration = llv; continue; }
/* ── Signal ───────────────────────────────────────────────── */ /* ── Signal ───────────────────────────────────────────────── */
int sig; int sig;
@@ -266,6 +271,12 @@ struct json_object *clients_build_sta_table(const char *wlan_iface,
json_object_object_add(o, "rx_bytes", json_object_new_int64(s->rx_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, "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, "rx_packets", json_object_new_int64(s->rx_packets));
json_object_object_add(o, "tx_retries", json_object_new_int64(s->tx_retries));
json_object_object_add(o, "tx_failed", json_object_new_int64(s->tx_failed));
json_object_object_add(o, "tx_dropped", json_object_new_int64(s->tx_failed));
json_object_object_add(o, "rx_dropped", json_object_new_int64(s->rx_dropped));
json_object_object_add(o, "tx_duration", json_object_new_int64(s->tx_duration));
json_object_object_add(o, "rx_duration", json_object_new_int64(s->rx_duration));
json_object_object_add(o, "uptime", json_object_new_int(s->uptime)); 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, "radio", json_object_new_string(s->radio));
json_object_object_add(o, "channel", json_object_new_int(s->channel)); json_object_object_add(o, "channel", json_object_new_int(s->channel));
+5
View File
@@ -55,6 +55,11 @@ typedef struct {
long long rx_bytes; long long rx_bytes;
long long tx_packets; long long tx_packets;
long long rx_packets; long long rx_packets;
long long tx_retries;
long long tx_failed;
long long rx_dropped;
long long tx_duration;
long long rx_duration;
int uptime; /* seconds online */ int uptime; /* seconds online */
char radio[8]; /* "ng" / "na" / "6g" */ char radio[8]; /* "ng" / "na" / "6g" */
int channel; int channel;
+29 -2
View File
@@ -2,7 +2,7 @@
#define OPENUF_CONFIG_H #define OPENUF_CONFIG_H
/* ─── Build-time defaults (override with /etc/openuf/openuf.conf) ─── */ /* ─── Build-time defaults (override with /etc/openuf/openuf.conf) ─── */
#define OPENUF_VERSION "0.3-C" #define OPENUF_VERSION "0.4.0-r1"
#define OPENUF_STATE_FILE "/etc/openuf/state.json" #define OPENUF_STATE_FILE "/etc/openuf/state.json"
#define OPENUF_CONF_FILE "/etc/openuf/openuf.conf" #define OPENUF_CONF_FILE "/etc/openuf/openuf.conf"
@@ -21,10 +21,37 @@
#if ENABLE_LOGGING #if ENABLE_LOGGING
#include <stdio.h> #include <stdio.h>
#include <stdarg.h>
#include <time.h>
extern FILE *log_fp; 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)
static inline void openuf_log_emit(FILE *stream, const char *prefix,
const char *fmt, ...)
{
char ts[32];
time_t now = time(NULL);
struct tm tm;
localtime_r(&now, &tm);
strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", &tm);
fprintf(stream, "[%s]", ts);
if (prefix && prefix[0])
fprintf(stream, " [%s]", prefix);
fputc(' ', stream);
va_list ap;
va_start(ap, fmt);
vfprintf(stream, fmt, ap);
va_end(ap);
fputc('\n', stream);
fflush(stream);
}
#define LOG(fmt, ...) do { if (log_fp) openuf_log_emit(log_fp, __func__, fmt, ##__VA_ARGS__); } while(0)
#define LOGF(stream, fmt, ...) openuf_log_emit(stream, "openuf", fmt, ##__VA_ARGS__)
#else #else
#define LOG(fmt, ...) do {} while(0) #define LOG(fmt, ...) do {} while(0)
#define LOGF(stream, fmt, ...) do { (void)(stream); } while(0)
#endif #endif
typedef struct { typedef struct {
-1253
View File
File diff suppressed because it is too large Load Diff
+164
View File
@@ -0,0 +1,164 @@
/*
* Run one inform exchange: select the adoption key and cipher, encode the
* request, post it to the controller, and dispatch the decoded response.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <json-c/json.h>
#include "inform.h"
#include "crypto.h"
#include "http.h"
#include "wlan.h"
#include "state.h"
#include "config.h"
#include "sysinfo.h"
#include "clients.h"
#include "lldp.h"
#include "inform_internal.h"
/* Public inform-cycle entry point. */
int inform_send(openuf_state_t *st,
const uf_model_t *model,
long uptime,
char *err_out)
{
if (!st->inform_url[0]) {
LOG("No inform_url set");
strncpy(err_out, "no inform_url", 127);
return -1;
}
const char *key_hex = (st->authkey[0]) ? st->authkey : DEFAULT_AUTH_KEY;
/* CRITICAL: When not adopted, ALWAYS use DEFAULT_AUTH_KEY */
if (!st->adopted && st->authkey[0] && strcmp(st->authkey, DEFAULT_AUTH_KEY) != 0) {
LOG("WARNING: Device not adopted but has custom authkey! Using DEFAULT instead!");
key_hex = DEFAULT_AUTH_KEY;
}
LOG("Sending inform: adopted=%d, authkey=%.8s..., inform_url=%s",
st->adopted, key_hex, st->inform_url);
/* MAC without colons */
char mac_hex[32] = {0};
{
const char *s = st->mac; int j = 0;
for (int i = 0; s[i] && j < 12; i++)
if (s[i] != ':') mac_hex[j++] = s[i];
}
char *payload = inform_build_payload(st, model, uptime);
if (!payload) { strncpy(err_out, "inform_build_payload OOM", 127); return -1; }
LOG("Built payload, length: %zu", strlen(payload));
unsigned char *resp_body = NULL;
size_t resp_len = 0;
int status = -1;
int selected_gcm = st->use_aes_gcm;
/*
* A controller remembers the negotiated cipher. If local state was
* created before use_aes_gcm was persisted, it rejects CBC with HTTP 400
* and cannot send another setparam. Retry once with the other cipher.
*/
for (int attempt = 0; attempt < 2; attempt++) {
size_t pkt_len = 0;
unsigned char *pkt = inform_packet_build(mac_hex, key_hex, payload,
selected_gcm, &pkt_len);
if (!pkt) {
free(payload);
strncpy(err_out, "inform_packet_build failed", 127);
return -1;
}
LOG("Built packet, length: %zu, cipher: %s", pkt_len,
selected_gcm ? "AES-GCM" : "AES-CBC");
status = http_post(st->inform_url,
"application/x-binary-data",
pkt, pkt_len,
&resp_body, &resp_len);
free(pkt);
LOG("HTTP POST to %s, status: %d, response length: %zu",
st->inform_url, status, resp_len);
if (status != 400 || !st->adopted || attempt != 0)
break;
free(resp_body);
resp_body = NULL;
resp_len = 0;
selected_gcm = !selected_gcm;
LOG("Controller rejected %s; retrying once with %s",
selected_gcm ? "AES-CBC" : "AES-GCM",
selected_gcm ? "AES-GCM" : "AES-CBC");
}
free(payload);
if (status < 0) {
snprintf(err_out, 127, "HTTP connect failed");
return -1;
}
if (status != 200) {
snprintf(err_out, 127, "HTTP %d", status);
free(resp_body);
return -1;
}
if (st->use_aes_gcm != selected_gcm) {
st->use_aes_gcm = selected_gcm;
state_save(st);
LOG("Recovered cipher state; persisted aes_gcm=%d",
st->use_aes_gcm);
}
if (!resp_body || resp_len == 0) {
LOG("No response body");
free(resp_body);
return 0;
}
char *resp_json = inform_packet_parse(resp_body, resp_len, key_hex);
free(resp_body);
if (!resp_json) {
LOG("Failed to parse response packet");
snprintf(err_out, 127, "inform_packet_parse failed");
return -1;
}
LOG("Parsed response JSON, length=%zu", strlen(resp_json));
struct json_object *resp_obj = json_tokener_parse(resp_json);
if (!resp_obj) {
LOG("Failed to parse JSON");
snprintf(err_out, 127, "JSON parse failed");
free(resp_json);
return -1;
}
inform_log_controller_response(resp_obj, resp_json);
free(resp_json);
struct json_object *response_type;
if (json_object_object_get_ex(resp_obj, "_type", &response_type))
LOG("Parsed response type: %s",
json_object_get_string(response_type));
char action[64] = "noop";
inform_handle_response(st, model, resp_obj, action);
json_object_put(resp_obj);
LOG("Response action: %s", action);
if (strcmp(action, "noop") != 0)
LOGF(stdout, "Action: %s", action);
return 0;
}
+34
View File
@@ -0,0 +1,34 @@
#ifndef OPENUF_INFORM_INTERNAL_H
#define OPENUF_INFORM_INTERNAL_H
#include <stddef.h>
#include <json-c/json.h>
#include "state.h"
#include "ufmodel.h"
/* Internal contracts shared by the focused inform implementation units. */
char *inform_build_payload(const openuf_state_t *state,
const uf_model_t *model,
long uptime);
unsigned char *inform_packet_build(const char *mac_hex,
const char *key_hex,
const char *payload,
int use_aes_gcm,
size_t *packet_length);
char *inform_packet_parse(const unsigned char *data,
size_t data_length,
const char *key_hex);
void inform_handle_response(openuf_state_t *state,
const uf_model_t *model,
struct json_object *response,
char *action_out);
void inform_log_controller_response(struct json_object *response,
const char *raw_json);
int inform_debug_level(void);
#endif /* OPENUF_INFORM_INTERNAL_H */
+147
View File
@@ -0,0 +1,147 @@
/*
* Encode and decode the binary TNBU envelope. Packet layout, byte order,
* flags, authenticated header bytes, and cipher selection are protocol ABI.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <json-c/json.h>
#include "inform.h"
#include "crypto.h"
#include "http.h"
#include "wlan.h"
#include "state.h"
#include "config.h"
#include "sysinfo.h"
#include "clients.h"
#include "lldp.h"
#include "inform_internal.h"
/* ─── Big-endian helpers ────────────────────────────────────────── */
static void put32be(unsigned char *p, uint32_t v)
{
p[0]=(v>>24)&0xff; p[1]=(v>>16)&0xff;
p[2]=(v>> 8)&0xff; p[3]=v&0xff;
}
static void put16be(unsigned char *p, uint16_t v)
{
p[0]=(v>>8)&0xff; p[1]=v&0xff;
}
static uint32_t get32be(const unsigned char *p)
{
return ((uint32_t)p[0]<<24)|((uint32_t)p[1]<<16)|
((uint32_t)p[2]<<8)|(uint32_t)p[3];
}
static uint16_t get16be(const unsigned char *p)
{
return ((uint16_t)p[0]<<8)|(uint16_t)p[1];
}
unsigned char *inform_packet_build(const char *mac_hex,
const char *key_hex,
const char *payload,
int use_aes_gcm,
size_t *out_len)
{
unsigned char iv_hex[33] = {0};
if (crypto_random_hex(iv_hex, 16) != 0) return NULL;
unsigned char mac_bin[6];
crypto_hex2bin(mac_hex, mac_bin, 6);
size_t pl_len = strlen(payload);
size_t body_len = use_aes_gcm
? pl_len + 16
: pl_len + (16 - (pl_len % 16));
size_t pkt_len = 40 + body_len;
unsigned char *pkt = malloc(pkt_len);
if (!pkt) return NULL;
unsigned char *p = pkt;
memcpy(p, INFORM_MAGIC, 4); p += 4;
put32be(p, INFORM_PKT_VERSION); p += 4;
memcpy(p, mac_bin, 6); p += 6;
put16be(p, INFORM_FLAG_ENCRYPTED |
(use_aes_gcm ? INFORM_FLAG_GCM : 0)); p += 2;
unsigned char iv_bin[16];
crypto_hex2bin((char *)iv_hex, iv_bin, 16);
memcpy(p, iv_bin, 16); p += 16;
put32be(p, INFORM_DATA_VERSION); p += 4;
put32be(p, (uint32_t)body_len); p += 4;
int enc_len;
if (use_aes_gcm) {
unsigned char tag[16];
enc_len = crypto_gcm_encrypt(key_hex, (char *)iv_hex,
pkt, 40,
(const unsigned char *)payload, pl_len,
p, tag);
if (enc_len >= 0)
memcpy(p + enc_len, tag, sizeof(tag));
} else {
enc_len = crypto_encrypt(key_hex, (char *)iv_hex,
(const unsigned char *)payload, pl_len, p);
}
if (enc_len < 0) {
free(pkt);
return NULL;
}
*out_len = pkt_len;
return pkt;
}
/* ═══════════════════════════════════════════════════════════════════
Parse binary response from the controller
═══════════════════════════════════════════════════════════════════ */
char *inform_packet_parse(const unsigned char *data, size_t data_len,
const char *key_hex)
{
if (data_len < 40) return NULL;
if (memcmp(data, INFORM_MAGIC, 4) != 0) return NULL;
uint16_t flags = get16be(data + 14);
const unsigned char *iv_bin = data + 16;
uint32_t body_len = get32be(data + 36);
const unsigned char *body = data + 40;
if (40 + body_len > data_len) return NULL;
if ((flags & INFORM_FLAG_GCM) != 0) {
if (body_len < 16) return NULL;
size_t cipher_len = body_len - 16;
char iv_hex[33];
crypto_bin2hex(iv_bin, 16, iv_hex);
unsigned char *plain = malloc(cipher_len + 1);
if (!plain) return NULL;
int pl = crypto_gcm_decrypt(key_hex, iv_hex, data, 40,
body, cipher_len, body + cipher_len,
plain);
if (pl < 0) { free(plain); return NULL; }
plain[pl] = '\0';
return (char *)plain;
}
if (flags & INFORM_FLAG_ENCRYPTED) {
char iv_hex[33];
crypto_bin2hex(iv_bin, 16, iv_hex);
unsigned char *plain = malloc(body_len + 1);
if (!plain) return NULL;
int pl = crypto_decrypt(key_hex, iv_hex, body, body_len, plain);
if (pl < 0) { free(plain); return NULL; }
plain[pl] = '\0';
return (char *)plain;
}
char *copy = malloc(body_len + 1);
if (!copy) return NULL;
memcpy(copy, body, body_len);
copy[body_len] = '\0';
return copy;
}
+801
View File
@@ -0,0 +1,801 @@
/*
* Collect device, radio, interface, client, and topology telemetry into the
* JSON payload sent during an inform exchange.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <json-c/json.h>
#include "inform.h"
#include "crypto.h"
#include "http.h"
#include "wlan.h"
#include "state.h"
#include "config.h"
#include "sysinfo.h"
#include "clients.h"
#include "lldp.h"
#include "inform_internal.h"
/* Build system CPU, memory, and load statistics. */
static struct json_object *build_sys_stats(int *cpu_percent,
double *mem_percent)
{
struct json_object *o = json_object_new_object();
*mem_percent = 0.0;
mem_stats_t mem;
if (sysinfo_mem(&mem) == 0) {
long used_kb = mem.total_kb - mem.free_kb
- mem.buffer_kb - mem.cached_kb;
if (used_kb < 0) used_kb = 0;
json_object_object_add(o, "mem_total",
json_object_new_int64(mem.total_kb * 1024LL));
json_object_object_add(o, "mem_used",
json_object_new_int64(used_kb * 1024LL));
json_object_object_add(o, "mem_buffer",
json_object_new_int64(mem.buffer_kb * 1024LL));
} else {
json_object_object_add(o, "mem_total", json_object_new_int(0));
json_object_object_add(o, "mem_used", json_object_new_int(0));
json_object_object_add(o, "mem_buffer", json_object_new_int(0));
}
if (mem.total_kb > 0)
*mem_percent = 100.0 * (double)(mem.total_kb - mem.free_kb -
mem.buffer_kb - mem.cached_kb) / (double)mem.total_kb;
FILE *loadavg = fopen("/proc/loadavg", "r");
if (loadavg) {
char one[16], five[16], fifteen[16];
if (fscanf(loadavg, "%15s %15s %15s", one, five, fifteen) == 3) {
json_object_object_add(o, "loadavg_1", json_object_new_string(one));
json_object_object_add(o, "loadavg_5", json_object_new_string(five));
json_object_object_add(o, "loadavg_15", json_object_new_string(fifteen));
}
fclose(loadavg);
}
*cpu_percent = sysinfo_cpu_percent();
return o;
}
static struct json_object *build_system_stats(int cpu_percent,
double mem_percent,
long uptime)
{
struct json_object *o = json_object_new_object();
char value[32];
snprintf(value, sizeof(value), "%.1f", (double)cpu_percent);
json_object_object_add(o, "cpu", json_object_new_string(value));
snprintf(value, sizeof(value), "%.1f", mem_percent);
json_object_object_add(o, "mem", json_object_new_string(value));
snprintf(value, sizeof(value), "%ld", uptime);
json_object_object_add(o, "uptime", json_object_new_string(value));
return o;
}
/* ═══════════════════════════════════════════════════════════════════
if_table — network interface statistics
═══════════════════════════════════════════════════════════════════
All Ethernet ports on the model are reported.
/proc/net/dev is read for counters, and /sys/class/net/<iface>/
for speed, duplex, and link status.
*/
static struct json_object *build_if_table(const uf_model_t *m,
const openuf_state_t *st)
{
struct json_object *arr = json_object_new_array();
for (int i = 0; i < m->port_table_len; i++) {
const char *ifname = m->port_table[i].ifname;
iface_stats_t stats;
sysinfo_iface(ifname, &stats);
struct json_object *o = json_object_new_object();
json_object_object_add(o, "name",
json_object_new_string(ifname));
json_object_object_add(o, "mac",
json_object_new_string(stats.mac[0] ? stats.mac : st->mac));
json_object_object_add(o, "ip",
json_object_new_string(stats.ip[0] ? stats.ip : st->ip));
json_object_object_add(o, "up",
json_object_new_boolean(stats.up));
json_object_object_add(o, "speed",
json_object_new_int(stats.speed > 0 ? stats.speed : 1000));
json_object_object_add(o, "full_duplex",
json_object_new_boolean(stats.full_duplex));
json_object_object_add(o, "num_port",
json_object_new_int(1));
json_object_object_add(o, "rx_bytes",
json_object_new_int64(stats.rx_bytes));
json_object_object_add(o, "tx_bytes",
json_object_new_int64(stats.tx_bytes));
json_object_object_add(o, "rx_packets",
json_object_new_int64(stats.rx_packets));
json_object_object_add(o, "tx_packets",
json_object_new_int64(stats.tx_packets));
json_object_object_add(o, "rx_errors",
json_object_new_int64(stats.rx_errors));
json_object_object_add(o, "tx_errors",
json_object_new_int64(stats.tx_errors));
json_object_object_add(o, "rx_dropped",
json_object_new_int64(stats.rx_dropped));
json_object_object_add(o, "tx_dropped",
json_object_new_int64(stats.tx_dropped));
json_object_object_add(o, "rx_multicast",
json_object_new_int64(stats.rx_multicast));
json_object_array_add(arr, o);
}
return arr;
}
static void radio_runtime_iface(const uf_model_t *m, const char *band,
char *out, size_t out_size)
{
const char *device = wlan_device_for_band(m, band);
int phy_index = 0;
if (device) sscanf(device, "radio%d", &phy_index);
snprintf(out, out_size, "phy%d-ap0", phy_index);
struct json_object *vaps = wlan_get_vap_table(m);
int count = json_object_array_length(vaps);
for (int i = 0; i < count; i++) {
struct json_object *vap = json_object_array_get_idx(vaps, i);
struct json_object *radio_obj, *ifname_obj;
if (json_object_object_get_ex(vap, "radio", &radio_obj) &&
json_object_object_get_ex(vap, "ifname", &ifname_obj) &&
!strcmp(json_object_get_string(radio_obj), band)) {
snprintf(out, out_size, "%s", json_object_get_string(ifname_obj));
break;
}
}
json_object_put(vaps);
}
static struct json_object *build_scan_table(const char *iface)
{
wifi_scan_t scans[MAX_SCAN_RESULTS];
int count = sysinfo_wifi_scan_cache(iface, scans, MAX_SCAN_RESULTS);
struct json_object *arr = json_object_new_array();
for (int i = 0; i < count; i++) {
struct json_object *o = json_object_new_object();
json_object_object_add(o, "age", json_object_new_int(scans[i].age));
json_object_object_add(o, "bssid", json_object_new_string(scans[i].bssid));
json_object_object_add(o, "essid", json_object_new_string(scans[i].essid));
json_object_object_add(o, "freq", json_object_new_int(scans[i].frequency));
json_object_object_add(o, "channel", json_object_new_int(scans[i].channel));
json_object_object_add(o, "signal", json_object_new_int(scans[i].signal));
json_object_object_add(o, "rssi", json_object_new_int(scans[i].rssi));
json_object_object_add(o, "security", json_object_new_string(
scans[i].secured ? "secured" : "open"));
json_object_object_add(o, "is_adhoc", json_object_new_boolean(false));
json_object_object_add(o, "is_ubnt", json_object_new_boolean(false));
json_object_array_add(arr, o);
}
return arr;
}
/* ═══════════════════════════════════════════════════════════════════
radio_table — static definition of the radio hardware
═══════════════════════════════════════════════════════════════════
Describes the physical capabilities of each radio to the controller.
The controller uses this to know which frequencies and modes it supports.
*/
static struct json_object *build_athstats(struct json_object *stats,
const char *radio_name)
{
struct json_object *o = json_object_new_object();
struct json_object *v;
json_object_object_add(o, "name", json_object_new_string(radio_name));
json_object_object_add(o, "noise_floor", json_object_new_int(-95));
json_object_object_add(o, "satisfaction", json_object_new_int(-1));
json_object_object_add(o, "satisfaction_now", json_object_new_int(-1));
json_object_object_add(o, "satisfaction_real", json_object_new_int(-1));
static const char *const fields[] = {
"cu_total", "cu_self_rx", "cu_self_tx", "tx_packets", "tx_retries"
};
for (size_t i = 0; i < sizeof(fields) / sizeof(fields[0]); i++) {
if (stats && json_object_object_get_ex(stats, fields[i], &v))
json_object_object_add(o, fields[i], json_object_get(v));
else
json_object_object_add(o, fields[i], json_object_new_int(0));
}
if (stats && json_object_object_get_ex(stats, "noise", &v)) {
json_object_object_del(o, "noise_floor");
json_object_object_add(o, "noise_floor", json_object_get(v));
}
return o;
}
static void build_radio_table(struct json_object *root,
const uf_model_t *m,
struct json_object *radio_stats)
{
struct json_object *arr = json_object_new_array();
for (int i = 0; i < m->radio_table_len; i++) {
const uf_radio_t *r = &m->radio_table[i];
struct json_object *o = json_object_new_object();
struct json_object *stats = NULL;
struct json_object *value;
if (radio_stats && i < json_object_array_length(radio_stats))
stats = json_object_array_get_idx(radio_stats, i);
int nss = r->nss;
int tx_antennas = r->nss;
int rx_antennas = r->nss;
if (stats && json_object_object_get_ex(stats, "nss", &value) &&
json_object_get_int(value) > 0)
nss = json_object_get_int(value);
if (stats && json_object_object_get_ex(stats, "num_tx_antennas", &value) &&
json_object_get_int(value) > 0)
tx_antennas = json_object_get_int(value);
if (stats && json_object_object_get_ex(stats, "num_rx_antennas", &value) &&
json_object_get_int(value) > 0)
rx_antennas = json_object_get_int(value);
char mimo[16];
snprintf(mimo, sizeof(mimo), "%dx%d", tx_antennas, rx_antennas);
json_object_object_add(o, "name", json_object_new_string(r->name));
json_object_object_add(o, "radio", json_object_new_string(r->radio));
json_object_object_add(o, "channel", json_object_new_int(r->channel));
json_object_object_add(o, "ht", json_object_new_string(r->ht));
json_object_object_add(o, "min_txpower", json_object_new_int(r->min_txpower));
json_object_object_add(o, "max_txpower", json_object_new_int(r->max_txpower));
json_object_object_add(o, "nss", json_object_new_int(nss));
json_object_object_add(o, "max_nss", json_object_new_int(nss));
json_object_object_add(o, "num_tx_antennas", json_object_new_int(tx_antennas));
json_object_object_add(o, "num_rx_antennas", json_object_new_int(rx_antennas));
json_object_object_add(o, "mimo", json_object_new_string(mimo));
json_object_object_add(o, "tx_power", json_object_new_int(r->tx_power));
json_object_object_add(o, "radio_caps", json_object_new_int(r->radio_caps));
json_object_object_add(o, "radio_caps2", json_object_new_int(r->radio_caps2));
json_object_object_add(o, "antenna_gain", json_object_new_int(r->antenna_gain));
json_object_object_add(o, "he_enabled", json_object_new_boolean(r->he_enabled));
json_object_object_add(o, "builtin_antenna", json_object_new_boolean(true));
json_object_object_add(o, "builtin_ant_gain", json_object_new_int(0));
json_object_object_add(o, "athstats", build_athstats(stats, r->name));
char iface[32];
radio_runtime_iface(m, r->radio, iface, sizeof(iface));
json_object_object_add(o, "scan_table", build_scan_table(iface));
/*
* Native UniFi device records expose both radio_table[] and a
* top-level radio_<band> alias. Some controller views resolve
* capabilities such as NSS/MIMO through the alias.
*/
char alias[16];
snprintf(alias, sizeof(alias), "radio_%s", r->radio);
json_object_object_add(root, alias, json_object_get(o));
json_object_array_add(arr, o);
if (inform_debug_level() > 0)
LOG("Protocol radio capability: name=%s band=%s nss=%d tx_chains=%d rx_chains=%d",
r->name, r->radio, nss, tx_antennas, rx_antennas);
}
json_object_object_add(root, "radio_table", arr);
}
/* ═══════════════════════════════════════════════════════════════════
radio_table_stats — dynamic channel statistics
═══════════════════════════════════════════════════════════════════
Channel utilization is read in real time using:
iw dev wlan0 survey dump → active/busy/tx/rx time
iw dev wlan0 info → current channel, power
The controller displays this data in the RF view.
*/
static struct json_object *build_radio_table_stats(const uf_model_t *m)
{
struct json_object *arr = json_object_new_array();
for (int i = 0; i < m->radio_map_len; i++) {
const uf_radio_map_t *rm = &m->radio_map[i];
char wlan_iface[32];
radio_runtime_iface(m, rm->band, wlan_iface, sizeof(wlan_iface));
/* Radio name in the static table */
const char *radio_name = (i < m->radio_table_len)
? m->radio_table[i].name : wlan_iface;
int default_ch = (i < m->radio_table_len)
? m->radio_table[i].channel : 6;
int default_pwr = (i < m->radio_table_len)
? m->radio_table[i].tx_power : 20;
int nss = (i < m->radio_table_len)
? m->radio_table[i].nss : 1;
radio_stats_t rs;
if (sysinfo_radio(wlan_iface, &rs) != 0) {
memset(&rs, 0, sizeof(rs));
rs.noise = -95;
}
if (rs.nss > 0) nss = rs.nss;
int tx_antennas = rs.tx_antennas > 0 ? rs.tx_antennas : nss;
int rx_antennas = rs.rx_antennas > 0 ? rs.rx_antennas : nss;
char mimo[16];
snprintf(mimo, sizeof(mimo), "%dx%d", tx_antennas, rx_antennas);
struct json_object *o = json_object_new_object();
json_object_object_add(o, "name",
json_object_new_string(radio_name));
json_object_object_add(o, "radio",
json_object_new_string(rm->band));
json_object_object_add(o, "state",
json_object_new_string("RUN"));
json_object_object_add(o, "nss", json_object_new_int(nss));
json_object_object_add(o, "max_nss", json_object_new_int(nss));
json_object_object_add(o, "num_tx_antennas", json_object_new_int(tx_antennas));
json_object_object_add(o, "num_rx_antennas", json_object_new_int(rx_antennas));
json_object_object_add(o, "mimo", json_object_new_string(mimo));
json_object_object_add(o, "channel",
json_object_new_int(rs.channel ? rs.channel : default_ch));
json_object_object_add(o, "tx_power",
json_object_new_int(rs.tx_power ? rs.tx_power : default_pwr));
json_object_object_add(o, "cu_self_tx",
json_object_new_int(rs.cu_self_tx));
json_object_object_add(o, "cu_self_rx",
json_object_new_int(rs.cu_self_rx));
json_object_object_add(o, "cu_total",
json_object_new_int(rs.cu_total));
json_object_object_add(o, "num_sta",
json_object_new_int(rs.num_sta));
json_object_object_add(o, "noise",
json_object_new_int(rs.noise));
json_object_object_add(o, "tx_packets",
json_object_new_int64(rs.tx_packets));
json_object_object_add(o, "tx_retries",
json_object_new_int64(rs.tx_retries));
json_object_object_add(o, "wifi_tx_dropped",
json_object_new_int64(rs.tx_failed));
json_object_object_add(o, "tx_duration",
json_object_new_int64(rs.tx_duration));
json_object_object_add(o, "rx_duration",
json_object_new_int64(rs.rx_duration));
json_object_array_add(arr, o);
}
return arr;
}
/* ═══════════════════════════════════════════════════════════════════
port_table — Real/actual status of the ethernet ports
═══════════════════════════════════════════════════════════════════
/sys/class/net/<iface>/speed and operstate are read to
reflect the actual status of each port on the controller.
*/
static void build_port_table(struct json_object *root,
const uf_model_t *m)
{
struct json_object *arr = json_object_new_array();
for (int i = 0; i < m->port_table_len; i++) {
const uf_port_t *pt = &m->port_table[i];
iface_stats_t stats;
sysinfo_iface(pt->ifname, &stats);
struct json_object *o = json_object_new_object();
json_object_object_add(o, "ifname",
json_object_new_string(pt->ifname));
json_object_object_add(o, "name",
json_object_new_string(pt->name));
json_object_object_add(o, "port_idx",
json_object_new_int(pt->port_idx));
json_object_object_add(o, "poe_caps",
json_object_new_int(pt->poe_caps));
json_object_object_add(o, "media",
json_object_new_string(pt->media));
json_object_object_add(o, "speed",
json_object_new_int(stats.speed > 0 ? stats.speed : pt->speed));
json_object_object_add(o, "up",
json_object_new_boolean(stats.up));
json_object_object_add(o, "is_uplink",
json_object_new_boolean(pt->is_uplink));
json_object_object_add(o, "full_duplex",
json_object_new_boolean(stats.full_duplex));
json_object_object_add(o, "rx_bytes",
json_object_new_int64(stats.rx_bytes));
json_object_object_add(o, "tx_bytes",
json_object_new_int64(stats.tx_bytes));
json_object_array_add(arr, o);
}
json_object_object_add(root, "port_table", arr);
}
static void build_eth_table(struct json_object *root, const uf_model_t *m)
{
struct json_object *arr = json_object_new_array();
for (int i = 0; i < m->ethernet_table_len; i++) {
const uf_eth_entry_t *e = &m->ethernet_table[i];
struct json_object *o = json_object_new_object();
json_object_object_add(o, "name", json_object_new_string(e->name));
json_object_object_add(o, "num_port", json_object_new_int(e->num_port));
json_object_array_add(arr, o);
}
json_object_object_add(root, "ethernet_table", arr);
}
/* ═══════════════════════════════════════════════════════════════════
vap_table — active VAPs with connected clients (sta_table)
═══════════════════════════════════════════════════════════════════
For each active VAP in UCI:
1. Interface statistics for the wlan are read with sysinfo_iface()
2. The current channel is obtained with sysinfo_radio()
3. Clients are enumerated with clients_build_sta_table()
→ iw dev wlan0 station dump (signal, bitrate, bytes, uptime)
→ /proc/net/arp (MAC → IP)
→ /tmp/dhcp.leases (MAC → hostname)
The nested sta_table is what the controller uses to:
- Display clients on the dashboard
- Calculate per-client statistics
- Draw the network topology
*/
static struct json_object *build_vap_table(const uf_model_t *m)
{
/* Get list of VAPs from UCI */
struct json_object *uci_vaps = wlan_get_vap_table(m);
int nvaps = json_object_array_length(uci_vaps);
struct json_object *arr = json_object_new_array();
for (int i = 0; i < nvaps; i++) {
struct json_object *vap = json_object_array_get_idx(uci_vaps, i);
struct json_object *v;
const char *essid = "";
const char *vap_name = "";
const char *radio = "ng";
const char *bssid = "00:00:00:00:00:00";
const char *vap_id = NULL;
const char *ifname = NULL;
int vlan_id = 0;
int is_11r = 0;
int band_steering = 0;
int handoff_suggestions = 0;
if (json_object_object_get_ex(vap, "essid", &v)) essid = json_object_get_string(v);
if (json_object_object_get_ex(vap, "name", &v)) vap_name = json_object_get_string(v);
if (json_object_object_get_ex(vap, "radio", &v)) radio = json_object_get_string(v);
if (json_object_object_get_ex(vap, "bssid", &v)) bssid = json_object_get_string(v);
if (json_object_object_get_ex(vap, "id", &v)) vap_id = json_object_get_string(v);
if (json_object_object_get_ex(vap, "ifname", &v)) ifname = json_object_get_string(v);
if (json_object_object_get_ex(vap, "vlan_id", &v)) vlan_id = json_object_get_int(v);
if (json_object_object_get_ex(vap, "fast_roaming_enabled", &v))
is_11r = json_object_get_boolean(v);
if (json_object_object_get_ex(vap, "band_steering", &v))
band_steering = json_object_get_boolean(v);
if (json_object_object_get_ex(vap, "handoff_suggestions", &v))
handoff_suggestions = json_object_get_boolean(v);
const char *radio_name = radio;
int radio_nss = 1;
int radio_tx_antennas = 1;
int radio_rx_antennas = 1;
for (int j = 0; j < m->radio_table_len; j++) {
if (!strcmp(m->radio_table[j].radio, radio)) {
radio_name = m->radio_table[j].name;
radio_nss = m->radio_table[j].nss;
radio_tx_antennas = radio_nss;
radio_rx_antennas = radio_nss;
break;
}
}
/* Map band → wlan interface and current channel */
char wlan_iface[32] = "phy0-ap0";
if (ifname && ifname[0])
snprintf(wlan_iface, sizeof(wlan_iface), "%s", ifname);
int channel = 6;
for (int j = 0; j < m->radio_map_len; j++) {
if (strcmp(m->radio_map[j].band, radio) == 0) {
int idx = 0;
const char *device = wlan_device_for_band(
m, m->radio_map[j].band);
if (!device) device = m->radio_map[j].device;
sscanf(device, "radio%d", &idx);
if (!ifname || !ifname[0])
snprintf(wlan_iface, sizeof(wlan_iface), "phy%d-ap0", idx);
radio_stats_t rs;
if (sysinfo_radio(wlan_iface, &rs) == 0 && rs.channel)
channel = rs.channel;
else if (idx < m->radio_table_len)
channel = m->radio_table[idx].channel;
break;
}
}
/* Wireless interface statistics */
iface_stats_t iface_st;
sysinfo_iface(wlan_iface, &iface_st);
/* Clients connected to this VAP */
struct json_object *sta_tbl =
clients_build_sta_table(wlan_iface, radio, channel, vap_name,
vlan_id, is_11r);
int num_sta = json_object_array_length(sta_tbl);
/* Calculate tx_power of the corresponding radio */
int tx_pwr = 20;
radio_stats_t rs2;
if (sysinfo_radio(wlan_iface, &rs2) == 0) {
if (rs2.tx_power) tx_pwr = rs2.tx_power;
if (rs2.nss > 0) radio_nss = rs2.nss;
if (rs2.tx_antennas > 0) radio_tx_antennas = rs2.tx_antennas;
if (rs2.rx_antennas > 0) radio_rx_antennas = rs2.rx_antennas;
}
char radio_mimo[16];
snprintf(radio_mimo, sizeof(radio_mimo), "%dx%d", radio_tx_antennas, radio_rx_antennas);
struct json_object *o = json_object_new_object();
json_object_object_add(o, "essid",
json_object_new_string(essid));
json_object_object_add(o, "bssid",
json_object_new_string(bssid));
json_object_object_add(o, "name",
json_object_new_string(vap_name));
json_object_object_add(o, "radio",
json_object_new_string(radio));
json_object_object_add(o, "radio_name",
json_object_new_string(radio_name));
json_object_object_add(o, "nss", json_object_new_int(radio_nss));
json_object_object_add(o, "max_nss", json_object_new_int(radio_nss));
json_object_object_add(o, "num_tx_antennas", json_object_new_int(radio_tx_antennas));
json_object_object_add(o, "num_rx_antennas", json_object_new_int(radio_rx_antennas));
json_object_object_add(o, "mimo", json_object_new_string(radio_mimo));
json_object_object_add(o, "state",
json_object_new_string(iface_st.up ? "RUN" : "INIT"));
if (vlan_id > 0)
json_object_object_add(o, "vlan_id", json_object_new_int(vlan_id));
json_object_object_add(o, "up",
json_object_new_boolean(iface_st.up));
json_object_object_add(o, "channel",
json_object_new_int(channel));
json_object_object_add(o, "tx_power",
json_object_new_int(tx_pwr));
json_object_object_add(o, "band_steering",
json_object_new_boolean(band_steering));
json_object_object_add(o, "bss_transition",
json_object_new_boolean(handoff_suggestions));
json_object_object_add(o, "handoff_suggestions",
json_object_new_boolean(handoff_suggestions));
json_object_object_add(o, "num_sta",
json_object_new_int(num_sta));
json_object_object_add(o, "rx_bytes",
json_object_new_int64(iface_st.rx_bytes));
json_object_object_add(o, "tx_bytes",
json_object_new_int64(iface_st.tx_bytes));
json_object_object_add(o, "rx_packets",
json_object_new_int64(iface_st.rx_packets));
json_object_object_add(o, "tx_packets",
json_object_new_int64(iface_st.tx_packets));
json_object_object_add(o, "rx_errors",
json_object_new_int64(iface_st.rx_errors));
json_object_object_add(o, "tx_errors",
json_object_new_int64(iface_st.tx_errors));
json_object_object_add(o, "rx_dropped",
json_object_new_int64(iface_st.rx_dropped));
json_object_object_add(o, "tx_dropped",
json_object_new_int64(iface_st.tx_dropped));
long long tx_retries = 0, tx_failed = 0, rx_dropped = 0;
long long signal_sum = 0, ccq_sum = 0;
int signal_count = 0;
int sta_count = json_object_array_length(sta_tbl);
for (int j = 0; j < sta_count; j++) {
struct json_object *station = json_object_array_get_idx(sta_tbl, j);
struct json_object *counter;
if (json_object_object_get_ex(station, "signal", &counter) &&
json_object_get_int(counter) < 0) {
signal_sum += json_object_get_int(counter);
signal_count++;
}
if (json_object_object_get_ex(station, "ccq", &counter))
ccq_sum += json_object_get_int(counter);
if (json_object_object_get_ex(station, "tx_retries", &counter))
tx_retries += json_object_get_int64(counter);
if (json_object_object_get_ex(station, "tx_failed", &counter))
tx_failed += json_object_get_int64(counter);
if (json_object_object_get_ex(station, "rx_dropped", &counter))
rx_dropped += json_object_get_int64(counter);
}
json_object_object_add(o, "avg_client_signal", json_object_new_int(
signal_count ? (int)(signal_sum / signal_count) : 0));
json_object_object_add(o, "num_satisfaction_sta",
json_object_new_int(signal_count));
long long tx_attempts = iface_st.tx_packets + tx_retries;
json_object_object_add(o, "tx_retries", json_object_new_int64(tx_retries));
json_object_object_add(o, "tx_combined_retries",
json_object_new_int64(tx_retries));
json_object_object_add(o, "tx_rts_retries", json_object_new_int(0));
json_object_object_add(o, "tx_total", json_object_new_int64(tx_attempts));
json_object_object_add(o, "tx_success",
json_object_new_int64(iface_st.tx_packets));
json_object_object_add(o, "wifi_tx_attempts",
json_object_new_int64(tx_attempts));
json_object_object_add(o, "wifi_tx_dropped", json_object_new_int64(tx_failed));
json_object_object_add(o, "rx_frags", json_object_new_int64(iface_st.rx_frame));
json_object_object_add(o, "rx_crypts", json_object_new_int(0));
json_object_object_add(o, "rx_nwids", json_object_new_int64(rx_dropped));
/* Only controller-issued ObjectIds are valid in this field. */
if (vap_id)
json_object_object_add(o, "id", json_object_new_string(vap_id));
if (vap_id)
json_object_object_add(o, "wlanconf_id",
json_object_new_string(vap_id));
json_object_object_add(o, "usage",
json_object_new_string("user"));
json_object_object_add(o, "ccq",
json_object_new_int(signal_count ? (int)(ccq_sum / signal_count) : 0));
json_object_object_add(o, "t",
json_object_new_string("vap"));
/* Nested sta_table — clients of THIS VAP */
json_object_object_add(o, "sta_table", sta_tbl);
json_object_array_add(arr, o);
}
json_object_put(uci_vaps);
return arr;
}
/* Build the device-level station table UniFi uses for client ownership. */
static struct json_object *collect_sta_table(struct json_object *vap_table)
{
struct json_object *all = json_object_new_array();
int vap_count = json_object_array_length(vap_table);
for (int i = 0; i < vap_count; i++) {
struct json_object *vap = json_object_array_get_idx(vap_table, i);
struct json_object *stations;
if (!json_object_object_get_ex(vap, "sta_table", &stations) ||
!json_object_is_type(stations, json_type_array))
continue;
int count = json_object_array_length(stations);
for (int j = 0; j < count; j++)
json_object_array_add(all, json_object_get(
json_object_array_get_idx(stations, j)));
}
return all;
}
/* ═══════════════════════════════════════════════════════════════════
inform_build_payload — Complete assembly of the inform JSON
═══════════════════════════════════════════════════════════════════ */
char *inform_build_payload(const openuf_state_t *st,
const uf_model_t *m,
long uptime)
{
/* MAC without colons → serial (uppercase) */
char mac_clean[32] = {0};
{
const char *s = st->mac; int j = 0;
for (int i = 0; s[i] && j < 12; i++)
if (s[i] != ':') {
char c = s[i];
if (c >= 'a' && c <= 'f') c -= 32;
mac_clean[j++] = c;
}
}
char fw_version[64];
if (st->firmware_version[0])
snprintf(fw_version, sizeof(fw_version), "%s",
st->firmware_version);
else
snprintf(fw_version, sizeof(fw_version), "%s%s",
m->fw_pre, m->fw_ver);
char inform_url_buf[256];
if (st->inform_url[0])
strncpy(inform_url_buf, st->inform_url, sizeof(inform_url_buf)-1);
else
snprintf(inform_url_buf, sizeof(inform_url_buf),
"http://unifi:%d%s", INFORM_PORT, INFORM_PATH);
struct json_object *root = json_object_new_object();
/* ── Device identity ──────────────────────────────── */
json_object_object_add(root, "mac",
json_object_new_string(st->mac));
json_object_object_add(root, "serial",
json_object_new_string(mac_clean));
json_object_object_add(root, "model",
json_object_new_string(m->model));
json_object_object_add(root, "model_display",
json_object_new_string(m->model_display));
json_object_object_add(root, "display_name",
json_object_new_string(m->display_name));
json_object_object_add(root, "board_rev",
json_object_new_int(m->board_rev));
json_object_object_add(root, "version",
json_object_new_string(fw_version));
json_object_object_add(root, "bootrom_version",
json_object_new_string("openuf-v0.4"));
json_object_object_add(root, "required_version",
json_object_new_string("9.0.114"));
json_object_object_add(root, "ip",
json_object_new_string(st->ip));
json_object_object_add(root, "hostname",
json_object_new_string(st->hostname[0] ? st->hostname : m->display_name));
json_object_object_add(root, "inform_url",
json_object_new_string(inform_url_buf));
json_object_object_add(root, "uptime",
json_object_new_int64(uptime));
json_object_object_add(root, "time",
json_object_new_int64((long long)uptime));
json_object_object_add(root, "state",
json_object_new_int(st->adopted ? 4 : 1));
json_object_object_add(root, "default",
json_object_new_boolean(!st->adopted));
json_object_object_add(root, "cfgversion",
json_object_new_string(st->cfgversion));
json_object_object_add(root, "x_authkey",
json_object_new_string(st->adopted ? st->authkey : DEFAULT_AUTH_KEY));
json_object_object_add(root, "_default_key",
json_object_new_boolean(!st->adopted));
json_object_object_add(root, "has_eth1",
json_object_new_boolean(m->has_eth1));
json_object_object_add(root, "isolated",
json_object_new_boolean(false));
json_object_object_add(root, "locating",
json_object_new_boolean(false));
json_object_object_add(root, "uplink",
json_object_new_string("eth0"));
json_object_object_add(root, "country_code",
json_object_new_int(0));
/* Memory/load and percentage stats use distinct UniFi schemas. */
int cpu_percent;
double mem_percent;
json_object_object_add(root, "sys_stats",
build_sys_stats(&cpu_percent, &mem_percent));
json_object_object_add(root, "system-stats",
build_system_stats(cpu_percent, mem_percent, uptime));
/* ── Ethernet interfaces with real counters ──────────────── */
json_object_object_add(root, "if_table", build_if_table(m, st));
/* Native informs carry RF counters in radio_table[].athstats. Keep the
* normalized table too for controller versions that consume it directly. */
struct json_object *radio_stats = build_radio_table_stats(m);
build_radio_table(root, m, radio_stats);
json_object_object_add(root, "radio_table_stats", radio_stats);
/* ── Ethernet ports with actual status ───────────────────────── */
build_port_table(root, m);
build_eth_table(root, m);
/* Publish both per-VAP and device-level station views. */
struct json_object *vap_table = build_vap_table(m);
struct json_object *sta_table = collect_sta_table(vap_table);
int station_count = json_object_array_length(sta_table);
json_object_object_add(root, "vap_table", vap_table);
json_object_object_add(root, "sta_table", sta_table);
/* ── LLDP neighbors for visual topology ─────────────────────── */
json_object_object_add(root, "lldp_table", lldp_read_neighbors());
/* Global counters */
json_object_object_add(root, "bytes_r", json_object_new_int(0));
json_object_object_add(root, "bytes_d", json_object_new_int(0));
json_object_object_add(root, "num_sta", json_object_new_int(station_count));
const char *s = json_object_to_json_string(root);
/* Log shows what authkey is actually in the payload */
LOG("Payload state=%d, default=%s, adopted=%d, cfgversion=%s, config_applied=%d, x_authkey=%.8s...",
st->adopted ? 4 : 1,
!st->adopted ? "true" : "false",
st->adopted,
st->cfgversion,
st->config_applied,
st->authkey[0] ? st->authkey : "DEFAULT");
char *copy = strdup(s);
json_object_put(root);
return copy;
}
+360
View File
@@ -0,0 +1,360 @@
/*
* Handle controller commands, adoption state changes, provisioning results,
* firmware-version spoofing, and protocol-safe response diagnostics.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <json-c/json.h>
#include "inform.h"
#include "crypto.h"
#include "http.h"
#include "wlan.h"
#include "state.h"
#include "config.h"
#include "sysinfo.h"
#include "clients.h"
#include "lldp.h"
#include "inform_internal.h"
static int inform_valid_auth_key(const char *key)
{
if (!key || strlen(key) != 32)
return 0;
for (size_t i = 0; i < 32; i++)
if (!isxdigit((unsigned char)key[i]))
return 0;
return 1;
}
static int protocol_debug_level;
void inform_set_debug_level(int level)
{
protocol_debug_level = level < 0 ? 0 : level > 2 ? 2 : level;
}
int inform_debug_level(void)
{
return protocol_debug_level;
}
static int inform_debug_system_cfg_key(const char *key)
{
if (!key || (strncmp(key, "aaa.", 4) &&
strncmp(key, "wireless.", 9)))
return 0;
return strstr(key, ".ssid") || strstr(key, ".id") ||
strstr(key, ".vap_ind") || strstr(key, ".parent");
}
void inform_log_controller_response(struct json_object *response,
const char *raw_json)
{
if (protocol_debug_level <= 0 || !response)
return;
LOG("Protocol debug: decrypted controller response fields follow");
json_object_object_foreach(response, key, value) {
LOG("Protocol field: %s type=%s", key,
json_type_to_name(json_object_get_type(value)));
}
struct json_object *system_cfg_object;
if (json_object_object_get_ex(response, "system_cfg",
&system_cfg_object)) {
const char *system_cfg = json_object_get_string(system_cfg_object);
char *copy = system_cfg ? strdup(system_cfg) : NULL;
if (copy) {
char *save = NULL;
for (char *line = strtok_r(copy, "\n", &save);
line; line = strtok_r(NULL, "\n", &save)) {
line[strcspn(line, "\r")] = '\0';
char *equals = strchr(line, '=');
if (!equals)
continue;
*equals = '\0';
if (inform_debug_system_cfg_key(line))
LOG("Protocol system_cfg: %s=%s", line, equals + 1);
}
free(copy);
}
}
if (protocol_debug_level >= 2) {
LOG("WARNING: full decrypted response may contain credentials");
LOG("Protocol response JSON: %s", raw_json ? raw_json : "");
}
}
/* ═══════════════════════════════════════════════════════════════════
sys_stats — CPU and memory of the system
═══════════════════════════════════════════════════════════════════
The controller shows CPU and RAM in the device view.
We read /proc/stat and /proc/meminfo directly.
*/
static void reboot_openwrt(void)
{
LOG("Controller requested an OpenWrt reboot");
int status = system("/sbin/reboot");
if (status != 0)
LOG("OpenWrt reboot command failed with status=%d", status);
}
/* ═══════════════════════════════════════════════════════════════════
Process JSON command from the controller
═══════════════════════════════════════════════════════════════════
_type == "noop" → do nothing
_type == "reboot" → reboot OpenWrt
_type == "cmd" → set-adopt / legacy reboot / reset / locate
_type == "setstate" → apply radio_table + vap_table via UCI
_type == "setparam" → change a single parameter
*/
void inform_handle_response(openuf_state_t *st,
const uf_model_t *model,
struct json_object *resp,
char *action_out)
{
struct json_object *v;
const char *type = "noop";
if (json_object_object_get_ex(resp, "_type", &v) &&
json_object_is_type(v, json_type_string))
type = json_object_get_string(v);
LOG("Handling response type: %s", type);
/* An OpenWrt host cannot install UniFi firmware. Acknowledge the
* controller's request by reporting its target version from now on. */
if (!strcmp(type, "upgrade")) {
if (json_object_object_get_ex(resp, "version", &v) &&
json_object_is_type(v, json_type_string)) {
const char *version = json_object_get_string(v);
size_t len = strlen(version);
if (len > 0 && len < sizeof(st->firmware_version)) {
snprintf(st->firmware_version,
sizeof(st->firmware_version), "%s", version);
state_save(st);
LOG("Firmware upgrade spoofed; now reporting version=%s",
st->firmware_version);
strcpy(action_out, "upgrade-spoofed");
return;
}
}
LOG("Ignoring upgrade response without a valid version");
strcpy(action_out, "upgrade-invalid");
return;
}
/* ── noop ────────────────────────────────────────────────────── */
if (!strcmp(type, "noop")) {
strcpy(action_out, "noop");
return;
}
/* Modern controllers send reboot as a top-level response type rather
* than wrapping it in {"_type":"cmd","cmd":"reboot"}. */
if (!strcmp(type, "reboot")) {
strcpy(action_out, "reboot");
reboot_openwrt();
return;
}
/* ── setparam ────────────────────────────────────────────────── */
if (!strcmp(type, "setparam")) {
int received_adoption_key = 0;
int applied_system_cfg = 0;
/* First parse mgmt_cfg used by modern controllers. */
if (json_object_object_get_ex(resp, "mgmt_cfg", &v)) {
const char *mgmt_cfg = json_object_get_string(v);
LOG("Parsing mgmt_cfg: %s", mgmt_cfg);
/* Parse newline-separated key=value pairs. */
char cfg_copy[2048];
strncpy(cfg_copy, mgmt_cfg, sizeof(cfg_copy)-1);
cfg_copy[sizeof(cfg_copy)-1] = '\0';
char *line = strtok(cfg_copy, "\n");
while (line) {
char *eq = strchr(line, '=');
if (eq) {
*eq = '\0';
const char *key = line;
const char *val = eq + 1;
if (!strcmp(key, "authkey"))
LOG("mgmt_cfg param: authkey = %.8s...", val);
else
LOG("mgmt_cfg param: %s = %s", key, val);
if (!strcmp(key, "authkey")) {
if (inform_valid_auth_key(val) &&
strcmp(st->authkey, val) != 0) {
int replacing_key = st->authkey[0] &&
strcmp(st->authkey, DEFAULT_AUTH_KEY) != 0;
strncpy(st->authkey, val,
sizeof(st->authkey)-1);
st->authkey[sizeof(st->authkey)-1] = '\0';
received_adoption_key = 1;
LOG("%s device key from setparam",
replacing_key ? "Replaced" : "Accepted");
} else if (!inform_valid_auth_key(val)) {
LOG("Ignoring invalid authkey from setparam");
}
} else if (!strcmp(key, "cfgversion")) {
/*
* This is the version the controller wants, not proof
* that its setstate has been applied locally.
*/
LOG("Controller requested cfgversion=%s; currently applied=%s",
val, st->cfgversion);
} else if (!strcmp(key, "use_aes_gcm")) {
st->use_aes_gcm = !strcmp(val, "true") ||
!strcmp(val, "1");
LOG("AES-GCM %s for subsequent inform packets",
st->use_aes_gcm ? "enabled" : "disabled");
} else if (!strcmp(key, "mgmt_url")) {
/* Could save mgmt_url for future use */
}
/* Other management parameters are currently informational. */
}
line = strtok(NULL, "\n");
}
}
struct json_object *system_cfg_obj;
if (json_object_object_get_ex(resp, "system_cfg",
&system_cfg_obj)) {
const char *system_cfg = json_object_get_string(system_cfg_obj);
LOG("Applying legacy system_cfg, length=%zu",
strlen(system_cfg));
if (wlan_apply_system_cfg(system_cfg, model) == 0) {
applied_system_cfg = 1;
st->config_applied = true;
st->config_schema = OPENUF_CONFIG_SCHEMA;
if (json_object_object_get_ex(resp, "cfgversion", &v))
snprintf(st->cfgversion, sizeof(st->cfgversion), "%s",
json_object_get_string(v));
LOG("Legacy system_cfg applied successfully, cfgversion=%s",
st->cfgversion);
} else {
st->config_applied = false;
strncpy(st->cfgversion, "0",
sizeof(st->cfgversion) - 1);
LOG("Legacy system_cfg failed; requesting provisioning retry");
}
}
/* Fall back to the direct key/value format used by older controllers. */
if (json_object_object_get_ex(resp, "key", &v)) {
const char *key = json_object_get_string(v);
struct json_object *val_o;
if (json_object_object_get_ex(resp, "value", &val_o)) {
const char *val = json_object_get_string(val_o);
LOG("setparam key=%s val=%s", key, val);
if (!strcmp(key, "inform_url"))
strncpy(st->inform_url, val, sizeof(st->inform_url)-1);
else if (!strcmp(key, "authkey") && inform_valid_auth_key(val) &&
strcmp(st->authkey, val) != 0) {
int replacing_key = st->authkey[0] &&
strcmp(st->authkey, DEFAULT_AUTH_KEY) != 0;
strncpy(st->authkey, val, sizeof(st->authkey)-1);
st->authkey[sizeof(st->authkey)-1] = '\0';
received_adoption_key = 1;
LOG("%s device key from direct setparam",
replacing_key ? "Replaced" : "Accepted");
}
}
}
/*
* Modern controllers complete adoption by returning the per-device
* key in setparam. Mark the device adopted before its next inform so
* both the payload and packet encryption switch to that key.
*/
if (received_adoption_key) {
st->adopted = true;
LOG("Adoption completed through setparam; next inform will use the controller key");
}
state_save(st);
LOG("State saved after setparam");
strcpy(action_out, applied_system_cfg ? "provisioned" :
received_adoption_key ? "adopted" : "setparam");
return;
}
/* ── cmd ─────────────────────────────────────────────────────── */
if (!strcmp(type, "cmd")) {
const char *cmd = "";
if (json_object_object_get_ex(resp, "cmd", &v))
cmd = json_object_get_string(v);
if (!strcmp(cmd, "set-adopt") || !strcmp(cmd, "adopt")) {
if (json_object_object_get_ex(resp, "uri", &v))
strncpy(st->inform_url, json_object_get_string(v),
sizeof(st->inform_url)-1);
if (json_object_object_get_ex(resp, "key", &v))
strncpy(st->authkey, json_object_get_string(v),
sizeof(st->authkey)-1);
st->adopted = true;
state_save(st);
strcpy(action_out, "adopted");
LOG("Adopted successfully. Key: %.8s...", st->authkey);
} else if (!strcmp(cmd, "reboot")) {
strcpy(action_out, "reboot");
reboot_openwrt();
} else if (!strcmp(cmd, "reset")) {
strcpy(action_out, "reset");
system("rm -f " OPENUF_STATE_FILE);
reboot_openwrt();
} else if (!strcmp(cmd, "locate")) {
/* Blink LED — on OpenWrt: echo 1 > /sys/class/leds/.../trigger */
strcpy(action_out, "locate");
} else {
snprintf(action_out, 64, "cmd:%s", cmd);
}
return;
}
/* ── setstate — WiFi configuration from the controller ──────────── */
if (!strcmp(type, "setstate")) {
if (json_object_object_get_ex(resp, "cfgversion", &v))
snprintf(st->cfgversion, sizeof(st->cfgversion),
"%s", json_object_get_string(v));
struct json_object *rt = NULL, *vt = NULL;
json_object_object_get_ex(resp, "radio_table", &rt);
json_object_object_get_ex(resp, "vap_table", &vt);
int apply_ok = 0;
if (rt || vt) {
LOGF(stdout, "Applying controller WiFi configuration...");
apply_ok = wlan_apply_config(resp, model) == 0;
} else {
LOG("setstate contained neither radio_table nor vap_table");
}
st->config_applied = apply_ok;
if (apply_ok)
st->config_schema = OPENUF_CONFIG_SCHEMA;
if (!apply_ok) {
strncpy(st->cfgversion, "0", sizeof(st->cfgversion) - 1);
LOG("WiFi configuration failed; cfgversion reset so the controller retries");
}
state_save(st);
strcpy(action_out, apply_ok ? "setstate" : "setstate-failed");
return;
}
snprintf(action_out, 64, "unknown:%s", type);
}
+1 -1
View File
@@ -107,7 +107,7 @@ int lldp_send_frame(const char *ifname,
const char *model_desc, const char *model_desc,
int ttl) int ttl)
{ {
/* Socket raw — requiere root */ /* Raw packet sockets require root privileges or CAP_NET_RAW. */
int fd = socket(AF_PACKET, SOCK_RAW, htons(LLDP_ETHERTYPE)); int fd = socket(AF_PACKET, SOCK_RAW, htons(LLDP_ETHERTYPE));
if (fd < 0) return -1; /* EPERM without root → silent */ if (fd < 0) return -1; /* EPERM without root → silent */
+1 -1
View File
@@ -24,7 +24,7 @@
* TLV type=2 Port ID subtype=5(ifname), value="eth0" * TLV type=2 Port ID subtype=5(ifname), value="eth0"
* TLV type=3 TTL value=uint16_BE * TLV type=3 TTL value=uint16_BE
* TLV type=5 System Name value=hostname * TLV type=5 System Name value=hostname
* TLV type=6 System Desc value="modelo versión" * TLV type=6 System Description value="model version"
* TLV type=7 Capabilities cap=0x0040(WLAN-AP), en=0x0040 * TLV type=7 Capabilities cap=0x0040(WLAN-AP), en=0x0040
* TLV type=0 End of LLDPDU len=0 * TLV type=0 End of LLDPDU len=0
* *
+10 -12
View File
@@ -3,7 +3,7 @@
* *
* Main daemon. Loop with three tasks: * Main daemon. Loop with three tasks:
* 1. Announce UDP broadcast+multicast each 10s (discovery L2) * 1. Announce UDP broadcast+multicast each 10s (discovery L2)
* 2. Inform HTTP POST cifrado each 10s (adoption + telemetrics) * 2. Inform encrypted HTTP POST every 10s (adoption + telemetry)
* 3. LLDP Raw frame L2 each 30s (visual topology in UniFi) * 3. LLDP Raw frame L2 each 30s (visual topology in UniFi)
*/ */
@@ -174,20 +174,19 @@ int main(int argc, char *argv[])
controller_ip); controller_ip);
} else { } else {
state.inform_url[0] = '\0'; state.inform_url[0] = '\0';
fprintf(stderr, LOGF(stderr,
"[openuf] No controller configured and no IPv4 default " "No controller configured and no IPv4 default "
"gateway found\n"); "gateway found");
} }
} }
state_save(&state); state_save(&state);
printf("[openuf] Starting model=%-8s MAC=%s IP=%s\n", LOGF(stdout, "Starting version=%s model=%-8s MAC=%s IP=%s",
model->model, mac_str, ip_str); OPENUF_VERSION, model->model, mac_str, ip_str);
printf("[openuf] Controller: %s\n", state.inform_url); LOGF(stdout, "Controller: %s", state.inform_url);
printf("[openuf] Adopted: %s\n", state.adopted ? "yes" : "no"); LOGF(stdout, "Adopted: %s", state.adopted ? "yes" : "no");
printf("[openuf] LLDP available: %s\n", LOGF(stdout, "LLDP available: %s",
lldp_available() ? "yes (lldpd)" : "no (transmit only)"); lldp_available() ? "yes (lldpd)" : "no (transmit only)");
fflush(stdout);
LOG("Daemon started"); LOG("Daemon started");
@@ -212,8 +211,7 @@ int main(int argc, char *argv[])
time_t last_inform = 0; time_t last_inform = 0;
time_t last_lldp = 0; time_t last_lldp = 0;
printf("[openuf] Main loop started\n"); LOGF(stdout, "Main loop started");
fflush(stdout);
while (1) { while (1) {
time_t now = time(NULL); time_t now = time(NULL);
+1 -1
View File
@@ -8,7 +8,7 @@
#if ENABLE_LOGGING #if ENABLE_LOGGING
#include <stdio.h> #include <stdio.h>
extern FILE *log_fp; 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) #define LOG(fmt, ...) do { if (log_fp) openuf_log_emit(log_fp, __func__, fmt, ##__VA_ARGS__); } while(0)
#else #else
#define LOG(fmt, ...) do {} while(0) #define LOG(fmt, ...) do {} while(0)
#endif #endif
+1 -1
View File
@@ -3,7 +3,7 @@
#include <stdbool.h> #include <stdbool.h>
#define OPENUF_CONFIG_SCHEMA 6 #define OPENUF_CONFIG_SCHEMA 7
typedef struct { typedef struct {
bool adopted; bool adopted;
+266 -18
View File
@@ -38,6 +38,7 @@
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include <stdbool.h> #include <stdbool.h>
#include <time.h>
#include <net/if.h> #include <net/if.h>
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <sys/socket.h> #include <sys/socket.h>
@@ -200,6 +201,7 @@ int sysinfo_iface(const char *ifname, iface_stats_t *out)
&tb,&tp,&te,&td,&tf,&tcol,&tcomp,&tcarr); &tb,&tp,&te,&td,&tf,&tcol,&tcomp,&tcarr);
out->rx_bytes = rb; out->rx_packets = rp; out->rx_bytes = rb; out->rx_packets = rp;
out->rx_errors = re; out->rx_dropped = rd; out->rx_errors = re; out->rx_dropped = rd;
out->rx_frame = rframe;
out->rx_multicast= rmulti; out->rx_multicast= rmulti;
out->tx_bytes = tb; out->tx_packets = tp; out->tx_bytes = tb; out->tx_packets = tp;
out->tx_errors = te; out->tx_dropped = td; out->tx_errors = te; out->tx_dropped = td;
@@ -213,6 +215,46 @@ int sysinfo_iface(const char *ifname, iface_stats_t *out)
WiFi Radio WiFi Radio
═══════════════════════════════════════════════════════════════════ ═══════════════════════════════════════════════════════════════════
Survey counters are cumulative. Keep a small per-interface snapshot so
each inform reports utilization during the latest interval rather than an
average since the radio was started.
*/
typedef struct {
char iface[32];
long long active;
long long busy;
long long tx;
long long rx;
long long sta_tx_duration;
long long sta_rx_duration;
struct timespec duration_time;
int duration_valid;
int valid;
} survey_snapshot_t;
#define MAX_SURVEY_SNAPSHOTS 8
static survey_snapshot_t survey_snapshots[MAX_SURVEY_SNAPSHOTS];
static int utilization_percent(long long part, long long total)
{
if (part <= 0 || total <= 0) return 0;
long long value = (part * 100 + total / 2) / total;
if (value < 0) return 0;
if (value > 100) return 100;
return (int)value;
}
static int count_antenna_chains(unsigned int mask)
{
int count = 0;
while (mask) {
count += mask & 1U;
mask >>= 1;
}
return count;
}
/*
1. iw dev wlan0 info → channel and power 1. iw dev wlan0 info → channel and power
Example: Example:
Interface wlan0 Interface wlan0
@@ -239,6 +281,8 @@ int sysinfo_radio(const char *iface, radio_stats_t *out)
out->noise = -95; out->noise = -95;
char cmd[128]; char cmd[128];
int current_freq = 0;
int wiphy_index = -1;
/* iw dev <iface> info */ /* iw dev <iface> info */
snprintf(cmd, sizeof(cmd), "iw dev %s info 2>/dev/null", iface); snprintf(cmd, sizeof(cmd), "iw dev %s info 2>/dev/null", iface);
@@ -248,30 +292,83 @@ int sysinfo_radio(const char *iface, radio_stats_t *out)
char line[256]; char line[256];
while (fgets(line, sizeof(line), p)) { while (fgets(line, sizeof(line), p)) {
int ch; float mhz; int ch; float mhz;
if (sscanf(line, " channel %d (%f MHz)", &ch, &mhz) == 2) if (sscanf(line, " channel %d (%f MHz)", &ch, &mhz) == 2) {
out->channel = ch; out->channel = ch;
current_freq = (int)(mhz + 0.5f);
}
int index;
if (sscanf(line, " wiphy %d", &index) == 1)
wiphy_index = index;
float tp; float tp;
if (sscanf(line, " txpower %f dBm", &tp) == 1) if (sscanf(line, " txpower %f dBm", &tp) == 1)
out->tx_power = (int)tp; out->tx_power = (int)tp;
} }
pclose(p); pclose(p);
/* Read the physical radio rather than trusting the emulated model. */
if (wiphy_index >= 0) {
snprintf(cmd, sizeof(cmd), "iw phy phy%d info 2>/dev/null", wiphy_index);
p = popen(cmd, "r");
if (p) {
unsigned int available_tx = 0, available_rx = 0;
unsigned int configured_tx = 0, configured_rx = 0;
int max_mcs_nss = 0;
while (fgets(line, sizeof(line), p)) {
unsigned int tx_mask, rx_mask;
if (sscanf(line, " Configured Antennas: TX 0x%x RX 0x%x",
&tx_mask, &rx_mask) == 2) {
configured_tx = tx_mask;
configured_rx = rx_mask;
} else if (sscanf(line, " Available Antennas: TX 0x%x RX 0x%x",
&tx_mask, &rx_mask) == 2) {
available_tx = tx_mask;
available_rx = rx_mask;
}
int streams, top_mcs;
if (sscanf(line, " %d streams: MCS 0-%d", &streams, &top_mcs) == 2 &&
streams > max_mcs_nss)
max_mcs_nss = streams;
if (sscanf(line,
" HT TX/RX MCS rate indexes supported: 0-%d",
&top_mcs) == 1) {
int ht_nss = top_mcs / 8 + 1;
if (ht_nss > max_mcs_nss) max_mcs_nss = ht_nss;
}
}
pclose(p);
out->tx_antennas = count_antenna_chains(
configured_tx ? configured_tx : available_tx);
out->rx_antennas = count_antenna_chains(
configured_rx ? configured_rx : available_rx);
if (out->tx_antennas && out->rx_antennas)
out->nss = out->tx_antennas < out->rx_antennas
? out->tx_antennas : out->rx_antennas;
else
out->nss = max_mcs_nss;
}
}
/* iw dev <iface> survey dump */ /* iw dev <iface> survey dump */
snprintf(cmd, sizeof(cmd), "iw dev %s survey dump 2>/dev/null", iface); snprintf(cmd, sizeof(cmd), "iw dev %s survey dump 2>/dev/null", iface);
p = popen(cmd, "r"); p = popen(cmd, "r");
if (!p) return 0; if (!p) return 0;
long long active=0, busy=0, tx_t=0, rx_t=0; long long active=0, busy=0, tx_t=0, rx_t=0;
int in_use = 0; int selected = 0;
while (fgets(line, sizeof(line), p)) { while (fgets(line, sizeof(line), p)) {
if (strstr(line, "[in use]")) { int survey_freq;
in_use = 1; active=busy=tx_t=rx_t=0; continue; if (sscanf(line, " frequency: %d MHz", &survey_freq) == 1) {
} /* Some drivers omit the optional [in use] marker, especially on
if (!in_use) continue; * the secondary radio. Match the frequency reported by iw info. */
/* New frequency without [in use] resets the block */ selected = strstr(line, "[in use]") != NULL ||
if (strstr(line, "frequency:") && !strstr(line, "[in use]")) { (current_freq > 0 && survey_freq == current_freq);
in_use = 0; continue; if (selected)
active=busy=tx_t=rx_t=0;
continue;
} }
if (!selected) continue;
float noise; long long val; float noise; long long val;
if (sscanf(line, " noise: %f dBm", &noise) == 1) out->noise = (int)noise; 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 active time: %lld ms", &val) == 1) active = val;
@@ -281,18 +378,169 @@ int sysinfo_radio(const char *iface, radio_stats_t *out)
} }
pclose(p); pclose(p);
if (active > 0) { long long sample_active = active;
out->cu_total = (int)(busy * 100 / active); long long sample_busy = busy;
out->cu_self_tx = (int)(tx_t * 100 / active); long long sample_tx = tx_t;
out->cu_self_rx = (int)(rx_t * 100 / active); long long sample_rx = rx_t;
survey_snapshot_t *snapshot = NULL;
survey_snapshot_t *free_slot = NULL;
for (int i = 0; i < MAX_SURVEY_SNAPSHOTS; i++) {
if (survey_snapshots[i].valid &&
!strcmp(survey_snapshots[i].iface, iface)) {
snapshot = &survey_snapshots[i];
break;
}
if (!survey_snapshots[i].valid && !free_slot)
free_slot = &survey_snapshots[i];
}
if (!snapshot) snapshot = free_slot;
if (snapshot && snapshot->valid && active > snapshot->active &&
busy >= snapshot->busy && tx_t >= snapshot->tx && rx_t >= snapshot->rx) {
sample_active = active - snapshot->active;
sample_busy = busy - snapshot->busy;
sample_tx = tx_t - snapshot->tx;
sample_rx = rx_t - snapshot->rx;
} }
/* Number of associated clients */ if (snapshot && active > 0) {
snprintf(cmd, sizeof(cmd), snprintf(snapshot->iface, sizeof(snapshot->iface), "%s", iface);
"iw dev %s station dump 2>/dev/null | grep -c '^Station'", snapshot->active = active;
iface); snapshot->busy = busy;
snapshot->tx = tx_t;
snapshot->rx = rx_t;
snapshot->valid = 1;
}
if (sample_active > 0) {
out->cu_total = utilization_percent(sample_busy, sample_active);
out->cu_self_tx = utilization_percent(sample_tx, sample_active);
out->cu_self_rx = utilization_percent(sample_rx, sample_active);
}
/* Associated clients and counters exposed by nl80211. */
snprintf(cmd, sizeof(cmd), "iw dev %s station dump 2>/dev/null", iface);
p = popen(cmd, "r"); p = popen(cmd, "r");
if (p) { fscanf(p, "%d", &out->num_sta); pclose(p); } if (p) {
while (fgets(line, sizeof(line), p)) {
long long val;
if (!strncmp(line, "Station ", 8)) {
out->num_sta++;
} else if (sscanf(line, " tx packets: %lld", &val) == 1) {
out->tx_packets += val;
} else if (sscanf(line, " tx retries: %lld", &val) == 1) {
out->tx_retries += val;
} else if (sscanf(line, " tx failed: %lld", &val) == 1) {
out->tx_failed += val;
} else if (sscanf(line, " tx duration: %lld us", &val) == 1) {
out->tx_duration += val;
} else if (sscanf(line, " rx duration: %lld us", &val) == 1) {
out->rx_duration += val;
}
}
pclose(p);
}
/* Some drivers expose no survey busy time on their secondary radio but
* do expose per-station airtime durations. Use those interval counters as
* a conservative "This AP" fallback; interference remains zero because
* station data cannot measure neighboring transmitters. */
struct timespec now;
if (snapshot && clock_gettime(CLOCK_MONOTONIC, &now) == 0) {
long long elapsed_us = 0;
if (snapshot->duration_valid) {
elapsed_us = (now.tv_sec - snapshot->duration_time.tv_sec) * 1000000LL +
(now.tv_nsec - snapshot->duration_time.tv_nsec) / 1000LL;
}
if (snapshot->duration_valid && elapsed_us >= 1000000LL &&
out->tx_duration >= snapshot->sta_tx_duration &&
out->rx_duration >= snapshot->sta_rx_duration &&
out->cu_total == 0) {
long long tx_delta = out->tx_duration - snapshot->sta_tx_duration;
long long rx_delta = out->rx_duration - snapshot->sta_rx_duration;
out->cu_self_tx = utilization_percent(tx_delta, elapsed_us);
out->cu_self_rx = utilization_percent(rx_delta, elapsed_us);
out->cu_total = out->cu_self_tx + out->cu_self_rx;
if (out->cu_total > 100) out->cu_total = 100;
}
/* sysinfo_radio() is called more than once while building an inform.
* Ignore sub-second calls so they do not replace the interval base. */
if (!snapshot->duration_valid || elapsed_us >= 1000000LL) {
if (!snapshot->valid) {
snprintf(snapshot->iface, sizeof(snapshot->iface), "%s", iface);
snapshot->valid = 1;
}
snapshot->sta_tx_duration = out->tx_duration;
snapshot->sta_rx_duration = out->rx_duration;
snapshot->duration_time = now;
snapshot->duration_valid = 1;
}
}
return 0; return 0;
} }
static int frequency_to_channel(int frequency)
{
if (frequency == 2484) return 14;
if (frequency >= 2412 && frequency <= 2472)
return (frequency - 2407) / 5;
if (frequency >= 5000 && frequency <= 5895)
return (frequency - 5000) / 5;
if (frequency >= 5955 && frequency <= 7115)
return (frequency - 5950) / 5;
return 0;
}
int sysinfo_wifi_scan_cache(const char *iface, wifi_scan_t *out, int max_out)
{
if (!iface || !out || max_out <= 0) return 0;
char cmd[128];
snprintf(cmd, sizeof(cmd), "iw dev %s scan dump 2>/dev/null", iface);
FILE *p = popen(cmd, "r");
if (!p) return 0;
int count = 0;
wifi_scan_t *cur = NULL;
char line[512];
while (fgets(line, sizeof(line), p)) {
char bssid[32];
if (sscanf(line, "BSS %31[^ (]", bssid) == 1) {
if (count >= max_out) {
cur = NULL;
continue;
}
cur = &out[count++];
memset(cur, 0, sizeof(*cur));
snprintf(cur->bssid, sizeof(cur->bssid), "%s", bssid);
continue;
}
if (!cur) continue;
int value;
float signal;
char essid[64];
if (sscanf(line, " freq: %d", &value) == 1) {
cur->frequency = value;
cur->channel = frequency_to_channel(value);
} else if (sscanf(line, " signal: %f dBm", &signal) == 1) {
cur->signal = (int)signal;
value = (cur->signal + 100) * 2;
cur->rssi = value < 0 ? 0 : value > 100 ? 100 : value;
} else if (sscanf(line, " last seen: %d ms ago", &value) == 1) {
cur->age = value / 1000;
} else if (sscanf(line, " SSID: %63[^\n]", essid) == 1) {
snprintf(cur->essid, sizeof(cur->essid), "%s", essid);
} else if (strstr(line, "capability:") && strstr(line, "Privacy")) {
cur->secured = true;
} else if (strstr(line, "RSN:") || strstr(line, "WPA:")) {
cur->secured = true;
}
}
pclose(p);
return count;
}
+27 -2
View File
@@ -17,7 +17,7 @@
#include <stdbool.h> #include <stdbool.h>
/* ── Memoria ─────────────────────────────────────────────────────── */ /* ── Memory ─────────────────────────────────────────────────────── */
typedef struct { typedef struct {
long total_kb; long total_kb;
long free_kb; long free_kb;
@@ -33,7 +33,7 @@ int sysinfo_mem(mem_stats_t *out);
* With a 10s interval this gives a good average of usage. */ * With a 10s interval this gives a good average of usage. */
int sysinfo_cpu_percent(void); int sysinfo_cpu_percent(void);
/* ── Interfaz de red ─────────────────────────────────────────────── */ /* ── Network interface ─────────────────────────────────────────────── */
typedef struct { typedef struct {
char name[32]; char name[32];
char mac[32]; char mac[32];
@@ -49,6 +49,7 @@ typedef struct {
long long tx_errors; long long tx_errors;
long long rx_dropped; long long rx_dropped;
long long tx_dropped; long long tx_dropped;
long long rx_frame;
long long rx_multicast; long long rx_multicast;
} iface_stats_t; } iface_stats_t;
@@ -65,9 +66,33 @@ typedef struct {
int cu_self_rx; /* % time spent receiving */ int cu_self_rx; /* % time spent receiving */
int num_sta; int num_sta;
int noise; /* dBm */ int noise; /* dBm */
int nss; /* usable spatial streams */
int tx_antennas; /* configured TX chains */
int rx_antennas; /* configured RX chains */
long long tx_packets;
long long tx_retries;
long long tx_failed;
long long tx_duration; /* microseconds */
long long rx_duration; /* microseconds */
} radio_stats_t; } radio_stats_t;
/* iface: "wlan0", "wlan1" */ /* iface: "wlan0", "wlan1" */
int sysinfo_radio(const char *iface, radio_stats_t *out); int sysinfo_radio(const char *iface, radio_stats_t *out);
#define MAX_SCAN_RESULTS 128
typedef struct {
char bssid[32];
char essid[64];
int frequency;
int channel;
int signal; /* dBm */
int rssi; /* controller-compatible quality, 0-100 */
int age; /* seconds since last seen */
bool secured;
} wifi_scan_t;
/* Read the kernel's cached BSS list without starting a disruptive scan. */
int sysinfo_wifi_scan_cache(const char *iface, wifi_scan_t *out, int max_out);
#endif /* OPENUF_SYSINFO_H */ #endif /* OPENUF_SYSINFO_H */
-1787
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -15,9 +15,11 @@ void wlan_clear(void);
/* Apply radio-level settings from a UniFi radio_table entry. /* Apply radio-level settings from a UniFi radio_table entry.
* radio_json : JSON object with fields: channel, ht, tx_power * radio_json : JSON object with fields: channel, ht, tx_power
* device_name: OpenWrt radio device ("radio0", "radio1") */ * device_name: OpenWrt radio device ("radio0", "radio1")
* force_wifi4: cap the radio at 802.11n when requested by UniFi */
void wlan_apply_radio(struct json_object *radio_json, void wlan_apply_radio(struct json_object *radio_json,
const char *device_name); const char *device_name,
int force_wifi4);
/* Resolve the model's logical UniFi band against the bands advertised by /* Resolve the model's logical UniFi band against the bands advertised by
* the local PHYs. Falls back to the model mapping when discovery fails. */ * the local PHYs. Falls back to the model mapping when discovery fails. */
+229
View File
@@ -0,0 +1,229 @@
/*
* Shared translations and validators used by Wi-Fi provisioning and
* telemetry. This unit does not commit controller configuration itself.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <uci.h>
#include <json-c/json.h>
#include "wlan.h"
#include "ufmodel.h"
#include "crypto.h"
#include "wlan_internal.h"
const char *wlan_security_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 */
}
/* OpenWrt UCI encryption names to UniFi security names. */
const char *wlan_security_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";
}
/* Return true only for the 24-character hexadecimal IDs used by UniFi. */
int wlan_valid_object_id(const char *id)
{
if (!id || strlen(id) != 24)
return 0;
for (size_t i = 0; i < 24; i++)
if (!((id[i] >= '0' && id[i] <= '9') ||
(id[i] >= 'a' && id[i] <= 'f') ||
(id[i] >= 'A' && id[i] <= 'F')))
return 0;
return 1;
}
/*
* UniFi uses a 24-character hexadecimal MongoDB ObjectId to associate VAP
* telemetry and stations with a WLAN. Older legacy system_cfg payloads do
* not include that ID, so allocate a local one and keep it stable in UCI.
* All radio instances of the same WLAN must report the same ID.
*/
int wlan_ensure_vap_ids(struct json_object *vaps)
{
if (!vaps || !json_object_is_type(vaps, json_type_array))
return 0;
struct uci_context *ctx = uci_alloc_context();
struct uci_package *pkg = NULL;
if (ctx)
uci_load(ctx, "wireless", &pkg);
int count = json_object_array_length(vaps);
for (int i = 0; i < count; i++) {
struct json_object *vap = json_object_array_get_idx(vaps, i);
struct json_object *value;
if (!vap)
continue;
const char *id = NULL;
const char *id_keys[] = { "_id", "id", "wlanconf_id" };
for (size_t n = 0; n < sizeof(id_keys) / sizeof(id_keys[0]); n++) {
if (json_object_object_get_ex(vap, id_keys[n], &value)) {
const char *candidate = json_object_get_string(value);
if (wlan_valid_object_id(candidate)) {
id = candidate;
break;
}
}
}
if (id)
continue;
const char *ssid = "";
if (json_object_object_get_ex(vap, "essid", &value))
ssid = json_object_get_string(value);
/* Reuse an ID already assigned to this WLAN in the same payload. */
for (int j = 0; j < i && !id; j++) {
struct json_object *previous = json_object_array_get_idx(vaps, j);
struct json_object *previous_ssid;
struct json_object *previous_id;
if (previous &&
json_object_object_get_ex(previous, "essid", &previous_ssid) &&
!strcmp(json_object_get_string(previous_ssid), ssid) &&
json_object_object_get_ex(previous, "id", &previous_id) &&
wlan_valid_object_id(json_object_get_string(previous_id)))
id = json_object_get_string(previous_id);
}
/* Reuse the ID committed by an earlier provisioning cycle. */
if (!id && pkg) {
struct uci_element *element;
uci_foreach_element(&pkg->sections, element) {
struct uci_section *section = uci_to_section(element);
const char *stored_ssid;
const char *stored_id;
if (strcmp(section->type, "wifi-iface") ||
strncmp(section->e.name, "openuf_", 7))
continue;
stored_ssid = uci_lookup_option_string(ctx, section, "ssid");
stored_id = uci_lookup_option_string(ctx, section,
"openuf_vap_id");
if (stored_ssid && !strcmp(stored_ssid, ssid) &&
wlan_valid_object_id(stored_id)) {
id = stored_id;
break;
}
}
}
char generated[25];
if (!id) {
if (crypto_random_hex((unsigned char *)generated, 12) != 0) {
LOGF(stdout, "Failed to generate a VAP ID for '%s'", ssid);
if (pkg) uci_unload(ctx, pkg);
if (ctx) uci_free_context(ctx);
return -1;
}
id = generated;
LOGF(stdout, "Generated persistent VAP ID %s for '%s'",
id, ssid);
}
json_object_object_add(vap, "id", json_object_new_string(id));
json_object_object_add(vap, "wlanconf_id", json_object_new_string(id));
}
if (pkg) uci_unload(ctx, pkg);
if (ctx) uci_free_context(ctx);
return 0;
}
/* Build one stable 802.11r mobility domain shared by every AP for an SSID. */
void wlan_mobility_domain_for_ssid(const char *ssid, char out[5])
{
unsigned int hash = 2166136261u;
const unsigned char *p = (const unsigned char *)(ssid ? ssid : "");
while (*p) {
hash ^= *p++;
hash *= 16777619u;
}
snprintf(out, 5, "%04x", (hash ^ (hash >> 16)) & 0xffffu);
}
/* Return true when an OpenWrt radio is backed by the ath9k kernel driver. */
int wlan_radio_uses_ath9k(const char *device_name)
{
int phy_index;
if (!device_name || sscanf(device_name, "radio%d", &phy_index) != 1)
return 0;
char path[128];
char target[256];
snprintf(path, sizeof(path),
"/sys/class/ieee80211/phy%d/device/driver", phy_index);
ssize_t length = readlink(path, target, sizeof(target) - 1);
if (length < 0)
return 0;
target[length] = '\0';
return strstr(target, "ath9k") != NULL;
}
/* Read a UniFi boolean while accepting names used by controller versions. */
int wlan_json_boolean_any(struct json_object *object,
const char *const *keys, size_t key_count)
{
struct json_object *value;
for (size_t i = 0; i < key_count; i++) {
if (!json_object_object_get_ex(object, keys[i], &value))
continue;
if (json_object_is_type(value, json_type_string)) {
const char *text = json_object_get_string(value);
if (!text || !text[0] || !strcasecmp(text, "disabled") ||
!strcasecmp(text, "false") || !strcasecmp(text, "off") ||
!strcasecmp(text, "none") || !strcmp(text, "0"))
return 0;
/* Also accepts controller modes such as "prefer_5g". */
return 1;
}
return json_object_get_boolean(value) ? 1 : 0;
}
return 0;
}
/* Interpret Boolean text and UniFi feature modes such as "prefer_5g". */
int wlan_feature_enabled(const char *text)
{
return text && text[0] && strcasecmp(text, "disabled") &&
strcasecmp(text, "false") && strcasecmp(text, "off") &&
strcasecmp(text, "none") && strcmp(text, "0");
}
/* Safe UCI identifier fragment (maximum 15 characters). */
void wlan_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=='_')
out[j++] = c;
else
out[j++] = '_';
}
out[j] = '\0';
}
+270
View File
@@ -0,0 +1,270 @@
/*
* Parse the legacy newline-separated system_cfg representation and translate
* it into the same JSON configuration consumed by modern provisioning.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <uci.h>
#include <json-c/json.h>
#include "wlan.h"
#include "ufmodel.h"
#include "crypto.h"
#include "wlan_internal.h"
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;
while (value_len && line[key_len + value_len] == '\r')
value_len--;
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));
/* Preserve the WLAN ObjectId used to attach clients in topology. */
const char *id_suffixes[] = { "id", "_id", "wlanconf_id" };
for (size_t id_index = 0;
id_index < sizeof(id_suffixes) / sizeof(id_suffixes[0]);
id_index++) {
snprintf(key, sizeof(key), "aaa.%d.%s", i,
id_suffixes[id_index]);
if (system_cfg_get(system_cfg, key, value, sizeof(value)) &&
wlan_valid_object_id(value)) {
json_object_object_add(vap, "id", json_object_new_string(value));
json_object_object_add(vap, "wlanconf_id",
json_object_new_string(value));
break;
}
}
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));
/* UniFi's legacy Force WiFi 4 mode is carried as wireless.N.iot. */
snprintf(key, sizeof(key), "wireless.%d.iot", i);
if (system_cfg_get(system_cfg, key, value, sizeof(value)))
json_object_object_add(vap, "iot", json_object_new_boolean(
wlan_feature_enabled(value)));
char passphrase[256] = {0};
snprintf(key, sizeof(key), "aaa.%d.wpa.psk", i);
int has_passphrase = system_cfg_get(system_cfg, key, passphrase,
sizeof(passphrase));
if (!has_passphrase) {
snprintf(key, sizeof(key), "aaa.%d.sae.psk.1.psk", i);
has_passphrase = system_cfg_get(system_cfg, key, passphrase,
sizeof(passphrase));
}
char key_mgmt[256] = {0};
snprintf(key, sizeof(key), "aaa.%d.wpa.key.1.mgmt", i);
int has_key_mgmt = system_cfg_get(system_cfg, key, key_mgmt,
sizeof(key_mgmt));
int wpa3_support = 0;
snprintf(key, sizeof(key), "aaa.%d.wpa3.support", i);
if (system_cfg_get(system_cfg, key, value, sizeof(value)))
wpa3_support = wlan_feature_enabled(value);
int wpa3_transition = 0;
snprintf(key, sizeof(key), "aaa.%d.wpa3.transition", i);
if (system_cfg_get(system_cfg, key, value, sizeof(value)))
wpa3_transition = wlan_feature_enabled(value);
const char *security = "open";
if (has_key_mgmt && strstr(key_mgmt, "WPA-EAP")) {
security = wpa3_support ? "wpa3enterprise" : "wpa2enterprise";
} else if (has_key_mgmt && strstr(key_mgmt, "SAE")) {
/* A transition BSS permits both WPA2-PSK and SAE. */
security = wpa3_transition || strstr(key_mgmt, "WPA-PSK")
? "wpa3transition" : "wpa3";
} else if (has_passphrase) {
security = "wpa2psk";
}
json_object_object_add(vap, "security",
json_object_new_string(security));
int enterprise = !strcmp(security, "wpa2enterprise") ||
!strcmp(security, "wpa3enterprise");
if (has_passphrase && !enterprise) {
json_object_object_add(vap, "x_passphrase",
json_object_new_string(passphrase));
}
if (enterprise) {
snprintf(key, sizeof(key), "aaa.%d.radius.auth.1.ip", i);
if (system_cfg_get(system_cfg, key, value, sizeof(value)))
json_object_object_add(vap, "auth_server",
json_object_new_string(value));
snprintf(key, sizeof(key), "aaa.%d.radius.auth.1.port", i);
if (system_cfg_get(system_cfg, key, value, sizeof(value)))
json_object_object_add(vap, "auth_port",
json_object_new_int(atoi(value)));
snprintf(key, sizeof(key), "aaa.%d.radius.auth.1.secret", i);
if (system_cfg_get(system_cfg, key, value, sizeof(value)))
json_object_object_add(vap, "auth_secret",
json_object_new_string(value));
snprintf(key, sizeof(key), "aaa.%d.radius.acct.1.ip", i);
if (system_cfg_get(system_cfg, key, value, sizeof(value)))
json_object_object_add(vap, "acct_server",
json_object_new_string(value));
snprintf(key, sizeof(key), "aaa.%d.radius.acct.1.port", i);
if (system_cfg_get(system_cfg, key, value, sizeof(value)))
json_object_object_add(vap, "acct_port",
json_object_new_int(atoi(value)));
snprintf(key, sizeof(key), "aaa.%d.radius.acct.1.secret", i);
if (system_cfg_get(system_cfg, key, value, sizeof(value)))
json_object_object_add(vap, "acct_secret",
json_object_new_string(value));
}
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")));
const char *band_steer_suffixes[] = {
"band_steering", "band_steering_enabled", "band_steering_mode",
"steering"
};
for (size_t n = 0;
n < sizeof(band_steer_suffixes) /
sizeof(band_steer_suffixes[0]); n++) {
snprintf(key, sizeof(key), "aaa.%d.%s", i,
band_steer_suffixes[n]);
if (system_cfg_get(system_cfg, key, value, sizeof(value))) {
json_object_object_add(vap, "band_steering",
json_object_new_boolean(wlan_feature_enabled(value)));
break;
}
}
const char *handoff_suffixes[] = {
"bss_transition", "bss_transition_enabled",
"handoff_suggestions", "handoff_suggestions_enabled"
};
for (size_t n = 0;
n < sizeof(handoff_suffixes) / sizeof(handoff_suffixes[0]); n++) {
snprintf(key, sizeof(key), "aaa.%d.%s", i,
handoff_suffixes[n]);
if (system_cfg_get(system_cfg, key, value, sizeof(value))) {
json_object_object_add(vap, "bss_transition",
json_object_new_boolean(wlan_feature_enabled(value)));
break;
}
}
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);
LOGF(stdout, "Parsed legacy system_cfg: %zu radios, %zu VAPs",
json_object_array_length(radios), json_object_array_length(vaps));
int result = wlan_apply_config(root, model);
json_object_put(root);
return result;
}
+585
View File
@@ -0,0 +1,585 @@
/*
* Apply controller radio and VAP configuration, preserving the openuf_
* ownership boundary and committing the resulting UCI configuration.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <uci.h>
#include <json-c/json.h>
#include "wlan.h"
#include "ufmodel.h"
#include "crypto.h"
#include "wlan_internal.h"
static int apply_vap(struct uci_context *ctx,
struct uci_package *pkg,
struct json_object *vap_json,
const char *device_name,
const char *radio_band,
const char *mac_str,
int vap_idx)
{
struct json_object *v;
(void)mac_str;
const char *essid = "";
const char *security = "wpa2psk";
const char *pass = "";
const char *auth_server = NULL;
const char *auth_secret = NULL;
const char *acct_server = NULL;
const char *acct_secret = NULL;
int auth_port = 1812;
int acct_port = 1813;
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);
int enterprise = !strcmp(security, "wpa2enterprise") ||
!strcmp(security, "wpa3enterprise");
if (json_object_object_get_ex(vap_json, "auth_server", &v))
auth_server = json_object_get_string(v);
if (json_object_object_get_ex(vap_json, "auth_secret", &v))
auth_secret = json_object_get_string(v);
if (json_object_object_get_ex(vap_json, "auth_port", &v))
auth_port = json_object_get_int(v);
if (json_object_object_get_ex(vap_json, "acct_server", &v))
acct_server = json_object_get_string(v);
if (json_object_object_get_ex(vap_json, "acct_secret", &v))
acct_secret = json_object_get_string(v);
if (json_object_object_get_ex(vap_json, "acct_port", &v))
acct_port = json_object_get_int(v);
/* Resolve the final network before creating the VAP; never fail open. */
int vid = 0;
char target_network[32] = "lan";
if (json_object_object_get_ex(vap_json, "vlan_id", &v))
vid = json_object_get_int(v);
if (vid > 0) {
if (wlan_ensure_vlan_network(vid) != 0) {
LOGF(stdout, "Failed to configure VLAN network %d", vid);
return -1;
}
snprintf(target_network, sizeof(target_network), "vlan%d", vid);
}
/* Section name: openuf_<idx>_<ssid_safe> */
char safe[16] = {0};
wlan_safe_section_name(essid, safe, sizeof(safe));
char sec_name[48];
snprintf(sec_name, sizeof(sec_name), "openuf_%d_%s", vap_idx, safe);
if (wlan_uci_ensure_section(ctx, pkg, sec_name, "wifi-iface") != 0) {
LOGF(stdout, "Failed to create VAP section '%s'", sec_name);
return -1;
}
WLAN_UCI_SET(ctx, "wireless", sec_name, "device", device_name);
WLAN_UCI_SET(ctx, "wireless", sec_name, "mode", "ap");
WLAN_UCI_SET(ctx, "wireless", sec_name, "ssid", essid);
WLAN_UCI_SET(ctx, "wireless", sec_name, "network", target_network);
WLAN_UCI_SET(ctx, "wireless", sec_name, "encryption", wlan_security_to_uci(security));
/*
* Preserve the controller's WLAN configuration ID. Inform telemetry must
* refer to this ObjectId; a label such as "user" is not a valid VAP ID.
* Controller versions use different keys, so accept the known variants.
*/
const char *vap_id = NULL;
const char *id_keys[] = { "_id", "id", "wlanconf_id" };
for (size_t i = 0; i < sizeof(id_keys) / sizeof(id_keys[0]); i++) {
if (json_object_object_get_ex(vap_json, id_keys[i], &v)) {
const char *candidate = json_object_get_string(v);
if (wlan_valid_object_id(candidate)) {
vap_id = candidate;
break;
}
}
}
if (vap_id)
WLAN_UCI_SET(ctx, "wireless", sec_name, "openuf_vap_id", vap_id);
/* Password */
if (!enterprise && pass && pass[0] && strcmp(security,"open") != 0)
WLAN_UCI_SET(ctx, "wireless", sec_name, "key", pass);
if (enterprise) {
if (!auth_server || !auth_server[0] ||
!auth_secret || !auth_secret[0]) {
LOGF(stdout, "Refusing Enterprise VAP '%s': missing RADIUS "
"authentication server or secret", essid);
return -1;
}
char path[256];
char port[16];
snprintf(path, sizeof(path), "wireless.%s.auth_server", sec_name);
snprintf(port, sizeof(port), "%d", auth_port > 0 ? auth_port : 1812);
if (wlan_uci_add_list(ctx, path, auth_server) != 0 ||
wlan_uci_set_required(ctx, pkg, sec_name, "auth_port", port) != 0 ||
wlan_uci_set_required(ctx, pkg, sec_name, "auth_secret",
auth_secret) != 0) {
LOGF(stdout, "Failed to store RADIUS authentication for "
"VAP '%s'", essid);
return -1;
}
if (acct_server && acct_server[0]) {
if (!acct_secret || !acct_secret[0]) {
LOGF(stdout, "Refusing Enterprise VAP '%s': accounting "
"server has no secret", essid);
return -1;
}
snprintf(path, sizeof(path), "wireless.%s.acct_server", sec_name);
snprintf(port, sizeof(port), "%d",
acct_port > 0 ? acct_port : 1813);
if (wlan_uci_add_list(ctx, path, acct_server) != 0 ||
wlan_uci_set_required(ctx, pkg, sec_name, "acct_port", port) != 0 ||
wlan_uci_set_required(ctx, pkg, sec_name, "acct_secret",
acct_secret) != 0) {
LOGF(stdout, "Failed to store RADIUS accounting for "
"VAP '%s'", essid);
return -1;
}
}
}
/* hidden SSID */
int hidden = 0;
if (json_object_object_get_ex(vap_json, "hide_ssid", &v))
hidden = json_object_get_boolean(v) ? 1 : 0;
WLAN_UCI_SET_INT(ctx, "wireless", sec_name, "hidden", hidden);
/* Client isolation (guest network) */
int isolate = 0;
if (json_object_object_get_ex(vap_json, "guest_policy", &v))
isolate = json_object_get_boolean(v) ? 1 : 0;
WLAN_UCI_SET_INT(ctx, "wireless", sec_name, "isolate", isolate);
/* U-APSD (power saving for mobile clients) */
int uapsd = 1;
if (json_object_object_get_ex(vap_json, "uapsd", &v))
uapsd = json_object_get_boolean(v) ? 1 : 0;
WLAN_UCI_SET_INT(ctx, "wireless", sec_name, "uapsd", uapsd);
/* ── PMF (Protected Management Frames / 802.11w) ──────────────
* "disabled" → 0, "optional" → 1, "required" → 2
* WPA3-only requires PMF. WPA2/WPA3 transition mode must leave PMF
* optional so WPA2-only clients can still associate. */
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;
}
if (!strcmp(security,"wpa3") || !strcmp(security,"wpa3enterprise"))
pmf = 2;
else if (!strcmp(security,"wpa3transition") && pmf < 1)
pmf = 1;
WLAN_UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211w", pmf);
/* ── Fast Roaming (802.11r FT) ────────────────────────────────
* Allows clients to move between APs without re-authentication
* complete. The FT handshake only takes ~50ms vs ~200-300ms for a normal one. */
const char *ft_keys[] = {
"fast_roaming_enabled", "fast_roaming", "ft_enabled", "ieee80211r"
};
int ft = wlan_json_boolean_any(vap_json, ft_keys,
sizeof(ft_keys) / sizeof(ft_keys[0]));
/*
* This legacy 2.4 GHz ath9k PHY rejects every FT beacon tested, including
* WPA2 with PMF disabled. Preserve the controller request for telemetry,
* but disable 802.11r on this one unsupported PHY so the BSS can start.
*/
if (ft && radio_band && !strcmp(radio_band, "ng") &&
wlan_radio_uses_ath9k(device_name)) {
WLAN_UCI_SET_INT(ctx, "wireless", sec_name, "openuf_ft_requested", 1);
ft = 0;
LOGF(stdout, "Disabled FT on unsupported 2.4 GHz ath9k radio %s",
device_name);
}
if (ft) {
char mdomain[5];
wlan_mobility_domain_for_ssid(essid, mdomain);
WLAN_UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211r", 1);
WLAN_UCI_SET_INT(ctx, "wireless", sec_name, "ft_over_ds", 0);
WLAN_UCI_SET_INT(ctx, "wireless", sec_name, "ft_psk_generate_local", 1);
/* Local key generation avoids external R0KH/R1KH dependencies. */
WLAN_UCI_SET(ctx, "wireless", sec_name, "mobility_domain", mdomain);
} else {
WLAN_UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211r", 0);
}
/* Enable the hostapd capabilities used by steering and 802.11v hints. */
const char *band_steer_keys[] = {
"band_steering", "band_steering_enabled", "band_steering_mode",
"steering_enabled"
};
const char *handoff_keys[] = {
"bss_transition", "bss_transition_enabled",
"bss_transition_management", "handoff_suggestions",
"handoff_suggestions_enabled", "ieee80211v"
};
int band_steer = wlan_json_boolean_any(
vap_json, band_steer_keys,
sizeof(band_steer_keys) / sizeof(band_steer_keys[0]));
int handoff = wlan_json_boolean_any(
vap_json, handoff_keys,
sizeof(handoff_keys) / sizeof(handoff_keys[0]));
int rrm = band_steer || handoff;
int bss_transition_requested = band_steer || handoff;
WLAN_UCI_SET_INT(ctx, "wireless", sec_name, "openuf_band_steering",
band_steer);
WLAN_UCI_SET_INT(ctx, "wireless", sec_name, "openuf_handoff_suggestions",
handoff);
WLAN_UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211k", rrm);
WLAN_UCI_SET_INT(ctx, "wireless", sec_name, "rrm_neighbor_report", rrm);
WLAN_UCI_SET_INT(ctx, "wireless", sec_name, "rrm_beacon_report", rrm);
/* Enable 802.11v at runtime after hostapd starts. Putting this option
* in UCI makes builds without CONFIG_WNM_AP reject the entire BSS. */
if (bss_transition_requested)
WLAN_UCI_SET_INT(ctx, "wireless", sec_name,
"openuf_bss_transition_requested", 1);
/* Record the controller VLAN for telemetry and diagnostics. */
if (vid > 0)
WLAN_UCI_SET_INT(ctx, "wireless", sec_name, "vlan_id", vid);
/* Reassert and validate every option required to start a secure AP. */
if (wlan_uci_set_required(ctx, pkg, sec_name, "device", device_name) != 0) {
LOGF(stdout, "Failed to bind VAP '%s' to %s",
essid, device_name);
return -1;
}
if (wlan_uci_set_required(ctx, pkg, sec_name, "mode", "ap") != 0 ||
wlan_uci_set_required(ctx, pkg, sec_name, "ssid", essid) != 0 ||
wlan_uci_set_required(ctx, pkg, sec_name, "encryption",
wlan_security_to_uci(security)) != 0) {
LOGF(stdout, "Refusing incomplete VAP '%s': core AP options "
"could not be stored", essid);
return -1;
}
if (!enterprise && pass && pass[0] && strcmp(security, "open") != 0 &&
wlan_uci_set_required(ctx, pkg, sec_name, "key", pass) != 0) {
LOGF(stdout, "Refusing unsecured VAP '%s': key could not be stored",
essid);
return -1;
}
if (wlan_uci_set_required(ctx, pkg, sec_name, "network", target_network) != 0) {
LOGF(stdout, "Refusing unsafe VAP '%s': cannot bind to %s",
essid, target_network);
return -1;
}
LOGF(stdout, "VAP '%s' -> %s device=%s network=%s enc=%s "
"ft=%d bs=%d handoff=%d pmf=%d",
essid, sec_name, device_name, target_network, wlan_security_to_uci(security),
ft, band_steer, handoff, pmf);
return 0;
}
/* True when a UniFi WLAN on this band explicitly requests WiFi 4 mode. */
static int force_wifi4_for_band(struct json_object *vaps, const char *band)
{
if (!vaps || !json_object_is_type(vaps, json_type_array))
return 0;
const char *force_keys[] = {
"iot", "force_wifi4", "force_wifi4_mode"
};
int count = json_object_array_length(vaps);
for (int i = 0; i < count; i++) {
struct json_object *vap = json_object_array_get_idx(vaps, i);
struct json_object *value;
if (!vap || !wlan_json_boolean_any(vap, force_keys,
sizeof(force_keys) /
sizeof(force_keys[0])))
continue;
const char *vap_band = NULL;
if (json_object_object_get_ex(vap, "radio", &value))
vap_band = json_object_get_string(value);
if (!vap_band || !vap_band[0] || !strcmp(vap_band, "both") ||
!strcmp(vap_band, "all") || !strcmp(vap_band, band) ||
(!strcmp(vap_band, "2g") && !strcmp(band, "ng")) ||
(!strcmp(vap_band, "5g") && !strcmp(band, "na")) ||
(!strcmp(vap_band, "6GHz") && !strcmp(band, "6g")))
return 1;
}
return 0;
}
/* ═══════════════════════════════════════════════════════════════════
wlan_apply_config — apply the controller's full configuration
═══════════════════════════════════════════════════════════════════
Called from inform.c → handle_response() when _type=="setstate".
config_json is the controller's complete JSON.
Process:
1. Remove old VAPs (openuf_ prefix)
2. Apply radio_table (channel, power, htmode) per radio
3. Create one VAP for each entry in vap_table
4. Commit UCI
5. Run "wifi reload" to apply without rebooting the 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);
/* Fill IDs omitted by legacy provisioning before old UCI VAPs are removed. */
if (wlan_ensure_vap_ids(vt_arr) != 0)
return -1;
/* Get the AP's MAC for 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';
}
/* Stop hostapd so a deleted BSS cannot survive a netifd reload race. */
LOGF(stdout, "Stopping Wi-Fi before controller provisioning...");
system("wifi down >/dev/null 2>&1");
/* Replace prior openuf_ VAPs while preserving unrelated UCI sections. */
wlan_clear();
/* 2. Apply 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;
/* Find the UCI device corresponding to this band */
const char *radio_band = "";
if (json_object_object_get_ex(r, "radio", &v))
radio_band = json_object_get_string(v);
const char *device_name = wlan_device_for_band(model, radio_band);
if (!device_name) {
LOGF(stdout, "Ignoring settings for unknown radio '%s'",
radio_band);
continue;
}
wlan_apply_radio(r, device_name,
force_wifi4_for_band(vt_arr, radio_band));
}
}
/*
* 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) {
LOGF(stdout, "Failed to allocate UCI context");
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");
LOGF(stdout, "Failed to load UCI wireless configuration: %s",
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)) {
WLAN_UCI_SET(ctx, "wireless", section->e.name, "disabled", "1");
disabled_defaults++;
}
}
if (disabled_defaults)
LOGF(stdout, "Disabled %d default OpenWrt VAPs",
disabled_defaults);
/* 3. Create VAPs and determine whether any WLAN requests steering. */
int steering_policy_enabled = 0;
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;
const char *band_steer_keys[] = {
"band_steering", "band_steering_enabled",
"band_steering_mode", "steering_enabled"
};
const char *handoff_keys[] = {
"bss_transition", "bss_transition_enabled",
"bss_transition_management", "handoff_suggestions",
"handoff_suggestions_enabled", "ieee80211v"
};
if (wlan_json_boolean_any(vap, band_steer_keys,
sizeof(band_steer_keys) /
sizeof(band_steer_keys[0])) ||
wlan_json_boolean_any(vap, handoff_keys,
sizeof(handoff_keys) /
sizeof(handoff_keys[0])))
steering_policy_enabled = 1;
/* A VAP without an explicit band is a model-wide WLAN. */
const char *radio_band = NULL;
if (json_object_object_get_ex(vap, "radio", &v))
radio_band = json_object_get_string(v);
if (radio_band && !strcmp(radio_band, "2g")) radio_band = "ng";
if (radio_band && !strcmp(radio_band, "5g")) radio_band = "na";
if (radio_band && !strcmp(radio_band, "6GHz")) radio_band = "6g";
int all_radios = !radio_band || !radio_band[0] ||
!strcmp(radio_band, "both") ||
!strcmp(radio_band, "all");
int applied = 0;
for (int j = 0; j < model->radio_map_len; j++) {
if (!all_radios &&
strcmp(model->radio_map[j].band, radio_band))
continue;
int section_idx = i * model->radio_map_len + j;
const char *device = wlan_device_for_band(
model, model->radio_map[j].band);
if (!device || apply_vap(ctx, pkg, vap, device,
model->radio_map[j].band,
mac_str, section_idx) != 0) {
uci_unload(ctx, pkg);
uci_free_context(ctx);
return -1;
}
applied++;
}
if (!applied) {
LOGF(stdout, "Ignoring VAP with unknown radio '%s'",
radio_band ? radio_band : "");
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");
LOGF(stdout, "Failed to commit UCI wireless configuration: %s",
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);
if (wlan_configure_band_steering(steering_policy_enabled) != 0)
LOGF(stdout, "Failed to configure the band steering policy");
/*
* Reload netifd for generated VLAN devices, then bring the radios up one
* at a time. Some dual-ath9k devices intermittently fail their first beacon
* setup after ACS. Start and verify each PHY independently, retrying a failed radio so
* provisioning cannot leave one band visible but unusable.
*/
LOGF(stdout, "Starting controller-managed Wi-Fi sequentially...");
system("ubus call network reload >/dev/null 2>&1");
for (int i = 0; i < model->radio_map_len; i++) {
char command[256];
const char *device = wlan_device_for_band(
model, model->radio_map[i].band);
/* Model radio names are internal constants, but validate defensively. */
if (!device ||
strspn(device,
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-") !=
strlen(device)) {
LOGF(stdout, "Refusing invalid radio name");
continue;
}
int radio_up = 0;
for (int attempt = 1; attempt <= 2 && !radio_up; attempt++) {
LOGF(stdout, "Starting %s (%s), attempt %d...",
device, model->radio_map[i].band, attempt);
snprintf(command, sizeof(command),
"wifi up %s >/dev/null 2>&1", device);
system(command);
/* ACS normally takes 6-8 seconds on this ath9k hardware. */
sleep(10);
int phy_index = -1;
if (sscanf(device, "radio%d", &phy_index) != 1)
phy_index = -1;
snprintf(command, sizeof(command),
"iw dev phy%d-ap0 info 2>/dev/null | "
"grep -q '^[[:space:]]*ssid ' && echo true",
phy_index);
FILE *status = popen(command, "r");
if (status) {
char value[16] = {0};
if (fgets(value, sizeof(value), status) &&
!strncmp(value, "true", 4))
radio_up = 1;
pclose(status);
}
if (!radio_up)
LOGF(stdout, "%s did not reach the up state; retrying",
device);
}
if (!radio_up)
LOGF(stdout, "%s failed after 2 start attempts", device);
}
/* Enable management features only after hostapd has registered each BSS.
* Unsupported WNM methods fail harmlessly without preventing AP startup. */
if (steering_policy_enabled) {
for (int i = 0; i < model->radio_map_len; i++) {
const char *device = wlan_device_for_band(
model, model->radio_map[i].band);
int phy_index = -1;
if (!device || sscanf(device, "radio%d", &phy_index) != 1)
continue;
char command[256];
snprintf(command, sizeof(command),
"ubus -S call hostapd.phy%d-ap0 bss_mgmt_enable "
"%c{ \"neighbor_report\": true, "
"\"beacon_report\": true, "
"\"bss_transition\": true }%c >/dev/null 2>&1",
phy_index, 39, 39);
if (system(command) != 0)
LOGF(stdout, "hostapd on phy%d lacks runtime 802.11v "
"support; continuing without BSS Transition",
phy_index);
}
}
/* Restart after hostapd has registered both BSSes on ubus. */
system("/etc/init.d/usteer restart >/dev/null 2>&1");
return 0;
}
+317
View File
@@ -0,0 +1,317 @@
/*
* Resolve model bands to local PHYs and apply channel, width, generation, and
* transmit-power settings to OpenWrt radio devices.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <uci.h>
#include <json-c/json.h>
#include "wlan.h"
#include "ufmodel.h"
#include "crypto.h"
#include "wlan_internal.h"
#define MAX_RESOLVED_RADIOS 8
typedef struct {
char device[32];
unsigned int bands;
} radio_capability_t;
static const uf_model_t *resolved_model;
static char resolved_devices[MAX_RESOLVED_RADIOS][32];
static unsigned int band_bit(const char *band)
{
if (!band) return 0;
if (!strcmp(band, "ng") || !strcmp(band, "2g")) return 1u;
if (!strcmp(band, "na") || !strcmp(band, "5g")) return 2u;
if (!strcmp(band, "6g") || !strcmp(band, "6GHz")) return 4u;
return 0;
}
static int bit_count(unsigned int value)
{
int count = 0;
while (value) {
count += value & 1u;
value >>= 1;
}
return count;
}
/* OpenWrt's generated radioN and phyN indices correspond for mac80211
* devices. Read actual frequencies instead of assuming PHY band order. */
static unsigned int detect_radio_bands(const char *device)
{
int phy_index = -1;
char command[96];
char line[256];
unsigned int bands = 0;
if (!device || sscanf(device, "radio%d", &phy_index) != 1 || phy_index < 0)
return 0;
snprintf(command, sizeof(command), "iw phy phy%d info 2>/dev/null", phy_index);
FILE *pipe = popen(command, "r");
if (!pipe) return 0;
while (fgets(line, sizeof(line), pipe)) {
char *mhz = strstr(line, " MHz [");
if (!mhz || strstr(line, "(disabled)"))
continue;
char *start = mhz;
while (start > line &&
((start[-1] >= '0' && start[-1] <= '9') || start[-1] == '.'))
start--;
double frequency = strtod(start, NULL);
if (frequency >= 2300.0 && frequency < 3000.0)
bands |= 1u;
else if (frequency >= 4900.0 && frequency < 5925.0)
bands |= 2u;
else if (frequency >= 5925.0 && frequency < 7200.0)
bands |= 4u;
}
pclose(pipe);
return bands;
}
static void resolve_radio_map(const uf_model_t *model)
{
if (!model || resolved_model == model)
return;
memset(resolved_devices, 0, sizeof(resolved_devices));
resolved_model = model;
int count = model->radio_map_len;
if (count > MAX_RESOLVED_RADIOS)
count = MAX_RESOLVED_RADIOS;
radio_capability_t caps[MAX_RESOLVED_RADIOS] = {0};
int used[MAX_RESOLVED_RADIOS] = {0};
for (int i = 0; i < count; i++) {
snprintf(caps[i].device, sizeof(caps[i].device), "%s",
model->radio_map[i].device);
caps[i].bands = detect_radio_bands(caps[i].device);
}
for (int i = 0; i < count; i++) {
unsigned int wanted = band_bit(model->radio_map[i].band);
int best = -1;
int best_band_count = 99;
for (int j = 0; j < count; j++) {
if (used[j] || !(caps[j].bands & wanted))
continue;
int supported = bit_count(caps[j].bands);
if (supported < best_band_count) {
best = j;
best_band_count = supported;
}
}
if (best >= 0) {
used[best] = 1;
snprintf(resolved_devices[i], sizeof(resolved_devices[i]), "%s",
caps[best].device);
} else {
snprintf(resolved_devices[i], sizeof(resolved_devices[i]), "%s",
model->radio_map[i].device);
}
LOGF(stdout, "Radio mapping: %s -> %s%s",
model->radio_map[i].band, resolved_devices[i],
best >= 0 ? " (detected)" : " (model fallback)");
}
}
const char *wlan_device_for_band(const uf_model_t *model, const char *band)
{
if (!model || !band) return NULL;
resolve_radio_map(model);
for (int i = 0; i < model->radio_map_len; i++)
if (!strcmp(model->radio_map[i].band, band))
return i < MAX_RESOLVED_RADIOS && resolved_devices[i][0]
? resolved_devices[i] : model->radio_map[i].device;
return NULL;
}
const char *wlan_band_for_device(const uf_model_t *model, const char *device)
{
if (!model || !device) return NULL;
resolve_radio_map(model);
for (int i = 0; i < model->radio_map_len; i++) {
const char *mapped = i < MAX_RESOLVED_RADIOS && resolved_devices[i][0]
? resolved_devices[i]
: model->radio_map[i].device;
if (!strcmp(mapped, device))
return model->radio_map[i].band;
}
return NULL;
}
/* UniFi security names to OpenWrt UCI encryption names. */
/* Return the newest 802.11 generation advertised by the local PHY. */
static enum wifi_standard radio_max_standard(const char *device_name)
{
int phy_index = -1;
char command[96];
char line[256];
enum wifi_standard standard = WIFI_STANDARD_UNKNOWN;
if (!device_name || sscanf(device_name, "radio%d", &phy_index) != 1 ||
phy_index < 0)
return WIFI_STANDARD_UNKNOWN;
snprintf(command, sizeof(command), "iw phy phy%d info 2>/dev/null",
phy_index);
FILE *pipe = popen(command, "r");
if (!pipe)
return WIFI_STANDARD_UNKNOWN;
while (fgets(line, sizeof(line), pipe)) {
if (strstr(line, "EHT Iftypes"))
standard = WIFI_STANDARD_7;
else if (standard < WIFI_STANDARD_6 && strstr(line, "HE Iftypes"))
standard = WIFI_STANDARD_6;
else if (standard < WIFI_STANDARD_5 &&
strstr(line, "VHT Capabilities"))
standard = WIFI_STANDARD_5;
else if (standard < WIFI_STANDARD_4 && strstr(line, "Capabilities:"))
standard = WIFI_STANDARD_4;
}
pclose(pipe);
return standard;
}
/* Keep the controller-selected width but use the newest PHY generation. */
static void select_htmode(const char *device_name, const char *radio_band,
const char *requested, int force_wifi4,
char *result, size_t result_size)
{
enum wifi_standard standard = radio_max_standard(device_name);
int width = 20;
const char *number = requested;
while (number && *number && (*number < '0' || *number > '9'))
number++;
if (number && *number)
width = atoi(number);
if (force_wifi4) {
/* 802.11n cannot use 80 MHz or wider channels. */
standard = WIFI_STANDARD_4;
if (width > 40)
width = 40;
} else if (standard == WIFI_STANDARD_5 && radio_band &&
(!strcmp(radio_band, "ng") || !strcmp(radio_band, "2g"))) {
/* OpenWrt does not use VHT modes on the 2.4 GHz band. */
standard = WIFI_STANDARD_4;
}
const char *prefix = standard == WIFI_STANDARD_7 ? "EHT" :
standard == WIFI_STANDARD_6 ? "HE" :
standard == WIFI_STANDARD_5 ? "VHT" :
standard == WIFI_STANDARD_4 ? "HT" : NULL;
if (!prefix) {
snprintf(result, result_size, "%s", requested ? requested : "HT20");
return;
}
snprintf(result, result_size, "%s%d", prefix, width);
}
/* ═══════════════════════════════════════════════════════════════════
wlan_apply_radio — apply radio config (channel, HT, power)
═══════════════════════════════════════════════════════════════════
Reading parameters from the controller's JSON:
channel → wireless.<device>.channel
ht → wireless.<device>.htmode ("HT20" / "HT40" / "HT80" / "HE80")
tx_power → wireless.<device>.txpower
min_rssi → not mapped to UCI (requires an external daemon)
*/
void wlan_apply_radio(struct json_object *radio_json,
const char *device_name,
int force_wifi4)
{
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. */
const char *radio_band = NULL;
if (json_object_object_get_ex(radio_json, "radio", &v)) {
radio_band = json_object_get_string(v);
const char *band = !strcmp(radio_band, "ng") ? "2g" :
!strcmp(radio_band, "na") ? "5g" :
!strcmp(radio_band, "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);
}
}
if (json_object_object_get_ex(radio_json, "ht", &v)) {
const char *requested = json_object_get_string(v);
char htmode[16];
select_htmode(device_name, radio_band, requested, force_wifi4,
htmode, sizeof(htmode));
snprintf(path, sizeof(path), "wireless.%s.htmode=%s",
device_name, htmode);
struct uci_ptr ptr;
if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK)
uci_set(ctx, &ptr);
LOGF(stdout, "Radio %s standard: %s%s", device_name, htmode,
force_wifi4 ? " (Force WiFi 4)" : " (newest supported)");
}
/* Channel: 0 = auto in 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);
}
/* Enable the 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);
uci_commit(ctx, &pkg, false);
uci_unload(ctx, pkg);
uci_free_context(ctx);
}
+166
View File
@@ -0,0 +1,166 @@
/*
* Read openuf-managed UCI VAP sections and expose the controller-compatible
* vap_table representation, including runtime interface and BSSID data.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <uci.h>
#include <json-c/json.h>
#include "wlan.h"
#include "ufmodel.h"
#include "crypto.h"
#include "wlan_internal.h"
/*
* Only wifi-iface sections with the openuf_ prefix are reported. Runtime
* nl80211 interface names and sysfs BSSIDs are joined with their effective
* UCI security, roaming, steering, PMF, visibility, and VLAN settings.
*/
/* Resolve a configured VAP to the live interface reported by nl80211. */
static int find_runtime_vap(int phy_index, const char *ssid,
char *out, size_t out_size)
{
FILE *pipe = popen("iw dev 2>/dev/null", "r");
if (!pipe) return -1;
int phy = -1;
char candidate[32] = "";
char line[256];
while (fgets(line, sizeof(line), pipe)) {
int parsed_phy;
char value[128];
if (sscanf(line, "phy#%d", &parsed_phy) == 1) {
phy = parsed_phy;
candidate[0] = '\0';
continue;
}
if (sscanf(line, " Interface %31s", value) == 1) {
snprintf(candidate, sizeof(candidate), "%s", value);
continue;
}
if (phy == phy_index && candidate[0] &&
sscanf(line, " ssid %127[^\n]", value) == 1 &&
!strcmp(value, ssid)) {
snprintf(out, out_size, "%s", candidate);
pclose(pipe);
return 0;
}
}
pclose(pipe);
return -1;
}
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;
/* Only report VAPs managed by 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 *ft_req = UCI_GET("openuf_ft_requested");
const char *k11 = UCI_GET("ieee80211k");
const char *btm = UCI_GET("bss_transition");
const char *bs_req = UCI_GET("openuf_band_steering");
const char *ho_req = UCI_GET("openuf_handoff_suggestions");
const char *w11 = UCI_GET("ieee80211w");
const char *hidden = UCI_GET("hidden");
const char *vap_id = UCI_GET("openuf_vap_id");
const char *vlan = UCI_GET("vlan_id");
if (!ssid) ssid = "";
if (!device) device = "radio0";
/* Band of this radio */
const char *radio_band = wlan_band_for_device(model, device);
if (!radio_band) radio_band = "ng";
/* Resolve the actual netifd interface (for example phy1-ap0). */
char wlan_iface[32];
int ridx = 0;
sscanf(device, "radio%d", &ridx);
if (find_runtime_vap(ridx, ssid, wlan_iface, sizeof(wlan_iface)) != 0)
snprintf(wlan_iface, sizeof(wlan_iface), "phy%d-ap0", ridx);
/* Read the actual BSSID from 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")) ||
(ft_req && !strcmp(ft_req,"1"));
bool bs_on = bs_req ? !strcmp(bs_req, "1") :
(k11 && !strcmp(k11, "1"));
bool handoff_on = ho_req ? !strcmp(ho_req, "1") :
(btm && !strcmp(btm, "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(wlan_iface));
json_object_object_add(o, "ifname", json_object_new_string(wlan_iface));
json_object_object_add(o, "radio", json_object_new_string(radio_band));
json_object_object_add(o, "security", json_object_new_string(wlan_security_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, "bss_transition", json_object_new_boolean(handoff_on));
json_object_object_add(o, "handoff_suggestions", json_object_new_boolean(handoff_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));
if (vlan && atoi(vlan) > 0)
json_object_object_add(o, "vlan_id", json_object_new_int(atoi(vlan)));
if (wlan_valid_object_id(vap_id))
json_object_object_add(o, "id", json_object_new_string(vap_id));
if (wlan_valid_object_id(vap_id))
json_object_object_add(o, "wlanconf_id",
json_object_new_string(vap_id));
json_object_array_add(arr, o);
#undef UCI_GET
}
uci_unload(ctx, pkg);
uci_free_context(ctx);
return arr;
}
+308
View File
@@ -0,0 +1,308 @@
/*
* Small UCI operations shared by provisioning: named sections, VLAN network
* construction, steering policy, and managed-VAP cleanup.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <uci.h>
#include <json-c/json.h>
#include "wlan.h"
#include "ufmodel.h"
#include "crypto.h"
#include "wlan_internal.h"
/* Set one UCI option from a fully qualified assignment path. */
int wlan_uci_set(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);
if (ret == UCI_OK)
ret = uci_set(ctx, &ptr);
/* ptr.value may point inside p, so free it only after uci_set(). */
free(p);
return ret == UCI_OK ? 0 : -1;
}
/* Set and verify an option whose absence would make a VAP unusable. */
int wlan_uci_set_required(struct uci_context *ctx,
struct uci_package *pkg,
const char *section_name,
const char *option_name,
const char *value)
{
char path[256];
snprintf(path, sizeof(path), "%s.%s.%s",
pkg->e.name, section_name, option_name);
if (wlan_uci_set(ctx, path, value) != 0)
return -1;
struct uci_section *section =
uci_lookup_section(ctx, pkg, section_name);
const char *stored = section ?
uci_lookup_option_string(ctx, section, option_name) : NULL;
return stored && !strcmp(stored, value) ? 0 : -1;
}
/* Add one value to a UCI list option. */
int wlan_uci_add_list(struct uci_context *ctx,
const char *path, const char *val)
{
struct uci_ptr ptr;
char *assignment = malloc(strlen(path) + strlen(val) + 2);
if (!assignment) return -1;
sprintf(assignment, "%s=%s", path, val);
int ret = uci_lookup_ptr(ctx, &ptr, assignment, true);
if (ret == UCI_OK)
ret = uci_add_list(ctx, &ptr);
/* ptr.value may point inside assignment. */
free(assignment);
return ret == UCI_OK ? 0 : -1;
}
/* Find or create a named UCI section. */
int wlan_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; /* The section already exists. */
}
/* 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;
}
/*
* Resolve the physical port below network.lan's bridge. VLAN tagging must
* happen on that port (for example eth0.11), not above the management bridge.
*/
static void find_vlan_uplink(struct uci_context *ctx,
struct uci_package *pkg,
char *out, size_t out_size)
{
const char *lan_device = "br-lan";
struct uci_element *element;
uci_foreach_element(&pkg->sections, element) {
struct uci_section *section = uci_to_section(element);
if (!strcmp(section->type, "interface") &&
!strcmp(section->e.name, "lan")) {
const char *device = uci_lookup_option_string(ctx, section,
"device");
if (device && device[0]) lan_device = device;
break;
}
}
uci_foreach_element(&pkg->sections, element) {
struct uci_section *section = uci_to_section(element);
const char *name;
struct uci_option *ports;
if (strcmp(section->type, "device")) continue;
name = uci_lookup_option_string(ctx, section, "name");
if (!name || strcmp(name, lan_device)) continue;
ports = uci_lookup_option(ctx, section, "ports");
if (!ports) break;
if (ports->type == UCI_TYPE_STRING) {
snprintf(out, out_size, "%s", ports->v.string);
return;
}
if (ports->type == UCI_TYPE_LIST && !uci_list_empty(&ports->v.list)) {
struct uci_element *port =
list_to_element(ports->v.list.next);
snprintf(out, out_size, "%s", port->name);
return;
}
break;
}
snprintf(out, out_size, "eth0");
}
int wlan_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 vlan_section[48], bridge_section[48], interface_section[32];
char vlan_uplink[32], vlan_device[32], bridge_device[32], vid_string[16];
find_vlan_uplink(ctx, pkg, vlan_uplink, sizeof(vlan_uplink));
snprintf(vlan_section, sizeof(vlan_section),
"openuf_vlan%d", vid);
snprintf(bridge_section, sizeof(bridge_section), "openuf_br%d", vid);
snprintf(interface_section, sizeof(interface_section),
"vlan%d", vid);
snprintf(vlan_device, sizeof(vlan_device), "%s.%d", vlan_uplink, vid);
snprintf(bridge_device, sizeof(bridge_device), "br-openuf-%d", vid);
snprintf(vid_string, sizeof(vid_string), "%d", vid);
int ok = wlan_uci_ensure_section(ctx, pkg, vlan_section, "device") == 0 &&
wlan_uci_ensure_section(ctx, pkg, bridge_section, "device") == 0 &&
wlan_uci_ensure_section(ctx, pkg, interface_section, "interface") == 0;
if (ok) {
WLAN_UCI_SET(ctx, "network", vlan_section, "type", "8021q");
WLAN_UCI_SET(ctx, "network", vlan_section, "ifname", vlan_uplink);
WLAN_UCI_SET(ctx, "network", vlan_section, "vid", vid_string);
WLAN_UCI_SET(ctx, "network", vlan_section, "name", vlan_device);
/* A VAP needs a bridge containing the tagged wired device. */
WLAN_UCI_SET(ctx, "network", bridge_section, "type", "bridge");
WLAN_UCI_SET(ctx, "network", bridge_section, "name", bridge_device);
char ports_path[128];
snprintf(ports_path, sizeof(ports_path), "network.%s.ports",
bridge_section);
/* Replace the list so repeated provisioning never duplicates ports. */
struct uci_ptr ports_ptr;
char ports_lookup[128];
snprintf(ports_lookup, sizeof(ports_lookup), "%s", ports_path);
if (uci_lookup_ptr(ctx, &ports_ptr, ports_lookup, true) == UCI_OK &&
ports_ptr.o)
uci_delete(ctx, &ports_ptr);
ok = wlan_uci_add_list(ctx, ports_path, vlan_device) == 0;
WLAN_UCI_SET(ctx, "network", interface_section, "proto", "none");
WLAN_UCI_SET(ctx, "network", interface_section, "device", bridge_device);
ok = ok && uci_commit(ctx, &pkg, false) == UCI_OK;
}
uci_unload(ctx, pkg);
uci_free_context(ctx);
if (ok)
LOGF(stdout, "Configured VLAN %d on uplink %s as network '%s'",
vid, vlan_uplink, interface_section);
return ok ? 0 : -1;
}
/*
* Configure OpenWrt's steering policy engine. The hostapd 802.11k/v flags
* only expose measurements and transition commands; they do not decide when
* a station should move. usteer supplies that missing policy loop.
*/
int wlan_configure_band_steering(int enabled)
{
struct uci_context *ctx = uci_alloc_context();
if (!ctx)
return -1;
struct uci_package *pkg = NULL;
if (uci_load(ctx, "usteer", &pkg) != UCI_OK) {
LOGF(stdout, "Cannot load /etc/config/usteer");
uci_free_context(ctx);
return -1;
}
struct uci_section *settings = NULL;
struct uci_element *element;
uci_foreach_element(&pkg->sections, element) {
struct uci_section *section = uci_to_section(element);
if (!strcmp(section->type, "usteer")) {
settings = section;
break;
}
}
if (!settings) {
if (wlan_uci_ensure_section(ctx, pkg, "openuf", "usteer") != 0) {
uci_unload(ctx, pkg);
uci_free_context(ctx);
return -1;
}
settings = uci_lookup_section(ctx, pkg, "openuf");
}
if (!settings) {
uci_unload(ctx, pkg);
uci_free_context(ctx);
return -1;
}
/*
* A zero interval disables higher-band steering. A zero station-count
* threshold is important for small networks: usteer's default of five
* otherwise prevents a lone client from being considered. The signal
* floor avoids pushing a client onto 5 GHz when that link is too weak.
*/
WLAN_UCI_SET(ctx, "usteer", settings->e.name, "band_steering_interval",
enabled ? "30000" : "0");
WLAN_UCI_SET(ctx, "usteer", settings->e.name, "band_steering_threshold", "0");
WLAN_UCI_SET(ctx, "usteer", settings->e.name, "band_steering_min_snr", "-65");
int ok = uci_commit(ctx, &pkg, false) == UCI_OK;
uci_unload(ctx, pkg);
uci_free_context(ctx);
LOGF(stdout, "Band steering policy %s (usteer)",
enabled ? "enabled" : "disabled");
return ok ? 0 : -1;
}
/* ═══════════════════════════════════════════════════════════════════
wlan_clear — remove only VAPs owned by openUF
═══════════════════════════════════════════════════════════════════ */
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;
}
/* Collect sections to remove (do not modify during iteration) */
char *sections_to_delete[64];
int delete_count = 0;
struct uci_element *element;
uci_foreach_element(&pkg->sections, element) {
struct uci_section *section = uci_to_section(element);
if (!strcmp(section->type, "wifi-iface") &&
!strncmp(section->e.name, "openuf_", 7) && delete_count < 64) {
sections_to_delete[delete_count++] = strdup(section->e.name);
}
}
for (int i = 0; i < delete_count; i++) {
struct uci_ptr ptr;
char path[128];
snprintf(path, sizeof(path), "wireless.%s", sections_to_delete[i]);
if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK)
uci_delete(ctx, &ptr);
free(sections_to_delete[i]);
}
if (delete_count > 0) {
uci_commit(ctx, &pkg, false);
LOGF(stdout, "wlan_clear: removed %d managed VAPs",
delete_count);
}
uci_unload(ctx, pkg);
uci_free_context(ctx);
}
+61
View File
@@ -0,0 +1,61 @@
#ifndef OPENUF_WLAN_INTERNAL_H
#define OPENUF_WLAN_INTERNAL_H
#include <stddef.h>
#include <stdio.h>
#include <json-c/json.h>
#include <uci.h>
#include "config.h"
enum wifi_standard {
WIFI_STANDARD_UNKNOWN = 0,
WIFI_STANDARD_4 = 4,
WIFI_STANDARD_5 = 5,
WIFI_STANDARD_6 = 6,
WIFI_STANDARD_7 = 7,
};
const char *wlan_security_to_uci(const char *unifi_security);
const char *wlan_security_to_unifi(const char *uci_security);
int wlan_valid_object_id(const char *object_id);
int wlan_ensure_vap_ids(struct json_object *vaps);
void wlan_mobility_domain_for_ssid(const char *ssid, char output[5]);
int wlan_radio_uses_ath9k(const char *device_name);
int wlan_json_boolean_any(struct json_object *object,
const char *const *keys,
size_t key_count);
int wlan_feature_enabled(const char *text);
void wlan_safe_section_name(const char *ssid, char *output, size_t output_size);
int wlan_uci_set(struct uci_context *context,
const char *path,
const char *value);
int wlan_uci_set_required(struct uci_context *context,
struct uci_package *package,
const char *section_name,
const char *option_name,
const char *value);
int wlan_uci_add_list(struct uci_context *context,
const char *path,
const char *value);
int wlan_uci_ensure_section(struct uci_context *context,
struct uci_package *package,
const char *section_name,
const char *section_type);
int wlan_ensure_vlan_network(int vlan_id);
int wlan_configure_band_steering(int enabled);
#define WLAN_UCI_SET(context, package, section, option, value) do { \
char openuf_uci_path[256]; \
snprintf(openuf_uci_path, sizeof(openuf_uci_path), "%s.%s.%s", \
package, section, option); \
wlan_uci_set(context, openuf_uci_path, value); \
} while (0)
#define WLAN_UCI_SET_INT(context, package, section, option, integer_value) do { \
char openuf_uci_value[32]; \
snprintf(openuf_uci_value, sizeof(openuf_uci_value), "%d", integer_value); \
WLAN_UCI_SET(context, package, section, option, openuf_uci_value); \
} while (0)
#endif /* OPENUF_WLAN_INTERNAL_H */