Skip to content
dirvpklPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

tppr 0.13.0 — a global pool, LAN-reachable proxy ports, an optional dispatcher, pinned accounts, local pool aggregation, and custom probe checks.

Vibe-coded side project, built to demo a fun idea: a self-hosted proxy router.

What it does

  • tppr-mihomo-relay exposes HTTP/SOCKS inside Docker and publishes the proxy ports on all host interfaces (HOST_MIXED_PORT, 17890 in the checked-in example). The controller stays on 127.0.0.1 only. The POOL selector is the production route; the separate TEST_POOL selector is used only by the speed tester, so benchmarks never reroute live Telegram or YouTube connections.
  • Health loop: only the current production node is checked every 10s. On failure, candidates are latency-probed in batches of 100 with 100 workers; failover stops after 1 good candidate; a scheduled sweep collects up to 5 good candidates and benchmarks at most 5 total nodes including production.
  • Slow loop (tppr-speed-tester, default every 300s): downloads a test file through up to 5 total nodes (the current production node plus up to 4 discovered candidates), measures throughput, then updates only POOL to the fastest healthy node. Results persist in SQLite (tester-data volume): nodes failing 3 sweeps in a row are skipped but retried once failures age past HISTORY_COOLDOWN_H; old rows are pruned. Mihomo's persisted production selection is kept on startup; stale or dead selections go through speed-tested failover. No background health checks run across the full provider.
  • Routing: Telegram and YouTube go through POOL, everything else is DIRECT.
  • Custom upstream proxies live in mihomo/providers/mine.yaml and are inlined into the generated config; the generator is the only writer of the active config, so scripts/reload.ps1 is the only supported way to apply changes.

Services and fixed ports

  • Each [[services]] block in the gitignored services.toml gets a group and, unless the service is reachable through the dispatcher, a fixed port reachable from the LAN. Ports are declared in that file, not in .env, and are generated into docker-compose.override.yml. The controller API is the only thing still bound to 127.0.0.1.
  • A service selects upstream nodes in one of three ways:
    • subscriptions = ["name", ...] — every node of those providers;
    • primary = "proxy-name" — one exact proxy, optionally followed by fallback = ["other-proxy"] and the providers in subscriptions;
    • select = true — a plain list that never moves on its own; an external prober steers it (see Custom probe checks).
  • Service groups are Mihomo fallback groups, so a dead node is skipped for the next one. empty-fallback: REJECT means an empty provider rejects the connection instead of leaking traffic direct.

Custom probe checks

  • Mihomo's own health check is a GET against one URL, which is not enough when an endpoint only answers meaningfully to a POST with headers and a body. Two pieces cover that:
    • [subscriptions.health] overrides the provider health check for one subscription: url, interval, timeout, max_failed_times, expected_status, each falling back to the global [health] values. A service may not mix providers with different checks — that is a generation error, not a silent choice.
    • post-prober/ steers a select = true service with a real POST. Every interval_s it sends the configured request through the group's current node via the dispatcher and expects one exact status. Anything else means reroll: it switches to the next candidate immediately instead of waiting, so a node that answers 403 RegionError (wrong country) is abandoned in about a second. If nothing answers as expected, the group stays put and the next round retries from the top.
  • The prober is a separate container because its job is a steering loop, not node admission. It reads the dispatcher port and the account password from services.toml (one source for both) and verifies at startup that target.group and target.provider really belong to the account it probes — a typo there aborts instead of quietly steering nothing.
  • Request shape lives in the gitignored post-prober.toml and post-prober-body.json; copy the .example files to start. It carries the URL, method, headers, body file, expected status and interval.
  • Each completed round touches a heartbeat file, and the container reports unhealthy when no round lands within POST_PROBER_HEARTBEAT_MAX_AGE_S (default 180 s, keep it above interval_s). "Running" and "still working" stop being the same claim.

Subscriptions

  • Add one [[subscriptions]] block per remote feed; Mihomo pulls it natively as a proxy-provider, so Clash/Mihomo YAML, URI lists, and base64 subscriptions all work without a converter:
    [[subscriptions]]
    name = "my-vless"
    url = "https://example.com/subscription"
    interval = 21600
  • Subscription URLs can contain tokens; keep services.toml local and do not commit it.

Local pool aggregation

  • Copy pool-sources.example.toml to pool-sources.toml and assign the ten sources to named pools. The file is gitignored because source URLs may contain tokens.
  • The pool-aggregation Compose profile starts SubConv and an internal pool worker. No worker or SubConv port is published to Windows.
  • The worker calls SubConv's /provider endpoint, accepts Clash/URI/base64 output, removes cross-source duplicates by canonical node fingerprint, and writes deterministic pool files such as mihomo/providers/pool-free-pool.yaml.
  • Sources are fetched in parallel (worker.max_workers). A dead source is reported as degraded in /healthz and skipped instead of taking the other pools down; the last good pool snapshot is kept when every source fails.
  • The worker also drops nodes Mihomo cannot parse. Mihomo empties an entire proxy-provider when a single node fails to initialize, so one free node with an unsupported shadowsocks cipher or a malformed key would otherwise take the whole pool down. The count is reported as dropped_nodes in /healthz.
  • The worker then probes every remaining HTTP/SOCKS5 node with a real HTTP(S) exchange through the proxy tunnel (common/probe.py): method, headers, body and cookies are configurable in the [check] section, and [check.expect] decides what counts as live — status codes or 2xx-style classes, header/body/cookie matches, and a latency ceiling. Without [check.expect] any completed exchange is live. Only passing nodes reach a pool file (checked_nodes/dead_nodes in /healthz). Other protocols pass through unchecked for now.
  • max_nodes caps a pool at 1000 nodes; raise MAX_NODES in pool-worker/worker.py to go higher.
  • Reference pool names from services.toml with subscriptions = ["free-pool"]. A source may appear in several pools; a node is assigned to one pool by stable hash unless the pool is marked shared = true.
  • Pools use Mihomo fallback failover. Per-pool throughput rotation is not enabled; the existing speed tester manages only the global POOL.
  • reload.ps1 enables the profile automatically when pool-sources.toml exists.

Dispatcher

  • Add one optional authenticated entry point; fixed per-service ports keep working unchanged:
    [dispatcher]
    port = 20000
  • Add username and password to any service that should be reachable through it. A service with credentials no longer needs its own port, so hundreds of accounts can share the single dispatcher port:
    [[services]]
    name = "acc01"
    primary = "my-proxy-01"
    subscriptions = ["my-vless"]
    username = "acc01"
    password = "change-me-at-least-8"
  • Mihomo authenticates the user and routes the connection with IN-USER to SVC_acc01. Use http://acc01:change-me-at-least-8@127.0.0.1:20000 or socks5://acc01:change-me-at-least-8@127.0.0.1:20000 in clients that support proxy authentication.
  • Services without credentials remain available only on their fixed port. Passwords stay in the gitignored services.toml.
  • MAX_SERVICES is 1000. Exceeding it names the constant and the file to edit.
  • scripts/new_accounts.py writes account blocks in bulk; see scripts/README.md.

Pinned accounts and fallback

  • primary names one proxy from mihomo/providers/mine.yaml. Mihomo does not resolve provider proxies by name, so the generator inlines those definitions into the generated config; a name that is not in the file fails generation instead of starting a dead group.
  • With primary plus subscriptions, the group is a fallback chain: the pinned proxy first, then the free pool. When the pinned proxy dies the account silently continues on a free node.
  • lock_proxy = true removes every fallback source for that account. When its proxy is down the connection is rejected instead of leaving through a free node. The generator refuses to combine lock_proxy with fallback/subscriptions.
  • fallback = ["name"] inserts extra named proxies between the primary and the free pool.
  • balance = "round-robin" turns the group into a load-balance pool: every new connection exits through the next node instead of sticking to one. It needs subscriptions and forbids primary, fallback and lock_proxy. The effective variety equals the currently healthy nodes, so a free pool rotates between its survivors.
  • One node per service, proxies may repeat. A node is a named entry in mine.yaml; a paid proxy is the upstream account behind it. Several nodes may share one paid proxy (same creds under different names) — that is normal. Each service still gets its OWN node. A service without its own node gets no primary; pool-only is an outage waiting to happen, not a fallback.

Hash-routed expiring access

  • Port 17894 serves the all-pools mix without touching Mihomo (override with HASH_GATE_ALL_PORT). The free-only and custom-only gateways keep listening inside the compose network; publish 17895/17896 to enable them.
  • Every connection exits through a uniformly random pool node. Authentication only gates lifetime: the username is any name (random works), the password is a TTL in seconds, from 5 up to 30 days. The countdown starts at the first request and is stored in SQLite, so restarts do not extend it. An expired credential is rejected like a wrong password.
  • A dead pick never fails the client outright: the gateway retries against up to 5 other random nodes of the same pool before giving up.
  • Only HTTP and SOCKS5 upstreams are forwardable, so roughly two thirds of a pool are reachable through these ports. The rest needs the Mihomo groups.
  • reload.ps1 starts the gate with the pool profile and waits for it to report healthy before applying anything else.

Global pool endpoints

  • 17890 (from HOST_MIXED_PORT) is the POOL group: subscriptions, local pools, and custom proxies.
  • Two optional extra endpoints split that mix:
    [global_pools]
    free_port = 17891    # FREE group: subscriptions and local pools only
    custom_port = 17892  # CUSTOM group: mihomo/providers/mine.yaml only
  • Both share the LAN-reachable binding with every other proxy port in the stack; only the controller stays on 127.0.0.1.
  • A port that has no matching source is a generation error: custom_port without custom proxies, or free_port without any subscription or pool.

Quickstart

Copy-Item .env.example .env
Copy-Item services.example.toml services.toml
# ports default to 7890/9090 — change them in .env, README examples follow .env
# private upstreams are optional — free pool works alone:
# Copy-Item mihomo\providers\mine.yaml.example mihomo\providers\mine.yaml
# local pool aggregation is optional — copy pool-sources.example.toml to
# pool-sources.toml to switch the pool-aggregation profile on.
powershell -ExecutionPolicy Bypass -File .\scripts\reload.ps1
curl.exe --proxy http://127.0.0.1:7890 https://www.gstatic.com/generate_204 -o NUL -w "%{http_code}`n"

Scaling ports

Add another [[services]] block with a fixed localhost port in services.toml, then run scripts/reload.ps1. Service ports do not need variables in .env.

Manual rotation

# list production members (not a speed ranking)
Invoke-RestMethod http://127.0.0.1:9090/proxies/POOL | Select-Object -ExpandProperty all
# pin a node
Invoke-RestMethod -Method Put http://127.0.0.1:9090/proxies/POOL -Body '{"name":"node-17"}' -ContentType 'application/json'
# pool worker health, including per-source node counts and dropped nodes
docker exec tppr-pool-worker python -c "import urllib.request;print(urllib.request.urlopen('http://127.0.0.1:8080/healthz').read().decode())"

Controller and mixed ports in those examples are container ports; on the host use the values of HOST_CONTROLLER_PORT and HOST_MIXED_PORT from .env.

Publishing to GitHub

git remote add origin https://github.com/<you>/tppr.git
git push -u origin master

.env, services.toml, pool-sources.toml, mihomo/providers/mine.yaml, and mihomo/generated/ are gitignored — secrets never leave the machine.

Layout

  • docker-compose.yml / .env.example — topology and tunables (single source).
  • mihomo/config.base.yaml — template: isolated production/test groups, internal speed-test listener and TG/YT rules.
  • services.example.toml / services.toml — dispatcher, subscriptions, pinned proxies, and fixed-port service-to-proxy mapping; one block can be copied for each service or account, and the same proxy may appear in multiple blocks.
  • pool-sources.example.toml / pool-sources.toml — remote source catalog, pool membership, caps, and shared-pool policy.
  • scripts/generate.py — validates the mapping and generates active config/Compose override.
  • scripts/new_accounts.py — writes dispatcher account blocks with generated passwords.
  • pool-worker/worker.py — SubConv client, node validation, canonical dedupe, deterministic pool builder, and internal pool HTTP endpoint.
  • mihomo/providers/mine.yaml.example — template for private upstreams.
  • mihomo/generated/config.yaml — generated active config; never edit it by hand.
  • speed-tester/tester.py — throughput sweeps (stdlib only, structured logs).
  • speed-tester/history.py — SQLite sweep history and cooldown tracking.
  • post-prober/prober.py — POST probe that steers one select = true service.
  • post-prober.example.toml / post-prober-body.example.json — request-shape templates; the live copies are gitignored.
  • scripts/README.md — what each script does and every error it can raise.
  • api/ — management REST API on 127.0.0.1:18080 (Bearer token): accounts CRUD, status, hash-lease revocation.
  • scripts/smoke.ps1 — config + syntax sanity checks.
  • scripts/reload.ps1 — regenerate, wait for the pool worker, validate with Mihomo, then recreate the relay and tester.
  • AGENTS.md — contributor onboarding: architecture, workflows, verification commands, and verified Mihomo behaviors.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages