Files
remote-control-support-webapp/public/docs.html
T
rmancinasandClaude Opus 5 6895167964
Build and Deploy Remote Control Support / Test (push) Successful in 14s
Build and Deploy Remote Control Support / Build Image (push) Successful in 36s
Build and Deploy Remote Control Support / Deploy to Portainer (push) Successful in 6s
fix(setup): print the hub's public URL, not the host the reader typed
The setup pages build commands that get pasted into a terminal on a *different*
machine, but they built them from location.origin. Open the console by LAN IP or
over a tunnel and the enrolment one-liner told the target machine to curl an
address it may not resolve at all.

/docs and /enroll/:token are now served through a template that stamps in
config.baseUrl — PUBLIC_URL where set, the proxied request host otherwise — with
location.origin left as a fallback for the unsubstituted file. The remaining
127.0.0.1 references are the loopback VNC port on the machine being registered,
which is meant to be literal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 11:41:31 -07:00

347 lines
16 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<title>Registering a device — Remote Support</title>
<link rel="stylesheet" href="/styles.css">
<style>
.docs-shell { max-width: 860px; margin: 0 auto; padding: 28px 20px 80px; position: relative; z-index: 1; }
.docs-shell h2 { margin: 34px 0 10px; font-size: 17px; }
.docs-shell h3 { margin: 22px 0 8px; font-size: 14px; color: var(--text); }
.docs-shell p { margin: 8px 0; font-size: 14px; line-height: 1.65; color: var(--text-dim, var(--text)); }
.docs-shell ul { margin: 8px 0 8px 18px; font-size: 14px; line-height: 1.7; }
.docs-shell table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 13.5px; }
.docs-shell th, .docs-shell td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border, #2a2a2a); vertical-align: top; }
.docs-shell th { color: var(--text-faint); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }
.os-panel[hidden] { display: none; }
.copy-wrap { position: relative; }
.copy-wrap button { position: absolute; top: 8px; right: 8px; }
/* The button floats over the block, so keep the long one-liners from
scrolling underneath it. */
.copy-wrap pre.code { padding-right: 76px; }
</style>
</head>
<body>
<div class="bg-field" id="bg-field" aria-hidden="true"></div>
<div class="docs-shell">
<div class="card-header" style="padding-left:0;border:0">
<div class="brand-mark">RC</div>
<div>
<strong style="font-size:14px">Remote Support</strong>
<span class="brand-sub">Documentation</span>
</div>
<div style="margin-left:auto"><a class="btn ghost small" href="/">Back to console</a></div>
</div>
<span class="card-kicker">Setup guide</span>
<h1 style="margin:6px 0 4px">Registering a device</h1>
<p class="faint">
How to put a machine under remote support, on macOS, Windows and Linux — what to install,
what the one-command installer actually does, and how to keep it running.
</p>
<h2>Before you start</h2>
<p>
Every registered machine needs two things: a <strong>VNC server</strong> serving its screen on
<span class="mono">127.0.0.1:5900</span>, and the <strong>agent</strong>, which dials out to this
hub and forwards that port over an authenticated WebSocket. The agent is what makes NAT and
firewalls stop mattering — nothing inbound is ever opened on the machine.
</p>
<p>
You also need an <strong>enrollment link</strong>. In the console, open
<strong>Machines → Invite a machine</strong>. The link carries a one-time token and expires;
open it on the machine being registered and it shows the exact command with the token already
filled in.
</p>
<div class="notice">
<strong>The alternative is direct mode</strong>, where this hub connects straight to the
machine's VNC port. That only works when the hub can route to it. If the machine is on another
subnet, behind NAT, or reachable only through a VPN or SSH tunnel, use the agent — a direct
entry will just time out.
</div>
<h2>Pick a platform</h2>
<nav class="tabs" id="os-tabs" style="margin-bottom:16px">
<button data-os="macos" class="active">macOS</button>
<button data-os="windows">Windows</button>
<button data-os="linux">Linux</button>
</nav>
<!-- ------------------------------------------------------------ macOS -->
<section class="os-panel" data-os="macos">
<h3>1. Turn on Screen Sharing</h3>
<p>
<strong>System Settings → General → Sharing → Screen Sharing</strong>. Under
<em>Computer Settings</em>, enable <em>VNC viewers may control screen with password</em> and
set a password — the hub authenticates with it and never passes it to the browser.
</p>
<p class="faint">
macOS serves VNC on port 5900 on all interfaces. The agent only ever connects to it over
loopback, so you can firewall the port off from the network.
</p>
<h3>2. Install Node.js 22 or newer</h3>
<p>
<span class="mono">brew install node</span>, or the installer from
<a href="https://nodejs.org" target="_blank" rel="noopener">nodejs.org</a>. Check with
<span class="mono">node -v</span>.
</p>
<h3>3. Run the installer</h3>
<p>Paste this in Terminal, replacing <span class="mono">TOKEN</span> with the one from your enrollment link:</p>
<div class="copy-wrap">
<pre class="code" id="cmd-macos"></pre>
<button class="small" data-copy="cmd-macos">Copy</button>
</div>
<p>
It installs to <span class="mono">~/.rcs-agent</span>, enrolls the machine, and loads a
<strong>launchd</strong> agent so it starts at login and restarts if it dies.
</p>
<h3>Managing it</h3>
<table>
<tr><th>Do this</th><th>Command</th></tr>
<tr><td>Stop</td><td class="mono">launchctl unload ~/Library/LaunchAgents/com.freakma.rcs-agent.plist</td></tr>
<tr><td>Start</td><td class="mono">launchctl load ~/Library/LaunchAgents/com.freakma.rcs-agent.plist</td></tr>
<tr><td>Watch the log</td><td class="mono">tail -f ~/.rcs-agent/agent.log</td></tr>
<tr><td>Remove</td><td class="mono">launchctl unload …plist &amp;&amp; rm -rf ~/.rcs-agent</td></tr>
</table>
</section>
<!-- ---------------------------------------------------------- Windows -->
<section class="os-panel" data-os="windows" hidden>
<h3>1. Install a VNC server</h3>
<p>
Windows has none built in. <a href="https://www.tightvnc.com" target="_blank" rel="noopener">TightVNC</a>
or UltraVNC both work. During setup, register it as a system service so it survives reboots,
and set a password.
</p>
<h3>2. Run the installer</h3>
<p>In PowerShell, with <span class="mono">TOKEN</span> from your enrollment link:</p>
<div class="copy-wrap">
<pre class="code" id="cmd-windows"></pre>
<button class="small" data-copy="cmd-windows">Copy</button>
</div>
<p>
No prerequisites. If Node.js 22+ happens to be installed the script uses it; otherwise it
downloads <span class="mono">rcs-agent.exe</span>, a self-contained build with its own
runtime inside. Everything lands in
<span class="mono">%LOCALAPPDATA%\RemoteControlSupport</span> and no administrator rights
are needed.
</p>
<h3>Windows 7 and Server 2008 R2</h3>
<p>
Those ship PowerShell 2.0, which has no <span class="mono">irm</span>, and .NET 3.5, which
does not negotiate TLS 1.2 by default — the short command fails on both counts. Use this
instead, from <strong>cmd.exe</strong>:
</p>
<div class="copy-wrap">
<pre class="code" id="cmd-windows-legacy"></pre>
<button class="small" data-copy="cmd-windows-legacy">Copy</button>
</div>
<p class="faint">
If it still cannot connect, the machine is missing
<a href="https://support.microsoft.com/kb/3154518" target="_blank" rel="noopener">KB3154518</a>,
which adds TLS 1.2 to .NET 3.5. As a last resort,
<a href="/download/agent.exe" download="rcs-agent.exe">download the agent</a> on another
machine, copy it across, and run
<span class="mono">rcs-agent.exe enroll &lt;link&gt;</span> then
<span class="mono">rcs-agent.exe run</span>.
</p>
<h3>Managing it</h3>
<table>
<tr><th>Do this</th><th>Command</th></tr>
<tr><td>Check it exists</td><td class="mono">schtasks /query /tn RemoteControlSupportAgent</td></tr>
<tr><td>Start</td><td class="mono">schtasks /run /tn RemoteControlSupportAgent</td></tr>
<tr><td>Stop</td><td class="mono">schtasks /end /tn RemoteControlSupportAgent</td></tr>
<tr><td>Remove</td><td class="mono">schtasks /delete /tn RemoteControlSupportAgent /f</td></tr>
</table>
<div class="notice">
<strong>Do not install the agent as a Windows service</strong> on a machine where
<em>ask first</em> is enabled. Services run in session 0 and cannot draw on the interactive
desktop, so the consent prompt would never appear and every session would time out waiting
for an answer. The installer registers a <em>logon task</em> precisely to avoid this — it
runs in the console session, where the prompt is visible.
</div>
</section>
<!-- ------------------------------------------------------------ Linux -->
<section class="os-panel" data-os="linux" hidden>
<h3>1. Start a VNC server on the loopback</h3>
<p>For an existing X session — what you want for support, since it shows the real screen:</p>
<div class="copy-wrap">
<pre class="code" id="cmd-linux-x11vnc"></pre>
<button class="small" data-copy="cmd-linux-x11vnc">Copy</button>
</div>
<p class="faint">
On Wayland, <span class="mono">x11vnc</span> cannot see the screen. Either log in on an Xorg
session, or use a Wayland-native server such as <span class="mono">wayvnc</span>.
</p>
<h3>2. Install Node.js 22 or newer</h3>
<table>
<tr><th>Distro</th><th>Command</th></tr>
<tr><td>Debian / Ubuntu</td><td class="mono">curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - &amp;&amp; sudo apt install -y nodejs</td></tr>
<tr><td>Fedora / RHEL</td><td class="mono">sudo dnf install nodejs</td></tr>
<tr><td>Arch</td><td class="mono">sudo pacman -S nodejs</td></tr>
</table>
<h3>3. Run the installer</h3>
<div class="copy-wrap">
<pre class="code" id="cmd-linux"></pre>
<button class="small" data-copy="cmd-linux">Copy</button>
</div>
<p>
It installs to <span class="mono">~/.rcs-agent</span> and registers a
<strong>systemd user service</strong>. It also enables lingering, so the agent keeps running
when nobody is logged in — without that, the machine drops off the console at logout, which
is exactly when you tend to need it.
</p>
<h3>Managing it</h3>
<table>
<tr><th>Do this</th><th>Command</th></tr>
<tr><td>Status</td><td class="mono">systemctl --user status rcs-agent</td></tr>
<tr><td>Logs</td><td class="mono">journalctl --user -u rcs-agent -f</td></tr>
<tr><td>Restart</td><td class="mono">systemctl --user restart rcs-agent</td></tr>
<tr><td>Remove</td><td class="mono">systemctl --user disable --now rcs-agent &amp;&amp; rm -rf ~/.rcs-agent</td></tr>
</table>
</section>
<h2>Registering a machine that cannot run the agent</h2>
<p>
Printers, appliances, locked-down boxes, anything too old for a current runtime — the agent
does not have to run <em>on</em> the machine it serves. Run it on any other machine on the same
LAN and point it across:
</p>
<div class="copy-wrap">
<pre class="code" id="cmd-proxy"></pre>
<button class="small" data-copy="cmd-proxy">Copy</button>
</div>
<p class="faint">
That host now needs to reach the target's VNC port, and it becomes the machine whose uptime
determines whether the target is reachable.
</p>
<h2>Checking it worked</h2>
<ul>
<li>The machine appears in <strong>Machines</strong> in the console, marked online.</li>
<li>Its hostname and OS show up — the agent reports both when it registers.</li>
<li>Clicking <strong>Connect</strong> opens the screen.</li>
</ul>
<h2>When it does not</h2>
<table>
<tr><th>Symptom</th><th>Cause</th></tr>
<tr>
<td class="mono">timed out connecting to …:5900</td>
<td>This hub cannot route to that address. A direct-mode entry on a subnet the hub cannot
reach — register it with the agent instead.</td>
</tr>
<tr>
<td class="mono">cannot reach …:5900 (ECONNREFUSED)</td>
<td>Routing is fine, nothing is listening. The VNC server is not running.</td>
</tr>
<tr>
<td class="mono">VNC authentication failed</td>
<td>Wrong stored password. Note that VNC authentication only uses the first 8 characters.</td>
</tr>
<tr>
<td>Enrollment says the link expired</td>
<td>Enrollment tokens are one-time and time-limited. Mint a new one.</td>
</tr>
<tr>
<td>Machine shows offline right after install</td>
<td>The agent is not running. Check the service commands for your platform above.</td>
</tr>
<tr>
<td>Sessions hang on "waiting for consent"</td>
<td><em>Ask first</em> is on and the agent cannot show a dialog — it is running headless, as
a Windows service, or on Linux without <span class="mono">zenity</span>/<span class="mono">kdialog</span>.</td>
</tr>
</table>
<h2>What the installer does</h2>
<p>
Both scripts are served by this hub with its address and your token already substituted in, so
there is nothing to fill in. They are worth reading before you pipe them into a shell —
<a id="raw-sh" href="/install.sh">install.sh</a> and
<a id="raw-ps1" href="/install.ps1">install.ps1</a> are the exact text that runs. In order they:
</p>
<ul>
<li>check for a usable runtime, and stop with instructions rather than guessing;</li>
<li>warn if nothing is listening on <span class="mono">127.0.0.1:5900</span>;</li>
<li>download the agent into a per-user directory — no root, no administrator;</li>
<li>enroll, which exchanges the one-time token for an agent key stored with <span class="mono">0600</span> permissions;</li>
<li>register a login-scoped service so it comes back after a reboot.</li>
</ul>
<p class="login-foot" style="margin-top:30px">
Access control, support links and session policy are covered in the project README.
</p>
</div>
<script src="/particles.js"></script>
<script>
// Substituted by the server with this hub's public address. These commands run
// on a different machine, so location.origin would be wrong whenever the reader
// reached this page by IP or over a tunnel. Falls back only if served unmodified.
const stamped = '__HUB__';
const origin = stamped.indexOf('__') === 0 ? location.origin : stamped;
const commands = {
'cmd-macos': `curl -fsSL ${origin}/install.sh?token=TOKEN | sh`,
'cmd-linux': `curl -fsSL ${origin}/install.sh?token=TOKEN | sh`,
'cmd-windows': `irm ${origin}/install.ps1?token=TOKEN | iex`,
'cmd-windows-legacy': `powershell -c "[Net.ServicePointManager]::SecurityProtocol=3072; (New-Object Net.WebClient).DownloadString('${origin}/install.ps1?token=TOKEN') | iex"`,
'cmd-linux-x11vnc': 'x11vnc -localhost -rfbport 5900 -forever -shared -passwd YOURPASSWORD',
'cmd-proxy': `curl -fsSL ${origin}/download/agent.js -o rcs-agent.js\n`
+ `node rcs-agent.js enroll ${origin}/enroll/TOKEN --vnc-host 192.168.1.50 --name "old-box"\n`
+ 'node rcs-agent.js run',
};
for (const [id, text] of Object.entries(commands)) {
const el = document.getElementById(id);
if (el) el.textContent = text;
}
document.querySelectorAll('[data-copy]').forEach((button) => {
button.onclick = () => {
navigator.clipboard.writeText(commands[button.dataset.copy]).then(() => {
const old = button.textContent;
button.textContent = 'Copied';
setTimeout(() => { button.textContent = old; }, 1500);
});
};
});
const tabs = document.getElementById('os-tabs');
tabs.onclick = (event) => {
const button = event.target.closest('button[data-os]');
if (!button) return;
tabs.querySelectorAll('button').forEach((b) => b.classList.toggle('active', b === button));
document.querySelectorAll('.os-panel').forEach((panel) => {
panel.hidden = panel.dataset.os !== button.dataset.os;
});
};
// Land on the tab for whatever the reader is sitting at.
const guess = /Mac/i.test(navigator.platform) ? 'macos'
: /Win/i.test(navigator.platform) ? 'windows'
: /Linux/i.test(navigator.platform) ? 'linux' : null;
if (guess) tabs.querySelector(`button[data-os="${guess}"]`)?.click();
</script>
</body>
</html>