# HTTP tunnel

Starting with version **0.36.0**, tuna shows a [full-screen interface](https://tuna.am/en/docs/tunnels/tui.md) in the terminal with a live request feed, request details and replay.

An HTTP tunnel exposes a website, API, GraphQL or WebSocket server running locally to the internet. Tunnels work on all plans with no time limit. The free plan includes a dynamic address and one [free permanent subdomain](#free-subdomain).

## Quick start

```shell
tuna http 8080

```

The console shows a public HTTPS address like `https://4l7mqf-212-49-103-2.ru.tuna.am` — requests to it reach the application on `localhost:8080`. Next to it is a link to the [request inspector](https://tuna.am/en/docs/tunnels/http/inspect.md), where you can see every request and response.

note

All flags with hints are in the built-in help:

```shell
tuna http --help

```

Every flag has an [environment variable](https://tuna.am/en/docs/guides/environment-variables.md) — see the [flag reference](#flags).

## Common recipes

Flags can be freely combined. A few typical tasks:

| Task                                                                | Command                                                                                              |
| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| Show a website to a client behind a password at a permanent address | `tuna http 3000 --subdomain=brave-otter-4821 --basic-auth="client:s3cret"`                           |
| Accept GitLab webhooks only with a valid signature                  | `tuna http 8080 --subdomain=brave-otter-4821 --verify-webhook=gitlab --verify-webhook-secret=s3cret` |
| Expose an API to a frontend on another domain                       | `tuna http 8080 --cors --key-auth="dev-key"`                                                         |
| Check a layout on a phone                                           | `tuna http 5173 --qr`                                                                                |
| Share a folder with a colleague                                     | `tuna http --file-server ./share --basic-auth="team:s3cret"`                                         |
| Expose a service that checks the `Host` header                      | `tuna http 11434 --request-header="host:localhost:11434"`                                            |
| Publish a built SPA (React, Vue)                                    | `tuna http --file-server-spa ./dist`                                                                 |

In Docker, CI and systemd it is easier to pass the same settings as environment variables:

```shell
TUNA_TOKEN=tt_*** TUNA_SUBDOMAIN=brave-otter-4821 TUNA_INSPECT=false tuna http 8080

```

## What you can publish

The first argument of `tuna http` is the address of the application to expose:

| Argument                          | Where the request goes                                             |
| --------------------------------- | ------------------------------------------------------------------ |
| `8080`                            | `http://localhost:8080`                                            |
| `localhost:8080`, `10.0.0.1:8080` | the given host and port, including another machine on your network |
| `https://localhost:443`           | an application that serves HTTPS itself                            |
| `unix:///run/gunicorn.sock`       | an application on a unix socket                                    |

Instead of an address you can serve a directory with a built-in server: [`--file-server`](#file-server), [`--file-server-spa`](#spa) or [`--webdav`](#webdav).

### HTTPS application

```shell
tuna http https://localhost:443

```

By default the application's certificate is not verified, so a self-signed one works too. To enable verification, use `--tls-skip-verify=false`.

### Unix socket

If the application accepts HTTP on a unix socket, like gunicorn does, pass the socket path with the `unix://` prefix:

```shell
tuna http unix:///run/gunicorn.sock

```

The [inspector](https://tuna.am/en/docs/tunnels/http/inspect.md), request replay and [traffic policies](https://tuna.am/en/docs/traffic-policy.md) work the same way as with a port. TLS to an application behind a socket is not supported, and `--tls-skip-verify` does not apply to it. If the application does not respond, the 502 error page shows the socket path (`unix:///run/gunicorn.sock`) instead of `localhost` with a port.

Path requirements, permissions and the behavior when the socket is missing are the same as for a [TCP tunnel](https://tuna.am/en/docs/tunnels/tcp.md#unix-socket).

### File server

Serves files from a directory and shows a file listing:

```shell
tuna http --file-server ./

```

### Single page application (SPA)

For built [single page applications](https://en.wikipedia.org/wiki/Single-page_application) (React, Vue, Angular): any non-existent path returns `index.html`, so client-side routing works.

```shell
tuna http --file-server-spa ./dist

```

### WebDAV

The directory can be mounted as a network drive over WebDAV, with read and write access:

```shell
tuna http --webdav ./

```

warning

The file server and WebDAV expose the directory to the whole internet. Protect it with a [password](#basic-auth) or a [key](#key-auth).

## Tunnel address

| Address                                       | How to get it                  | Plan                                     |
| --------------------------------------------- | ------------------------------ | ---------------------------------------- |
| Dynamic, `4l7mqf-212-49-103-2.ru.tuna.am`     | no flags                       | any                                      |
| Free permanent, `brave-otter-4821.ru.tuna.am` | `--subdomain=<your-subdomain>` | any, one per account                     |
| Any free subdomain, `billing.ru.tuna.am`      | `--subdomain=billing`          | [subscription](https://tuna.am/#pricing) |
| Custom domain, `api.example.com`              | `--domain=api.example.com`     | [subscription](https://tuna.am/#pricing) |

Abuse protection

On the free plan, a browser visitor sees a warning page on the first visit saying that the site is served through tuna. It does not affect API clients and webhooks, and it can be skipped with the `tuna-skip-browser-warning` header. Why this is needed and how we fight phishing and malicious content in general is described on the [Abuse and Complaints](https://tuna.am/en/docs/abuse.md) page.

### Dynamic address

Assigned automatically on every start and changes when the tunnel reconnects. The address follows the `<random-part>-<your-IP>.<location>.tuna.am` scheme: the source IP address is visible in the link, so malicious content cannot be hosted anonymously — see [Anonymity](https://tuna.am/en/docs/abuse.md#anonymity). The permanent subdomain does not contain your IP address. Good for a one-off demo or debugging. If you need an address that does not change — for webhooks, payment callbacks, a browser bookmark — use a permanent subdomain.

### Free permanent subdomain

Every user gets one permanent subdomain — for free and on any plan. The name is assigned randomly in the `<adjective>-<noun>-<4 digits>` format, for example `brave-otter-4821`, and is the same in all locations: `brave-otter-4821.ru.tuna.am` and `brave-otter-4821.nl.tuna.am`.

You can find your subdomain in the [dashboard](https://my.tuna.am/domains): it is labeled "Free" and comes with a ready-to-use launch command. The client also suggests it when you start a tunnel without flags:

```shell
Your permanent free domain: brave-otter-4821.ru.tuna.am. Use it with --subdomain=brave-otter-4821

```

By default a tunnel still gets a dynamic address — the permanent subdomain has to be specified explicitly:

```shell
tuna http 8080 --subdomain=brave-otter-4821

```

In another location:

```shell
tuna http 8080 --subdomain=brave-otter-4821 --location=nl

```

Or by the full name:

```shell
tuna http 8080 --domain=brave-otter-4821.ru.tuna.am

```

Token required

The subdomain is bound to your account, so the client must be authorized with your token. Save it once with `tuna login` or `tuna config save-token <TOKEN>` (the token is on a [separate page](https://my.tuna.am/token)), or pass it via the `TUNA_TOKEN` environment variable — for example, in Docker and CI. Without a token, or with another account's token, the subdomain will not work.

* You cannot choose or change the name — a subdomain with any name is available with a [subscription](https://tuna.am/#pricing).
* The subdomain cannot be deleted or transferred to another user, `tuna domain clear` skips it.
* The subdomain is kept when you change plans and does not count towards the subscription's domain limit.
* If you registered earlier, the subdomain appears the first time you open the [Domains](https://my.tuna.am/domains) section or start a tunnel without flags.

### Reserved subdomain

With a subscription you can take any free subdomain — the address stays the same after the tunnel restarts:

```shell
tuna http 8080 --subdomain=billing

```

The same via an [environment variable](https://tuna.am/en/docs/guides/environment-variables.md):

```shell
TUNA_SUBDOMAIN="billing" tuna http 8080

```

The subdomain is reserved automatically on the first start with the flag. All your domains are in the [dashboard](https://my.tuna.am/domains).

Attention

The `Domain already reserved` error means the subdomain in this location is already taken by another user — choose a different name.

### Custom domain

Add **your domain** following the instructions in the [dashboard](https://my.tuna.am/domains), after DNS verification and Let's Encrypt certificate issuance you can use it:

```shell
tuna http 8080 --domain=my-api-project.example.com

```

Step by step — in the article [Connecting your own domain](https://tuna.am/en/docs/tunnels/guides/connect-self-domain.md).

### Location

The tunnel is created in the `ru` [location](https://tuna.am/en/docs/tunnels/guides/locations.md) unless specified otherwise. Pick the one closest to your site's users:

```shell
tuna http 8080 --location=nl

```

### QR code

The `--qr` flag prints a QR code with the tunnel link in the console — handy for opening the site on a phone.

```shell
tuna http 5173 --qr

```

![](/docs/img/examples/qr.png)

## Access protection

The tunnel is reachable from the whole internet. The checks below run in the tuna client — a request that fails them never reaches the application.

| Flag                                     | Who gets through                            |
| ---------------------------------------- | ------------------------------------------- |
| [`--basic-auth`](#basic-auth)            | those who know the login and password       |
| [`--key-auth`](#key-auth)                | requests with a key in the `X-Token` header |
| [`--cidr-allow`, `--cidr-deny`](#cidr)   | requests from allowed subnets               |
| [`--ua-allow`, `--ua-deny`](#user-agent) | requests with a matching `User-Agent`       |
| [`--verify-webhook`](#verify-webhook)    | webhooks with a valid signature             |
| [`--rate-limit`](#rate-limit)            | no more than N requests per second          |
| [`--https-redirect`](#https-redirect)    | HTTPS only, HTTP is redirected              |

List flags (`--basic-auth`, `--key-auth`, `--cidr-*`, `--ua-*`, `--*-header`) can be repeated. In environment variables, separate values with commas.

### Password (basic auth)

The browser shows a login and password prompt:

```shell
tuna http 8080 --basic-auth="login:password"

```

Several users — repeat the flag:

```shell
tuna http 8080 --basic-auth="anna:pass1" --basic-auth="ivan:pass2"

```

From a script:

```shell
curl -u login:password https://brave-otter-4821.ru.tuna.am/

```

### Key in a header (API token)

For APIs and services without a browser. A request gets through only if the `X-Token` header contains one of the keys:

```shell
tuna http 8080 --key-auth="my-secret-key"

```

```shell
curl -H "X-Token: my-secret-key" https://brave-otter-4821.ru.tuna.am/

```

### Access by IP subnet

An allow list of [subnets](https://en.wikipedia.org/wiki/Subnet) in CIDR format — everyone else is denied:

```shell
tuna http 8080 --cidr-allow="203.0.113.0/24" --cidr-allow="198.51.100.7/32"

```

Or, the other way around, deny specific subnets:

```shell
tuna http 8080 --cidr-deny="10.0.0.1/32"

```

### Access by User-Agent

Matching is by substring. Deny `curl` and allow everyone else:

```shell
tuna http 8080 --ua-deny=curl

```

Allow only Chrome:

```shell
tuna http 8080 --ua-allow=Chrome

```

note

`User-Agent` is easy to fake — use this filter against bots and random scanners, not as protection.

### Webhook signature verification

tuna lets through only webhooks signed with the secret you set at the provider. Other requests get `401 Unauthorized`.

```shell
tuna http 8080 --verify-webhook=gitlab --verify-webhook-secret=1234

```

Supported providers: `gitlab`, `github`, `sentry`, `linear`. For other services and more complex logic, use [traffic policies](https://tuna.am/en/docs/traffic-policy.md).

### Request rate limit

Protects the local application from overload — no more than N requests per second:

```shell
tuna http 8080 --rate-limit=2

```

When the limit is exceeded, the client gets `429 Too Many Requests`.

### HTTPS only

By default the tunnel answers over both HTTP and HTTPS. With `--https-redirect`, HTTP requests are redirected to HTTPS:

```shell
tuna http 8080 --https-redirect

```

## Headers and CORS

### Request headers

Add or replace a header before the request reaches the application. A common case is a service that only accepts requests with its "own" `Host`, like Ollama or dev servers:

```shell
tuna http 11434 --request-header="host:localhost:11434"

```

### Response headers

Add or replace a header in the response to the client:

```shell
tuna http 8080 --response-header="env:test" --response-header="x-robots-tag:noindex"

```

### CORS

The `--cors` flag or the `TUNA_CORS=true` variable adds CORS headers to all responses. By default the following headers are set:

```shell
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Accept, Accept-Language, Content-Language, Origin
Access-Control-Allow-Methods: GET, HEAD, POST
Access-Control-Allow-Origin: *

```

They can be overridden with the [`--response-header`](#response-header) flag.

**OPTIONS** requests are answered automatically with `200 OK` without reaching the proxied server.

note

If you have trouble with CORS, we recommend reading [this article](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). We also recommend [CORS Tester](https://corsfix.com/tools/cors-tester) for checking your CORS headers.

## More features

### Traffic policies

When flags are not enough — path routing, conditions, custom responses — describe the rules in a [traffic policy](https://tuna.am/en/docs/traffic-policy.md) file. By default `.tuna.yml` from the current directory is used, another path can be set with `--policy-file`. The `--policy-url` flag loads the rules from a [URL](https://tuna.am/en/docs/traffic-policy.md#policy-url). A policy error stops the tunnel from starting, see [Policy errors](https://tuna.am/en/docs/traffic-policy.md#policy-errors).

### Request inspector

Enabled by default at `http://127.0.0.1:4040`. The inspector keeps requests in memory, so on servers and embedded devices it is better to turn it off: `--inspect=false`. More on the [inspector](https://tuna.am/en/docs/tunnels/http/inspect.md) page.

### Bug reports

The `--capture-key` flag embeds the [reports](https://tuna.am/en/docs/reports.md) widget into HTML pages: a tester can send a bug with a screenshot without any changes to the site's code.

```shell
tuna http 3000 --capture-key=YOUR_CAPTURE_KEY

```

### Token

Usually the token is saved once with `tuna login`. You can specify another token with the `--token` flag or the `TUNA_TOKEN` variable, overriding follows the [configuration ordering](https://tuna.am/en/docs/guides/config-ordering.md):

```shell
tuna http 8080 --token=tt_***

```

## Flag reference

| Flag                      | Environment variable         | Description                                                                            |
| ------------------------- | ---------------------------- | -------------------------------------------------------------------------------------- |
| `-s`, `--subdomain`       | `TUNA_SUBDOMAIN`             | [Subdomain](#address) in the tuna.am zone                                              |
| `-d`, `--domain`          | `TUNA_DOMAIN`                | [Full domain name](#custom-domain)                                                     |
| `-l`, `--location`        | `TUNA_LOCATION`              | [Location](#location): `ru` (default), `nl`                                            |
| `-f`, `--file-server`     | `TUNA_FILE_SERVER`           | [Serve a directory](#file-server)                                                      |
| `-F`, `--file-server-spa` | `TUNA_FILE_SERVER_SPA`       | [Serve an SPA](#spa)                                                                   |
| `--webdav`                | `TUNA_WEBDAV`                | [WebDAV server](#webdav) for a directory                                               |
| `--basic-auth`            | `TUNA_BASIC_AUTH`            | [Login and password](#basic-auth), `login:password`                                    |
| `--key-auth`              | `TUNA_KEY_AUTH`              | [Key](#key-auth) in the `X-Token` header                                               |
| `--cidr-allow`            | `TUNA_CIDR_ALLOW`            | [Allowed subnets](#cidr)                                                               |
| `--cidr-deny`             | `TUNA_CIDR_DENY`             | [Denied subnets](#cidr)                                                                |
| `--ua-allow`              | `TUNA_UA_ALLOW`              | [Allowed User-Agents](#user-agent)                                                     |
| `--ua-deny`               | `TUNA_UA_DENY`               | [Denied User-Agents](#user-agent)                                                      |
| `--verify-webhook`        | `TUNA_VERIFY_WEBHOOK`        | [Webhook provider](#verify-webhook)                                                    |
| `--verify-webhook-secret` | `TUNA_VERIFY_WEBHOOK_SECRET` | Webhook signing secret                                                                 |
| `--rate-limit`            | `TUNA_RATE_LIMIT`            | [Requests per second](#rate-limit)                                                     |
| `--https-redirect`        | `TUNA_HTTPS_REDIRECT`        | [Redirect HTTP to HTTPS](#https-redirect)                                              |
| `--request-header`        | `TUNA_REQUEST_HEADER`        | [Request header](#request-header), `key:value`                                         |
| `--response-header`       | `TUNA_RESPONSE_HEADER`       | [Response header](#response-header), `key:value`                                       |
| `--cors`                  | `TUNA_CORS`                  | [CORS headers](#cors) and `OPTIONS` handling                                           |
| `--policy-file`           | `TUNA_POLICY_FILE`           | [Policy file](#traffic-policy), `.tuna.yml` by default                                 |
| `--policy-url`            | `TUNA_POLICY_URL`            | [Policy URL](#traffic-policy), https only                                              |
| `--inspect`               | `TUNA_INSPECT`               | [Inspector](#inspect), enabled by default                                              |
| `--tls-skip-verify`       | `TUNA_TLS_SKIP_VERIFY`       | [Skip certificate verification](#https-upstream) of the application, `true` by default |
| `--qr`                    | `TUNA_QR_CODE`               | [QR code](#qr) with the link                                                           |
| `--capture-key`           | `TUNA_CAPTURE_KEY`           | [Reports widget](#capture-key)                                                         |
| `--token`                 | `TUNA_TOKEN`                 | Authorization [token](#token)                                                          |

## Behavior

### Hop-by-hop

**NOT** supported are hop-by-hop headers except for the `Connection: upgrade` header, which is necessary for Websocket connections.

### WebSocket

Supports the protocol with bidirectional connection between client (e.g., browser) and server, no additional settings are required. The transmitted data will **NOT** be displayed in the [inspector](https://tuna.am/en/docs/tunnels/http/inspect.md).

### Server-sent events

Supports the technology of sending notifications from the server to the web browser, no additional settings are required. The transmitted data will **NOT** be displayed in the [inspector](https://tuna.am/en/docs/tunnels/http/inspect.md).

note

With the inspector enabled, it's important that the client sends the `Accept: text/event-stream` header, otherwise the inspector will try to intercept the request and this will cause delays. Browsers send this header by default.

### HTTP and HTTPS

When starting the tunnel, an HTTPS link is displayed, but you can also access it via HTTP. You can prohibit HTTP access by adding the `--https-redirect` flag.

### Flag and rule processing order

Flags affecting traffic processing behavior are always executed in a specific sequence, regardless of the order they are passed in the arguments.

1. [https-redirect](#https-redirect)
2. [cors](#cors)
3. [cidr-allow](#cidr)
4. [cidr-deny](#cidr)
5. [ua-allow](#user-agent)
6. [ua-deny](#user-agent)
7. [rate-limit](#rate-limit)
8. [key-auth](#key-auth)
9. [basic-auth](#basic-auth)
10. [policy-file / policy-url](#traffic-policy)
11. [verify-webhook](#verify-webhook)
12. [request-header](#request-header)
13. [response-header](#response-header)

### Managing CORS headers yourself

If you want to manage CORS headers yourself on the downstream server and at the same time use authorization in tuna with `--basic-auth` or `--key-auth`. Then [**Preflight requests**](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request) - **will not work**, because these *OPTIONS* requests do not contain `Authorization`, `X-Token` and any other specific headers.

Solution to choose:

1. Do not use authorization in tuna.
2. Manage CORS headers in tuna, not yourself.
3. Handle `OPTIONS` and authorization separately using more flexible [traffic policies](https://tuna.am/en/docs/traffic-policy.md).

❓ How it looks on a diagram

#### Case 1

You manage CORS on the downstream Caddy server and enable authorization in tuna.

![Diagram depicting Case 1](/docs/img/tunnels/cors/case1.png)

#### Case 2

You manage CORS on the downstream Caddy server and **DO NOT** enable authorization in tuna.

![Diagram depicting Case 2](/docs/img/tunnels/cors/case2.png)

#### Case 3

You manage CORS and authorization in tuna.

![Diagram depicting Case 3](/docs/img/tunnels/cors/case3.png)
