{"id":50453135,"url":"https://github.com/hamzayslmn/esp32-tunnel","last_synced_at":"2026-06-30T02:00:31.669Z","repository":{"id":348705691,"uuid":"1199459036","full_name":"HamzaYslmn/esp32-tunnel","owner":"HamzaYslmn","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-08T15:02:14.000Z","size":54,"stargazers_count":6,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-12T06:32:23.298Z","etag":null,"topics":["arduino","arduino-library","bore","esp32","esp32-arduino","freertos","localtunnel","ngrok-alternative","public-url","tunnel","websocket"],"latest_commit_sha":null,"homepage":"","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/HamzaYslmn.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"HamzaYslmn"}},"created_at":"2026-04-02T11:19:27.000Z","updated_at":"2026-06-08T15:18:50.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/HamzaYslmn/esp32-tunnel","commit_stats":null,"previous_names":["hamzayslmn/esp32-localtunnel"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/HamzaYslmn/esp32-tunnel","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HamzaYslmn%2Fesp32-tunnel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HamzaYslmn%2Fesp32-tunnel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HamzaYslmn%2Fesp32-tunnel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HamzaYslmn%2Fesp32-tunnel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/HamzaYslmn","download_url":"https://codeload.github.com/HamzaYslmn/esp32-tunnel/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HamzaYslmn%2Fesp32-tunnel/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34949234,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-30T02:00:05.919Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["arduino","arduino-library","bore","esp32","esp32-arduino","freertos","localtunnel","ngrok-alternative","public-url","tunnel","websocket"],"created_at":"2026-06-01T01:02:16.820Z","updated_at":"2026-06-30T02:00:31.660Z","avatar_url":"https://github.com/HamzaYslmn.png","language":"C","funding_links":["https://github.com/sponsors/HamzaYslmn"],"categories":[],"sub_categories":[],"readme":"# esp32-tunnel\n\n[![Arduino Library](https://img.shields.io/badge/Arduino-Library-blue?logo=arduino)](https://github.com/HamzaYslmn/esp32-tunnel)\n[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)\n[![GitHub Stars](https://img.shields.io/github/stars/HamzaYslmn/esp32-tunnel?style=social)](https://github.com/HamzaYslmn/esp32-tunnel)\n\nExpose your ESP32 web server to the public internet — no port forwarding, no ngrok, no cloud accounts, no companion devices.\n\nThree providers (pick one):\n- **Self-hosted** (default) — plain WebSocket relay, no TLS on ESP32, saves ~40 KB RAM\n- **[localtunnel](https://localtunnel.me)** — free HTTPS subdomain URLs (uses WiFiClientSecure)\n- **[bore](https://github.com/ekzhang/bore)** — free TCP tunnel via bore.pub (no login, no account, no TLS)\n\n## Features\n\n- **One API, three providers** — `tunnelSetup(SELFHOST | LOCALTUNNEL | BORE, ...)`\n- **Secure by default** — auto-generated per-device access key (self-hosted)\n- **Runs itself** — a FreeRTOS task drives the tunnel; `loop()` stays free (ESP32)\n- **ESPAsyncWebServer compatible** — or handler mode for direct, no-proxy handling\n- **Local + remote** — reach the device at `http://\u003cid\u003e.local` (mDNS) or the public URL\n- **Auto-reconnect** — WiFi drops, stale connections, tunnel expiry all handled\n- **Lean** — self-hosted uses plain WiFiClient (no TLS), ~40 KB less RAM\n\n## Footprint \u0026 trimming (ESP32)\n\nMeasured with arduino-cli. The library's own code is small — most of the flash is\nWiFi + TLS (the `https` relay) + your web server, not the tunnel.\n\n| Config | Flash | Trim |\n|---|---|---|\n| Relay + local web server + mDNS (typical) | ~1108 KB | — |\n| Handler/P2P mode + `#define TUN_MDNS 0` | ~1037 KB | **−71 KB** |\n\n- **Drop the local web server** — use handler mode (`tunnelSetup(SELFHOST, handler, \"host/id\")`); no `ESPAsyncWebServer`, saves its flash **and** its runtime `AsyncTCP` task/buffers.\n- **`#define TUN_MDNS 0`** before the include — drops mDNS (~30 KB) if you don't need `\u003cid\u003e.local`.\n- **`#define TUN_TASK_STACK 6144`** — trims the background-task stack (RAM). Keep ≥8 KB if your relay is `https` (TLS needs the headroom).\n- **CPU** is a non-issue — the task idles on a 10 ms poll (microsecond checks). Don't optimize it.\n\n## Dependencies\n\nAutomatically installed:\n- **[espfetch](https://github.com/HamzaYslmn/espfetch)** — neofetch-style system info + ESPLogger\n- **[esp-rtosSerial](https://github.com/HamzaYslmn/esp-rtosSerial)** — thread-safe Serial for FreeRTOS (`#include \u003crtosSerial.h\u003e`)\n\n## Installation\n\n### Arduino Library Manager\n\n1. Open Arduino IDE\n2. **Sketch → Include Library → Manage Libraries**\n3. Search for **\"esp32-tunnel\"**\n4. Click **Install**\n\n### Manual\n\nDownload ZIP → **Sketch → Include Library → Add .ZIP Library**\n\n## Quick Start\n\n### Self-hosted (default — lightweight, no TLS on ESP)\n\n```cpp\n#include \u003cESPAsyncWebServer.h\u003e\n#include \u003cesp32tunnel.h\u003e\n#include \u003cesp32tunnel_testpage.h\u003e\n\nAsyncWebServer server(80);\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.begin(\"SSID\", \"PASS\");\n  while (WiFi.status() != WL_CONNECTED) delay(500);\n\n  server.on(\"/\", HTTP_GET, [](AsyncWebServerRequest *r) {\n    r-\u003esend(200, \"text/html\", TUN_TEST_HTML);\n  });\n  server.begin();\n\n  tunnelSetup(SELFHOST, \"myserver.com/my-device\");\n}\n\nvoid loop() {}   // ESP32: tunnel runs in its own task — loop() is free\n```\n\n\u003e **ESP32:** `tunnelSetup()` starts a background FreeRTOS task, so you do **not**\n\u003e call `tunnelLoop()`. Leave `loop()` empty, use it for your own code, or\n\u003e `vTaskDelete(NULL)` to reclaim its stack. Enable request logs with `tunnelLog()`.\n\u003e Tune the task with `-DTUN_TASK_CORE=0 -DTUN_TASK_PRIO=2 -DTUN_TASK_STACK=10240`.\n\u003e\n\u003e **ESP8266** has no such task — there you must call `tunnelLoop()` in `loop()`.\n\n### Localtunnel (free HTTPS — no server needed)\n\n```cpp\n#include \u003cESPAsyncWebServer.h\u003e\n#include \u003cesp32tunnel.h\u003e\n#include \u003cesp32tunnel_testpage.h\u003e\n\nAsyncWebServer server(80);\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.begin(\"SSID\", \"PASS\");\n  while (WiFi.status() != WL_CONNECTED) delay(500);\n\n  server.on(\"/\", HTTP_GET, [](AsyncWebServerRequest *r) {\n    r-\u003esend(200, \"text/html\", TUN_TEST_HTML);\n  });\n  server.begin();\n\n  tunnelSetup(LOCALTUNNEL, \"my-esp32\");\n}\n\nvoid loop() { tunnelLoop(); }\n```\n\n### Bore (free TCP tunnel — no login, no account)\n\n```cpp\n#include \u003cESPAsyncWebServer.h\u003e\n#include \u003cesp32tunnel.h\u003e\n#include \u003cesp32tunnel_testpage.h\u003e\n\nAsyncWebServer server(80);\n\nvoid setup() {\n  Serial.begin(115200);\n  WiFi.begin(\"SSID\", \"PASS\");\n  while (WiFi.status() != WL_CONNECTED) delay(500);\n\n  server.on(\"/\", HTTP_GET, [](AsyncWebServerRequest *r) {\n    r-\u003esend(200, \"text/html\", TUN_TEST_HTML);\n  });\n  server.begin();\n\n  tunnelSetup(BORE);  // public URL: http://bore.pub:PORT\n}\n\nvoid loop() { tunnelLoop(); }\n```\n\n## API\n\nAll providers expose the same public API:\n\n| Function | Description |\n|---|---|\n| `tunnelSetup(...)` | Start tunnel (args differ per provider). ESP32: spawns the task |\n| `tunnelLog(bool)` | Enable/disable request logging |\n| `tunnelPublic()` | Disable auth — open access (call before `tunnelSetup`) |\n| `tunnelKey()` | The device's access key (`\"\"` if public) |\n| `tunnelLocalURL()` | Direct LAN URL `http://\u003cid\u003e.local` (mDNS) |\n| `tunnelLoop()` | ESP8266 only — drive tunnel in `loop()`. No-op once the ESP32 task runs |\n| `tunnelStop()` | Stop tunnel, end the task, free resources |\n| `tunnelURL()` | Public URL or `\"(connecting...)\"` |\n| `tunnelReady()` | `true` when tunnel is live |\n| `tunnelLastIP()` | Last requester's IP address |\n| `tunnelProviderName()` | `\"self-hosted\"`, `\"localtunnel\"`, or `\"bore\"` |\n\n### Self-hosted `tunnelSetup()`\n\n```cpp\ntunnelSetup(SELFHOST, \"host/device-id\");           // proxy mode (local port 80)\ntunnelSetup(SELFHOST, handler, \"host/device-id\");  // handler callback (no proxy)\ntunnelSetup(SELFHOST, \"host/device-id\", \"pass\");   // custom password (whole tunnel)\ntunnelSetup(SELFHOST, \"host/device-id\", routes);   // per-route auth\ntunnelPublic();                                    // before setup: OPEN access (no key)\ntunnelP2P(answerFn);                               // opt-in WebRTC P2P (see below)\n```\n\n#### Access paths — local or public, your choice\n\n| Path | URL | Latency | Needs |\n|---|---|---|---|\n| **Local (direct)** | `http://\u003cid\u003e.local/` or the LAN IP | ~5 ms | same WiFi |\n| **Public (WS relay)** | `https://server/\u003cid\u003e?key=\u003ckey\u003e` | ~200 ms | works anywhere |\n| **Public (P2P)** | via `p2p.js` | direct RTT | a WebRTC engine on the device |\n\nOn the same network, skip the tunnel entirely — the device serves its own pages at\n`http://\u003cid\u003e.local/` (mDNS, on by default; `tunnelLocalURL()`) or its LAN IP. The\ntunnel is only for reaching it from outside. The dashboard exposes all three as tabs.\n\n#### Access keys (secure by default)\n\nSelf-hosted devices are **private by default** — the ESP32 generates a random key on\nfirst boot (persisted in NVS, read via `tunnelKey()`). Send it as `?key=\u003ckey\u003e` or the\n`X-Tunnel-Key` header (the dashboard and `p2p.js` do this for you). Set your own via\nthe password overload above, or call `tunnelPublic()` to disable auth. Direct LAN\naccess hits the device's own server and isn't gated by the key.\n\n#### P2P mode (offload traffic from your relay)\n\nBy default the server relays every request/response (acts like an HTTP VPN). With\n`tunnelP2P()`, the server only brokers a WebRTC handshake and visitors connect\n**peer-to-peer** — your server stops carrying the traffic. Falls back to the relay\nautomatically when P2P can't be established (symmetric NAT, P2P disabled).\n\nThe WebRTC engine itself (ICE+DTLS+SCTP, e.g. [libpeer](https://github.com/sepfy/libpeer))\nis **not** bundled — it's the one piece that can't be header-only. You provide it in\n`answerFn`; the library does the signaling. Browser side loads `https://yourserver/p2p.js`.\nSee [`examples/P2P`](examples/P2P/P2P.ino).\n\n### Localtunnel `tunnelSetup()`\n\n```cpp\ntunnelSetup(LOCALTUNNEL);                            // random subdomain\ntunnelSetup(LOCALTUNNEL, \"my-esp32\");                // custom subdomain\ntunnelSetup(LOCALTUNNEL, \"my-esp32\", TUN_STRICT);    // fail if subdomain is taken\ntunnelSetup(LOCALTUNNEL, handler, \"my-esp32\");       // handler callback\n```\n\n### Bore `tunnelSetup()`\n\n```cpp\ntunnelSetup(BORE);                                   // bore.pub, random port\ntunnelSetup(BORE, \"your-server.com\");                // self-hosted bore server\n```\n\n## Providers\n\n| | Self-hosted | localtunnel | bore |\n|---|---|---|---|\n| Enum | `SELFHOST` | `LOCALTUNNEL` | `BORE` |\n| TLS on ESP32 | ❌ (plain WS) | ✅ (WiFiClientSecure) | ❌ (plain TCP) |\n| RAM usage | Low (~40 KB less) | Higher (TLS) | Low |\n| URL format | `http://host/device-id` | `https://xxx.loca.lt` | `http://bore.pub:PORT` |\n| Protocol | WebSocket relay | TCP pool | TCP tunnel |\n| Custom name | ✅ path-based | ✅ subdomain | ❌ random port |\n| Needs server | ✅ | ❌ | ❌ (bore.pub free) |\n| Account needed | ❌ | ❌ | ❌ |\n\n## Self-Hosted Server\n\nA free public server is available at `esp32-tunnel.onrender.com`.\nPick a unique device ID:\n\n```cpp\n#include \u003cesp32tunnel.h\u003e\n\nvoid setup() {\n  // ...\n  tunnelSetup(SELFHOST, \"esp32-tunnel.onrender.com/my-device\");\n  // Visit: http://esp32-tunnel.onrender.com/my-device\n}\n```\n\n\u003e **Note:** The relay server must accept plain WebSocket (`ws://`) connections.\n\u003e If your server is behind HTTPS-only (e.g. Render.com), you'll need a plain WS\n\u003e endpoint or a proxy that terminates TLS before reaching the ESP32 connection.\n\n### Deploy Your Own (Render.com)\n\n[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/HamzaYslmn/esp32-tunnel)\n\nOr manually:\n\n1. Fork this repo on GitHub\n2. Go to [render.com](https://render.com) → **New Web Service**\n3. Connect your fork\n4. Configure:\n\n| Setting | Value |\n|---|---|\n| **Root Directory** | `python` |\n| **Environment** | Add `PORT` = `8000` |\n| **Build Command** | `pip install uv \u0026\u0026 uv sync --active` |\n| **Start Command** | `uv run --active main.py` |\n\n5. Deploy — your server will be at `your-app.onrender.com`\n\nThe server provides:\n- **WebSocket tunnel** relay between visitors and ESP32\n- **Dashboard** at the root URL with live server stats\n- **Status API** at `/api/status` for health checks\n\n## Tuning\n\nOverride **before** `#include`:\n\n```cpp\n#define TUN_POOL      2         // localtunnel pool size (default: 2)\n#define TUN_STALE     30000     // recycle connections after 30s\n#define TUN_REALLOC   12        // re-allocate tunnel every 12h\n#define TUN_LOG       0         // disable tunnel Serial logs\n#include \u003cesp32tunnel.h\u003e\n```\n\n## How It Works\n\n### Self-hosted\n```\nBrowser → your-server (HTTPS) → WebSocket → ESP32 → JSON response → back\n```\n\n### localtunnel\n```\nBrowser → loca.lt (HTTPS) → TCP pool → ESP32 → HTTP response → back\n```\n\n### bore\n```\nBrowser → bore.pub:PORT (HTTP) → TCP tunnel → ESP32 localhost:80 → back\n```\n\n## Examples\n\n| Example | Description |\n|---|---|\n| [SelfHosted](examples/SelfHosted) | Full-featured self-hosted relay (auth, TLS, handler) |\n| [Localtunnel](examples/Localtunnel) | Free HTTPS URL via localtunnel.me |\n| [Bore](examples/Bore) | Free TCP tunnel via bore.pub (no login) |\n| [HandlerMode](examples/HandlerMode) | Direct request handling (no AsyncWebServer) |\n| [DualCore](examples/DualCore) | ESP32 FreeRTOS task on dedicated core |\n\n## Companion Libraries\n\n- [espfetch](https://github.com/HamzaYslmn/espfetch) — neofetch-style system info + ESPLogger (Python-style logging)\n- [esp-rtosSerial](https://github.com/HamzaYslmn/esp-rtosSerial) — thread-safe Serial reads for FreeRTOS\n\n## License\n\nMIT\n\n## Author\n\n**Hamza Yesilmen** — [@HamzaYslmn](https://github.com/HamzaYslmn)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhamzayslmn%2Fesp32-tunnel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhamzayslmn%2Fesp32-tunnel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhamzayslmn%2Fesp32-tunnel/lists"}