Skip to content

Release v0.10.0

Latest

Choose a tag to compare

@github-actions github-actions released this 07 Aug 07:40

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

  • settings top-level command: list / get <KEY> / set <KEY> / export for site-level Session API settings
  • firewall groups CRUD for port groups, address groups, and IPv6 address groups via the Session API, with --type filter on list
  • Port/address group references in firewall policies via --dst-port-group, --src-port-group, --dst-address-group, --src-address-group flags and --from-file shorthand fields
  • --after-system on firewall policies create to place a policy after system-defined rules in one step
  • --from-file shorthand fields for firewall policies: dst_ip, dst_port, src_ip, src_port, dst_network, src_network resolve into canonical filter structures
  • wifi neighbors lists rogue/neighboring APs; wifi channels shows per-radio regulatory availability
  • clients roams <mac> surfaces a client's connection timeline; clients wifi <ip> shows Wi-Fi experience metrics
  • Client::switch_port populated from session-side sw_port, visible in clients get detail 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 list while update and delete still worked. The refresh now fetches NAT via list_nat_rules() with error recovery.
  • DNS policies parse modern *_RECORD type tokens (#27). Controllers send A_RECORD-style identifiers; the old short-name tables mislabeled every non-forward record as ForwardDomain. 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/stop per Ubiquiti's spec; the model demanded invented startPort/endPort names. 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 -k failed TLS while the CLI worked.
  • Profile timeout takes effect. The --timeout flag'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_RANGE instead of PORT_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 to session/, LegacyClient to SessionClient, error types and access-check methods renamed accordingly
  • TUI: AuthMode::Legacy renamed to AuthMode::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_fg transition 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.rs cover defaults application, profile override, flag override, --insecure=false forcing 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 with TryFrom (affects unifly-api library consumers). The conversion now returns UnrecognizedDnsRecordType instead of silently mislabeling unknown record types as ForwardDomain.
  • Credential precedence reversed. When both an explicit api_key (or password) 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)