aboutsummaryrefslogtreecommitdiffstats
path: root/alfred/vpn.py
blob: 6209a96af9818f3d6adf5b12f37f8dc77d01fb70 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
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>")