Controlling a Remote Visor from the CLI¶
Every skywire cli command normally talks to the visor on the same
machine over local RPC (localhost:3435). The same commands can drive
a visor on another machine over the skywire network itself — full
typed API: start and stop apps, change routing-session settings
(min-hops, mux), manage transports and routes, and read the runtime
log ring.
The motivating workflow: diagnosing a device you cannot shell into. Instead of reproducing a VPN/proxy failure by tapping through a phone's GUI, attach that visor to a machine you control and run the client from your desk with the logs in front of you — the difference between "it doesn't connect" and an actual trace.
Two independent mechanisms exist, and they differ in direction:
| Direction | What you get | Target needs | |
|---|---|---|---|
--via CLI bridge |
controller dials target | the full skywire cli surface against the target |
controller's PK in the target's trust list, restart |
| Hypervisor attach | target dials controller | web UI, hv ls, hv tui (apps, routing knobs, transports, logs) |
controller's PK in the target's hypervisors, restart |
Listing a controller PK in hypervisors enables both — it puts
the PK in the trust list and makes the target dial out to it.
Who may control a visor¶
A visor's management surfaces all consult one live whitelist
(pkg/visor/peer_whitelist.go): the visor's own PK, every PK in
the config's hypervisors list, and every PK in
dmsgpty.whitelist. A PK on that list has full control —
including a remote shell where dmsgpty is enabled. Only list keys you
own.
Two consequences worth knowing before you troubleshoot:
- The inbound listeners only start when the boot config trusts
someone. With no hypervisor or pty-whitelist PKs at startup the
visor logs
No hypervisor PKs or dmsgpty whitelist; dmsg visor-RPC server disabledand nothing listens. A visor nobody configured to be managed is unreachable by construction. - The whitelist and listeners are built at boot.
skywire cli visor hv add <pk>connects out to the new hypervisor immediately and writes the PK to the json, but inbound access for that PK (the--viapath below) starts on the target's next restart. - A runtime-added PK does not survive a package update. The json
is regenerated from
/etc/skywire.confbyskywire autoconfig, so a PK added only withhv addis dropped. Put it inHYPERVISORPKSto make the grant durable.
Trust is transitive upward: when a visor connects to its hypervisor, the hypervisor pushes its own hypervisors into the visor's live whitelist — so a hypervisor-of-hypervisors reaches every visor down the chain without being listed on each one.
Set up¶
On the controller — a normal visor. Note its PK:
Running a hypervisor (config gen -i, or skywire cli visor hv
enable) is only needed for the web UI / hv ls / hv tui views.
The --via CLI bridge works visor-to-visor without one — being
listed by the target is enough.
On the target — put the controller's PK in the config, then restart the visor:
# at generation
skywire cli config gen --hvpks <controller-pk> ...
# or on an existing config file
skywire cli config update hv --add-pks <controller-pk>
# or on a running visor (persists; inbound access needs the restart)
skywire cli visor hv add <controller-pk>
cli_addr is not involved: it gates only the local TCP RPC
listener, and the dmsg/transport surfaces start independently of it.
(Builds older than the decoupling in initCLI did skip every RPC
surface when cli_addr was blank — if a trusted, restarted target
still refuses dials, check its binary age first.)
Driving the target¶
Add --via dmsg://<target-pk> to any command. The local visor opens
a dmsg stream to the target's visor-RPC port and bridges bytes; the
CLI speaks its normal protocol over it, using the local visor's
identity — which is exactly the PK the target was told to trust. No
separate CLI keypair.
skywire cli --via dmsg://<pk> visor info
skywire cli --via dmsg://<pk> tp # transports
skywire cli --via dmsg://<pk> route groups # active route groups
skywire cli --via dmsg://<pk> visor app ls
skywire cli --via dmsg://<pk> visor app log skysocks-client beginning
--via skynet://<pk> does the same over a skywire transport
(stcpr/sudph, auto-created) instead of the dmsg relay — lower latency
when a direct path exists; prefer dmsg:// for NAT-bound targets.
--rpc dmsg://<pk> / --rpc skynet://<pk> are accepted as aliases.
The diagnosis session¶
The shape that replaces poking at a device's GUI — one terminal following the target's runtime log ring, one driving the client:
# terminal 1: the target's own account of what happens
skywire cli --via dmsg://<pk> visor log --follow --min-level debug
# terminal 2: run the client on the target
skywire cli --via dmsg://<pk> proxy start <server-pk> --min-hops 3
visor log reads the target's in-memory ring — route-setup,
transport, and router events arrive tagged with the app session, so a
route-finder refusal or a dead intermediate shows up as itself rather
than as a hung connect. --module <regex> and --min-level narrow
it. proxy start / vpn start flags (--min-hops, --mux,
--existing-tp…) apply to the target's routing session, so a
"fails at min_hops 3, works at 1" report reproduces exactly.
One caveat: proxy start --verbose opens its scoped log stream over
local gRPC and does not follow --via — with a remote target it
would stream the local visor's logs. Use the visor log --follow
terminal instead.
Hypervisor views¶
When the controller runs a hypervisor, every visor listing it connects inbound and can be driven through:
skywire cli visor hv ls # all connected visors, one row each
skywire cli visor hv tui # interactive: apps, min_hops/mux, transports, routes
plus the web UI on localhost:8000. These ride the connection the
target dialed, so they work even for targets whose inbound RPC
surfaces are off.
Testing the mobile app's visor from a desktop¶
The Android app generates and owns its visor's config, and out of the
box the phone trusts nobody and runs no RPC surface — nothing on the
phone listens unless its owner asked. The ask is Settings → Remote
management → Grant access…: paste the controlling visor's PK
(skywire cli visor pk on that machine), then disconnect/reconnect
the core. The grant is one key, shown on the card, revocable in one
tap; granting and revoking both apply on the next core start.
That single grant is both directions at once. The phone dials the
desktop hypervisor — it appears in hv ls / hv tui / the web UI —
and admits the desktop on its dmsg RPC surfaces, so the full CLI
works too:
skywire cli --via dmsg://<phone-pk> visor info
skywire cli --via dmsg://<phone-pk> visor log --follow --min-level debug
skywire cli --via dmsg://<phone-pk> proxy start <server-pk> --min-hops 3
— the two-terminal diagnosis session above, against the phone in your pocket. The phone's PK is on the app's identity screen and in the diagnostics export.
Narrower than a desktop grant by construction: the phone runs no
dmsgpty and pins dmsgscp off, so the granted key gets the typed API
only — no remote shell, no file access. The app also re-pins
hypervisors to exactly the granted key on every launch, so a key
that arrived any other way (a hand-edited config, a persisted runtime
hv add) does not outlive the setting.
Expect a phone visor to be reachable only while the app's core is running (foreground service); Android may still throttle the radio in deep Doze, which shows up as dmsg session drops, not as auth errors.
Troubleshooting¶
- Dial fails / times out on
--via dmsg://<pk>— the target's listener is off (no trusted PKs at its last boot, or a binary old enough that blankcli_addrdisabled the dmsg surfaces too), you typo'd the PK, or the target is offline in dmsg. Check the target's boot log forDmsg visor-RPC server listening. - Connection accepted, then dropped immediately — the connecting visor's PK is not on the target's live whitelist (added at runtime without a restart, or you are dialing from a different visor than the one that was whitelisted).
bridge needs the local visor RPC at localhost:3435—--viabridges through the local visor; start one.- A visor is missing from
hv ls— the target dials out only if the controller's PK was in itshypervisorsat boot;hv addon the target (or a restart after a config edit) fixes it.
Related: configuration.md (hypervisor setup,
hv tui), VISOR_CONFIG_RUNTIME.md
(runtime vs config-file changes),
manual-routing.md (what to do with the routes
you can now see).