A self-hosted Cloudflare Worker tunnel with a built-in control console, native subscriptions, and preferred-IP exports.
English · Simplified Chinese · Spanish · Persian
Re_edgetunnel accepts VLESS and Trojan traffic over WebSocket, XHTTP, or gRPC, and Shadowsocks SIP003 AEAD over WebSocket. Outbound TCP connections are opened through Cloudflare's Socket API. The same Worker serves a local administration console for subscriptions, preferred Cloudflare IPs, settings, logs, integrations, backup, and recovery.
No panel, JavaScript bundle, font, QR service, or runtime configuration is downloaded from a third-party host. The Worker code, UI assets, and QR renderer are all shipped in this repository. Optional integrations remain off until the operator supplies an endpoint.
Important
Use this software only for lawful work and for systems and networks you are authorized to access. The operator is responsible for Cloudflare's terms, local law, client configuration, and destination policy.
| Area | Included |
|---|---|
| Inbound protocols | VLESS, Trojan, Shadowsocks SIP003 AEAD |
| Transports | WebSocket, XHTTP stream-one, gRPC Hunk; Shadowsocks uses WebSocket |
| Outbound | TCP through cloudflare:sockets, direct or through an operator-configured upstream proxy |
| Native exports | Mihomo/Clash YAML and share links; no public converter required |
| Preferred IPs | Import local scan results, store them in KV, and generate persistent URLs with ip, port, and name |
| Administration | Password login, KV sessions, overview, nodes, settings, logs, integrations, backup/restore, logout |
| Optional upstreams | SOCKS5, HTTP CONNECT, HTTPS CONNECT, TURN/TURNS RFC 6062, SSTP |
| Not provided | A local ISP scanner, native QUIC/UDP inbound, Hysteria2, TUIC, WireGuard, or VLESS Reality |
The console is currently written in Simplified Chinese. Its exports and network protocols are language-neutral; this repository provides English, Chinese, Spanish, and Persian operating guides.
flowchart LR
C["VLESS / Trojan / Shadowsocks client"] -->|"WS, XHTTP, or gRPC"| W["Your EdgeTunnel Worker"]
B["Your browser"] -->|"/login and /admin"| W
W --> K["Your Workers KV"]
W -->|"TCP Socket"| D["Authorized destination"]
S["Local IP scanner"] -->|"Import results"| B
W -. "optional" .-> P["Operator-owned upstream proxy"]
W -. "optional" .-> O["Operator-owned DNS, converter, or diagnostics"]
The data path and the control console share one Worker but remain separate routes. Opening /admin does not change tunnel forwarding. Preferred-IP exports change the client connection address only; they do not reroute Worker egress.
These screenshots were captured from the current source running locally with a synthetic UUID and RFC documentation addresses. They contain no production domain, account identifier, subscription token, or live credential.
- A Cloudflare account with Workers enabled.
- A Workers KV namespace dedicated to this deployment.
- A current Node.js LTS release, npm, and Git.
- A custom domain in the same Cloudflare account if you want zone-level gRPC support.
- A compatible client such as Mihomo/Clash for the generated configuration.
This project targets Cloudflare Workers because it uses cloudflare:sockets. It is not a drop-in Vercel Function or Vercel Edge Function.
git clone https://github.com/tianrking/Re_edgetunnel.git
cd Re_edgetunnel
npm ciUse a project-local Wrangler installation when you want a pinned deployment tool:
npm install --save-dev wrangler@latest
npx wrangler --versionnpx wrangler login
npx wrangler whoamiAlways read the whoami output before creating KV or deploying. This avoids placing a Worker in the wrong account.
The tracked wrangler.toml is a public template. Copy it to the ignored local filename before adding deployment-specific values:
cp wrangler.toml wrangler.local.toml
# PowerShell: Copy-Item wrangler.toml wrangler.local.tomlYou may change the Worker name in wrangler.local.toml. Do not commit this local file.
npx wrangler kv namespace create KVWrangler prints a namespace ID. Replace the placeholder only in wrangler.local.toml:
[[kv_namespaces]]
binding = "KV"
id = "paste-your-kv-namespace-id-here"The binding name must remain KV. Use separate namespaces for production and testing; sharing KV also shares settings, address lists, logs, and active sessions.
npm run check
npm test
npx wrangler deploy --dry-run --config wrangler.local.toml
npx wrangler deploy --config wrangler.local.tomlBefore ADMIN is configured, normal HTTP requests intentionally return 503 Administrator password is not configured.
Generate an administrator password and a separate RFC 4122 version-4 UUID locally:
node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))"
node -e "console.log(require('node:crypto').randomUUID())"Enter each value at Wrangler's interactive prompt:
npx wrangler secret put ADMIN --config wrangler.local.toml
npx wrangler secret put UUID --config wrangler.local.toml
npx wrangler secret list --config wrangler.local.tomlADMINis the password for/login.UUIDis the VLESS credential and the Trojan/Shadowsocks password used in generated nodes.- The subscription
TOKENis not the administrator password. It is derived from the active hostname and UUID.
Use different values for ADMIN and UUID. Rotating ADMIN does not change nodes. Rotating UUID invalidates existing nodes and changes the subscription token.
Add the route only to wrangler.local.toml:
routes = [
{ pattern = "tunnel.example.com", custom_domain = true }
]Deploy again:
npx wrangler deploy --config wrangler.local.tomlFor gRPC, enable gRPC in the Cloudflare zone's Network settings and keep the client SNI/servername on the custom hostname. A workers.dev hostname and a custom hostname produce different subscription tokens.
Open:
https://tunnel.example.com/login
Sign in with the value stored in ADMIN. The root URL normally shows an nginx-style camouflage page; that is expected.
After login:
- Open Nodes & subscriptions.
- Leave the preferred IP empty to use the Worker hostname, or select an address imported from your local scan.
- Generate the preview.
- Copy the refreshable URL or download Mihomo/Clash YAML.
- Import it into the client and test the actual route.
Native endpoints:
| Output | URL |
|---|---|
| Raw URI list | /sub?token=TOKEN |
| Base64 URI list | /sub?token=TOKEN&base64 |
| Mihomo/Clash YAML | /sub?token=TOKEN&format=clash |
| Share-link text | /sub?token=TOKEN&format=links |
| Clash with a preferred address | /sub?token=TOKEN&format=clash&ip=IP |
| Download instead of inline display | Add &download=1 |
Native preferred-IP parameters:
| Parameter | Meaning |
|---|---|
ip |
Valid IPv4 or IPv6 connection address |
port |
Optional port from 1 to 65535; default 443 |
name |
Optional node label, limited and sanitized by the Worker |
Treat every subscription URL as a credential. Do not paste it into an issue, screenshot, analytics service, or public converter.
The Worker cannot measure the path between your ISP and Cloudflare. Run the scanner on the device or network that will use the tunnel, then import the result into /admin.
The third-party open-source tool XIU2/CloudflareSpeedTest can measure HTTP latency from the current network to Cloudflare edge addresses. Re_edgetunnel does not bundle, host, or remotely execute this scanner. Download the build for your operating system and architecture from its official Releases.
Run the following PowerShell command in the directory containing cfst.exe and ip.txt. Replace the example hostname with your own Worker hostname and use only a public HTTPS root URL. Never put a subscription token, administration path, or other credential in -url.
.\cfst.exe -f ip.txt -tp 443 -httping -url https://worker.example.com/ -n 50 -p 30 -dd -o result-httping.csv| Option | Purpose in this example |
|---|---|
-f ip.txt |
Reads Cloudflare IP addresses or CIDR ranges from ip.txt |
-tp 443 |
Tests the standard HTTPS port 443 |
-httping |
Measures latency with the HTTP/HTTPS URL instead of the default TCPing mode |
-url https://worker.example.com/ |
Uses your Worker hostname to validate TLS, HTTP status, and Cloudflare routing |
-n 50 |
Uses 50 latency-test workers; this is concurrency, not the number of displayed IPs |
-p 30 |
Prints the first 30 sorted results in the terminal |
-dd |
Disables download testing and sorts the result by average latency |
-o result-httping.csv |
Writes the complete result to a CSV file in the current directory |
The displayed available count means only that HTTPing completed without a timeout and returned an accepted status code. It does not mean every address is suitable. Prefer results with 0.00 packet loss, low average latency, and stable repeated runs. Because this command includes -dd, a download-speed value of 0.00 MB/s is expected and is not a failed latency test.
Add -tlr 0 to exclude every result with packet loss. Add a filter such as -cfcolo SIN,HKG,NRT to limit the output to selected Cloudflare locations. HTTPing is still network scanning, so keep concurrency moderate and avoid repeated high-frequency runs. Disable the system proxy, or explicitly make CFST bypass it, so the result represents the client network rather than a proxy route.
result-httping.csv is the raw CFST CSV. The current console import box accepts one normalized address per line rather than the complete CSV. Select the useful rows, convert them as shown below, and paste them into the Preferred IP page under /admin:
CFST CSV: 104.18.46.92,4,4,0.00,54.65,0.00,SIN
Console line: 104.18.46.92:443#SIN,54.65ms
The address above illustrates field conversion only; it is not a universal recommendation. After importing it, generate a Clash or share link and test connection success, throughput, and peak-hour stability in the real client. A single HTTPing ranking is not a substitute for an end-to-end tunnel test.
Accepted import lines:
IP
IP:PORT
IP:PORT#LABEL
IP:PORT#LABEL,28ms
[IPv6]:PORT#LABEL,42ms
Documentation-only examples:
198.51.100.42:443#Example-v4,28ms
[2001:db8::42]:443#Example-v6,42ms
When ip is used, Re_edgetunnel changes only the generated node's server and optional port:
| Field | Result |
|---|---|
server / connection address |
Replaced with the selected IP |
TLS servername / SNI |
Keeps the Worker hostname |
WebSocket Host |
Keeps the Worker hostname |
XHTTP host |
Keeps the Worker hostname |
| gRPC service name | Keeps the configured tunnel path without the leading slash |
| UUID/password and path | Unchanged |
Changing server, SNI, Host, and path to the same IP breaks Cloudflare routing. The IP is only the edge connection target; the hostname still identifies your Worker.
The console uses a random 256-bit session token stored as a SHA-256-derived key in KV. Its cookie is HttpOnly, Secure, and SameSite=Strict. Sessions expire after 24 hours; logout revokes the active session immediately.
| Section | What it does |
|---|---|
| Overview | Shows protocol/transport status, host, tunnel path, masked credential, recent subscription count, and preferred-IP count |
| Nodes & subscriptions | Builds VLESS, Trojan, and Shadowsocks nodes; renders local QR codes; exports links and Clash YAML |
| Preferred IP | Imports, validates, deduplicates, stores, selects, and deletes up to 128 IPv4/IPv6 results |
| Service settings | Changes subscription name, tunnel path, transports, fingerprint, refresh interval, certificate policy, 0-RTT, and Shadowsocks settings |
| Access logs | Reads KV-backed request records with credential-bearing query parameters removed |
| Integrations & diagnostics | Displays explicitly configured converter, proxy check, usage API, DNS, ECH, Telegram, and masquerade options |
| Security | Exports a secret-free backup, restores validated settings, and resets UI-managed defaults |
Important routes:
| Route | Method | Purpose |
|---|---|---|
/login |
GET, POST | Create an administrator session |
/admin |
GET | Load the self-contained console |
/admin/api/bootstrap |
GET | Return the sanitized console model and native exports |
/admin/api/preview |
GET | Preview nodes for the hostname or a preferred IP |
/admin/api/settings |
POST | Save UI-managed settings without discarding unrelated configuration |
/admin/api/preferred-ips |
POST | Import and store local scan results |
/admin/api/backup |
GET | Export settings and IPs without administrator, UUID, token, or integration secrets |
/admin/api/restore |
POST | Restore a validated console backup |
/admin/config.json |
GET, POST | Advanced access to the effective legacy-compatible configuration |
/admin/ADD.txt |
GET, POST | Read or replace the operator-owned address list |
/admin/log.json |
GET | Read request logs |
/admin/init |
POST | Reset config.json; does not erase address lists or logs |
/admin/check |
GET | Test an explicitly configured SOCKS5/HTTP upstream |
/logout |
GET | Revoke the current session |
Configuration-changing POST requests require a same-origin Origin or Referer header as CSRF protection.
Keep secrets in Cloudflare Secrets. Put non-sensitive values in the ignored wrangler.local.toml only when they must be deployment-specific.
| Variable | Recommended storage | Purpose |
|---|---|---|
ADMIN |
Secret; required | Administrator password |
UUID |
Secret; strongly recommended | Canonical v4 UUID used by generated nodes |
KEY |
Secret; optional | Additional private shortcut path and legacy secret key |
HOST |
Variable; optional | Override generated host list |
PATH |
Variable; optional | Tunnel path; default /tunnel |
URL |
Variable; optional | Root camouflage: nginx, 1101, or an explicit HTTPS origin |
PROXYIP |
Variable or Secret | Operator-selected fallback proxy IP |
UPSTREAM_PROXY |
Secret when credentialed | socks5://, http://, https://, turn://, turns://, or sstp:// upstream |
UPSTREAM_PROXY_MODE |
Variable | always (default) or selective cloudflare routing |
TCP_CONCURRENT_DIAL |
Variable | Direct connection race width, clamped to 1-4 |
PROXY_CONCURRENT_DIAL |
Variable | Proxy candidate race width, clamped to 1-4 |
SPEEDTEST_MODE |
Variable | local returns bounded local HTTP 204 responses; block closes the test tunnel |
SPEEDTEST_DOMAINS |
Variable | Domains handled by the local connectivity-test path |
DNS_RESOLVER / DNS_RESOLVER_PORT |
Variable | Operator-owned TCP DNS for supported DNS forwarding and TURN/SSTP resolution |
PROXY_CHECK_HOST / PORT / PATH |
Variable | Operator-owned HTTP endpoint used by proxy diagnostics |
LOCATIONS_API |
Variable | Operator-owned HTTPS location data endpoint |
ECH_DOH_URL |
Variable | Explicit HTTPS DoH endpoint used only when ECH is enabled |
ALLOW_REMOTE_USAGE_API |
Variable | Must be true before a stored remote Cloudflare usage API is called |
Legacy aliases such as PASSWORD or TOKEN are accepted for compatibility, but new deployments should use ADMIN. Do not place any credential, Cloudflare account ID, KV namespace ID, private domain, or generated subscription URL in a tracked file.
Cloudflare Workers block raw outbound TCP sockets to Cloudflare-owned IP ranges. UPSTREAM_PROXY_MODE=cloudflare handles that platform boundary without sending unrelated traffic through the upstream:
- An IP-literal destination is checked locally against Cloudflare's published IPv4 and IPv6 ranges.
- A hostname destination is resolved through Cloudflare's JSON DNS-over-HTTPS endpoint. A and AAAA results are cached in the Worker isolate for 30 seconds to one hour according to bounded DNS TTLs.
- If any resolved address belongs to Cloudflare, the Worker uses
UPSTREAM_PROXY. Other destinations keep the direct socket path. - If both DNS lookups fail, routing fails open to the direct path instead of silently sending all traffic upstream.
always remains the default for backward compatibility. It preserves the historical behavior in which every TCP destination uses UPSTREAM_PROXY. PROXYIP remains a separate fallback and does not override the selective decision. Keep a credentialed upstream URL in a Wrangler Secret:
npx wrangler secret put UPSTREAM_PROXY --config wrangler.local.tomlThe upstream proxy endpoint itself must resolve to an address outside Cloudflare-owned ranges. Put its hostname on a DNS-only record, or use a direct non-Cloudflare address; proxying that first hop through Cloudflare recreates the same blocked socket path. The bundled range snapshot is sourced from Cloudflare's IPv4 and IPv6 lists. Review those sources when updating the snapshot. The DNS lookup is performed only when the operator explicitly selects cloudflare mode.
For long-lived TCP streams, traffic in either direction refreshes a 15-minute idle timer. A separate one-hour session ceiling remains in place.
Native format=clash and format=links exports never require a converter. Legacy client-format requests are available only after you configure your own HTTPS converter and configuration URL:
| Request | External requirement |
|---|---|
?clash |
Operator-owned SUBAPI and SUBCONFIG |
?singbox |
Operator-owned SUBAPI and SUBCONFIG |
?surge |
Operator-owned SUBAPI and SUBCONFIG |
?quanx |
Operator-owned SUBAPI and SUBCONFIG |
?loon |
Operator-owned SUBAPI and SUBCONFIG |
Without those values the Worker returns HTTP 501 instead of silently sending the subscription to a public service.
workers/clash-sub is a separate, password-protected Worker that can publish a Clash-only subscription for one EdgeTunnel hostname. It has its own generic Wrangler template and requires these Secrets:
SECRET_TOKENPAGE_PASSWORDCLOUDFLARE_UUID
It also requires CLOUDFLARE_HOST, and the UUID must match the main Worker. See workers/clash-sub/README.md. Do not copy a personal deployment file into the repository.
Supported:
- VLESS over WebSocket, XHTTP
stream-one, and gRPC Hunk. - Trojan over WebSocket, XHTTP at the Worker route, and gRPC Hunk; native Clash export emits the client combinations it can describe safely.
- Shadowsocks
aes-128-gcmandaes-256-gcmwith SIP003 AEAD framing over WebSocket. - TCP destinations reachable through Cloudflare's Socket API.
- VLESS/Trojan DNS when an operator-owned TCP resolver is configured.
- SOCKS5, HTTP(S) CONNECT, TURN(S) RFC 6062, and SSTP as upstream paths.
Not supported:
- Hysteria2 or TUIC, because they require native QUIC/UDP.
- WireGuard inbound.
- VLESS Reality, because Cloudflare terminates TLS.
- Arbitrary UDP forwarding; the implemented UDP case is configured VLESS/Trojan DNS.
- A native TCP listener or a general-purpose HTTP forward proxy.
TURN is limited to RFC 6062 TCP allocation and connection binding. SSTP is limited to TLS, PPP PAP/IPCP, IPv4, and inner TCP; it does not claim MPPE, IPv6CP, or vendor-extension coverage.
Before every public commit:
- Keep
wrangler.local.toml,.dev.vars, and.wrangler/untracked. - Store
ADMIN,UUID, proxy credentials, API tokens, and companion-Worker credentials as Secrets. - Use reserved examples such as
198.51.100.0/24and2001:db8::/32in documentation. - Capture screenshots from a synthetic local deployment, not production.
- Check the current tree and the full reachable Git history; deleting a secret in a later commit does not remove it from earlier commits.
- Rotate any credential that was ever committed, even after history is rewritten.
Request logs remove common credential-bearing query parameters before storage. Backups omit ADMIN, UUID, subscription token, sessions, and integration secrets. This reduces accidental disclosure; it does not make a public subscription URL safe to share.
git pull --ff-only
npm ci
npm run check
npm test
npx wrangler deploy --dry-run --config wrangler.local.toml
npx wrangler deploy --config wrangler.local.tomlCloudflare keeps Worker versions:
npx wrangler versions list --config wrangler.local.toml
npx wrangler rollback --config wrangler.local.tomlExport a console backup before changing stored settings. A code rollback does not automatically roll back KV data.
That is the default camouflage page. Open /login.
npx wrangler secret put ADMIN --config wrangler.local.tomlConfirm that the namespace ID is real and the binding is exactly KV. Confirm that Wrangler is logged into the account that owns the namespace.
Copy the subscription URL again from the same hostname. Tokens differ between workers.dev and a custom domain, and change after UUID rotation.
Log in again, confirm the KV binding, and check whether another proxy or extension blocks /assets/edgetunnel-ui.css or /assets/edgetunnel-admin.js.
Use a custom domain, enable gRPC for its Cloudflare zone, and keep the client SNI/servername on that hostname. Do not replace SNI with the preferred IP.
Check UUID/password, SNI, Host, path, destination port, Cloudflare egress restrictions, and Worker logs:
npx wrangler tail --config wrangler.local.tomlConfigure an operator-owned SUBAPI and SUBCONFIG, or use native format=clash / format=links.
npm run check
npm testOptional tests against a dedicated Cloudflare deployment:
npm run test:cloudflare:http
npm run test:cloudflareDo not run external protocol tests against production credentials or production KV.
Project layout:
src/
├── index.js Worker entry and route dispatch
├── config.js Configuration, KV, derived links, and logs
├── controllers/ Authentication, admin APIs, and subscriptions
├── core/ Socket lifecycle, dialing, HTTP tunnels, speed tests
├── protocols/ Protocol parsing and upstream adapters
├── subscriptions/native.js Native Clash/share links and preferred-IP substitution
├── ui/ Self-hosted pages, styles, scripts, and QR rendering
└── utils/ Input parsing, safety checks, pages, and diagnostics
workers/clash-sub/ Optional standalone Clash subscription Worker
test/ Node test suite
scripts/ Dedicated Cloudflare verification scripts
docs/images/ Sanitized documentation screenshots
Re_edgetunnel builds on ideas from cmliu/edgetunnel and zizifn/edgetunnel. The maintained code in this repository is modular and does not fetch either upstream repository at runtime.
See LICENSE. No warranty is provided. Operators remain responsible for deployment security, lawful use, and any traffic handled by their Worker.



