Skip to content

Mesh Tunnels

Reach a whole fleet of devices behind NAT from one tunnel and one token. Connect devices one at a time or bring a network online from a single hub, mix HTTP, TCP, and TLS, and address every device by name.

Summary

Reach every device on a private network individually, over the internet, from one tunnel and one token. Connect devices one at a time, or bring a whole network online at once from a single hub machine, which is how devices that cannot run software of their own get reached. Each device is addressed by the name you gave it, and HTTP, TCP, and TLS devices live side by side in the same mesh.

How a mesh tunnel works#

A standard tunnel is one service, one address. A mesh tunnel is one tunnel that many services join, each under a name you choose, and each reachable on its own.

Devices join a mesh two ways, and it is built for both. One at a time, when a device runs the agent itself and names itself as it connects. Many at once, when a single machine brings up an entire network of them from one config file. The two mix freely on the same mesh, because the tunnel only cares that each name is unique.

The name is the identity. Connect something as plc-line-1 and that is where it lives: not an IP that changes when the network does, not a port you keep a spreadsheet of, and not a record anyone has to add to DNS. The address follows the device to another network, another site, or another cabinet.

Every connection is outbound, so nothing on site needs an inbound rule, a static IP, or a port forward. That is what makes this work on the networks these devices actually live on: NAT, CGNAT, a cellular router, a plant network nobody is allowed to reconfigure.

Each device is reached on its own, not as a group

A mesh addresses devices individually: a request goes to the one device you named, never to all of them. If you want the opposite, one address that fans a request out to everyone connected, that is a shared tunnel.

To follow along you need a Localport account and something listening on a port on each device you want to reach.

Step 1: Create a mesh in the dashboard#

Open dashboard.localport.io, go to Mesh, and click New. The create panel asks for a name, a region, and, if you have one ready, a custom domain.

The domain field is worth pausing on, because it decides what your fleet's addresses look like. A mesh needs a wildcard domain (*.plant.acme.com), since each device is served on its own subdomain of it. Domains that are not wildcards show as Needs wildcard in the dropdown. You can skip it now and attach one later; putting your fleet on your own domain covers what changes.

Create the mesh, then flip Custom Name and No UI on the Connect step and watch the command rewrite itself.

Step 2: Install the agent#

Install it on whatever will do the connecting: each device that can run it, or the one hub machine that fronts a network of devices that cannot. It is a single dependency-free binary for macOS, Linux, and Windows, so it drops onto an industrial PC, a rack server, or a Raspberry Pi the same way. Follow Installation, then confirm it with localport --version.

The agent is open source, and it is the only part on your side

Everything Localport runs inside your network is this one binary, Apache 2.0 licensed at github.com/localport/agent. Your security team can read exactly what it does, build it from source, and ship the version they audited. More on what runs on your machines.

Step 3: Copy the token and shape the command#

Creating the mesh moves the panel to the Connect step, which you can reopen any time from the Connect button. Three controls matter here.

Token: one credential for the whole fleet

The Token row shows the mesh's token, masked, with a copy button. This is the only credential involved: every device and every hub on this mesh uses the same one. Copy it once into your secret store, hand it to your provisioning system, and adding the fortieth device needs no new secret. Rotating it in the mesh's settings replaces it everywhere at once.

Custom Name: the switch that gives a device its identity

Turn on Custom Name and a field appears (it suggests my-device on a mesh). What you type is appended as --name, and on a mesh that name is the address. Leave it off and the device connects unnamed, which is fine for a single tunnel and pointless for a mesh.

No UI: for machines with nobody watching

No UI adds --noui: plain log lines instead of the live panel, so systemd, Docker, and CI collect the output normally. Turn it on for anything unattended. The agent also switches by itself when no terminal is attached.
localport tcp 502 --token tok_k8f2 --region eu --name plc-line-1 --noui

Step 4: Connect devices one at a time#

When a device can run the agent itself, a gateway, an industrial PC, a Raspberry Pi doing real work, it connects on its own and names itself. Run that command on it and leave it running.

If two devices claim the same name, the first keeps it and the second becomes plc-line-1-2, so a copy and pasted provisioning script can never take over an address that is already serving traffic. Names are lowercase letters, numbers, and hyphens.

Step 5: Connect a whole network from one hub#

Most devices worth reaching remotely cannot run an agent at all. A PLC, a Modbus meter, an IP camera, a label printer, a card reader: closed firmware, no package manager, no room for another process, and no appetite from whoever certified it for anything new to be installed.

They do not need to run anything. A mesh needs an agent per network, not per device. One ordinary machine on the LAN, an industrial PC, a small server in the rack, a Raspberry Pi in the cabinet, runs Localport once and fronts everything it can reach:

# localport.yaml on the hub machine
version: 1
spec:
  token: ${env.LOCALPORT_TOKEN}
  region: eu
  endpoints:
    - name: plc-line-1
      url: tcp://192.168.10.40:502
    - name: plc-line-2
      url: tcp://192.168.10.41:502
    - name: meter-main
      url: tcp://192.168.10.55:502
    - name: camera-dock
      url: http://192.168.10.21:80
    - name: scada
      url: tls://192.168.10.70:8443
localport --config localport.yaml --noui

One process, one token, five devices now individually reachable without a single change to any of them. Adding the sixth is three lines and a restart.

A few things that matter when this runs in a plant rather than on a laptop:

  • The devices stay untouched. The hub reaches them over the LAN exactly as any other machine on that network does. No firmware, no configuration, no agent, no reboot.
  • The token comes from the environment. ${env.LOCALPORT_TOKEN} is read at start, so the file you commit and ship to every site carries no secret.
  • Each endpoint needs a real port. 192.168.10.21:80, not 192.168.10.21. The hub dials the address as written.
  • The same file goes to every site. Sites wired the same way take an identical config, which turns a rollout into a deployment rather than a project.
  • Run it as a service. With --noui, systemd or your init of choice starts it at boot, so a hub that loses power comes back on its own.

One hub per network, not one per device

The unit that needs the agent is the network. A site with forty devices on one LAN is one hub with forty endpoints: one token to provision, one process to monitor, one file to review.

The full YAML schema, including several tokens or regions in one file, is on the Configuration page.

What runs on your machines#

Two things tend to decide whether a fleet like this is allowed in a plant at all: what the software is, and how it behaves when the network misbehaves.

The agent is open source, and it is the only part on your side

Being able to read the source matters more than usual here, because this binary sits inside the plant network and talks to equipment. It is Apache 2.0, so review, packaging, and change control work exactly the way they already do: audit it, build it, pin it, ship your own build if that is the policy.

Nothing else is installed anywhere. The devices are untouched, the hub carries one auditable binary, and the traffic is yours: never inspected, never logged, never used for training.

It expects the network to fail, and recovers on its own

Site networks drop. Cellular routers fail over, switches reboot, a VPN rearranges routes, a cable gets pulled during maintenance. A mesh treats all of that as normal operating conditions rather than a reason to page anyone.

  • Addresses survive the outage. A device that drops keeps its name and port reserved for 60 seconds, so a reboot or a blip returns it on exactly the address it had. Alert rules, dashboards, and scripts never see it move.
  • It notices in seconds, not minutes. Host network changes are detected at the kernel level (netlink on Linux, including embedded devices, the equivalent on macOS and BSD, a light poll on Windows), so every endpoint re-probes the moment the machine's addresses change instead of waiting out an idle timeout.
  • Forty endpoints do not stampede. Those signals are debounced and filtered down to real address changes, so link-down noise and route churn never turn one flapping switch into a reconnect storm.
  • It backs off, then catches up. Reconnects use exponential backoff with jitter so a site coming back does not hammer anything, and a fresh network signal cuts the remaining wait short.
  • It resumes rather than restarts. A reconnect picks the existing session back up in place instead of tearing everything down and renegotiating.
  • It stays cheap. One small binary, no runtime to install, and an idle connection parked in a blocking read with no polling. That matters on a hub doing real work and on metered cellular links.

The practical effect is a fleet that heals itself: a site offline for ten minutes comes back with the same names, the same ports, and the same addresses, without anyone visiting the cabinet.

Step 6: See the fleet in the dashboard#

Open the mesh and the Clients tab lists every connected device with its address, live connections, total traffic, and uptime. This is the view an operator watches: what is online, what went quiet, and when.

The Clients tab: TCP, HTTP, and TLS devices in one mesh, each with its own address.

Every card carries the device's address to copy and a Disconnect button that drops that one device and leaves the rest of the fleet alone. Devices fronted by the same hub share its public IP, which is why several rows can show the same address.

One mesh, three protocols

Protocol is chosen per device, not per mesh, so the fleet above is doing all three at once: a PLC over TCP, a camera over HTTP, and a SCADA service that terminates its own TLS over TLS. They share one token, one mesh, and one dashboard view.

That matters in the field, where a site is never one kind of equipment. You do not run three tunnels for three protocols, and you do not sort devices by what they speak. Each endpoint declares its protocol, in the url scheme on a hub or in the command on a device, and gets the addresses that protocol calls for: TCP and TLS devices get a host and port, HTTP devices get an https:// URL.

Reach any device#

Each address behaves like any other address on the internet. A browser opens the camera:

https://camera-dock.plant.acme.com

A Modbus client, a historian, or a script talks to the PLC by host and port:

mbpoll -a 1 -r 100 -p 41892 plc-line-1.plant.acme.com

Nothing on either end knows a tunnel is involved. The PLC answers on its own LAN as it always has, and your software connects to a hostname.

Put your fleet on your own domain#

A mesh works without a custom domain. Devices are addressed on Localport's domain instead, and everything on this page behaves the same way. Those addresses are just longer and machine-shaped, which nobody wants in a runbook, an alert, or a support call at two in the morning.

Attach a wildcard domain you own and the whole fleet moves onto it, one subdomain per device:

tcp://plc-line-1.plant.acme.com:41892
https://camera-dock.plant.acme.com
tls://scada.plant.acme.com:47810

Same fleet, same token, same commands. Now the addresses read as inventory: anyone who knows the naming scheme can guess where a device lives, monitoring targets are legible, and a device added at a new site is addressable the moment its hub picks it up, with no DNS record of its own to create. Multiply that across sites and it is the difference between a fleet you can operate and a fleet you have to look up.

Setup is one wildcard CNAME for *.plant.acme.com and then choosing the domain in the mesh's settings. The custom domains guide walks the whole flow.

Wildcard is the requirement, and the reason

Each device is served at <device>.<your-domain>, so the DNS record and the certificate both have to cover names that do not exist yet. That is precisely what a wildcard is for. Add the domain with *. in front and the rest is handled for you.

Lock down the fleet#

A mesh carries the same Network & Security settings as any tunnel, applied to the whole fleet at once.

  • Inbound IP Whitelist restricts which addresses can reach your devices, for example your monitoring system and your office range. To everyone else the fleet does not answer.
  • Network IPs restricts where devices may connect from, so a token that escaped cannot be used to attach a machine to your mesh from an unfamiliar network.
  • Token rotation in General invalidates the token everywhere at once and every device reconnects with the new one, which is the fastest way to cut off a site or a machine you no longer trust.

Make the fleet private with mTLS#

IP rules are tied to networks, and networks change. To take the fleet off the public internet entirely, turn on Mutual TLS (mTLS) in the mesh's Network & Security settings. From that moment nothing reaches any device without presenting a client certificate you issued, and a certificate you revoke stops working immediately — including sessions it already holds.

A certificate gets a caller to the door. What it may actually reach is a separate decision you make per device: a grant naming * for every device, a prefix like plc-*, or one exact name. A certificate with no grant reaches nothing, so issuing one is safe on its own.

The addresses stay exactly as they were. What changes is who is allowed to open them.

TLS passthrough and mTLS cannot both be on

Checking a client certificate means terminating TLS at the gateway, so a device

registered as tls is refused on a locked mesh. Use tcp for those — the

connection is still encrypted end to end by mTLS itself.

With mTLS on, every device carries a lock and an Access button that builds its connect command.

Reaching a locked device takes one more step, and the dashboard writes it for you: press Access on the device and it hands you a command that forwards a local port through the tunnel using your certificate.

localport connect tcp://plc-line-1.plant.acme.com:41892 --pem ./ops-laptop.pem -p 5020

The PLC is now at localhost:5020 on your machine, and your Modbus client, historian, or engineering software connects to it as if it were on the bench in front of you. Nobody without a certificate can reach it at all.

The enterprise combination

One wildcard domain, attached once to the mesh, carries the entire fleet: every device that connects lands on its own subdomain of it, with no record to add and nothing to attach per device. The mesh keeps each of them individually reachable behind NAT, and mTLS puts the whole thing behind certificates you issue and revoke. Attach a domain once, name a device once, and it is addressed like inventory, reachable from anywhere, and open to nobody else.

Issuing certificates, downloading them, revoking a lost laptop, and the PKCS#12 route for platforms that prefer it are covered in locked tunnels.


Frequently asked questions#

How do I reach devices that cannot run any software?

Put one hub machine on the same network and let it front them. The hub runs Localport with a YAML config listing one endpoint per device, each pointing at that device's LAN address, and every device becomes individually reachable by name. The devices themselves are never modified.

Can one mesh mix HTTP, TCP, and TLS devices?

Yes, and most real fleets do. Protocol is per device: a camera on HTTP, a PLC on TCP, and a service with its own certificate on TLS all sit in the same mesh under the same token. Each device gets the addresses its protocol calls for, and the Clients tab shows them together.

Does every device need its own token?

No. One mesh means one token, whether it is used by forty endpoints in a hub config or by forty devices connecting on their own. Copy it once from the connect panel and reuse it, which is what makes provisioning a new site a copy of an existing one. Rotating it replaces the credential everywhere at once.

How do I name a device?

Turn on the Custom Name switch in the connect panel and type the name, which adds --name to the command. On a hub, each endpoints: entry carries its own name: instead. If two devices ask for the same name, the second becomes <name>-2.

How do I run this on a machine with no terminal?

Turn on the No UI switch, which adds --noui and prints plain log lines instead of drawing the live panel, so systemd, Docker, or CI collect the output normally. The agent also detects a missing terminal and switches by itself.

Do the devices need a static IP, a port forward, or a VPN?

None of the three. Every connection is outbound from your network to Localport, so it works behind NAT, CGNAT, a cellular router, or a firewall that permits no inbound traffic at all.

What is the difference between a mesh tunnel and a shared tunnel?

A mesh gives each device its own address, and a request reaches the one device you addressed. A shared tunnel is the inverse: one address, and every connected client receives every request. Use a mesh to reach a fleet individually, and a shared tunnel to deliver the same traffic to several places at once.

How many devices can one mesh hold?

Each connected endpoint counts as one client on the mesh, and the tunnel's client limit is the ceiling. Devices come and go freely beneath it, and one that disconnects stops counting once its 60 second hold expires.

Do I need a custom domain for a mesh tunnel?

No. Without one, devices are addressed on Localport's domain as <device>-<mesh>.<region>.localport.dev, which works identically. A wildcard domain of your own is what makes the addresses readable (plc-line-1.plant.acme.com), which starts to matter the moment more than one person has to operate the fleet. See custom domains.

Can I keep the fleet off the public internet entirely?

Yes. Turn on mTLS for the mesh and no device answers anyone without a client certificate you issued, with per-certificate revocation when a machine or a laptop is lost. Consumers reach a device with localport connect <address> --pem ./cert.pem -p <local port>, which forwards it to a port on their own machine. See locked tunnels.

  • Configuration. The YAML schema a hub runs on, in full.
  • IoT Devices. Rolling out and maintaining fleets in the field.
  • Custom Domains. Put the fleet on a domain you own, with one wildcard record.
  • Locked Tunnels. Require a client certificate before any device is reachable.
  • Shared Tunnels. One address that every connected client receives.
  • CLI. Every flag, option, and exit code.