Release Notes v0.10.0
Released: 2026-08-07
This release brings a full VPN management surface, Site Manager cloud fleet commands, switch port config-as-code, and TUI auto-reconnect. Significant work went into fixing real-world controller compatibility: DNS record type parsing, firewall payload decoding, NAT visibility under session-only auth, and a config resolution overhaul that finally honors [defaults] fields and respects the TLS/timeout precedence chain.
✨ Highlights
Full VPN Management Surface
Site-to-site CRUD, remote-access CRUD (with suggest-port and download-config), client profiles, peer management, connections list/get/restart, magic-site-to-site, and vpn settings with flag groups for teleport, OpenVPN, and peer-to-peer. Most write paths accept --from-file for declarative config. Detail views (vpn servers get, vpn tunnels get) expose subnet, port, protocol, peer address, and IKE version. vpn status and vpn health provide live IPsec SA state and subsystem health.
Site Manager Cloud Fleet Commands
New cloud command group (hosts, sites, devices, isp, sdwan) backed by a unifly-api fleet client with nextToken pagination and rate-limit handling. config cloud-setup walks through API key validation, console selection, site discovery, and profile writing. cloud switch <site> retargets an active cloud profile to a different controller site. Cloud host auto-resolution kicks in for Integration commands when the profile omits host_id but the API key maps to a single console.
Switch Port Management as Code
devices ports, devices ports-export, and devices port-set cover mode, native VLAN, tagged VLANs, PoE, and speed per port. port-set --from-file accepts JSONC with splice semantics (omitted ports keep their current override; per-port "reset": true clears one). ports-export emits sparse JSONC by default; --with-clients injects // last-seen <ISO8601>: <mac> comments for git-diff drift detection.
TUI Auto-Reconnect
A failed controller connection no longer leaves the dashboard dead. The data bridge retries with exponential backoff (2s doubling to a 30s cap), the status bar shows the attempt number while reconnecting, and Ctrl+r triggers an immediate manual retry. Connect failures now display the reason in the status bar instead of showing a silent "disconnected".
Create Commands Print to stdout
Every create command now emits the created entity on stdout in the requested --output format, so pipelines can capture IDs directly: unifly networks create ... -o json | jq -r .id. Covers networks, wifi, firewall policies/zones/groups, NAT, ACL, DNS, traffic-lists, hotspot vouchers, and VPN records. The human confirmation stays on stderr. If the response cannot be rendered, a warning is emitted on stderr instead of failing the command.
Config Resolution Overhaul
The [defaults] fields insecure and timeout are now honored during profile resolution. Precedence follows: CLI flags > env vars > profile > [defaults] > builtin. A profile's ca_cert beats an inherited defaults.insecure = true, and --insecure=false explicitly re-enables verification. Cloud profiles always use strict TLS. config init now asks about self-signed certificates with smart defaults based on URL heuristics (yes for IPs, .local/.lan hosts, single-label hostnames; no for public FQDNs).
🔧 CLI and API Surface
settingstop-level command:list / get <KEY> / set <KEY> / exportfor site-level Session API settingsfirewall groupsCRUD for port groups, address groups, and IPv6 address groups via the Session API, with--typefilter on list- Port/address group references in firewall policies via
--dst-port-group,--src-port-group,--dst-address-group,--src-address-groupflags and--from-fileshorthand fields --after-systemonfirewall policies createto place a policy after system-defined rules in one step--from-fileshorthand fields for firewall policies:dst_ip,dst_port,src_ip,src_port,dst_network,src_networkresolve into canonical filter structureswifi neighborslists rogue/neighboring APs;wifi channelsshows per-radio regulatory availabilityclients roams <mac>surfaces a client's connection timeline;clients wifi <ip>shows Wi-Fi experience metricsClient::switch_portpopulated from session-sidesw_port, visible inclients getdetail view
🐛 Fixes
- NAT policies visible under session-only auth. The session-only refresh snapshot omitted NAT entirely; rules created on the controller were invisible to
nat policies listwhile update and delete still worked. The refresh now fetches NAT vialist_nat_rules()with error recovery. - DNS policies parse modern
*_RECORDtype tokens (#27). Controllers sendA_RECORD-style identifiers; the old short-name tables mislabeled every non-forward record asForwardDomain. One canonical mapping now covers read and write, unknown record types are skipped with a warning, and the CLI shows short names (A,CNAME,Forward). - Firewall policy lists survive real controller payloads (#26). Port ranges arrive as
start/stopper Ubiquiti's spec; the model demanded inventedstartPort/endPortnames. Integration list pages now decode per-item, so a single unparseable record cannot empty an entire collection, and pagination advances by wire count so dropped items cannot truncate a scan. - TUI honors CLI flag and env overrides (#25). The profile path dropped
--insecure,--site,--api-key,--totp,--no-cache, and--controller;UNIFI_INSECURE=true unifly tui -kfailed TLS while the CLI worked. - Profile timeout takes effect. The
--timeoutflag's baked-in clap default shadowed every profile timeout. Resolution now follows flag/env, then profile, then the 30-second builtin, including cloud transports. - Port range items in firewall payloads now serialize as
PORT_NUMBER_RANGEinstead ofPORT_RANGE(still accepted on read for backward compatibility) - TUI redraws correctly after direct keyboard input
♻️ Nomenclature: "Legacy" → "Session"
The UniFi /api/* and /v2/api/* HTTP surface is not deprecated; Ubiquiti ships new functionality there regularly, and with API-key discovery it is no longer tied to cookie session auth. It is now consistently called the Session API in code, types, docs, and user-facing messages.
- Rust:
legacy/module renamed tosession/,LegacyClienttoSessionClient, error types and access-check methods renamed accordingly - TUI:
AuthMode::Legacyrenamed toAuthMode::Session; onboarding wizard shows "Username / Password (Session API)" - CLI errors: differentiate "API key rejected" from "session expired" based on auth kind
- Config:
auth_mode = "session"is the canonical value;"legacy"is accepted as a backward-compatible alias and normalized silently EntityId::Legacy(String)is kept, as it names an ID format (MongoDB ObjectId), not the API surface
🖥️ TUI Enhancements
- HyperChart widget with tachyonfx-driven effects and Octant Canvas bandwidth chart on the Stats screen
- Rank column on ranked-bar lists
- 160ms
fade_from_fgtransition between screens - Bridge connect attempts are now cancellation-safe
✅ Testing
- New e2e tests cover firewall group CRUD round-trip, NAT policy lifecycle (validates the session-only fix), and switch port read/export path shape
- Nine new unit tests in
resolve.rscover defaults application, profile override, flag override,--insecure=falseforcing verification, CA cert precedence, and timeout precedence at all three layers - DNS conversion tests validate all 7 record types with both long and short wire names, plus graceful skip of unknown types
⚠️ Breaking Changes
From<DnsPolicyResponse>replaced withTryFrom(affectsunifly-apilibrary consumers). The conversion now returnsUnrecognizedDnsRecordTypeinstead of silently mislabeling unknown record types asForwardDomain.- Credential precedence reversed. When both an explicit
api_key(orpassword) in TOML config and a keyring entry exist, the explicit config value now wins. The older behavior preferred the keyring. This makes headless and shared-machine workflows predictable (PR #22).
📝 Documentation
- README updated for the 0.10 surface: WiFi screen, new flags, defaults
- New guide pages:
docs/content/guide/vpn.md,cloud.md,switch-ports.md,settings.md - CLI reference refreshed for the 28-command surface
- AGENTS.md updated with architecture facts and gotchas for 0.10
- CONTRIBUTING.md documents the dockerized e2e suite
- SKILL.md and skill references updated for session-only surface and create-stdout capture
👷 Build
- Rust toolchain pinned in
rust-toolchain.toml - Dependency graph refreshed (
Cargo.lock)