From c311ddb5d2fab424cbe4707146edc1aed141dbc8 Mon Sep 17 00:00:00 2001 From: 2GT-Rich <107219997+2GT-Rich@users.noreply.github.com> Date: Sat, 16 May 2026 16:16:58 -0700 Subject: [PATCH] fix: packet capture SFTP path + pre-stop + flush delay RouterOS SFTP root maps to the device filesystem root, not /flash/. Capture file is written as /.pcap, not /flash/.pcap. Also stop any in-progress capture before reconfiguring, add a 2s flush delay between sniffer stop and SFTP download, improve error messages to expose exact failure point, and delete remote file after download. Co-Authored-By: Claude Sonnet 4.6 --- backend/src/routes/devices.ts | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/backend/src/routes/devices.ts b/backend/src/routes/devices.ts index b4f4d90..59e0c91 100644 --- a/backend/src/routes/devices.ts +++ b/backend/src/routes/devices.ts @@ -1788,11 +1788,14 @@ router.post('/:id/tools/capture', requireWrite, async (req: Request, res: Respon } const fileName = `cap-${Date.now()}`; - const remotePath = `/flash/${fileName}.pcap`; + // RouterOS SFTP root = device file system root; sniffer writes .pcap there + const remotePath = `/${fileName}.pcap`; const client = makeToolClient(device); try { await client.connect(); + // Stop any previous capture before reconfiguring + await client.execute('/tool/sniffer/stop').catch(() => {}); const setParams: Record = { 'file-name': fileName, 'file-limit': '10240' }; if (iface) setParams['interface'] = iface; if (filter_ip) setParams['filter-ip-address'] = filter_ip; @@ -1801,21 +1804,30 @@ router.post('/:id/tools/capture', requireWrite, async (req: Request, res: Respon await new Promise((r) => setTimeout(r, captureSec * 1000)); await client.execute('/tool/sniffer/stop').catch(() => {}); client.disconnect(); + // Allow RouterOS a moment to flush the PCAP file before SFTP download + await new Promise((r) => setTimeout(r, 2000)); - // Download PCAP via SFTP + // Download PCAP via SFTP then delete remote file const pcapBuffer = await new Promise((resolve, reject) => { const ssh = new SshClient(); ssh.on('ready', () => { ssh.sftp((err, sftp) => { - if (err) { ssh.end(); return reject(err); } + if (err) { ssh.end(); return reject(new Error(`SFTP init failed: ${err.message}`)); } const chunks: Buffer[] = []; const stream = sftp.createReadStream(remotePath); stream.on('data', (chunk: Buffer) => chunks.push(chunk)); - stream.on('end', () => { ssh.end(); resolve(Buffer.concat(chunks)); }); - stream.on('error', (e: Error) => { ssh.end(); reject(e); }); + stream.on('end', () => { + // Best-effort delete; don't block the response on it + sftp.unlink(remotePath, () => { ssh.end(); }); + resolve(Buffer.concat(chunks)); + }); + stream.on('error', (e: Error) => { + ssh.end(); + reject(new Error(`SFTP read failed (path=${remotePath}): ${e.message}`)); + }); }); }); - ssh.on('error', reject); + ssh.on('error', (e) => reject(new Error(`SSH connect failed: ${e.message}`))); ssh.connect({ host: device.ip_address, port: device.ssh_port ?? 22, @@ -1830,6 +1842,7 @@ router.post('/:id/tools/capture', requireWrite, async (req: Request, res: Respon return res.send(pcapBuffer); } catch (err) { client.disconnect(); + console.error('[capture]', (err as Error).message); return res.status(500).json({ error: (err as Error).message }); } });