Add comprehensive in-depth README

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
monoadmin
2026-04-11 00:11:55 -07:00
parent dcf88c7863
commit a1bc360be7

637
README.md
View File

@@ -0,0 +1,637 @@
# 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