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>")
|