Device identity
Every upgraded agent generates a random UUID v4 on first start and stores it in
<WANCTL_CONFIG_DIR>/device_id (the usual wanctl config directory when the
variable is unset). wanctl id prints this ID, the certificate fingerprint, and
the effective config directory. Concurrent starts publish the same ID; an invalid
existing file is an error rather than an invitation to silently reset identity.
The three concepts are independent:
device_ididentifies an installation. Relay routes, sharing grants, notification settings, audit associations, and controller pins usenamespace/device_id.--namesets a display name (the local host name on macOS, the product model on Android, the hostname elsewhere). Portal aliases are display labels too. Both may repeat and change without replacing the installation.- The certificate fingerprint authenticates the endpoint. Keeping the device ID while replacing the certificate still requires explicit identity confirmation.
Use wanctl peers to find IDs and labels, and --target <ID> or
--target <namespace>/<ID> to select an installation. A unique name or alias is
also accepted. Ambiguous names fail instead of selecting a device by registration
order. PostgreSQL-backed resolution includes offline devices in this check.
The portal uses full IDs for actions and shows a short ID beside duplicate labels.
Upgrade
Upgrade the relay and portal together, then agents and controllers. Migration 007
renames the database routing column to device_id, adds mutable display_name
and migration metadata, and removes the alias uniqueness index. This is a schema
change: do not run old relay binaries against the migrated database. Back up the
database before deploying; rolling back requires restoring the corresponding
schema/data snapshot as well as the old binaries.
Legacy agents can keep their old routing key until upgraded. When a new agent
first reports its UUID, a legacy row in the same namespace is promoted if its
certificate fingerprint matches, whatever its old name was. Promotion preserves
the database row, alias, sharing grants, notification settings/health, and audit
associations in one transaction. The old name is kept as legacy_name; the
display name becomes the label the agent reports, as it does on any later rename.
A same-name device with a different fingerprint creates a separate record and
inherits none of those associations - the name is a label and cannot move an
installation's identity. The old name cannot be re-registered by an outdated
agent once promoted.
New controllers resolve targets before checking trust. A promoted row exposes its previous target so an existing pin can be copied to the UUID target. The copied value is always the controller's stored fingerprint, never the relay's offered value. Existing UUID pins are never overwritten. Old controllers can still target UUIDs, but need an explicit initial pin because they do not migrate name-based pins.
Renaming a device across its first upgrade is safe: promotion follows the
certificate, not the name. Until v0.7.1 the match also required the old name, so
a host whose name drifted on its own registered as a new device and left the old
row offline - macOS is the usual case, where os.Hostname() reads localhost or
bogon depending on the network. On macOS the agent now labels itself with
scutil --get LocalHostName, which does not drift. Repairing a database that
already split one installation in two is a manual step: delete the orphaned new
row and let the agent re-register, or re-point its associations.
A legacy record already overwritten by the old same-name collision cannot recover
the overwritten device's identity automatically.
Protocol compatibility
Agents send device=<UUID>, device_id=<UUID>, and name=<display name> on both
WebSocket registration and HTTP polling. Subsequent session, notification,
pairing, and deregistration requests use the UUID. inst remains an ephemeral
process instance marker, not the persistent identity.
For compatibility with existing consumers, /peers still returns canonical
routes in devices, and labels in aliases. Device-list JSON retains name as a
legacy alias for the canonical route and adds explicit device_id,
display_name, and legacy_name fields. Consumers must not use display labels as
map keys. Legacy records have no UUID device_id field until upgraded.
GET /resolve?target=... returns the authorized canonical target and, after a
legacy promotion, legacy_target. The same owner/ACL checks used for dialing
apply. An old relay returning 404 uses the older peers-based resolution path;
other failures do not fall back to guessing a target.
Resetting and cloning
Keeping the config directory keeps the installation ID across upgrades and
renames. Deleting the directory creates a new installation. A full VM/config
clone also copies its device ID and certificate: before starting wanctl in an
independent clone, remove its copied device_id, cert.pem, and key.pem, then
re-enroll it as a new installation. The original device must retain its files.
This ID is not a MAC address, hardware serial number, or proof of ownership.
Verification
go test ./... covers concurrent ID creation, same-name agents across both
transports, rename/restart continuity, and certificate mismatch rejection.
To run the real PostgreSQL migration, sharing, and pin-migration checks against a disposable PostgreSQL server:
WANCTL_TEST_POSTGRES='postgres://[email protected]:5432/postgres?sslmode=disable' \
go test ./internal/relay -run TestDeviceIDPostgres -v
The integration test creates and removes its own schema. It requires permission to create schemas, and does not modify existing schemas.
Android application sandbox
Android denies hard-link creation in an ordinary app's private data directory.
Since v0.6.1, Android serializes first-time ID publishers with device_id.lock
and atomically renames the synced temporary file after checking for an existing
ID. The lock file is retained; kernel locks are released on process exit. Other
platforms retain hard-link publication. Existing IDs are never replaced.
For a packaged-binary regression check, start a disposable relay with
WANCTL_TOKENS=sandbox-token:sandbox go run . relay --addr 127.0.0.1:18740, then
run scripts/android-id-smoke.sh /path/to/emulator-ABI.apk with a booted emulator,
Android SDK/JDK and the usual ~/.android/debug.keystore. The probe is a normal,
non-debuggable APK: it checks untrusted_app context, 16 concurrent ID processes,
persistence, and agent registration. Running the binary as adb shell is not
an equivalent permission test.