diff --git a/README.md b/README.md index e69de29..fe5ec93 100644 --- a/README.md +++ b/README.md @@ -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":"","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