3 Commits
Author SHA1 Message Date
aria.hacker a53e9006a5 Fix SPICE CA cert not applied: .vv format needs inline ca=, not tls-ca-file=<path> 2026-07-06 22:10:41 +02:00
aria.hacker aa5383d826 Fix SPICE proxy tunnel: proxy URL was written into host= instead of a real proxy= line
Proxmox's spiceproxy API returns proxy as a ready-made "http://<addr>:3128"
URL and host as the real (often cluster-internal) SPICE target. buildVVFile
picked params.proxy first and wrote that whole URL into the .vv file's
host= field, and never emitted a proxy= line at all — so virt-viewer tried
to dial "http://<ip>:3128" as a literal hostname instead of tunneling
through the proxy, and every connection failed.

Bump to 1.0.7.
2026-07-06 21:46:07 +02:00
aria.hacker a299eaf925 Always render VM list; surface SPICE connect target for diagnosis
- Auto-connect (single-VM shortcut and autostart match) now renders the
  VM list first instead of skipping straight to the connect attempt.
  With cluster dedup collapsing 3 duplicate entries down to 1, the VM
  vanished from the screen entirely whenever the connection failed.
- proxmox:connect now returns the resolved host:port it handed to
  remote-viewer, shown in the success notice, so connection failures
  (e.g. Proxmox handing out a cluster-internal node address the client
  cannot route to) can be diagnosed without racing delete-this-file.
2026-07-06 20:45:50 +02:00
3 changed files with 34 additions and 16 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "proxmox-spice-client",
"version": "1.0.5",
"version": "1.0.8",
"description": "VDI SPICE Client f\u00fcr Proxmox",
"main": "src/main.js",
"scripts": {
+22 -7
View File
@@ -222,9 +222,9 @@ ipcMain.handle('proxmox:connect', async (_e, { host, node, vmid }) => {
const entry = clients.find((c) => c.host === host) || clients[0];
if (!entry) throw new Error('Nicht angemeldet.');
const params = await entry.client.getSpiceTicket(node, vmid);
const vvPath = buildVVFile(params, vmid, entry.client);
const { vvPath, target } = buildVVFile(params, vmid, entry.client);
launchRemoteViewer(vvPath);
return { success: true };
return { success: true, target };
} catch (err) {
return { success: false, error: err.message };
}
@@ -266,17 +266,27 @@ ipcMain.handle('settings:setAutoConnect', (_e, target) => {
function buildVVFile(params, vmid, client) {
const lines = ['[virt-viewer]', `type=${params.type || 'spice'}`];
const host = params.proxy || params.host || client.host.split(':')[0];
// params.host = the real SPICE target (often a cluster-internal node
// address) and params.proxy = a ready-made "http://<reachable-host>:3128"
// URL that virt-viewer tunnels through via HTTP CONNECT. They are two
// different .vv fields — writing params.proxy into host= (as before)
// handed virt-viewer a full URL as a hostname and never opened the
// tunnel, so it tried (and failed) to dial the internal address directly.
const host = params.host || client.host.split(':')[0];
lines.push(`host=${host}`);
if (params['tls-port']) lines.push(`tls-port=${params['tls-port']}`);
if (params.port) lines.push(`port=${params.port}`);
if (params.password) lines.push(`password=${params.password}`);
if (params.proxy) lines.push(`proxy=${params.proxy}`);
// virt-viewer's .vv format only understands the CA inline via `ca=`, with
// newlines escaped as literal "\n" -- there is no `tls-ca-file=<path>` key,
// so pointing at a temp .pem file was silently ignored and left the
// self-signed cluster CA unverified, failing the TLS handshake.
if (params.ca) {
const caPath = path.join(os.tmpdir(), 'proxmox-spice-ca.pem');
fs.writeFileSync(caPath, params.ca);
lines.push(`tls-ca-file=${caPath}`);
const caInline = String(params.ca).replace(/\r\n|\r|\n/g, '\\n');
lines.push(`ca=${caInline}`);
}
if (params['host-subject']) lines.push(`host-subject=${params['host-subject']}`);
@@ -290,7 +300,12 @@ function buildVVFile(params, vmid, client) {
const vvPath = path.join(os.tmpdir(), `spice-${vmid}-${Date.now()}.vv`);
fs.writeFileSync(vvPath, lines.join('\n') + '\n', { mode: 0o600 });
return vvPath;
// Surfaced to the renderer so a failed connection is diagnosable without
// having to catch the .vv file before remote-viewer deletes it.
const port = params['tls-port'] || params.port || '?';
const target = params.proxy ? `${host}:${port} via ${params.proxy}` : `${host}:${port}`;
return { vvPath, target };
}
// VirtViewer's Windows installer names its folder after the bundled version
+11 -8
View File
@@ -76,6 +76,15 @@ async function loadVMs() {
return;
}
const autoConnect = await api.settings.getAutoConnect();
const autoMatch = autoConnect && vms.find(
(vm) => vm.host === autoConnect.host && String(vm.vmid) === String(autoConnect.vmid)
);
// Render the list first — even when we're about to auto-connect, Stefan
// needs to see the VM (and reach Settings/retry) if that connection fails.
renderList(vms, autoConnect);
// Auto-connect when there is exactly one SPICE VM
if (vms.length === 1) {
showNotice(`Nur eine VM verfügbar — verbinde mit „${vms[0].name}" …`);
@@ -84,18 +93,11 @@ async function loadVMs() {
}
// Auto-connect the VM the user marked for autostart (only one at a time)
const autoConnect = await api.settings.getAutoConnect();
const autoMatch = autoConnect && vms.find(
(vm) => vm.host === autoConnect.host && String(vm.vmid) === String(autoConnect.vmid)
);
if (autoMatch) {
showNotice(`Autostart-VM „${autoMatch.name}" markiert — verbinde …`);
await connectVM(autoMatch);
return;
}
renderList(vms, autoConnect);
}
loadVMs();
@@ -187,7 +189,8 @@ async function connectVM(vm) {
if (!result.success) {
showError(result.error || 'Verbindung fehlgeschlagen.');
} else {
showNotice(`${vm.name}" — SPICE-Sitzung gestartet.`);
const targetInfo = result.target ? ` (Ziel: ${result.target})` : '';
showNotice(`${vm.name}" — SPICE-Sitzung gestartet${targetInfo}.`);
}
}