Device groups (private networking)#
A device group lets your devices talk to each other by name — mymac-anna-myteam.internal — without anything being exposed to the internet. There's no public DNS record, no certificate, no public listener: an internal name is unreachable from outside the group by construction, not by obscurity.
Use it for the machine-to-machine half of a fleet: a sensor pushing to a collector, SSH between your own boxes, a dashboard reading a database on another site — all over the devices' existing tunnels.
How it works#
Every device already holds an authenticated tunnel to the relay. A group connection uses those tunnels end to end: the initiating agent asks the relay to connect it to a service on an internal name, the relay checks that both devices are members of a group the service is published to (and the same organization, and the route is enabled — anything else is refused), then splices the two tunnel legs together. The receiving device just sees an ordinary incoming channel to its local target.
- Internal names never exist publicly. They're never written to DNS, never issued a certificate, and the public ingress won't route them —
.internalis an ICANN-reserved TLD that cannot publicly resolve. - Plain TCP works. Unlike public hostname routes (which route on TLS SNI), the internal plane doesn't need the connection to be TLS — SSH, Postgres, HTTP, MQTT all work as-is.
- The privacy note. Group traffic is mTLS-protected between each device and the relay, but the relay splices the stream, so for the strongest posture run TLS (or mTLS — your devices already have CA-issued certs) inside the group connection. Self-encrypting protocols like SSH give you this for free.
Set it up#
1. Create the group. Console → Fleet → Groups → New group. Add the member devices. Membership is the authorization boundary — only members can reach (or be reached by) the group's routes.
2. Publish a service to the group. On the serving device's page, add a route with Exposure: internal (group) (the option appears once the device is in a group), pick the group, and set the internal target as usual — e.g. 127.0.0.1:22. The console shows where members reach it:
mymac-anna-myteam.internal:22The name is {device}-{username}-{group}.internal — the same device-username shape as the device's public address, plus the group it is published to. Services are reached there on their own ports, exactly like a machine on your LAN: publish several services (one route per port) and they all answer at the same name. A device published to two groups answers to one name per group; the group also decides who may connect.
**3. Enable peer sync on the devices that will initiate connections** (Go agent). Add to the device's agent.env:
PEERS_URL=https://app.<your-apex>/device/peers(the same origin as TARGETS_URL). Optional: PEERS_REFRESH_INTERVAL (default 15m) and HOSTS_FILE (default /etc/hosts). The agent then maintains a managed hosts-file block mapping each peer's name to a local loopback address and listens there on each published port, so the names resolve only on group members.
Two practical notes:
- Writing
/etc/hostsand binding a low port (like:22for SSH parity) needs privileges — for a systemd unit,AmbientCapabilities=CAP_NET_BIND_SERVICEcovers the port part. - Devices that only receive group connections need no changes at all — not even an agent update. Older agents and ESP32 boards can host group services as they are; initiating from an ESP32 uses the library's
dialPeer(hostname, port)call.
4. Connect from any member device:
ssh pi@mymac-anna-myteam.internal # by name, as typed
curl http://collector-anna-myteam.internal/ # port 80
psql "host=dbbox-anna-myteam.internal port=5432 …"Group SSH vs public SSH#
Both work — they solve different problems:
| Public port route | Internal group route | |
|---|---|---|
| Reachable from | anywhere on the internet | member devices only |
| Address | relay-host:PORT | device-username-group.internal (+ the service port) |
| Exposure | a real public listener (use keys!) | none — nothing to scan |
| Client needs | any SSH client | the device's agent with peer sync |
If only your own devices ever need to reach a box, an internal route is strictly the safer choice: there is no public artifact to protect.
Limits & notes#
- Group traffic transits the relay (hub-and-spoke, not peer-to-peer) and shares the device's tunnel with its public traffic.
- The receiving agent's target allowlist still applies — the relay can't steer a device to a target its operator didn't authorize, group or no group.
- Initiating devices need an agent speaking
tollan.v2; the console flags devices whose agent needs an update. Receivers work on any version.