From 8c7ac9f9d3c804b95c8f49aea4535582ec495ee3 Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Mon, 9 Sep 2024 22:22:53 +0800 Subject: [PATCH] initial commit --- .gitignore | 4 + conf/example.toml | 17 +++ pyproject.toml | 53 +++++++ requirements-dev.txt | 84 +++++++++++ requirements.txt | 21 +++ src/wg_builder.egg-info/PKG-INFO | 27 ++++ src/wg_builder.egg-info/SOURCES.txt | 10 ++ src/wg_builder.egg-info/dependency_links.txt | 1 + src/wg_builder.egg-info/entry_points.txt | 2 + src/wg_builder.egg-info/requires.txt | 11 ++ src/wg_builder.egg-info/top_level.txt | 1 + src/wg_builder/__init__.py | 0 src/wg_builder/builder.py | 143 +++++++++++++++++++ src/wg_builder/cli.py | 37 +++++ 14 files changed, 411 insertions(+) create mode 100644 .gitignore create mode 100644 conf/example.toml create mode 100644 pyproject.toml create mode 100644 requirements-dev.txt create mode 100644 requirements.txt create mode 100644 src/wg_builder.egg-info/PKG-INFO create mode 100644 src/wg_builder.egg-info/SOURCES.txt create mode 100644 src/wg_builder.egg-info/dependency_links.txt create mode 100644 src/wg_builder.egg-info/entry_points.txt create mode 100644 src/wg_builder.egg-info/requires.txt create mode 100644 src/wg_builder.egg-info/top_level.txt create mode 100644 src/wg_builder/__init__.py create mode 100644 src/wg_builder/builder.py create mode 100644 src/wg_builder/cli.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..161beda --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.venv +.idea +*.pyc +__pycache__ diff --git a/conf/example.toml b/conf/example.toml new file mode 100644 index 0000000..959e73c --- /dev/null +++ b/conf/example.toml @@ -0,0 +1,17 @@ +interface_name = "docker-cluster" +netmask = "10.45.62.0/24" +dns = [ "192.168.78.7", "1.1.1.1"] + +[[server]] +name = "odroid-c4" +address = '155.24.76.11' +port = 1234 + +[[peer]] +name = "odroid-hc1" + +[[peer]] +name = "woryzen" + +[[peer]] +name = "sugo14" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3f83604 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,53 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "wg_builder" +version = "0.0.1" +authors = [ + { name="Walter Oggioni", email="oggioni.walter@gmail.com" }, +] +description = "Wireguard network builder" +readme = "README.md" +requires-python = ">=3.12" +classifiers = [ + 'Development Status :: 3 - Alpha', + 'Topic :: Utilities', + 'License :: OSI Approved :: MIT License', + 'Intended Audience :: System Administrators', + 'Intended Audience :: Developers', + 'Environment :: Console', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3', +] +dependencies = [ + 'python-wireguard', + 'toml', + 'netaddr', + 'typing_extensions==4.7.1', + 'pwo' +] + +[project.optional-dependencies] +dev = [ + "build", "pip-tools", "mypy", "ipdb" +] + + +[project.urls] +"Homepage" = "https://gitea.woggioni.net/woggioni/wg_builder" +"Bug Tracker" = "https://gitea.woggioni.net/woggioni/wg_builder/issues" + +[project.scripts] +wg-builder = "wg_builder.cli:main" + +[tool.mypy] +python_version = "3.12" +disallow_untyped_defs = true +show_error_codes = true +no_implicit_optional = true +warn_return_any = true +warn_unused_ignores = true +exclude = ["scripts", "docs", "test"] +strict = true \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..235d061 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,84 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile --extra=dev --output-file=requirements-dev.txt --strip-extras pyproject.toml +# +--index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple/ +--extra-index-url https://pypi.org/simple + +asttokens==2.4.1 + # via stack-data +build==1.2.2 + # via + # pip-tools + # wg_builder (pyproject.toml) +click==8.1.7 + # via pip-tools +decorator==5.1.1 + # via + # ipdb + # ipython +executing==2.1.0 + # via stack-data +ipdb==0.13.13 + # via wg_builder (pyproject.toml) +ipython==8.27.0 + # via ipdb +jedi==0.19.1 + # via ipython +matplotlib-inline==0.1.7 + # via ipython +mypy==1.11.2 + # via wg_builder (pyproject.toml) +mypy-extensions==1.0.0 + # via mypy +netaddr==1.3.0 + # via wg_builder (pyproject.toml) +packaging==24.1 + # via build +parso==0.8.4 + # via jedi +pexpect==4.9.0 + # via ipython +pip-tools==7.4.1 + # via wg_builder (pyproject.toml) +prompt-toolkit==3.0.47 + # via ipython +ptyprocess==0.7.0 + # via pexpect +pure-eval==0.2.3 + # via stack-data +pwo==0.0.2 + # via wg_builder (pyproject.toml) +pygments==2.18.0 + # via ipython +pyproject-hooks==1.1.0 + # via + # build + # pip-tools +python-wireguard==0.2.2 + # via wg_builder (pyproject.toml) +six==1.16.0 + # via asttokens +stack-data==0.6.3 + # via ipython +toml==0.10.2 + # via wg_builder (pyproject.toml) +traitlets==5.14.3 + # via + # ipython + # matplotlib-inline +typing-extensions==4.7.1 + # via + # mypy + # pwo + # wg_builder (pyproject.toml) +wcwidth==0.2.13 + # via prompt-toolkit +wheel==0.44.0 + # via pip-tools + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0686243 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,21 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile --output-file=requirements.txt --strip-extras pyproject.toml +# +--index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple/ +--extra-index-url https://pypi.org/simple + +netaddr==1.3.0 + # via wg_builder (pyproject.toml) +pwo==0.0.2 + # via wg_builder (pyproject.toml) +python-wireguard==0.2.2 + # via wg_builder (pyproject.toml) +toml==0.10.2 + # via wg_builder (pyproject.toml) +typing-extensions==4.7.1 + # via + # pwo + # wg_builder (pyproject.toml) diff --git a/src/wg_builder.egg-info/PKG-INFO b/src/wg_builder.egg-info/PKG-INFO new file mode 100644 index 0000000..d0d12c9 --- /dev/null +++ b/src/wg_builder.egg-info/PKG-INFO @@ -0,0 +1,27 @@ +Metadata-Version: 2.1 +Name: wg_builder +Version: 0.0.1 +Summary: Wireguard network builder +Author-email: Walter Oggioni +Project-URL: Homepage, https://gitea.woggioni.net/woggioni/wg_builder +Project-URL: Bug Tracker, https://gitea.woggioni.net/woggioni/wg_builder/issues +Classifier: Development Status :: 3 - Alpha +Classifier: Topic :: Utilities +Classifier: License :: OSI Approved :: MIT License +Classifier: Intended Audience :: System Administrators +Classifier: Intended Audience :: Developers +Classifier: Environment :: Console +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Requires-Python: >=3.12 +Description-Content-Type: text/markdown +Requires-Dist: python-wireguard +Requires-Dist: toml +Requires-Dist: netaddr +Requires-Dist: typing_extensions==4.7.1 +Requires-Dist: pwo +Provides-Extra: dev +Requires-Dist: build; extra == "dev" +Requires-Dist: pip-tools; extra == "dev" +Requires-Dist: mypy; extra == "dev" +Requires-Dist: ipdb; extra == "dev" diff --git a/src/wg_builder.egg-info/SOURCES.txt b/src/wg_builder.egg-info/SOURCES.txt new file mode 100644 index 0000000..94eb095 --- /dev/null +++ b/src/wg_builder.egg-info/SOURCES.txt @@ -0,0 +1,10 @@ +pyproject.toml +src/wg_builder/__init__.py +src/wg_builder/builder.py +src/wg_builder/cli.py +src/wg_builder.egg-info/PKG-INFO +src/wg_builder.egg-info/SOURCES.txt +src/wg_builder.egg-info/dependency_links.txt +src/wg_builder.egg-info/entry_points.txt +src/wg_builder.egg-info/requires.txt +src/wg_builder.egg-info/top_level.txt \ No newline at end of file diff --git a/src/wg_builder.egg-info/dependency_links.txt b/src/wg_builder.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/wg_builder.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/wg_builder.egg-info/entry_points.txt b/src/wg_builder.egg-info/entry_points.txt new file mode 100644 index 0000000..6cdbcbc --- /dev/null +++ b/src/wg_builder.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +wg-builder = wg_builder.cli:main diff --git a/src/wg_builder.egg-info/requires.txt b/src/wg_builder.egg-info/requires.txt new file mode 100644 index 0000000..175611e --- /dev/null +++ b/src/wg_builder.egg-info/requires.txt @@ -0,0 +1,11 @@ +python-wireguard +toml +netaddr +typing_extensions==4.7.1 +pwo + +[dev] +build +pip-tools +mypy +ipdb diff --git a/src/wg_builder.egg-info/top_level.txt b/src/wg_builder.egg-info/top_level.txt new file mode 100644 index 0000000..52ecead --- /dev/null +++ b/src/wg_builder.egg-info/top_level.txt @@ -0,0 +1 @@ +wg_builder diff --git a/src/wg_builder/__init__.py b/src/wg_builder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/wg_builder/builder.py b/src/wg_builder/builder.py new file mode 100644 index 0000000..bbd18da --- /dev/null +++ b/src/wg_builder/builder.py @@ -0,0 +1,143 @@ +import tarfile +from base64 import b64encode +from os import makedirs +from pathlib import Path +from random import SystemRandom +from tarfile import TarInfo +from textwrap import dedent + +import toml +from netaddr import IPNetwork +from netaddr.ip import IPAddress +from pwo import Maybe +from python_wireguard import Key + +random = SystemRandom() +from io import BytesIO +import codecs + +def genPsk() -> str: + return b64encode(random.randbytes(32)).decode('UTF-8') + + +def build_network(input_file: Path = Path('/dev/stdin'), output_folder: Path = Path('.'), archive=False, **kwargs): + with open(input_file) as conf: + configuration = toml.load(conf) + + if 'dns' in configuration: + dns = tuple(IPAddress(address) for address in configuration['dns']) + else: + dns = tuple() + + network_name = Maybe.of_nullable(configuration.get('network_name')).or_else(input_file.stem) + network = IPNetwork(configuration['netmask']) + address_it = network.iter_hosts() + + class Server: + endpoint: str + address: str + port: int + private_key: str + public_key: str + + def __init__(self, conf): + self.name = conf['name'] + self.private_key, self.public_key = Key.key_pair() + self.address = str(address_it.__next__()) + self.endpoint = conf['address'] + self.port = conf['port'] + + class Peer: + name: str + private_key: str + public_key: str + allowed_ip: str + + def __init__(self, conf): + self.name = conf['name'] + self.private_key, self.public_key = Key.key_pair() + self.allowed_ip = str(address_it.__next__()) + + servers = [Server(server) for server in configuration['server']] + + peers = [Peer(peer) for peer in configuration['peer']] + + pre_shared_keys = {(server, peer): genPsk() for peer in peers for server in servers} + + + def write_server_conf(server, file_obj): + text = dedent(f"""\ + [Interface] + Address = {server.address} + ListenPort = {server.port} + PrivateKey = {str(server.private_key)} + """) + file_obj.write(text) + for peer in peers: + text = dedent(f"""\ + + [Peer] + PublicKey = {peer.public_key} + AllowedIPs = {peer.allowed_ip} + PresharedKey = {pre_shared_keys[(server, peer)]} + """) + file_obj.write(text) + + def write_client_conf(peer, file_obj): + text = dedent(f"""\ + [Interface] + Address = {peer.allowed_ip}/32 + PrivateKey = {str(peer.private_key)} + """) + dns_config = (Maybe.of(dns) + .filter(lambda it: len(it) > 0) + .map(lambda it: f'DNS = {', '.join((str(addr) for addr in it))}\n') + .or_else('')) + file_obj.write(text + dns_config) + for server in servers: + text = dedent(f"""\ + + [Peer] + PublicKey = {server.public_key} + PresharedKey = {pre_shared_keys[(server, peer)]} + Endpoint = {server.endpoint}:{server.port} + AllowedIPs = {str(network)} + """) + file_obj.write(text) + + makedirs(output_folder, exist_ok=True) + for server in servers: + if archive: + with tarfile.open(output_folder / f'{server.name}.tar.gz', 'w:gz') as archive: + with BytesIO() as bio: + writer = codecs.getwriter('utf-8')(bio) + write_server_conf(server, writer) + name = f'/etc/wireguard/{network_name}.conf' + tarinfo = TarInfo(name) + tarinfo.size = bio.tell() + bio.seek(0) + archive.addfile(tarinfo, bio) + else: + server_folder = output_folder / f'{server.name}' + makedirs(server_folder, exist_ok=True) + with (open(server_folder / f'{network_name}.conf', 'w') as f): + write_server_conf(server, f) + + for peer in peers: + if archive: + with tarfile.open(output_folder / f'{peer.name}.tar.gz', 'w:gz') as archive: + with BytesIO() as bio: + writer = codecs.getwriter('utf-8')(bio) + write_client_conf(peer, writer) + name = f'/etc/wireguard/{network_name}.conf' + tarinfo = TarInfo(name) + tarinfo.size = bio.tell() + bio.seek(0) + archive.addfile(tarinfo, bio) + else: + peer_folder = output_folder / f'{peer.name}' + makedirs(peer_folder, exist_ok=True) + with (open(peer_folder / f'{network_name}.conf', 'w') as f): + write_client_conf(peer, f) + + diff --git a/src/wg_builder/cli.py b/src/wg_builder/cli.py new file mode 100644 index 0000000..c5b845d --- /dev/null +++ b/src/wg_builder/cli.py @@ -0,0 +1,37 @@ +from typing_extensions import Optional, List + +import argparse +import sys + +from pathlib import Path +from .builder import build_network + +def main(args: Optional[List[str]] = None): + # Create the parser + parser = argparse.ArgumentParser(description="A simple CLI program to create Wireguard networks") + # subparsers = parser.add_subparsers(title="subcommands", description="valid subcommands") + parser.add_argument( + '-i', + '--input-file', + help='Input file (defaults to stdin)', + type=Path, + ) + parser.add_argument( + '-o', + '--output-folder', + help='Output folder (defaults to .)', + type=Path, + ) + parser.add_argument( + '-a', + '--archive', + help='store the configuration in archive files', + action='store_true', + ) + parser.set_defaults(func=build_network) + args = parser.parse_args(args) + args.func(**vars(args)) + + +if __name__ == '__main__': + main(sys.argv)