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