# Upterm

[Upterm](https://github.com/owenthereal/upterm) is an open-source tool enabling developers to share terminal sessions securely over the web. It’s perfect for remote pair programming, accessing computers behind NATs/firewalls, remote debugging, and more.

This is a [blog post](https://owenou.com/upterm) to describe Upterm in depth.

## :movie_camera: Quick Demo

[![demo](https://raw.githubusercontent.com/owenthereal/upterm/gh-pages/demo.gif)](https://asciinema.org/a/efeKPxxzKi3pkyu9LWs1yqdbB)

## :rocket: Getting Started

## Installation

### Mac

```console
brew install --cask owenthereal/upterm/upterm
```

#### Migrating from Formula to Cask

If you previously installed upterm using the Homebrew formula (without `--cask`), you'll need to migrate to the Cask version:

```console
# Uninstall the old formula version
brew uninstall upterm

# Install the new Cask version
brew install --cask owenthereal/upterm/upterm
```

**Note:** Running `brew upgrade` with the old formula installed will fail with an error. Follow the migration steps above to resolve this.

### Windows

```powershell
scoop bucket add upterm https://github.com/owenthereal/scoop-upterm
scoop install upterm
```

### Standalone

`upterm` can be easily installed as an executable. Download the latest [compiled binaries](https://github.com/owenthereal/upterm/releases) and put it in your executable path.

### From source

```console
git clone git@github.com:owenthereal/upterm.git
cd upterm
go install ./cmd/upterm/...
```

## :wrench: Basic Usage

1. Host starts a terminal session:

   ```console
   upterm host
   ```

1. Host retrieves and shares the SSH connection string:

   ```console
   upterm session current
   ```

1. Client connects using the shared string:

   ```console
   ssh TOKEN@uptermd.upterm.dev
   ```

## :blue_book: Quick Reference

Dive into more commands and advanced usage in the [documentation](docs/upterm.md).
Below are some notable highlights:

### Command Execution

Host a session with any desired command:

```console
upterm host -- docker run --rm -ti ubuntu bash
```

### Access Control

Host a session with specified client public key(s) authorized to connect:

```console
upterm host --authorized-keys PATH_TO_PUBLIC_KEY
```

Authorize specified GitHub, GitLab, SourceHut, Codeberg users with their corresponding public keys:

```console
upterm host --github-user username
upterm host --gitlab-user username
upterm host --srht-user username
upterm host --codeberg-user username
```

### Force command

Host a session initiating `tmux new -t pair-programming`, while ensuring clients join with `tmux attach -t pair-programming`.
This mirrors functionality provided by tmate:

```console
upterm host --force-command 'tmux attach -t pair-programming' -- tmux new -t pair-programming
```

### WebSocket Connection

In scenarios where your host restricts ssh transport, establish a connection to `uptermd.upterm.dev` (or your self-hosted server) via WebSocket:

```console
upterm host --server wss://uptermd.upterm.dev -- bash
```

Clients can connect to the host session via WebSocket as well:

```console
ssh -o ProxyCommand='upterm proxy wss://TOKEN@uptermd.upterm.dev' TOKEN@uptermd.upterm.dev:443
```

### Debug GitHub Actions

`upterm` can be integrated with GitHub Actions to enable real-time SSH debugging, allowing you to interact directly with the runner system during workflow execution. This is achieved through [action-upterm](https://github.com/owenthereal/action-upterm), which sets up an `upterm` session within your CI pipeline.

To get started, include `action-upterm` in your GitHub Actions workflow as follows:

```yaml
name: CI
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Setup upterm session
      uses: owenthereal/action-upterm@v1
```

This setup allows you to SSH into the workflow runner whenever you need to troubleshoot or inspect the execution environment. Find the SSH connection string in the `Checks` tab of your Pull Request or in the workflow logs.

For comprehensive details on configuring and using this integration, visit the [action-upterm GitHub repo](https://github.com/owenthereal/action-upterm).

## :bulb: Tips

### Resolving Tmux Session Display Issue

**Issue**: The command `upterm session current` does not display the current session when used within Tmux.

**Cause**: This occurs because `upterm session current` requires the `UPTERM_ADMIN_SOCKET` environment variable, which is set in the specified command. Tmux, however, does not carry over environment variables not on its default list to any Tmux session unless instructed to do so ([Reference](http://man.openbsd.org/i386/tmux.1#GLOBAL_AND_SESSION_ENVIRONMENT)).

**Solution**: To rectify this, add the following line to your `~/.tmux.conf`:

```conf
set-option -ga update-environment " UPTERM_ADMIN_SOCKET"
```

### Identifying Upterm Session

**Issue**: It might be unclear whether your shell command is running in an upterm session, especially with common shell commands like `bash` or `zsh`.

**Solution**: Use `upterm session current -o go-template` to customize your shell prompt with session info. Add to your `~/.bashrc` or `~/.zshrc`:

```bash
# Show 🆙 emoji and connected client count when in upterm session
export PS1='$(upterm session current -o go-template="🆙 {{.ClientCount}} " 2>/dev/null)'"$PS1"
```

**Template variables available** (Go templates use PascalCase field names):

- `{{.SessionID}}` - Session ID
- `{{.ClientCount}}` - Number of connected clients
- `{{.Host}}` - Server host
- `{{.Command}}` - Command being shared
- `{{.ForceCommand}}` - Force command (if set)

> **Note**: JSON output (`-o json`) uses camelCase keys (e.g., `sessionId`, `clientCount`).
>
> **Tip**: The same template mechanism can be used for terminal titles or other integrations.

**Alternative** (simpler, without client count):

```bash
export PS1="$([[ ! -z "${UPTERM_ADMIN_SOCKET}"  ]] && echo -e '\xF0\x9F\x86\x99 ')$PS1"
```

## :gear: How it works

Upterm starts an SSH server (a.k.a. `sshd`) in the host machine and sets up a reverse SSH tunnel to a [Upterm server](https://github.com/owenthereal/upterm/tree/master/cmd/uptermd) (a.k.a. `uptermd`).
Clients connect to a terminal session over the public internet via `uptermd` using `ssh` or `ssh` over WebSocket.

![upterm flowchart](https://raw.githubusercontent.com/owenthereal/upterm/gh-pages/upterm-flowchart.svg?sanitize=true)

## :hammer_and_wrench: Deployment

### Kubernetes

You can deploy uptermd to a Kubernetes cluster. Install it with [helm](https://helm.sh):

```console
helm repo add upterm https://upterm.dev
helm repo update
helm install uptermd upterm/uptermd
```

### Fly.io

The cheapest way to deploy a worry-free [Upterm server](https://github.com/owenthereal/upterm/tree/master/cmd/uptermd) (a.k.a. `uptermd`) is to use [Fly.io](https://fly.io).
Fly offers a generous free tier and excellent global performance. The official uptermd community server is hosted on Fly.

1. Install the Fly CLI and authenticate:

   ```console
   curl -L https://fly.io/install.sh | sh
   flyctl auth login
   ```

1. Copy and customize the [`fly.example.toml`](./fly.example.toml) file to `fly.toml` for your deployment configuration.
1. Deploy your uptermd server:

  ```console
  flyctl deploy
  ```

Your uptermd server will be available at `your-app-name.fly.dev`. You can connect using either SSH or WebSocket protocols.

### Heroku

You can deploy an [Upterm server](https://github.com/owenthereal/upterm/tree/master/cmd/uptermd) (a.k.a. `uptermd`) to [Heroku](https://heroku.com).
Note that Heroku discontinued their free tier in November 2022, so this option now requires paid plans.

You can deploy with one click of the following button:

[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)

You can also automate the deployment with [Heroku Terraform](https://devcenter.heroku.com/articles/using-terraform-with-heroku).
The Heroku Terraform scripts are in the [terraform/heroku folder](./terraform/heroku).
A [util script](./bin/heroku-install) is provided for your convenience to automate everything:

```console
git clone https://github.com/owenthereal/upterm
cd upterm
```

Provision uptermd in Heroku Common Runtime. Follow instructions.

```console
bin/heroku-install
```

Provision uptermd in Heroku Private Spaces. Follow instructions.

```console
TF_VAR_heroku_region=REGION TF_VAR_heroku_space=SPACE_NAME TF_VAR_heroku_team=TEAM_NAME bin/heroku-install
```

You **must** use WebSocket as the protocol for a Heroku-deployed Uptermd server because the platform only support HTTP/HTTPS routing.
This is how you host a session and join a session:

Use the Heroku-deployed Uptermd server via WebSocket

```console
upterm host --server wss://YOUR_HEROKU_APP_URL -- YOUR_COMMAND
```

A client connects to the host session via WebSocket

```console
ssh -o ProxyCommand='upterm proxy wss://TOKEN@YOUR_HEROKU_APP_URL' TOKEN@YOUR_HEROKU_APP_URL:443
```

### Digital Ocean

There is an util script that makes provisioning [Digital Ocean Kubernetes](https://www.digitalocean.com/products/kubernetes) and an Upterm server easier:

```bash
TF_VAR_do_token=$DO_PAT \
TF_VAR_uptermd_host=uptermd.upterm.dev \
TF_VAR_uptermd_acme_email=YOUR_EMAIL \
TF_VAR_uptermd_helm_repo=http://localhost:8080 \
TF_VAR_uptermd_host_keys_dir=PATH_TO_HOST_KEYS \
bin/do-install
```

### Systemd

A hardened systemd service is provided in `systemd/uptermd.service`. You can use it to easily run a
secured `uptermd` on your machine:

```console
cp systemd/uptermd.service /etc/systemd/system/uptermd.service
systemctl daemon-reload
systemctl start uptermd
```

### Traefik

Below is an example `docker-compose` configuration for deploying `uptermd` behind [Traefik](https://doc.traefik.io/traefik/), including support for both SSH and WebSocket connections:

```yaml
services:
  upterm:
    build: 
        context: https://github.com/owenthereal/upterm.git
        dockerfile: Dockerfile.uptermd
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=web"
      # SSH over TCP (port 2222)
      - "traefik.tcp.services.uptermd.loadbalancer.server.port=2222"
      - "traefik.tcp.services.uptermd.loadbalancer.proxyProtocol.version=2" # required for real IP forwarding over TCP
      - "traefik.tcp.routers.uptermd.service=uptermd"
      - "traefik.tcp.routers.uptermd.rule=HostSNI(`*`)"
      - "traefik.tcp.routers.uptermd.entrypoints=uptermd"
      # WebSocket over HTTPS (port 8443)
      - "traefik.http.services.uptermd-wss.loadbalancer.server.port=8443"
      - "traefik.http.routers.uptermd-wss.service=uptermd-wss"
      - "traefik.http.routers.uptermd-wss.rule=Host(`upterm.example.com`)" # edit as needed
      - "traefik.http.routers.uptermd-wss.entrypoints=websecure"
      - "traefik.http.routers.uptermd-wss.tls.certresolver=<your cert resolver here>"

    command:
      - --ssh-addr=0.0.0.0:2222
      - --ws-addr=0.0.0.0:8443
      - --ssh-proxy-protocol

    networks:
      - web

networks:
  web:
    external: true
```

**Important notes:**

- **Proxy Protocol:**
  The `--ssh-proxy-protocol` flag (or `UPTERMD_SSH_PROXY_PROTOCOL=true` environment variable) tells `uptermd` to expect the [PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) header on incoming SSH connections. This is essential when using Traefik (or other TCP proxies like HAProxy or AWS ELB) to preserve the real client IP address.
  **If you enable `--ssh-proxy-protocol`, all incoming SSH connections must come through a proxy that supports and is configured to use the PROXY protocol. Direct SSH connections will fail, as `uptermd` will expect the protocol header.**

- **Entrypoints:**
  Make sure to configure the appropriate [Traefik entrypoints](https://doc.traefik.io/traefik/routing/entrypoints/). This example uses two: one for SSH (`uptermd` on port `2222`) and one for WebSocket/HTTPS (`websecure` on port `443`).

- **WebSocket:**
  The WebSocket service allows clients to connect to `uptermd` over HTTPS, which is useful in restrictive network environments.

- **Certificates:**
  Replace `<your cert resolver here>` with your actual Traefik certificate resolver for TLS.

For more details on Traefik TCP and HTTP routing, see the [Traefik documentation](https://doc.traefik.io/traefik/routing/overview/).

## :chart_with_upwards_trend: Monitoring

`uptermd` exposes Prometheus metrics at the `/metrics` endpoint when configured with `--metric-addr` (or `UPTERMD_METRIC_ADDR` environment variable).

Available metrics:

- `routing_connections_count` (Counter) - Total number of SSH connections accepted
- `routing_active_connections_count` (Gauge) - Current number of active SSH connections
- `routing_connection_duration_seconds` (Histogram) - Connection duration in seconds
- `routing_errors_count` (Counter) - Total number of connection errors
- `routing_connection_timeout_count` (Counter) - Number of connections that timed out during establishment

## :balance_scale: Comparison with Prior Arts

Upterm stands as a modern alternative to [Tmate](https://github.com/tmate-io/tmate).

Tmate originates as a fork from an older iteration of Tmux, extending terminal sharing capabilities atop Tmux 2.x. However, Tmate has no plans to align with the latest Tmux updates, compelling Tmate & Tmux users to manage two separate configurations. For instance, the necessity to [bind identical keys twice, conditionally](https://github.com/tmate-io/tmate/issues/108).

On the flip side, Upterm is architected from the ground up to be an independent solution, not a fork. It embodies the idea of connecting the input & output of any shell command between a host and its clients, transcending beyond merely `tmux`. This paves the way for securely sharing terminal sessions utilizing containers.

Written in Go, Upterm is more hack-friendly compared to Tmate, which is crafted in C, akin to Tmux. The seamless compilation of Upterm CLI and server (`uptermd`) into a single binary facilitates swift [deployment of your pairing server](#hammer_and_wrench-deployment) across any cloud environment, devoid of dependencies.

## License

[Apache 2.0](https://github.com/owenthereal/upterm/blob/master/LICENSE)
