Deployment View¶
Configuration¶
Default config.yaml:
mode: "" # "server" or "client" (enforced by CLI)
log_level: info # debug, info, warn, error
proxy: "" # e.g. "socks5://user:pass@host:port" or "http://host:port"
xray:
uuid: "" # auto-generated on first run
relay_host: "" # e.g. relay.example.com
relay_port: 443
path: /tw
server: # only needed for `tw serve`
ssh_port: 2222
api_port: 50051
dashboard_port: 8080
relay_ssh_port: 22
relay_ssh_user: ubuntu
remote_port: 2222 # port exposed on relay for clients
temp_xray_port: 59000 # temp tunnel for relay config updates
client: # only needed for `tw connect`
ssh_user: tunnel
server_ssh_port: 2222 # server's SSH port on relay
tunnels:
- local_port: 5432 # listen on client localhost
remote_host: 127.0.0.1 # target on server (localhost only)
remote_port: 5432 # PostgreSQL
Mode field
The mode field prevents accidental cross-mode usage. When set, requireMode() in the CLI rejects server commands in client mode and vice versa. The dashboard adapts its UI based on this field.
File Layout (Server)¶
/etc/tw/config/ # or C:\ProgramData\tw\config\ on Windows
├── config.yaml # server configuration
├── id_ed25519 # server SSH private key
├── id_ed25519.pub # server SSH public key
├── ssh_host_ed25519_key # SSH host key
├── authorized_keys # client public keys (with permitopen)
├── relay/ # Terraform state + generated files
│ ├── main.tf # provider-specific Terraform
│ ├── cloud-init.yaml # rendered cloud-init
│ ├── terraform.tfstate
│ └── terraform.tfvars # cloud credentials (Hetzner/DO only)
└── users/ # per-user client configs
└── alice/
├── config.yaml # client config (send to client)
├── id_ed25519 # client private key (send to client)
├── id_ed25519.pub # client public key
└── .applied # marker: user registered on current relay
File Layout (Client)¶
/etc/tw/config/ # or C:\ProgramData\tw\config\ on Windows
├── config.yaml # client configuration (from server admin)
├── id_ed25519 # client SSH private key (from server admin)
└── id_ed25519.pub # client SSH public key
Override
Set TW_CONFIG_DIR environment variable to use a custom config directory.
Terraform Templates¶
Provider-specific Terraform files are embedded in the Go binary via go:embed and written to the relay directory during provisioning:
| Provider | Template | Instance | Firewall |
|---|---|---|---|
| AWS | aws.tf.tmpl | t3.micro, Ubuntu 24.04, us-east-1 | Security group: 80 + 443 ingress |
| Hetzner | hetzner.tf.tmpl | cx22, Ubuntu 24.04, nbg1 | Hetzner firewall: 80 + 443 |
| DigitalOcean | digitalocean.tf.tmpl | s-1vcpu-1gb, Ubuntu 24.04, fra1 | DO firewall: 80 + 443 |
All templates use user_data = file("${path.module}/cloud-init.yaml") and output relay_ip.
Xray version pinning
The Xray version installed on the relay is pinned to v26.2.6 via the terraform.XrayVersion constant in generate.go. This version is baked into both the cloud-init template and the manual install script via the --version flag on the official Xray installer. Keeping this in sync with the xray-core dependency in go.mod ensures protocol compatibility between the in-process Xray (server/client) and the relay's Xray.
Building¶
Requires Go 1.25+.
Version Injection¶
The Version variable in internal/version/version.go defaults to "dev" and is overridden at build time via -ldflags:
The Makefile auto-detects the version from the latest git tag (git describe --tags --always --dirty). Override with make build VERSION=v1.2.3. The GitHub Actions release workflow injects the exact tag name (e.g. v1.2.3) from github.ref_name.
The version is used in:
tw --version— CLI version output/api/status—versionfield in the JSON response
Makefile Targets¶
| Target | Command | Description |
|---|---|---|
make build | go build -ldflags "..." -o bin/tw ./cmd/tw | Build for current platform (version auto-detected) |
make build-linux | GOOS=linux GOARCH=amd64 go build -ldflags "..." ... | Cross-compile for Linux amd64 |
make build-windows | GOOS=windows GOARCH=amd64 go build -ldflags "..." ... | Cross-compile for Windows amd64 |
make build-darwin | GOOS=darwin GOARCH=amd64 go build -ldflags "..." ... | Cross-compile for macOS amd64 |
make build-all | Build Linux, Windows, and macOS | |
make run | Build + execute ./bin/tw | Build and run |
make clean | rm -rf bin/ | Remove build artifacts |
make proto | protoc --go_out=... --go-grpc_out=... | Regenerate gRPC stubs from .proto |
What tw serve Starts¶
flowchart TD
START([tw serve]) --> LOAD[Load / create config.yaml]
LOAD --> KEYS{Keys exist?}
KEYS -->|No| GEN["Generate ed25519 key pair<br/>+ host key + seed authorized_keys"]
KEYS -->|Yes| HASH
GEN --> HASH["Save config file hash as cfgHash"]
HASH --> INIT[Initialize core service]
INIT --> SSHD["Start SSH Server :2222<br/><i>dynamic auth + permitopen</i>"]
SSHD --> RELAY{relay_host<br/>configured?}
RELAY -->|No| GRPC
RELAY -->|Yes| UUID{UUID exists?}
UUID -->|No| GENUUID[Generate UUID, save to config]
UUID -->|Yes| XRAY
GENUUID --> XRAY
XRAY["Start Xray in-process<br/><i>dokodemo :2223 → relay SSH :22</i>"]
XRAY --> RTUNNEL["Open SSH reverse tunnel<br/><i>-R remote_port:localhost:ssh_port</i>"]
RTUNNEL --> GRPC["Start gRPC API :50051"]
GRPC --> READY([Server Ready])
style START fill:#1565C0,color:#fff
style READY fill:#00897B,color:#fff
style SSHD fill:#1976D2,color:#fff
style XRAY fill:#1976D2,color:#fff
style RTUNNEL fill:#1976D2,color:#fff
style GRPC fill:#1976D2,color:#fff - Loads (or creates)
config.yamlfrom the platform config directory - Generates an ed25519 SSH key pair (
id_ed25519/id_ed25519.pub) if missing - Generates an ed25519 host key (
ssh_host_ed25519_key) for the embedded SSH server - Seeds
authorized_keyswith the server's own public key - Saves
config.FileHash()ascfgHashfor change detection - Initializes the core service
- Starts the embedded SSH server on the configured port (default
:2222)- Dynamic
authorized_keys-- re-read on every authentication attempt permitopenenforcement -- restricts port forwarding per client key
- Dynamic
- If
xray.relay_hostis set:- Generates UUID if missing and saves to config
- Starts Xray in-process (dokodemo-door on
sshPort+1-> VLESS/XHTTP/TLS to relay) - Opens an SSH reverse tunnel through Xray to the relay (
-R remote_port:localhost:ssh_port)
- Starts the gRPC API server on the configured port (default
:50051)
What tw connect Starts¶
flowchart TD
START([tw connect]) --> LOAD[Load config, ensure keys exist]
LOAD --> VALIDATE["Validate relay_host +<br/>at least one tunnel mapping"]
VALIDATE --> UUID{UUID exists?}
UUID -->|No| GENUUID[Generate UUID, save to config]
UUID -->|Yes| HASH
GENUUID --> HASH
HASH["Save config file hash as cfgHash"]
HASH --> XRAY["Start Xray client<br/><i>dokodemo :54001 → relay :2222</i>"]
XRAY --> SSH["SSH via Xray tunnel<br/><i>public key auth</i>"]
SSH --> PORTS["Start local port listeners<br/><i>:5432, :8080, ...</i>"]
PORTS --> BLOCK(["Block — auto-reconnect on failure"])
style START fill:#00897B,color:#fff
style BLOCK fill:#00897B,color:#fff
style XRAY fill:#00ACC1,color:#fff
style SSH fill:#00ACC1,color:#fff
style PORTS fill:#00ACC1,color:#fff - Loads config and ensures keys exist
- Validates that
relay_hostand at least one tunnel mapping are configured - Generates UUID if missing
- Saves
config.FileHash()ascfgHashfor change detection - Starts Xray in client mode (dokodemo-door on
:54001-> VLESS/XHTTP/TLS to relay, targetingserver_ssh_port) - Opens a single SSH session through Xray to the server's embedded SSH (public key auth)
- Starts local port listeners for all configured tunnel mappings, forwarding through the SSH session
- Blocks until stopped (Ctrl-C), with automatic reconnection on failure