Files
remotelink-docker/README.md
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

638 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```bash
git clone https://git.pathcore.org/monoadmin/remotelink-docker.git
cd remotelink-docker
cp .env.example .env
```
Edit `.env`:
```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
```bash
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
```nginx
# 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:
```env
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):
```bash
docker compose up -d --build app
```
---
## Environment Variables
### App (`docker-compose.yml` → `app` 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.yml` → `relay` 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)
```bash
# 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:
```bash
./remotelink-agent
```
#### Systemd service (run on boot)
```ini
# /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
```
```bash
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](#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:
```batch
:: 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:**
```batch
"C:\Program Files\RemoteLink\Uninstall.exe" /S
```
### macOS
```bash
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](#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:
```bash
./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:
```yaml
# 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:
```python
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
```bash
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:**
- Python 3.11+ from [python.org](https://python.org)
- [NSIS 3.x](https://nsis.sourceforge.io) (for the installer)
```powershell
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
```bash
# 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:
```bash
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:
```bash
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