EdgeWire: Reach Services on a Robot
:::note Early access EdgeWire is in early access. To use it, contact Admiral to enable it for your organisation. :::
EdgeWire gives your laptop or application a direct, encrypted connection to a named service inside a device's workload, without publishing a port on the device's network and without a VPN. You declare the services in the workload's configuration, then forward one to a local port with the CLI or open it from code with the Go library.
- Direct when possible. The two ends try a peer-to-peer path first (ICE) and fall back to a relay through Admiral when no direct path exists.
- Transport. QUIC, adapting to the link (for example satellite or 5G) and built for 10 Gbit/s links.
- Authorisation. Each connection needs a signed grant from Admiral, issued only if you are allowed to use EdgeWire on that device and the service is declared in the configuration.
Typical uses: a Foxglove or web UI served by the workload (Foxglove & RViz), a database or HTTP API for debugging, or a UDP stream.
1. Declare Services in the Configuration
Services are part of the workload configuration, under spec.edgewire.services. They are versioned, rolled out and assigned to fleets like any other setting.
spec:
image: registry.example.com/robot/app:1.4.2
edgewire:
services:
- name: foxglove
protocol: tcp
port: 8765
- name: telemetry
protocol: udp
port: 9000
Edit the configuration with admrl edit configuration <name> or admrl apply -f (see CLI). The server validates the block and shows the render diff before anything is written.
| Rule | Detail |
|---|---|
name | Unique within the configuration. Lowercase, starts with a letter, then letters, digits, _ or -; at most 64 characters. |
| Reserved names | ssh, exec and debug cannot be used. |
protocol | Exactly tcp or udp. |
port | The port the application listens on inside the workload, 1 to 65535. |
| Count | At most 32 services. |
| Other fields | Rejected. There is no host, address or Unix socket option. |
Things to know:
portis the application's own listening port. You do not need to add it to Published ports; EdgeWire is independent of published ports, so nothing becomes reachable on the device's local network.- Both isolated (default) and host-network workloads can expose services. On a host-network workload the device agent forwards only to sockets owned by the workload's own processes, so other daemons on the device stay out of reach.
- An empty or absent
edgewireblock means the workload exposes nothing. - The dashboard's configuration editor has an EdgeWire section, but it is part of the early access preview and may not appear for your organisation yet. Editing the document with the CLI works regardless. The editor currently disables service editing while Use the device's network directly is on; set
spec.edgewirein the document instead.
2. Forward a Service from the CLI
admrl edgewire forward <device> <service> <listen>
<service> is the name from the configuration (not a port on the device). <listen> is a local TCP address such as 127.0.0.1:8080, or a Unix socket path. For a udp service the listen address is a UDP address. Each local connection opens its own stream to the service. The command stays in the foreground; press Ctrl-C to stop.
# Forward the "foxglove" service to local port 8765
admrl edgewire forward edge-1 foxglove 127.0.0.1:8765
# Let the OS choose the local port, and refuse relayed paths
admrl edgewire forward edge-1 foxglove 127.0.0.1:0 --direct
While running, the CLI prints the bound address and the path in use on stderr, in this form:
admrl: forwarding 127.0.0.1:8765, [::1]:8765 -> foxglove on <device id> (Enter / Ctrl-T for path status)
admrl: [mesh] <kind> path <local> (<type>) -> <remote> (<type>)
Press Enter or Ctrl-T for the current path, how long it has been up, and the round-trip time. Loopback addresses bind on both IPv4 and IPv6.
Other commands in admrl edgewire:
| Command | Purpose |
|---|---|
admrl edgewire forward <device> <service> <listen> | Listen locally and forward each connection to the service. |
admrl edgewire stdio <device> <service> | Pipe stdin and stdout to the service, for tools that take a proxy command. |
admrl edgewire token [--ttl 1h] [--device <id|name>] [--target <service>] | Print a token (admrl_ew_...) for programs other than the CLI. Shown once. Default lifetime 1 hour, maximum 24 hours. |
admrl edgewire token revoke <id> | Revoke a token by the id printed with it. |
stdio suits clients that accept a proxy command: the CLI help gives ssh -o ProxyCommand='admrl edgewire stdio edge-1 ssh' user@edge-1 as its example shape. Note that ssh is a reserved service name for customers, so that exact command is for Admiral staff; for your own service, substitute its name and a client that speaks the protocol through a ProxyCommand-style helper.
Choosing a path:
| Flag | Effect |
|---|---|
--direct | Use direct peer-to-peer paths only. |
--relay | Use TURN relay paths only. |
--priority low|normal|high | Traffic priority (default normal). |
--cc bbr|cubic | Congestion controller (default bbr). |
--rate <Mbps> | Cap local egress rate; 0 uses the signed limit. |
-v, --debug | Log transport details to stderr. |
--public-keys | Show the authenticated public identity keys of both ends. |
forward and stdio also accept --api, --org and --no-login, as the other CLI commands do (see CLI), plus advanced ICE timing flags; run admrl edgewire forward --help for the full list.
3. Use EdgeWire from Code
The Go library github.com/AdmrlOS/edgewire does the same thing in your own program. This snippet forwards the service http on a device to a local port:
package main
import (
"context"
"log"
"os"
"os/signal"
"github.com/AdmrlOS/edgewire"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
err := edgewire.Forward(ctx, "127.0.0.1:18080", edgewire.Options{
DeviceID: os.Getenv("DEVICE"),
OrgID: os.Getenv("ADMRL_ORG"),
Service: "http",
})
if err != nil && ctx.Err() == nil {
log.Fatal(err)
}
}
Set ADMRL_TOKEN to a token from admrl edgewire token; ADMRL_API selects a private control plane. The library is a private module today (GOPRIVATE=github.com/AdmrlOS) and the public release is not yet announced; talk to Admiral for access. To obtain a connection object directly instead of a local listener, use edgewire.Open with the same options.
4. Security Model
- Named services only. A caller picks a service name from the configuration. It cannot supply a host, port or address, and the backend never accepts an arbitrary network address.
- The workload's own sockets. The device agent resolves the name to the running workload, and forwards only to listening sockets owned by that workload's processes. Host services are not reachable, including on host-network workloads.
- Checked on every open and renewal. A connection needs an organisation entitlement, EdgeWire permission on the device, and the service present in the workload's rendered configuration. Sessions are renewed under the same checks.
- Signed grants. Each connection carries a signed grant (ML-DSA signatures) issued by Admiral for the application
workload-forward. - Revocation. Removing a service from the configuration (or changing it) closes existing streams on the device and denies new grants and renewals; it does not wait for a grant to expire. Tokens can be revoked with
admrl edgewire token revoke. - No fallback to the host. If the device agent cannot provide the forwarding for the workload, the connection fails rather than reaching the host.
- Separate from shell access.
admrl execandadmrl shellremain separate capabilities (see CLI).
Admiral staff-only paths such as host SSH are not customer services; the names ssh, exec and debug are reserved for that reason.
5. Troubleshooting
| Symptom | Likely cause and fix |
|---|---|
<addr> is already in use by another program; choose another local port | The local port is taken. Pick another, or use port 0 to let the OS choose. |
| Not entitled / not authorised | EdgeWire is not enabled for the organisation, or your role lacks EdgeWire permission on the device. Contact Admiral to enable it; ask an administrator about your role. |
| Service not configured / service denied | The name is not in the rendered configuration the device is running. Check spelling, that the configuration version containing the service has been rolled out to this device, and that the name is not reserved. |
| Device not found / unreachable | Check the device name and organisation (--org, admrl use-org), and that the device is online. |
| No path | Neither a direct nor a relayed path could be established. If you used --direct, retry without it. If you used --relay and it fails, relay may not be enabled for your organisation. |
| Client upgrade required, or device unsupported | Update admrl, and check the device runs an agent release that supports workload forwarding. |
| Connects but the application does not answer | The workload is not listening on port yet, or listens only on a different port. Check from inside the workload with admrl exec <device> -- ... (for example ss -ltn if the image includes it). |
admrl: open service: ... printed while running | A single connection failed to open; the listener stays up. The message names the cause above. |
Check the path with Enter or Ctrl-T while a forward is running, and use -v for transport detail. A relayed path works everywhere but adds latency; --direct is useful to confirm whether a direct path exists, for example from behind a restrictive firewall. If --direct fails and the plain command works, you are on the relay.
Related: Networking & Failover, Foxglove & RViz, CLI.