refactor #1 + adding documentation
Validate source documentation / generated-wiki (pull_request) Failing after 10s

This commit is contained in:
2026-07-19 20:25:12 +00:00
parent db81260d7f
commit b37e0d6bce
31 changed files with 4068 additions and 2598 deletions
+20
View File
@@ -0,0 +1,20 @@
name: Validate source documentation
on:
pull_request:
workflow_dispatch:
permissions:
code: read
jobs:
generated-wiki:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Check generated Gitea Wiki pages
run: ./scripts/docs/generate-wiki.py --check
- name: Check patch whitespace
run: git diff --check HEAD^
+2
View File
@@ -22,6 +22,8 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Verify generated developer Wiki
run: ./scripts/docs/generate-wiki.py --check
- name: Install metadata dependencies
run: sudo apt-get update && sudo apt-get install --yes jq
- name: Read release configuration
+14 -3
View File
@@ -19,16 +19,18 @@ threading or event framework; the main loop runs once per second.
| Lifecycle and scheduling | `src/main.c` | Load config/state, choose model, run announce/inform/LLDP timers |
| 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. After source or architecture changes, run:
```sh
./scripts/docs/generate-wiki.py
./scripts/docs/generate-wiki.py --check
```
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
+15 -14
View File
@@ -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
View File
@@ -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 \
+29 -7
View File
@@ -7,13 +7,13 @@ 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()` |
| **Remote reboot** | Reboots OpenWrt when requested by the controller | `inform.c``handle_response()` |
| **Firmware spoofing** | Persists and reports the target version requested by an upgrade | `inform.c``handle_response()` |
| **WiFi Config** | Creates WiFi networks from the controller via UCI | `wlan.c``wlan_apply_config()` |
| **Band Steering** | 802.11k/v Neighbor Reports + BSS Transition | `wlan.c``apply_vap()` |
| **Fast Roaming** | 802.11r FT with mobility_domain derived from MAC | `wlan.c``apply_vap()` |
| **WPA3 / PMF** | SAE, SAE-mixed, 802.11w 0/1/2 | `wlan.c``sec_to_uci()` |
| **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` |
@@ -46,6 +46,28 @@ 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 Gitea Wiki pages live in [`wiki/`](wiki/). They include a
developer guide, a generated Mermaid function-call graph, and runtime flow
charts. Refresh them after source or architecture changes:
```shell
./scripts/docs/generate-wiki.py
```
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
```
Because Gitea stores Wiki pages in a separate Git repository, publishing is an
explicit authenticated step: `./scripts/docs/publish-wiki.sh`. The script
derives the `.wiki.git` URL from `origin` and accepts an explicit URL override.
No credentials are stored in this repository.
## Automated releases
Every push to `main` runs `.gitea/workflows/release.yaml`. It builds with pinned
+581
View File
@@ -0,0 +1,581 @@
#!/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"
DEFAULT_WIKI_ROOT = REPOSITORY_ROOT / "wiki"
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
Generate pages after changing C code or architecture:
```sh
./scripts/docs/generate-wiki.py
```
Run the linter-style drift and structure check in CI or before committing:
```sh
./scripts/docs/generate-wiki.py --check
```
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 a repository Wiki in a separate Git repository whose URL normally
ends in `.wiki.git`. The generated `wiki/` directory is ready for that remote.
Run `scripts/docs/publish-wiki.sh` from a trusted machine with suitable
credentials. It derives the Wiki remote from `origin`; an explicit URL may be
passed when needed. The publisher regenerates and checks the pages,
updates only the generated Markdown files, commits changed pages, and pushes
them 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 main
repository so architecture documentation changes can be reviewed with code.
- [[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` 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, default=DEFAULT_WIKI_ROOT,
help="Wiki output directory (default: repository wiki/)")
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 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())
+55
View File
@@ -0,0 +1,55 @@
#!/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
case $origin_url in
*.git) wiki_repository_url=${origin_url%.git}.wiki.git ;;
*) wiki_repository_url=${origin_url}.wiki.git ;;
esac
fi
cleanup() {
rm -rf -- "$temporary_directory"
}
trap cleanup EXIT HUP INT TERM
"$script_directory/generate-wiki.py"
"$script_directory/generate-wiki.py" --check
git clone -- "$wiki_repository_url" "$temporary_directory/wiki"
for page in \
Home.md \
Developer-Guide.md \
Call-Graph.md \
Runtime-Flow.md \
_Sidebar.md
do
cp -- "$repository_root/wiki/$page" "$temporary_directory/wiki/$page"
done
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 commit -m "docs: update generated developer wiki"
git push origin HEAD
+3 -3
View File
@@ -196,7 +196,7 @@ 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");
@@ -204,7 +204,7 @@ int announce_init(announce_ctx_t *ctx,
}
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,
@@ -262,7 +262,7 @@ int announce_send(announce_ctx_t *ctx)
inet_pton(AF_INET, "233.89.188.1", &dest_mcast.sin_addr);
if (sendto(ctx->sockfd_mcast, ctx->pkt, ctx->pkt_len, 0,
(struct sockaddr *)&dest_mcast, sizeof(dest_mcast)) < 0) {
/* No es error crítico — algunos kernels no tienen ruta multicast */
/* This is nonfatal; some kernels have no multicast route. */
}
}
+164
View File
@@ -0,0 +1,164 @@
/*
* Run one inform exchange: select the adoption key and cipher, encode the
* request, post it to the controller, and dispatch the decoded response.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <json-c/json.h>
#include "inform.h"
#include "crypto.h"
#include "http.h"
#include "wlan.h"
#include "state.h"
#include "config.h"
#include "sysinfo.h"
#include "clients.h"
#include "lldp.h"
#include "inform_internal.h"
/* Public inform-cycle entry point. */
int inform_send(openuf_state_t *st,
const uf_model_t *model,
long uptime,
char *err_out)
{
if (!st->inform_url[0]) {
LOG("No inform_url set");
strncpy(err_out, "no inform_url", 127);
return -1;
}
const char *key_hex = (st->authkey[0]) ? st->authkey : DEFAULT_AUTH_KEY;
/* CRITICAL: When not adopted, ALWAYS use DEFAULT_AUTH_KEY */
if (!st->adopted && st->authkey[0] && strcmp(st->authkey, DEFAULT_AUTH_KEY) != 0) {
LOG("WARNING: Device not adopted but has custom authkey! Using DEFAULT instead!");
key_hex = DEFAULT_AUTH_KEY;
}
LOG("Sending inform: adopted=%d, authkey=%.8s..., inform_url=%s",
st->adopted, key_hex, st->inform_url);
/* MAC without colons */
char mac_hex[32] = {0};
{
const char *s = st->mac; int j = 0;
for (int i = 0; s[i] && j < 12; i++)
if (s[i] != ':') mac_hex[j++] = s[i];
}
char *payload = inform_build_payload(st, model, uptime);
if (!payload) { strncpy(err_out, "inform_build_payload OOM", 127); return -1; }
LOG("Built payload, length: %zu", strlen(payload));
unsigned char *resp_body = NULL;
size_t resp_len = 0;
int status = -1;
int selected_gcm = st->use_aes_gcm;
/*
* A controller remembers the negotiated cipher. If local state was
* created before use_aes_gcm was persisted, it rejects CBC with HTTP 400
* and cannot send another setparam. Retry once with the other cipher.
*/
for (int attempt = 0; attempt < 2; attempt++) {
size_t pkt_len = 0;
unsigned char *pkt = inform_packet_build(mac_hex, key_hex, payload,
selected_gcm, &pkt_len);
if (!pkt) {
free(payload);
strncpy(err_out, "inform_packet_build failed", 127);
return -1;
}
LOG("Built packet, length: %zu, cipher: %s", pkt_len,
selected_gcm ? "AES-GCM" : "AES-CBC");
status = http_post(st->inform_url,
"application/x-binary-data",
pkt, pkt_len,
&resp_body, &resp_len);
free(pkt);
LOG("HTTP POST to %s, status: %d, response length: %zu",
st->inform_url, status, resp_len);
if (status != 400 || !st->adopted || attempt != 0)
break;
free(resp_body);
resp_body = NULL;
resp_len = 0;
selected_gcm = !selected_gcm;
LOG("Controller rejected %s; retrying once with %s",
selected_gcm ? "AES-CBC" : "AES-GCM",
selected_gcm ? "AES-GCM" : "AES-CBC");
}
free(payload);
if (status < 0) {
snprintf(err_out, 127, "HTTP connect failed");
return -1;
}
if (status != 200) {
snprintf(err_out, 127, "HTTP %d", status);
free(resp_body);
return -1;
}
if (st->use_aes_gcm != selected_gcm) {
st->use_aes_gcm = selected_gcm;
state_save(st);
LOG("Recovered cipher state; persisted aes_gcm=%d",
st->use_aes_gcm);
}
if (!resp_body || resp_len == 0) {
LOG("No response body");
free(resp_body);
return 0;
}
char *resp_json = inform_packet_parse(resp_body, resp_len, key_hex);
free(resp_body);
if (!resp_json) {
LOG("Failed to parse response packet");
snprintf(err_out, 127, "inform_packet_parse failed");
return -1;
}
LOG("Parsed response JSON, length=%zu", strlen(resp_json));
struct json_object *resp_obj = json_tokener_parse(resp_json);
if (!resp_obj) {
LOG("Failed to parse JSON");
snprintf(err_out, 127, "JSON parse failed");
free(resp_json);
return -1;
}
inform_log_controller_response(resp_obj, resp_json);
free(resp_json);
struct json_object *response_type;
if (json_object_object_get_ex(resp_obj, "_type", &response_type))
LOG("Parsed response type: %s",
json_object_get_string(response_type));
char action[64] = "noop";
inform_handle_response(st, model, resp_obj, action);
json_object_put(resp_obj);
LOG("Response action: %s", action);
if (strcmp(action, "noop") != 0)
printf("[openuf] Action: %s\n", action);
return 0;
}
+34
View File
@@ -0,0 +1,34 @@
#ifndef OPENUF_INFORM_INTERNAL_H
#define OPENUF_INFORM_INTERNAL_H
#include <stddef.h>
#include <json-c/json.h>
#include "state.h"
#include "ufmodel.h"
/* Internal contracts shared by the focused inform implementation units. */
char *inform_build_payload(const openuf_state_t *state,
const uf_model_t *model,
long uptime);
unsigned char *inform_packet_build(const char *mac_hex,
const char *key_hex,
const char *payload,
int use_aes_gcm,
size_t *packet_length);
char *inform_packet_parse(const unsigned char *data,
size_t data_length,
const char *key_hex);
void inform_handle_response(openuf_state_t *state,
const uf_model_t *model,
struct json_object *response,
char *action_out);
void inform_log_controller_response(struct json_object *response,
const char *raw_json);
int inform_debug_level(void);
#endif /* OPENUF_INFORM_INTERNAL_H */
+148
View File
@@ -0,0 +1,148 @@
/*
* 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;
}
+8 -649
View File
@@ -1,52 +1,9 @@
/*
* openuf - inform.c
*
* UniFi Inform Protocol full implementation.
*
* HOW IT WORKS
*
* Every 10 seconds the AP makes an HTTP POST to http://<controller>:8080/inform
* with a binary TNBU packet containing JSON encrypted with AES-128-CBC.
*
* The controller responds with another TNBU packet. The AP decrypts, parses
* the JSON, and executes the action (_type).
*
* TNBU BINARY PACKET
*
* Offset Bytes Field
* ------ ----- -----
* 0 4 Magic "TNBU"
* 4 4 Packet version (=0), uint32 BE
* 8 6 AP MAC address
* 14 2 Flags: bit0=encrypted, bit1=zlib
* 16 16 AES IV (when encrypted)
* 32 4 Data version (=1), uint32 BE
* 36 4 Payload length, uint32 BE
* 40 N JSON payload, encrypted with AES-128-CBC
*
* HOW PARAMETERS ARE READ
*
* CPU: sysinfo_cpu_percent() /proc/stat (delta across 2 calls)
* RAM: sysinfo_mem() /proc/meminfo
* Interfaces: sysinfo_iface() /proc/net/dev + /sys/class/net/
* Radios: sysinfo_radio() iw dev <iface> info + survey
* UCI VAPs: wlan_get_vap_table() libuci wireless.*
* WiFi clients: clients_build_sta_table() iw dev <iface> station dump
* IP clients: clients_mac_to_ip() /proc/net/arp
* Client names: clients_mac_to_hostname() /tmp/dhcp.leases
* LLDP neighbors: lldp_read_neighbors() lldpctl -f json
*
* ADOPTION CYCLE
*
* 1. AP sends inform with key=DEFAULT, default=true, state=1
* 2. Controller responds: {_type:"cmd", cmd:"set-adopt",
* key:"new32hexkey", uri:"http://..."}
* 3. AP saves the new key + URL to state.json, adopted=true
* 4. AP sends inform with the new key, state=4, default=false
* 5. Controller responds: {_type:"setstate", radio_table:[...], vap_table:[...]}
* 6. AP applies WiFi config via wlan_apply_config() libuci wifi reload
* 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>
@@ -63,98 +20,9 @@
#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];
}
static int valid_authkey(const char *key)
{
if (!key || strlen(key) != 32)
return 0;
for (size_t i = 0; i < 32; i++)
if (!isxdigit((unsigned char)key[i]))
return 0;
return 1;
}
static int protocol_debug_level;
void inform_set_debug_level(int level)
{
protocol_debug_level = level < 0 ? 0 : level > 2 ? 2 : level;
}
static int debug_system_cfg_key(const char *key)
{
if (!key || (strncmp(key, "aaa.", 4) &&
strncmp(key, "wireless.", 9)))
return 0;
return strstr(key, ".ssid") || strstr(key, ".id") ||
strstr(key, ".vap_ind") || strstr(key, ".parent");
}
static void debug_log_controller_response(struct json_object *response,
const char *raw_json)
{
if (protocol_debug_level <= 0 || !response)
return;
LOG("Protocol debug: decrypted controller response fields follow");
json_object_object_foreach(response, key, value) {
LOG("Protocol field: %s type=%s", key,
json_type_to_name(json_object_get_type(value)));
}
struct json_object *system_cfg_object;
if (json_object_object_get_ex(response, "system_cfg",
&system_cfg_object)) {
const char *system_cfg = json_object_get_string(system_cfg_object);
char *copy = system_cfg ? strdup(system_cfg) : NULL;
if (copy) {
char *save = NULL;
for (char *line = strtok_r(copy, "\n", &save);
line; line = strtok_r(NULL, "\n", &save)) {
line[strcspn(line, "\r")] = '\0';
char *equals = strchr(line, '=');
if (!equals)
continue;
*equals = '\0';
if (debug_system_cfg_key(line))
LOG("Protocol system_cfg: %s=%s", line, equals + 1);
}
free(copy);
}
}
if (protocol_debug_level >= 2) {
LOG("WARNING: full decrypted response may contain credentials");
LOG("Protocol response JSON: %s", raw_json ? raw_json : "");
}
}
/* ═══════════════════════════════════════════════════════════════════
sys_stats CPU and memory of the system
The controller shows CPU and RAM in the device view.
We read /proc/stat and /proc/meminfo directly.
*/
/* Build system CPU, memory, and load statistics. */
static struct json_object *build_sys_stats(int *cpu_percent,
double *mem_percent)
{
@@ -410,7 +278,7 @@ static void build_radio_table(struct json_object *root,
json_object_object_add(root, alias, json_object_get(o));
json_object_array_add(arr, o);
if (protocol_debug_level > 0)
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);
}
@@ -795,9 +663,9 @@ static struct json_object *collect_sta_table(struct json_object *vap_table)
}
/* ═══════════════════════════════════════════════════════════════════
build_payload Complete assembly of the inform JSON
inform_build_payload Complete assembly of the inform JSON
*/
static char *build_payload(const openuf_state_t *st,
char *inform_build_payload(const openuf_state_t *st,
const uf_model_t *m,
long uptime)
{
@@ -932,512 +800,3 @@ static char *build_payload(const openuf_state_t *st,
return copy;
}
/* ═══════════════════════════════════════════════════════════════════
TNBU binary packet
*/
static unsigned char *build_packet(const char *mac_hex,
const char *key_hex,
const char *payload,
int use_aes_gcm,
size_t *out_len)
{
unsigned char iv_hex[33] = {0};
if (crypto_random_hex(iv_hex, 16) != 0) return NULL;
unsigned char mac_bin[6];
crypto_hex2bin(mac_hex, mac_bin, 6);
size_t pl_len = strlen(payload);
size_t body_len = use_aes_gcm
? pl_len + 16
: pl_len + (16 - (pl_len % 16));
size_t pkt_len = 40 + body_len;
unsigned char *pkt = malloc(pkt_len);
if (!pkt) return NULL;
unsigned char *p = pkt;
memcpy(p, INFORM_MAGIC, 4); p += 4;
put32be(p, INFORM_PKT_VERSION); p += 4;
memcpy(p, mac_bin, 6); p += 6;
put16be(p, INFORM_FLAG_ENCRYPTED |
(use_aes_gcm ? INFORM_FLAG_GCM : 0)); p += 2;
unsigned char iv_bin[16];
crypto_hex2bin((char *)iv_hex, iv_bin, 16);
memcpy(p, iv_bin, 16); p += 16;
put32be(p, INFORM_DATA_VERSION); p += 4;
put32be(p, (uint32_t)body_len); p += 4;
int enc_len;
if (use_aes_gcm) {
unsigned char tag[16];
enc_len = crypto_gcm_encrypt(key_hex, (char *)iv_hex,
pkt, 40,
(const unsigned char *)payload, pl_len,
p, tag);
if (enc_len >= 0)
memcpy(p + enc_len, tag, sizeof(tag));
} else {
enc_len = crypto_encrypt(key_hex, (char *)iv_hex,
(const unsigned char *)payload, pl_len, p);
}
if (enc_len < 0) {
free(pkt);
return NULL;
}
*out_len = pkt_len;
return pkt;
}
/* ═══════════════════════════════════════════════════════════════════
Parse binary response from the controller
*/
static char *parse_packet(const unsigned char *data, size_t data_len,
const char *key_hex)
{
if (data_len < 40) return NULL;
if (memcmp(data, INFORM_MAGIC, 4) != 0) return NULL;
uint16_t flags = get16be(data + 14);
const unsigned char *iv_bin = data + 16;
uint32_t body_len = get32be(data + 36);
const unsigned char *body = data + 40;
if (40 + body_len > data_len) return NULL;
if ((flags & INFORM_FLAG_GCM) != 0) {
if (body_len < 16) return NULL;
size_t cipher_len = body_len - 16;
char iv_hex[33];
crypto_bin2hex(iv_bin, 16, iv_hex);
unsigned char *plain = malloc(cipher_len + 1);
if (!plain) return NULL;
int pl = crypto_gcm_decrypt(key_hex, iv_hex, data, 40,
body, cipher_len, body + cipher_len,
plain);
if (pl < 0) { free(plain); return NULL; }
plain[pl] = '\0';
return (char *)plain;
}
if (flags & INFORM_FLAG_ENCRYPTED) {
char iv_hex[33];
crypto_bin2hex(iv_bin, 16, iv_hex);
unsigned char *plain = malloc(body_len + 1);
if (!plain) return NULL;
int pl = crypto_decrypt(key_hex, iv_hex, body, body_len, plain);
if (pl < 0) { free(plain); return NULL; }
plain[pl] = '\0';
return (char *)plain;
}
char *copy = malloc(body_len + 1);
if (!copy) return NULL;
memcpy(copy, body, body_len);
copy[body_len] = '\0';
return copy;
}
static void reboot_openwrt(void)
{
LOG("Controller requested an OpenWrt reboot");
int status = system("/sbin/reboot");
if (status != 0)
LOG("OpenWrt reboot command failed with status=%d", status);
}
/* ═══════════════════════════════════════════════════════════════════
Process JSON command from the controller
_type == "noop" do nothing
_type == "reboot" reboot OpenWrt
_type == "cmd" set-adopt / legacy reboot / reset / locate
_type == "setstate" apply radio_table + vap_table via UCI
_type == "setparam" change a single parameter
*/
static void handle_response(openuf_state_t *st,
const uf_model_t *model,
struct json_object *resp,
char *action_out)
{
struct json_object *v;
const char *type = "noop";
if (json_object_object_get_ex(resp, "_type", &v) &&
json_object_is_type(v, json_type_string))
type = json_object_get_string(v);
LOG("Handling response type: %s", type);
/* An OpenWrt host cannot install UniFi firmware. Acknowledge the
* controller's request by reporting its target version from now on. */
if (!strcmp(type, "upgrade")) {
if (json_object_object_get_ex(resp, "version", &v) &&
json_object_is_type(v, json_type_string)) {
const char *version = json_object_get_string(v);
size_t len = strlen(version);
if (len > 0 && len < sizeof(st->firmware_version)) {
snprintf(st->firmware_version,
sizeof(st->firmware_version), "%s", version);
state_save(st);
LOG("Firmware upgrade spoofed; now reporting version=%s",
st->firmware_version);
strcpy(action_out, "upgrade-spoofed");
return;
}
}
LOG("Ignoring upgrade response without a valid version");
strcpy(action_out, "upgrade-invalid");
return;
}
/* ── noop ────────────────────────────────────────────────────── */
if (!strcmp(type, "noop")) {
strcpy(action_out, "noop");
return;
}
/* Modern controllers send reboot as a top-level response type rather
* than wrapping it in {"_type":"cmd","cmd":"reboot"}. */
if (!strcmp(type, "reboot")) {
strcpy(action_out, "reboot");
reboot_openwrt();
return;
}
/* ── setparam ────────────────────────────────────────────────── */
if (!strcmp(type, "setparam")) {
int received_adoption_key = 0;
int applied_system_cfg = 0;
/* First parse mgmt_cfg used by modern controllers. */
if (json_object_object_get_ex(resp, "mgmt_cfg", &v)) {
const char *mgmt_cfg = json_object_get_string(v);
LOG("Parsing mgmt_cfg: %s", mgmt_cfg);
/* Parse newline-separated key=value pairs. */
char cfg_copy[2048];
strncpy(cfg_copy, mgmt_cfg, sizeof(cfg_copy)-1);
cfg_copy[sizeof(cfg_copy)-1] = '\0';
char *line = strtok(cfg_copy, "\n");
while (line) {
char *eq = strchr(line, '=');
if (eq) {
*eq = '\0';
const char *key = line;
const char *val = eq + 1;
if (!strcmp(key, "authkey"))
LOG("mgmt_cfg param: authkey = %.8s...", val);
else
LOG("mgmt_cfg param: %s = %s", key, val);
if (!strcmp(key, "authkey")) {
if (valid_authkey(val) &&
strcmp(st->authkey, val) != 0) {
int replacing_key = st->authkey[0] &&
strcmp(st->authkey, DEFAULT_AUTH_KEY) != 0;
strncpy(st->authkey, val,
sizeof(st->authkey)-1);
st->authkey[sizeof(st->authkey)-1] = '\0';
received_adoption_key = 1;
LOG("%s device key from setparam",
replacing_key ? "Replaced" : "Accepted");
} else if (!valid_authkey(val)) {
LOG("Ignoring invalid authkey from setparam");
}
} else if (!strcmp(key, "cfgversion")) {
/*
* This is the version the controller wants, not proof
* that its setstate has been applied locally.
*/
LOG("Controller requested cfgversion=%s; currently applied=%s",
val, st->cfgversion);
} else if (!strcmp(key, "use_aes_gcm")) {
st->use_aes_gcm = !strcmp(val, "true") ||
!strcmp(val, "1");
LOG("AES-GCM %s for subsequent inform packets",
st->use_aes_gcm ? "enabled" : "disabled");
} else if (!strcmp(key, "mgmt_url")) {
/* Could save mgmt_url for future use */
}
/* Other management parameters are currently informational. */
}
line = strtok(NULL, "\n");
}
}
struct json_object *system_cfg_obj;
if (json_object_object_get_ex(resp, "system_cfg",
&system_cfg_obj)) {
const char *system_cfg = json_object_get_string(system_cfg_obj);
LOG("Applying legacy system_cfg, length=%zu",
strlen(system_cfg));
if (wlan_apply_system_cfg(system_cfg, model) == 0) {
applied_system_cfg = 1;
st->config_applied = true;
st->config_schema = OPENUF_CONFIG_SCHEMA;
if (json_object_object_get_ex(resp, "cfgversion", &v))
snprintf(st->cfgversion, sizeof(st->cfgversion), "%s",
json_object_get_string(v));
LOG("Legacy system_cfg applied successfully, cfgversion=%s",
st->cfgversion);
} else {
st->config_applied = false;
strncpy(st->cfgversion, "0",
sizeof(st->cfgversion) - 1);
LOG("Legacy system_cfg failed; requesting provisioning retry");
}
}
/* Fall back to the direct key/value format used by older controllers. */
if (json_object_object_get_ex(resp, "key", &v)) {
const char *key = json_object_get_string(v);
struct json_object *val_o;
if (json_object_object_get_ex(resp, "value", &val_o)) {
const char *val = json_object_get_string(val_o);
LOG("setparam key=%s val=%s", key, val);
if (!strcmp(key, "inform_url"))
strncpy(st->inform_url, val, sizeof(st->inform_url)-1);
else if (!strcmp(key, "authkey") && valid_authkey(val) &&
strcmp(st->authkey, val) != 0) {
int replacing_key = st->authkey[0] &&
strcmp(st->authkey, DEFAULT_AUTH_KEY) != 0;
strncpy(st->authkey, val, sizeof(st->authkey)-1);
st->authkey[sizeof(st->authkey)-1] = '\0';
received_adoption_key = 1;
LOG("%s device key from direct setparam",
replacing_key ? "Replaced" : "Accepted");
}
}
}
/*
* Modern controllers complete adoption by returning the per-device
* key in setparam. Mark the device adopted before its next inform so
* both the payload and packet encryption switch to that key.
*/
if (received_adoption_key) {
st->adopted = true;
LOG("Adoption completed through setparam; next inform will use the controller key");
}
state_save(st);
LOG("State saved after setparam");
strcpy(action_out, applied_system_cfg ? "provisioned" :
received_adoption_key ? "adopted" : "setparam");
return;
}
/* ── cmd ─────────────────────────────────────────────────────── */
if (!strcmp(type, "cmd")) {
const char *cmd = "";
if (json_object_object_get_ex(resp, "cmd", &v))
cmd = json_object_get_string(v);
if (!strcmp(cmd, "set-adopt") || !strcmp(cmd, "adopt")) {
if (json_object_object_get_ex(resp, "uri", &v))
strncpy(st->inform_url, json_object_get_string(v),
sizeof(st->inform_url)-1);
if (json_object_object_get_ex(resp, "key", &v))
strncpy(st->authkey, json_object_get_string(v),
sizeof(st->authkey)-1);
st->adopted = true;
state_save(st);
strcpy(action_out, "adopted");
LOG("Adopted successfully. Key: %.8s...", st->authkey);
} else if (!strcmp(cmd, "reboot")) {
strcpy(action_out, "reboot");
reboot_openwrt();
} else if (!strcmp(cmd, "reset")) {
strcpy(action_out, "reset");
system("rm -f " OPENUF_STATE_FILE);
reboot_openwrt();
} else if (!strcmp(cmd, "locate")) {
/* Blink LED — on OpenWrt: echo 1 > /sys/class/leds/.../trigger */
strcpy(action_out, "locate");
} else {
snprintf(action_out, 64, "cmd:%s", cmd);
}
return;
}
/* ── setstate — WiFi configuration from the controller ──────────── */
if (!strcmp(type, "setstate")) {
if (json_object_object_get_ex(resp, "cfgversion", &v))
snprintf(st->cfgversion, sizeof(st->cfgversion),
"%s", json_object_get_string(v));
struct json_object *rt = NULL, *vt = NULL;
json_object_object_get_ex(resp, "radio_table", &rt);
json_object_object_get_ex(resp, "vap_table", &vt);
int apply_ok = 0;
if (rt || vt) {
printf("[openuf] Applying controller WiFi configuration...\n");
apply_ok = wlan_apply_config(resp, model) == 0;
} else {
LOG("setstate contained neither radio_table nor vap_table");
}
st->config_applied = apply_ok;
if (apply_ok)
st->config_schema = OPENUF_CONFIG_SCHEMA;
if (!apply_ok) {
strncpy(st->cfgversion, "0", sizeof(st->cfgversion) - 1);
LOG("WiFi configuration failed; cfgversion reset so the controller retries");
}
state_save(st);
strcpy(action_out, apply_ok ? "setstate" : "setstate-failed");
return;
}
snprintf(action_out, 64, "unknown:%s", type);
}
/* ═══════════════════════════════════════════════════════════════════
inform_send main public function
*/
int inform_send(openuf_state_t *st,
const uf_model_t *model,
long uptime,
char *err_out)
{
if (!st->inform_url[0]) {
LOG("No inform_url set");
strncpy(err_out, "no inform_url", 127);
return -1;
}
const char *key_hex = (st->authkey[0]) ? st->authkey : DEFAULT_AUTH_KEY;
/* CRITICAL: When not adopted, ALWAYS use DEFAULT_AUTH_KEY */
if (!st->adopted && st->authkey[0] && strcmp(st->authkey, DEFAULT_AUTH_KEY) != 0) {
LOG("WARNING: Device not adopted but has custom authkey! Using DEFAULT instead!");
key_hex = DEFAULT_AUTH_KEY;
}
LOG("Sending inform: adopted=%d, authkey=%.8s..., inform_url=%s",
st->adopted, key_hex, st->inform_url);
/* MAC without colons */
char mac_hex[32] = {0};
{
const char *s = st->mac; int j = 0;
for (int i = 0; s[i] && j < 12; i++)
if (s[i] != ':') mac_hex[j++] = s[i];
}
char *payload = build_payload(st, model, uptime);
if (!payload) { strncpy(err_out, "build_payload OOM", 127); return -1; }
LOG("Built payload, length: %zu", strlen(payload));
unsigned char *resp_body = NULL;
size_t resp_len = 0;
int status = -1;
int selected_gcm = st->use_aes_gcm;
/*
* A controller remembers the negotiated cipher. If local state was
* created before use_aes_gcm was persisted, it rejects CBC with HTTP 400
* and cannot send another setparam. Retry once with the other cipher.
*/
for (int attempt = 0; attempt < 2; attempt++) {
size_t pkt_len = 0;
unsigned char *pkt = build_packet(mac_hex, key_hex, payload,
selected_gcm, &pkt_len);
if (!pkt) {
free(payload);
strncpy(err_out, "build_packet failed", 127);
return -1;
}
LOG("Built packet, length: %zu, cipher: %s", pkt_len,
selected_gcm ? "AES-GCM" : "AES-CBC");
status = http_post(st->inform_url,
"application/x-binary-data",
pkt, pkt_len,
&resp_body, &resp_len);
free(pkt);
LOG("HTTP POST to %s, status: %d, response length: %zu",
st->inform_url, status, resp_len);
if (status != 400 || !st->adopted || attempt != 0)
break;
free(resp_body);
resp_body = NULL;
resp_len = 0;
selected_gcm = !selected_gcm;
LOG("Controller rejected %s; retrying once with %s",
selected_gcm ? "AES-CBC" : "AES-GCM",
selected_gcm ? "AES-GCM" : "AES-CBC");
}
free(payload);
if (status < 0) {
snprintf(err_out, 127, "HTTP connect failed");
return -1;
}
if (status != 200) {
snprintf(err_out, 127, "HTTP %d", status);
free(resp_body);
return -1;
}
if (st->use_aes_gcm != selected_gcm) {
st->use_aes_gcm = selected_gcm;
state_save(st);
LOG("Recovered cipher state; persisted aes_gcm=%d",
st->use_aes_gcm);
}
if (!resp_body || resp_len == 0) {
LOG("No response body");
free(resp_body);
return 0;
}
char *resp_json = parse_packet(resp_body, resp_len, key_hex);
free(resp_body);
if (!resp_json) {
LOG("Failed to parse response packet");
snprintf(err_out, 127, "parse_packet failed");
return -1;
}
LOG("Parsed response JSON, length=%zu", strlen(resp_json));
struct json_object *resp_obj = json_tokener_parse(resp_json);
if (!resp_obj) {
LOG("Failed to parse JSON");
snprintf(err_out, 127, "JSON parse failed");
free(resp_json);
return -1;
}
debug_log_controller_response(resp_obj, resp_json);
free(resp_json);
struct json_object *response_type;
if (json_object_object_get_ex(resp_obj, "_type", &response_type))
LOG("Parsed response type: %s",
json_object_get_string(response_type));
char action[64] = "noop";
handle_response(st, model, resp_obj, action);
json_object_put(resp_obj);
LOG("Response action: %s", action);
if (strcmp(action, "noop") != 0)
printf("[openuf] Action: %s\n", action);
return 0;
}
+361
View File
@@ -0,0 +1,361 @@
/*
* 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) {
printf("[openuf] Applying controller WiFi configuration...\n");
apply_ok = wlan_apply_config(resp, model) == 0;
} else {
LOG("setstate contained neither radio_table nor vap_table");
}
st->config_applied = apply_ok;
if (apply_ok)
st->config_schema = OPENUF_CONFIG_SCHEMA;
if (!apply_ok) {
strncpy(st->cfgversion, "0", sizeof(st->cfgversion) - 1);
LOG("WiFi configuration failed; cfgversion reset so the controller retries");
}
state_save(st);
strcpy(action_out, apply_ok ? "setstate" : "setstate-failed");
return;
}
snprintf(action_out, 64, "unknown:%s", type);
}
+1 -1
View File
@@ -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
View File
@@ -24,7 +24,7 @@
* TLV type=2 Port ID subtype=5(ifname), value="eth0"
* TLV type=3 TTL value=uint16_BE
* TLV type=5 System Name value=hostname
* TLV type=6 System 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
*
+1 -1
View File
@@ -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)
*/
+2 -2
View File
@@ -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];
-1906
View File
File diff suppressed because it is too large Load Diff
+229
View File
@@ -0,0 +1,229 @@
/*
* Shared translations and validators used by Wi-Fi provisioning and
* telemetry. This unit does not commit controller configuration itself.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <uci.h>
#include <json-c/json.h>
#include "wlan.h"
#include "ufmodel.h"
#include "crypto.h"
#include "wlan_internal.h"
const char *wlan_security_to_uci(const char *uf)
{
if (!uf || !strcmp(uf,"open")) return "none";
if (!strcmp(uf,"wpapsk")) return "psk";
if (!strcmp(uf,"wpa2psk")) return "psk2";
if (!strcmp(uf,"wpapskwpa2psk")) return "psk-mixed";
if (!strcmp(uf,"wpa3")) return "sae";
if (!strcmp(uf,"wpa3transition")) return "sae-mixed";
if (!strcmp(uf,"wpa2enterprise")) return "wpa2";
if (!strcmp(uf,"wpa3enterprise")) return "wpa3";
return "psk2"; /* default */
}
/* OpenWrt UCI encryption names to UniFi security names. */
const char *wlan_security_to_unifi(const char *uci)
{
if (!uci || !strcmp(uci,"none")) return "open";
if (!strcmp(uci,"psk")) return "wpapsk";
if (!strcmp(uci,"psk2")) return "wpa2psk";
if (!strcmp(uci,"psk-mixed")) return "wpapskwpa2psk";
if (!strcmp(uci,"sae")) return "wpa3";
if (!strcmp(uci,"sae-mixed")) return "wpa3transition";
if (!strcmp(uci,"wpa2")) return "wpa2enterprise";
if (!strcmp(uci,"wpa3")) return "wpa3enterprise";
return "wpa2psk";
}
/* Return true only for the 24-character hexadecimal IDs used by UniFi. */
int wlan_valid_object_id(const char *id)
{
if (!id || strlen(id) != 24)
return 0;
for (size_t i = 0; i < 24; i++)
if (!((id[i] >= '0' && id[i] <= '9') ||
(id[i] >= 'a' && id[i] <= 'f') ||
(id[i] >= 'A' && id[i] <= 'F')))
return 0;
return 1;
}
/*
* UniFi uses a 24-character hexadecimal MongoDB ObjectId to associate VAP
* telemetry and stations with a WLAN. Older legacy system_cfg payloads do
* not include that ID, so allocate a local one and keep it stable in UCI.
* All radio instances of the same WLAN must report the same ID.
*/
int wlan_ensure_vap_ids(struct json_object *vaps)
{
if (!vaps || !json_object_is_type(vaps, json_type_array))
return 0;
struct uci_context *ctx = uci_alloc_context();
struct uci_package *pkg = NULL;
if (ctx)
uci_load(ctx, "wireless", &pkg);
int count = json_object_array_length(vaps);
for (int i = 0; i < count; i++) {
struct json_object *vap = json_object_array_get_idx(vaps, i);
struct json_object *value;
if (!vap)
continue;
const char *id = NULL;
const char *id_keys[] = { "_id", "id", "wlanconf_id" };
for (size_t n = 0; n < sizeof(id_keys) / sizeof(id_keys[0]); n++) {
if (json_object_object_get_ex(vap, id_keys[n], &value)) {
const char *candidate = json_object_get_string(value);
if (wlan_valid_object_id(candidate)) {
id = candidate;
break;
}
}
}
if (id)
continue;
const char *ssid = "";
if (json_object_object_get_ex(vap, "essid", &value))
ssid = json_object_get_string(value);
/* Reuse an ID already assigned to this WLAN in the same payload. */
for (int j = 0; j < i && !id; j++) {
struct json_object *previous = json_object_array_get_idx(vaps, j);
struct json_object *previous_ssid;
struct json_object *previous_id;
if (previous &&
json_object_object_get_ex(previous, "essid", &previous_ssid) &&
!strcmp(json_object_get_string(previous_ssid), ssid) &&
json_object_object_get_ex(previous, "id", &previous_id) &&
wlan_valid_object_id(json_object_get_string(previous_id)))
id = json_object_get_string(previous_id);
}
/* Reuse the ID committed by an earlier provisioning cycle. */
if (!id && pkg) {
struct uci_element *element;
uci_foreach_element(&pkg->sections, element) {
struct uci_section *section = uci_to_section(element);
const char *stored_ssid;
const char *stored_id;
if (strcmp(section->type, "wifi-iface") ||
strncmp(section->e.name, "openuf_", 7))
continue;
stored_ssid = uci_lookup_option_string(ctx, section, "ssid");
stored_id = uci_lookup_option_string(ctx, section,
"openuf_vap_id");
if (stored_ssid && !strcmp(stored_ssid, ssid) &&
wlan_valid_object_id(stored_id)) {
id = stored_id;
break;
}
}
}
char generated[25];
if (!id) {
if (crypto_random_hex((unsigned char *)generated, 12) != 0) {
printf("[openuf] Failed to generate a VAP ID for '%s'\n", ssid);
if (pkg) uci_unload(ctx, pkg);
if (ctx) uci_free_context(ctx);
return -1;
}
id = generated;
printf("[openuf] Generated persistent VAP ID %s for '%s'\n",
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';
}
+271
View File
@@ -0,0 +1,271 @@
/*
* 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);
printf("[openuf] Parsed legacy system_cfg: %zu radios, %zu VAPs\n",
json_object_array_length(radios), json_object_array_length(vaps));
int result = wlan_apply_config(root, model);
json_object_put(root);
return result;
}
+586
View File
@@ -0,0 +1,586 @@
/*
* 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) {
printf("[openuf] Failed to configure VLAN network %d\n", 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) {
printf("[openuf] Failed to create VAP section '%s'\n", 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]) {
printf("[openuf] Refusing Enterprise VAP '%s': missing RADIUS "
"authentication server or secret\n", 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) {
printf("[openuf] Failed to store RADIUS authentication for "
"VAP '%s'\n", essid);
return -1;
}
if (acct_server && acct_server[0]) {
if (!acct_secret || !acct_secret[0]) {
printf("[openuf] Refusing Enterprise VAP '%s': accounting "
"server has no secret\n", 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) {
printf("[openuf] Failed to store RADIUS accounting for "
"VAP '%s'\n", 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;
printf("[openuf] Disabled FT on unsupported 2.4 GHz ath9k radio %s\n",
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) {
printf("[openuf] Failed to bind VAP '%s' to %s\n",
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) {
printf("[openuf] Refusing incomplete VAP '%s': core AP options "
"could not be stored\n", essid);
return -1;
}
if (!enterprise && pass && pass[0] && strcmp(security, "open") != 0 &&
wlan_uci_set_required(ctx, pkg, sec_name, "key", pass) != 0) {
printf("[openuf] Refusing unsecured VAP '%s': key could not be stored\n",
essid);
return -1;
}
if (wlan_uci_set_required(ctx, pkg, sec_name, "network", target_network) != 0) {
printf("[openuf] Refusing unsafe VAP '%s': cannot bind to %s\n",
essid, target_network);
return -1;
}
printf("[openuf] VAP '%s' -> %s device=%s network=%s enc=%s "
"ft=%d bs=%d handoff=%d pmf=%d\n",
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. */
printf("[openuf] Stopping Wi-Fi before controller provisioning...\n");
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) {
printf("[openuf] Ignoring settings for unknown radio '%s'\n",
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) {
printf("[openuf] Failed to allocate UCI context\n");
return -1;
}
struct uci_package *pkg = NULL;
if (uci_load(ctx, "wireless", &pkg) != UCI_OK) {
char *uci_error = NULL;
uci_get_errorstr(ctx, &uci_error, "wireless");
printf("[openuf] Failed to load UCI wireless configuration: %s\n",
uci_error ? uci_error : "unknown UCI error");
free(uci_error);
uci_free_context(ctx);
return -1;
}
/*
* Once UniFi provisioning owns Wi-Fi, disable OpenWrt's generated
* default VAPs. Leaving them enabled keeps broadcasting "OpenWrt"
* alongside the controller-managed SSIDs.
*/
int disabled_defaults = 0;
struct uci_element *default_element;
uci_foreach_element(&pkg->sections, default_element) {
struct uci_section *section = uci_to_section(default_element);
if (!strcmp(section->type, "wifi-iface") &&
!strncmp(section->e.name, "default_radio", 13)) {
WLAN_UCI_SET(ctx, "wireless", section->e.name, "disabled", "1");
disabled_defaults++;
}
}
if (disabled_defaults)
printf("[openuf] Disabled %d default OpenWrt VAPs\n",
disabled_defaults);
/* 3. 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) {
printf("[openuf] Ignoring VAP with unknown radio '%s'\n",
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");
printf("[openuf] Failed to commit UCI wireless configuration: %s\n",
uci_error ? uci_error : "unknown UCI error");
free(uci_error);
uci_unload(ctx, pkg);
uci_free_context(ctx);
return -1;
}
uci_unload(ctx, pkg);
uci_free_context(ctx);
if (wlan_configure_band_steering(steering_policy_enabled) != 0)
printf("[openuf] Failed to configure the band steering policy\n");
/*
* 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.
*/
printf("[openuf] Starting controller-managed Wi-Fi sequentially...\n");
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)) {
printf("[openuf] Refusing invalid radio name\n");
continue;
}
int radio_up = 0;
for (int attempt = 1; attempt <= 2 && !radio_up; attempt++) {
printf("[openuf] Starting %s (%s), attempt %d...\n",
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)
printf("[openuf] %s did not reach the up state; retrying\n",
device);
}
if (!radio_up)
printf("[openuf] %s failed after 2 start attempts\n", 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)
printf("[openuf] hostapd on phy%d lacks runtime 802.11v "
"support; continuing without BSS Transition\n",
phy_index);
}
}
/* Restart after hostapd has registered both BSSes on ubus. */
system("/etc/init.d/usteer restart >/dev/null 2>&1");
return 0;
}
+318
View File
@@ -0,0 +1,318 @@
/*
* 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);
}
printf("[openuf] Radio mapping: %s -> %s%s\n",
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);
printf("[openuf] Radio %s standard: %s%s\n", device_name, htmode,
force_wifi4 ? " (Force WiFi 4)" : " (newest supported)");
}
/* Channel: 0 = auto in UniFi */
if (json_object_object_get_ex(radio_json, "channel", &v)) {
int ch = json_object_get_int(v);
if (ch == 0) {
snprintf(path, sizeof(path), "wireless.%s.channel=auto", device_name);
} else {
snprintf(path, sizeof(path), "wireless.%s.channel=%d", device_name, ch);
}
struct uci_ptr ptr;
if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK)
uci_set(ctx, &ptr);
}
/* tx_power */
if (json_object_object_get_ex(radio_json, "tx_power", &v)) {
snprintf(path, sizeof(path), "wireless.%s.txpower=%d",
device_name, json_object_get_int(v));
struct uci_ptr ptr;
if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK)
uci_set(ctx, &ptr);
}
/* Enable the radio */
snprintf(path, sizeof(path), "wireless.%s.disabled=0", device_name);
struct uci_ptr ptr;
if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK)
uci_set(ctx, &ptr);
uci_commit(ctx, &pkg, false);
uci_unload(ctx, pkg);
uci_free_context(ctx);
}
+166
View File
@@ -0,0 +1,166 @@
/*
* Read openuf-managed UCI VAP sections and expose the controller-compatible
* vap_table representation, including runtime interface and BSSID data.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <uci.h>
#include <json-c/json.h>
#include "wlan.h"
#include "ufmodel.h"
#include "crypto.h"
#include "wlan_internal.h"
/*
* Only wifi-iface sections with the openuf_ prefix are reported. Runtime
* nl80211 interface names and sysfs BSSIDs are joined with their effective
* UCI security, roaming, steering, PMF, visibility, and VLAN settings.
*/
/* Resolve a configured VAP to the live interface reported by nl80211. */
static int find_runtime_vap(int phy_index, const char *ssid,
char *out, size_t out_size)
{
FILE *pipe = popen("iw dev 2>/dev/null", "r");
if (!pipe) return -1;
int phy = -1;
char candidate[32] = "";
char line[256];
while (fgets(line, sizeof(line), pipe)) {
int parsed_phy;
char value[128];
if (sscanf(line, "phy#%d", &parsed_phy) == 1) {
phy = parsed_phy;
candidate[0] = '\0';
continue;
}
if (sscanf(line, " Interface %31s", value) == 1) {
snprintf(candidate, sizeof(candidate), "%s", value);
continue;
}
if (phy == phy_index && candidate[0] &&
sscanf(line, " ssid %127[^\n]", value) == 1 &&
!strcmp(value, ssid)) {
snprintf(out, out_size, "%s", candidate);
pclose(pipe);
return 0;
}
}
pclose(pipe);
return -1;
}
struct json_object *wlan_get_vap_table(const uf_model_t *model)
{
struct json_object *arr = json_object_new_array();
struct uci_context *ctx = uci_alloc_context();
if (!ctx) return arr;
struct uci_package *pkg = NULL;
if (uci_load(ctx, "wireless", &pkg) != UCI_OK) {
uci_free_context(ctx);
return arr;
}
struct uci_element *e;
uci_foreach_element(&pkg->sections, e) {
struct uci_section *sec = uci_to_section(e);
if (strcmp(sec->type, "wifi-iface") != 0) continue;
/* Only report VAPs managed by openuf */
if (strncmp(sec->e.name, "openuf_", 7) != 0) continue;
#define UCI_GET(opt) uci_lookup_option_string(ctx, sec, opt)
const char *ssid = UCI_GET("ssid");
const char *device = UCI_GET("device");
const char *enc = UCI_GET("encryption");
const char *dis = UCI_GET("disabled");
const char *r11 = UCI_GET("ieee80211r");
const char *ft_req = UCI_GET("openuf_ft_requested");
const char *k11 = UCI_GET("ieee80211k");
const char *btm = UCI_GET("bss_transition");
const char *bs_req = UCI_GET("openuf_band_steering");
const char *ho_req = UCI_GET("openuf_handoff_suggestions");
const char *w11 = UCI_GET("ieee80211w");
const char *hidden = UCI_GET("hidden");
const char *vap_id = UCI_GET("openuf_vap_id");
const char *vlan = UCI_GET("vlan_id");
if (!ssid) ssid = "";
if (!device) device = "radio0";
/* Band of this radio */
const char *radio_band = wlan_band_for_device(model, device);
if (!radio_band) radio_band = "ng";
/* Resolve the actual netifd interface (for example phy1-ap0). */
char wlan_iface[32];
int ridx = 0;
sscanf(device, "radio%d", &ridx);
if (find_runtime_vap(ridx, ssid, wlan_iface, sizeof(wlan_iface)) != 0)
snprintf(wlan_iface, sizeof(wlan_iface), "phy%d-ap0", ridx);
/* Read the actual BSSID from sysfs */
char bssid[32] = "00:00:00:00:00:00";
{
char path[128];
snprintf(path, sizeof(path), "/sys/class/net/%s/address", wlan_iface);
FILE *f = fopen(path, "r");
if (f) {
fgets(bssid, sizeof(bssid), f); fclose(f);
bssid[strcspn(bssid, "\r\n")] = '\0';
}
}
/* PMF: ieee80211w → "disabled"/"optional"/"required" */
const char *pmf = "disabled";
if (w11) {
if (!strcmp(w11,"1")) pmf = "optional";
if (!strcmp(w11,"2")) pmf = "required";
}
bool ft_on = (r11 && !strcmp(r11,"1")) ||
(ft_req && !strcmp(ft_req,"1"));
bool bs_on = bs_req ? !strcmp(bs_req, "1") :
(k11 && !strcmp(k11, "1"));
bool handoff_on = ho_req ? !strcmp(ho_req, "1") :
(btm && !strcmp(btm, "1"));
bool hid = (hidden && !strcmp(hidden,"1"));
bool up = !(dis && !strcmp(dis,"1"));
struct json_object *o = json_object_new_object();
json_object_object_add(o, "essid", json_object_new_string(ssid));
json_object_object_add(o, "bssid", json_object_new_string(bssid));
json_object_object_add(o, "name", json_object_new_string(wlan_iface));
json_object_object_add(o, "ifname", json_object_new_string(wlan_iface));
json_object_object_add(o, "radio", json_object_new_string(radio_band));
json_object_object_add(o, "security", json_object_new_string(wlan_security_to_unifi(enc)));
json_object_object_add(o, "up", json_object_new_boolean(up));
json_object_object_add(o, "hide_ssid", json_object_new_boolean(hid));
json_object_object_add(o, "fast_roaming_enabled",json_object_new_boolean(ft_on));
json_object_object_add(o, "band_steering", json_object_new_boolean(bs_on));
json_object_object_add(o, "bss_transition", json_object_new_boolean(handoff_on));
json_object_object_add(o, "handoff_suggestions", json_object_new_boolean(handoff_on));
json_object_object_add(o, "pmf_mode", json_object_new_string(pmf));
json_object_object_add(o, "num_sta", json_object_new_int(0));
if (vlan && atoi(vlan) > 0)
json_object_object_add(o, "vlan_id", json_object_new_int(atoi(vlan)));
if (wlan_valid_object_id(vap_id))
json_object_object_add(o, "id", json_object_new_string(vap_id));
if (wlan_valid_object_id(vap_id))
json_object_object_add(o, "wlanconf_id",
json_object_new_string(vap_id));
json_object_array_add(arr, o);
#undef UCI_GET
}
uci_unload(ctx, pkg);
uci_free_context(ctx);
return arr;
}
+310
View File
@@ -0,0 +1,310 @@
/*
* 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)
printf("[openuf] Configured VLAN %d on uplink %s as network '%s'\n",
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) {
printf("[openuf] Cannot load /etc/config/usteer\n");
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);
printf("[openuf] Band steering policy %s (usteer)\n",
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);
printf("[openuf] wlan_clear: removed %d managed VAPs\n",
delete_count);
}
uci_unload(ctx, pkg);
uci_free_context(ctx);
}
+60
View File
@@ -0,0 +1,60 @@
#ifndef OPENUF_WLAN_INTERNAL_H
#define OPENUF_WLAN_INTERNAL_H
#include <stddef.h>
#include <stdio.h>
#include <json-c/json.h>
#include <uci.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 */
+411
View File
@@ -0,0 +1,411 @@
# Function Call Graph
Generated from `src/**/*.c` by `scripts/docs/generate-wiki.py`.
```mermaid
flowchart LR
subgraph module_0["src/announce.c"]
function_announce_c_tlv_append["tlv_append()"]
function_announce_c_tlv_str["tlv_str()"]
function_announce_c_put32be["put32be()"]
function_announce_c_parse_mac["parse_mac()"]
function_announce_c_parse_ip["parse_ip()"]
function_announce_c_announce_init["announce_init()"]
function_announce_c_announce_send["announce_send()"]
function_announce_c_announce_close["announce_close()"]
end
subgraph module_1["src/clients.c"]
function_clients_c_mac_lower["mac_lower()"]
function_clients_c_clients_mac_to_ip["clients_mac_to_ip()"]
function_clients_c_clients_mac_to_hostname["clients_mac_to_hostname()"]
function_clients_c_parse_rate_kbps["parse_rate_kbps()"]
function_clients_c_clients_read_wifi["clients_read_wifi()"]
function_clients_c_clients_build_sta_table["clients_build_sta_table()"]
end
subgraph module_2["src/config.c"]
function_config_c_config_load["config_load()"]
end
subgraph module_3["src/crypto.c"]
function_crypto_c_crypto_hex2bin["crypto_hex2bin()"]
function_crypto_c_crypto_bin2hex["crypto_bin2hex()"]
function_crypto_c_crypto_random_hex["crypto_random_hex()"]
function_crypto_c_crypto_encrypt["crypto_encrypt()"]
function_crypto_c_crypto_decrypt["crypto_decrypt()"]
function_crypto_c_crypto_gcm_encrypt["crypto_gcm_encrypt()"]
function_crypto_c_crypto_gcm_decrypt["crypto_gcm_decrypt()"]
end
subgraph module_4["src/http.c"]
function_http_c_parse_url["parse_url()"]
function_http_c_http_post["http_post()"]
end
subgraph module_5["src/inform/inform.c"]
function_inform_inform_c_inform_send["inform_send()"]
end
subgraph module_6["src/inform/packet.c"]
function_inform_packet_c_put32be["put32be()"]
function_inform_packet_c_put16be["put16be()"]
function_inform_packet_c_get32be["get32be()"]
function_inform_packet_c_get16be["get16be()"]
function_inform_packet_c_inform_packet_build["inform_packet_build()"]
function_inform_packet_c_inform_packet_parse["inform_packet_parse()"]
end
subgraph module_7["src/inform/payload.c"]
function_inform_payload_c_build_sys_stats["build_sys_stats()"]
function_inform_payload_c_build_system_stats["build_system_stats()"]
function_inform_payload_c_build_if_table["build_if_table()"]
function_inform_payload_c_radio_runtime_iface["radio_runtime_iface()"]
function_inform_payload_c_build_scan_table["build_scan_table()"]
function_inform_payload_c_build_athstats["build_athstats()"]
function_inform_payload_c_build_radio_table["build_radio_table()"]
function_inform_payload_c_build_radio_table_stats["build_radio_table_stats()"]
function_inform_payload_c_build_port_table["build_port_table()"]
function_inform_payload_c_build_eth_table["build_eth_table()"]
function_inform_payload_c_build_vap_table["build_vap_table()"]
function_inform_payload_c_collect_sta_table["collect_sta_table()"]
function_inform_payload_c_inform_build_payload["inform_build_payload()"]
end
subgraph module_8["src/inform/response.c"]
function_inform_response_c_inform_valid_auth_key["inform_valid_auth_key()"]
function_inform_response_c_inform_set_debug_level["inform_set_debug_level()"]
function_inform_response_c_inform_debug_level["inform_debug_level()"]
function_inform_response_c_inform_debug_system_cfg_key["inform_debug_system_cfg_key()"]
function_inform_response_c_inform_log_controller_response["inform_log_controller_response()"]
function_inform_response_c_reboot_openwrt["reboot_openwrt()"]
function_inform_response_c_inform_handle_response["inform_handle_response()"]
end
subgraph module_9["src/lldp.c"]
function_lldp_c_tlv_write["tlv_write()"]
function_lldp_c_tlv_str["tlv_str()"]
function_lldp_c_parse_mac["parse_mac()"]
function_lldp_c_lldp_send_frame["lldp_send_frame()"]
function_lldp_c_lldp_available["lldp_available()"]
function_lldp_c_lldp_read_neighbors["lldp_read_neighbors()"]
end
subgraph module_10["src/main.c"]
function_main_c_get_mac["get_mac()"]
function_main_c_get_ip["get_ip()"]
function_main_c_get_default_gateway["get_default_gateway()"]
function_main_c_main["main()"]
end
subgraph module_11["src/models.c"]
function_models_c_ufmodel_find["ufmodel_find()"]
end
subgraph module_12["src/state.c"]
function_state_c_state_defaults["state_defaults()"]
function_state_c_state_load["state_load()"]
function_state_c_state_save["state_save()"]
end
subgraph module_13["src/sysinfo.c"]
function_sysinfo_c_sysinfo_mem["sysinfo_mem()"]
function_sysinfo_c_read_cpu["read_cpu()"]
function_sysinfo_c_sysinfo_cpu_percent["sysinfo_cpu_percent()"]
function_sysinfo_c_read_sysfs_str["read_sysfs_str()"]
function_sysinfo_c_read_sysfs_int["read_sysfs_int()"]
function_sysinfo_c_read_ip_ioctl["read_ip_ioctl()"]
function_sysinfo_c_sysinfo_iface["sysinfo_iface()"]
function_sysinfo_c_utilization_percent["utilization_percent()"]
function_sysinfo_c_count_antenna_chains["count_antenna_chains()"]
function_sysinfo_c_sysinfo_radio["sysinfo_radio()"]
function_sysinfo_c_frequency_to_channel["frequency_to_channel()"]
function_sysinfo_c_sysinfo_wifi_scan_cache["sysinfo_wifi_scan_cache()"]
end
subgraph module_14["src/wlan/common.c"]
function_wlan_common_c_wlan_security_to_uci["wlan_security_to_uci()"]
function_wlan_common_c_wlan_security_to_unifi["wlan_security_to_unifi()"]
function_wlan_common_c_wlan_valid_object_id["wlan_valid_object_id()"]
function_wlan_common_c_wlan_ensure_vap_ids["wlan_ensure_vap_ids()"]
function_wlan_common_c_wlan_mobility_domain_for_ssid["wlan_mobility_domain_for_ssid()"]
function_wlan_common_c_wlan_radio_uses_ath9k["wlan_radio_uses_ath9k()"]
function_wlan_common_c_wlan_json_boolean_any["wlan_json_boolean_any()"]
function_wlan_common_c_wlan_feature_enabled["wlan_feature_enabled()"]
function_wlan_common_c_wlan_safe_section_name["wlan_safe_section_name()"]
end
subgraph module_15["src/wlan/legacy.c"]
function_wlan_legacy_c_system_cfg_get["system_cfg_get()"]
function_wlan_legacy_c_wlan_apply_system_cfg["wlan_apply_system_cfg()"]
end
subgraph module_16["src/wlan/provision.c"]
function_wlan_provision_c_apply_vap["apply_vap()"]
function_wlan_provision_c_force_wifi4_for_band["force_wifi4_for_band()"]
function_wlan_provision_c_wlan_apply_config["wlan_apply_config()"]
end
subgraph module_17["src/wlan/radio.c"]
function_wlan_radio_c_band_bit["band_bit()"]
function_wlan_radio_c_bit_count["bit_count()"]
function_wlan_radio_c_detect_radio_bands["detect_radio_bands()"]
function_wlan_radio_c_resolve_radio_map["resolve_radio_map()"]
function_wlan_radio_c_wlan_device_for_band["wlan_device_for_band()"]
function_wlan_radio_c_wlan_band_for_device["wlan_band_for_device()"]
function_wlan_radio_c_radio_max_standard["radio_max_standard()"]
function_wlan_radio_c_select_htmode["select_htmode()"]
function_wlan_radio_c_wlan_apply_radio["wlan_apply_radio()"]
end
subgraph module_18["src/wlan/telemetry.c"]
function_wlan_telemetry_c_find_runtime_vap["find_runtime_vap()"]
function_wlan_telemetry_c_wlan_get_vap_table["wlan_get_vap_table()"]
end
subgraph module_19["src/wlan/uci.c"]
function_wlan_uci_c_wlan_uci_set["wlan_uci_set()"]
function_wlan_uci_c_wlan_uci_set_required["wlan_uci_set_required()"]
function_wlan_uci_c_wlan_uci_add_list["wlan_uci_add_list()"]
function_wlan_uci_c_wlan_uci_ensure_section["wlan_uci_ensure_section()"]
function_wlan_uci_c_find_vlan_uplink["find_vlan_uplink()"]
function_wlan_uci_c_wlan_ensure_vlan_network["wlan_ensure_vlan_network()"]
function_wlan_uci_c_wlan_configure_band_steering["wlan_configure_band_steering()"]
function_wlan_uci_c_wlan_clear["wlan_clear()"]
end
function_announce_c_tlv_str --> function_announce_c_tlv_append
function_announce_c_announce_init --> function_announce_c_tlv_append
function_announce_c_announce_init --> function_announce_c_tlv_str
function_announce_c_announce_init --> function_announce_c_parse_mac
function_announce_c_announce_init --> function_announce_c_parse_ip
function_announce_c_announce_send --> function_announce_c_put32be
function_clients_c_clients_mac_to_ip --> function_clients_c_mac_lower
function_clients_c_clients_mac_to_hostname --> function_clients_c_mac_lower
function_clients_c_clients_read_wifi --> function_clients_c_clients_mac_to_ip
function_clients_c_clients_read_wifi --> function_clients_c_clients_mac_to_hostname
function_clients_c_clients_read_wifi --> function_clients_c_parse_rate_kbps
function_clients_c_clients_build_sta_table --> function_clients_c_clients_read_wifi
function_crypto_c_crypto_random_hex --> function_crypto_c_crypto_bin2hex
function_crypto_c_crypto_encrypt --> function_crypto_c_crypto_hex2bin
function_crypto_c_crypto_decrypt --> function_crypto_c_crypto_hex2bin
function_crypto_c_crypto_gcm_encrypt --> function_crypto_c_crypto_hex2bin
function_crypto_c_crypto_gcm_decrypt --> function_crypto_c_crypto_hex2bin
function_http_c_http_post --> function_http_c_parse_url
function_inform_inform_c_inform_send --> function_http_c_http_post
function_inform_inform_c_inform_send --> function_inform_packet_c_inform_packet_build
function_inform_inform_c_inform_send --> function_inform_packet_c_inform_packet_parse
function_inform_inform_c_inform_send --> function_inform_payload_c_inform_build_payload
function_inform_inform_c_inform_send --> function_inform_response_c_inform_log_controller_response
function_inform_inform_c_inform_send --> function_inform_response_c_inform_handle_response
function_inform_inform_c_inform_send --> function_state_c_state_save
function_inform_packet_c_inform_packet_build --> function_crypto_c_crypto_hex2bin
function_inform_packet_c_inform_packet_build --> function_crypto_c_crypto_random_hex
function_inform_packet_c_inform_packet_build --> function_crypto_c_crypto_encrypt
function_inform_packet_c_inform_packet_build --> function_crypto_c_crypto_gcm_encrypt
function_inform_packet_c_inform_packet_build --> function_inform_packet_c_put32be
function_inform_packet_c_inform_packet_build --> function_inform_packet_c_put16be
function_inform_packet_c_inform_packet_parse --> function_crypto_c_crypto_bin2hex
function_inform_packet_c_inform_packet_parse --> function_crypto_c_crypto_decrypt
function_inform_packet_c_inform_packet_parse --> function_crypto_c_crypto_gcm_decrypt
function_inform_packet_c_inform_packet_parse --> function_inform_packet_c_get32be
function_inform_packet_c_inform_packet_parse --> function_inform_packet_c_get16be
function_inform_payload_c_build_sys_stats --> function_sysinfo_c_sysinfo_mem
function_inform_payload_c_build_sys_stats --> function_sysinfo_c_sysinfo_cpu_percent
function_inform_payload_c_build_if_table --> function_sysinfo_c_sysinfo_iface
function_inform_payload_c_radio_runtime_iface --> function_wlan_radio_c_wlan_device_for_band
function_inform_payload_c_radio_runtime_iface --> function_wlan_telemetry_c_wlan_get_vap_table
function_inform_payload_c_build_scan_table --> function_sysinfo_c_sysinfo_wifi_scan_cache
function_inform_payload_c_build_radio_table --> function_inform_payload_c_radio_runtime_iface
function_inform_payload_c_build_radio_table --> function_inform_payload_c_build_scan_table
function_inform_payload_c_build_radio_table --> function_inform_payload_c_build_athstats
function_inform_payload_c_build_radio_table --> function_inform_response_c_inform_debug_level
function_inform_payload_c_build_radio_table_stats --> function_inform_payload_c_radio_runtime_iface
function_inform_payload_c_build_radio_table_stats --> function_sysinfo_c_sysinfo_radio
function_inform_payload_c_build_port_table --> function_sysinfo_c_sysinfo_iface
function_inform_payload_c_build_vap_table --> function_clients_c_clients_build_sta_table
function_inform_payload_c_build_vap_table --> function_sysinfo_c_sysinfo_iface
function_inform_payload_c_build_vap_table --> function_sysinfo_c_sysinfo_radio
function_inform_payload_c_build_vap_table --> function_wlan_radio_c_wlan_device_for_band
function_inform_payload_c_build_vap_table --> function_wlan_telemetry_c_wlan_get_vap_table
function_inform_payload_c_inform_build_payload --> function_inform_payload_c_build_sys_stats
function_inform_payload_c_inform_build_payload --> function_inform_payload_c_build_system_stats
function_inform_payload_c_inform_build_payload --> function_inform_payload_c_build_if_table
function_inform_payload_c_inform_build_payload --> function_inform_payload_c_build_radio_table
function_inform_payload_c_inform_build_payload --> function_inform_payload_c_build_radio_table_stats
function_inform_payload_c_inform_build_payload --> function_inform_payload_c_build_port_table
function_inform_payload_c_inform_build_payload --> function_inform_payload_c_build_eth_table
function_inform_payload_c_inform_build_payload --> function_inform_payload_c_build_vap_table
function_inform_payload_c_inform_build_payload --> function_inform_payload_c_collect_sta_table
function_inform_payload_c_inform_build_payload --> function_lldp_c_lldp_read_neighbors
function_inform_response_c_inform_log_controller_response --> function_inform_response_c_inform_debug_system_cfg_key
function_inform_response_c_inform_handle_response --> function_inform_response_c_inform_valid_auth_key
function_inform_response_c_inform_handle_response --> function_inform_response_c_reboot_openwrt
function_inform_response_c_inform_handle_response --> function_state_c_state_save
function_inform_response_c_inform_handle_response --> function_wlan_legacy_c_wlan_apply_system_cfg
function_inform_response_c_inform_handle_response --> function_wlan_provision_c_wlan_apply_config
function_lldp_c_tlv_str --> function_lldp_c_tlv_write
function_lldp_c_lldp_send_frame --> function_lldp_c_tlv_write
function_lldp_c_lldp_send_frame --> function_lldp_c_tlv_str
function_lldp_c_lldp_send_frame --> function_lldp_c_parse_mac
function_lldp_c_lldp_read_neighbors --> function_lldp_c_lldp_available
function_main_c_main --> function_announce_c_announce_init
function_main_c_main --> function_announce_c_announce_send
function_main_c_main --> function_announce_c_announce_close
function_main_c_main --> function_config_c_config_load
function_main_c_main --> function_inform_inform_c_inform_send
function_main_c_main --> function_inform_response_c_inform_set_debug_level
function_main_c_main --> function_lldp_c_lldp_send_frame
function_main_c_main --> function_lldp_c_lldp_available
function_main_c_main --> function_main_c_get_mac
function_main_c_main --> function_main_c_get_ip
function_main_c_main --> function_main_c_get_default_gateway
function_main_c_main --> function_models_c_ufmodel_find
function_main_c_main --> function_state_c_state_load
function_main_c_main --> function_state_c_state_save
function_state_c_state_load --> function_state_c_state_defaults
function_sysinfo_c_sysinfo_cpu_percent --> function_sysinfo_c_read_cpu
function_sysinfo_c_read_sysfs_int --> function_sysinfo_c_read_sysfs_str
function_sysinfo_c_sysinfo_iface --> function_sysinfo_c_read_sysfs_str
function_sysinfo_c_sysinfo_iface --> function_sysinfo_c_read_sysfs_int
function_sysinfo_c_sysinfo_iface --> function_sysinfo_c_read_ip_ioctl
function_sysinfo_c_sysinfo_radio --> function_sysinfo_c_utilization_percent
function_sysinfo_c_sysinfo_radio --> function_sysinfo_c_count_antenna_chains
function_sysinfo_c_sysinfo_wifi_scan_cache --> function_sysinfo_c_frequency_to_channel
function_wlan_common_c_wlan_ensure_vap_ids --> function_crypto_c_crypto_random_hex
function_wlan_common_c_wlan_ensure_vap_ids --> function_wlan_common_c_wlan_valid_object_id
function_wlan_legacy_c_wlan_apply_system_cfg --> function_wlan_common_c_wlan_valid_object_id
function_wlan_legacy_c_wlan_apply_system_cfg --> function_wlan_common_c_wlan_feature_enabled
function_wlan_legacy_c_wlan_apply_system_cfg --> function_wlan_legacy_c_system_cfg_get
function_wlan_legacy_c_wlan_apply_system_cfg --> function_wlan_provision_c_wlan_apply_config
function_wlan_provision_c_apply_vap --> function_wlan_common_c_wlan_security_to_uci
function_wlan_provision_c_apply_vap --> function_wlan_common_c_wlan_valid_object_id
function_wlan_provision_c_apply_vap --> function_wlan_common_c_wlan_mobility_domain_for_ssid
function_wlan_provision_c_apply_vap --> function_wlan_common_c_wlan_radio_uses_ath9k
function_wlan_provision_c_apply_vap --> function_wlan_common_c_wlan_json_boolean_any
function_wlan_provision_c_apply_vap --> function_wlan_common_c_wlan_safe_section_name
function_wlan_provision_c_apply_vap --> function_wlan_uci_c_wlan_uci_set_required
function_wlan_provision_c_apply_vap --> function_wlan_uci_c_wlan_uci_add_list
function_wlan_provision_c_apply_vap --> function_wlan_uci_c_wlan_uci_ensure_section
function_wlan_provision_c_apply_vap --> function_wlan_uci_c_wlan_ensure_vlan_network
function_wlan_provision_c_force_wifi4_for_band --> function_wlan_common_c_wlan_json_boolean_any
function_wlan_provision_c_wlan_apply_config --> function_wlan_common_c_wlan_ensure_vap_ids
function_wlan_provision_c_wlan_apply_config --> function_wlan_common_c_wlan_json_boolean_any
function_wlan_provision_c_wlan_apply_config --> function_wlan_provision_c_apply_vap
function_wlan_provision_c_wlan_apply_config --> function_wlan_provision_c_force_wifi4_for_band
function_wlan_provision_c_wlan_apply_config --> function_wlan_radio_c_wlan_device_for_band
function_wlan_provision_c_wlan_apply_config --> function_wlan_radio_c_wlan_apply_radio
function_wlan_provision_c_wlan_apply_config --> function_wlan_uci_c_wlan_configure_band_steering
function_wlan_provision_c_wlan_apply_config --> function_wlan_uci_c_wlan_clear
function_wlan_radio_c_resolve_radio_map --> function_wlan_radio_c_band_bit
function_wlan_radio_c_resolve_radio_map --> function_wlan_radio_c_bit_count
function_wlan_radio_c_resolve_radio_map --> function_wlan_radio_c_detect_radio_bands
function_wlan_radio_c_wlan_device_for_band --> function_wlan_radio_c_resolve_radio_map
function_wlan_radio_c_wlan_band_for_device --> function_wlan_radio_c_resolve_radio_map
function_wlan_radio_c_select_htmode --> function_wlan_radio_c_radio_max_standard
function_wlan_radio_c_wlan_apply_radio --> function_wlan_radio_c_select_htmode
function_wlan_telemetry_c_wlan_get_vap_table --> function_wlan_common_c_wlan_security_to_unifi
function_wlan_telemetry_c_wlan_get_vap_table --> function_wlan_common_c_wlan_valid_object_id
function_wlan_telemetry_c_wlan_get_vap_table --> function_wlan_radio_c_wlan_band_for_device
function_wlan_telemetry_c_wlan_get_vap_table --> function_wlan_telemetry_c_find_runtime_vap
function_wlan_uci_c_wlan_uci_set_required --> function_wlan_uci_c_wlan_uci_set
function_wlan_uci_c_wlan_ensure_vlan_network --> function_wlan_uci_c_wlan_uci_add_list
function_wlan_uci_c_wlan_ensure_vlan_network --> function_wlan_uci_c_wlan_uci_ensure_section
function_wlan_uci_c_wlan_ensure_vlan_network --> function_wlan_uci_c_find_vlan_uplink
function_wlan_uci_c_wlan_configure_band_steering --> function_wlan_uci_c_wlan_uci_ensure_section
```
## Caller/callee index
| Source | Caller | Internal callees |
| --- | --- | --- |
| `src/announce.c:67` | `tlv_append()` | — |
| `src/announce.c:78` | `tlv_str()` | `tlv_append()` |
| `src/announce.c:85` | `put32be()` | — |
| `src/announce.c:94` | `parse_mac()` | — |
| `src/announce.c:102` | `parse_ip()` | — |
| `src/announce.c:110` | `announce_init()` | `tlv_append()`, `tlv_str()`, `parse_mac()`, `parse_ip()` |
| `src/announce.c:233` | `announce_send()` | `put32be()` |
| `src/announce.c:273` | `announce_close()` | — |
| `src/clients.c:45` | `mac_lower()` | — |
| `src/clients.c:55` | `clients_mac_to_ip()` | `mac_lower()` |
| `src/clients.c:86` | `clients_mac_to_hostname()` | `mac_lower()` |
| `src/clients.c:120` | `parse_rate_kbps()` | — |
| `src/clients.c:130` | `clients_read_wifi()` | `clients_mac_to_ip()`, `clients_mac_to_hostname()`, `parse_rate_kbps()` |
| `src/clients.c:247` | `clients_build_sta_table()` | `clients_read_wifi()` |
| `src/config.c:7` | `config_load()` | — |
| `src/crypto.c:13` | `crypto_hex2bin()` | — |
| `src/crypto.c:22` | `crypto_bin2hex()` | — |
| `src/crypto.c:30` | `crypto_random_hex()` | `crypto_bin2hex()` |
| `src/crypto.c:58` | `crypto_encrypt()` | `crypto_hex2bin()` |
| `src/crypto.c:93` | `crypto_decrypt()` | `crypto_hex2bin()` |
| `src/crypto.c:123` | `crypto_gcm_encrypt()` | `crypto_hex2bin()` |
| `src/crypto.c:144` | `crypto_gcm_decrypt()` | `crypto_hex2bin()` |
| `src/http.c:24` | `parse_url()` | — |
| `src/http.c:65` | `http_post()` | `parse_url()` |
| `src/inform/inform.c:26` | `inform_send()` | `http_post()`, `inform_packet_build()`, `inform_packet_parse()`, `inform_build_payload()`, `inform_log_controller_response()`, `inform_handle_response()`, `state_save()` |
| `src/inform/packet.c:26` | `put32be()` | — |
| `src/inform/packet.c:31` | `put16be()` | — |
| `src/inform/packet.c:35` | `get32be()` | — |
| `src/inform/packet.c:40` | `get16be()` | — |
| `src/inform/packet.c:45` | `inform_packet_build()` | `crypto_hex2bin()`, `crypto_random_hex()`, `crypto_encrypt()`, `crypto_gcm_encrypt()`, `put32be()`, `put16be()` |
| `src/inform/packet.c:103` | `inform_packet_parse()` | `crypto_bin2hex()`, `crypto_decrypt()`, `crypto_gcm_decrypt()`, `get32be()`, `get16be()` |
| `src/inform/payload.c:26` | `build_sys_stats()` | `sysinfo_mem()`, `sysinfo_cpu_percent()` |
| `src/inform/payload.c:69` | `build_system_stats()` | — |
| `src/inform/payload.c:91` | `build_if_table()` | `sysinfo_iface()` |
| `src/inform/payload.c:139` | `radio_runtime_iface()` | `wlan_device_for_band()`, `wlan_get_vap_table()` |
| `src/inform/payload.c:162` | `build_scan_table()` | `sysinfo_wifi_scan_cache()` |
| `src/inform/payload.c:191` | `build_athstats()` | — |
| `src/inform/payload.c:220` | `build_radio_table()` | `radio_runtime_iface()`, `build_scan_table()`, `build_athstats()`, `inform_debug_level()` |
| `src/inform/payload.c:296` | `build_radio_table_stats()` | `radio_runtime_iface()`, `sysinfo_radio()` |
| `src/inform/payload.c:373` | `build_port_table()` | `sysinfo_iface()` |
| `src/inform/payload.c:410` | `build_eth_table()` | — |
| `src/inform/payload.c:439` | `build_vap_table()` | `clients_build_sta_table()`, `sysinfo_iface()`, `sysinfo_radio()`, `wlan_device_for_band()`, `wlan_get_vap_table()` |
| `src/inform/payload.c:647` | `collect_sta_table()` | — |
| `src/inform/payload.c:668` | `inform_build_payload()` | `build_sys_stats()`, `build_system_stats()`, `build_if_table()`, `build_radio_table()`, `build_radio_table_stats()`, `build_port_table()`, `build_eth_table()`, `build_vap_table()`, `collect_sta_table()`, `lldp_read_neighbors()` |
| `src/inform/response.c:26` | `inform_valid_auth_key()` | — |
| `src/inform/response.c:38` | `inform_set_debug_level()` | — |
| `src/inform/response.c:43` | `inform_debug_level()` | — |
| `src/inform/response.c:48` | `inform_debug_system_cfg_key()` | — |
| `src/inform/response.c:57` | `inform_log_controller_response()` | `inform_debug_system_cfg_key()` |
| `src/inform/response.c:102` | `reboot_openwrt()` | — |
| `src/inform/response.c:120` | `inform_handle_response()` | `inform_valid_auth_key()`, `reboot_openwrt()`, `state_save()`, `wlan_apply_system_cfg()`, `wlan_apply_config()` |
| `src/lldp.c:75` | `tlv_write()` | — |
| `src/lldp.c:86` | `tlv_str()` | `tlv_write()` |
| `src/lldp.c:94` | `parse_mac()` | — |
| `src/lldp.c:104` | `lldp_send_frame()` | `tlv_write()`, `tlv_str()`, `parse_mac()` |
| `src/lldp.c:193` | `lldp_available()` | — |
| `src/lldp.c:210` | `lldp_read_neighbors()` | `lldp_available()` |
| `src/main.c:36` | `get_mac()` | — |
| `src/main.c:51` | `get_ip()` | — |
| `src/main.c:70` | `get_default_gateway()` | — |
| `src/main.c:116` | `main()` | `announce_init()`, `announce_send()`, `announce_close()`, `config_load()`, `inform_send()`, `inform_set_debug_level()`, `lldp_send_frame()`, `lldp_available()`, `get_mac()`, `get_ip()`, `get_default_gateway()`, `ufmodel_find()`, `state_load()`, `state_save()` |
| `src/models.c:179` | `ufmodel_find()` | — |
| `src/state.c:16` | `state_defaults()` | — |
| `src/state.c:27` | `state_load()` | `state_defaults()` |
| `src/state.c:102` | `state_save()` | — |
| `src/sysinfo.c:53` | `sysinfo_mem()` | — |
| `src/sysinfo.c:81` | `read_cpu()` | — |
| `src/sysinfo.c:92` | `sysinfo_cpu_percent()` | `read_cpu()` |
| `src/sysinfo.c:115` | `read_sysfs_str()` | — |
| `src/sysinfo.c:130` | `read_sysfs_int()` | `read_sysfs_str()` |
| `src/sysinfo.c:137` | `read_ip_ioctl()` | — |
| `src/sysinfo.c:151` | `sysinfo_iface()` | `read_sysfs_str()`, `read_sysfs_int()`, `read_ip_ioctl()` |
| `src/sysinfo.c:238` | `utilization_percent()` | — |
| `src/sysinfo.c:247` | `count_antenna_chains()` | — |
| `src/sysinfo.c:277` | `sysinfo_radio()` | `utilization_percent()`, `count_antenna_chains()` |
| `src/sysinfo.c:486` | `frequency_to_channel()` | — |
| `src/sysinfo.c:498` | `sysinfo_wifi_scan_cache()` | `frequency_to_channel()` |
| `src/wlan/common.c:20` | `wlan_security_to_uci()` | — |
| `src/wlan/common.c:34` | `wlan_security_to_unifi()` | — |
| `src/wlan/common.c:48` | `wlan_valid_object_id()` | — |
| `src/wlan/common.c:65` | `wlan_ensure_vap_ids()` | `crypto_random_hex()`, `wlan_valid_object_id()` |
| `src/wlan/common.c:157` | `wlan_mobility_domain_for_ssid()` | — |
| `src/wlan/common.c:169` | `wlan_radio_uses_ath9k()` | — |
| `src/wlan/common.c:187` | `wlan_json_boolean_any()` | — |
| `src/wlan/common.c:209` | `wlan_feature_enabled()` | — |
| `src/wlan/common.c:217` | `wlan_safe_section_name()` | — |
| `src/wlan/legacy.c:20` | `system_cfg_get()` | — |
| `src/wlan/legacy.c:44` | `wlan_apply_system_cfg()` | `wlan_valid_object_id()`, `wlan_feature_enabled()`, `system_cfg_get()`, `wlan_apply_config()` |
| `src/wlan/provision.c:20` | `apply_vap()` | `wlan_security_to_uci()`, `wlan_valid_object_id()`, `wlan_mobility_domain_for_ssid()`, `wlan_radio_uses_ath9k()`, `wlan_json_boolean_any()`, `wlan_safe_section_name()`, `wlan_uci_set_required()`, `wlan_uci_add_list()`, `wlan_uci_ensure_section()`, `wlan_ensure_vlan_network()` |
| `src/wlan/provision.c:291` | `force_wifi4_for_band()` | `wlan_json_boolean_any()` |
| `src/wlan/provision.c:335` | `wlan_apply_config()` | `wlan_ensure_vap_ids()`, `wlan_json_boolean_any()`, `apply_vap()`, `force_wifi4_for_band()`, `wlan_device_for_band()`, `wlan_apply_radio()`, `wlan_configure_band_steering()`, `wlan_clear()` |
| `src/wlan/radio.c:29` | `band_bit()` | — |
| `src/wlan/radio.c:38` | `bit_count()` | — |
| `src/wlan/radio.c:50` | `detect_radio_bands()` | — |
| `src/wlan/radio.c:85` | `resolve_radio_map()` | `band_bit()`, `bit_count()`, `detect_radio_bands()` |
| `src/wlan/radio.c:135` | `wlan_device_for_band()` | `resolve_radio_map()` |
| `src/wlan/radio.c:146` | `wlan_band_for_device()` | `resolve_radio_map()` |
| `src/wlan/radio.c:162` | `radio_max_standard()` | — |
| `src/wlan/radio.c:195` | `select_htmode()` | `radio_max_standard()` |
| `src/wlan/radio.c:241` | `wlan_apply_radio()` | `select_htmode()` |
| `src/wlan/telemetry.c:25` | `find_runtime_vap()` | — |
| `src/wlan/telemetry.c:58` | `wlan_get_vap_table()` | `wlan_security_to_unifi()`, `wlan_valid_object_id()`, `wlan_band_for_device()`, `find_runtime_vap()` |
| `src/wlan/uci.c:21` | `wlan_uci_set()` | — |
| `src/wlan/uci.c:36` | `wlan_uci_set_required()` | `wlan_uci_set()` |
| `src/wlan/uci.c:56` | `wlan_uci_add_list()` | — |
| `src/wlan/uci.c:72` | `wlan_uci_ensure_section()` | — |
| `src/wlan/uci.c:100` | `find_vlan_uplink()` | — |
| `src/wlan/uci.c:143` | `wlan_ensure_vlan_network()` | `wlan_uci_add_list()`, `wlan_uci_ensure_section()`, `find_vlan_uplink()` |
| `src/wlan/uci.c:208` | `wlan_configure_band_steering()` | `wlan_uci_ensure_section()` |
| `src/wlan/uci.c:268` | `wlan_clear()` | — |
+166
View File
@@ -0,0 +1,166 @@
# 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 |
| --- | --- |
| `src/announce.c` | UniFi layer-2 UDP discovery. |
| `src/clients.c` | Wireless and bridge client telemetry. |
| `src/config.c` | Static daemon configuration parser and defaults. |
| `src/crypto.c` | AES-CBC, AES-GCM, and encoding helpers. |
| `src/http.c` | Minimal HTTP/1.0 transport. |
| `src/inform/inform.c` | One inform request/response exchange. |
| `src/inform/packet.c` | TNBU binary envelope codec. |
| `src/inform/payload.c` | Inform JSON telemetry assembly. |
| `src/inform/response.c` | Controller command and provisioning dispatch. |
| `src/lldp.c` | LLDP frame transmission and neighbor collection. |
| `src/main.c` | Daemon lifecycle and one-second scheduler. |
| `src/models.c` | Emulated hardware model registry. |
| `src/state.c` | Persistent adoption and controller state. |
| `src/sysinfo.c` | Kernel and nl80211 device telemetry. |
| `src/wlan/common.c` | Shared Wi-Fi translations and validators. |
| `src/wlan/legacy.c` | Legacy system_cfg translation. |
| `src/wlan/provision.c` | Modern setstate Wi-Fi provisioning. |
| `src/wlan/radio.c` | Runtime radio mapping and radio settings. |
| `src/wlan/telemetry.c` | Managed VAP telemetry from UCI and nl80211. |
| `src/wlan/uci.c` | Reusable UCI, VLAN, steering, and cleanup operations. |
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
Generate pages after changing C code or architecture:
```sh
./scripts/docs/generate-wiki.py
```
Run the linter-style drift and structure check in CI or before committing:
```sh
./scripts/docs/generate-wiki.py --check
```
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
900 lines.
## Publishing to the Gitea Wiki
Gitea stores a repository Wiki in a separate Git repository whose URL normally
ends in `.wiki.git`. The generated `wiki/` directory is ready for that remote.
Run `scripts/docs/publish-wiki.sh` from a trusted machine with suitable
credentials. It derives the Wiki remote from `origin`; an explicit URL may be
passed when needed. The publisher regenerates and checks the pages,
updates only the generated Markdown files, commits changed pages, and pushes
them to the Wiki repository.
+13
View File
@@ -0,0 +1,13 @@
# openUF Developer Wiki
This Wiki is generated from the current source tree and maintained in the main
repository so architecture documentation changes can be reviewed with code.
- [[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` to verify these pages are current.
+76
View File
@@ -0,0 +1,76 @@
# 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]
```
+4
View File
@@ -0,0 +1,4 @@
- [[Home]]
- [[Developer Guide|Developer-Guide]]
- [[Runtime Flow|Runtime-Flow]]
- [[Function Call Graph|Call-Graph]]