Skip to content
Merged
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3672,6 +3672,9 @@ QUOTA_STORE_DRIVER=sqlite
# src/lib/services/bootstrap.ts when OmniRoute manages the Bifrost sidecar lifecycle.
# Default: 8080.
# BIFROST_PORT=8080
# Port the supervised sing-box embedded service binds to (127.0.0.1:<port>).
# Default: 20140.
# SINGBOX_PORT=20140
# API key for the Bifrost gateway (sent as Authorization: Bearer ...). If
# unset, the route expects the request to carry a valid OmniRoute API key;
# this key is for gateway-side auth only.
Expand Down
1 change: 1 addition & 0 deletions changelog.d/features/12962-singbox-tproxy-sidecar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **feat(services):** sing-box embedded service — a supervised TPROXY sidecar installed from the official SagerNet release (pinned v1.14.1, SHA256-verified per platform; any other version or a checksum mismatch is refused). It gets the full embedded-service surface (8 loopback-only `/api/services/singbox/*` routes, dashboard tab, `SINGBOX_PORT`, seed migration 221). When TPROXY capture is applied, sing-box listens on its own port — never the native IP_TRANSPARENT listener's `onPort` — and every outbound is SO_MARK'd with the capture `bypassMark` the `mangle OUTPUT` rule excludes, so it neither collides with the native listener nor loops its own egress; without a `bypassMark` it is not started, and every failure path logs at error level. Thanks @rqzbeh. (#12962)
62 changes: 47 additions & 15 deletions docs/frameworks/EMBEDDED-SERVICES.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
---
title: "Embedded Services"
description: "Reference for 9Router, CLIProxyAPI, Mux, Bifrost, open-wa, and LLMLingua"
description: "Reference for 9Router, CLIProxyAPI, Mux, Bifrost, open-wa, LLMLingua, and sing-box"
---

# Embedded Services

> **Version:** v3.8.44
> **Last updated:** 2026-09-16
> **Audience:** Engineers adding, maintaining, or debugging embedded services (9Router, CLIProxyAPI, Mux, Bifrost, open-wa, LLMLingua).
> **Last updated:** 2026-10-28
> **Audience:** Engineers adding, maintaining, or debugging embedded services (9Router, CLIProxyAPI, Mux, Bifrost, open-wa, LLMLingua, sing-box).

Embedded services are locally-installed process sidecar tools that OmniRoute installs, supervises, and
exposes as first-class routing targets. Unlike external providers (which are reached over the internet
Expand All @@ -32,19 +32,20 @@ via API keys), embedded services run on the same machine as OmniRoute and commun

### Why embedded services?

Seven services are embedded:
Eight services are embedded:

| Service | npm package | Default port | Purpose |
| --------------- | ---------------------------------- | :----------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **9Router** | `9router` | 20130 | AI router that OmniRoute can use as a sub-provider. Models exposed as `9router/{sub}/{model}` |
| **CLIProxyAPI** | GitHub release binary (`cliproxy`) | 8317 | Local proxy adapter for Anthropic CLI auth flows. Provides fallback routing when OAuth tokens expire |
| **Mux** | `mux` (headless `mux server`) | 8322 | Local agent-orchestration daemon (coder/mux). Lifecycle-managed only — not a routing target (no LLM proxying). |
| **Bifrost** | `@maximhq/bifrost` | 8080 | Go AI-gateway relay backend. When running, auto-selected by the relay route (`/v1/relay/`) |
| **Dario** | `@askalf/dario` | 3456 | Claude-subscription proxy — alternative/failover to CLIProxyAPI for Claude-Code-shaped traffic; the injected key becomes `DARIO_ADMIN_TOKEN` gating its `/admin/*` OAuth control plane |
| **open-wa** | `@open-wa/wa-automate` | 8323 | WhatsApp Web automation (headless Chromium via Puppeteer). Lifecycle-managed only — not a routing target. |
| Service | npm package | Default port | Purpose |
| --------------- | ---------------------------------- | :----------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **9Router** | `9router` | 20130 | AI router that OmniRoute can use as a sub-provider. Models exposed as `9router/{sub}/{model}` |
| **CLIProxyAPI** | GitHub release binary (`cliproxy`) | 8317 | Local proxy adapter for Anthropic CLI auth flows. Provides fallback routing when OAuth tokens expire |
| **Mux** | `mux` (headless `mux server`) | 8322 | Local agent-orchestration daemon (coder/mux). Lifecycle-managed only — not a routing target (no LLM proxying). |
| **Bifrost** | `@maximhq/bifrost` | 8080 | Go AI-gateway relay backend. When running, auto-selected by the relay route (`/v1/relay/`) |
| **Dario** | `@askalf/dario` | 3456 | Claude-subscription proxy — alternative/failover to CLIProxyAPI for Claude-Code-shaped traffic; the injected key becomes `DARIO_ADMIN_TOKEN` gating its `/admin/*` OAuth control plane |
| **open-wa** | `@open-wa/wa-automate` | 8323 | WhatsApp Web automation (headless Chromium via Puppeteer). Lifecycle-managed only — not a routing target. |
| **LLMLingua** | `@atjsh/llmlingua-2` | 20135 | Prompt-compression sidecar — real LLMLingua-2 ONNX model (JS/TS port of Microsoft's algorithm). `open-sse/services/compression/engines/llmlingua/index.ts` dispatches `/compress` over HTTP to it, falling back to the in-process worker-thread backend when the sidecar is down. Lifecycle-managed only — not a routing target. |
| **sing-box** | GitHub release binary (`singbox`) | 20140 | TPROXY sidecar backing `applyTproxy()`'s transparent-proxy interception (`src/mitm/tproxy/setup.ts`). Installer downloads the pinned, checksum-verified release. Lifecycle-managed only — not a routing target (no LLM proxying) |

All seven follow the same supervisory model:
All eight follow the same supervisory model:

- OmniRoute installs them under `DATA_DIR/services/{name}/` (isolated from OmniRoute's own `package.json`)
- OmniRoute spawns and monitors them as child processes
Expand Down Expand Up @@ -154,7 +155,7 @@ All seven follow the same supervisory model:
| `src/lib/services/installers/cliproxy.ts` | npm install/update/uninstall for CLIProxyAPI |
| `src/lib/services/installers/mux.ts` | npm install/update/uninstall for Mux |
| `src/lib/services/installers/openwa.ts` | npm install/update/uninstall for open-wa |
| `src/lib/services/installers/llmlingua.ts` | npm install/update/uninstall for LLMLingua |
| `src/lib/services/installers/llmlingua.ts` | npm install/update/uninstall for LLMLingua |
| `src/app/api/services/9router/_lib.ts` | `getOrInitSupervisor()` helper |
| `src/app/api/services/[name]/logs/route.ts` | Shared SSE logs endpoint |
| `open-sse/executors/ninerouter.ts` | Provider executor (Layer 4) |
Expand Down Expand Up @@ -574,7 +575,38 @@ response) falls back to the in-process worker-thread backend (`./worker.ts`).

---

### 4.8 Reverse proxy (9Router dashboard embed)
### 4.8 sing-box endpoints (8 routes)

sing-box has the same endpoint shape as Mux — no `rotate-key` route, no API key
injection (`needsApiKey: false` in `bootstrap.ts`). It is lifecycle-managed only:
never a routing/LLM provider. `install()` downloads the official GitHub release
binary (`SagerNet/sing-box`) for the pinned version (`SINGBOX_PINNED_VERSION` in
`src/lib/services/installers/singbox.ts`) and verifies its SHA256 against a
checksum baked into the same file before ever writing it to disk — any other
requested version is rejected.

| Method | Path | Description |
| ------ | -------------------------------------------- | --------------------------------------------------------- |
| `POST` | `/api/services/singbox/install` | Download + verify the pinned sing-box release |
| `POST` | `/api/services/singbox/start` | Start sing-box (`sing-box run -c config.json`) |
| `POST` | `/api/services/singbox/stop` | Stop sing-box |
| `POST` | `/api/services/singbox/restart` | Restart sing-box |
| `POST` | `/api/services/singbox/update` | Re-verify/reinstall the pinned release |
| `GET` | `/api/services/singbox/status` | Live + DB status |
| `POST` | `/api/services/singbox/auto-start` | Toggle auto-start |
| `POST` | `/api/services/singbox/auto-restart-adopted` | Toggle auto-restart for an adopted (pre-existing) process |
| `GET` | `/api/services/singbox/logs` | SSE log tail (via shared `[name]/logs` dynamic route) |

**TPROXY wiring:** `ensureSingboxTproxy()` (`src/mitm/tproxy/setup.ts`) writes
`config.json` and starts the supervisor before `applyTproxy()` installs the
TPROXY firewall rules. If the supervisor is missing or fails to reach `running`,
it now logs at error level (rather than silently swallowing the failure) so an
operator can tell the TPROXY rules were applied without a working listener
behind them.

---

### 4.9 Reverse proxy (9Router dashboard embed)

The dashboard embeds the 9Router web UI inside an iframe via an internal reverse
proxy at:
Expand Down
180 changes: 180 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6558,6 +6558,186 @@ paths:
"500":
description: Update failed

/api/services/singbox/install:
post:
tags: [Embedded Services]
summary: Install sing-box
description: >-
Downloads the pinned sing-box release binary from GitHub
(SagerNet/sing-box) and verifies its SHA256 against a checksum baked
into `src/lib/services/installers/singbox.ts` before writing it to
DATA_DIR/services/singbox/. Any version other than the pinned one is
rejected. **LOCAL_ONLY** — loopback only.
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
version:
type: string
default: latest
responses:
"200":
description: Install succeeded
content:
application/json:
schema:
type: object
properties:
ok:
type: boolean
installedVersion:
type: string
"400":
description: Invalid request body, or a version with no pinned checksum
"500":
description: Download failed or checksum verification failed

/api/services/singbox/start:
post:
tags: [Embedded Services]
summary: Start sing-box
description: >-
Spawns `sing-box run -c config.json`. Idempotent if already running.
**LOCAL_ONLY** — loopback only.
responses:
"200":
description: Service started
content:
application/json:
schema:
$ref: "#/components/schemas/ServiceStatus"
"409":
description: sing-box is not installed
"503":
description: Start failed

/api/services/singbox/stop:
post:
tags: [Embedded Services]
summary: Stop sing-box
description: >-
Gracefully stops sing-box. Idempotent.
**LOCAL_ONLY** — loopback only.
responses:
"200":
description: Service stopped
content:
application/json:
schema:
$ref: "#/components/schemas/ServiceStatus"

/api/services/singbox/restart:
post:
tags: [Embedded Services]
summary: Restart sing-box
description: >-
stop() then start() under the operation lock.
**LOCAL_ONLY** — loopback only.
responses:
"200":
description: Service restarted
content:
application/json:
schema:
$ref: "#/components/schemas/ServiceStatus"

/api/services/singbox/update:
post:
tags: [Embedded Services]
summary: Re-verify/reinstall the pinned sing-box release
description: >-
Stops, re-downloads and checksum-verifies the pinned release, restarts.
**LOCAL_ONLY** — loopback only.
responses:
"200":
description: Update succeeded
content:
application/json:
schema:
type: object
properties:
ok:
type: boolean
installedVersion:
type: string
"500":
description: Update failed

/api/services/singbox/status:
get:
tags: [Embedded Services]
summary: Get sing-box status
description: >-
Returns live supervisor state and DB metadata.
**LOCAL_ONLY** — loopback only.
responses:
"200":
description: Status response
content:
application/json:
schema:
$ref: "#/components/schemas/ServiceStatus"

/api/services/singbox/auto-start:
post:
tags: [Embedded Services]
summary: Toggle sing-box auto-start
description: >-
When enabled, sing-box starts automatically on the next OmniRoute boot.
**LOCAL_ONLY** — loopback only.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [enabled]
properties:
enabled:
type: boolean
responses:
"200":
description: Auto-start flag updated
content:
application/json:
schema:
type: object
properties:
autoStart:
type: boolean
"400":
description: Invalid request body

/api/services/singbox/auto-restart-adopted:
post:
tags: [Embedded Services]
summary: Toggle sing-box auto-restart-when-adopted
description: >-
When enabled, an externally-adopted (not OmniRoute-spawned) sing-box
process is restarted under OmniRoute's own supervisor on the next
health-check cycle instead of being left as adopted-only.
**LOCAL_ONLY** — loopback only.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [enabled]
properties:
enabled:
type: boolean
responses:
"204":
description: Flag updated
"400":
description: Invalid request body
"500":
description: Update failed

/api/services/{name}/logs:
get:
tags: [Embedded Services]
Expand Down
Loading
Loading