zero signup · automatic TLS · 20 language SDKs

your localhost,
on the internet

One command exposes any local service through NAT and firewalls to a live public HTTPS URL. No account, no config, no waiting.

$npx @easytunnel/et-sdk 3000

Built for developers

Everything localhost.run and friends do, without the signup wall.

🔒

Instant HTTPS

Wildcard TLS on every subdomain. Your webhook receivers, OAuth callbacks and payment IPNs work on the first try.

⚡

Zero signup

Anonymous tunnels are free: random subdomain, 4-hour lifetime, 1 Mbps. No email, no credit card, no waitlist.

💻

Any language

20 dependency-light SDKs - Go, Node, Python, Rust, Swift, Java, C, Elixir... Embed the tunnel straight in your app.

📈

WebSocket ready

Full 101-upgrade pass-through, verified end-to-end. Real-time apps, sockets and streams just work.

🌐

Self-hostable

The server is one Go binary. Point it at your domain, get your own tunnel infra in minutes.

💳

Pay for more

Custom subdomains, 30-day tunnels and 10 Mbps from $3/mo - card, PayPal or crypto.

How it works

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

1. Your process dials out

The SDK opens wss://<domain>/tunnel and says hello. The server answers welcome with the public URL and the plan's TTL.

2. A request lands

The server matches the Host header to your tunnel, allocates a stream id and forwards the request as JSON metadata plus binary body frames.

3. Your app answers

The SDK replays the request against localhost:3000 and streams the response back over the same socket. Upgrades become a raw byte pipe.

Quickstart

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.

tunnel.js embed npm install @easytunnel/et-sdk full docs
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); });
tunnel.ts embed npm install @easytunnel/et-sdk full docs
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);
tunnel.ts embed deno add jsr:@ggcode/etunnel full docs
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);
});
tunnel.py embed pip install etunnel full docs
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"]()
main.go embed go get github.com/topcheer/etunnel/sdk full docs
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 {}
}
Main.java embed dev.ggcode:etunnel (Maven Central, JDK 11+, no dependencies) full docs
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
    }
}
Main.kt embed dev.ggcode:etunnel (Maven Central, JDK 11+) full docs
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()
}
Main.scala embed dev.ggcode:etunnel (Maven Central, JDK 11+) full docs
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()
  }
}
Program.cs embed single file: sdks/csharp/Etunnel.cs (.NET 8+, zero dependencies) full docs
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
main.swift embed single file: sdks/swift/etunnel.swift (Foundation only) full docs
import Foundation

let tunnel = Etunnel(server: "wss://t.etun.uk/tunnel", local: "3000")
let url = try await tunnel.connect()
print("public URL: " + url)
bin/tunnel.dart embed dart pub add etunnel full docs
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));
}
tunnel.rb embed gem install etunnel faye-websocket full docs
require 'etunnel'

tunnel = Etunnel.connect('wss://t.etun.uk/tunnel', 3000)
puts "public URL: #{tunnel.url}"

trap('INT') { exit }
sleep
tunnel.php embed composer require ggcode/etunnel full docs
<?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
tunnel.pl embed cpanm JSON::PP (single file: sdks/perl/etunnel.pl) full docs
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
shell cli luarocks install ggcode.etunnel (LuaSec + lua-http) full docs
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
src/app/core.clj embed dev.ggcode/etunnel-clj {:mvn/version "1.2.4"} + cheshire full docs
(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
lib/app/application.ex embed {:etunnel, "~> 1.2"} full docs
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))
shell cli cargo install etunnel full docs
# 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()
shell cli single file: sdks/c/etunnel.c (libwebsockets + libcurl + OpenSSL) full docs
# 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>
shell cli single file: sdks/cpp/etunnel.cpp (websocketpp + OpenSSL) full docs
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.

Pricing

Start free. Upgrade when your demo needs a name of its own.

Free
$0
  • random subdomain
  • 4-hour tunnels
  • 1 Mbps
  • no account needed
Run it now
Starter
$3/mo
  • custom subdomain
  • 72-hour tunnels
  • 10 Mbps
  • API key + portal
Subscribe
Pro
$9/mo
  • 5 custom subdomains
  • 30-day tunnels
  • 10 Mbps
  • priority support
Subscribe