Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,17 @@ lado. O mesmo vale para os gateways: cada um tem a sua.

| Serviço | Porta interna | Publicada no host |
| :--- | :--- | :--- |
| 9Router | `20128` | `20128` |
| OmniRoute | `20128` | `20129` |
| LiteLLM | `4000` | `20130` |
| 9Router | `20128` | `8081` |
| OmniRoute | `20128` | `8082` |
| LiteLLM | `4000` | `8083` |
| 9RTKSync (painel) | `9090` | `9091` |
| OminiRTkSync (painel) | `9090` | `9092` |
| LiteLlmRTKSync (painel) | `9090` | `9093` |

A stack dos artigos (`claudegravity`) fica com a **`20128`**, a porta padrão do
9Router. As stacks dos repositórios saem dessa faixa de propósito: assim você
roda o artigo e os três sincronizadores ao mesmo tempo, sem conflito.

Tudo preso a `127.0.0.1`: o gateway carrega credenciais reais e não deve ficar
acessível na rede local. Para mudar qualquer uma, altere o lado esquerdo do
mapeamento no compose — o lado direito é a porta interna, que o processo escuta.
Expand All @@ -137,12 +141,13 @@ services:
container_name: omniroute
restart: unless-stopped
ports:
- "127.0.0.1:20128:20128"
# 20128 dentro do container; 8082 no host.
- "127.0.0.1:8082:20128"
environment:
- DATA_DIR=/app/data
- PORT=20128
- HOSTNAME=0.0.0.0
- NEXT_PUBLIC_BASE_URL=http://localhost:20128
- NEXT_PUBLIC_BASE_URL=http://localhost:8082
- NODE_ENV=production
- INITIAL_PASSWORD=${INITIAL_PASSWORD:?defina no .env}
- JWT_SECRET=${JWT_SECRET:?openssl rand -hex 32}
Expand Down
10 changes: 5 additions & 5 deletions docker-compose.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ services:
container_name: omniroute
restart: unless-stopped
ports:
# Porta interna 20128 (padrao do OmniRoute); publicada em 20129 no host.
# O 9Router usa a 20128, e as duas stacks de exemplo colidiam quando
# alguem subia as duas na mesma maquina -- que e o caso de quem compara
# os dois gateways.
- "127.0.0.1:20129:20128"
# Porta interna 20128 (padrao do OmniRoute); publicada em 8082 no host.
# Cada gateway tem a sua porta no host -- 8081 9Router, 8082 OmniRoute,
# 8083 LiteLLM -- para que os tres rodem juntos, inclusive ao lado da
# stack do artigo, que fica com a 20128.
- "127.0.0.1:8082:20128"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the public base URL with the port mapping

When users run docker-compose.example.yml, the gateway is now reachable from the browser only on host port 8082, but NEXT_PUBLIC_BASE_URL remains http://localhost:20128. Any browser-facing URLs or redirects derived from that variable will target a port where this compose publishes nothing; update it to 8082 alongside the mapping, as the README example already does.

Useful? React with 👍 / 👎.

environment:
- DATA_DIR=/app/data
- PORT=20128
Expand Down
1 change: 1 addition & 0 deletions docs/wiki/Home.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ republishes these pages automatically. Editing a page directly here will be over
| [Authentication](Authentication) | Credentials, headless mode, break-glass recovery |
| [Logging](Logging) | Persistent file log, rotation, 30-day retention |
| [Architecture](Architecture) | How the sync engine talks to the OmniRoute database |
| [Remote Access](Remote-Access) | Tunnel, Tailscale, and what has to be on before either |
| [Egress and Multi-Session](Egress-And-Multi-Session) | Why several accounts sharing one outbound address is the risk, and what the gateway models |
| [Troubleshooting](Troubleshooting) | Concrete symptoms and what they actually mean |
| [Upstream Fixes](Upstream-Fixes) | Bugs found in the gateways and the patches sent upstream |
Expand Down
13 changes: 9 additions & 4 deletions docs/wiki/Installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ services:
container_name: omniroute
restart: unless-stopped
ports:
- "127.0.0.1:20128:20128"
# 20128 dentro do container; 8082 no host.
- "127.0.0.1:8082:20128"
environment:
- DATA_DIR=/app/data
- PORT=20128
Expand Down Expand Up @@ -169,12 +170,16 @@ Same for the gateways.

| Service | Inside | Published |
| :--- | :--- | :--- |
| 9Router | `20128` | `20128` |
| OmniRoute | `20128` | `20129` |
| LiteLLM | `4000` | `20130` |
| 9Router | `20128` | `8081` |
| OmniRoute | `20128` | `8082` |
| LiteLLM | `4000` | `8083` |
| 9RTKSync panel | `9090` | `9091` |
| OminiRTkSync panel | `9090` | `9092` |
| LiteLlmRTKSync panel | `9090` | `9093` |

The article stack (`claudegravity`) keeps **`20128`**, 9Router's default port.
The repository stacks stay out of that range on purpose, so you can run the
article and all three synchronizers at once without a conflict.

All bound to `127.0.0.1`: the gateway holds real credentials and should not be
reachable from the local network.
271 changes: 271 additions & 0 deletions docs/wiki/Remote-Access.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
# Remote access: tunnel, Tailscale and what must come first

*(Versão em português ao final.)*

Reaching the gateway from another machine — your laptop away from home, a
teammate, a phone — has three usual answers. They differ in who can reach you,
and the order in which you turn things on decides whether that is safe.

> **This page is about getting IN.** Sending traffic OUT through a chosen
> address, so each account has its own source IP, is a different problem with a
> different answer — see [Egress and Multi-Session](Egress-And-Multi-Session).
> Tailscale appears in both pages doing two unrelated jobs.

---

## Read this before enabling anything

The stacks in this repository ship with:

```yaml
- REQUIRE_API_KEY=false
- REQUIRE_LOGIN=false
```

That is **safe while the port is bound to `127.0.0.1`**, which is how every
compose here publishes it: only your own machine can reach it, and demanding a
password from yourself on localhost adds friction without adding safety.

The moment you expose the gateway, that reasoning inverts. 9Router's dashboard says it in a yellow banner,
and the warning applies here just the same:

> *Enable "Require login" and set a custom password before activating the tunnel.*

Two things make this sharper than it looks:

1. **`/v1` is a public prefix.** In `src/dashboardGuard.js`, the gateway lists its
own inference endpoints — `/v1`, `/v1beta`, `/api/v1`, `/codex`, `/responses` —
as public, because they are meant to be called by your coding tools without a
dashboard session. With `REQUIRE_API_KEY=false` and the gateway on a public URL,
**anyone who learns the URL can spend your accounts**.
2. **The gateway's admin routes fall open too.** Its `/api/settings`, `/api/keys`,
`/api/providers` and the rest of the gateway's admin surface are protected
*only when `requireLogin` is on*.
With it off, an exposed gateway hands over its own configuration.

So the order is not a preference:

```
1. REQUIRE_LOGIN=true + a password you chose
2. REQUIRE_API_KEY=true + a key for your tools
3. only then, the tunnel or Tailscale
Comment on lines +48 to +51

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Wire the security flags through the documented env flow

When an operator follows the repository's documented .env workflow, sets REQUIRE_LOGIN=true and REQUIRE_API_KEY=true, and then starts the public Cloudflare tunnel, those settings have no effect: docker-compose.example.yml passes both variables as literal false rather than expanding the values already provided by .env.example. The gateway consequently remains unauthenticated despite this checklist and becomes publicly reachable; make the compose use ${REQUIRE_LOGIN:-false} and ${REQUIRE_API_KEY:-false} (or explicitly document editing and recreating the service) before recommending exposure.

Useful? React with 👍 / 👎.

```

---

## Option 1 — Cloudflare tunnel (no built-in button here)

OmniRoute has no tunnel button of its own — that is a 9Router feature. To get
the same result, run `cloudflared` yourself against the published port:

```bash
cloudflared tunnel --url http://127.0.0.1:8082
```

It prints a public `https://…trycloudflare.com` address that reaches your
gateway without opening any port on your router.

**When it fits:** you need a URL reachable from anywhere, including devices you
do not control, and you accept that the address is public to whoever has it.

**What to know:**

- The URL is **public**. There is no allow-list — the only thing between the
internet and your accounts is `REQUIRE_LOGIN` and `REQUIRE_API_KEY`.
- The address changes every time the tunnel is re-enabled, unless you bring your
own named Cloudflare tunnel.
- The dashboard blocks the button while login is off — but that gate lives in the
screen. The gateway's own `POST /api/tunnel/enable` does not re-check it, so a script or an
extension can enable the tunnel while login is still off. Set the two flags
first and the question does not arise.

---

## Option 2 — Tailscale (the one to prefer)

Your machine joins your private tailnet and the gateway becomes reachable at a
`100.x.y.z` address, or at a MagicDNS name like `http://your-host:8082`.

**When it fits:** almost always. Only devices you enrolled in your tailnet can
reach the gateway — the address is not public, and there is nothing for a
stranger to find.

**Setting it up by hand**, which is also how you do it for OmniRoute and LiteLLM:

```bash
# 1. On the host that runs the gateway
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up

# 2. Find the address it received
tailscale ip -4 # e.g. 100.101.102.103

# 3. Publish the gateway on the tailnet interface instead of loopback
# (in the compose, replace 127.0.0.1 with the tailnet address)
ports:
- "100.101.102.103:8082:20128"

# 4. From another device already in the tailnet
curl http://100.101.102.103:8082/v1/models
```

Binding to the tailnet address rather than `0.0.0.0` matters: `0.0.0.0` also
exposes the gateway to the local network — the café Wi-Fi, the office VLAN —
which is exactly what you were avoiding.

**MagicDNS** makes this readable: with it on, `http://your-host:8082` works from
any device in the tailnet, and the address survives a change of IP.

---

## Option 3 — your own reverse proxy

A VPS with Caddy or nginx in front, TLS terminated there, Basic Auth or mTLS on
top. More work, and the only option that lets you put your own authentication
layer in front of the gateway instead of relying on its flags.

Worth it when several people share one gateway and you want access logs and
revocation per person — neither of which the gateway's own login gives you.

---

## Which one

| | Tunnel | Tailscale | Reverse proxy |
| :--- | :--- | :--- | :--- |
| Who can reach it | anyone with the URL | only your tailnet | whoever you let through |
| Setup | one command | 3 commands | real work |
| Stable address | no (unless named) | yes (MagicDNS) | yes |
| Built-in button in the dashboard | 9Router only | 9Router only | — |
| Sensible default | for a one-off demo | **for everyday use** | shared or audited setups |

---

## After exposing it, check what you exposed

```bash
# From another device, WITHOUT credentials — both should refuse
curl -si https://<your-address>/v1/models | head -1 # expect 401
curl -si https://<your-address>/ | head -1 # expect 401 or a login page
Comment on lines +148 to +149

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use HTTP when checking the Tailscale endpoint

For users following Option 2, the endpoint created above is explicitly plain http://100.x.y.z:8082, with no TLS terminator. These unconditional https:// probes therefore fail during TLS negotiation instead of returning the expected 401 or login response, which can make a transport failure look like successful authentication protection. Provide an HTTP check for Tailscale and keep HTTPS for the Cloudflare/reverse-proxy cases.

Useful? React with 👍 / 👎.


# The synchronizer panel should not be exposed at all
curl -si https://<your-address>:9092/ | head -1 # expect connection refused
```

The panel of this synchronizer has no reason to leave the machine: it reads the
gateway's database and shows credentials' health. Keep its port on `127.0.0.1`
and reach it through the same tunnel or tailnet you use for everything else.
Comment on lines +155 to +157

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Provide a route to the loopback-bound panel

When a user follows either remote-access setup above, only gateway port 8082 is exposed: the Cloudflare command targets that port, while the Tailscale example rebinds only that mapping. The panel remains published solely on 127.0.0.1:9092, which neither a remote tailnet client nor that quick tunnel can reach, so the instruction to access it through the same tunnel or tailnet is nonfunctional. Document a separate tunnel/SSH/Tailscale Serve route or a tailnet-only panel binding.

Useful? React with 👍 / 👎.


---

# Em português

Alcançar o gateway de outra máquina tem três respostas usuais. Elas diferem em
**quem consegue chegar até você**, e a ordem em que você liga as coisas decide se
isso é seguro.

> **Esta página é sobre entrar.** Fazer o tráfego **sair** por um endereço
> escolhido, para que cada conta tenha o seu IP, é outro problema — veja
> [Egress and Multi-Session](Egress-And-Multi-Session). O Tailscale aparece nas
> duas páginas fazendo trabalhos diferentes.

## Leia antes de ligar qualquer coisa

As stacks deste repositório sobem com `REQUIRE_API_KEY=false` e
`REQUIRE_LOGIN=false`. Isso é **seguro enquanto a porta está presa em
`127.0.0.1`** — só a sua máquina alcança, e exigir senha de si mesmo no
localhost acrescenta atrito sem acrescentar segurança.

No instante em que o gateway é exposto, o raciocínio se inverte. O painel do 9Router avisa, e o aviso
vale igual aqui:

> *Enable "Require login" and set a custom password before activating the tunnel.*

Dois detalhes tornam isso mais sério do que parece:

1. **`/v1` é prefixo público.** Em `src/dashboardGuard.js`, os endpoints de
inferência do gateway (`/v1`, `/v1beta`, `/api/v1`, `/codex`, `/responses`)
são públicos por projeto — é assim que as ferramentas de código chamam o gateway
sem sessão. Com `REQUIRE_API_KEY=false` e o gateway numa URL pública,
**qualquer um que descubra o endereço gasta as suas contas**.
2. **As rotas administrativas do gateway também caem.** As dele — `/api/settings`,
`/api/keys`, `/api/providers` e afins do gateway — só são protegidas **quando `requireLogin` está
ligado**. Com ele desligado, um gateway exposto entrega a própria
configuração.

A ordem, portanto, não é preferência:

```
1. REQUIRE_LOGIN=true + uma senha escolhida por você
2. REQUIRE_API_KEY=true + uma chave para as suas ferramentas
3. só então, o túnel ou o Tailscale
```

## Opção 1 — túnel Cloudflare (nativo do 9Router)

A tela **API Endpoint** tem o botão `Tunnel`. Ele registra um quick tunnel da
Cloudflare e devolve um endereço público `https://…trycloudflare.com` que
alcança o gateway sem abrir porta nenhuma no seu roteador.
Comment on lines +204 to +208

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Replace the Portuguese 9Router tunnel instructions

A Portuguese-speaking OmniRoute user following this option is told to use an API Endpoint screen and Tunnel button that this gateway does not have—the English version correctly states that these controls are 9Router-only and supplies the required cloudflared tunnel --url http://127.0.0.1:8082 command. Because the Portuguese section omits that manual command entirely, its Cloudflare option cannot be followed in this repository; translate the OmniRoute-specific flow instead.

Useful? React with 👍 / 👎.


**Quando serve:** você precisa de uma URL alcançável de qualquer lugar,
inclusive de dispositivos que você não controla, e aceita que o endereço seja
público para quem o tiver.

**O que saber:** a URL é pública e não há lista de permissão — entre a internet
e as suas contas existem apenas `REQUIRE_LOGIN` e `REQUIRE_API_KEY`. O endereço
muda a cada reativação, a menos que você use um túnel nomeado seu. E o bloqueio
do botão enquanto o login está desligado vive **na tela**: o `POST
/api/tunnel/enable` do gateway não reavalia a condição. Ligue as duas variáveis antes e a
questão não se coloca.

## Opção 2 — Tailscale (nativo do 9Router, e o que preferir)

A mesma tela tem o botão `Tailscale`, que instala e conecta o daemon. A sua
máquina entra na sua tailnet e o gateway passa a ser alcançável num endereço
`100.x.y.z`, ou num nome MagicDNS como `http://seu-host:20128`.

**Quando serve:** quase sempre. Só os dispositivos que você cadastrou alcançam o
gateway — o endereço não é público e não há o que um estranho descubra.

**Configurando à mão**, que é também como se faz no OmniRoute e no LiteLLM:

```bash
# 1. No host que roda o gateway
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up

# 2. Descubra o endereço recebido
tailscale ip -4 # ex.: 100.101.102.103

# 3. Publique o gateway na interface da tailnet, em vez do loopback
ports:
- "100.101.102.103:8082:20128"

# 4. De outro dispositivo já na tailnet
curl http://100.101.102.103:8082/v1/models
```

Prender no endereço da tailnet em vez de `0.0.0.0` importa: `0.0.0.0` também
expõe o gateway à rede local — o Wi-Fi do café, a VLAN do escritório — que é
justamente o que se queria evitar.

## Opção 3 — proxy reverso próprio

Um VPS com Caddy ou nginx na frente, TLS terminado ali, Basic Auth ou mTLS por
cima. Dá mais trabalho, e é a única opção que permite colocar a **sua** camada
de autenticação na frente do gateway em vez de depender das flags dele.

Compensa quando várias pessoas dividem um gateway e você quer log de acesso e
revogação por pessoa — coisas que o login do gateway não oferece.

## Depois de expor, confira o que você expôs

```bash
# De outro dispositivo, SEM credencial — os dois têm de recusar
curl -si https://<seu-endereco>/v1/models | head -1 # espera-se 401
curl -si https://<seu-endereco>/ | head -1 # espera-se 401 ou tela de login
```

O painel deste sincronizador não tem motivo para sair da máquina: ele lê o banco
do gateway e mostra a saúde das credenciais. Mantenha a porta dele em
`127.0.0.1` e alcance-o pelo mesmo túnel ou tailnet que você já usa.
1 change: 1 addition & 0 deletions docs/wiki/_Sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [Logging](Logging)
- [Architecture](Architecture)
- [Egress and Multi-Session](Egress-And-Multi-Session)
- [Remote Access](Remote-Access)
- [Troubleshooting](Troubleshooting)
- [Upstream Fixes](Upstream-Fixes)

Expand Down
Loading
Loading