Expose your ESP32 web server to the public internet — no port forwarding, no ngrok, no cloud accounts, no companion devices.
Three providers (pick one):
- Self-hosted (default) — plain WebSocket relay, no TLS on ESP32, saves ~40 KB RAM
- localtunnel — free HTTPS subdomain URLs (uses WiFiClientSecure)
- bore — free TCP tunnel via bore.pub (no login, no account, no TLS)
- Unified API —
tunnelSetup(SELFHOST, ...)ortunnelSetup(LOCALTUNNEL, ...)ortunnelSetup(BORE) - No WiFiClientSecure for self-hosted — plain WiFiClient, ~40 KB less RAM
- ESPAsyncWebServer compatible — forwards requests to localhost (local proxy)
- Handler mode — optional callback for direct request handling (no proxy)
- FreeRTOS dual-core — Core 0 maintains tunnel, Core 1 serves requests
- Auto-reconnect — WiFi drops, stale connections, tunnel expiry all handled
- Security hardened — path sanitization, bounded reads
- Built-in logging — Python/FastAPI-style
logger.info(),logger.request()via ESPLogger - Thread-safe Serial — reads via esp-rtosSerial
- Simple API —
tunnelSetup(),tunnelURL(),tunnelReady()
Automatically installed:
- espfetch — neofetch-style system info + ESPLogger
- esp-rtosSerial — thread-safe Serial for FreeRTOS (
#include <rtosSerial.h>)
- Open Arduino IDE
- Sketch → Include Library → Manage Libraries
- Search for "esp32-tunnel"
- Click Install
Download ZIP → Sketch → Include Library → Add .ZIP Library
#include <ESPAsyncWebServer.h>
#include <esp32tunnel.h>
#include <esp32tunnel_testpage.h>
AsyncWebServer server(80);
void setup() {
Serial.begin(115200);
WiFi.begin("SSID", "PASS");
while (WiFi.status() != WL_CONNECTED) delay(500);
server.on("/", HTTP_GET, [](AsyncWebServerRequest *r) {
r->send(200, "text/html", TUN_TEST_HTML);
});
server.begin();
tunnelSetup(SELFHOST, "myserver.com/my-device");
}
void loop() { tunnelLoop(); }#include <ESPAsyncWebServer.h>
#include <esp32tunnel.h>
#include <esp32tunnel_testpage.h>
AsyncWebServer server(80);
void setup() {
Serial.begin(115200);
WiFi.begin("SSID", "PASS");
while (WiFi.status() != WL_CONNECTED) delay(500);
server.on("/", HTTP_GET, [](AsyncWebServerRequest *r) {
r->send(200, "text/html", TUN_TEST_HTML);
});
server.begin();
tunnelSetup(LOCALTUNNEL, "my-esp32");
}
void loop() { tunnelLoop(); }#include <ESPAsyncWebServer.h>
#include <esp32tunnel.h>
#include <esp32tunnel_testpage.h>
AsyncWebServer server(80);
void setup() {
Serial.begin(115200);
WiFi.begin("SSID", "PASS");
while (WiFi.status() != WL_CONNECTED) delay(500);
server.on("/", HTTP_GET, [](AsyncWebServerRequest *r) {
r->send(200, "text/html", TUN_TEST_HTML);
});
server.begin();
tunnelSetup(BORE); // public URL: http://bore.pub:PORT
}
void loop() { tunnelLoop(); }All providers expose the same public API:
| Function | Description |
|---|---|
tunnelSetup(...) |
Start tunnel (args differ per provider) |
tunnelLoop() |
Drive tunnel — call in loop() or FreeRTOS task |
tunnelStop() |
Stop tunnel and free resources |
tunnelURL() |
Public URL or "(connecting...)" |
tunnelReady() |
true when tunnel is live |
tunnelLastIP() |
Last requester's IP address |
tunnelProviderName() |
"self-hosted", "localtunnel", or "bore" |
tunnelSetup(SELFHOST, "host/device-id"); // proxy mode (local port 80)
tunnelSetup(SELFHOST, handler, "host/device-id"); // handler callback (no proxy)
tunnelSetup(SELFHOST, "host/device-id", "pass"); // global password (?key=pass)
tunnelSetup(SELFHOST, "host/device-id", routes); // per-route authtunnelSetup(LOCALTUNNEL); // random subdomain
tunnelSetup(LOCALTUNNEL, "my-esp32"); // custom subdomain
tunnelSetup(LOCALTUNNEL, "my-esp32", TUN_STRICT); // fail if subdomain is taken
tunnelSetup(LOCALTUNNEL, handler, "my-esp32"); // handler callbacktunnelSetup(BORE); // bore.pub, random port
tunnelSetup(BORE, "your-server.com"); // self-hosted bore server| Self-hosted | localtunnel | bore | |
|---|---|---|---|
| Enum | SELFHOST |
LOCALTUNNEL |
BORE |
| TLS on ESP32 | ❌ (plain WS) | ✅ (WiFiClientSecure) | ❌ (plain TCP) |
| RAM usage | Low (~40 KB less) | Higher (TLS) | Low |
| URL format | http://host/device-id |
https://xxx.loca.lt |
http://bore.pub:PORT |
| Protocol | WebSocket relay | TCP pool | TCP tunnel |
| Custom name | ✅ path-based | ✅ subdomain | ❌ random port |
| Needs server | ✅ | ❌ | ❌ (bore.pub free) |
| Account needed | ❌ | ❌ | ❌ |
A free public server is available at esp32-tunnel.onrender.com.
Pick a unique device ID:
#include <esp32tunnel.h>
void setup() {
// ...
tunnelSetup(SELFHOST, "esp32-tunnel.onrender.com/my-device");
// Visit: http://esp32-tunnel.onrender.com/my-device
}Note: The relay server must accept plain WebSocket (
ws://) connections. If your server is behind HTTPS-only (e.g. Render.com), you'll need a plain WS endpoint or a proxy that terminates TLS before reaching the ESP32 connection.
Or manually:
- Fork this repo on GitHub
- Go to render.com → New Web Service
- Connect your fork
- Configure:
| Setting | Value |
|---|---|
| Root Directory | python |
| Environment | Add PORT = 8000 |
| Build Command | pip install uv && uv sync --active |
| Start Command | uv run --active main.py |
- Deploy — your server will be at
your-app.onrender.com
The server provides:
- WebSocket tunnel relay between visitors and ESP32
- Dashboard at the root URL with live server stats
- Status API at
/api/statusfor health checks
Override before #include:
#define TUN_POOL 2 // localtunnel pool size (default: 2)
#define TUN_STALE 30000 // recycle connections after 30s
#define TUN_REALLOC 12 // re-allocate tunnel every 12h
#define TUN_LOG 0 // disable tunnel Serial logs
#include <esp32tunnel.h>Browser → your-server (HTTPS) → WebSocket → ESP32 → JSON response → back
Browser → loca.lt (HTTPS) → TCP pool → ESP32 → HTTP response → back
Browser → bore.pub:PORT (HTTP) → TCP tunnel → ESP32 localhost:80 → back
| Example | Description |
|---|---|
| SelfHosted | Full-featured self-hosted relay (auth, TLS, handler) |
| Localtunnel | Free HTTPS URL via localtunnel.me |
| Bore | Free TCP tunnel via bore.pub (no login) |
| HandlerMode | Direct request handling (no AsyncWebServer) |
| DualCore | ESP32 FreeRTOS task on dedicated core |
- espfetch — neofetch-style system info + ESPLogger (Python-style logging)
- esp-rtosSerial — thread-safe Serial reads for FreeRTOS
MIT
Hamza Yesilmen — @HamzaYslmn