Quando ho dovuto scegliere come collegarmi in VPN ad un servizio per faccende di lavoro, mi sono trovato con il mio pinguino a dover scegliere come farlo, dopo qualche prova e qualche esperimento le mie scelte sono converg… convers… finite su Wireguard, che sostanzialmente era quello che rompeva meno le scatole.
HINT!
Chiunque usi una VPN con WireGuard sa che il comando magico è sempre quello:
sudo systemctl start wg-quick@wg0
Hai bisogno di configurare WireGuard?
Pratico, sì… ma un po’ spartano: niente icona, niente stato visibile, niente click per avviare o fermare, solo terminale bruto … bene ma non benissimo insomma.
Sul mio bell’Ubuntu 22 ho spazio a sufficienza in alto a destra, per mostrare una meravigliosa pallina verde o rossa (no, non è la contrada dell’Oca) che ti dica qualcosa sullo stato del servizio.
Con un poco di intelligenza naturale e un poco di quella artificiale ho trovato una soluzione elegante.
L’idea è avere
- Una piccola icona nella tray bar (AppIndicator di GNOME).
- Pallina verde/grigia/rossa a seconda dello stato (
active
,inactive
,failed
). - Menu a tendina con le voci: Start, Stop, Restart, Quit.
- Una riga in fondo al menu: Current status: Active/Stopped/Error.
- Notifica desktop all’avvio e ad ogni cambio di stato (così se succede qualcosa, casca il mondo o la connessione, almeno ti si avvisa).
Ci spiego (credetemi lo sto facendo soprattutto per me stesso futuro), come raggiungere il risultato from scratch, come si dice in quella lingua tonta che è l’inglese.
Requisiti
Prima installiamo i pacchetti necessari:
sudo apt update
sudo apt install -y python3-gi gir1.2-appindicator3-0.1 python3-gi-cairo gir1.2-notify-0.7
Per comodità, conviene permettere all’utente corrente di gestire il servizio WireGuard senza dover digitare ogni volta la password.
Nota che, non è una cosa obbligatoria, anche se in alcuni contesti può essere utile per motivi di sicurezza.
Nel mio caso era utile non stare là a mettere password, così ho aggiunto questa regola ai sudoers
:
echo "$USER ALL=NOPASSWD: /bin/systemctl start wg-quick@wg0, /bin/systemctl stop wg-quick@wg0, /bin/systemctl restart wg-quick@wg0" \
| sudo tee /etc/sudoers.d/wg-indicator
Controlliamo che sia tutto valido:
sudo visudo -cf /etc/sudoers.d/wg-indicator
Qui dovrebbe venir fuori qualcosa tipo “/etc/sudoers.d/wg-indicator: analisi effettuata correttamente”
Lo script in Python
Ed ecco il cuore della faccenda: ~/.local/bin/wg-indicator.py
. Creati un attimo questo file (per esempio con touch o nano o come ti pare).
Lo script usa GTK, AppIndicator e Notify. Crea tre icone PNG per le palline colorate, gestisce i comandi systemctl
e aggiorna lo stato ogni 3 secondi.
#!/usr/bin/env python3
import os, subprocess, pathlib
import gi
gi.require_version('AppIndicator3', '0.1')
gi.require_version('Gtk', '3.0')
gi.require_version('Notify', '0.7')
from gi.repository import AppIndicator3, Gtk, GLib, Notify, GdkPixbuf
SERVICE = "wg-quick@wg0"
APP_ID = "wg-indicator"
ICON_DIR = os.path.join(pathlib.Path.home(), ".local", "share", "wg-indicator")
ICONS = {
"green": os.path.join(ICON_DIR, "dot-green.png"),
"grey": os.path.join(ICON_DIR, "dot-grey.png"),
"red": os.path.join(ICON_DIR, "dot-red.png"),
}
# stato precedente per notifiche su cambio stato
last_state = None
def ensure_icons():
os.makedirs(ICON_DIR, exist_ok=True)
try:
from PIL import Image, ImageDraw # se Pillow c'è, icone più “tonde”
for name, color in (("green",(0,185,0)), ("grey",(140,140,140)), ("red",(220,0,0))):
path = ICONS[name]
if not os.path.exists(path):
img = Image.new("RGBA", (24,24), (0,0,0,0))
d = ImageDraw.Draw(img)
d.ellipse((3,3,21,21), fill=color+(255,), outline=(0,0,0,40))
img.save(path, "PNG")
except Exception:
# fallback senza Pillow
for name, rgba in (("green",(0,185,0,255)), ("grey",(140,140,140,255)), ("red",(220,0,0,255))):
path = ICONS[name]
if os.path.exists(path):
continue
pix = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, 24, 24)
pix.fill(0x00000000)
rs, gs, bs, a = rgba
rowstride = pix.get_rowstride()
pixels = pix.get_pixels()
for y in range(24):
for x in range(24):
dx, dy = x-12, y-12
if dx*dx + dy*dy <= 9*9:
off = y*rowstride + x*4
pixels[off+0] = rs
pixels[off+1] = gs
pixels[off+2] = bs
pixels[off+3] = a
pix.savev(path, "png", [], [])
def svc_status():
try:
out = subprocess.check_output(["systemctl","is-active",SERVICE], text=True).strip()
except subprocess.CalledProcessError:
out = "inactive"
if out in ("active","inactive","activating","deactivating","reloading"):
return out
try:
f = subprocess.check_output(["systemctl","is-failed",SERVICE], text=True).strip()
if f == "failed":
return "failed"
except subprocess.CalledProcessError:
pass
return "unknown"
def human_status(state):
return {
"active":"Active",
"inactive":"Stopped",
"activating":"Starting…",
"deactivating":"Stopping…",
"reloading":"Reloading…",
"failed":"Error",
"unknown":"Error",
}.get(state, "Error")
def color_for_status(state):
if state == "active": return "green"
if state in ("inactive","activating","deactivating","reloading"): return "grey"
return "red" # failed/unknown
def icon_for_status(state):
return ICONS[color_for_status(state)]
def notify_status(prefix=None, state=None):
if state is None:
state = svc_status()
text = human_status(state)
title = "WireGuard Indicator"
body = f"{('' if not prefix else prefix + ' ')}Current status: {text}"
n = Notify.Notification.new(title, body, None)
n.set_timeout(3000)
try: n.show()
except: pass
def update_status_label(state):
# Aggiorna la label "Current status: X" con X colorato
global status_label
color = color_for_status(state)
text = human_status(state)
status_label.set_markup(f'<span alpha="80%">Current status: </span>'
f'<span weight="bold" foreground="{color}">{text}</span>')
def refresh(ind):
global last_state
state = svc_status()
ind.set_icon_full(icon_for_status(state), f"WireGuard: {human_status(state)}")
ind.set_label("", "") # niente testo accanto all’icona
update_status_label(state)
if last_state is None:
# notifica all’avvio (load)
notify_status(prefix="", state=state)
elif state != last_state:
# notifica su ogni cambio stato (anche se avviene fuori dal menu)
notify_status(prefix="", state=state)
last_state = state
return True
def run_cmd(cmd, verb):
try:
subprocess.check_call(["sudo","-n"] + cmd)
# La notifica “di conferma” la facciamo dopo, quando rileviamo lo stato aggiornato
# (così è affidabile). Però diamo un hint immediato:
notify_status(prefix=f"{verb} requested.", state=None)
except subprocess.CalledProcessError:
# Chiede autorizzazione grafica
ret = subprocess.call(["pkexec"] + cmd)
if ret != 0:
n = Notify.Notification.new("WireGuard Indicator", f"{verb} failed (authorization?)", None)
n.set_timeout(3000)
try: n.show()
except: pass
# Aggiorna subito l’icona; ulteriori cambi verranno col polling
GLib.idle_add(refresh, indicator)
def on_start(_): run_cmd(["/bin/systemctl","start",SERVICE], "Start")
def on_stop(_): run_cmd(["/bin/systemctl","stop",SERVICE], "Stop")
def on_restart(_): run_cmd(["/bin/systemctl","restart",SERVICE], "Restart")
def on_quit(_):
# Notifica finale con stato corrente e poi quit
notify_status(prefix="", state=svc_status())
Gtk.main_quit()
def build_menu():
global status_label
menu = Gtk.Menu()
for label, cb in (("Start", on_start), ("Stop", on_stop), ("Restart", on_restart)):
item = Gtk.MenuItem(label=label); item.connect("activate", cb); menu.append(item)
menu.append(Gtk.SeparatorMenuItem())
quit_item = Gtk.MenuItem(label="Quit")
quit_item.connect("activate", on_quit)
menu.append(quit_item)
# Status non cliccabile, sotto Quit
menu.append(Gtk.SeparatorMenuItem())
status_item = Gtk.MenuItem()
status_item.set_sensitive(False) # non cliccabile
status_label = Gtk.Label()
status_label.set_use_markup(True)
status_label.set_xalign(0.0)
status_item.add(status_label)
status_item.show_all()
menu.append(status_item)
menu.show_all()
return menu
# ---- main
ensure_icons()
Notify.init(APP_ID)
indicator = AppIndicator3.Indicator.new(APP_ID, ICONS["grey"], AppIndicator3.IndicatorCategory.SYSTEM_SERVICES)
indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
indicator.set_menu(build_menu())
# primo refresh + polling
refresh(indicator)
GLib.timeout_add_seconds(3, lambda: refresh(indicator))
Gtk.main()
Salviamo, rendiamo eseguibile:
chmod +x ~/.local/bin/wg-indicator.py
~/.local/bin/wg-indicator.py
Magia: in alto a destra compare la pallina (si spera).
Avvio automatico
Per farlo partire ad ogni login, creiamo un file .desktop
:
mkdir -p ~/.config/autostart
cat > ~/.config/autostart/wg-indicator.desktop <<'EOF'
[Desktop Entry]
Type=Application
Name=WireGuard Indicator
Exec=/usr/bin/env python3 /home/YOURUSER/.local/bin/wg-indicator.py
X-GNOME-Autostart-enabled=true
EOF
Ovviamente dove vedi YOURUSER
ci va il nome utente tuo.
In conclusione direi che …
Questo piccolo script fa quello che GNOME non ci regala di default: un indicatore di stato semplice e comprensibile per WireGuard. È pensato per wg-quick@wg0
, ma basta cambiare la variabile SERVICE
se usate un’altra interfaccia.
Ci sono alternative, a quanto mi suggeriva il ciattone GPT, ma se siete fan del fai-da-te digitale, questo approccio con Python vi dà più flessibilità e zero dipendenze strane, cosa che talvolta è preferibile.
Spero ciò sia utile a qualcheduno che respira, non solo ai bot delle AI che stanno in giro a ciucciare contenuti.
🪰