From f0545ce3a300ef98867dad5afa090af818545372 Mon Sep 17 00:00:00 2001 From: Elias Renman Date: Fri, 26 Jun 2026 11:42:14 +0200 Subject: [PATCH] Add Telebit client wrapper --- Dockerfile.client | 21 ++++ README.md | 153 +++++++++++++++++++++++++++++ telebitctl | 238 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 412 insertions(+) create mode 100644 Dockerfile.client create mode 100644 README.md create mode 100755 telebitctl diff --git a/Dockerfile.client b/Dockerfile.client new file mode 100644 index 0000000..a958336 --- /dev/null +++ b/Dockerfile.client @@ -0,0 +1,21 @@ +FROM golang:1.24-alpine AS builder + +ARG TELEBIT_REF=master +ARG TARGETOS=linux +ARG TARGETARCH=amd64 + +RUN apk add --no-cache git + +WORKDIR /src +RUN git clone --depth 1 --branch "${TELEBIT_REF}" https://github.com/therootcompany/telebit.git . + +RUN go generate -mod=vendor ./... + +# The upstream vendored x/net revision does not build on current macOS toolchains. +RUN go get golang.org/x/net@v0.40.0 + +RUN CGO_ENABLED=0 GOOS="${TARGETOS}" GOARCH="${TARGETARCH}" go build -mod=mod -o /out/telebit ./cmd/telebit +RUN CGO_ENABLED=0 GOOS="${TARGETOS}" GOARCH="${TARGETARCH}" go build -mod=mod -o /out/signjwt ./cmd/signjwt + +FROM scratch AS export +COPY --from=builder /out/ / diff --git a/README.md b/README.md new file mode 100644 index 0000000..b42df44 --- /dev/null +++ b/README.md @@ -0,0 +1,153 @@ +# Telebit Relay + +Self-hosted Telebit relay and management API for running behind an external nginx wildcard HTTPS setup. + +## Server Setup + +Create `.env` from `.env.example`: + +```sh +cp .env.example .env +``` + +Set at least: + +```env +DOMAIN=t.renman.dev +TUNNEL_DOMAIN=t.renman.dev +SECRET=change-this-to-a-long-random-secret +POSTGRES_PASSWORD=change-this-postgres-password +TELEBIT_PORT=8085 +MGMT_PORT=6468 +``` + +Start the relay: + +```sh +docker compose up -d --build +``` + +The relay listens on `127.0.0.1:8085` through Docker's published port, and management is bound to `127.0.0.1:6468`. + +## Nginx + +Nginx should terminate HTTPS. The relay container is patched to run as a plain HTTP/WebSocket backend and should not handle Let's Encrypt itself. + +Use plain `http://` upstreams: + +```nginx +server { + listen 80; + listen [::]:80; + server_name t.example.com *.t.example.com; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + + server_name t.example.com *.t.example.com; + + ssl_certificate /etc/letsencrypt/live/t.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/t.example.com/privkey.pem; + + location = /ws { + proxy_pass http://127.0.0.1:8085/ws; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Authorization $http_authorization; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } + + location /api/ { + proxy_pass http://127.0.0.1:6468/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://127.0.0.1:8085; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +Reload: + +```sh +sudo nginx -t +sudo systemctl reload nginx +``` + +## Client Wrapper + +`telebitctl` manages local client keys and runs the Telebit client. + +Build local binaries with Docker: + +```sh +./telebitctl build +``` + +This writes: + +```text +bin/telebit +bin/signjwt +``` + +Create a new tunnel key for a subdomain: + +```sh +./telebitctl bind test +``` + +This creates `test.$DOMAIN` and writes `telebit-client.env`. + +Run the tunnel to a local service: + +```sh +./telebitctl up test 80 +``` + +That forwards: + +```text +https://test.$DOMAIN -> localhost:80 +``` + +`up` also installs the active client config to: + +```text +~/.config/telebit/client.env +``` + +Rotate the client key: + +```sh +./telebitctl auth refresh test +``` + +Release a subdomain: + +```sh +./telebitctl release test +``` + +## Notes + +- `.env`, `telebit-client.env`, and `bin/` are ignored by git. +- `SECRET` in `.env` is the management/relay master secret. Do not use it directly as a long-lived client key. +- Client keys are one-time registration secrets. Use `bind` or `auth refresh` to create device-specific keys. +- If `/ws` returns nginx `502`, nginx is probably still proxying to `https://127.0.0.1:8085`; it must use `http://127.0.0.1:8085`. diff --git a/telebitctl b/telebitctl new file mode 100755 index 0000000..1564bce --- /dev/null +++ b/telebitctl @@ -0,0 +1,238 @@ +#!/bin/sh +set -eu + +ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +SERVER_ENV="${TELEBIT_SERVER_ENV:-$ROOT_DIR/.env}" +CLIENT_ENV="${TELEBIT_CLIENT_ENV:-$ROOT_DIR/telebit-client.env}" +INSTALLED_CLIENT_ENV="${TELEBIT_INSTALLED_CLIENT_ENV:-$HOME/.config/telebit/client.env}" +TELEBIT_BIN="${TELEBIT_BIN:-$ROOT_DIR/bin/telebit}" +SIGNJWT_BIN="${SIGNJWT_BIN:-$ROOT_DIR/bin/signjwt}" + +usage() { + cat <<'USAGE' +Usage: + telebitctl build [os] [arch] + telebitctl bind + telebitctl release + telebitctl auth refresh [slug] + telebitctl up + +Examples: + ./telebitctl build + ./telebitctl bind test + ./telebitctl up test 80 + ./telebitctl auth refresh test + ./telebitctl release test +USAGE +} + +die() { + printf '%s\n' "$*" >&2 + exit 1 +} + +load_server_env() { + [ -f "$SERVER_ENV" ] || die "Missing $SERVER_ENV. Copy .env.example to .env and set DOMAIN, TUNNEL_DOMAIN, and SECRET." + set -a + # shellcheck disable=SC1090 + . "$SERVER_ENV" + set +a + + : "${DOMAIN:?DOMAIN is required in $SERVER_ENV}" + : "${TUNNEL_DOMAIN:?TUNNEL_DOMAIN is required in $SERVER_ENV}" + : "${SECRET:?SECRET is required in $SERVER_ENV}" + + VENDOR_ID="${VENDOR_ID:-renman-dev}" + MGMT_URL="${MGMT_URL:-https://$TUNNEL_DOMAIN/api}" +} + +need_bin() { + [ -x "$1" ] || die "Missing executable $1. Run: ./telebitctl build" +} + +admin_token() { + need_bin "$SIGNJWT_BIN" + "$SIGNJWT_BIN" \ + --expires-in 5m \ + --vendor-id "$VENDOR_ID" \ + --secret "$SECRET" \ + --machine-ppid "$SECRET" | awk '/^eyJ/ {print; exit}' +} + +current_slug() { + [ -f "$CLIENT_ENV" ] || return 1 + sed -n "s#^LOCALS=https:\\([^:]*\\):.*#\\1#p" "$CLIENT_ENV" | sed "s#\\.$DOMAIN\$##" | sed -n '1p' +} + +current_port() { + [ -f "$CLIENT_ENV" ] || return 1 + sed -n 's#^LOCALS=https:[^:]*:\([0-9][0-9]*\).*#\1#p' "$CLIENT_ENV" | sed -n '1p' +} + +write_client_env() { + slug="$1" + shared_key="$2" + port="${3:-$(current_port || true)}" + install="${4:-false}" + port="${port:-80}" + + cat > "$CLIENT_ENV" <&2 + die "Release failed with HTTP $code" + ;; + esac +} + +refresh_auth() { + load_server_env + slug="${1:-$(current_slug || true)}" + slug="${slug:-test}" + token=$(admin_token) + delete_device "$slug" "$token" false >/dev/null 2>&1 || true + bind_slug "$slug" +} + +up_client() { + load_server_env + need_bin "$TELEBIT_BIN" + slug="${1:?up requires a slug}" + port="${2:?up requires a local port}" + key="$(client_key || true)" + [ -n "$key" ] || die "No client key found. Run: ./telebitctl bind $slug" + write_client_env "$slug" "$key" "$port" true >/dev/null + unset SECRET DOMAIN TUNNEL_DOMAIN MGMT_URL POSTGRES_PASSWORD TELEBIT_PORT MGMT_PORT VENDOR_ID + cd "$HOME" + exec "$TELEBIT_BIN" --env "$INSTALLED_CLIENT_ENV" +} + +build_client() { + os="${1:-}" + arch="${2:-}" + + if [ -z "$os" ]; then + case "$(uname -s)" in + Darwin) os=darwin ;; + Linux) os=linux ;; + *) die "Unsupported OS. Pass os and arch explicitly." ;; + esac + fi + + if [ -z "$arch" ]; then + case "$(uname -m)" in + arm64|aarch64) arch=arm64 ;; + x86_64|amd64) arch=amd64 ;; + *) die "Unsupported arch. Pass os and arch explicitly." ;; + esac + fi + + mkdir -p "$ROOT_DIR/bin" + docker build \ + -f "$ROOT_DIR/Dockerfile.client" \ + --build-arg "TELEBIT_REF=${TELEBIT_REF:-master}" \ + --build-arg "TARGETOS=$os" \ + --build-arg "TARGETARCH=$arch" \ + --output "type=local,dest=$ROOT_DIR/bin" \ + "$ROOT_DIR" + + chmod +x "$ROOT_DIR/bin/telebit" "$ROOT_DIR/bin/signjwt" + printf 'Built bin/telebit and bin/signjwt for %s/%s\n' "$os" "$arch" +} + +cmd="${1:-}" +case "$cmd" in + build) + shift + build_client "$@" + ;; + bind) + shift + bind_slug "$@" + ;; + release) + shift + release_slug "$@" + ;; + auth) + shift + sub="${1:-}" + [ "$sub" = "refresh" ] || die "Unknown auth command: ${sub:-}. Expected: auth refresh [slug]" + shift || true + refresh_auth "$@" + ;; + up) + shift + up_client "$@" + ;; + -h|--help|help|"") + usage + ;; + *) + usage + exit 1 + ;; +esac