feat: WireGuard VPN-Funktionalität hinzugefügt und Versionsnummer auf 1.0.9 erhöht

This commit is contained in:
2026-05-09 02:45:12 +02:00
parent aae616c92b
commit 33b805b582
2 changed files with 363 additions and 3 deletions

364
app.py
View File

@@ -61,6 +61,8 @@ DEFAULT_CONFIG = {
# WiFi
'wifi_ssid': '', 'wifi_password': '',
'ap_ssid': 'PiCopy', 'ap_password': 'PiCopy,',
# WireGuard
'wireguard_auto': False,
}
# ── Persistenter Kopierstatus ───────────────────────────────────────────────
@@ -268,6 +270,146 @@ def wifi_monitor():
time.sleep(30)
# ── WireGuard VPN ─────────────────────────────────────────────────────────────
WG_CONF = Path('/etc/wireguard/picopy.conf')
WG_IFACE = 'picopy'
def wg_is_installed():
return shutil.which('wg-quick') is not None
wg_state = {
'connected': False,
'ip': '',
'peer': '',
'error': None,
'has_config': False,
'installed': False,
'pkg_running': False,
'pkg_action': '',
'pkg_error': None,
}
wg_lock = threading.Lock()
def wg_update_state():
inst = wg_is_installed()
has_conf = WG_CONF.exists()
if not inst:
with wg_lock:
wg_state.update(installed=False, connected=False, ip='', peer='',
has_config=has_conf)
return
r = subprocess.run(['wg', 'show', WG_IFACE],
capture_output=True, text=True, timeout=5)
if r.returncode != 0:
with wg_lock:
wg_state.update(installed=True, connected=False, ip='', peer='',
has_config=has_conf)
return
ip_r = subprocess.run(['ip', '-4', 'addr', 'show', WG_IFACE],
capture_output=True, text=True, timeout=5)
ip = ''
for line in ip_r.stdout.splitlines():
if line.strip().startswith('inet '):
ip = line.strip().split()[1].split('/')[0]
break
peer = ''
for line in r.stdout.splitlines():
if line.startswith('peer:'):
peer = line.split(':', 1)[-1].strip()
break
with wg_lock:
wg_state.update(installed=True, connected=True, ip=ip, peer=peer,
error=None, has_config=has_conf)
def wg_connect():
if not WG_CONF.exists():
with wg_lock:
wg_state['error'] = 'Keine Konfiguration vorhanden'
return False
r = subprocess.run(['wg-quick', 'up', WG_IFACE],
capture_output=True, text=True, timeout=30)
if r.returncode == 0:
time.sleep(1)
wg_update_state()
log.info('WireGuard verbunden')
return True
err = (r.stderr.strip().splitlines()[-1]
if r.stderr.strip() else 'Unbekannter Fehler')
with wg_lock:
wg_state.update(connected=False, error=err)
log.error(f'WireGuard Fehler: {err}')
return False
def wg_disconnect():
r = subprocess.run(['wg-quick', 'down', WG_IFACE],
capture_output=True, text=True, timeout=15)
with wg_lock:
wg_state.update(connected=False, ip='', peer='', error=None)
log.info('WireGuard getrennt')
return r.returncode == 0
def _wg_apt(action: str, packages: list):
"""Führt apt-get install/remove aus und aktualisiert pkg_state."""
with wg_lock:
if wg_state['pkg_running']:
return
wg_state.update(pkg_running=True, pkg_action=action, pkg_error=None)
try:
cmd = ['apt-get', action, '-y'] + packages
r = subprocess.run(cmd, capture_output=True, text=True, timeout=300,
env={**os.environ, 'DEBIAN_FRONTEND': 'noninteractive'})
if r.returncode != 0:
err = (r.stderr.strip().splitlines()[-1]
if r.stderr.strip() else f'apt-get {action} fehlgeschlagen')
log.error(f'WireGuard apt {action}: {err}')
with wg_lock:
wg_state['pkg_error'] = err
else:
log.info(f'WireGuard apt {action} abgeschlossen')
except Exception as e:
with wg_lock:
wg_state['pkg_error'] = str(e)
finally:
with wg_lock:
wg_state['pkg_running'] = False
wg_state['pkg_action'] = ''
wg_update_state()
def wg_install():
_wg_apt('install', ['wireguard', 'wireguard-tools'])
def wg_uninstall():
wg_disconnect()
_wg_apt('remove', ['wireguard', 'wireguard-tools'])
def wg_save_config(content: str):
try:
WG_CONF.parent.mkdir(parents=True, exist_ok=True)
WG_CONF.write_text(content, encoding='utf-8')
WG_CONF.chmod(0o600)
return True, ''
except Exception as e:
return False, str(e)
def wg_monitor():
while True:
try:
wg_update_state()
except Exception:
pass
time.sleep(10)
# ── USB Geräteerkennung ───────────────────────────────────────────────────────
def usb_port_of(dev_name):
@@ -774,7 +916,9 @@ def r_status():
cs = dict(copy_state)
with wifi_lock:
ws = dict(wifi_state)
return jsonify(copy=cs, wifi=ws)
with wg_lock:
wgs = dict(wg_state)
return jsonify(copy=cs, wifi=ws, vpn=wgs)
@app.route('/api/copy/start', methods=['POST'])
def r_start():
@@ -860,6 +1004,63 @@ def r_wifi_status():
return jsonify(dict(wifi_state))
# ── WireGuard Routes ─────────────────────────────────────────────────────────
@app.route('/api/wireguard/config', methods=['GET', 'POST'])
def r_wg_config():
if request.method == 'POST':
data = request.get_json(force=True)
content = data.get('content', '')
if not content.strip():
return jsonify(error='Konfiguration ist leer'), 400
ok, err = wg_save_config(content)
if not ok:
return jsonify(error=err), 500
auto = data.get('auto')
if auto is not None:
c = load_cfg()
c['wireguard_auto'] = bool(auto)
save_cfg(c)
with wg_lock:
wg_state['has_config'] = True
return jsonify(ok=True)
if WG_CONF.exists():
content = WG_CONF.read_text(encoding='utf-8')
masked = re.sub(r'(PrivateKey\s*=\s*)(.+)', r'\1****', content)
return jsonify(exists=True, config=masked)
return jsonify(exists=False, config='')
@app.route('/api/wireguard/connect', methods=['POST'])
def r_wg_connect():
threading.Thread(target=wg_connect, daemon=True).start()
return jsonify(ok=True, msg='Verbindungsversuch gestartet')
@app.route('/api/wireguard/disconnect', methods=['POST'])
def r_wg_disconnect():
ok = wg_disconnect()
return jsonify(ok=ok)
@app.route('/api/wireguard/install', methods=['POST'])
def r_wg_install():
with wg_lock:
if wg_state['pkg_running']:
return jsonify(error='Bereits aktiv'), 400
threading.Thread(target=wg_install, daemon=True).start()
return jsonify(ok=True)
@app.route('/api/wireguard/uninstall', methods=['POST'])
def r_wg_uninstall():
with wg_lock:
if wg_state['pkg_running']:
return jsonify(error='Bereits aktiv'), 400
threading.Thread(target=wg_uninstall, daemon=True).start()
return jsonify(ok=True)
# ── Upload Routes ──────────────────────────────────────────────────────────────
@app.route('/api/upload/targets', methods=['GET'])
@@ -1332,6 +1533,9 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
.sec{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--sub);padding:.1rem 0;margin:.7rem 0 .5rem;display:flex;align-items:center;gap:.5rem}
.sec::after{content:'';flex:1;height:1px;background:var(--brd)}
.empty{color:var(--sub);font-size:.85rem;padding:.3rem 0}
/* ── WireGuard VPN ── */
.wdot.vpn{background:var(--pur);box-shadow:0 0 6px var(--pur)}
</style>
</head>
<body>
@@ -1350,6 +1554,11 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
<span id="wifi-label">Verbinde…</span>
<span id="wifi-ip"></span>
</div>
<div id="vpn-pill" class="topbar-wifi" style="display:none">
<div class="wdot d" id="vpn-dot"></div>
<span id="vpn-label" style="font-weight:600;color:var(--txt)">VPN</span>
<span id="vpn-ip" style="color:var(--sub);font-family:monospace;font-size:.76rem"></span>
</div>
</header>
<main class="page">
@@ -1618,6 +1827,72 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
</div>
</div>
<!-- ── WireGuard VPN ── -->
<div class="card">
<div class="card-head">
<div class="card-icon pur">⚿</div>
<span class="card-title">WireGuard VPN</span>
<span class="card-sub" id="wg-status-sub"></span>
</div>
<div class="card-body">
<!-- Paket nicht installiert -->
<div id="wg-not-installed" style="display:none">
<div style="display:flex;align-items:center;gap:.75rem;padding:.75rem .9rem;background:rgba(251,191,36,.07);border:1px solid rgba(251,191,36,.28);border-radius:.5rem;margin-bottom:.6rem">
<span style="font-size:1.3rem;flex-shrink:0">📦</span>
<div style="flex:1;min-width:0">
<div style="font-weight:600;font-size:.87rem;color:var(--ylw)">WireGuard nicht installiert</div>
<div style="font-size:.76rem;color:var(--sub);margin-top:.15rem">wireguard + wireguard-tools werden per apt-get auf dem Pi installiert</div>
</div>
<button class="btn pri" onclick="wgInstall()" style="flex-shrink:0">Installieren</button>
</div>
</div>
<!-- Paketoperation läuft -->
<div id="wg-pkg-progress" style="display:none">
<div style="display:flex;align-items:center;gap:.75rem;padding:.75rem .9rem;background:rgba(79,142,247,.07);border:1px solid rgba(79,142,247,.25);border-radius:.5rem;margin-bottom:.6rem">
<span style="font-size:1.3rem;flex-shrink:0" id="wg-pkg-icon">⏳</span>
<div>
<div style="font-weight:600;font-size:.87rem" id="wg-pkg-title">Installiere WireGuard…</div>
<div style="font-size:.76rem;color:var(--sub);margin-top:.1rem">apt-get läuft bitte warten (bis 60 s)</div>
</div>
</div>
</div>
<!-- Hauptbereich (wenn installiert) -->
<div id="wg-installed-ui" style="display:none">
<!-- Verbindungsstatus -->
<div style="display:flex;align-items:center;gap:.75rem;margin-bottom:.85rem;padding:.65rem .85rem;background:var(--bg2);border-radius:.5rem;border:1px solid var(--brd)">
<div id="wg-dot" class="wdot d"></div>
<div style="flex:1;min-width:0">
<div id="wg-label" style="font-weight:600;font-size:.87rem">Getrennt</div>
<div id="wg-detail" style="font-size:.74rem;color:var(--sub);font-family:monospace;margin-top:.1rem"></div>
</div>
<button id="wg-btn-connect" class="btn grn sm" onclick="wgConnect()" style="display:none">⚿&nbsp;Verbinden</button>
<button id="wg-btn-disconnect" class="btn danger sm" onclick="wgDisconnect()" style="display:none">✕&nbsp;Trennen</button>
</div>
<!-- Konfiguration -->
<div class="sec" style="margin-top:0">Konfiguration</div>
<div style="font-size:.8rem;color:var(--sub);margin-bottom:.65rem;line-height:1.5">
WireGuard .conf einfügen — wird als <code style="background:var(--bg2);padding:.1rem .3rem;border-radius:.25rem">/etc/wireguard/picopy.conf</code> gespeichert (Permissions 600). Der private Schlüssel wird maskiert angezeigt.
</div>
<div class="field">
<label>WireGuard Konfiguration (.conf)</label>
<textarea id="wg-config" rows="9" placeholder="[Interface]&#10;PrivateKey = ...&#10;Address = 10.x.x.x/32&#10;DNS = ...&#10;&#10;[Peer]&#10;PublicKey = ...&#10;Endpoint = mein-nas.dyndns.org:51820&#10;AllowedIPs = 192.168.1.0/24" style="font-family:ui-monospace,monospace;font-size:.77rem;line-height:1.6"></textarea>
</div>
<label class="tog"><input type="checkbox" id="wg-auto"><span>Beim Start automatisch verbinden</span></label>
<div class="btn-row">
<button class="btn pri" onclick="wgSaveConfig()">✓&nbsp;Konfiguration speichern</button>
<button class="btn danger" onclick="wgUninstall()" style="margin-left:auto" title="wireguard-Paket entfernen">✕&nbsp;Deinstallieren</button>
</div>
</div>
<div id="wg-flash" class="flash" style="margin-top:.4rem"></div>
</div>
</div>
<!-- ── System ── -->
<div class="card">
<div class="card-head">
@@ -1957,7 +2232,44 @@ function fmtSpd(bps){
// ── Poll ──────────────────────────────────────────────────────────────────────
async function poll(){
try{
const {copy:c,wifi:w}=await api('/status');
const {copy:c,wifi:w,vpn:v}=await api('/status');
// VPN Topbar + Card
if(v){
const vp=$('vpn-pill'),vdot=$('vpn-dot'),vl=$('vpn-label'),vi=$('vpn-ip');
const ni=$('wg-not-installed'),pp=$('wg-pkg-progress'),ui=$('wg-installed-ui');
if(v.pkg_running){
ni.style.display='none'; pp.style.display='block'; ui.style.display='none';
const act=v.pkg_action==='remove'?'Deinstalliere':'Installiere';
$('wg-pkg-title').textContent=act+' WireGuard…';
$('wg-pkg-icon').textContent='';
$('wg-status-sub').textContent=act+'';
vp.style.display='none';
} else if(!v.installed){
ni.style.display='block'; pp.style.display='none'; ui.style.display='none';
$('wg-status-sub').textContent='Nicht installiert';
vp.style.display='none';
if(v.pkg_error) flash('wg-flash','err',v.pkg_error);
} else {
ni.style.display='none'; pp.style.display='none'; ui.style.display='block';
const wgd=$('wg-dot'),wgl=$('wg-label'),wgdet=$('wg-detail');
const bc=$('wg-btn-connect'),bd=$('wg-btn-disconnect');
if(v.connected){
vp.style.display='flex'; vdot.className='wdot c';
vl.textContent='VPN aktiv'; vi.textContent=v.ip||'';
wgd.className='wdot c'; wgl.textContent='Verbunden';
wgdet.textContent=v.ip?(v.ip+(v.peer?' · peer …'+v.peer.slice(-8):'')):'';
bc.style.display='none'; bd.style.display=''; bd.disabled=false;
$('wg-status-sub').textContent=v.ip||'';
} else {
vp.style.display=v.has_config?'flex':'none';
vdot.className='wdot d'; vl.textContent='VPN'; vi.textContent='';
wgd.className='wdot d'; wgl.textContent='Getrennt';
wgdet.textContent=v.error||'';
bc.style.display=v.has_config?'':'none'; bc.disabled=false; bd.style.display='none';
$('wg-status-sub').textContent=v.has_config?'Konfiguriert':'Nicht konfiguriert';
}
}
}
// WiFi
const wd=$('wdot'),wl=$('wifi-label'),wi=$('wifi-ip');
if(w.mode==='client'){wd.className='wdot c';wl.textContent=w.ssid||'Verbunden';wi.textContent=w.ip||'';}
@@ -2120,6 +2432,49 @@ async function rebootDevice() {
}, 10000);
}
// ── WireGuard VPN ─────────────────────────────────────────────────────────────
async function wgInstall(){
if(!confirm('wireguard + wireguard-tools jetzt per apt-get installieren?\n\nDauer: ca. 3090 Sekunden.'))return;
flash('wg-flash','ok','Starte Installation…');
const r=await api('/wireguard/install','POST');
if(r.error) flash('wg-flash','err',r.error);
}
async function wgUninstall(){
if(!confirm('WireGuard wirklich deinstallieren?\n\nDer aktive VPN-Tunnel wird vorher getrennt.\nDie Konfigurationsdatei bleibt erhalten.'))return;
flash('wg-flash','ok','Deinstalliere…');
const r=await api('/wireguard/uninstall','POST');
if(r.error) flash('wg-flash','err',r.error);
}
async function wgConnect(){
$('wg-btn-connect').disabled=true;
flash('wg-flash','ok','Verbinde VPN…');
await api('/wireguard/connect','POST');
}
async function wgDisconnect(){
$('wg-btn-disconnect').disabled=true;
flash('wg-flash','ok','Trenne VPN…');
const r=await api('/wireguard/disconnect','POST');
if(!r.ok) flash('wg-flash','err','Trennen fehlgeschlagen');
}
async function wgSaveConfig(){
const content=$('wg-config').value.trim();
if(!content){flash('wg-flash','err','Konfiguration ist leer');return;}
if(!content.includes('[Interface]')){flash('wg-flash','err','Ungültige Konfiguration [Interface] fehlt');return;}
const auto=$('wg-auto').checked;
flash('wg-flash','ok','Speichere…');
const r=await api('/wireguard/config','POST',{content,auto});
if(r.error){flash('wg-flash','err',r.error);return;}
flash('wg-flash','ok','✓ Konfiguration gespeichert');
}
async function loadWgConfig(){
try{
const r=await api('/wireguard/config');
if(r.exists && r.config) $('wg-config').value=r.config;
const c=await api('/config');
$('wg-auto').checked=!!c.wireguard_auto;
}catch(e){}
}
function flash(id,cls,msg){
const el=$(id); el.className='flash '+cls; el.textContent=msg; el.style.display='block';
if(cls==='ok') setTimeout(()=>el.style.display='none',3500);
@@ -2129,6 +2484,7 @@ function flash(id,cls,msg){
await loadCfg();
await refreshDevices();
await loadUTs();
await loadWgConfig();
expl.load('');
setInterval(poll,1500);
setInterval(refreshDevices,8000);
@@ -2144,8 +2500,12 @@ function flash(id,cls,msg){
if __name__ == '__main__':
cleanup_stale_mounts()
load_state()
wg_update_state()
threading.Thread(target=usb_monitor, daemon=True).start()
threading.Thread(target=wifi_monitor, daemon=True).start()
threading.Thread(target=wg_monitor, daemon=True).start()
threading.Thread(target=update_check_loop, daemon=True).start()
if load_cfg().get('wireguard_auto') and WG_CONF.exists():
threading.Thread(target=wg_connect, daemon=True).start()
log.info(f'PiCopy v{VERSION} läuft auf http://0.0.0.0:8080')
app.run(host='0.0.0.0', port=8080, debug=False, use_reloader=False)