monoadmin a1bc360be7 Add comprehensive in-depth README
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 00:11:55 -07:00
2026-04-10 15:36:33 -07:00
2026-04-10 15:36:33 -07:00
2026-04-10 15:36:33 -07:00
2026-04-10 15:36:33 -07:00
2026-04-10 15:36:33 -07:00
2026-04-10 15:36:33 -07:00
2026-04-10 15:36:33 -07:00
2026-04-10 15:36:33 -07:00
2026-04-10 15:36:33 -07:00
2026-04-10 15:36:33 -07:00
2026-04-10 15:36:33 -07:00
2026-04-10 15:36:33 -07:00
2026-04-10 15:36:33 -07:00
2026-04-10 15:36:33 -07:00
2026-04-11 00:11:55 -07:00
2026-04-10 15:36:33 -07:00

RemoteLink

A self-hosted remote support platform similar to ScreenConnect / ConnectWise Control. Connect to and control remote machines through a browser — no port forwarding required.

Features

  • Browser-based viewer — full mouse, keyboard, and scroll control over WebSocket; no plugins
  • Multi-monitor — switch between displays mid-session
  • Clipboard sync — paste from your clipboard to the remote machine and vice versa
  • File transfer — drag-and-drop upload, directory browser, one-click download from remote
  • Script execution — run PowerShell, cmd, or bash commands and stream output back (admin only)
  • Session chat — text chat forwarded to the remote machine during a session
  • Wake-on-LAN — send a magic packet to wake an offline machine
  • Attended mode — agent prompts the local user to accept before the session starts
  • Agent auto-update — agents check their version on each heartbeat and self-replace when a new binary is published
  • Session recording — relay can archive JPEG frame streams to disk (opt-in)
  • Machine tags & notes — label and annotate machines; filter by tag or group
  • Machine groups — organise machines and control technician access
  • Invite-only registration — no open sign-up; admin sends email invites
  • Open enrollment — agents register without a token when OPEN_ENROLLMENT=true
  • Light / dark / system theme
  • Mass deployment — NSIS silent installer for Windows; shell one-liner for Linux

Architecture

Browser  ──── HTTPS ────▶  Next.js App (port 3000)
                │                    │
                │              PostgreSQL 17
                │
Browser  ──── WSS ─────▶  Relay (port 8765)
                                     │
Agent    ──── WSS ─────▶  Relay ─────┘
Component Technology Purpose
Web app Next.js 16 / NextAuth v5 / Drizzle ORM Dashboard, auth, API
Relay FastAPI + asyncpg (Python) WebSocket bridge between agent and browser
Database PostgreSQL 17 Machines, sessions, tokens, groups
Agent Python + PyInstaller Screen capture, input control, file transfer

Quick Start (Docker)

1. Clone and configure

git clone https://git.pathcore.org/monoadmin/remotelink-docker.git
cd remotelink-docker
cp .env.example .env

Edit .env:

POSTGRES_PASSWORD=a_strong_random_password
AUTH_SECRET=                  # generate: openssl rand -base64 32
NEXT_PUBLIC_APP_URL=http://your-server-ip:3000
NEXT_PUBLIC_RELAY_URL=your-server-ip:8765

2. Start the stack

docker compose up -d

Three containers start:

  • remotelink-app — web dashboard on port 3000
  • remotelink-relay — WebSocket relay on port 8765
  • remotelink-db — PostgreSQL (internal only)

3. Create your first account

Open http://your-server:3000 and sign up. The first account is automatically given the admin role.

After the first account is created, registration is invite-only by default. Additional users require an invite link from Admin → User Invites.


Production Deployment (Reverse Proxy)

Nginx example

# Web app
server {
    listen 443 ssl;
    server_name remotelink.example.com;

    ssl_certificate     /etc/letsencrypt/live/remotelink.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/remotelink.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# WebSocket relay
server {
    listen 443 ssl;
    server_name relay.remotelink.example.com;

    ssl_certificate     /etc/letsencrypt/live/remotelink.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/remotelink.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8765;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 3600s;
    }
}

Update .env for production:

NEXT_PUBLIC_APP_URL=https://remotelink.example.com
NEXT_PUBLIC_RELAY_URL=relay.remotelink.example.com   # no port — nginx handles 443
ALLOWED_ORIGINS=https://remotelink.example.com

Rebuild the app container after changing NEXT_PUBLIC_* variables (they are baked into the Next.js build):

docker compose up -d --build app

Environment Variables

App (docker-compose.ymlapp service)

Variable Default Description
POSTGRES_PASSWORD Required. PostgreSQL password
AUTH_SECRET Required. NextAuth signing secret (openssl rand -base64 32)
NEXT_PUBLIC_APP_URL http://localhost:3000 Public URL shown in invite links
NEXT_PUBLIC_RELAY_URL localhost:8765 Relay address as seen from the browser (host:port or host if behind proxy on 443)
OPEN_ENROLLMENT false Set true to allow agents to register without a token
OPEN_ENROLLMENT_USER_EMAIL Email of the user who owns auto-registered machines. Falls back to first admin.

Relay (docker-compose.ymlrelay service)

Variable Default Description
DATABASE_URL Required. PostgreSQL connection string
ALLOWED_ORIGINS * Comma-separated allowed CORS origins. Set to your app URL in production.
RECORDING_DIR Path inside the container to save session recordings. Leave empty to disable.

Agent Installation

Linux (one-liner)

# With enrollment token
curl -L https://your-server.com/downloads/remotelink-agent-linux -o remotelink-agent
chmod +x remotelink-agent
./remotelink-agent --server https://your-server.com --enroll YOUR_TOKEN

# Open enrollment (no token)
./remotelink-agent --server https://your-server.com

Config is saved to /etc/remotelink/agent.json. On subsequent runs, just:

./remotelink-agent

Systemd service (run on boot)

# /etc/systemd/system/remotelink-agent.service
[Unit]
Description=RemoteLink Agent
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/remotelink-agent
Restart=always
RestartSec=10
User=root

[Install]
WantedBy=multi-user.target
cp remotelink-agent /usr/local/bin/
systemctl daemon-reload
systemctl enable --now remotelink-agent

Windows (NSIS installer)

Download RemoteLink-Setup.exe from your dashboard (Admin → Agent Enrollment) or build it yourself (see Building the Windows Installer).

Interactive install — double-click and follow the wizard. Enter your server URL and optional enrollment token when prompted.

Silent mass deployment — no UI, suitable for GPO / RMM / Intune:

:: Open enrollment (no token required)
RemoteLink-Setup.exe /S /SERVER=https://your-server.com

:: With enrollment token
RemoteLink-Setup.exe /S /SERVER=https://your-server.com /ENROLL=YOUR_TOKEN

The installer:

  1. Copies binaries to C:\Program Files\RemoteLink\
  2. Enrolls the machine with the server
  3. Installs and starts a Windows service (RemoteLinkAgent) that runs on boot

Uninstall:

"C:\Program Files\RemoteLink\Uninstall.exe" /S

macOS

curl -L https://your-server.com/downloads/remotelink-agent-macos -o remotelink-agent
chmod +x remotelink-agent
./remotelink-agent --server https://your-server.com --enroll YOUR_TOKEN

macOS binaries require the agent to be built on a Mac (PyInstaller limitation). See Building the Agent.

Agent CLI Reference

remotelink-agent [OPTIONS]

Options:
  --server URL      Server URL (required for first-time registration)
  --relay URL       Relay WebSocket URL (auto-derived from --server if omitted)
  --enroll TOKEN    Enrollment token (omit if open enrollment is enabled)
  --attended        Prompt local user to accept each incoming connection
  --run-once        Exit when the session ends (don't save config)
  --verbose, -v     Verbose logging

Agent Enrollment

Option A — Enrollment Tokens (default)

  1. Go to Admin → Agent Enrollment
  2. Create a token (optional: label, expiry, max uses)
  3. Copy the install command shown in the UI and run it on the target machine

Tokens can be single-use (max uses = 1) for one machine, or unlimited for mass deployment.

Option B — Open Enrollment

Set OPEN_ENROLLMENT=true in .env and restart the app container. Agents can then register without any token:

./remotelink-agent --server https://your-server.com

Machines appear immediately in the dashboard under the account set by OPEN_ENROLLMENT_USER_EMAIL (or the first admin user).

Security note: Open enrollment allows any machine that can reach your server to register. Only enable it on private/VPN-accessible deployments or temporarily during a rollout.


Connecting to a Machine

Session code (from the agent's tray / terminal)

  1. The agent generates a 6-character session code on demand
  2. Go to Quick Connect in the dashboard and enter the code
  3. The viewer opens directly — no relay polling required

Direct connect (from Machines page)

Click Connect next to any online machine. A session is created and the viewer opens.


Viewer Controls

Action How
Mouse Move / click / right-click / scroll normally on the canvas
Keyboard Type directly — all keystrokes are forwarded
Ctrl+Alt+Del Toolbar → CAD button
Paste clipboard Toolbar → clipboard icon (reads your local clipboard and sends it to the remote)
Switch monitor Toolbar → monitor dropdown (appears when agent has >1 display)
File transfer Toolbar → folder icon → sidebar opens
Chat Toolbar → message icon → sidebar opens
Quality Toolbar → signal icon → High / Medium / Low
Fullscreen Toolbar → maximise icon (toolbar auto-hides after 3 s, reappears on mouse move)
Wake-on-LAN Toolbar → lightning bolt (shown when agent is offline and MAC address is known)
End session Toolbar → X button

File Transfer

The file transfer sidebar has two areas:

Remote file browser (top half)

  • Navigate directories on the remote machine
  • Click a file's download button to pull it to your browser
  • The agent's home directory (~) is the default starting location

Upload drop zone (bottom half)

  • Drag files from your computer onto the drop zone, or click to browse
  • Files are sent to the remote machine's current directory (shown in the browser path)

Files are transferred as base64-encoded chunks through the existing WebSocket relay — no separate HTTP upload server needed.


Machine Management

Tags

Add colour-coded tags to machines (e.g. server, windows, client-acme). Tags appear as badges on each machine card and can be used to filter the machines list.

  • Click the three-dot menu on a machine card → Edit
  • Type a tag in the tag input and press Enter
  • Click the × on a tag badge to remove it

Built-in tag colours: server (blue), windows (cyan), linux (orange), mac (grey), workstation (purple), laptop (green). Unknown tags use the default muted style.

Notes

Free-text notes per machine — useful for recording IP ranges, owner contacts, software installed, etc.

  • Click the three-dot menu → Show notes to view inline
  • Click Edit to modify

Groups

Groups let you organise machines by client, location, or team.

  1. Go to Admin → Groups and create a group
  2. Edit a machine card and select a group from the dropdown
  3. Use the group filter on the Machines page to view only machines in a group

Session Recording

To enable recording, set RECORDING_DIR in the relay service environment:

# docker-compose.yml
relay:
  environment:
    RECORDING_DIR: /recordings
  volumes:
    - ./recordings:/recordings

Each session produces a .remrec file in that directory. The format is:

Header: "RLREC\x01" (6 bytes)
Frames: [8-byte ms timestamp][4-byte length][JPEG bytes] × N

A simple Python replay script:

import struct, time

with open("session.remrec", "rb") as f:
    assert f.read(6) == b"RLREC\x01"
    while True:
        hdr = f.read(12)
        if not hdr: break
        ts, length = struct.unpack(">QI", hdr)
        frame = f.read(length)
        # frame is a JPEG — display with Pillow, cv2, etc.

Building the Agent

Linux / macOS

cd agent
pip install pyinstaller -r requirements.txt
pyinstaller agent.spec
# Output: dist/remotelink-agent

The Linux binary requires xclip or xsel on the target machine for clipboard sync.

Windows

Building must be done on a Windows machine — PyInstaller cannot cross-compile Windows executables from Linux.

Prerequisites:

cd agent
pip install pyinstaller
pip install -r requirements.txt
pyinstaller agent.spec
# Output: dist\remotelink-agent.exe + dist\remotelink-agent-service.exe

makensis installer.nsi
# Output: RemoteLink-Setup.exe

Note: agent.spec references assets\icon.ico. Either add your own .ico to agent\assets\ or remove the icon= and !define MUI_ICON lines from agent.spec and installer.nsi respectively to use the defaults.


Security

Authentication

  • Invite-only user registration — no public sign-up
  • Passwords hashed with bcrypt (via NextAuth credentials provider)
  • JWT sessions with configurable secret (AUTH_SECRET)

Agent authentication

  • Each machine has a unique 256-bit access_key generated at enrollment
  • The relay validates the access key against the database before accepting any agent connection
  • Viewer connections are authenticated via a per-session viewer_token (UUID)

Script execution

  • exec_script commands from the viewer are only forwarded to the agent if the viewer's user has the admin role
  • Checked at the relay level — non-admin viewers receive an error; the command never reaches the agent

Transport

  • All connections should be run behind TLS in production (nginx + Let's Encrypt)
  • The relay enforces ALLOWED_ORIGINS CORS to prevent cross-site WebSocket hijacking

Attended mode

  • Running the agent with --attended makes it prompt the local user (via zenity / kdialog on Linux, PowerShell MessageBox on Windows) before any session starts
  • The connection is rejected if the user declines or doesn't respond within 30 seconds

Development

Prerequisites

  • Node.js 22+ with pnpm (npm i -g pnpm)
  • Docker + Docker Compose
  • Python 3.12+

Run locally

# Start database only
docker compose up -d postgres

# Install JS dependencies
pnpm install

# Set up .env (copy from .env.example, fill in values)
cp .env.example .env

# Start the Next.js dev server
pnpm dev

# In another terminal — start the relay
cd relay
pip install -r requirements.txt
DATABASE_URL=postgresql://remotelink:password@localhost:5432/remotelink uvicorn main:app --port 8765 --reload

# In another terminal — run the agent
cd agent
pip install -r requirements.txt
python agent.py --server http://localhost:3000 --enroll YOUR_TOKEN

Database schema

Table Purpose
users User accounts with roles (admin, user)
machines Registered agent machines (online status, tags, notes, group)
groups Machine groups for access control
sessions Active and historical viewer sessions
session_codes Short-lived 6-character codes for Quick Connect
enrollment_tokens Admin-generated tokens for agent registration
invites Email invite tokens for new user sign-up

Migrations are in db/migrations/ and applied automatically when the PostgreSQL container initialises from scratch.

Project structure

remotelink-docker/
├── app/                        # Next.js App Router pages and API routes
│   ├── (dashboard)/            # Authenticated dashboard pages
│   │   └── dashboard/
│   │       ├── admin/          # Admin panel (invites, enrollment, groups)
│   │       ├── connect/        # Quick Connect (session code entry)
│   │       ├── machines/       # Machine list with search/filter
│   │       ├── sessions/       # Session history
│   │       └── settings/       # User profile settings
│   ├── api/                    # REST API handlers
│   │   ├── agent/              # Agent: register, heartbeat, session-code
│   │   ├── groups/             # Machine groups CRUD
│   │   ├── machines/           # Machine CRUD + Wake-on-LAN
│   │   ├── sessions/           # Session read/update
│   │   └── ...
│   └── viewer/[sessionId]/     # Full-screen remote viewer
├── agent/                      # Python agent
│   ├── agent.py                # Main agent (capture, input, file transfer)
│   ├── service_win.py          # Windows Service wrapper
│   ├── agent.spec              # PyInstaller build spec
│   ├── installer.nsi           # NSIS Windows installer script
│   └── requirements.txt
├── components/
│   ├── dashboard/              # Dashboard UI components
│   │   ├── machine-card.tsx    # Machine card with inline edit
│   │   └── machines-filter.tsx # Search/tag/group filter bar
│   └── viewer/                 # Viewer UI components
│       ├── toolbar.tsx         # Session toolbar
│       ├── connection-status.tsx
│       ├── file-transfer-panel.tsx
│       └── chat-panel.tsx
├── db/migrations/              # SQL migration files
├── lib/db/schema.ts            # Drizzle ORM schema
├── relay/
│   ├── main.py                 # FastAPI WebSocket relay
│   ├── Dockerfile
│   └── requirements.txt
├── docker-compose.yml
├── Dockerfile                  # Multi-stage Next.js production build
└── .env.example

Relay WebSocket Protocol

Agent → Relay (/ws/agent?machine_id=&access_key=)

Message Direction Type Description
JPEG frame agent→relay binary Raw screen frame forwarded to all viewers
{"type":"ping"} agent→relay JSON Updates last_seen; relay responds with online status
{"type":"monitor_list","monitors":[...]} agent→relay JSON Sent after start_stream; relayed to viewer
{"type":"clipboard_content","content":"..."} agent→relay JSON Clipboard change; relayed to viewer
{"type":"file_chunk",...} agent→relay JSON File download chunk; relayed to viewer
{"type":"file_list","entries":[...]} agent→relay JSON Directory listing; relayed to viewer
{"type":"script_output",...} agent→relay JSON Script stdout line; relayed to viewer
{"type":"chat_message","text":"..."} agent→relay JSON Chat message; relayed to viewer

Viewer → Relay (/ws/viewer?session_id=&viewer_token=)

Message Direction Type Description
{"type":"start_stream"} relay→agent JSON Sent by relay when viewer connects
{"type":"stop_stream"} relay→agent JSON Sent by relay when viewer disconnects
{"type":"mouse_move","x":...,"y":...} viewer→agent JSON
{"type":"mouse_click","button":"left",...} viewer→agent JSON
{"type":"mouse_scroll","dx":...,"dy":...} viewer→agent JSON
{"type":"key_press","key":"a"} viewer→agent JSON Printable character
{"type":"key_special","key":"enter"} viewer→agent JSON Special key
{"type":"exec_key_combo","keys":["ctrl_l","alt_l","delete"]} viewer→agent JSON Key combo (admin only)
{"type":"set_monitor","index":1} viewer→agent JSON Switch capture monitor
{"type":"set_quality","quality":"high"} viewer→agent JSON high / medium / low
{"type":"clipboard_paste","content":"..."} viewer→agent JSON Set remote clipboard
{"type":"exec_script","script":"...","shell":"bash"} viewer→agent JSON Run script (admin only)
{"type":"file_download","path":"/tmp/log.txt"} viewer→agent JSON Request file
{"type":"file_upload_start","filename":"...","dest_path":"..."} viewer→agent JSON
{"type":"file_upload_chunk","chunk":"<base64>","seq":0} viewer→agent JSON
{"type":"file_upload_end"} viewer→agent JSON
{"type":"list_files","path":"~"} viewer→agent JSON Browse remote filesystem
{"type":"chat_message","text":"..."} viewer→agent JSON

Troubleshooting

Agent connects but viewer shows blank screen (Linux)

The agent can't find a display. Check:

echo $DISPLAY          # should be :0 or similar
xdpyinfo               # should list display info

The agent auto-detects $DISPLAY by scanning /tmp/.X11-unix/. If you're running the agent as a system service, make sure DISPLAY is set in the service environment.

Relay shows "Cannot call receive once a disconnect message has been received"

This was a known bug — it is fixed. If you see it on an older version, update the relay container:

docker compose up -d --build relay

File transfer fails for large files

The relay holds chunks in memory. For files over ~100 MB, consider increasing the Docker memory limit or using an alternative file transfer method for large transfers. Chunk size is 64 KB.

Agent auto-update isn't working

The relay's CURRENT_AGENT_VERSION in app/api/agent/heartbeat/route.ts must be bumped and the new binary placed at /public/downloads/remotelink-agent-linux (and /windows.exe). The version check is a simple string equality — it won't auto-update unless the constant is changed.

Wake-on-LAN doesn't work

  • The agent must have connected at least once since the MAC address feature was added (it's reported on registration)
  • WoL only works if the target machine and the server are on the same L2 network segment, or your router forwards broadcast packets
  • The machine's BIOS must have WoL enabled, and the NIC must be configured to wake on magic packet

Licence

MIT

Description
Self-hosted remote desktop application - Docker stack
Readme 56 MiB
Languages
TypeScript 83.2%
Python 11.1%
CSS 2%
HTML 1.9%
NSIS 1.1%
Other 0.6%