English · Русский
Local proxy rotator. One config file defines profiles (sources of free nodes) and their nodes. Traffic starts from Cloudflare WARP (top priority, not a node), then follows the chain down: profiles by profile priority, nodes by node priority, to the first alive node. When one dies, openrot rotates to the next.
Huge thanks to opencode for existing. This project is built around it: openrot keeps a cascade of free nodes alive and serves a loopback bridge so that opencode (or any OpenAI-compatible client) keeps working on free providers, with auto-rotation.
WARP (enabled by default)
│ warp_enabled: true, host-only (skipped in Docker)
▼
Profile A (priority 0) ── node 0, node 1, node 2 ◀─ choose first alive
Profile B (priority 1) ── node 0, node 1
...
Public V2Ray configs and proxy lists die constantly. You add several sources as profiles, and openrot keeps them alive: it fetches them on a schedule, health-checks every node, picks the best one, and rotates when a node or WARP goes down.
Standalone binary (macOS / Linux) — downloads the prebuilt release, installs
the warp-cli + sing-box prerequisites and brings WARP up:
curl -fsSL https://raw.githubusercontent.com/vispar-tech/openrot/main/install.sh | bashDocker (WARP still runs on the host) — installs prerequisites and runs the image from GHCR:
curl -fsSL https://raw.githubusercontent.com/vispar-tech/openrot/main/install-docker.sh | bashThe first method installs the standalone binary to
~/.local/lib/openrot(symlink in~/.local/bin/openrot). If your distro already has the prerequisites, you can skip them:OPENROT_SKIP_WARP=1 ... | bash.
- Python 3.14+, Poetry
- sing-box — external binary, NOT a Python dependency:
- macOS:
brew install sing-box - GitHub Releases
- macOS:
- warp-cli (Cloudflare WARP client) — optional, host-only, unavailable in Docker
poetry install
poetry run openrot --help# 1. add a source profile (a repo of vless:// links or free proxies)
poetry run openrot profile add gitlab https://gitlab.com/igareck/vpn-configs-for-russia/-/raw/main/WHITE-CIDR-RU-all.txt --kind relay --priority 0
# 2. fetch its nodes
poetry run openrot update --relay
# 3. health-check everything
poetry run openrot test
# 4. start the local proxy (listens on 127.0.0.1:7890)
poetry run openrot start cascade
# in another terminal:
curl -x http://127.0.0.1:7890 https://ifconfig.me
# 5. status / rotate / stop
poetry run openrot status
poetry run openrot rotate
poetry run openrot stop| Command | Description |
|---|---|
openrot profile add NAME URL --kind relay|proxy [--priority N] [--interval S] [--disabled] |
Register a source profile |
openrot profile list [--json] |
List profiles |
openrot profile set NAME [--priority N] [--interval S] [--enabled|--disabled] |
Update a profile's priority, refresh interval, state |
openrot profile remove NAME |
Remove a profile and its nodes |
openrot list [--alive] [--json] |
List nodes across profiles |
openrot test [--json] |
Health-check all nodes |
openrot update [--relay] [--proxy] [--name X] |
Fetch nodes for all enabled profiles (default; filter with flags) |
openrot start cascade|bridge [--daemon] |
Start the cascade (foreground = logs stream to the terminal; --daemon = background) or the bridge 429-rotation server (foreground, or --daemon as a background process) |
openrot stop [-y] |
Stop the running stack: WARP or local proxy, plus any bridge daemon |
openrot status [--json] [-v|--verbose] |
Show active level and connectivity (--verbose = full stack diagnostics) |
openrot rotate |
Rotate WARP IP or select the next node |
openrot logs [-n N] [--follow/--no-follow] |
Tail daemon, events and bridge logs in one stream |
openrot config |
Open the config file in $EDITOR (default vim) |
openrot warp on|off|install |
Enable/connect or disable/disconnect WARP |
openrot warp status [--json] |
Show WARP status |
openrot run -- <cmd> |
Run a command with the proxy env vars set |
openrot probe <url> |
Request url through the active stack, showing each stage |
opencode talks to openrot through the loopback bridge — no launcher, no config magic. Bring the bridge up, then point opencode at it by merging one key into its config:
openrot start bridge # cascade + bridge, foreground; Ctrl-C to stop
openrot start bridge --daemon # or run it as a persistent background daemonMerge this into ~/.config/opencode/opencode.json. The override is scoped to
opencode's built-in opencode provider — the one serving the default free
opencode/* models — so those models route through the bridge with 429
self-rotation, and nothing else changes:
{ "provider": { "opencode": { "options": { "baseURL": "http://127.0.0.1:7891/v1" } } } }<bridge_port> defaults to 7891 (config bridge_port). opencode only ever
sees plain loopback HTTP — no TLS, no proxy env vars, nothing to restore; the
bridge itself terminates the upstream leg over TLS through the cascade. A
background bridge daemon is stopped with openrot stop (log:
~/.config/openrot/openrot-bridge.log).
openrot status # bridge: running (http://127.0.0.1:7891/v1)
curl -s http://127.0.0.1:7891/v1/modelsThe proxy tunnel routes opencode's traffic opaquely, so it cannot see the
provider returning an HTTP 429 (the request is already inside a TLS CONNECT
tunnel). To rotate a rate-limited node automatically, point opencode at the
local bridge: a loopback OpenAI-compatible reverse proxy (baseURL) that
forwards each request through the active cascade and watches the upstream
status. On a 429 it rotates the cascade (openrot rotate — next node / WARP)
and retries the request once, transparently.
The upstream endpoint and port are configurable — see the config fields
bridge_port (default 7891) and bridge_upstream (default
https://opencode.ai/zen/v1, the endpoint opencode's built-in provider uses
natively), or the env overrides OPENROT_BRIDGE_PORT and OPENROT_UPSTREAM.
Everything you add is a profile: a URL that returns a list of nodes.
--kind tells openrot how to parse it.
Sources of vless:// subscription links. Public repos and mirrors that republish
daily dumps of working configurations:
- igareck/vpn-configs-for-russia — direct raw file:
https://gitlab.com/igareck/vpn-configs-for-russia/-/raw/main/WHITE-CIDR-RU-all.txt - Search GitHub/GitLab for
vless config list/v2ray subscription— most of the repos that come up work as a relay profile.
poetry run openrot profile add gitlab <URL> --kind relay --priority 0
poetry run openrot update --relayOnly
vless://is parsed today. Files mixing other protocols (hysteria2, vmess, ss) aren't handled yet. SeeNodeProtocolinmodels/enums.py.
Sources of free http / socks5 forward proxies (proto://host:port lines).
Updated lists from aggregators:
- proxifly/free-proxy-list — validated every 5 minutes, served from CDN, raw text:
Protocol-specific:
https://cdn.jsdelivr.net/gh/proxifly/free-proxy-list@main/proxies/all/data.txt.../proxies/protocols/http/data.txt,.../proxies/protocols/socks5/data.txt. free-proxy-listmirrors and Telegram channels that post dailyproto://host:portdumps work too.
poetry run openrot profile add proxifly <URL> --kind proxy --priority 10
poetry run openrot update --proxyFree proxies are the least stable layer, so they usually get a lower priority
(a higher --priority number) than relay profiles: they only kick in when the
relay chain is exhausted. update --proxy keeps the 20 fastest alive.
--priority N: lower number, higher priority. Profiles are tried top-down, then nodes within each profile by their own priority.--interval S: per-profile refresh seconds. Defaults to the globalupdate_interval.openrot start cascaderefreshes each profile when its interval elapses. Manualopenrot updaterespects--nameand the flag-based kind filter.--disabled: add the profile as off; enable it later by editingconfig.yaml.
Stored in ~/.config/openrot/config.yaml.
Overridden by OPENROT_DIR (base dir), OPENROT_CONFIG (full config path),
OPENROT_PORT (int), OPENROT_SINGBOX_BIN (sing-box binary name/path),
OPENROT_BRIDGE_PORT (int), and OPENROT_UPSTREAM (bridge upstream base URL).
All standardized fields are StrEnum values.
port: 7890
strategy: fallback # fallback | urltest
urltest_url: "https://www.gstatic.com/generate_204" # latency reference target
health_interval: 30
health_timeout: 5
fail_threshold: 3
update_interval: 3600 # seconds; per-profile refresh default
warp_enabled: true # WARP = top priority; disable via 'openrot warp off'
profiles:
- name: gitlab
kind: relay # relay | proxy
url: https://gitlab.com/igareck/vpn-configs-for-russia/-/raw/main/WHITE-CIDR-RU-all.txt
priority: 0 # lower = higher
enabled: true
interval: null # null → uses update_interval
nodes:
- id: "node-..."
raw: "vless://..."
protocol: vless # vless | http | socks5
priority: 0
status: unknown # unknown | alive | dead
latency_ms: null
fails: 0
last_check: null
active_level: none # none | warp | node
current_node_id: null
singbox_bin: "sing-box"
bridge_port: 7891 # loopback port for the opencode bridge
bridge_upstream: "https://opencode.ai/zen/v1"Node latency is the time of an HTTP request to urltest_url
(default https://www.gstatic.com/generate_204); relay nodes go through
sing-box, http/socks5 proxies are probed directly.
openrot update and the scheduler run every fetched node through the pipeline:
parse (dedupe vless:// / proto://host:port)
-> TCP reachability (parallel, 50 workers)
-> TLS handshake (only for tls/reality/wss nodes)
-> sing-box config check (only relay)
-> HTTP probe of urltest_url via the node
The probe requests urltest_url (default generate_204); a node needs a 2xx
response to survive. Every node that passes the earlier stages is probed (all
in parallel), survivors are ranked by latency (the measured request time) and
the best 20 (TOP_LIMIT) are published per profile.
Progress is printed live: update shows a per-stage counter/bar
(verify parse: 3/3, verify probe: 12/100), and openrot probe <url> prints
the same stage counts as plain lines. openrot test re-verifies the currently
published nodes through the same pipeline.
fallback— first alive node top-down by profile/node priority (default)urltest— lowest latency among alive nodes
In foreground mode, the current node is re-checked every health_interval
seconds and rotated when it fails (fail_threshold consecutive failures = dead).
Both installers auto-install warp-cli and bring WARP up on the host in proxy
mode.
Standalone binary (PyInstaller build, no Python needed):
./install.sh # from a checkout
make installer # equivalentEnvironment overrides: OPENROT_BIN_URL, OPENROT_VERSION, OPENROT_PREFIX,
OPENROT_SKIP_WARP. On macOS you'll be prompted to grant WARP a system
extension / VPN permission the first time.
Docker (not auto-installed; the script prints instructions):
./install-docker.sh
make install-docker # equivalentEnvironment overrides: OPENROT_IMAGE, OPENROT_PORT, OPENROT_CONFIG_DIR,
OPENROT_WARP_HOST, NETWORK.
Add sing-box too; openrot needs it for relay nodes
(brew install sing-box on macOS).
WARP runs on the host, not in the container. warp-cli needs a TUN
device and the OS network stack, so it can't tunnel inside Docker. The container
reaches the host-side WARP through its SOCKS5 upstream, so WARP and the node
chain work together.
make docker-run # builds image and runs openrot, wired to the host WARP
make docker-run MODE=both # same, but both daemons in the background
make docker-sh # interactive shell in the image (same config mount)By default the container uses bridge networking and points at the host's WARP via
OPENROT_WARP_HOST=host.docker.internal (host port 40000):
- macOS (Docker Desktop): works out of the box.
- Linux: the Makefile adds
--add-host host.docker.internal:host-gateway. - On Linux you can instead run
NETWORK=host make docker-runto share the host loopback, so WARP answers at127.0.0.1:40000directly.
The container takes one argument — the run mode (the image's default CMD is
both):
| Mode | What runs |
|---|---|
cascade |
Proxy/cascade in the foreground; the events log streams to docker logs. |
bridge |
429-rotation bridge in the foreground (starts the cascade along with it). |
both |
Cascade + bridge as background daemons, started simultaneously; openrot logs streams all of them to stdout and keeps the container alive. |
docker run --rm -it ... openrot cascade # proxy only, logs in foreground
docker run --rm -it ... openrot bridge # bridge only, foreground
docker run --rm -it ... openrot both # both daemons, logs via `openrot logs`
docker stop <container> # stops supervision; daemons die with itdocker build -t openrot .
docker run --rm -it \
-p 7890:7890 \
-p 7891:7891 \
--add-host host.docker.internal:host-gateway \
-e OPENROT_WARP_HOST=host.docker.internal \
-v $HOME/.config/openrot:/root/.config/openrot \
openrot both- Config and data are shared read-write via the
-v $HOME/.config/openrot:...mount, so a profile you add on the host (or in the container) is visible on both sides.OPENROT_DIRis already set to/root/.config/openrotinside the image (seeDockerfile). - Ports: the mixed proxy listens on
7890, the bridge on7891; map a different host port with-p $PORT:7890/-p $PORT_BRIDGE:7891. - Listen address: the proxy and bridge bind
127.0.0.1by default; the image setsOPENROT_LISTEN=0.0.0.0so the published ports are reachable from the host (override with-e OPENROT_LISTEN=127.0.0.1to lock down). - WARP address:
OPENROT_WARP_HOST(default127.0.0.1) andOPENROT_WARP_PORT(default40000) point openrot at the host-side WARP SOCKS5 proxy. In the container setOPENROT_WARP_HOSTto the host. OPENROT_PORT/OPENROT_SINGBOX_BIN: set with-eif you need to overrideopenrotitself;sing-boxis already preinstalled in the image.- Mode at container start:
docker run ... openrot cascade|bridge|both, or re-run an existing container withdocker start -ai <name> <mode>(the mode becomes the container's command).
Before make docker-run, start WARP once on the host in proxy mode:
poetry run openrot warp onThen the container (or any openrot on the host) can use it. If the host WARP is
not reachable, openrot falls back to the profiles + node chain and
status reports WARP: available on host only.
- Tests:
poetry run pytest(ormake check— ruff + mypy + coverage). - Coverage standard: keep the local report above 85 %; the CI gate is set lower (75 %) as a safety floor.
Do I need to host anything? No. openrot only pulls remote lists, health-checks them, and runs a local sing-box. Nothing is stored server-side.
Why not just one node? Free sources die. openrot treats them as a pool with priorities: when the top node dies, it rotates down the chain, and profiles get re-fetched on their interval.