aboutsummaryrefslogtreecommitdiffstats
path: root/alfred/vpn.py
diff options
context:
space:
mode:
authorAhmed <git@gumx.cc>2026-06-14 01:46:29 +0300
committerAhmed <git@gumx.cc>2026-06-14 01:46:29 +0300
commit5a8d568931d9b23ce0df1265d05259a7012081c9 (patch)
tree4ea19b8bb1763a38246f342f172c285f6579e09b /alfred/vpn.py
init: mostly vibed
Diffstat (limited to 'alfred/vpn.py')
-rw-r--r--alfred/vpn.py170
1 files changed, 170 insertions, 0 deletions
diff --git a/alfred/vpn.py b/alfred/vpn.py
new file mode 100644
index 0000000..230aae1
--- /dev/null
+++ b/alfred/vpn.py
@@ -0,0 +1,170 @@
+"""
+Sopel plugin: WireGuard VPN peer management
+
+ !vpn add <name> <pubkey> - add peer, returns client config URL
+ !vpn remove <name> - remove peer
+ !vpn list - list all peers with IPs
+ !vpn pubkey - print server public key
+"""
+
+import json, os, subprocess, tempfile
+
+import requests
+from sopel import plugin
+
+PEERS = "/etc/wireguard/peers.json"
+PUBKEY = "/etc/wireguard/server.pub"
+FHOST = "http://127.0.0.1:5000"
+SUBNET = "10.0.0."
+
+
+def _owner(bot, trigger):
+ return trigger.nick == bot.settings.core.owner
+
+
+def _peers():
+ if os.path.exists(PEERS):
+ with open(PEERS) as f:
+ return json.load(f)
+ return {}
+
+
+def _save(peers):
+ with open(PEERS, "w") as f:
+ json.dump(peers, f, indent=2)
+
+
+def _next_ip(peers):
+ used = {p["ip"] for p in peers.values()}
+ for i in range(2, 255):
+ ip = f"{SUBNET}{i}"
+ if ip not in used:
+ return ip
+ return None
+
+
+def _server_pub():
+ with open(PUBKEY) as f:
+ return f.read().strip()
+
+
+def _wg(*args):
+ subprocess.run(["/usr/bin/wg"] + list(args), check=True)
+
+
+def _rewrite_conf(peers):
+ conf_path = "/etc/wireguard/wg0.conf"
+ with open(conf_path) as f:
+ lines = f.readlines()
+ new_lines, skip = [], False
+ for line in lines:
+ if line.strip() == "[Peer]":
+ skip = True
+ elif line.strip().startswith("[") and line.strip() != "[Peer]":
+ skip = False
+ if not skip:
+ new_lines.append(line)
+ conf = "".join(new_lines).rstrip() + "\n"
+ for name, p in peers.items():
+ conf += f"\n[Peer]\n# {name}\nPublicKey = {p['pubkey']}\nAllowedIPs = {p['ip']}/32\n"
+ with open(conf_path, "w") as f:
+ f.write(conf)
+
+
+def _upload(content):
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".conf", delete=False) as f:
+ f.write(content)
+ name = f.name
+ try:
+ with open(name, "rb") as fh:
+ r = requests.post(FHOST, files={"file": fh}, timeout=10)
+ r.raise_for_status()
+ return r.text.strip()
+ finally:
+ os.unlink(name)
+
+
+@plugin.command("vpn")
+def cmd_vpn(bot, trigger):
+ if not _owner(bot, trigger):
+ return
+
+ args = (trigger.group(2) or "").split()
+ if not args:
+ bot.say("usage: !vpn <add|remove|list|pubkey>")
+ return
+
+ action = args[0]
+ peers = _peers()
+
+ if action == "pubkey":
+ bot.say(_server_pub())
+
+ elif action == "list":
+ if not peers:
+ bot.say("no peers")
+ return
+ for name, p in peers.items():
+ bot.say(f"{name}: {p['ip']} {p['pubkey'][:24]}...")
+
+ elif action == "add":
+ if len(args) < 3:
+ bot.say("usage: !vpn add <name> <pubkey>")
+ return
+ name, pubkey = args[1], args[2]
+ if name in peers:
+ bot.say(f"{name}: already exists")
+ return
+ ip = _next_ip(peers)
+ if not ip:
+ bot.say("no IPs available")
+ return
+ peers[name] = {"ip": ip, "pubkey": pubkey}
+ _save(peers)
+ try:
+ _wg("set", "wg0", "peer", pubkey, "allowed-ips", f"{ip}/32")
+ _rewrite_conf(peers)
+ except subprocess.CalledProcessError as e:
+ bot.say(f"error: {e}")
+ return
+ pub = _server_pub()
+ cfg = (
+ f"[Interface]\n"
+ f"PrivateKey = <YOUR_PRIVATE_KEY>\n"
+ f"Address = {ip}/32\n"
+ f"DNS = 1.1.1.1\n\n"
+ f"[Peer]\n"
+ f"PublicKey = {pub}\n"
+ f"Endpoint = wk.fo:51820\n"
+ f"AllowedIPs = 0.0.0.0/0\n"
+ f"PersistentKeepalive = 25\n"
+ )
+ try:
+ url = _upload(cfg)
+ bot.say(f"{name}: {ip} — {url}")
+ except Exception:
+ bot.say(f"{name}: {ip} — upload failed. server pubkey: {pub}")
+
+ elif action == "remove":
+ if len(args) < 2:
+ bot.say("usage: !vpn remove <name>")
+ return
+ name = args[1]
+ if name not in peers:
+ bot.say(f"{name}: not found")
+ return
+ pubkey = peers[name]["pubkey"]
+ try:
+ _wg("set", "wg0", "peer", pubkey, "remove")
+ except subprocess.CalledProcessError as e:
+ bot.say(f"warning: live removal failed: {e}")
+ del peers[name]
+ _save(peers)
+ try:
+ _rewrite_conf(peers)
+ except Exception:
+ pass
+ bot.say(f"{name}: removed")
+
+ else:
+ bot.say("usage: !vpn <add|remove|list|pubkey>")