Compare commits

..
1 Commits
Author SHA1 Message Date
Koda 39a9916351 added reporting uplink 2026-07-15 16:59:03 +00:00
31 changed files with 2689 additions and 3846 deletions
-11
View File
@@ -1,11 +0,0 @@
{
"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
@@ -1,75 +0,0 @@
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
@@ -1,112 +0,0 @@
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
+3 -19
View File
@@ -19,18 +19,16 @@ 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 |
| 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` |
| 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 |
| Inform/adoption protocol | `src/inform.[ch]` | Build telemetry, encode TNBU packets, handle controller commands |
| Transport and encryption | `src/http.[ch]`, `src/crypto.[ch]` | Raw HTTP/1.0 client and AES-CBC/AES-GCM helpers |
| 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 |
| WiFi provisioning | `src/wlan.[ch]` | Translate controller config to UCI and report VAP state |
| 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 |
| 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 |
Runtime flow: `main()` -> `inform_send()` -> build telemetry -> encrypt ->
`http_post()` -> decrypt response -> `inform_handle_response()` -> optionally
`http_post()` -> decrypt response -> `handle_response()` -> optionally
`wlan_apply_config()`/`wlan_apply_system_cfg()` and `state_save()`.
## Important invariants
@@ -72,15 +70,6 @@ make package/OpenUniFi/compile
`Makefile.standalone` is for compiling on an OpenWrt device with development
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,
check paths and commands against the source and package Makefile. For C changes,
the cross-build is the minimum verification; report any device/controller
@@ -114,11 +103,6 @@ behavior that still needs manual testing.
- 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
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
pitfall or workaround is verified to be deterministic or repeatedly encountered,
+14 -15
View File
@@ -26,28 +26,27 @@ endef
define Build/Prepare
mkdir -p $(PKG_BUILD_DIR)
$(CP) ./src/. $(PKG_BUILD_DIR)/
$(CP) ./src/* $(PKG_BUILD_DIR)/
endef
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_CFLAGS += -I$(STAGING_DIR)/usr/include -DENABLE_LOGGING=1
TARGET_LDFLAGS += -lmbedtls -lmbedcrypto -luci -ljson-c
define Build/Compile
$(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_LDFLAGS) \
-o $(PKG_BUILD_DIR)/openuf \
$(addprefix $(PKG_BUILD_DIR)/,$(OPENUF_SRCS))
$(PKG_BUILD_DIR)/main.c \
$(PKG_BUILD_DIR)/config.c \
$(PKG_BUILD_DIR)/state.c \
$(PKG_BUILD_DIR)/crypto.c \
$(PKG_BUILD_DIR)/http.c \
$(PKG_BUILD_DIR)/announce.c \
$(PKG_BUILD_DIR)/inform.c \
$(PKG_BUILD_DIR)/wlan.c \
$(PKG_BUILD_DIR)/sysinfo.c \
$(PKG_BUILD_DIR)/clients.c \
$(PKG_BUILD_DIR)/lldp.c \
$(PKG_BUILD_DIR)/models.c
endef
define Package/openuf/install
+3 -11
View File
@@ -10,7 +10,7 @@
# make -f Makefile.standalone install
CC = gcc
CFLAGS = -Wall -Wextra -O2 -I/usr/include -Isrc -Isrc/inform -Isrc/wlan -DENABLE_LOGGING=1
CFLAGS = -Wall -Wextra -O2 -I/usr/include -DENABLE_LOGGING=1
LDFLAGS = -lmbedtls -lmbedcrypto -luci -ljson-c
SRCS = src/main.c \
@@ -19,16 +19,8 @@ SRCS = src/main.c \
src/crypto.c \
src/http.c \
src/announce.c \
src/inform/inform.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/inform.c \
src/wlan.c \
src/sysinfo.c \
src/clients.c \
src/lldp.c \
+8 -72
View File
@@ -7,17 +7,18 @@ Daemon that makes an OpenWrt router appear as a UniFi AP to UniFi Network contro
| Feature | Description | Implementation |
| --- | --- | --- |
| **L2 Discovery** | UDP broadcast + multicast every 10s | `announce.c` → port 10001 |
| **Adoption** | AES-128-CBC/GCM handshake with the controller | `inform/response.c``inform_handle_response()` |
| **Remote reboot** | Reboots OpenWrt when requested by the controller | `inform/response.c``inform_handle_response()` |
| **Firmware spoofing** | Persists and reports the target version requested by an upgrade | `inform/response.c``inform_handle_response()` |
| **WiFi Config** | Creates WiFi networks from the controller via UCI | `wlan/provision.c``wlan_apply_config()` |
| **Band Steering** | 802.11k/v Neighbor Reports + BSS Transition | `wlan/provision.c` and `wlan/uci.c` |
| **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` |
| **Adoption** | AES-128-CBC handshake with the controller | `inform.c``handle_response()` |
| **Remote reboot** | Reboots OpenWrt when requested by the controller | `inform.c``handle_response()` |
| **Firmware spoofing** | Persists and reports the target version requested by an upgrade | `inform.c``handle_response()` |
| **WiFi Config** | Creates WiFi networks from the controller via UCI | `wlan.c``wlan_apply_config()` |
| **Band Steering** | 802.11k/v Neighbor Reports + BSS Transition | `wlan.c``apply_vap()` |
| **Fast Roaming** | 802.11r FT with mobility_domain derived from MAC | `wlan.c``apply_vap()` |
| **WPA3 / PMF** | SAE, SAE-mixed, 802.11w 0/1/2 | `wlan.c``sec_to_uci()` |
| **WiFi Clients** | MAC, signal, bitrate, bytes per VAP | `clients.c``iw station dump` |
| **Wired Clients** | MACs from bridge FDB | `clients.c``bridge fdb` |
| **CPU / RAM** | Real-time usage | `sysinfo.c``/proc/stat` + `/proc/meminfo` |
| **Interfaces** | Speed, duplex, rx/tx counters | `sysinfo.c``/proc/net/dev` |
| **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` |
| **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` |
@@ -46,71 +47,6 @@ make -f Makefile.standalone
Contributors and AI agents should read [`AGENTS.md`](AGENTS.md) for the concise
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
```shell
-139
View File
@@ -1,139 +0,0 @@
#!/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
@@ -1,42 +0,0 @@
#!/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
@@ -1,589 +0,0 @@
#!/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
@@ -1,51 +0,0 @@
#!/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
+6 -7
View File
@@ -27,7 +27,6 @@
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
@@ -38,7 +37,7 @@
#ifdef ENABLE_LOGGING
#include <stdio.h>
extern FILE *log_fp;
#define LOG(fmt, ...) do { if (log_fp) openuf_log_emit(log_fp, __func__, fmt, ##__VA_ARGS__); } while(0)
#define LOG(fmt, ...) do { if (log_fp) { fprintf(log_fp, "[%s] " fmt "\n", __func__, ##__VA_ARGS__); fflush(log_fp); } } while(0)
#else
#define LOG(fmt, ...) do {} while(0)
#endif
@@ -197,15 +196,15 @@ int announce_init(announce_ctx_t *ctx,
ctx->counter = 0;
ctx->uptime = 10;
/* ── Socket for broadcast 255.255.255.255 ─────────────────── */
/* ── Socket para broadcast 255.255.255.255 ─────────────────── */
ctx->sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (ctx->sockfd < 0) {
LOGF(stderr, "announce socket: %s", strerror(errno));
perror("[openuf] announce socket");
return -1;
}
int on = 1;
setsockopt(ctx->sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
/* Bind an ephemeral port; OpenWrt does not allow broadcast setpeername(). */
/* Bind a puerto efímero — OpenWrt no permite setpeername() a broadcast */
struct sockaddr_in bind_addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = INADDR_ANY,
@@ -250,7 +249,7 @@ int announce_send(announce_ctx_t *ctx)
};
if (sendto(ctx->sockfd, ctx->pkt, ctx->pkt_len, 0,
(struct sockaddr *)&dest_bcast, sizeof(dest_bcast)) < 0) {
LOGF(stderr, "announce sendto broadcast: %s", strerror(errno));
perror("[openuf] announce sendto broadcast");
ret = -1;
}
@@ -263,7 +262,7 @@ int announce_send(announce_ctx_t *ctx)
inet_pton(AF_INET, "233.89.188.1", &dest_mcast.sin_addr);
if (sendto(ctx->sockfd_mcast, ctx->pkt, ctx->pkt_len, 0,
(struct sockaddr *)&dest_mcast, sizeof(dest_mcast)) < 0) {
/* This is nonfatal; some kernels have no multicast route. */
/* No es error crítico — algunos kernels no tienen ruta multicast */
}
}
+1 -28
View File
@@ -21,37 +21,10 @@
#if ENABLE_LOGGING
#include <stdio.h>
#include <stdarg.h>
#include <time.h>
extern FILE *log_fp;
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__)
#define LOG(fmt, ...) do { if (log_fp) { fprintf(log_fp, "[%s] " fmt "\n", __func__, ##__VA_ARGS__); fflush(log_fp); } } while(0)
#else
#define LOG(fmt, ...) do {} while(0)
#define LOGF(stream, fmt, ...) do { (void)(stream); } while(0)
#endif
typedef struct {
+722 -10
View File
@@ -1,9 +1,52 @@
/*
* Collect device, radio, interface, client, and topology telemetry into the
* JSON payload sent during an inform exchange.
* openuf - inform.c
*
* UniFi Inform Protocol full implementation.
*
* HOW IT WORKS
*
* Every 10 seconds the AP makes an HTTP POST to http://<controller>:8080/inform
* with a binary TNBU packet containing JSON encrypted with AES-128-CBC.
*
* The controller responds with another TNBU packet. The AP decrypts, parses
* the JSON, and executes the action (_type).
*
* TNBU BINARY PACKET
*
* Offset Bytes Field
* ------ ----- -----
* 0 4 Magic "TNBU"
* 4 4 Packet version (=0), uint32 BE
* 8 6 AP MAC address
* 14 2 Flags: bit0=encrypted, bit1=zlib
* 16 16 AES IV (when encrypted)
* 32 4 Data version (=1), uint32 BE
* 36 4 Payload length, uint32 BE
* 40 N JSON payload, encrypted with AES-128-CBC
*
* HOW PARAMETERS ARE READ
*
* CPU: sysinfo_cpu_percent() /proc/stat (delta across 2 calls)
* RAM: sysinfo_mem() /proc/meminfo
* Interfaces: sysinfo_iface() /proc/net/dev + /sys/class/net/
* Radios: sysinfo_radio() iw dev <iface> info + survey
* UCI VAPs: wlan_get_vap_table() libuci wireless.*
* WiFi clients: clients_build_sta_table() iw dev <iface> station dump
* IP clients: clients_mac_to_ip() /proc/net/arp
* Client names: clients_mac_to_hostname() /tmp/dhcp.leases
* LLDP neighbors: lldp_read_neighbors() lldpctl -f json
*
* ADOPTION CYCLE
*
* 1. AP sends inform with key=DEFAULT, default=true, state=1
* 2. Controller responds: {_type:"cmd", cmd:"set-adopt",
* key:"new32hexkey", uri:"http://..."}
* 3. AP saves the new key + URL to state.json, adopted=true
* 4. AP sends inform with the new key, state=4, default=false
* 5. Controller responds: {_type:"setstate", radio_table:[...], vap_table:[...]}
* 6. AP applies WiFi config via wlan_apply_config() libuci wifi reload
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -20,9 +63,98 @@
#include "sysinfo.h"
#include "clients.h"
#include "lldp.h"
#include "inform_internal.h"
/* Build system CPU, memory, and load statistics. */
/* ─── Big-endian helpers ────────────────────────────────────────── */
static void put32be(unsigned char *p, uint32_t v)
{
p[0]=(v>>24)&0xff; p[1]=(v>>16)&0xff;
p[2]=(v>> 8)&0xff; p[3]=v&0xff;
}
static void put16be(unsigned char *p, uint16_t v)
{
p[0]=(v>>8)&0xff; p[1]=v&0xff;
}
static uint32_t get32be(const unsigned char *p)
{
return ((uint32_t)p[0]<<24)|((uint32_t)p[1]<<16)|
((uint32_t)p[2]<<8)|(uint32_t)p[3];
}
static uint16_t get16be(const unsigned char *p)
{
return ((uint16_t)p[0]<<8)|(uint16_t)p[1];
}
static int valid_authkey(const char *key)
{
if (!key || strlen(key) != 32)
return 0;
for (size_t i = 0; i < 32; i++)
if (!isxdigit((unsigned char)key[i]))
return 0;
return 1;
}
static int protocol_debug_level;
void inform_set_debug_level(int level)
{
protocol_debug_level = level < 0 ? 0 : level > 2 ? 2 : level;
}
static int 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");
}
static void debug_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 (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 struct json_object *build_sys_stats(int *cpu_percent,
double *mem_percent)
{
@@ -182,6 +314,73 @@ static struct json_object *build_scan_table(const char *iface)
return arr;
}
/* Build the wired uplink object expected by UniFi controllers.
* The model owns the interface mapping; live state and counters come from
* sysfs and /proc so third-party upstream switches still get a usable link. */
static struct json_object *build_uplink(const uf_model_t *m,
const openuf_state_t *st)
{
const uf_port_t *port = NULL;
for (int i = 0; i < m->port_table_len; i++) {
if (m->port_table[i].is_uplink) {
port = &m->port_table[i];
break;
}
}
if (!port && m->port_table_len > 0)
port = &m->port_table[0];
struct json_object *o = json_object_new_object();
if (!port)
return o;
iface_stats_t stats;
sysinfo_iface(port->ifname, &stats);
int speed = stats.speed > 0 ? stats.speed : port->speed;
json_object_object_add(o, "name",
json_object_new_string(port->ifname));
json_object_object_add(o, "type",
json_object_new_string("wire"));
json_object_object_add(o, "media",
json_object_new_string(port->media));
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 : "0.0.0.0"));
json_object_object_add(o, "num_port",
json_object_new_int(1));
json_object_object_add(o, "port_idx",
json_object_new_int(port->port_idx));
json_object_object_add(o, "up",
json_object_new_boolean(stats.up));
json_object_object_add(o, "speed",
json_object_new_int(speed));
json_object_object_add(o, "max_speed",
json_object_new_int(port->speed));
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_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));
return o;
}
/* ═══════════════════════════════════════════════════════════════════
radio_table static definition of the radio hardware
@@ -278,7 +477,7 @@ static void build_radio_table(struct json_object *root,
json_object_object_add(root, alias, json_object_get(o));
json_object_array_add(arr, o);
if (inform_debug_level() > 0)
if (protocol_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);
}
@@ -663,9 +862,9 @@ static struct json_object *collect_sta_table(struct json_object *vap_table)
}
/* ═══════════════════════════════════════════════════════════════════
inform_build_payload Complete assembly of the inform JSON
build_payload Complete assembly of the inform JSON
*/
char *inform_build_payload(const openuf_state_t *st,
static char *build_payload(const openuf_state_t *st,
const uf_model_t *m,
long uptime)
{
@@ -743,8 +942,6 @@ char *inform_build_payload(const openuf_state_t *st,
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));
@@ -759,6 +956,11 @@ char *inform_build_payload(const openuf_state_t *st,
/* ── Ethernet interfaces with real counters ──────────────── */
json_object_object_add(root, "if_table", build_if_table(m, st));
/* ── Structured wired uplink for controller topology ─────── */
json_object_object_add(root, "uplink", build_uplink(m, st));
json_object_object_add(root, "uplink_table",
json_object_new_array());
/* 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);
@@ -799,3 +1001,513 @@ char *inform_build_payload(const openuf_state_t *st,
json_object_put(root);
return copy;
}
/* ═══════════════════════════════════════════════════════════════════
TNBU binary packet
*/
static unsigned char *build_packet(const char *mac_hex,
const char *key_hex,
const char *payload,
int use_aes_gcm,
size_t *out_len)
{
unsigned char iv_hex[33] = {0};
if (crypto_random_hex(iv_hex, 16) != 0) return NULL;
unsigned char mac_bin[6];
crypto_hex2bin(mac_hex, mac_bin, 6);
size_t pl_len = strlen(payload);
size_t body_len = use_aes_gcm
? pl_len + 16
: pl_len + (16 - (pl_len % 16));
size_t pkt_len = 40 + body_len;
unsigned char *pkt = malloc(pkt_len);
if (!pkt) return NULL;
unsigned char *p = pkt;
memcpy(p, INFORM_MAGIC, 4); p += 4;
put32be(p, INFORM_PKT_VERSION); p += 4;
memcpy(p, mac_bin, 6); p += 6;
put16be(p, INFORM_FLAG_ENCRYPTED |
(use_aes_gcm ? INFORM_FLAG_GCM : 0)); p += 2;
unsigned char iv_bin[16];
crypto_hex2bin((char *)iv_hex, iv_bin, 16);
memcpy(p, iv_bin, 16); p += 16;
put32be(p, INFORM_DATA_VERSION); p += 4;
put32be(p, (uint32_t)body_len); p += 4;
int enc_len;
if (use_aes_gcm) {
unsigned char tag[16];
enc_len = crypto_gcm_encrypt(key_hex, (char *)iv_hex,
pkt, 40,
(const unsigned char *)payload, pl_len,
p, tag);
if (enc_len >= 0)
memcpy(p + enc_len, tag, sizeof(tag));
} else {
enc_len = crypto_encrypt(key_hex, (char *)iv_hex,
(const unsigned char *)payload, pl_len, p);
}
if (enc_len < 0) {
free(pkt);
return NULL;
}
*out_len = pkt_len;
return pkt;
}
/* ═══════════════════════════════════════════════════════════════════
Parse binary response from the controller
*/
static char *parse_packet(const unsigned char *data, size_t data_len,
const char *key_hex)
{
if (data_len < 40) return NULL;
if (memcmp(data, INFORM_MAGIC, 4) != 0) return NULL;
uint16_t flags = get16be(data + 14);
const unsigned char *iv_bin = data + 16;
uint32_t body_len = get32be(data + 36);
const unsigned char *body = data + 40;
if (40 + body_len > data_len) return NULL;
if ((flags & INFORM_FLAG_GCM) != 0) {
if (body_len < 16) return NULL;
size_t cipher_len = body_len - 16;
char iv_hex[33];
crypto_bin2hex(iv_bin, 16, iv_hex);
unsigned char *plain = malloc(cipher_len + 1);
if (!plain) return NULL;
int pl = crypto_gcm_decrypt(key_hex, iv_hex, data, 40,
body, cipher_len, body + cipher_len,
plain);
if (pl < 0) { free(plain); return NULL; }
plain[pl] = '\0';
return (char *)plain;
}
if (flags & INFORM_FLAG_ENCRYPTED) {
char iv_hex[33];
crypto_bin2hex(iv_bin, 16, iv_hex);
unsigned char *plain = malloc(body_len + 1);
if (!plain) return NULL;
int pl = crypto_decrypt(key_hex, iv_hex, body, body_len, plain);
if (pl < 0) { free(plain); return NULL; }
plain[pl] = '\0';
return (char *)plain;
}
char *copy = malloc(body_len + 1);
if (!copy) return NULL;
memcpy(copy, body, body_len);
copy[body_len] = '\0';
return copy;
}
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
*/
static void handle_response(openuf_state_t *st,
const uf_model_t *model,
struct json_object *resp,
char *action_out)
{
struct json_object *v;
const char *type = "noop";
if (json_object_object_get_ex(resp, "_type", &v) &&
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 (valid_authkey(val) &&
strcmp(st->authkey, val) != 0) {
int replacing_key = st->authkey[0] &&
strcmp(st->authkey, DEFAULT_AUTH_KEY) != 0;
strncpy(st->authkey, val,
sizeof(st->authkey)-1);
st->authkey[sizeof(st->authkey)-1] = '\0';
received_adoption_key = 1;
LOG("%s device key from setparam",
replacing_key ? "Replaced" : "Accepted");
} else if (!valid_authkey(val)) {
LOG("Ignoring invalid authkey from setparam");
}
} else if (!strcmp(key, "cfgversion")) {
/*
* This is the version the controller wants, not proof
* that its setstate has been applied locally.
*/
LOG("Controller requested cfgversion=%s; currently applied=%s",
val, st->cfgversion);
} else if (!strcmp(key, "use_aes_gcm")) {
st->use_aes_gcm = !strcmp(val, "true") ||
!strcmp(val, "1");
LOG("AES-GCM %s for subsequent inform packets",
st->use_aes_gcm ? "enabled" : "disabled");
} else if (!strcmp(key, "mgmt_url")) {
/* Could save mgmt_url for future use */
}
/* Other management parameters are currently informational. */
}
line = strtok(NULL, "\n");
}
}
struct json_object *system_cfg_obj;
if (json_object_object_get_ex(resp, "system_cfg",
&system_cfg_obj)) {
const char *system_cfg = json_object_get_string(system_cfg_obj);
LOG("Applying legacy system_cfg, length=%zu",
strlen(system_cfg));
if (wlan_apply_system_cfg(system_cfg, model) == 0) {
applied_system_cfg = 1;
st->config_applied = true;
st->config_schema = OPENUF_CONFIG_SCHEMA;
if (json_object_object_get_ex(resp, "cfgversion", &v))
snprintf(st->cfgversion, sizeof(st->cfgversion), "%s",
json_object_get_string(v));
LOG("Legacy system_cfg applied successfully, cfgversion=%s",
st->cfgversion);
} else {
st->config_applied = false;
strncpy(st->cfgversion, "0",
sizeof(st->cfgversion) - 1);
LOG("Legacy system_cfg failed; requesting provisioning retry");
}
}
/* Fall back to the direct key/value format used by older controllers. */
if (json_object_object_get_ex(resp, "key", &v)) {
const char *key = json_object_get_string(v);
struct json_object *val_o;
if (json_object_object_get_ex(resp, "value", &val_o)) {
const char *val = json_object_get_string(val_o);
LOG("setparam key=%s val=%s", key, val);
if (!strcmp(key, "inform_url"))
strncpy(st->inform_url, val, sizeof(st->inform_url)-1);
else if (!strcmp(key, "authkey") && valid_authkey(val) &&
strcmp(st->authkey, val) != 0) {
int replacing_key = st->authkey[0] &&
strcmp(st->authkey, DEFAULT_AUTH_KEY) != 0;
strncpy(st->authkey, val, sizeof(st->authkey)-1);
st->authkey[sizeof(st->authkey)-1] = '\0';
received_adoption_key = 1;
LOG("%s device key from direct setparam",
replacing_key ? "Replaced" : "Accepted");
}
}
}
/*
* Modern controllers complete adoption by returning the per-device
* key in setparam. Mark the device adopted before its next inform so
* both the payload and packet encryption switch to that key.
*/
if (received_adoption_key) {
st->adopted = true;
LOG("Adoption completed through setparam; next inform will use the controller key");
}
state_save(st);
LOG("State saved after setparam");
strcpy(action_out, applied_system_cfg ? "provisioned" :
received_adoption_key ? "adopted" : "setparam");
return;
}
/* ── cmd ─────────────────────────────────────────────────────── */
if (!strcmp(type, "cmd")) {
const char *cmd = "";
if (json_object_object_get_ex(resp, "cmd", &v))
cmd = json_object_get_string(v);
if (!strcmp(cmd, "set-adopt") || !strcmp(cmd, "adopt")) {
if (json_object_object_get_ex(resp, "uri", &v))
strncpy(st->inform_url, json_object_get_string(v),
sizeof(st->inform_url)-1);
if (json_object_object_get_ex(resp, "key", &v))
strncpy(st->authkey, json_object_get_string(v),
sizeof(st->authkey)-1);
st->adopted = true;
state_save(st);
strcpy(action_out, "adopted");
LOG("Adopted successfully. Key: %.8s...", st->authkey);
} else if (!strcmp(cmd, "reboot")) {
strcpy(action_out, "reboot");
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) {
printf("[openuf] Applying controller WiFi configuration...\n");
apply_ok = wlan_apply_config(resp, model) == 0;
} else {
LOG("setstate contained neither radio_table nor vap_table");
}
st->config_applied = apply_ok;
if (apply_ok)
st->config_schema = OPENUF_CONFIG_SCHEMA;
if (!apply_ok) {
strncpy(st->cfgversion, "0", sizeof(st->cfgversion) - 1);
LOG("WiFi configuration failed; cfgversion reset so the controller retries");
}
state_save(st);
strcpy(action_out, apply_ok ? "setstate" : "setstate-failed");
return;
}
snprintf(action_out, 64, "unknown:%s", type);
}
/* ═══════════════════════════════════════════════════════════════════
inform_send main public function
*/
int inform_send(openuf_state_t *st,
const uf_model_t *model,
long uptime,
char *err_out)
{
if (!st->inform_url[0]) {
LOG("No inform_url set");
strncpy(err_out, "no inform_url", 127);
return -1;
}
const char *key_hex = (st->authkey[0]) ? st->authkey : DEFAULT_AUTH_KEY;
/* CRITICAL: When not adopted, ALWAYS use DEFAULT_AUTH_KEY */
if (!st->adopted && st->authkey[0] && strcmp(st->authkey, DEFAULT_AUTH_KEY) != 0) {
LOG("WARNING: Device not adopted but has custom authkey! Using DEFAULT instead!");
key_hex = DEFAULT_AUTH_KEY;
}
LOG("Sending inform: adopted=%d, authkey=%.8s..., inform_url=%s",
st->adopted, key_hex, st->inform_url);
/* MAC without colons */
char mac_hex[32] = {0};
{
const char *s = st->mac; int j = 0;
for (int i = 0; s[i] && j < 12; i++)
if (s[i] != ':') mac_hex[j++] = s[i];
}
char *payload = build_payload(st, model, uptime);
if (!payload) { strncpy(err_out, "build_payload OOM", 127); return -1; }
LOG("Built payload, length: %zu", strlen(payload));
unsigned char *resp_body = NULL;
size_t resp_len = 0;
int status = -1;
int selected_gcm = st->use_aes_gcm;
/*
* A controller remembers the negotiated cipher. If local state was
* created before use_aes_gcm was persisted, it rejects CBC with HTTP 400
* and cannot send another setparam. Retry once with the other cipher.
*/
for (int attempt = 0; attempt < 2; attempt++) {
size_t pkt_len = 0;
unsigned char *pkt = build_packet(mac_hex, key_hex, payload,
selected_gcm, &pkt_len);
if (!pkt) {
free(payload);
strncpy(err_out, "build_packet failed", 127);
return -1;
}
LOG("Built packet, length: %zu, cipher: %s", pkt_len,
selected_gcm ? "AES-GCM" : "AES-CBC");
status = http_post(st->inform_url,
"application/x-binary-data",
pkt, pkt_len,
&resp_body, &resp_len);
free(pkt);
LOG("HTTP POST to %s, status: %d, response length: %zu",
st->inform_url, status, resp_len);
if (status != 400 || !st->adopted || attempt != 0)
break;
free(resp_body);
resp_body = NULL;
resp_len = 0;
selected_gcm = !selected_gcm;
LOG("Controller rejected %s; retrying once with %s",
selected_gcm ? "AES-CBC" : "AES-GCM",
selected_gcm ? "AES-GCM" : "AES-CBC");
}
free(payload);
if (status < 0) {
snprintf(err_out, 127, "HTTP connect failed");
return -1;
}
if (status != 200) {
snprintf(err_out, 127, "HTTP %d", status);
free(resp_body);
return -1;
}
if (st->use_aes_gcm != selected_gcm) {
st->use_aes_gcm = selected_gcm;
state_save(st);
LOG("Recovered cipher state; persisted aes_gcm=%d",
st->use_aes_gcm);
}
if (!resp_body || resp_len == 0) {
LOG("No response body");
free(resp_body);
return 0;
}
char *resp_json = parse_packet(resp_body, resp_len, key_hex);
free(resp_body);
if (!resp_json) {
LOG("Failed to parse response packet");
snprintf(err_out, 127, "parse_packet failed");
return -1;
}
LOG("Parsed response JSON, length=%zu", strlen(resp_json));
struct json_object *resp_obj = json_tokener_parse(resp_json);
if (!resp_obj) {
LOG("Failed to parse JSON");
snprintf(err_out, 127, "JSON parse failed");
free(resp_json);
return -1;
}
debug_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";
handle_response(st, model, resp_obj, action);
json_object_put(resp_obj);
LOG("Response action: %s", action);
if (strcmp(action, "noop") != 0)
printf("[openuf] Action: %s\n", action);
return 0;
}
-164
View File
@@ -1,164 +0,0 @@
/*
* 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
@@ -1,34 +0,0 @@
#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
@@ -1,147 +0,0 @@
/*
* 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;
}
-360
View File
@@ -1,360 +0,0 @@
/*
* 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,
int ttl)
{
/* Raw packet sockets require root privileges or CAP_NET_RAW. */
/* Socket raw — requiere root */
int fd = socket(AF_PACKET, SOCK_RAW, htons(LLDP_ETHERTYPE));
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=3 TTL value=uint16_BE
* TLV type=5 System Name value=hostname
* TLV type=6 System Description value="model version"
* TLV type=6 System Desc value="modelo versión"
* TLV type=7 Capabilities cap=0x0040(WLAN-AP), en=0x0040
* TLV type=0 End of LLDPDU len=0
*
+13 -11
View File
@@ -3,7 +3,7 @@
*
* Main daemon. Loop with three tasks:
* 1. Announce UDP broadcast+multicast each 10s (discovery L2)
* 2. Inform encrypted HTTP POST every 10s (adoption + telemetry)
* 2. Inform HTTP POST cifrado each 10s (adoption + telemetrics)
* 3. LLDP Raw frame L2 each 30s (visual topology in UniFi)
*/
@@ -174,19 +174,20 @@ int main(int argc, char *argv[])
controller_ip);
} else {
state.inform_url[0] = '\0';
LOGF(stderr,
"No controller configured and no IPv4 default "
"gateway found");
fprintf(stderr,
"[openuf] No controller configured and no IPv4 default "
"gateway found\n");
}
}
state_save(&state);
LOGF(stdout, "Starting version=%s model=%-8s MAC=%s IP=%s",
OPENUF_VERSION, model->model, mac_str, ip_str);
LOGF(stdout, "Controller: %s", state.inform_url);
LOGF(stdout, "Adopted: %s", state.adopted ? "yes" : "no");
LOGF(stdout, "LLDP available: %s",
lldp_available() ? "yes (lldpd)" : "no (transmit only)");
printf("[openuf] Starting version=%s model=%-8s MAC=%s IP=%s\n",
OPENUF_VERSION, model->model, mac_str, ip_str);
printf("[openuf] Controller: %s\n", state.inform_url);
printf("[openuf] Adopted: %s\n", state.adopted ? "yes" : "no");
printf("[openuf] LLDP available: %s\n",
lldp_available() ? "yes (lldpd)" : "no (transmit only)");
fflush(stdout);
LOG("Daemon started");
@@ -211,7 +212,8 @@ int main(int argc, char *argv[])
time_t last_inform = 0;
time_t last_lldp = 0;
LOGF(stdout, "Main loop started");
printf("[openuf] Main loop started\n");
fflush(stdout);
while (1) {
time_t now = time(NULL);
+1 -1
View File
@@ -8,7 +8,7 @@
#if ENABLE_LOGGING
#include <stdio.h>
extern FILE *log_fp;
#define LOG(fmt, ...) do { if (log_fp) openuf_log_emit(log_fp, __func__, fmt, ##__VA_ARGS__); } while(0)
#define LOG(fmt, ...) do { if (log_fp) { fprintf(log_fp, "[%s] " fmt "\n", __func__, ##__VA_ARGS__); fflush(log_fp); } } while(0)
#else
#define LOG(fmt, ...) do {} while(0)
#endif
+2 -2
View File
@@ -17,7 +17,7 @@
#include <stdbool.h>
/* ── Memory ─────────────────────────────────────────────────────── */
/* ── Memoria ─────────────────────────────────────────────────────── */
typedef struct {
long total_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. */
int sysinfo_cpu_percent(void);
/* ── Network interface ─────────────────────────────────────────────── */
/* ── Interfaz de red ─────────────────────────────────────────────── */
typedef struct {
char name[32];
char mac[32];
+1906
View File
File diff suppressed because it is too large Load Diff
-229
View File
@@ -1,229 +0,0 @@
/*
* 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
@@ -1,270 +0,0 @@
/*
* 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
@@ -1,585 +0,0 @@
/*
* 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
@@ -1,317 +0,0 @@
/*
* 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
@@ -1,166 +0,0 @@
/*
* 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
@@ -1,308 +0,0 @@
/*
* 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
@@ -1,61 +0,0 @@
#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 */