dashboardwireguard-vpnwireguard-tunnelwireguard-dashboardwireguardwg-managervpnsite-to-siteobfuscationwireguard-vpn-setup
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
405 lines
12 KiB
405 lines
12 KiB
import logging
|
|
import random
|
|
import subprocess
|
|
import tempfile
|
|
import requests
|
|
import typing
|
|
import configparser
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
import const
|
|
import models
|
|
import schemas
|
|
import os
|
|
import re
|
|
import ipaddress
|
|
import util
|
|
from database import SessionLocal
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class WGAlreadyStartedError(Exception):
|
|
pass
|
|
|
|
|
|
class WGAlreadyStoppedError(Exception):
|
|
pass
|
|
|
|
|
|
class WGPermissionsError(Exception):
|
|
pass
|
|
|
|
|
|
class WGPortAlreadyInUse(Exception):
|
|
pass
|
|
|
|
|
|
class TempServerFile():
|
|
def __init__(self, server: schemas.WGServer):
|
|
self.server = server
|
|
self.td = tempfile.TemporaryDirectory(prefix="wg_man_")
|
|
self.server_file = os.path.join(self.td.name, f"{server.interface}.conf")
|
|
|
|
def __enter__(self):
|
|
with open(self.server_file, "w+") as f:
|
|
f.write(self.server.configuration)
|
|
return self.server_file
|
|
|
|
def __exit__(self, type, value, traceback):
|
|
self.td.cleanup()
|
|
|
|
|
|
def _run_wg(server: schemas.WGServer, command):
|
|
try:
|
|
output = subprocess.check_output(const.CMD_WG_COMMAND + command, stderr=subprocess.STDOUT)
|
|
return output
|
|
except Exception as e:
|
|
if b'Operation not permitted' in e.output:
|
|
raise WGPermissionsError("The user has insufficient permissions for interface %s" % server.interface)
|
|
|
|
|
|
def is_installed():
|
|
output = subprocess.check_output(const.CMD_WG_COMMAND)
|
|
return output == b'' or b'interface' in output
|
|
|
|
|
|
def generate_keys() -> typing.Dict[str, str]:
|
|
private_key = subprocess.check_output(const.CMD_WG_COMMAND + ["genkey"])
|
|
public_key = subprocess.check_output(
|
|
const.CMD_WG_COMMAND + ["pubkey"],
|
|
input=private_key
|
|
)
|
|
|
|
private_key = private_key.decode("utf-8").strip()
|
|
public_key = public_key.decode("utf-8").strip()
|
|
return dict(
|
|
private_key=private_key,
|
|
public_key=public_key
|
|
)
|
|
|
|
|
|
def generate_psk():
|
|
return subprocess.check_output(const.CMD_WG_COMMAND + ["genpsk"]).decode("utf-8").strip()
|
|
|
|
|
|
def start_interface(server: typing.Union[schemas.WGServer, schemas.WGPeer]):
|
|
with TempServerFile(server) as server_file:
|
|
try:
|
|
# print(*const.CMD_WG_QUICK, "up", server_file)
|
|
output = subprocess.check_output(const.CMD_WG_QUICK + ["up", server_file], stderr=subprocess.STDOUT)
|
|
return output
|
|
except Exception as e:
|
|
print(e.output)
|
|
if b'already exists' in e.output:
|
|
raise WGAlreadyStartedError("The wireguard device %s is already started." % server.interface)
|
|
elif b'Address already in use' in e.output:
|
|
raise WGPortAlreadyInUse("The port %s is already used by another application." % server.listen_port)
|
|
|
|
|
|
def stop_interface(server: schemas.WGServer):
|
|
with TempServerFile(server) as server_file:
|
|
try:
|
|
output = subprocess.check_output(const.CMD_WG_QUICK + ["down", server_file], stderr=subprocess.STDOUT)
|
|
return output
|
|
except Exception as e:
|
|
if b'is not a WireGuard interface' in e.output:
|
|
raise WGAlreadyStoppedError("The wireguard device %s is already stopped." % server.interface)
|
|
|
|
|
|
def restart_interface(server: schemas.WGServer):
|
|
try:
|
|
stop_interface(server)
|
|
except WGAlreadyStoppedError:
|
|
pass
|
|
start_interface(server)
|
|
|
|
|
|
def is_running(server: schemas.WGServer):
|
|
try:
|
|
output = _run_wg(server, ["show", server.interface])
|
|
if output is None or b'Unable to access interface: No such device' in output:
|
|
return False
|
|
except Exception as e:
|
|
print(e.output)
|
|
if b'No such device' in e.output:
|
|
return False
|
|
return True
|
|
|
|
|
|
def add_peer(server: schemas.WGServer, peer: schemas.WGPeer):
|
|
try:
|
|
output = _run_wg(server, ["set", server.interface, "peer", peer.public_key, "allowed-ips", peer.address])
|
|
return output == b''
|
|
except Exception as e:
|
|
_LOGGER.exception(e)
|
|
return False
|
|
|
|
|
|
def remove_peer(server: schemas.WGServer, peer: schemas.WGPeer):
|
|
try:
|
|
output = _run_wg(server, ["set", server.interface, "peer", peer.public_key, "remove"])
|
|
return output == b''
|
|
except Exception as e:
|
|
_LOGGER.exception(e)
|
|
return False
|
|
|
|
|
|
def get_stats(server: schemas.WGServer):
|
|
try:
|
|
output = _run_wg(server, ["show", server.interface])
|
|
if not output:
|
|
return []
|
|
regex = r"peer:.*?^\n"
|
|
test_str = output.decode("utf-8") + "\n"
|
|
|
|
peers = []
|
|
|
|
peers_raw = re.findall(regex, test_str, re.MULTILINE | re.DOTALL)
|
|
|
|
for peer in peers_raw:
|
|
peer = peer.strip()
|
|
lines = [x.split(": ")[1] for x in peer.split("\n")]
|
|
|
|
if len(lines) == 2:
|
|
public_key, allowed_ips = lines
|
|
peers.append(dict(
|
|
public_key=public_key,
|
|
client_endpoint=None,
|
|
allowed_ips=allowed_ips,
|
|
handshake=None,
|
|
rx=None,
|
|
tx=None
|
|
))
|
|
elif len(lines) == 5 or len(lines) == 6:
|
|
public_key = lines[0]
|
|
client_endpoint, allowed_ips, handshake, rx_tx = lines[-4:] # [1] is sometimes psk
|
|
|
|
rx = re.match(r"^(.*) received", rx_tx).group(1)
|
|
tx = re.match(r"^.*, (.*)sent", rx_tx).group(1)
|
|
peers.append(dict(
|
|
public_key=public_key,
|
|
client_endpoint=client_endpoint,
|
|
allowed_ips=allowed_ips,
|
|
handshake=handshake,
|
|
rx=rx,
|
|
tx=tx
|
|
))
|
|
|
|
else:
|
|
ValueError("We have not handled peers with line number of %s" % str(len(lines)))
|
|
|
|
return peers
|
|
except Exception as e:
|
|
_LOGGER.exception(e)
|
|
return []
|
|
|
|
|
|
def move_server_dir(interface, interface1):
|
|
old_server_dir = const.SERVER_DIR(interface)
|
|
old_server_file = const.SERVER_FILE(interface)
|
|
new_server_dir = const.SERVER_DIR(interface1)
|
|
new_server_file = old_server_file.replace(f"{interface}.conf", f"{interface1}.conf")
|
|
|
|
os.rename(old_server_file, new_server_file)
|
|
os.rename(old_server_dir, new_server_dir)
|
|
|
|
|
|
def generate_config(obj: typing.Union[typing.Dict[schemas.WGPeer, schemas.WGServer], schemas.WGServer]):
|
|
if isinstance(obj, dict) and "server" in obj and "peer" in obj:
|
|
template = "peer.j2"
|
|
is_ipv6 = obj["server"].v6_address is not None
|
|
elif isinstance(obj, schemas.WGServer) or isinstance(obj, models.WGServer):
|
|
template = "server.j2"
|
|
is_ipv6 = obj.v6_address is not None
|
|
else:
|
|
raise ValueError("Incorrect input type. Should be WGPeer or WGServer")
|
|
|
|
result = util.jinja_env.get_template(template).render(
|
|
data=obj,
|
|
is_ipv6=is_ipv6
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
def retrieve_client_conf_from_server(
|
|
client_name,
|
|
server_interface,
|
|
server_host,
|
|
server_api_key
|
|
):
|
|
const.CLIENT_NAME = "client-1"
|
|
const.CLIENT_SERVER_INTERFACE = "wg0"
|
|
const.CLIENT_SERVER_HOST = "http://localhost:4200"
|
|
const.CLIENT_API_KEY = "8bae20143fb962930614952d80634822361fd5ab9488053866a56de5881f9d7b"
|
|
|
|
assert server_interface is not None and \
|
|
server_host is not None and \
|
|
server_api_key is not None, "Client configuration is invalid: %s, %s, api-key-is-null?: %s" % (
|
|
server_interface,
|
|
server_host,
|
|
server_api_key is None
|
|
)
|
|
|
|
api_get_or_add = f"{server_host}/api/v1/peer/configuration/get_or_add"
|
|
|
|
response = requests.post(api_get_or_add, json={
|
|
"server_interface": server_interface,
|
|
"name": client_name
|
|
}, headers={
|
|
"X-API-Key": server_api_key
|
|
})
|
|
|
|
if response.status_code != 200:
|
|
print(response.text)
|
|
raise RuntimeError("Could not retrieve config from server: %s" % (api_get_or_add,))
|
|
|
|
return response.text
|
|
|
|
|
|
def create_client_config(sess: Session, configuration, client_name, client_routes):
|
|
|
|
parser = configparser.ConfigParser()
|
|
parser.read_string(configuration)
|
|
public_key = parser["Peer"]["PublicKey"]
|
|
|
|
assert len(set(parser.sections()) - {"Interface", "Peer"}) == 0, "Missing Interface or Peer section"
|
|
|
|
# Parse Server
|
|
# Check if server already exists.
|
|
|
|
is_new_server = False
|
|
is_new_peer = False
|
|
|
|
try:
|
|
db_server = sess.query(models.WGServer).filter_by(
|
|
public_key=public_key,
|
|
read_only=1
|
|
).one()
|
|
except:
|
|
db_server = None
|
|
|
|
if db_server is None:
|
|
db_server = models.WGServer()
|
|
is_new_server = True
|
|
|
|
db_server.read_only = 1
|
|
db_server.public_key = parser["Peer"]["PublicKey"]
|
|
db_server.address = parser["Peer"]["Endpoint"]
|
|
db_server.listen_port = random.randint(69000, 19292009)
|
|
|
|
db_server.v6_address = "N/A"
|
|
db_server.v6_subnet = 0
|
|
db_server.address = "N/A"
|
|
db_server.subnet = 0
|
|
db_server.private_key = "N/A"
|
|
db_server.dns = "N/A"
|
|
db_server.post_up = "N/A"
|
|
db_server.post_down = "N/A"
|
|
db_server.is_running = False
|
|
db_server.configuration = "N/A"
|
|
|
|
# Parse client
|
|
try:
|
|
db_peer = sess.query(models.WGPeer).filter_by(
|
|
private_key=parser["Interface"]["PrivateKey"],
|
|
read_only=1
|
|
).one()
|
|
except:
|
|
db_peer = None
|
|
|
|
if db_peer is None:
|
|
db_peer = models.WGPeer()
|
|
is_new_peer = True
|
|
|
|
db_peer.read_only = 1
|
|
db_peer.name = client_name
|
|
|
|
addresses_split = parser["Interface"]["Address"].split(",")
|
|
assert len(addresses_split) > 0, "Must be at least one address"
|
|
|
|
for address_with_subnet in addresses_split:
|
|
addr, subnet = address_with_subnet.split("/")
|
|
|
|
if isinstance(ipaddress.ip_address(addr), ipaddress.IPv4Address):
|
|
db_peer.address = address_with_subnet
|
|
elif isinstance(ipaddress.ip_address(addr), ipaddress.IPv6Address):
|
|
db_peer.v6_address = address_with_subnet
|
|
else:
|
|
raise RuntimeError("Incorrect IP Address: %s, %s" % (addr, subnet))
|
|
|
|
db_peer.private_key = parser["Interface"]["PrivateKey"]
|
|
db_peer.public_key = "N/A"
|
|
db_peer.allowed_ips = client_routes if client_routes else parser["Peer"]["AllowedIPs"]
|
|
db_peer.configuration = configuration
|
|
db_server.interface = f"client_{db_peer.name}"
|
|
db_server.configuration = configuration
|
|
try:
|
|
db_peer.shared_key = parser["Interface"]["PrivateKey"]
|
|
except KeyError:
|
|
db_peer.shared_key = "N/A"
|
|
|
|
db_peer.dns = parser["Interface"]["DNS"]
|
|
db_peer.server = db_server
|
|
|
|
if is_new_server:
|
|
sess.add(db_server)
|
|
else:
|
|
sess.merge(db_server)
|
|
sess.commit()
|
|
|
|
if is_new_peer:
|
|
sess.add(db_peer)
|
|
else:
|
|
sess.merge(db_peer)
|
|
sess.commit()
|
|
|
|
if const.CLIENT_START_AUTOMATICALLY and not is_running(db_server):
|
|
start_interface(db_server)
|
|
|
|
|
|
def load_environment_clients(sess: Session):
|
|
i = 1
|
|
while True:
|
|
|
|
client_name = os.getenv(f"CLIENT_{i}_NAME", None)
|
|
client_server_interface = os.getenv(f"CLIENT_{i}_SERVER_INTERFACE", None)
|
|
client_server_host = os.getenv(f"CLIENT_{i}_SERVER_HOST", None)
|
|
client_api_key = os.getenv(f"CLIENT_{i}_API_KEY", None)
|
|
client_routes = os.getenv(f"CLIENT_{i}_ROUTES", None)
|
|
|
|
if client_api_key is None or \
|
|
client_server_interface is None or \
|
|
client_server_host is None or \
|
|
client_api_key is None:
|
|
break
|
|
|
|
_LOGGER.warning(
|
|
f"Found client configuration: name={client_name},siface={client_server_interface},shost={client_server_host}")
|
|
|
|
config = retrieve_client_conf_from_server(
|
|
client_name=client_name,
|
|
server_interface=client_server_interface,
|
|
server_host=client_server_host,
|
|
server_api_key=client_api_key
|
|
)
|
|
|
|
create_client_config(sess, configuration=config, client_name=client_name, client_routes=client_routes)
|
|
|
|
i += 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
os.environ["CLIENT_1_NAME"] = "client-1"
|
|
os.environ["CLIENT_1_SERVER_INTERFACE"] = "wg0"
|
|
os.environ["CLIENT_1_SERVER_HOST"] = "http://localhost:4200"
|
|
os.environ["CLIENT_1_API_KEY"] = "8bae20143fb962930614952d80634822361fd5ab9488053866a56de5881f9d7b"
|
|
os.environ["CLIENT_2_NAME"] = "client-2"
|
|
os.environ["CLIENT_2_SERVER_INTERFACE"] = "wg0"
|
|
os.environ["CLIENT_2_SERVER_HOST"] = "http://localhost:4200"
|
|
os.environ["CLIENT_2_API_KEY"] = "8bae20143fb962930614952d80634822361fd5ab9488053866a56de5881f9d7b"
|
|
sess: Session = SessionLocal()
|
|
load_environment_clients(sess)
|
|
sess.close()
|
|
|