feat: acesso remoto documentado e cada gateway na sua porta no host - #7
Conversation
**Portas.** O claudegravity dos artigos ocupa a 20128, que e a porta padrao do 9Router. As stacks dos repositorios saem dessa faixa para que tudo rode junto: 9Router em 8081, OmniRoute em 8082, LiteLLM em 8083; os paineis seguem em 9091, 9092 e 9093. Os exemplos de compose embutidos na documentacao foram sincronizados -- um exemplo que diverge do arquivo real e pior que exemplo nenhum, porque o leitor copia e fica com duas verdades. Novo teste: toda porta citada na documentacao tem de existir num compose deste repositorio. Verificado introduzindo 127.0.0.1:9999 numa copia. **Acesso remoto.** Pagina nova na wiki, Remote-Access, cobrindo tunel Cloudflare, Tailscale e proxy reverso -- com a ordem que importa: 1. exigir login e definir senha propria 2. exigir chave de API 3. so entao expor A ordem nao e preferencia. As stacks sobem com REQUIRE_LOGIN=false e REQUIRE_API_KEY=false, o que e seguro enquanto a porta esta presa em 127.0.0.1 -- e deixa de ser no instante em que o gateway ganha um endereco publico: - `/v1` e prefixo publico por projeto no 9Router (src/dashboardGuard.js), para que as ferramentas de codigo chamem o gateway sem sessao. Com chave de API desligada e URL publica, quem descobrir o endereco gasta as contas; - as rotas administrativas do gateway so sao protegidas quando requireLogin esta ligado. Com ele desligado, um gateway exposto entrega a propria configuracao; - o bloqueio do botao de tunel enquanto o login esta desligado vive na tela: o endpoint nao reavalia a condicao. A pagina tambem separa duas coisas que o Tailscale faz e que se confundem: ENTRAR (alcancar o gateway de fora, aqui) e SAIR (cada conta pelo seu endereco, em Egress-And-Multi-Session). Versoes proprias para OmniRoute e LiteLLM, que nao tem botao nativo: o caminho e cloudflared ou tailscale a mao, e no LiteLLM a protecao e a master key, nao uma flag de login.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1865598959
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # Porta interna 20128 (padrao do 9Router); publicada em 8081 no host. | ||
| # A 20128 do host fica para a stack do artigo (claudegravity), que usa | ||
| # a porta padrao -- assim as duas rodam juntas sem colidir. | ||
| - "127.0.0.1:8081:20128" |
There was a problem hiding this comment.
Update host-facing URLs to use the new port
When this compose is used from the host, the gateway now listens on 8081, but NEXT_PUBLIC_BASE_URL immediately below and the repository's local ROUTER_URL defaults still point to 127.0.0.1:20128. Consequently, client-facing URLs generated by 9Router and locally run 9RTKSync diagnostics can target the article stack or an unused port instead of this gateway; migrate the host-facing defaults to 8081 while retaining http://9router:20128 for container-to-container traffic.
Useful? React with 👍 / 👎.
| # 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:20128:20128" | ||
|
|
There was a problem hiding this comment.
Preserve the assigned host port in the Tailscale recipe
When multiple documented gateways run on the same host, this recipe assigns them all the shown Tailnet port 20128, despite the commit allocating 8081, 8082, and 8083 specifically so they coexist. Following the stated “same recipe” for 9Router, OmniRoute, and LiteLLM therefore creates port conflicts on the Tailnet address; only the bind address should change here, while each gateway keeps its allocated host port (for this stack, 100.101.102.103:8081:20128) and matching client URL.
Useful? React with 👍 / 👎.
| # The synchronizer panel should not be exposed at all | ||
| curl -si https://<your-address>:9091/ | head -1 # expect connection refused | ||
| ``` |
There was a problem hiding this comment.
Check the panel over its actual HTTP protocol
If the panel is accidentally reachable on port 9091, this HTTPS request still fails because the embedded dashboard serves plain HTTP, so the check cannot distinguish an exposed panel from a closed port; piping into head also hides curl's failure status. A user can therefore receive the expected-looking failure while the credential dashboard remains publicly reachable over http://<address>:9091; probe the actual HTTP endpoint or perform an explicit TCP-port check.
Useful? React with 👍 / 👎.
| import unittest | ||
|
|
||
| RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | ||
| RX_PORTA = re.compile(r'-\s*"127\.0\.0\.1:(\d+):(\d+)"') |
There was a problem hiding this comment.
Validate non-loopback documentation mappings too
The new consistency test hard-codes 127.0.0.1, so it silently ignores every remote-access mapping, including the newly added 100.101.102.103:20128:20128 example that already disagrees with the compose's 8081:20128 host mapping. Thus the test does not enforce its stated invariant precisely where this change introduces alternate bind addresses; parse arbitrary bind addresses and compare the published/internal port pair.
Useful? React with 👍 / 👎.
| # Porta interna 20128 (padrao do 9Router); publicada em 8081 no host. | ||
| # A 20128 do host fica para a stack do artigo (claudegravity), que usa | ||
| # a porta padrao -- assim as duas rodam juntas sem colidir. | ||
| - "127.0.0.1:8081:20128" |
There was a problem hiding this comment.
Update host-facing URLs to use the new port
When this compose is used from the host, the gateway now listens on 8081, but NEXT_PUBLIC_BASE_URL immediately below and the repository's local ROUTER_URL defaults still point to 127.0.0.1:20128. Consequently, client-facing URLs generated by 9Router and locally run 9RTKSync diagnostics can target the article stack or an unused port instead of this gateway; migrate the host-facing defaults/docs with the port change (while retaining the service URL inside Compose).
Useful? React with 👍 / 👎.
| # The synchronizer panel should not be exposed at all | ||
| curl -si https://<your-address>:9091/ | head -1 # expect connection refused |
There was a problem hiding this comment.
Check the panel over its actual HTTP protocol
If the panel is accidentally exposed on port 9091, this HTTPS probe still fails because the synchronizer serves plain HTTP there, so it cannot distinguish an exposed panel from a refused connection; piping through head also hides curl's failing exit status. In the exact misconfiguration this safety check is meant to detect, a reader can therefore conclude the panel is private while http://<address>:9091/ remains reachable. Probe the panel with HTTP or perform an explicit TCP check.
Useful? React with 👍 / 👎.
| import unittest | ||
|
|
||
| RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | ||
| RX_PORTA = re.compile(r'-\s*"127\.0\.0\.1:(\d+):(\d+)"') |
There was a problem hiding this comment.
Validate non-loopback mappings too
The new consistency test hard-codes 127.0.0.1, so it ignores every mapping that uses a tailnet or other interface address—including the newly added 100.101.102.103:20128:20128 examples. That allows the inconsistent host ports in this same commit, and future remote-access examples, to pass despite the test's stated guarantee that every documented mapping matches a compose. Parse the address independently and compare the host/internal port pair for non-loopback mappings as well.
Useful? React with 👍 / 👎.
| 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. |
There was a problem hiding this comment.
Provide a route before promising remote panel access
With the panel mapping kept on 127.0.0.1, a remote tailnet client cannot connect to <tailnet-ip>:9091, and 9Router's built-in Cloudflare tunnel forwards to the gateway rather than this separate synchronizer service. Thus neither setup documented above makes the panel reachable as this sentence promises. Document a separate SSH/local port forward, Tailscale Serve or tunnel route, or bind the panel specifically to the tailnet address.
Useful? React with 👍 / 👎.
| **Setting it up by hand**, which is also how you do it for OmniRoute and LiteLLM: | ||
|
|
There was a problem hiding this comment.
Document LiteLLM authentication before exposing it
The page explicitly directs OmniRoute and LiteLLM users to this remote-access recipe, but the mandatory authentication steps above only name 9Router's REQUIRE_LOGIN and REQUIRE_API_KEY flags. LiteLLM does not use those flags and must instead be protected with its master key; because the page never mentions or configures that prerequisite, a LiteLLM user following the advertised recipe can publish an unauthenticated proxy backed by paid accounts. Add gateway-specific authentication steps before the shared Tailscale instructions.
Useful? React with 👍 / 👎.
| contexto = "".join(linhas[max(0, n - 3):n + 1]) | ||
| if RX_ROTA_DO_GATEWAY.search(contexto): | ||
| continue |
There was a problem hiding this comment.
Restrict gateway context to the actual route statement
This rolling four-line context suppresses validation for unrelated project routes whenever a nearby line mentions the gateway. In the current README, the gateway diagnostic at line 261 causes /api/change-password, /api/sync, and /api/cron-run on the following lines to be skipped, so deleting any of those handlers would no longer fail the documentation test. Determine ownership from the route's actual sentence or a real paragraph/block rather than an arbitrary neighboring-line window.
Useful? React with 👍 / 👎.
Portas
O
claudegravitydos artigos ocupa a 20128, porta padrão do 9Router. As stacks dos repositórios saem dessa faixa para que tudo rode junto na mesma máquina:2012880812012880824000808390909091/9092/9093Os exemplos de compose embutidos na documentação foram sincronizados — um exemplo que diverge do arquivo real é pior que exemplo nenhum: o leitor copia e fica com duas verdades.
Novo teste: toda porta citada na documentação tem de existir num compose do repositório. Verificado introduzindo
127.0.0.1:9999numa cópia descartável.Acesso remoto
Página nova na wiki — Remote Access — cobrindo túnel Cloudflare, Tailscale e proxy reverso, com a ordem que importa:
A ordem não é preferência. As stacks sobem com
REQUIRE_LOGIN=falseeREQUIRE_API_KEY=false, o que é seguro enquanto a porta está presa em127.0.0.1— e deixa de ser no instante em que o gateway ganha endereço público:/v1é prefixo público por projeto no 9Router (src/dashboardGuard.js), para que as ferramentas de código chamem o gateway sem sessão. Com chave de API desligada e URL pública, quem descobrir o endereço gasta as contas;requireLoginestá ligado. Com ele desligado, um gateway exposto entrega a própria configuração;A página separa duas coisas que o Tailscale faz e que se confundem: entrar (alcançar o gateway de fora — esta página) e sair (cada conta pelo seu endereço —
Egress and Multi-Session).Versões próprias para OmniRoute e LiteLLM, que não têm botão nativo: o caminho é
cloudflaredoutailscaleà mão, e no LiteLLM a proteção é a master key, não uma flag de login.https://claude.ai/code/session_01Dw2Zc66wvY8QBZJmPBeMsT