> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mobileboost.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Test against internal environments

> Let apps on MobileBoost devices reach staging APIs and other services that are not on the public internet

Most staging environments are not reachable from the internet. MobileBoost Local
solves that: you run a small binary inside your network, and apps under test on
MobileBoost devices can reach `staging.acme.internal`, `10.0.5.12:8443`, or a
service on your own laptop as if they were running inside your network.

You do not open a firewall port, and you do not give MobileBoost access to your
network. The binary makes one outbound connection, and every request travels
back down that connection.

## How it works

<Steps>
  <Step title="You start the tunnel">
    `mb-local` runs on a machine that can already reach your internal services:
    your laptop, a CI runner, or a long-lived host in your network. It opens one
    outbound TLS connection on port 443.
  </Step>

  <Step title="You run tests against it">
    Name the tunnel when you trigger a run. MobileBoost gives that test session a
    proxy connected to your tunnel.
  </Step>

  <Step title="The app reaches your services">
    When the app calls `staging.acme.internal`, the request travels down your
    tunnel, and `mb-local` makes the connection from inside your network. The
    response comes back the same way.
  </Step>
</Steps>

Hostnames are resolved on your machine, not ours. Split-horizon DNS, `/etc/hosts`
entries, and VPN resolvers all behave exactly as they do for you locally.

## Quickstart

<Steps>
  <Step title="Install the binary">
    <Tabs>
      <Tab title="macOS">
        ```bash theme={null}
        curl -fsSL https://get.mobileboost.io/mb-local/latest/darwin-arm64 -o mb-local
        chmod +x mb-local
        ```

        Use `darwin-amd64` on Intel Macs. The binary is signed and notarized, so
        macOS runs it without a Gatekeeper prompt.
      </Tab>

      <Tab title="Linux">
        ```bash theme={null}
        curl -fsSL https://get.mobileboost.io/mb-local/latest/linux-amd64 -o mb-local
        chmod +x mb-local
        ```

        Use `linux-arm64` on ARM hosts. The binary is statically linked, so it
        runs on Alpine as well as glibc distributions.
      </Tab>
    </Tabs>

    Checksums for every release are published alongside the binaries at
    `https://get.mobileboost.io/mb-local/latest/SHA256SUMS`:

    ```bash theme={null}
    curl -fsSL https://get.mobileboost.io/mb-local/latest/SHA256SUMS -o SHA256SUMS
    grep ' darwin-arm64$' SHA256SUMS | sed 's| darwin-arm64$| mb-local|' | shasum -a 256 -c -
    ```

    ```
    mb-local: OK
    ```

    The checksums list each file under its release name, so the `sed` rewrites
    that name to whatever you saved the binary as. Substitute your platform's
    name on both sides if you downloaded a different one.

    <Tip>
      In CI, pin a version instead of using `latest`: replace it with a release
      such as `v1.0.0`. Versioned paths never change once published, so a build
      that worked yesterday still works today.
    </Tip>
  </Step>

  <Step title="Start a tunnel">
    ```bash theme={null}
    export MOBILEBOOST_API_KEY=your-api-key

    ./mb-local --only-hosts '*.acme.internal,10.0.0.0/8'
    ```

    This is your ordinary MobileBoost API key, the same one you use for the rest
    of the API. There is no separate tunnel credential to create.

    The tunnel names itself and prints the name you will need:

    ```
      Tunnel ready: patient-garnet-gazelle

      Start a test run with:

          "tunnelName": "patient-garnet-gazelle"
    ```

    The name belongs to the machine and stays the same across restarts, so you
    can leave it in your run configuration. It survives changing networks and
    upgrading the binary; it changes only if you clear `~/.mb-local`. Pass
    `--tunnel-name` to choose your own, which is worth doing for a shared
    tunnel that a whole team references.

    <Note>
      Versions before 0.2.5 derived the name from the hostname, which on a
      laptop changes with the network. Upgrading from one of those gives the
      machine its final name once; pin that one.
    </Note>

    The tunnel stays open until you stop it with `Ctrl+C`.

    <Note>
      Opening a tunnel needs a normal API key. Read-only keys, the kind issued
      for pulling audit data, are refused: a tunnel is a route into your own
      network, which is the opposite of read-only.
    </Note>

    <Note>
      `--only-hosts` is the list of destinations this tunnel is allowed to reach.
      Anything else is refused on your own machine. It is optional, but setting
      it is strongly recommended: see [Choose what the tunnel can reach](#choose-what-the-tunnel-can-reach).
    </Note>
  </Step>

  <Step title="Run tests through the tunnel">
    Pass `tunnelName` to `POST /tests/run`, using the name the binary printed:

    ```json theme={null}
    {
      "organisationId": "org123",
      "uploadId": "BUILD_ID",
      "tags": ["critical"],
      "tunnelName": "patient-garnet-gazelle"
    }
    ```

    Every device in that run is pointed at your tunnel. Requests the app makes to
    hosts covered by your tunnel go through it; everything else uses the device's
    normal internet connection.

    <Note>
      Tunnels are available on the **AI SDET** path, `POST /tests/run`, where
      MobileBoost runs generated test code on its own devices. A tunnel
      terminates on the device host, so the run has to be on a MobileBoost
      device for one to exist.

      `POST /tests/execute` runs on a third-party device cloud that a tunnel
      cannot reach, and rejects `tunnelName` rather than accepting it and
      quietly ignoring it.
    </Note>

    Naming the tunnel is deliberate rather than automatic. An organisation
    routinely has several connected at once, a shared host plus a few laptops
    and CI jobs, and picking one for you could route a pipeline through
    somebody's laptop. A run without a name simply uses no tunnel.
  </Step>
</Steps>

Confirm the tunnel is working before you start a run:

```bash theme={null}
./mb-local status --tunnel-name my-tunnel
```

```
tunnel      my-tunnel (tun_4kd8xz2m9qwerty1)
state       connected, 4/4 links up
policy      allow=*.acme.internal,10.0.0.0/8
sessions    0 attached
traffic     0 dials, 0 errors, 0 denied, 0 up / 0 down bytes
```

## Choose what the tunnel can reach

`--only-hosts` defines the destinations the tunnel serves. Requests to anything
else are refused, and refused on your machine rather than ours.

| Rule                   | Matches                                       |
| ---------------------- | --------------------------------------------- |
| `acme.internal`        | that exact host                               |
| `*.acme.internal`      | any subdomain, but not `acme.internal` itself |
| `.acme.internal`       | `acme.internal` and every subdomain           |
| `10.0.0.0/8`           | any address in that range                     |
| `staging.acme.io:8443` | that host, on that port only                  |

Combine rules with commas, and use `--exclude-hosts` to carve out exceptions.
Exclusions always win.

```bash theme={null}
./mb-local --tunnel-name my-tunnel \
           --only-hosts '.acme.internal,10.0.0.0/8' \
           --exclude-hosts 'secrets.acme.internal'
```

Rules are checked twice: once against the hostname the app asked for, and again
against the address it resolves to. A name that resolves into an excluded range
is still refused.

<Tip>
  If a host is not covered by your rules, the request does not fail. It leaves
  the device over its normal internet connection instead. That means public APIs
  keep working without you listing them.
</Tip>

## Reach a service on your own machine

On a device, `localhost` means the device itself, so it cannot reach a server
running on your laptop.

Point your app at **`mb-local.net`** instead. It is a public DNS name that
resolves to `127.0.0.1`, and `mb-local` maps it back to loopback on your
machine.

```bash theme={null}
# Your dev server on http://localhost:3000 becomes
# http://mb-local.net:3000 from the app's point of view
./mb-local --tunnel-name my-laptop --only-hosts 'mb-local.net:3000'
```

<Warning>
  Requests then arrive with the header `Host: mb-local.net`. Some frameworks
  reject requests whose host header is not `localhost`. In Django, add
  `mb-local.net` to `ALLOWED_HOSTS`; in Rails, add it to `config.hosts`. Vite and
  Next.js accept it by default.
</Warning>

## Where to run the tunnel

<Columns cols={3}>
  <Card title="Your laptop" icon="laptop">
    Best for developing and debugging a test against an environment only you can
    reach. The tunnel lives as long as the terminal.
  </Card>

  <Card title="A CI job" icon="arrows-rotate">
    Best when each pipeline run has its own environment. Start the tunnel in the
    job, run the tests, stop it. See [Local testing in CI](/test-agent/local-testing-ci).
  </Card>

  <Card title="A shared host" icon="server">
    Best for teams. One long-lived tunnel on a host in your network serves every
    engineer and every CI job, and nobody sets up anything locally.
  </Card>
</Columns>

For most teams the shared host is the right answer. It removes tunnel setup from
every pipeline, and it survives laptops going to sleep. See
[Run a shared tunnel](/test-agent/local-testing-ci#run-a-shared-tunnel).

## What MobileBoost can and cannot see

<Note>
  The tunnel carries bytes without decrypting them. Your app's TLS connection
  terminates at your own server, exactly as it would on a real device on your
  network.
</Note>

* **We cannot start a connection into your network.** The relay can only answer
  on the connection your binary opened. Nothing in `mb-local` listens for inbound
  connections.
* **We do not see your traffic.** Requests are relayed at the TCP level, so
  HTTPS stays end to end. Apps that pin certificates work normally.
* **You control the reach.** Your `--only-hosts` rules are enforced by the binary
  on your machine. Your organisation's administrator can also set rules centrally
  that no local flag can widen.
* **Every connection is recorded.** The audit trail records which host and port
  each connection reached, how long it lasted, and how many bytes it moved. It
  never records content.
* **The tunnel dies with the process.** Stop the binary and the access is gone.

Your API key is exchanged for a short-lived token when the tunnel starts, and the
key itself never reaches the machines that relay traffic.

## Limitations

| Limitation                                                                        | What to do                                                                                                         |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Traffic over QUIC (HTTP/3) does not go through the tunnel                         | Most mobile HTTP clients fall back to TCP automatically. If your app forces HTTP/3, disable it for test builds     |
| Some HTTP clients ignore the device proxy, notably Flutter's `dart:io` and Cronet | See [apps that ignore the proxy](/test-agent/local-testing-troubleshooting#the-app-ignores-the-proxy)              |
| Requests made while the tunnel is reconnecting fail                               | Connections already open are dropped when a tunnel reconnects. Normal retry logic in your app or test handles this |
| On iOS, sessions using different tunnels do not run in parallel on the same host  | Android sessions are unaffected. If you need high iOS concurrency, prefer one shared tunnel over many small ones   |

## Next steps

<Columns cols={3}>
  <Card title="Local testing in CI" icon="arrows-rotate" href="/test-agent/local-testing-ci">
    Start and stop tunnels in GitHub Actions, GitLab, Bitrise, and Jenkins.
  </Card>

  <Card title="CLI reference" icon="terminal" href="/test-agent/local-testing-reference">
    Every flag, command, environment variable, and exit code.
  </Card>

  <Card title="Troubleshooting" icon="wrench" href="/test-agent/local-testing-troubleshooting">
    Corporate proxies, TLS inspection, and what to send your network team.
  </Card>
</Columns>
