Building Block View¶
Level 1 -- System Overview¶
graph TB
subgraph SN["Server Network"]
S["<b>Server</b><br/>tw server start"]
SSHD["SSH Server<br/>:2222"]
XS["Xray<br/>:2223"]
API["gRPC API<br/>:50051"]
S --- SSHD
S --- XS
S --- API
end
subgraph RELAY["Relay VM (Public Cloud)"]
CADDY["Caddy<br/>:443 (mTLS gate)"]
XR["Xray<br/>vless-in-<id><br/>127.0.0.1:port+10000"]
OSSH["OpenSSH<br/>127.0.0.1:22"]
CADDY -->|"handle /tw/<id>*<br/>+ cert CN match"| XR
XR -->|"freedom<br/>(loopback only)"| OSSH
end
subgraph CN["Client Network"]
CL["<b>Client</b><br/>tw client connect"]
XC["Xray<br/>:54001"]
FWD["Port Forwards<br/>:5432, :8080, ..."]
CL --- XC
CL --- FWD
end
XS ==>|"VLESS+XHTTP<br/>mTLS :443"| CADDY
XC ==>|"VLESS+XHTTP<br/>mTLS :443"| CADDY
SSHD -.->|"reverse tunnel<br/>-R 2222"| OSSH
FWD -.->|"forward tunnel<br/>-L ports"| SSHD
style S fill:#1565C0,color:#fff
style CL fill:#00897B,color:#fff
style CADDY fill:#5C6BC0,color:#fff
style XR fill:#5C6BC0,color:#fff
style OSSH fill:#5C6BC0,color:#fff
style SSHD fill:#1976D2,color:#fff
style XS fill:#1976D2,color:#fff
style API fill:#1976D2,color:#fff
style XC fill:#00ACC1,color:#fff
style FWD fill:#00ACC1,color:#fff Server (tw server start)¶
The server brings up its internal services:
- SSH Server -- an embedded SSH server (Go
golang.org/x/crypto/ssh) that listens on a configurable port (default:2222), supportsdirect-tcpipport forwarding, readsauthorized_keysdynamically, and enforcespermitopen(and optionalsingle-session) restrictions per client key - Xray Instance -- in-process xray-core creating a VLESS+XHTTP+mTLS tunnel to the relay on the server's own path
/tw/<server-id>; presents the per-server X.509 client certificate (usage: "client-cert", CN = server-id) so the relay's Caddyclient_authgate admits it; dokodemo-door inbound onsshPort+1(orserver.xray_port) forwards to the relay's SSH port - Reverse Tunnel -- SSH reverse port forward (
-R) through Xray, exposing the server's SSH on the relay at its admin-assignedremote_port - Per-server CA -- a small certificate authority (
internal/pki) generated on first run that issues the client certificate presented at the relay; the CA public certificate is shipped to the relay's trust pool (at provisioning for the admin's own entry, at enrollment for joined servers), the signing key never leaves the server - API Server -- a gRPC service exposing status and management operations (
:50051) - Dashboard -- started alongside the server when
server.dashboard_portis set
A server that is not the relay's admin joins via a zero-file invite: the admin mints a one-time code (tw relay invite), the server redeems it (tw join <relay-host> <code>), a SPAKE2 exchange derives a shared key from the code, both sides display a short authentication string (SAS) that the humans read back to each other, and only on an exact match does the admin's side enroll the tenant and hand back the grant (relay host, path, port, mode signature) over the same encrypted channel.
Relay¶
The relay is a lightweight VM provisioned by the admin machine (mode relay) via tw relay create — through Terraform on a cloud provider, or via a generated install script on a bring-your-own VM ("manual" provider). It runs:
- Caddy -- reverse proxy on
:443, automatic server TLS via Let's Encrypt, mutual TLS (client_auth verify_if_given, TLS 1.3 only) verifying a presented client certificate against a trust pool holding one CA per tenant — a bare handshake with no certificate is allowed too, so certless enrollees can reach/enroll. Each tenant gets ahandleblock matching both its path (/tw/<server-id>*) and its verified certificate CN (CN=<server-id>), proxying h2c to that tenant's loopback VLESS inbound, plus an@enroll_<server-id>block routing/enroll/<tok>/*to its local invite listener (no certificate required there — the one-time code is the gate); anything else gets a 404. This is the relay's admission gate — see Relay Authentication - Xray (standalone, pinned version) -- one
vless-in-<server-id>inbound per tenant on127.0.0.1:<remote-port>+10000with XHTTP transport, anapi-indokodemo inbound on127.0.0.1:10085exposingHandlerService/StatsService/RoutingService, a freedom outbound whosefinalRulesallow loopback destinations only, and per-tenant allow (ports22,<remote-port>) / deny (blackhole) routing rules - SSH -- OpenSSH on
127.0.0.1:22(--ssh-openprovisions it on0.0.0.0instead); password authentication disabled. The tw-managedauthorized_keysholds the admin's key (pinnedfrom="127.0.0.1"unless--ssh-open) plus one restricted line per tenant (from="127.0.0.1",restrict,port-forwarding,permitopen=<sentinel>,permitlisten=<own remote-port>) — only the admin can shell in; tenants can only publish their own reverse tunnel - Firewall (ufw) -- only ports 80 and 443 open (plus 22 with
--ssh-open)
Supported cloud providers: Hetzner, DigitalOcean, AWS — plus Manual (bring your own VM).
Client (tw client connect)¶
The client starts:
- Xray Instance -- in-process xray-core with dokodemo-door inbound on
:54001(client.xray_port) forwarding to the server's remote SSH port on the relay - Forward Tunnel -- SSH local port forwards (
-L) through Xray, mapping multiple local ports to server services over a single SSH session; listeners bindclient.listen_address(default127.0.0.1)
Clients receive their identity over a zero-file invite: the server mints a one-time code (tw server user invite), the client redeems it (tw join), generating its own SSH key and a certificate signing request locally — neither private key ever transits — and the server signs the CSR and sends back the coordinates over the same SPAKE2-encrypted, SAS-confirmed channel. The result is stored as a context, switched with tw config use-context like any other; tw config export/import remain available for moving an existing context's identity between a client's own machines.
Dashboard (tw dashboard)¶
The dashboard is a web UI served by an embedded HTTP server. It provides:
- Tee Handler -- wraps the
sloghandler chain to duplicate log records into a ring buffer. The SSE/api/logsendpoint streams entries from this buffer to connected browsers in real time. - SSE Hub -- manages progress event sessions for long-running operations (relay provisioning, user creation, server start/stop). Each operation gets a unique session ID; the browser subscribes via
/api/events/{id}. - WebSocket SSH Terminal -- the
/api/relay/sshendpoint upgrades to a WebSocket and bridges it to an interactive SSH session on the relay via the Xray tunnel. The browser runs xterm.js to render the terminal. Binary messages carry stdin/stdout data; text messages carry JSON control frames (e.g., terminal resize). - Mode-aware UI -- pages and navigation adapt based on the configured
mode(relay,server, orclient). Relay-only pages (relay provisioning, enrolled servers) and server-only pages (users, apps) are hidden in the other roles. Admin enroll/un-enroll actions remain CLI-only. - Settings Management -- the config page exposes all server, client, and Xray settings through form-based editing. Changes are persisted via REST API (
/api/settings/server,/api/settings/xray,/api/settings/client) and the config YAML preview auto-refreshes after each save.
Dashboard Component Architecture¶
graph LR
subgraph Browser
UI[Web UI]
XTERM[xterm.js]
end
subgraph "Dashboard Server"
HTTP["HTTP Server<br/>:8080"]
SSE_HUB["SSE Hub<br/>/api/events"]
LOG_SSE["Log SSE<br/>/api/logs"]
WS["WebSocket<br/>/api/relay/ssh"]
REST["REST API<br/>/api/*"]
end
subgraph Backend
OPS[Ops Layer]
LOGBUF["Log Buffer<br/>(ring, 500)"]
SLOG["slog<br/>teeHandler"]
end
UI -->|"EventSource"| SSE_HUB
UI -->|"EventSource"| LOG_SSE
UI -->|"fetch"| REST
XTERM -->|"WebSocket"| WS
REST --> OPS
SSE_HUB --> OPS
WS --> OPS
SLOG --> LOGBUF
LOGBUF --> LOG_SSE
style UI fill:#26A69A,color:#fff
style XTERM fill:#26A69A,color:#fff
style HTTP fill:#1565C0,color:#fff
style SSE_HUB fill:#1976D2,color:#fff
style LOG_SSE fill:#1976D2,color:#fff
style WS fill:#1976D2,color:#fff
style REST fill:#1976D2,color:#fff
style OPS fill:#00897B,color:#fff
style LOGBUF fill:#00897B,color:#fff
style SLOG fill:#00897B,color:#fff Level 2 -- Project Structure¶
tw/
├── cmd/
│ └── tw/ # binary entry point (main.go)
├── internal/
│ ├── cli/ # cobra commands, grouped by role
│ │ ├── root.go # root command, --log-level/--log-format/--config-dir, requireMode() + mode signature check
│ │ ├── groups.go # tw server / tw server user command groups
│ │ ├── relay.go # tw relay group (relay role)
│ │ ├── create_relay.go # tw relay create (wizard; cloud or manual, --ssh-open)
│ │ ├── destroy_relay.go # tw relay destroy
│ │ ├── relay_addserver.go # tw relay add-server (same-machine self-enrollment)
│ │ ├── relay_getservers.go # tw relay get-servers
│ │ ├── relay_invite.go # tw relay invite (mint code, wait, SAS-approve)
│ │ ├── relay_unenroll.go # tw relay un-enroll-server
│ │ ├── relay_ssh.go # tw relay ssh (+ _unix.go / _windows.go)
│ │ ├── serve.go # tw server start
│ │ ├── join.go # tw join <relay-host> <code> (role-neutral, no mode required)
│ │ ├── invite_user.go # tw server user invite (zero-file client enrollment; wizard when run without a name)
│ │ ├── list_users.go # tw server user list
│ │ ├── delete_user.go # tw server user delete
│ │ ├── apply_users.go # tw server user apply / unregister
│ │ ├── app.go # tw server app list/create/edit/delete
│ │ ├── client.go # tw client group, tw client listen
│ │ ├── connect.go # tw client connect
│ │ ├── test_relay.go # tw relay|server|client test
│ │ ├── status.go # tw status (+ per-role status)
│ │ ├── config.go # tw config *-context / import / export / view
│ │ ├── dashboard.go # tw dashboard
│ │ ├── proxy.go # tw proxy (show) / set / clear
│ │ ├── service.go # tw service install/uninstall/start/stop
│ │ ├── completion.go # shell completion
│ │ └── coverage_test.go # fails the build unless e2e/coverage.yaml maps every command
│ ├── config/ # YAML config, platform-specific paths
│ │ ├── config.go # Load/Save, Dir/RelayDir/UsersDir, FileHash(), ModeAuth, CanonicalMode
│ │ └── context.go # context index (contexts.yaml), ContextsDir, ShortID
│ ├── pki/ # per-server CA + client cert issuance (ECDSA P-256)
│ │ └── pki.go # GenerateCA(), IssueClientCert(), GenerateKeyAndCSR(), SignClientCSR() — the last two back the invite flow's client-local CSRs
│ ├── enroll/ # zero-file invite protocol: SPAKE2 PAKE, SAS, single-use codes
│ │ ├── invite.go # Mint/Redeem/ParseCode — code format <tok>-NN-word-word, burn-on-first-attempt
│ │ ├── pake.go # SPAKE2 session + SAS derivation (HKDF-SHA256, "tw-sas v1")
│ │ ├── issuer.go # Handler: the issuer side of the /enroll HTTP exchange (mint → offer → grant)
│ │ ├── enrollee.go # RunEnrollee: the redeemer side (tw join)
│ │ └── wire.go # HTTP wire format between issuer and enrollee, tunnelled over the relay
│ ├── cryptobox/ # sealed context bundles (argon2id + AES-256-GCM, TWBOX1)
│ ├── auth/ # auth primitives (Credentials, Claims, JWT provider)
│ ├── ops/ # business logic shared by CLI + dashboard
│ │ ├── ops.go # Ops struct, config change detection, lifecycle
│ │ ├── modeauth/ # ed25519 signature over (mode, identity) — tamper-evidence
│ │ ├── keys.go # SSH key + CA/client-cert management (ensureCerts, applyClientCertPaths)
│ │ ├── identity.go # deriveServerID, enrollPort (tunnel port + 20000)
│ │ ├── join.go # JoinRequest/JoinResponse payload types (now travel inside invite grants, not files)
│ │ ├── joinflow.go # Ops.Join: the tw join enrollee side (SPAKE2 offer, applies the grant)
│ │ ├── invite.go # Ops.InviteServer / InviteUser: the tw relay invite / tw server user invite issuer side
│ │ ├── addserver.go # AddLocalServer: same-machine self-enrollment (tw relay add-server)
│ │ ├── enroll.go # EnrollServer: registry add + full relay rewrite + gRPC live-add
│ │ ├── unenroll.go # UnenrollServer: block re-auth, drop live state, clean files
│ │ ├── registry.go # enrolled-server registry (servers/ dir, relay role)
│ │ ├── oplock.go # local file lock serializing enroll/un-enroll
│ │ ├── relaygrpc.go # live tenant add via Xray gRPC (AddInbound/AddRule)
│ │ ├── context.go # context list/switch/import/export
│ │ ├── profilebundle.go # seal/unseal the live profile as a context bundle
│ │ ├── setup.go # first-run setup
│ │ ├── cloud.go # cloud provider credential testing
│ │ ├── user.go # user CRUD, online tracking, relay UUID hot-add/remove
│ │ ├── client.go # clientManager lifecycle (start/stop/reconnect)
│ │ ├── relay.go # provisioning, relay SSH helpers, manual install script
│ │ ├── server.go # serverManager lifecycle (start/stop/restart)
│ │ └── terraform.go # Terraform init/apply/destroy wrappers
│ ├── logging/ # structured logging
│ │ └── logging.go # Setup(), SetLevel(), dynamic slog.LevelVar
│ ├── api/ # gRPC API service (JSON codec; proto is documentation only)
│ │ ├── server.go # gRPC server bootstrap
│ │ ├── service.go # service implementation
│ │ ├── handlers.go # RPC handlers
│ │ ├── client.go # gRPC client for CLI commands
│ │ └── codec.go # JSON gRPC codec
│ ├── ssh/ # SSH key generation, embedded server, tunnels
│ │ ├── server.go # embedded SSH server with dynamic auth + permitopen + single-session
│ │ ├── client.go # SSH client helpers
│ │ ├── forward.go # client-side local port forwarding (-L)
│ │ ├── reverse.go # server-side reverse port forwarding (-R)
│ │ └── keygen.go # ed25519 key pair generation
│ ├── stats/ # opt-in bandwidth analytics (collector, prometheus, counting writers)
│ ├── xray/ # in-process xray-core
│ │ └── xray.go # server + client config builders, instance management
│ ├── relay/
│ │ ├── caddy/ # relay Caddyfile renderer (mTLS gate + per-tenant handles)
│ │ │ ├── config.go # RenderCaddyfile(), Server/Config types
│ │ │ └── Caddyfile.tmpl # client_auth verify_if_given, trust_pool, tls1.3, path+CN matchers, per-tenant /enroll routes
│ │ ├── xray/ # relay Xray config renderer (multi-tenant)
│ │ │ ├── config.go # RenderConfig(), Tenant (VlessInPort = remote_port+10000)
│ │ │ ├── relayconfig.json.tmpl # api-in :10085, per-tenant inbounds/rules, freedom finalRules (loopback only)
│ │ │ └── tenant.go # single-tenant fragments for the gRPC live-add path
│ │ └── terraform/ # cloud-init + Terraform templates (go:embed)
│ │ ├── cloud-init.yaml.tmpl
│ │ ├── install-script.sh.tmpl # manual install script template
│ │ ├── aws.tf.tmpl
│ │ ├── hetzner.tf.tmpl
│ │ ├── digitalocean.tf.tmpl
│ │ └── generate.go # template rendering, XrayVersion constant
│ ├── dashboard/ # web dashboard
│ │ ├── server.go # HTTP server, routes, template parsing
│ │ ├── embed.go # go:embed for templates/ and static/
│ │ ├── logbuf.go # ring buffer, teeHandler, subscriber support
│ │ ├── handlers_api.go # REST API (status, config, users, relay, server/client control)
│ │ ├── handlers_sse.go # SSE hub, progress event streaming
│ │ ├── handlers_ws.go # WebSocket SSH terminal bridge
│ │ ├── handlers_pages.go # HTML page handlers
│ │ ├── templates/
│ │ │ ├── layout.html # base layout
│ │ │ ├── partials/nav.html # navigation (mode-aware)
│ │ │ └── pages/ # index, setup, config, relay, relay_home, relay_wizard,
│ │ │ # servers, users, user_detail, bandwidth, apps, app_new/edit
│ │ └── static/ # css/ + js/ (app, status, config, relay, servers, users,
│ │ # bandwidth, apps) + vendor xterm.js
│ ├── service/ # native service install/run (systemd / SCM / launchd, build tags)
│ ├── tunnel/ # near-empty placeholder (real tunneling is internal/ssh over internal/xray)
│ └── version/ # Version variable (ldflags-injected)
├── proto/ # gRPC protobuf definitions (documentation only — wire format is JSON)
│ └── api/v1/
│ └── service.proto
├── e2e/ # full-product e2e suite (Docker Compose, 19 scenarios, `make e2e`)
│ ├── docker-compose.yaml # relay (systemd) + admin/server/server2/client containers
│ ├── e2e_test.go # scenario runner (dependency order)
│ └── coverage.yaml # command → scenario map enforced by cli/coverage_test.go
├── docs/
│ └── architecture/
├── go.mod
├── go.sum
└── Makefile