emu

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 24, 2026 License: MIT Imports: 16 Imported by: 0

README

unifi-emu

Fake UniFi devices that speak the inform protocol and get adopted by a real controller. A device simulator/emulator for integration testing — give a UniFi controller a fleet of deterministic, controllable APs / switches / gateways without any hardware.

emu = emulator (and a flightless bird that struts around pretending it belongs).

Status

🐓 Fully fledged. A live-proven fleet — 1 gateway, 2 switches, 2 APs — adopts all the way to CONNECTED against a real controller (ghcr.io/jamesbraid/unifi-network:sim), including a controller-requested firmware "upgrade" survived with an emulated reboot. Shipped:

  • Library (package emu) — fleet API: New/Add/Start/State/WaitState/Stop.
  • CLI (cmd/unifi-emu) — single-device flags, a full -devices file / SIM_DEVICES env (YAML/JSON), or a terse -models / SIM_MODELS list (U7PRO,USM8P:2,UGW3) that auto-derives MAC/IP.
  • Container image — docker build -t unifi-emu:dev . (static, scratch, ~9MB). Bakes the 5-device fleet as its default, so a bare docker run boots it; -e SIM_MODELS=…, -e SIM_DEVICES=…, or the flags override it. The live suite builds this image from the checkout.
  • Adopt helpers — classic Network App (ClassicClient) and UniFi OS ucore/CSRF (UOSClient), live-proven against the published seeded UOS image through its controller-requested AP firmware upgrade.
  • Consumer integrations — AdoptDevice + StartDeviceSim in go-unifi's controllertest (jamesbraid/go-unifi#16) and a compose sidecar in terraform-provider-unifi (jamesbraid/terraform-provider-unifi#11).

Not yet: the module/image aren't published anywhere (both PRs note it).

Quick start
go test ./... # unit tests, no container runtime needed
go test -tags integration -run TestClassicUGWLive -v -count=1 .
go test -tags integration -run TestClassicFleetLive -v -count=1 .
go test -tags integration -run TestUOSAPUpgradeLive -v -count=1 .
docker build -t unifi-emu:dev . && docker run --rm unifi-emu:dev -h

The image boots its baked default fleet, and any explicit source overrides it:

docker run --rm unifi-emu:dev -inform http://CTRL:8080/inform                 # baked 5-device fleet
docker run --rm -e SIM_MODELS=U7PRO,USM8P:2,UGW3 unifi-emu:dev -inform …       # pick models (MAC/IP auto)
docker run --rm -e SIM_DEVICES="$(cat my-fleet.yaml)" unifi-emu:dev -inform …  # full control

SIM_MODELS counts models (USM8P:2 = two of them) and derives each device's MAC/IP from SIM_MAC_BASE/SIM_IP_BASE (defaults 00:27:22:e0:00:00 / 192.168.1.100); a fleet may hold at most one gateway.

The live tests use testcontainers-go. Each test creates an isolated network, a fresh controller, and an emulator container built from the checkout. Controller APIs use random host ports. Inform traffic stays on the container network. Logs and device documents remain under tmp/itest/<test-name>/.

Set UNIFI_EMU_ITEST_EMULATOR_IMAGE to test a prebuilt emulator instead. UNIFI_EMU_ITEST_CLASSIC_IMAGE and UNIFI_EMU_ITEST_UOS_IMAGE select controller images. Defaults are ghcr.io/jamesbraid/unifi-network:sim and ghcr.io/jamesbraid/unifi-os-server:seeded.

The newer UOS path uses a fresh seeded controller and proves the negotiated CBC-to-AES-GCM transition and AP firmware upgrade. Its controller healthcheck stays enabled. The harness also waits for seeded-owner and API readiness.

Model registry

The registry covers the full current UniFi AP / switch / gateway lineup — 182 models at controller 10.4.57. model_profiles.json is the checked-in reduced catalog; it is embedded (go:embed) and parsed at startup, so there is no generated Go to commit and go build/go test need no extra step.

cmd/modelgen builds the catalog from a controller's hardware database plus a couple of Ubiquiti sources. The bundle isn't in git, so refreshing needs a controller — a deliberate step, not part of the build. Harvest from a controller the UI's swai.*.js bundle (every model's ports and radios) and .../dl/firmware/bundles.json (display names), and fetch Ubiquiti's firmware-latest and device fingerprint JSON. Then:

# real AP ethernet from Tech Specs, written into model_overrides.json
go run ./cmd/modelgen -fetch-eth -bundle swai.js -fingerprint fingerprint.json

# generate the catalog
go run ./cmd/modelgen -bundle swai.js -bundles-json bundles.json \
  -firmware-json firmware-latest.json -overrides model_overrides.json \
  -controller-version 10.4.57
go test ./...

Firmware versions come from Ubiquiti's fw-update API, joined on the model code. AP Ethernet — which the hardware DB omits — comes from Tech Specs, matched to the fingerprint DB by model code and then by hardware sysid (so the hex-coded 10GbE flagships resolve). Facts the bundle can't express live in model_overrides.json. A model the bundle can't render — an unknown radio band, say — is skipped and logged, never emitted wrong; an AP with no resolved ethernet keeps a 1×GbE default, also logged.

More

  • docs/DESIGN.md — what it is, the verified inform-protocol facts, architecture, and how it plugs into go-unifi / terraform-provider-unifi.
  • docs/BUILD-PROMPT.md — the kickoff plan for the first build phase (a gateway that adopts to CONNECTED).

The one hard rule

Devices enter a controller only through the real inform/adoption lifecycle — no MongoDB/DB seeding. DB-injected devices render permanently disconnected; the whole point of this tool is real, connected, adoptable devices.

License

MIT — see LICENSE.

Documentation

Overview

Package emu emulates UniFi devices (UAP/USW/UGW) against a real UniFi controller using the inform protocol.

Index

Constants

View Source
const DefaultKey = inform.DefaultKey

DefaultKey is the inform authkey of unadopted devices.

Variables

This section is empty.

Functions

func Models added in v0.2.0

func Models() []string

Models returns the names of every model in the generated registry, sorted for a stable order. Read-only view for callers that enumerate the known models — the integration suite uses it to pick a random fleet.

Types

type DeviceSpec

type DeviceSpec struct {
	MAC          string `json:"mac" yaml:"mac"`
	Type         string `json:"type" yaml:"type"`
	Model        string `json:"model" yaml:"model"`
	ModelDisplay string `json:"modeldisplay" yaml:"modeldisplay"`
	Version      string `json:"version" yaml:"version"`
	Name         string `json:"name" yaml:"name"`
	IP           string `json:"ip" yaml:"ip"`
	Ports        int    `json:"ports" yaml:"ports"` // overrides the profile port layout when > 0
	// SSIDs opts the AP into emitting vaps. Empty by default: this
	// controller build rejects default vaps with log noise until a
	// setstate provisions real WLAN config (the setstate echo path
	// overlays vap_table), so devices inform with an empty vap_table.
	SSIDs []string `json:"ssids" yaml:"ssids"`
}

DeviceSpec describes one emulated device. Type, ModelDisplay and Version default from the model profile when empty; Name defaults to "UBNT". An explicit Type must equal the profile's: the profile drives the payload shape, so a mismatched Type would describe an incoherent device and is an error, not an override.

The json/yaml tags are the fleet-file contract (unifi-emu -devices, SIM_DEVICES); keep the two families identical so either format names the same keys.

func (*DeviceSpec) UnmarshalYAML added in v0.2.0

func (d *DeviceSpec) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML lets a fleet-list entry be a bare model string ("U7PRO") or a full mapping. A scalar becomes {Model: scalar}; a mapping decodes the known keys and rejects any other. JSON files parse through the same YAML decoder, so both formats get this behavior.

type DeviceState

type DeviceState int

DeviceState is the adoption state of an emulated device.

const (
	StatePending DeviceState = iota
	StateAdopting
	StateConnected
)

func (DeviceState) String

func (s DeviceState) String() string

type Emu

type Emu struct {
	// contains filtered or unexported fields
}

Emu is a fleet of emulated UniFi devices informing one controller.

func New

func New(informURL string, opts ...Option) *Emu

New builds a fleet whose devices will inform informURL.

func (*Emu) Add

func (e *Emu) Add(specs ...DeviceSpec) error

Add validates specs and adds them to the fleet. MACs are normalized before keying, so the same device added twice errors however it was spelled. The first invalid spec aborts the call; earlier specs stay added. Add errors once Start has been called: a running fleet is fixed, and devices added after Start would never be launched.

func (*Emu) Start

func (e *Emu) Start(ctx context.Context) error

Start launches one inform goroutine per device, all tied to ctx. Start is one-shot: a second Start errors "emu: already started" even after Stop — that is intended, build a fresh fleet with New to restart. Starting an empty fleet errors rather than welding it shut: once started, Add rejects new devices.

func (*Emu) State

func (e *Emu) State(mac string) (DeviceState, bool)

State reports the adoption state of one device, ok=false when mac is unknown to the fleet or unparseable.

func (*Emu) Stop

func (e *Emu) Stop()

Stop cancels every device loop and waits for them to return. It is safe to call more than once.

func (*Emu) WaitState

func (e *Emu) WaitState(ctx context.Context, mac string, want DeviceState) error

WaitState polls every 10ms until mac reaches want or ctx is done. The timeout error names the last observed state so a stalled adoption tells the caller where it stalled.

type ModelProfile

type ModelProfile struct {
	Model        string      `json:"model"`
	ModelDisplay string      `json:"model_display"`
	Type         string      `json:"type"` // "ugw", "usw", "uap"
	Version      string      `json:"version"`
	Ports        []PortSpec  `json:"ports"`  // usw + ugw + uap (eth port)
	Radios       []RadioSpec `json:"radios"` // uap only
}

ModelProfile is the per-model shape the controller expects to see: identity strings plus the port/radio/SSID layout tables are built from.

func Profile added in v0.2.0

func Profile(model string) (ModelProfile, bool)

Profile returns the model profile for a known model. The bool is false for a model the generated registry does not contain. Read-only: callers must not mutate the returned slices.

type Option

type Option func(*Emu)

Option customizes an Emu fleet.

func WithInformInterval

func WithInformInterval(d time.Duration) Option

WithInformInterval sets the inform interval every added device starts with. Controller responses can still retune it per device later.

type PortSpec

type PortSpec struct {
	IfName   string `json:"ifname"`
	Name     string `json:"name"`
	PortIdx  int    `json:"port_idx"`
	Media    string `json:"media"` // "GE", "SFP+"
	PoECaps  int    `json:"poe_caps"`
	IsUplink bool   `json:"is_uplink"`
}

PortSpec is one switch/gateway/ethernet port in a model's layout.

type RadioSpec

type RadioSpec struct {
	Name        string `json:"name"`  // "wifi-ng", "wifi-na"
	Radio       string `json:"radio"` // "ng", "na"
	Channel     int    `json:"-"`     // derived at load time; not in model_profiles.json
	HT          string `json:"ht"`    // "20", "40"
	MinTxPower  int    `json:"min_txpower"`
	MaxTxPower  int    `json:"max_txpower"`
	NSS         int    `json:"nss"`
	RadioCaps   int    `json:"radio_caps"`
	AntennaGain int    `json:"antenna_gain"`
}

RadioSpec is one wireless radio in an AP model's layout.

Directories

Path Synopsis
cmd
modelgen command
Command modelgen reduces an adopted UniFi simulation fleet (or a harvested controller hardware database bundle) to model_profiles.json, the model catalog the emulator embeds at build time.
Command modelgen reduces an adopted UniFi simulation fleet (or a harvested controller hardware database bundle) to model_profiles.json, the model catalog the emulator embeds at build time.
unifi-emu command
Command unifi-emu runs a fleet of emulated UniFi devices informing a real controller until interrupted.
Command unifi-emu runs a fleet of emulated UniFi devices informing a real controller until interrupted.
Package inform implements the UniFi inform wire protocol: the TNBU binary packet, AES-128-CBC/GCM encryption, and zlib/snappy compression.
Package inform implements the UniFi inform wire protocol: the TNBU binary packet, AES-128-CBC/GCM encryption, and zlib/snappy compression.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL