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