diff options
| author | Ahmed <git@gumx.cc> | 2026-06-14 01:46:29 +0300 |
|---|---|---|
| committer | Ahmed <git@gumx.cc> | 2026-06-14 01:46:29 +0300 |
| commit | 5a8d568931d9b23ce0df1265d05259a7012081c9 (patch) | |
| tree | 4ea19b8bb1763a38246f342f172c285f6579e09b /alfred/vpn.py | |
init: mostly vibed
Diffstat (limited to 'alfred/vpn.py')
| -rw-r--r-- | alfred/vpn.py | 170 |
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>") |
