Sync: warn CachyOS/UFW/firewalld users to open the sync ports with copyable commands; ask before sharing newly created SSH keys, passwords and VPN configurations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 23:15:49 +02:00
co-authored by Claude Opus 5.5
parent 6a63e0e80d
commit eba3e38620
6 changed files with 113 additions and 10 deletions
+1
View File
@@ -267,6 +267,7 @@ handle('sync:pair', (id) => sync.pair(id));
handle('sync:unpair', (id) => sync.unpair(id));
handle('sync:now', () => sync.syncAll());
handle('sync:share', (sel) => sync.setShare(sel));
handle('sync:shareItem', (c, id, yes) => sync.shareItem(c, id, yes));
handle('sync:probe', (address) => sync.probe(address));
// ---------- VPN ----------
+31 -2
View File
@@ -14,6 +14,21 @@ const net = require('net');
const os = require('os');
const crypto = require('crypto');
const i18n = require('../i18n');
const fs = require('fs');
const { execFileSync } = require('child_process');
// Lokale Firewall erkennen, die Broadcasts/eingehende Verbindungen blockieren könnte (Linux).
// Ob die Ports bereits freigegeben sind, lässt sich ohne root nicht prüfen (UFW-Regeln sind nur für root lesbar).
function localFirewall() {
if (process.platform !== 'linux') return null;
const read = (f) => { try { return fs.readFileSync(f, 'utf8'); } catch { return ''; } };
const active = (unit) => { try { return execFileSync('systemctl', ['is-active', unit], { timeout: 3000 }).toString().trim() === 'active'; } catch { return false; } };
const os = (read('/etc/os-release').match(/^ID=(.*)$/m) || [])[1]?.replace(/"/g, '') || '';
const ufw = /^ENABLED=yes/m.test(read('/etc/ufw/ufw.conf')) || active('ufw');
const firewalld = active('firewalld');
if (!ufw && !firewalld && os !== 'cachyos') return null;
return { os, ufw, firewalld };
}
const UDP_PORT = 47811;
const TCP_PORT = 47812;
@@ -170,6 +185,7 @@ class SyncService {
this.running = false;
this.lastError = '';
this.syncing = new Set();
this.firewall = localFirewall();
}
get cfg() {
@@ -271,7 +287,7 @@ class SyncService {
enabled: c.enabled, running: this.running, deviceId: c.deviceId, deviceName: c.deviceName, pairable: this.pairable, port: this.port,
peers: c.peers.map((p) => ({ id: p.id, name: p.name, lastSync: p.lastSync || 0, online: this.seen.has(p.id), address: p.address, error: p.error || '' })),
nearby: [...this.seen.values()].filter((p) => !c.peers.some((x) => x.id === p.id)).map(({ id, name, address, pairable }) => ({ id, name, address, pairable })),
share: c.share, asked: !!c.asked, sealed: !!this.store.sealed,
share: c.share, asked: !!c.asked, sealed: !!this.store.sealed, firewall: this.firewall,
};
}
emitState() { this.emit('sync:state', this.status()); }
@@ -452,12 +468,25 @@ class SyncService {
setShare(share) {
const clean = (a) => (Array.isArray(a) ? a.filter((x) => typeof x === 'string') : []);
const c = this.cfg;
c.share = { keys: clean(share?.keys), hosts: clean(share?.hosts), vpns: clean(share?.vpns) };
c.share = { keys: clean(share?.keys), hosts: clean(share?.hosts), vpns: clean(share?.vpns), declined: { keys: [], hosts: [], vpns: [] } };
c.asked = true;
this.store.save();
return this.status();
}
// Einzelnes neues Geheimnis teilen oder ablehnen (Ablehnung wird gemerkt, damit nicht erneut gefragt wird)
shareItem(c, id, yes) {
if (!SECRETS[c] || typeof id !== 'string') throw new Error('Invalid');
const sh = this.cfg.share;
sh.declined ||= { keys: [], hosts: [], vpns: [] };
sh[c] = (sh[c] || []).filter((x) => x !== id);
sh.declined[c] = (sh.declined[c] || []).filter((x) => x !== id);
(yes ? sh[c] : sh.declined[c]).push(id);
this.store.save();
if (yes) this.schedule(500);
return this.status();
}
// Gerät per IP hinzufügen (wenn Broadcasts im Netz blockiert sind)
async probe(address, port = TCP_PORT) {
if (!/^[\w.:-]+$/.test(address)) throw new Error('Invalid address');