One command exposes any local service through NAT and firewalls to a live public HTTPS URL. No account, no config, no waiting.
Everything localhost.run and friends do, without the signup wall.
Wildcard TLS on every subdomain. Your webhook receivers, OAuth callbacks and payment IPNs work on the first try.
Anonymous tunnels are free: random subdomain, 4-hour lifetime, 1 Mbps. No email, no credit card, no waitlist.
20 dependency-light SDKs - Go, Node, Python, Rust, Swift, Java, C, Elixir... Embed the tunnel straight in your app.
Full 101-upgrade pass-through, verified end-to-end. Real-time apps, sockets and streams just work.
The server is one Go binary. Point it at your domain, get your own tunnel infra in minutes.
Custom subdomains, 30-day tunnels and 10 Mbps from $3/mo - card, PayPal or crypto.
One outbound WebSocket from your process. No inbound port, no router config, no firewall change.
flowchart LR V["visitor
https://quiet-frog-42.t.etun.uk"] -->|HTTPS 443| E["etunneld
wildcard TLS + plan limits"] E -->|wss /tunnel
multiplexed streams| C["SDK client
inside your process"] C -->|HTTP/1.1| A["localhost:3000
your app"] E -.->|101 upgrade
raw byte pipe| A
The SDK opens wss://<domain>/tunnel and says hello. The server answers welcome with the public URL and the plan's TTL.
The server matches the Host header to your tunnel, allocates a stream id and forwards the request as JSON metadata plus binary body frames.
The SDK replays the request against localhost:3000 and streams the response back over the same socket. Upgrades become a raw byte pipe.
The same protocol in 20 languages. Copy the snippet into your service and your app is reachable from the internet; run the file on its own and it doubles as a CLI. Free tunnels need no account.
const { connect } = require('@easytunnel/et-sdk'); async function main() { // 3000 may also be '127.0.0.1:3000' or 'localhost:3000' const tunnel = await connect('wss://t.etun.uk/tunnel', 3000); console.log('public URL:', tunnel.url); // the tunnel lives as long as this process (and the plan's TTL) process.on('SIGINT', () => { tunnel.close(); process.exit(0); }); } main().catch((err) => { console.error(err); process.exit(1); });
import { connect } from '@easytunnel/et-sdk'; const tunnel = await connect('wss://t.etun.uk/tunnel', 3000); console.log('public URL:', tunnel.url); console.log('expires on plan:', tunnel.plan);
import { connect } from 'jsr:@ggcode/etunnel'; const tunnel = await connect('wss://t.etun.uk/tunnel', 3000); console.log('public URL:', tunnel.url); Deno.addSignalListener('SIGINT', () => { tunnel.close(); Deno.exit(0); });
from etunnel import connect # port number or "host:port"; the tunnel runs on a background thread tunnel = connect("wss://t.etun.uk/tunnel", 3000) print("public URL:", tunnel["url"]) try: input("press Enter to stop\n") finally: tunnel["close"]()
package main import ( "context" "log" "github.com/topcheer/etunnel/sdk" ) func main() { tunnel, err := etunnel.Connect(context.Background(), etunnel.Options{ Server: "wss://t.etun.uk/tunnel", LocalAddr: "127.0.0.1:3000", }) if err != nil { log.Fatal(err) } defer tunnel.Close("app exiting") log.Printf("public URL: %s", tunnel.URL()) // start your own HTTP server, or just block while the tunnel serves select {} }
import dev.ggcode.Etunnel; public class Main { public static void main(String[] args) throws Exception { Etunnel.Tunnel tunnel = Etunnel.connect("wss://t.etun.uk/tunnel", 3000, null); System.out.println("public URL: " + tunnel.url); Runtime.getRuntime().addShutdownHook(new Thread(tunnel::close)); Thread.currentThread().join(); // keep the tunnel open } }
fun main() { val tunnel = Etunnel.connect("wss://t.etun.uk/tunnel", 3000) println("public URL: " + tunnel.url) Runtime.getRuntime().addShutdownHook(Thread { tunnel.close() }) Thread.currentThread().join() }
object Main { def main(args: Array[String]): Unit = { val url = Etunnel.connect("wss://t.etun.uk/tunnel", 3000) println("public URL: " + url) Thread.currentThread().join() } }
using System; var tunnel = await Etunnel.Connect("wss://t.etun.uk/tunnel", "3000"); Console.WriteLine("public URL: " + tunnel.Url); Console.CancelKeyPress += async (_, e) => { e.Cancel = true; await tunnel.Close(); }; await Task.Delay(Timeout.Infinite); // keep the tunnel open
import Foundation let tunnel = Etunnel(server: "wss://t.etun.uk/tunnel", local: "3000") let url = try await tunnel.connect() print("public URL: " + url)
import 'package:etunnel/etunnel.dart'; Future<void> main() async { final tunnel = await Etunnel.connect('wss://t.etun.uk/tunnel', '3000'); print('public URL: ' + tunnel.url); ProcessSignal.sigint.watch().listen((_) => exit(0)); }
require 'etunnel' tunnel = Etunnel.connect('wss://t.etun.uk/tunnel', 3000) puts "public URL: #{tunnel.url}" trap('INT') { exit } sleep
<?php require __DIR__ . '/vendor/autoload.php'; $tunnel = Etunnel::connect('wss://t.etun.uk/tunnel', 3000); echo "public URL: {$tunnel->url}\n"; $tunnel->run(); // serves requests until the process exits
use strict; use warnings; require './etunnel.pl'; # ships as one file, core modules only my $tunnel = etunnel_connect('wss://t.etun.uk/tunnel', '127.0.0.1:3000', {}); print "public URL: $tunnel->{url}\n"; sleep 3600; # the tunnel closes when the process exits
luarocks install ggcode.etunnel lua etunnel.lua 3000 # or as a script next to your app: # ETUNNEL_SERVER='wss://t.etun.uk/tunnel' lua /path/to/etunnel.lua 3000
(ns app.core (:require [etunnel :as et])) (defn -main [& _] (let [tunnel (et/connect! "wss://t.etun.uk/tunnel" 3000)] (println "public URL:" (:url @tunnel)) @(promise))) ;; block: the tunnel closes with the JVM
defmodule App.Application do use Application def start(_type, _args) do children = [ {Etunnel, server: "wss://t.etun.uk/tunnel", local: 3000} ] Supervisor.start_link(children, strategy: :one_for_one) end end # or, outside a supervision tree: # {:ok, tunnel} = Etunnel.start_link(server: "wss://t.etun.uk/tunnel", local: 3000) # IO.puts("public URL: " <> Etunnel.url(tunnel))
# the crate ships a binary, not a library: run it next to your service cargo install etunnel etunnel 3000 # or drive it as a child process from your own binary: # Command::new("etunnel").arg("3000").spawn()
# build once, run beside your service cc -O2 -o etunnel etunnel.c -lwebsockets -lcurl -lssl -lcrypto ETUNNEL_SERVER='wss://t.etun.uk/tunnel' ./etunnel 3000 # prints: TUNNEL_URL: https://<subdomain>.<domain>
c++ -std=c++17 -O2 -o etunnel etunnel.cpp -lssl -lcrypto
ETUNNEL_SERVER='wss://t.etun.uk/tunnel' ./etunnel 3000
Custom subdomain, 30-day lifetime and 10 Mbps come with an API key - pass token (and subdomain) to the same call, see pricing.
Start free. Upgrade when your demo needs a name of its own.