Self-hosted SSH + SFTP + RDP in a browser — MobaXterm replacement
Go to file
Vantz Stockwell a8974da37d feat: FreeRDP3 purego backend for Windows with platform stub
Add the real FreeRDP3 RDP backend that loads libfreerdp3.dll at runtime
via syscall.NewLazyDLL (no CGO required). The Windows implementation
(freerdp_windows.go) calls FreeRDP3 functions for instance lifecycle,
settings configuration, input forwarding, and event handling. A stub
(freerdp_stub.go) compiles on non-Windows platforms and returns errors,
keeping macOS/Linux development and tests working. A factory function
(freerdp_factory.go) selects the right backend based on runtime.GOOS,
and app.go now uses NewProductionBackend instead of always using mock.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 07:43:31 -04:00
.gitea/workflows ci: build + sign workflow for Windows release (Azure Key Vault + jsign) 2026-03-17 06:38:49 -04:00
docs docs: Phase 2 implementation plan — SSH + SFTP with 12 tasks 2026-03-17 06:48:58 -04:00
frontend feat(ui): add protocol icons and environment tag badges to tab bar 2026-03-17 07:27:54 -04:00
images feat: convert Settings to right sidebar panel, remove light mode 2026-03-13 10:25:24 -04:00
internal feat: FreeRDP3 purego backend for Windows with platform stub 2026-03-17 07:43:31 -04:00
.gitignore chore: exclude .claude worktrees from git 2026-03-17 06:22:18 -04:00
go.mod feat: SFTP service + credential service with encrypted key/password storage 2026-03-17 06:55:18 -04:00
go.sum feat: SFTP service + credential service with encrypted key/password storage 2026-03-17 06:55:18 -04:00
LICENSE feat: Wails v3 + Vue 3 project scaffold with Tailwind dark theme 2026-03-17 06:11:19 -04:00
main.go feat: wire RDP service into Wails app 2026-03-17 07:17:44 -04:00
README.md docs: comprehensive README with architecture, build, and plugin guide 2026-03-17 06:35:21 -04:00

Wraith

Wraith

Native desktop SSH + RDP + SFTP client — a MobaXTerm replacement built with Go and Vue.

Go Wails v3 Vue 3 License


Features

  • Multi-tabbed SSH terminal with xterm.js + WebGL rendering
  • SFTP sidebar on every SSH session (MobaXTerm's killer feature) -- same SSH connection, separate channel
  • RDP via FreeRDP3 dynamic linking (purego, no CGO)
  • Encrypted vault -- master password derived with Argon2id, secrets sealed with AES-256-GCM
  • Connection manager with hierarchical groups, tags, color labels, and full-text search
  • 7 built-in terminal themes -- Dracula, Nord, Monokai, One Dark, Solarized Dark, Gruvbox Dark, MobaXTerm Classic
  • Tab detach / reattach -- sessions live in the Go backend; tabs can be torn off into separate windows and reattached without dropping the connection
  • MobaXTerm import -- plugin interface for .mobaconf and other formats
  • Command palette (Ctrl+K) for quick connection search and actions
  • Single binary -- ships as wraith.exe + freerdp3.dll, no Docker, no database server

Tech Stack

Backend (Go)

Component Technology Purpose
Framework Wails v3 Desktop shell, multi-window, type-safe Go-to-JS bindings
SSH golang.org/x/crypto/ssh SSH client, PTY, key/password auth
SFTP github.com/pkg/sftp Remote filesystem over SSH channel
RDP FreeRDP3 via purego RDP protocol, bitmap rendering
Database SQLite via modernc.org/sqlite (pure Go) Connections, credentials, settings, themes
Encryption AES-256-GCM + Argon2id Vault encryption at rest

Frontend (Vue 3 in WebView2)

Component Technology Purpose
Framework Vue 3 (Composition API) UI framework
Terminal xterm.js 5.x + WebGL addon SSH terminal emulator
CSS Tailwind CSS 4 Utility-first styling
Components Naive UI Tree, tabs, modals, dialogs
State Pinia Reactive stores for sessions, connections, app state
Build Vite 6 Frontend build tooling

Prerequisites

Tool Version Install
Go 1.22+ go.dev/dl
Node.js 20+ nodejs.org
Wails CLI v3 go install github.com/wailsapp/wails/v3/cmd/wails3@latest

Quick Start

# Clone
git clone https://github.com/vstockwell/wraith.git
cd wraith

# Install frontend dependencies
cd frontend && npm install && cd ..

# Run in dev mode (hot-reload frontend + Go backend)
wails3 dev

The app opens a 1400x900 window. On first launch you will be prompted to create a master password for the vault.

Building

# Production build for Windows
wails3 build

# Output: build/bin/wraith.exe

The build embeds the compiled frontend (frontend/dist) into the Go binary via //go:embed. Ship wraith.exe alongside freerdp3.dll for RDP support.

Project Structure

wraith/
  main.go                          # Entry point -- Wails app setup, service registration
  go.mod                           # Go module (github.com/vstockwell/wraith)
  internal/
    app/
      app.go                       # WraithApp -- wires all services, vault create/unlock
    db/
      sqlite.go                    # SQLite open with WAL mode, busy timeout, FK enforcement
      migrations.go                # Embedded SQL migration runner
      migrations/
        001_initial.sql            # Schema: groups, connections, credentials, ssh_keys,
                                   #         themes, host_keys, connection_history, settings
    vault/
      service.go                   # Argon2id key derivation + AES-256-GCM encrypt/decrypt
    connections/
      service.go                   # Connection and Group CRUD (hierarchical tree)
      search.go                    # Full-text search + tag filtering via json_each()
    settings/
      service.go                   # Key-value settings store (vault salt, preferences)
    theme/
      builtins.go                  # 7 built-in theme definitions
      service.go                   # Theme CRUD + idempotent seeding
    session/
      session.go                   # SessionInfo struct + state machine (connecting/connected/detached)
      manager.go                   # Concurrent session manager -- create, detach, reattach, 32-session cap
    plugin/
      interfaces.go                # ProtocolHandler + Importer + Session interfaces
      registry.go                  # Plugin registry -- register/lookup protocol handlers and importers
  frontend/
    package.json                   # Vue 3, Pinia, Naive UI, Tailwind CSS, Vite
    vite.config.ts                 # Vite + Vue + Tailwind plugin config
    src/
      main.ts                      # App bootstrap -- createApp, Pinia, mount
      App.vue                      # Root component
      layouts/
        MainLayout.vue             # Sidebar + tab bar + session area + status bar
        UnlockLayout.vue           # Master password entry screen
      components/
        sidebar/
          ConnectionTree.vue       # Hierarchical connection/group tree
          SidebarToggle.vue        # Collapse/expand sidebar
        session/
          TabBar.vue               # Draggable session tabs
          SessionContainer.vue     # Active session viewport
        common/
          StatusBar.vue            # Bottom status bar
      stores/
        app.store.ts               # Global app state (sidebar, vault status)
        connection.store.ts        # Connection + group state
        session.store.ts           # Active sessions state
  images/
    wraith-logo.png                # Application logo

Architecture

Go Backend                    Wails v3 Bindings              Vue 3 Frontend
(services + business logic)   (type-safe Go <-> JS)          (WebView2)
                                     |
  WraithApp ─────────────────────────┼──────────────> Pinia Stores
    |-- VaultService                 |                   |-- app.store
    |-- ConnectionService            |                   |-- connection.store
    |-- ThemeService                 |                   |-- session.store
    |-- SettingsService              |                        |
    |-- SessionManager               |                   Vue Components
    |-- PluginRegistry               |                   |-- MainLayout
                                     |                   |-- ConnectionTree
  SQLite (WAL mode)                  |                   |-- TabBar
    wraith.db                        |                   |-- SessionContainer
    %APPDATA%\Wraith\                |                   |-- StatusBar

How it fits together:

  1. main.go creates a WraithApp and registers Go services as Wails bindings.
  2. Wails generates type-safe JavaScript bindings so the Vue frontend can call Go methods directly.
  3. The Vue frontend uses Pinia stores to manage reactive state, calling into Go services for all data operations.
  4. All secrets (passwords, SSH keys) are encrypted with AES-256-GCM before being written to SQLite. The encryption key is derived from the master password using Argon2id and is never persisted.
  5. Sessions are managed by the Go SessionManager -- they are decoupled from windows, enabling tab detach/reattach without dropping connections.

Data storage: SQLite with WAL mode at %APPDATA%\Wraith\wraith.db (Windows) or ~/.local/share/wraith/wraith.db (Linux/macOS dev). Foreign keys enforced, 5-second busy timeout.

Plugin Development

Wraith uses a plugin registry with two interfaces: ProtocolHandler for new connection protocols and Importer for loading connections from external tools.

Implementing a ProtocolHandler

package myplugin

import "github.com/vstockwell/wraith/internal/plugin"

type MyProtocol struct{}

func (p *MyProtocol) Name() string { return "myproto" }

func (p *MyProtocol) Connect(config map[string]interface{}) (plugin.Session, error) {
    // Establish connection, return a Session
}

func (p *MyProtocol) Disconnect(sessionID string) error {
    // Clean up resources
}

Implementing an Importer

package myplugin

import "github.com/vstockwell/wraith/internal/plugin"

type MyImporter struct{}

func (i *MyImporter) Name() string                  { return "myformat" }
func (i *MyImporter) FileExtensions() []string       { return []string{".myconf"} }
func (i *MyImporter) Parse(data []byte) (*plugin.ImportResult, error) {
    // Parse file bytes into ImportResult (groups, connections, host keys, theme)
}

Registering Plugins

Register handlers and importers with the plugin registry during app initialization:

app.Plugins.RegisterProtocol(&myplugin.MyProtocol{})
app.Plugins.RegisterImporter(&myplugin.MyImporter{})

The ImportResult struct supports groups, connections, host keys, and an optional theme -- everything needed to migrate from another tool in a single import.

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feat/my-feature)
  3. Make your changes
  4. Run tests: go test ./...
  5. Run frontend checks: cd frontend && npm run build
  6. Commit and push your branch
  7. Open a Pull Request

License

MIT -- Copyright (c) 2026 Vantz Stockwell