Reviewed-on: #36 Co-authored-by: Koda YeenBean <n122330@gmail.com>
This commit was merged in pull request #36.
This commit is contained in:
@@ -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^
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,27 +26,28 @@ endef
|
||||
|
||||
define Build/Prepare
|
||||
mkdir -p $(PKG_BUILD_DIR)
|
||||
$(CP) ./src/* $(PKG_BUILD_DIR)/
|
||||
$(CP) ./src/. $(PKG_BUILD_DIR)/
|
||||
endef
|
||||
|
||||
TARGET_CFLAGS += -I$(STAGING_DIR)/usr/include -DENABLE_LOGGING=1
|
||||
OPENUF_SRCS := \
|
||||
main.c config.c state.c crypto.c http.c announce.c \
|
||||
inform/inform.c inform/payload.c inform/packet.c inform/response.c \
|
||||
wlan/common.c wlan/uci.c wlan/radio.c wlan/provision.c \
|
||||
wlan/legacy.c wlan/telemetry.c \
|
||||
sysinfo.c clients.c lldp.c models.c
|
||||
|
||||
TARGET_CFLAGS += \
|
||||
-I$(STAGING_DIR)/usr/include \
|
||||
-I$(PKG_BUILD_DIR) \
|
||||
-I$(PKG_BUILD_DIR)/inform \
|
||||
-I$(PKG_BUILD_DIR)/wlan \
|
||||
-DENABLE_LOGGING=1
|
||||
TARGET_LDFLAGS += -lmbedtls -lmbedcrypto -luci -ljson-c
|
||||
|
||||
define Build/Compile
|
||||
$(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_LDFLAGS) \
|
||||
-o $(PKG_BUILD_DIR)/openuf \
|
||||
$(PKG_BUILD_DIR)/main.c \
|
||||
$(PKG_BUILD_DIR)/config.c \
|
||||
$(PKG_BUILD_DIR)/state.c \
|
||||
$(PKG_BUILD_DIR)/crypto.c \
|
||||
$(PKG_BUILD_DIR)/http.c \
|
||||
$(PKG_BUILD_DIR)/announce.c \
|
||||
$(PKG_BUILD_DIR)/inform.c \
|
||||
$(PKG_BUILD_DIR)/wlan.c \
|
||||
$(PKG_BUILD_DIR)/sysinfo.c \
|
||||
$(PKG_BUILD_DIR)/clients.c \
|
||||
$(PKG_BUILD_DIR)/lldp.c \
|
||||
$(PKG_BUILD_DIR)/models.c
|
||||
$(addprefix $(PKG_BUILD_DIR)/,$(OPENUF_SRCS))
|
||||
endef
|
||||
|
||||
define Package/openuf/install
|
||||
|
||||
+11
-3
@@ -10,7 +10,7 @@
|
||||
# make -f Makefile.standalone install
|
||||
|
||||
CC = gcc
|
||||
CFLAGS = -Wall -Wextra -O2 -I/usr/include -DENABLE_LOGGING=1
|
||||
CFLAGS = -Wall -Wextra -O2 -I/usr/include -Isrc -Isrc/inform -Isrc/wlan -DENABLE_LOGGING=1
|
||||
LDFLAGS = -lmbedtls -lmbedcrypto -luci -ljson-c
|
||||
|
||||
SRCS = src/main.c \
|
||||
@@ -19,8 +19,16 @@ SRCS = src/main.c \
|
||||
src/crypto.c \
|
||||
src/http.c \
|
||||
src/announce.c \
|
||||
src/inform.c \
|
||||
src/wlan.c \
|
||||
src/inform/inform.c \
|
||||
src/inform/payload.c \
|
||||
src/inform/packet.c \
|
||||
src/inform/response.c \
|
||||
src/wlan/common.c \
|
||||
src/wlan/uci.c \
|
||||
src/wlan/radio.c \
|
||||
src/wlan/provision.c \
|
||||
src/wlan/legacy.c \
|
||||
src/wlan/telemetry.c \
|
||||
src/sysinfo.c \
|
||||
src/clients.c \
|
||||
src/lldp.c \
|
||||
|
||||
@@ -7,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
|
||||
@@ -106,7 +128,7 @@ On the access point, run the following to install the package:
|
||||
apk del wpad-basic-mbedtls && apk add wpad-mbedtls && /etc/init.d/network restart
|
||||
|
||||
# Remove old installation
|
||||
apk -r del openuf
|
||||
apk -r del openuf
|
||||
|
||||
# Install new version
|
||||
apk add openuf-0.4.0-r3.apk --allow-untrusted
|
||||
@@ -146,7 +168,7 @@ version in `state.json` and reports it in subsequent inform packets.
|
||||
|
||||
## Glossary
|
||||
|
||||
### TNBU
|
||||
### TNBU
|
||||
|
||||
TNBU is the magic string/identifier at the start of the binary packet format used in this custom Inform protocol implementation.
|
||||
|
||||
|
||||
Executable
+581
@@ -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())
|
||||
Executable
+55
@@ -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
@@ -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. */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifndef OPENUF_INFORM_INTERNAL_H
|
||||
#define OPENUF_INFORM_INTERNAL_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <json-c/json.h>
|
||||
|
||||
#include "state.h"
|
||||
#include "ufmodel.h"
|
||||
|
||||
/* Internal contracts shared by the focused inform implementation units. */
|
||||
char *inform_build_payload(const openuf_state_t *state,
|
||||
const uf_model_t *model,
|
||||
long uptime);
|
||||
|
||||
unsigned char *inform_packet_build(const char *mac_hex,
|
||||
const char *key_hex,
|
||||
const char *payload,
|
||||
int use_aes_gcm,
|
||||
size_t *packet_length);
|
||||
|
||||
char *inform_packet_parse(const unsigned char *data,
|
||||
size_t data_length,
|
||||
const char *key_hex);
|
||||
|
||||
void inform_handle_response(openuf_state_t *state,
|
||||
const uf_model_t *model,
|
||||
struct json_object *response,
|
||||
char *action_out);
|
||||
|
||||
void inform_log_controller_response(struct json_object *response,
|
||||
const char *raw_json);
|
||||
int inform_debug_level(void);
|
||||
|
||||
#endif /* OPENUF_INFORM_INTERNAL_H */
|
||||
@@ -0,0 +1,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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
@@ -216,8 +84,8 @@ static struct json_object *build_system_stats(int cpu_percent,
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
if_table — network interface statistics
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
All Ethernet ports on the model are reported.
|
||||
/proc/net/dev is read for counters, and /sys/class/net/<iface>/
|
||||
All Ethernet ports on the model are reported.
|
||||
/proc/net/dev is read for counters, and /sys/class/net/<iface>/
|
||||
for speed, duplex, and link status.
|
||||
*/
|
||||
static struct json_object *build_if_table(const uf_model_t *m,
|
||||
@@ -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)
|
||||
{
|
||||
@@ -917,527 +785,18 @@ static char *build_payload(const openuf_state_t *st,
|
||||
json_object_object_add(root, "num_sta", json_object_new_int(station_count));
|
||||
|
||||
const char *s = json_object_to_json_string(root);
|
||||
|
||||
|
||||
/* Log shows what authkey is actually in the payload */
|
||||
LOG("Payload state=%d, default=%s, adopted=%d, cfgversion=%s, config_applied=%d, x_authkey=%.8s...",
|
||||
st->adopted ? 4 : 1,
|
||||
st->adopted ? 4 : 1,
|
||||
!st->adopted ? "true" : "false",
|
||||
st->adopted,
|
||||
st->cfgversion,
|
||||
st->config_applied,
|
||||
st->authkey[0] ? st->authkey : "DEFAULT");
|
||||
|
||||
|
||||
char *copy = strdup(s);
|
||||
json_object_put(root);
|
||||
return copy;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
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;
|
||||
}
|
||||
@@ -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
@@ -107,7 +107,7 @@ int lldp_send_frame(const char *ifname,
|
||||
const char *model_desc,
|
||||
int ttl)
|
||||
{
|
||||
/* Socket raw — requiere root */
|
||||
/* Raw packet sockets require root privileges or CAP_NET_RAW. */
|
||||
int fd = socket(AF_PACKET, SOCK_RAW, htons(LLDP_ETHERTYPE));
|
||||
if (fd < 0) return -1; /* EPERM without root → silent */
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@
|
||||
* TLV type=2 Port ID subtype=5(ifname), value="eth0"
|
||||
* TLV type=3 TTL value=uint16_BE
|
||||
* TLV type=5 System Name value=hostname
|
||||
* TLV type=6 System Desc value="modelo versión"
|
||||
* TLV type=6 System Description value="model version"
|
||||
* TLV type=7 Capabilities cap=0x0040(WLAN-AP), en=0x0040
|
||||
* TLV type=0 End of LLDPDU len=0
|
||||
*
|
||||
|
||||
+2
-2
@@ -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)
|
||||
*/
|
||||
|
||||
@@ -151,7 +151,7 @@ int main(int argc, char *argv[])
|
||||
strncpy(state.hostname, model->display_name, sizeof(state.hostname)-1);
|
||||
|
||||
/* Log initial state */
|
||||
LOG("Initial device state: adopted=%d, authkey=%.8s...", state.adopted,
|
||||
LOG("Initial device state: adopted=%d, authkey=%.8s...", state.adopted,
|
||||
state.authkey[0] ? state.authkey : "DEFAULT");
|
||||
|
||||
/*
|
||||
|
||||
+2
-2
@@ -17,7 +17,7 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/* ── Memoria ─────────────────────────────────────────────────────── */
|
||||
/* ── Memory ─────────────────────────────────────────────────────── */
|
||||
typedef struct {
|
||||
long total_kb;
|
||||
long free_kb;
|
||||
@@ -33,7 +33,7 @@ int sysinfo_mem(mem_stats_t *out);
|
||||
* With a 10s interval this gives a good average of usage. */
|
||||
int sysinfo_cpu_percent(void);
|
||||
|
||||
/* ── Interfaz de red ─────────────────────────────────────────────── */
|
||||
/* ── Network interface ─────────────────────────────────────────────── */
|
||||
typedef struct {
|
||||
char name[32];
|
||||
char mac[32];
|
||||
|
||||
-1906
File diff suppressed because it is too large
Load Diff
@@ -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';
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 */
|
||||
@@ -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()` | — |
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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]
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
- [[Home]]
|
||||
- [[Developer Guide|Developer-Guide]]
|
||||
- [[Runtime Flow|Runtime-Flow]]
|
||||
- [[Function Call Graph|Call-Graph]]
|
||||
Reference in New Issue
Block a user