|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +""" |
| 4 | + File: nebulder |
| 5 | +
|
| 6 | + Description: Generate Nebula configs based on a network outline |
| 7 | +
|
| 8 | + MIT License: Copyright (c) 2023 Eryk J. |
| 9 | +
|
| 10 | + Permission is hereby granted, free of charge, to any person obtaining a copy |
| 11 | + of this software and associated documentation files (the "Software"), to deal |
| 12 | + in the Software without restriction, including without limitation the rights |
| 13 | + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 14 | + copies of the Software, and to permit persons to whom the Software is |
| 15 | + furnished to do so, subject to the following conditions: |
| 16 | +
|
| 17 | + The above copyright notice and this permission notice shall be included in all |
| 18 | + copies or substantial portions of the Software. |
| 19 | +
|
| 20 | + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 21 | + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 22 | + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 23 | + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 24 | + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 25 | + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| 26 | + SOFTWARE. |
| 27 | +""" |
| 28 | + |
| 29 | +APP = 'nebulder' |
| 30 | +VERSION = 'v0.0.1' |
| 31 | + |
| 32 | + |
| 33 | +import argparse, os, subprocess, yaml |
| 34 | + |
| 35 | + |
| 36 | +def sh(command, arguments='', inp=''): |
| 37 | + res = subprocess.run([command, arguments], stdout=subprocess.PIPE, stderr=subprocess.PIPE, input=inp.encode('utf-8')) |
| 38 | + if res.stderr.decode('utf-8'): |
| 39 | + print(res.stderr.decode('utf-8')) |
| 40 | + exit() |
| 41 | + return res.stdout.decode('utf-8') |
| 42 | + |
| 43 | + |
| 44 | +def create_deploy_script(name, port): |
| 45 | + if port: |
| 46 | + note = f'echo "You may need to add a rule to your firewall to allow traffic to the WireGuard interface\'s port:"\necho "sudo ufw allow {port}/udp"\necho "sudo ufw reload"\n\n' |
| 47 | + else: |
| 48 | + note = '' |
| 49 | + return f'#!/bin/bash\n\nsystemctl stop wg-quick@{name}\nsystemctl disable wg-quick@{name}\n\ncp --remove-destination {name}.conf /etc/wireguard/\nchown root:root /etc/wireguard/{name}.conf\nchmod 600 /etc/wireguard/{name}.conf\n\nsystemctl enable wg-quick@{name}\nsystemctl start wg-quick@{name}\n\nwg show {name}\n\n{note}exit 0' |
| 50 | + |
| 51 | +def create_remove_script(name): |
| 52 | + return f'#!/bin/bash\n\nsystemctl stop wg-quick@{name}\nsystemctl disable wg-quick@{name}\n\nrm /etc/wireguard/{name}.conf\n\nwg show\n\necho "You may also need to check your firewall rules"\n\nexit 0' |
| 53 | + |
| 54 | + |
| 55 | +def process_config(config, dir): |
| 56 | + with open(config) as f: |
| 57 | + mesh = yaml.load(f, Loader=yaml.loader.SafeLoader) |
| 58 | + |
| 59 | + dir += '/' + mesh['NetworkName'] |
| 60 | + os.makedirs(dir, exist_ok=True) |
| 61 | + |
| 62 | + for device in mesh.keys(): |
| 63 | + if device == 'NetworkName': |
| 64 | + continue |
| 65 | + os.makedirs(dir + '/' + device, exist_ok=True) |
| 66 | + if 'PrivateKey' not in mesh[device].keys(): |
| 67 | + mesh[device]['PrivateKey'] = sh('wg', 'genkey').rstrip('\n') |
| 68 | + mesh[device]['PublicKey'] = sh('wg', 'pubkey', mesh[device]['PrivateKey']).rstrip('\n') |
| 69 | + |
| 70 | + for device in mesh.keys(): |
| 71 | + if device == 'NetworkName': |
| 72 | + continue |
| 73 | + if 'AllowedIPs' in mesh[device].keys(): |
| 74 | + subnet = '/24' |
| 75 | + routing = f"\n\n# IP forwarding\nPreUp = sysctl -w net.ipv4.ip_forward=1\n\n# IP masquerading\nPreUp = iptables -t mangle -A PREROUTING -i {mesh['NetworkName']} -j MARK --set-mark 0x30\nPreUp = iptables -t nat -A POSTROUTING ! -o {mesh['NetworkName']} -m mark --mark 0x30 -j MASQUERADE\nPostDown = iptables -t mangle -D PREROUTING -i {mesh['NetworkName']} -j MARK --set-mark 0x30\nPostDown = iptables -t nat -D POSTROUTING ! -o {mesh['NetworkName']} -m mark --mark 0x30 -j MASQUERADE" |
| 76 | + else: |
| 77 | + subnet = '/32' |
| 78 | + routing = '' |
| 79 | + conf = f"[Interface]\n# Name: {device}\nAddress = {mesh[device]['Address']}{subnet}\nPrivateKey = {mesh[device]['PrivateKey']}" |
| 80 | + if 'ListenPort' in mesh[device].keys(): |
| 81 | + conf += f"\nListenPort = {mesh[device]['ListenPort']}{routing}" |
| 82 | + else: |
| 83 | + mesh[device]['ListenPort'] = False |
| 84 | + if 'DNS' in mesh[device].keys(): |
| 85 | + conf += f"\nDNS = {mesh[device]['DNS']}" |
| 86 | + for peer in mesh.keys(): |
| 87 | + if peer == 'NetworkName' or peer == device: |
| 88 | + continue |
| 89 | + if 'Endpoint' not in mesh[peer].keys() and 'AllowedIPs' not in mesh[device].keys(): |
| 90 | + continue |
| 91 | + conf += f"\n\n[Peer]\n# Name: {peer}\nPublicKey = {mesh[peer]['PublicKey']}" |
| 92 | + if 'Endpoint' in mesh[peer].keys(): |
| 93 | + conf += f"\nEndpoint = {mesh[peer]['Endpoint']}:{mesh[peer]['ListenPort']}" |
| 94 | + |
| 95 | + if 'AllowedIPs' in mesh[peer].keys(): |
| 96 | + conf += f"\nAllowedIPs = {mesh[peer]['AllowedIPs']}" |
| 97 | + else: |
| 98 | + conf += f"\nAllowedIPs = {mesh[peer]['Address']}/32" |
| 99 | + if 'PersistentKeepalive' in mesh[device].keys(): |
| 100 | + conf += f"\nPersistentKeepalive = {mesh[device]['PersistentKeepalive']}" |
| 101 | + |
| 102 | + file_dir = f"{dir}/{device}/" |
| 103 | + with open(f"{file_dir}{mesh['NetworkName']}.conf", 'w', encoding='UTF-8') as f: |
| 104 | + f.write(conf) |
| 105 | + os.chmod(f"{file_dir}{mesh['NetworkName']}.conf", mode=0o600) |
| 106 | + with open(file_dir + f"deploy_{device}.sh", 'w', encoding='UTF-8') as f: |
| 107 | + f.write(create_deploy_script(mesh['NetworkName'], mesh[device]['ListenPort'])) |
| 108 | + os.chmod(f'{file_dir}deploy_{device}.sh', mode=0o740) |
| 109 | + with open(file_dir + f"remove_{device}.sh", 'w', encoding='UTF-8') as f: |
| 110 | + f.write(create_remove_script(mesh['NetworkName'])) |
| 111 | + os.chmod(f'{file_dir}remove_{device}.sh', mode=0o740) |
| 112 | + print(f'Generated config and scripts for {device}') |
| 113 | + |
| 114 | + |
| 115 | +parser = argparse.ArgumentParser(description="Generate Nebula configs based on a network outline") |
| 116 | +parser.add_argument('-v', '--version', action='version', version=f"{APP} {VERSION}") |
| 117 | +parser.add_argument("Outline", help='Network outline (YAML format)') |
| 118 | +parser.add_argument('-o', metavar='directory', help='Output directory (working dir if not provided)') |
| 119 | +args = vars(parser.parse_args()) |
| 120 | +if args['o']: |
| 121 | + dir = args['o'].rstrip('/') |
| 122 | +else: |
| 123 | + dir = '.' |
| 124 | +process_config(args['Outline'], dir) |
0 commit comments