Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 51 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,45 @@
# Ziina
# zmate

💻 📤 👥 Instant terminal sharing; using Zellij.

[![asciicast](https://asciinema.org/a/35n5GUFE4PXc1DMdp3BJozRGT.svg)](https://asciinema.org/a/35n5GUFE4PXc1DMdp3BJozRGT)

Ziina lets you invite peers into a local [Zellij](https://github.com/zellij-org/zellij) session over untrusted networks, despite you being behind a NAT gateway.
zmate lets you invite peers into a local [Zellij](https://github.com/zellij-org/zellij) session over untrusted networks, despite you being behind a NAT gateway.
It is heavily inspired by [tmate](https://github.com/tmate-io/tmate).

Ziina is (basically) server-less.
zmate is (basically) server-less.
You only need a standard OpenSSH server with a public IP that serves as an entry-point for your peers.
Your peers only need a standard OpenSSH client.

Starting zmate inside an already open Zellij session will invite peers into that session.
Otherwise zmate will spin up a new Zellij session with a random name.

## How does it work?

Ziina configures an ephemeral SSH remote port-forwarding tunnel on your public SSH server, pointing back to a local high-port.
zmate configures an ephemeral SSH remote port-forwarding tunnel on your public SSH server, pointing back to a local high-port.
It then starts a minimal SSH server on that local high-port, that throws connecting clients directly into a Zellij session.
Peers connecting to the high-port on your server via SSH are forwarded through the tunnel directly into your local Zellij session.
Once the host terminates Ziina (by closing the Zellij session), the remote port-forwarding tunnel and internal SSH server are terminated and all peers automatically kicked.
Once the host terminates zmate (by closing the Zellij session), the remote port-forwarding tunnel and internal SSH server are terminated and all peers automatically kicked.

> The host should always terminate the Zellij session by closing all tabs and panes.
> Simply detaching will still close Ziina and therefor terminate the builtin SSH server and the tunnel.
> Simply detaching will still close zmate and therefor terminate the builtin SSH server and the tunnel.
> However, it will leave behind a dangling Zellij session and also likely screw up your peers' terminal, because their connection gets terminated very disgracefully.

### Ziina Session Life-Cycle
### zmate Session Life-Cycle

```mermaid
sequenceDiagram
participant Host
participant Ziina
participant zmate
participant Zellij
participant Builtin-SSHd
participant Public-SSHd
participant Peer

Note over Host,Peer: Start Ziina
Host->>Ziina: Start Ziina
Ziina->>Builtin-SSHd: Start builtin SSHd (2222)
Ziina->>Public-SSHd: Configure remote port-forwarding tunnel (2222)
Note over Host,Peer: Start zmate
Host->>zmate: Start zmate
zmate->>Builtin-SSHd: Start builtin SSHd (2222)
zmate->>Public-SSHd: Configure remote port-forwarding tunnel (2222)
Public-SSHd-->>Builtin-SSHd: Forwards connections back to builtin SSHd (2222)

Note over Host,Public-SSHd: Host connects to session
Expand All @@ -50,26 +53,26 @@ sequenceDiagram
Builtin-SSHd->>Zellij: Exec into Zellij session

Note over Host,Peer: Zellij session closed or host detached
Ziina->>Public-SSHd: Terminate remote port-forwarding tunnel
Ziina->>Builtin-SSHd: Terminate builtin SSHd
Ziina->>Host: Terminate Ziina
zmate->>Public-SSHd: Terminate remote port-forwarding tunnel
zmate->>Builtin-SSHd: Terminate builtin SSHd
zmate->>Host: Terminate zmate
```

## Security Model

Both, the remote port-forwarding and the builtin minimal SSH server, are initiated and terminated with Ziina.
While Ziina is not running, no listening-port will be bound, neither on your server, nor locally.
You can choose the port on which to bind when you start Ziina; default is 2222.
Both, the remote port-forwarding and the builtin minimal SSH server, are initiated and terminated with zmate.
While zmate is not running, no listening-port will be bound, neither on your server, nor locally.
You can choose the port on which to bind when you start zmate; default is 2222.

The builtin minimal SSH server implements authentication and authorization solely via the username.
Connecting peers must know the correct username.
Peers connecting with a wrong username are immediately disconnected.

By default, Ziina will bind the builtin SSH server to `127.0.0.1:2222`.
By default, zmate will bind the builtin SSH server to `127.0.0.1:2222`.
If you explicitly decide to bind it to `:2222`, you can make your Zellij session available on your LAN.
Peers in your network can then connect to the high-port on your Zellij host, directly, effectively bypassing the round-trip through the tunnel.

If you don't provide an SSH host-key, Ziina will generate a random one on every start.
If you don't provide an SSH host-key, zmate will generate a random in-memory key, every time it starts.

## Installation

Expand All @@ -88,26 +91,43 @@ Your peers:
### Install via Go

```
go install github.com/ziinaio/ziina@latest
go install github.com/ziinaio/zmate@latest
```

Or you can download the [latest release build](https://github.com/ziinaio/zmate/releases) for your platform.

## Usage

```
NAME:
ziina - 💻 📤 👥 Instant terminal sharing; using Zellij.

███████╗██╗██╗███╗ ██╗ █████╗
╚══███╔╝██║██║████╗ ██║██╔══██╗
███╔╝ ██║██║██╔██╗ ██║███████║
███╔╝ ██║██║██║╚██╗██║██╔══██║
███████╗██║██║██║ ╚████║██║ ██║
╚══════╝╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝

zmate - 💻 📤 👥 Instant terminal sharing; using Zellij.

███████╗███╗ ███╗ █████╗ ████████╗███████╗
╚══███╔╝████╗ ████║██╔══██╗╚══██╔══╝██╔════╝
███╔╝ ██╔████╔██║███████║ ██║ █████╗
███╔╝ ██║╚██╔╝██║██╔══██║ ██║ ██╔══╝
███████╗██║ ╚═╝ ██║██║ ██║ ██║ ███████╗
╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝

USAGE:
ziina [global options] command [command options]

DESCRIPTION:

Invite peers in you LAN.

ziina -l 192.168.1.2:2222

Invite peers using **ssh.example.com** as entrypoint for your peers:

ziina -s ssh.example.com

Show connection info:

echo $ZIINA_CONNECTION_INFO
echo $ZIINA_CONNECTION_INFO_RO


COMMANDS:
help, h Shows a list of commands or help for one command

Expand All @@ -122,7 +142,7 @@ GLOBAL OPTIONS:
### Host

```
ziina -s myserver
zmate -s myserver
```

This will generate a random 7 digit Zellij session-name.
Expand Down
72 changes: 50 additions & 22 deletions cmd/ziina/main.go → cmd/zmate/main.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ziina
package zmate

import (
"bufio"
Expand Down Expand Up @@ -26,27 +26,27 @@ import (
)

const banner = `
███████╗██╗██╗███╗ ██╗ █████
╚══███╔╝██║██║████╗ ██║██╔══██╗
███╔╝ ██║██║██╔██╗ ██║███████║
███╔╝ ██║██║██║╚██╗██║██╔══██║
███████╗██║██║██║ ╚████║██║ ██║
╚══════╝╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ═╝
███████╗███╗ ███╗ █████╗ ████████╗███████╗
╚══███╔╝████╗ ████║██╔══██╗╚══██╔══╝██╔════╝
███╔╝ ██╔████╔██║█████████║ █████╗
███╔╝ ██║╚██╔╝██║██╔══██║ ██║ ██╔══
███████╗██║ ╚═╝ ██║██║ ██║ ██║ ███████╗
╚══════╝╚═╝ ╚═╝╚═╝ ╚═╚═╝ ╚══════╝
`

const examples = `
Invite peers in you LAN.

ziina -l 192.168.1.2:2222
zmate -l 192.168.1.2:2222

Invite peers using **ssh.example.com** as entrypoint for your peers:

ziina -s ssh.example.com
zmate -s ssh.example.com

Show connection info:

echo $ZIINA_CONNECTION_INFO
echo $ZIINA_CONNECTION_INFO_RO
echo $ZMATE_CONNECTION_INFO
echo $ZMATE_CONNECTION_INFO_RO
`

var (
Expand All @@ -59,6 +59,12 @@ var (

// roUser contains the username for read-only access
roUser = ""

// quickShareMode flags if zmate is running in quick-share mode
quickShareMode = false

// xdgRuntimeDir is needed for Zellij session discovery
xdgRuntimeDir = ""
)

// charset contains the list of available characters for random session-name generation.
Expand All @@ -79,7 +85,7 @@ func randomString(length int) (string, error) {

// App serves as entry-point for github.com/urfave/cli
var App = &cli.App{
Name: "ziina",
Name: "zmate",
Usage: "💻 📤 👥 Instant terminal sharing; using Zellij." + "\n" + gorainbow.Rainbow(banner),
Description: examples,
Flags: []cli.Flag{
Expand Down Expand Up @@ -108,6 +114,9 @@ var App = &cli.App{
},
},
Action: func(ctx *cli.Context) error {
// we need this for Zellij session discovery
xdgRuntimeDir = os.Getenv("XDG_RUNTIME_DIR")

// Separate out the port from the listen-address.
parts := strings.Split(ctx.String("listen"), ":")
if len(parts) != 2 {
Expand Down Expand Up @@ -135,10 +144,23 @@ var App = &cli.App{
username = currentUser.Username
}

// Generate a random Zellij session-name.
sessionName, err = randomString(7)
if err != nil {
return err
// Check ZELLIJ_SESSION_NAME env-var
zellijSessionName := os.Getenv("ZELLIJ_SESSION_NAME")
if zellijSessionName != "" {
fmt.Println("")
log.Println("Starting from within Zellij session: ", zellijSessionName)
quickShareMode = true
}

if quickShareMode {
sessionName = zellijSessionName
} else {
// Generate a random Zellij session-name.
sessionName, err = randomString(7)
if err != nil {
return err
}
sessionName = fmt.Sprintf("zmate-%s", sessionName)
}

// Generate a random username for full read-write access.
Expand Down Expand Up @@ -197,14 +219,19 @@ var App = &cli.App{
fmt.Printf(" ssh -p %d %s@%s # read-write\n", port, rwUser, displayHost)
fmt.Printf(" ssh -p %d %s@%s # read-only\n", port, roUser, displayHost)
}
fmt.Println("\nPress Enter to continue...")
bufio.NewReader(os.Stdin).ReadBytes('\n')

// Start the Zellij session over SSH
if server == "" {
serverOrHost = listenHost
}
return runZellij(serverOrHost, sessionName, port)

if quickShareMode {
select {} // just keep running
} else {
fmt.Println("\nPress Enter to continue...")
bufio.NewReader(os.Stdin).ReadBytes('\n')
return runZellij(serverOrHost, sessionName, port)
}
},
}

Expand Down Expand Up @@ -237,8 +264,9 @@ func runServer(chGuard chan struct{}, port int, listenAddr, hostKeyFile, entrypo
// Set TERM environment variable
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term))
cmd.Env = append(cmd.Env, fmt.Sprintf("SHELL=%s", os.Getenv("SHELL")))
cmd.Env = append(cmd.Env, fmt.Sprintf("ZIINA_CONNECTION_INFO=%s", fmt.Sprintf("ssh -p %d %s@%s", port, rwUser, entrypoint)))
cmd.Env = append(cmd.Env, fmt.Sprintf("ZIINA_CONNECTION_INFO_RO=%s", fmt.Sprintf("ssh -p %d %s@%s", port, roUser, entrypoint)))
cmd.Env = append(cmd.Env, fmt.Sprintf("XDG_RUNTIME_DIR=%s", xdgRuntimeDir))
cmd.Env = append(cmd.Env, fmt.Sprintf("ZMATE_CONNECTION_INFO=%s", fmt.Sprintf("ssh -p %d %s@%s", port, rwUser, entrypoint)))
cmd.Env = append(cmd.Env, fmt.Sprintf("ZMATE_CONNECTION_INFO_RO=%s", fmt.Sprintf("ssh -p %d %s@%s", port, roUser, entrypoint)))

// Start Zellij in a new PTY
ptmx, err := pty.Start(cmd)
Expand Down Expand Up @@ -285,7 +313,7 @@ func runServer(chGuard chan struct{}, port int, listenAddr, hostKeyFile, entrypo
chGuard <- struct{}{}
}()

log.Printf("Starting Ziina server on %s...\n", listenAddr)
log.Printf("Starting zmate server on %s...\n", listenAddr)
return server.ListenAndServe()
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module github.com/ziinaio/ziina
module github.com/ziinaio/zmate

go 1.24.2

Expand Down
4 changes: 2 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import (
"log"
"os"

"github.com/ziinaio/ziina/cmd/ziina"
"github.com/ziinaio/zmate/cmd/zmate"
)

func main() {
if err := ziina.App.Run(os.Args); err != nil {
if err := zmate.App.Run(os.Args); err != nil {
log.Fatal(err)
}
}