Llim.run

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:

  1. Open Network and select New tunnel.
  2. 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.
  3. 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.
  4. 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 run
import { 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:

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 staging
import 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:4000

Instances 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 staging

The 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 staging

Its 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:3000

The 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 messageCauseFix
tunnel <name> does not exist; create it in the console (Network) firstNo tunnel in your organization has that name.Create the tunnel, or correct the name.
tunnel <name> is offlineNo 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 30sThe 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 validThe 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 tunnelAnother connector holds the tunnel.Wait for it to stop, or pass --replace to take over.
The connector exits with a message containing was deletedThe tunnel was deleted.Create a new tunnel.

Next steps

network

iOS local services

Selector rules, traffic inspection, and per-instance destination tunnels.

network

Android local services

The Android port and address rules for tunnels.