# 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