Connect instances to your network with a persistent tunnel
A persistent tunnel connects instances to services in your private network. One connector on a machine in your network serves every iOS simulator and Android emulator that names the tunnel. CI jobs and coding agents create instances with --tunnel <name>, and PR preview links name it with a tunnel parameter. The app inside reaches the destinations you declared, such as localhost:3000 or *.internal.example.com, at the addresses it already uses. To route a single instance through the machine you are working on instead, start a destination tunnel; see Connect an iOS simulator to local services.
An organization admin sets up the tunnel once: create it, then keep its connector running. Organization members and admins can then name it when they create instances.
Create a tunnel
Organization admins create tunnels in the console:
- Open Network and select New tunnel.
- Enter a Name. Instances refer to the tunnel by this name, and it cannot be changed later. Use lowercase letters, digits, and dashes, at most 63 characters, starting and ending with a letter or digit.
- Under Selectors, enter the destinations instances reach through the connector, one per line. Selectors take the same forms as on a destination tunnel; see Choose selectors.
- Choose when the token expires under Token expires in, then select Create tunnel.
The dialog then shows the connector command, lim tunnel run --token <token>. Copy it before you close the dialog: Limrun does not store the token, so this is the only time it is shown. Keep the dialog open while you start the connector; its status changes from Waiting for the connector... to Connected.
Selectors resolve on the connector's machine. localhost:3000 reaches port 3000 on the machine that runs the connector, and a domain resolves through that machine's DNS, /etc/hosts, and VPN.
From a terminal or a pipeline, create the tunnel with lim tunnel create:
lim tunnel create staging --selector localhost:3000 --selector "*.internal.example.com"The command prints the connector command, lim tunnel run --token <token>, the only time the token is shown. --expiration-months sets the token's lifetime, from 1 to 60 months (default 12), and --quiet prints only the token, for example to store it as a CI secret. lim tunnel list shows each tunnel with its connector, selectors, and token expiry; add --json for tunnel IDs.
Run the connector
Run the connector on a machine that reaches every selector, such as a VM inside your network, a CI runner, or a laptop on your VPN. The lim tunnel commands need lim 0.43.0 or later, and runTunnel needs @limrun/api 0.62.0 or later. Keep the connector running for as long as instances need the tunnel:
lim tunnel run --token <token>
# LIM_TUNNEL_TOKEN works in place of --token, for example from a CI secret
lim tunnel runimport { runTunnel } from '@limrun/api';
// The CLI reads both IDs from the token. The organization ID is under
// Settings > Organization in the console; `lim tunnel list --json` shows tunnel IDs.
const connector = runTunnel({
token: process.env['LIM_TUNNEL_TOKEN']!,
baseURL: 'https://api.limrun.com',
organizationId: '<organization-id>',
tunnelId: '<tunnel-id>',
onEvent: (event) => console.log(event),
});
process.once('SIGINT', () => void connector.close());
await connector.closed;The CLI prints Tunnel staging is active. Instances created with --tunnel staging attach here; press Ctrl+C to stop., then one line for each instance it attaches.
The connector works like this:
- The token is its only credential. The connector needs no API key, and an API key cannot run a connector. The token runs this one tunnel and nothing else.
- One connector holds the tunnel at a time. A second connector started with the same token, for example on another machine, waits on standby. It takes over within about a second after the active connector stops, or within about a minute if the active connector crashes or loses its network.
--replacetakes the tunnel over at once. - Reconnects keep instances attached. If the connection to Limrun drops, the connector reconnects, and attached instances keep their tunnels meanwhile.
- Traffic is inspected by default.
--[no-]inspect,--persist,--ttl, and--har-body-limitwork as on a destination tunnel; see Inspect tunnel traffic. The connector does not print requests: open an instance in the console to see them in its network panel.--verboselogs every forwarded connection and dial failure.
Create instances that use the tunnel
Name the tunnel when you create an iOS simulator or an Android emulator:
lim ios create --tunnel staging
lim android create --tunnel stagingimport Limrun from '@limrun/api';
const lim = new Limrun({ apiKey: process.env['LIM_API_KEY'] });
const ios = await lim.iosInstances.create({ wait: true, spec: { tunnel: 'staging' } });
const android = await lim.androidInstances.create({ wait: true, spec: { tunnel: 'staging' } });curl -X POST "https://api.limrun.com/v1/ios_instances?wait=true" \
-H "Authorization: Bearer $LIM_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "spec": { "tunnel": "staging" } }'
curl -X POST "https://api.limrun.com/v1/android_instances?wait=true" \
-H "Authorization: Bearer $LIM_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "spec": { "tunnel": "staging" } }'The Python and Go SDKs do not expose tunnel; send the REST request from those languages.
Creation checks the tunnel first and fails when it does not exist or no connector holds it. The instance becomes ready, and its initial apps install, only after the tunnel attaches. If the tunnel does not attach within 30 seconds, creation fails and Limrun deletes the instance. Once the instance is ready, the app reaches each selector at the address you declared, as on a destination tunnel.
Android instances attach only when every exact selector uses port 1024 or higher. A lower port keeps every Android instance from attaching, while iOS instances are unaffected. The console warns about such ports, and the connector prints a Notice that Android instances refuse the tunnel.
Change the selectors
In the console, hover over the tunnel's row, select the pencil button next to its selectors, edit them, and select Save selectors. From a terminal, pass the complete new list:
lim tunnel update staging --selector localhost:3000 --selector localhost:4000Instances attached after the change get the new selectors; attached instances keep theirs. To apply the change to attached instances, restart the connector.
Rotate the token
If the token may have leaked, rotate it now. Otherwise, rotate it before it expires. In the console, select Rotate in the tunnel's Token column, choose Token expires in, select Rotate token, and copy the new connector command. From a terminal:
lim tunnel rotate stagingThe command prints the new connector command. The old token stops working at once, and connectors running with it exit within about 10 seconds. A tunnel has one token, so restart every connector, standbys included, with the new one.
From 14 days before the token expires, the connector logs a warning once a day and the console flags the token.
Delete a tunnel
Delete a tunnel with the delete button on its row in the console, then confirm with Delete tunnel. From a terminal:
lim tunnel delete stagingIts connector exits, its token stops working, and instances that name it lose access to your network.
Run a quick tunnel
To share a service for one session without creating a tunnel first, run a quick tunnel:
lim tunnel run --name scratch --selector localhost:3000The command creates a tunnel named scratch with its own token, runs its connector, and deletes the tunnel when you stop the command. If the connector crashes, Limrun deletes the tunnel about 10 minutes after the connector stops reaching it. Instances name it with --tunnel scratch like any other tunnel, and the console marks it Quick.
From the API, create the tunnel with "ephemeral": true and a token, then run it with runTunnel; closing the connector deletes the tunnel.
Troubleshooting
| Symptom or message | Cause | Fix |
|---|---|---|
tunnel <name> does not exist; create it in the console (Network) first | No tunnel in your organization has that name. | Create the tunnel, or correct the name. |
tunnel <name> is offline | No connector holds the tunnel. | Start the connector. A connector that started seconds ago can still show as offline; retry. |
tunnel <name> did not attach within 30s | The connector did not attach the instance in time. | Check the connector's output for Retrying and Could not attach lines. On Android, move services on ports below 1024 to higher ports. |
The connector exits with its tunnel token was rotated, revoked or has expired, or the tunnel token was revoked, has expired or is not valid | The token was rotated or revoked, or it expired. | Restart the connector with the current token. If you no longer have it, rotate the token. |
--token takes a tunnel token from the console (Network) or the API; this is not one. | --token or LIM_TUNNEL_TOKEN holds another credential, such as an API key. | Pass the tunnel's token. |
Tunnel <name> runs with its own token (--token). | --name names a tunnel created in the console or the API. | Run it with its token. |
Standby: <host> holds the tunnel | Another connector holds the tunnel. | Wait for it to stop, or pass --replace to take over. |
The connector exits with a message containing was deleted | The tunnel was deleted. | Create a new tunnel. |
Next steps
Was this guide helpful?