go-netops/docs/architecture.md
Claude b27034b710
docs: add production documentation and README
Co-Authored-By: Charon <charon@lethean.io>
2026-02-20 15:22:57 +00:00

7.7 KiB

Architecture

Module: forge.lthn.ai/core/go-netops

Overview

go-netops is a thin Go wrapper around the unpoller/unifi SDK that provides a simplified interface for querying UniFi network controllers. It handles authentication, TLS configuration, multi-site filtering, and config resolution from file, environment, and CLI flags.

Package Structure

go-netops/
├── unifi/
│   ├── client.go          # Client struct and constructor (New)
│   ├── clients.go         # Connected client queries with filtering
│   ├── config.go          # Config resolution + persistence (NewFromConfig, ResolveConfig, SaveConfig)
│   ├── devices.go         # Infrastructure device queries (UAP, USW, USG, UDM, UXG)
│   ├── networks.go        # Network configuration queries (VLANs, DHCP, firewall zones)
│   ├── routes.go          # Gateway routing table queries
│   ├── sites.go           # Site listing
│   ├── client_test.go     # Client creation tests
│   └── config_test.go     # Config resolution and persistence tests
├── go.mod
└── go.sum

All exported types live in the unifi package under the unifi/ subdirectory.

Key Types

Client

The central type. Wraps the unpoller SDK client with config-based authentication.

type Client struct {
    api *uf.Unifi
    url string
}

func New(url, user, pass, apikey string, insecure bool) (*Client, error)
func NewFromConfig(flagURL, flagUser, flagPass, flagAPIKey string, flagInsecure *bool) (*Client, error)
func (c *Client) API() *uf.Unifi
func (c *Client) URL() string

Two constructors are provided:

  • New — direct credentials, useful for tests and scripts.
  • NewFromConfig — three-tier config resolution (file, env, flags), used by CLI commands.

ClientFilter

Controls which connected clients are returned from a query.

type ClientFilter struct {
    Site     string // Filter by site name (empty = all sites)
    Wired    bool   // Show only wired clients
    Wireless bool   // Show only wireless clients
}

func (c *Client) GetClients(filter ClientFilter) ([]*uf.Client, error)

DeviceInfo

Flat representation of any UniFi infrastructure device, abstracting over the SDK's separate UAP/USW/USG/UDM/UXG containers.

type DeviceInfo struct {
    Name    string
    IP      string
    Mac     string
    Model   string
    Version string
    Type    string // uap, usw, usg, udm, uxg
    Status  int    // 1 = online
}

func (c *Client) GetDevices(siteName string) (*uf.Devices, error)
func (c *Client) GetDeviceList(siteName, deviceType string) ([]DeviceInfo, error)
  • GetDevices returns the raw SDK container (for callers that need full detail).
  • GetDeviceList returns a flat, uniform slice with optional type filtering.

NetworkConf

Network configuration entry covering VLANs, DHCP, DNS, firewall zones, and WAN settings.

type NetworkConf struct {
    ID                      string `json:"_id"`
    Name                    string `json:"name"`
    Purpose                 string `json:"purpose"`      // wan, corporate, remote-user-vpn
    IPSubnet                string `json:"ip_subnet"`    // CIDR
    VLAN                    int    `json:"vlan"`
    VLANEnabled             bool   `json:"vlan_enabled"`
    Enabled                 bool   `json:"enabled"`
    NetworkGroup            string `json:"networkgroup"` // LAN, WAN, WAN2
    NetworkIsolationEnabled bool   `json:"network_isolation_enabled"`
    InternetAccessEnabled   bool   `json:"internet_access_enabled"`
    IsNAT                   bool   `json:"is_nat"`
    DHCPEnabled             bool   `json:"dhcpd_enabled"`
    DHCPStart               string `json:"dhcpd_start"`
    DHCPStop                string `json:"dhcpd_stop"`
    DHCPDNS1                string `json:"dhcpd_dns_1"`
    DHCPDNS2                string `json:"dhcpd_dns_2"`
    DHCPDNSEnabled          bool   `json:"dhcpd_dns_enabled"`
    MDNSEnabled             bool   `json:"mdns_enabled"`
    FirewallZoneID          string `json:"firewall_zone_id"`
    GatewayType             string `json:"gateway_type"`
    VPNType                 string `json:"vpn_type"`
    WANType                 string `json:"wan_type"` // pppoe, dhcp, static
    WANNetworkGroup         string `json:"wan_networkgroup"`
}

func (c *Client) GetNetworks(siteName string) ([]NetworkConf, error)

Uses the raw controller REST API (/api/s/{site}/rest/networkconf) since the unpoller SDK does not wrap this endpoint.

Route

Gateway routing table entry with prefix, next-hop, interface, type, and metrics.

type Route struct {
    Network   string `json:"pfx"`      // CIDR prefix
    NextHop   string `json:"nh"`       // Next-hop address or interface
    Interface string `json:"intf"`     // Interface name (e.g. "br0", "eth4")
    Type      string `json:"type"`     // S=static, C=connected, K=kernel, B=bgp, O=ospf
    Distance  int    `json:"distance"` // Administrative distance
    Metric    int    `json:"metric"`
    Uptime    int    `json:"uptime"`   // Seconds
    Selected  bool   `json:"fib"`      // In forwarding table
}

func (c *Client) GetRoutes(siteName string) ([]Route, error)
func RouteTypeName(code string) string

Also uses the raw controller API (/api/s/{site}/stat/routing).

Design Decisions

Thin Wrapper

The package deliberately stays thin. It delegates transport, session management, and cookie handling to the unpoller SDK, adding only:

  • Config resolution — three-tier priority (config file, env vars, CLI flags).
  • TLS flexibilityinsecure flag for self-signed certificates on home lab controllers.
  • Flat typesDeviceInfo and NetworkConf flatten the SDK's nested/typed structures into uniform slices for easier consumption by CLI commands and templates.
  • Raw API accessGetNetworks and GetRoutes call controller endpoints the SDK does not wrap, parsing the JSON directly.

Config Resolution Order

All credential resolution follows the same three-tier pattern used across the core framework:

  1. Config file~/.core/config.yaml (keys: unifi.url, unifi.user, unifi.pass, unifi.apikey, unifi.insecure)
  2. Environment variablesUNIFI_URL, UNIFI_USER, UNIFI_PASS, UNIFI_APIKEY, UNIFI_INSECURE
  3. CLI flags — highest priority, override everything

Site Filtering

Most query methods accept a siteName parameter. An empty string queries all sites. The getSitesForFilter helper resolves site names and returns an error for unknown sites, preventing silent data omission.

TLS Configuration

The client enforces TLS 1.2 as the minimum version regardless of the insecure flag. The insecure flag only controls certificate verification, not protocol version.

API Patterns

Constructor Pattern

// Direct credentials
client, err := unifi.New("https://10.69.1.1", "admin", "pass", "", true)

// Config-resolved (CLI usage)
client, err := unifi.NewFromConfig(flagURL, flagUser, flagPass, flagAPIKey, &flagInsecure)

Query Pattern

All query methods follow the same shape: accept optional filters, return typed slices, wrap errors with log.E.

clients, err := client.GetClients(unifi.ClientFilter{Site: "default", Wireless: true})
devices, err := client.GetDeviceList("default", "uap")
networks, err := client.GetNetworks("default")
routes, err := client.GetRoutes("default")
sites, err := client.GetSites()

Config Persistence

// Save credentials
err := unifi.SaveConfig("https://10.69.1.1", "admin", "pass", "", nil)

// Resolve credentials (without creating a client)
url, user, pass, apikey, insecure, err := unifi.ResolveConfig("", "", "", "", nil)