diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5e1f856 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,42 @@ +# Changelog + +## 2.0.0 (November 15, 2020) + +Version 2.0.0 is a complete rewrite of this software. The whole software is completely re-designed from scratch. Previous versions had a lot of problems: + +- Non-modular design +- Not scalable +- Command line arguments are not compliant with Unix conventions +- The interactive terminal requires the user to respond to all questions +- Profiles have to be saved and loaded manually +- The `readline` package does not work on Windows +- I was a worse coder in general two years ago + +These problems are all addressed in version 2.0.0. Below are some other noteworthy changes. + +- Handed argument parsing to Python's `argparse` library +- Added tabled peer information output +- Added more supported attributes + +## 1.3.0 (August 10, 2019) + +- Complete rebuild of project code structure +- Changed Peer object read and write method + - Now using `peer.__dict__` instead of receiving values in object constructor +- Added new wg-quick fields + - AllowedIPs + - Table + - PreUp + - PostUp + - PreDown + - PostDown +- Peer configuration now separated into basic and advanced configurations + +## 1.2.0 (May 16, 2019) + +- You can now set Aliases and Descriptions for peers. +- Profiles can now be saved and loaded in JSON format. + +## 1.1.7 (Feburary 20, 2019) + +- Public address can now be either FQDN or IP address, as requested by @KipourosV diff --git a/README.md b/README.md index 370df94..d4f735a 100644 --- a/README.md +++ b/README.md @@ -1,224 +1,184 @@ -# WireGuard Mesh Configurator +# wg-meshconf ## On the Horizon: `wg-dynamic` -`wg-dynamic` is a tool designed officially by WireGuard developing team. This new utility will provide a convenient way of configuring networks dynamically, where mesh network being one of the them. If you're interested, check it out at [wg-dynamic@github](https://github.com/WireGuard/wg-dynamic) or [wg-dynamic@official repository](https://git.zx2c4.com/wg-dynamic)). +`wg-dynamic` is a tool designed officially by the WireGuard developing team. This new utility will provide a convenient way of configuring networks dynamically, where mesh network being one of the them. If you're interested, check it out at [wg-dynamic@github](https://github.com/WireGuard/wg-dynamic) or [wg-dynamic@official repository](https://git.zx2c4.com/wg-dynamic). -## 1.2.0 (May 16, 2019) +## Version 2.0.0 -- You can now set Aliases and Descriptions for peers. -- Profiles can now be saved and loaded in JSON format. - -## 1.1.7 (Feburary 20, 2019) - -- Public address can now be either FQDN or IP address, as requested by @KipourosV +Say hello to version 2.0.0! This version is a complete rewrite of the pervious versions. Detailed information can be found in the [changelog](CHANGELOG.md). Please tell me if you like or hate this new design by posting an issue. ## Introduction -WireGuard mesh configurator is a tool that will help you generating peer configuration files for wireguard mesh networks. You generate configuration files for a large amount of peers easily and quickly via this tool. +wg-meshconf is a tool that will help you to generate peer configuration files for WireGuard mesh networks. You can easily and quickly create WireGuard mesh networks using this tool. ## Prerequisites -### System Packages +- Python 3 +- prettytable (optional) -|Package|Explanation|Example| -|-|-|-| -|ncurses dev package|Required by the installation of the Python `readline` library.|`libncurses5-dev` on Debian| +It is highly recommended to install `prettytable` so you can get beautiful tabular display of peer information. If you choose not to install `prettytable`, only plaintext output will be available. -### Python Libraries +Python packages can be installed via `pip3 install --user -Ur requirements.txt`. -The following libraries can be installed easily through executing `pip3 install -r requirements.txt` under the root directory of this repository. +## Learn by an Example -|Package|Explanation| -|-|-| -|`avalon_framework`|Command line I/O library| -|`readline`|For better interactive command line interface| -|`netaddr`|For calculating IP addresses| +Usages are dull and boring. Let's see a real-life example of how this tool can be used. This section will demonstrate how to create a simple mesh network with four nodes using wg-meshconf. -## Learn By An Example +For this example, suppose you have four servers as shown below. These servers can reach each other via the `Endpoint` address. For instance, server `tokyo1` can ping server `shanghai1` with the address `shanghai1.com`. -In this section, we will be going through how to configure a mesh network with the topology shown below using wireguard mesh configurator (this tool, **WGC**). +![image](https://user-images.githubusercontent.com/21986859/99200153-94839e80-279b-11eb-81c9-189b609661ee.png) -![example_topology](https://user-images.githubusercontent.com/21986859/47622988-edfbd080-dae1-11e8-97f6-ff8ef56ffecc.png) +### Step 1: Add Peers -### Step 1: Create a Profile +First we need to add all peers in the mesh network into the database. The basic syntax for adding new peers is: -Launch the WGC interactive shell. This will be where you give instructions to WGC. - -``` -$ python3 wireguard_mesh_configurator.py int +```shell +./wg-meshconf addpeer NAME --address IP_ADDRESS --address IP_ADDRESS_2 --endpoint ENDPOINT ``` -Every profile in WGC contains a complete topology. To create a new mesh network, we start by creating a new profile. Once a profile is created, you will be prompted automatically to enroll new peers. This leads us to the next step. +- New private key will be generated automatically if unspecified +- ListenPort defaults to 51820 per WireGuard standard +- All other values are left empty by default +There are more options which you can specify. Use the command `./wg-meshconf addpeer -h` for more details. + +After adding all the peers into the database, you can verify that they have all been added correctly via the `./wg-meshconf showpeers` command. The `simplify` switch here omits all columns with only `None`s. + +![image](https://user-images.githubusercontent.com/21986859/99202459-1dec9e00-27a7-11eb-8190-a5a3c6644d2a.png) + +### Step 2: Export Configuration Files + +Use the `genconfig` command to generate configuration files for all peers. You may also export configurations for only one peer by specifying the peer's name. + +The configuration files will be named after the peers' names. By default, all configuration files are exported into a subdirectory named `output`. You can change this by specifying output directory using the `-o` or the `--output` option. + +![image](https://user-images.githubusercontent.com/21986859/99202483-352b8b80-27a7-11eb-8479-8749e945a81d.png) + +### Step 3: Copy Configuration Files to Peers + +Copy each of the configuration files to the corresponding peers. + +![image](https://user-images.githubusercontent.com/21986859/99201225-e4fdfa80-27a1-11eb-9b27-6e684d30b784.png) + +### Step 4: Start WireGuard Services + +Start up the WireGuard interfaces using the `wg-quick` command. It is also possible to control WireGuard interfaces via WireGuard's `wg-quick@` systemd service. WireGuard status can be verified via the `wg` command after WireGuard interfaces are set up. + +![image](https://user-images.githubusercontent.com/21986859/99202554-7459dc80-27a7-11eb-9e92-44cd02bdc2f7.png) + +### Step 5: Verify Connectivity + +Verify that all endpoints have been configured properly and can connect to each other. + +![image](https://user-images.githubusercontent.com/21986859/99202822-5e98e700-27a8-11eb-8bb2-3e0d2222258f.png) + +Done. Now a mesh network has been created between the four servers. + +## Updating Peer Information + +If you would like to update a peer's information, use the `updatepeer` command. The syntax of `updatepeer` is the same as that of the `addpeer` command. Instead of adding a new peer, this command overwrites values in existing entries. + +In the example below, suppose you would like to update `tokyo1`'s endpoint address and change it to `tokyo321.com`. Use the `updatepeer` command and specify the new endpoint to be `tokyo321.com`. This will overwrite `tokyo1`'s existing `Endpoint` value. + +![image](https://user-images.githubusercontent.com/21986859/99204025-3a3f0980-27ac-11eb-9159-0e40fc2eefeb.png) + +## Show Peer Information + +The `showpeers` command prints all peers' information by default. + +![image](https://user-images.githubusercontent.com/21986859/99205966-11ba0e00-27b2-11eb-994a-6d2552ff1ca4.png) + +Now that's a lot of info and a lot of unnecessary columns which only have `None`s. Therefore, I added the `-s`/`--simplify` command which omits those useless columns. + +![image](https://user-images.githubusercontent.com/21986859/99206017-38784480-27b2-11eb-9022-21ba791ce28c.png) + +You may also query information about a specific peer. + +![image](https://user-images.githubusercontent.com/21986859/99206049-547be600-27b2-11eb-89e9-d7c942dfac44.png) + +Plaintext mode has a similar usage. It's just a bit harder to read, at least for me. + +![image](https://user-images.githubusercontent.com/21986859/99206104-76756880-27b2-11eb-844b-e5197afcbf99.png) + +## Deleting Peers + +Use the `delpeer` command to delete peers. The syntax is `delpeer PEER_NAME`. + +This example below shows how to delete the peer `tokyo1` from the database. + +![image](https://user-images.githubusercontent.com/21986859/99204215-e123a580-27ac-11eb-93b1-d07345004fab.png) + +## Database Files + +Unlike 1.x.x versions of wg-meshconf, version 2.0.0 does not require the user to save or load profiles. Instead, all add peer, update peer and delete peer operations are file operations. The changes will be saved to the database file immediately. The database file to use can be specified via the `-d` or the `--database` option. If no database file is specified, `database.json` will be used. + +Database files are essentially just JSON files. Below is an example. + +```json +{ + "peers": { + "tokyo1": { + "PrivateKey": "8NgwtCjISH6tznAzj5e3ujr3llxgdrLzCje9U6mUD1c=", + "Address": [ + "10.1.0.1/16" + ], + "ListenPort": 51820, + "Endpoint": "tokyo1.com" + }, + "germany1": { + "PrivateKey": "GFtKAG0zhWfa3LxPOG/d9zN6OfZKjL1CTyZih4oUjlU=", + "Address": [ + "10.2.0.1/16" + ], + "ListenPort": 51820, + "Endpoint": "germany1.com" + } + } +} ``` -[WGC]> new -``` - -![step1](https://user-images.githubusercontent.com/21986859/47623179-5d72bf80-dae4-11e8-9705-9158ea8f75c2.png) - -### Step 2: Enroll Peers - -Now that a profile has already been created, we need to input peers' information into WGC, so it knows the layout of the network. - -![enroll_peers](https://user-images.githubusercontent.com/21986859/47623237-526c5f00-dae5-11e8-823a-863e5372faa9.png) - -### Step 3: Export Configurations - -Now that we have all the peers in the profile, it's time to export everything into wireguard configuration files. We can dump configuration files into a directory using the `GenerateConfigurations` command. The following command will dump all the configuration files into the `/tmp/wg` directory. - -``` -[WGC]> gen /tmp/wg -``` - -![gen_config](https://user-images.githubusercontent.com/21986859/47623276-f8b86480-dae5-11e8-9c41-54bab4523031.png) - -We can also take a look at the generated configuration files. - -![generated_configs](https://user-images.githubusercontent.com/21986859/47623330-a3c91e00-dae6-11e8-84bd-85971b3092b3.png) - -### Step 4: Copy Configuration Files to Endpoints - -With the configuration files generated, all that's left to do is to copy the configuration files to the endpoints. Copy each configuration to the corresponding device with any method you like (sftp, ftps, plain copy & paste, etc.). - -Put the configuration file to `/etc/wireguard/wg0.conf` is recommended, since it will make us able to use the `wg-quick` command for express configuration. - -### Step 5: Enable WireGuard and Apply the Configuration - -Lets tell wireguard to create an interface with this configuration and make it a service, so the interface will be created as system is booted up. - -``` -$ sudo wg-quick up wg0 -$ sudo systemctl enable wg-quick@wg0 -``` - -![apply_enable](https://user-images.githubusercontent.com/21986859/47623379-3f5a8e80-dae7-11e8-9350-555e61884691.png) - -You can then verify the wireguard status via the `wg` command. - -``` -$ sudo wg -``` - -![wg_status](https://user-images.githubusercontent.com/21986859/47623489-9ca30f80-dae8-11e8-9241-3c7421b982db.png) - -### Step 6: Saving and Loading Profiles - -We can save a profile for future use using the `SaveProfile` command. The following example will save the profile to `/home/k4yt3x/example.pkl`. - -``` -[WGC]> save /home/k4yt3x/example.pkl -``` - -To load the profile, just use the `LoadProfile` command. - -``` -[WGC]> load /home/k4yt3x/example.pkl -``` - -Then you can use `ShowPeers` command to verify that everything has been loaded correctly. - -``` -[WGC]> sh -``` - -![saving_loading](https://user-images.githubusercontent.com/21986859/47623453-2d2d2000-dae8-11e8-9c21-528a7d9acde0.png) - -That concludes the "Learn By An Example" section. Hope it helps. ## Detailed Usages -### Installing WGC +You may refer to the program's help page for usages. Use the `-h` switch or the `--help` switch to print the help page. -Clone the repository and enter it. +```shell +$./wg-meshconf -h +usage: wg-meshconf [-h] [-d DATABASE] {addpeer,updatepeer,delpeer,showpeers,genconfig} ... -``` -$ git clone https://github.com/K4YT3X/wireguard-mesh-configurator.git -$ cd wireguard-mesh-configurator/ +positional arguments: + {addpeer,updatepeer,delpeer,showpeers,genconfig} + +optional arguments: + -h, --help show this help message and exit + -d DATABASE, --database DATABASE + path where the database file is stored (default: database.json) ``` -Install Python 3 dependencies. +Specify `-h` or `--help` after a command to see this command's usages. -``` -$ sudo pip3 install -r requirements.txt -``` - -Run the tool. - -``` -$ python3 wireguard_mesh_configurator.py interactive -``` - -or - -``` -$ python3 wireguard_mesh_configurator.py int -``` - -### Creating A Profile - -Run the `NewProfile` command to create a new profile. - -``` -[WGC]> NewProfile # Create new profile -``` - -Then the peer enrolling wizard will ask you for all the information needed for all the peers. Select `n` when being asked if you want to add a new peer to end the wizard. - -### Adding a Peer - -Use the `AddPeer` command to initialize the wizard of appending a new peer to the profile. - -``` -[WGC]> AddPeer -``` - -### Deleting a Peer - -Use the `DeletePeer` command to remove a peer from the profile. - -``` -[WGC]> DeletePeer [Peer Address (e.g. 10.0.0.1/8)] -``` - -### Generating Configurations - -Run the following command to dump your currently-loaded profile into configuration files and export them to `output path`. - -``` -[WGC]> GenerateConfigurations [output path] -``` - -### Viewing All Peers - -To view all the peers configurations in the current profile: - -``` -[WGC]> ShowPeers -``` - -### Saving / Loading Profiles - -To save a profile in JSON format: - -``` -[WGC]> JSONSaveProfile [output path] -``` - -To save a profile in Pickle format: - -``` -[WGC]> PickleSaveProfile [output path] -``` - -To load a profile in JSON format: - -``` -[WGC]> JSONLoadProfile [output path] -``` - -To load a profile in Pickle format: - -``` -[WGC]> PickleLoadProfile [output path] +```shell +$./wg-meshconf addpeer -h +usage: wg-meshconf addpeer [-h] --address ADDRESS [--endpoint ENDPOINT] [--privatekey PRIVATEKEY] [--listenport LISTENPORT] [--fwmark FWMARK] [--dns DNS] [--mtu MTU] [--table TABLE] [--preup PREUP] [--postup POSTUP] [--predown PREDOWN] [--postdown POSTDOWN] [--saveconfig] name + +positional arguments: + name Name used to identify this node + +optional arguments: + -h, --help show this help message and exit + --address ADDRESS address of the server + --endpoint ENDPOINT peer's public endpoint address + --privatekey PRIVATEKEY + private key of server interface + --listenport LISTENPORT + port to listen on + --fwmark FWMARK fwmark for outgoing packets + --dns DNS server interface DNS servers + --mtu MTU server interface MTU + --table TABLE server routing table + --preup PREUP command to run before interface is up + --postup POSTUP command to run after interface is up + --predown PREDOWN command to run before interface is down + --postdown POSTDOWN command to run after interface is down + --saveconfig save server interface to config upon shutdown ``` diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index b823ce2..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -avalon_framework -readline -netaddr \ No newline at end of file diff --git a/src/database_manager.py b/src/database_manager.py new file mode 100755 index 0000000..213e25b --- /dev/null +++ b/src/database_manager.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Name: Database Manager +Dev: K4YT3X +Date Created: July 19, 2020 +Last Modified: November 15, 2020 +""" + +# built-in imports +import copy +import json +import pathlib +import sys + +# third party imports +from prettytable import PrettyTable + +# local imports +from wireguard import WireGuard + +INTERFACE_ATTRIBUTES = [ + "Address", + "ListenPort", + "FwMark", + "PrivateKey", + "DNS", + "MTU", + "Table", + "PreUp", + "PostUp", + "PreDown", + "PostDown", + "SaveConfig", +] + +INTERFACE_OPTIONAL_ATTRIBUTES = [ + "ListenPort", + "FwMark", + "DNS", + "MTU", + "Table", + "PreUp", + "PostUp", + "PreDown", + "PostDown", + "SaveConfig", +] + +PEER_ATTRIBUTES = [ + "PublicKey", + "PresharedKey", + "AllowedIPs", + "Endpoint", + "PersistentKeepalive", +] + + +class DatabaseManager: + def __init__(self, database_path: pathlib.Path): + self.database_path = database_path + self.database_template = {"peers": {}} + self.wireguard = WireGuard() + + def read_database(self): + """ read database file into dict + + Returns: + dict: content of database file in dict format + """ + if not self.database_path.is_file(): + return self.database_template + + with self.database_path.open(mode="r", encoding="utf-8") as database_file: + return json.load(database_file) + + def write_database(self, data: dict): + """ dump data into database file + + Args: + data (dict): content of database + """ + with self.database_path.open(mode="w", encoding="utf-8") as database_file: + json.dump(data, database_file, indent=4) + + def addpeer( + self, + name: str, + Address: list, + Endpoint: str = None, + ListenPort: int = None, + FwMark: str = None, + PrivateKey: str = None, + DNS: str = None, + MTU: int = None, + Table: str = None, + PreUp: str = None, + PostUp: str = None, + PreDown: str = None, + PostDown: str = None, + SaveConfig: bool = None, + ): + database = copy.deepcopy(self.database_template) + database.update(self.read_database()) + + if name in database["peers"]: + print(f"Peer with name {name} already exists") + return + + database["peers"][name] = {} + + # if private key is not specified, generate one + if locals().get("PrivateKey") is None: + privatekey = self.wireguard.genkey() + database["peers"][name]["PrivateKey"] = privatekey + + for key in INTERFACE_ATTRIBUTES + PEER_ATTRIBUTES: + if locals().get(key) is not None: + database["peers"][name][key] = locals().get(key) + + self.write_database(database) + + def updatepeer( + self, + name: str, + Address: list = None, + Endpoint: str = None, + ListenPort: int = None, + FwMark: str = None, + PrivateKey: str = None, + DNS: str = None, + MTU: int = None, + Table: str = None, + PreUp: str = None, + PostUp: str = None, + PreDown: str = None, + PostDown: str = None, + SaveConfig: bool = None, + ): + database = copy.deepcopy(self.database_template) + database.update(self.read_database()) + + if name not in database["peers"]: + print(f"Peer with name {name} does not exist") + return + + for key in INTERFACE_ATTRIBUTES + PEER_ATTRIBUTES: + if locals().get(key) is not None: + database["peers"][name][key] = locals().get(key) + + self.write_database(database) + + def delpeer(self, name: str): + database = copy.deepcopy(self.database_template) + database.update(self.read_database()) + + # abort if user doesn't exist + if name not in database["peers"]: + print(f"Peer with ID {name} does not exist") + return + + database["peers"].pop(name, None) + + # write changes into database + self.write_database(database) + + def showpeers(self, name: str, style: str = "table", simplify: bool = False): + database = self.read_database() + + # if name is specified, show the specified peer + if name is not None: + if name not in database["peers"]: + print(f"Peer with ID {name} does not exist") + return + peers = [name] + + # otherwise, show all peers + else: + peers = [p for p in database["peers"]] + + field_names = ["name"] + + # exclude all columns that only have None's in simplified mode + if simplify is True: + for peer in peers: + for key in INTERFACE_ATTRIBUTES + PEER_ATTRIBUTES: + if ( + database["peers"][peer].get(key) is not None + and key not in field_names + ): + field_names.append(key) + + # include all columns by default + else: + field_names += INTERFACE_ATTRIBUTES + PEER_ATTRIBUTES + + # if the style is table + # print with prettytable + if style == "table": + table = PrettyTable() + table.field_names = field_names + + for peer in peers: + table.add_row( + [peer] + + [ + database["peers"][peer].get(k) + if not isinstance(database["peers"][peer].get(k), list) + else ",".join(database["peers"][peer].get(k)) + for k in [i for i in table.field_names if i != "name"] + ] + ) + + print(table) + + # if the style is text + # print in plaintext format + elif style == "text": + for peer in peers: + print(f"{'peer': <14}{peer}") + for key in field_names: + print( + f"{key: <14}{database['peers'][peer].get(key)}" + ) if not isinstance( + database["peers"][peer].get(key), list + ) else print( + f"{key: <14}{','.join(database['peers'][peer].get(key))}" + ) + print() + + def genconfig(self, name: str, output: pathlib.Path): + database = self.read_database() + + # check if peer ID is specified + if name is not None: + peers = [name] + else: + peers = [p for p in database["peers"]] + + # check if output directory is valid + # create output directory if it does not exist + if output.exists() and not output.is_dir(): + print( + "Error: output path already exists and is not a directory", + file=sys.stderr, + ) + raise FileExistsError + elif not output.exists(): + print(f"Creating output directory: {output}") + output.mkdir(exist_ok=True) + + # for every peer in the database + for peer in peers: + with (output / f"{peer}.conf").open("w") as config: + config.write("[Interface]\n") + config.write("# Name: {}\n".format(peer)) + config.write( + "Address = {}\n".format( + ", ".join(database["peers"][peer]["Address"]) + ) + ) + config.write( + "PrivateKey = {}\n".format(database["peers"][peer]["PrivateKey"]) + ) + + for key in INTERFACE_OPTIONAL_ATTRIBUTES: + if database["peers"][peer].get(key) is not None: + config.write( + "{} = {}\n".format(key, database["peers"][peer][key]) + ) + + # generate [Peer] sections for all other peers + for p in [i for i in database["peers"] if i != peer]: + config.write("\n[Peer]\n") + config.write("# Name: {}\n".format(p)) + config.write( + "PublicKey = {}\n".format( + self.wireguard.pubkey(database["peers"][p]["PrivateKey"]) + ) + ) + + if database["peers"][p].get("Endpoint") is not None: + config.write( + "Endpoint = {}:{}\n".format( + database["peers"][p]["Endpoint"], + database["peers"][p]["ListenPort"], + ) + ) + + if database["peers"][p].get("Address") is not None: + config.write( + "AllowedIPs = {}\n".format( + ", ".join(database["peers"][p]["Address"]) + ) + ) diff --git a/src/requirements.txt b/src/requirements.txt new file mode 100644 index 0000000..deb2d14 --- /dev/null +++ b/src/requirements.txt @@ -0,0 +1 @@ +prettytable diff --git a/src/wg-meshconf b/src/wg-meshconf new file mode 100755 index 0000000..db8262d --- /dev/null +++ b/src/wg-meshconf @@ -0,0 +1,187 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +Name: WireGuard Client Manager +Dev: K4YT3X +Date Created: July 19, 2020 +Last Modified: November 15, 2020 + +Licensed under the GNU General Public License Version 3 (GNU GPL v3), + available at: https://www.gnu.org/licenses/gpl-3.0.txt +(C) 2018-2020 K4YT3X +""" + +# built-in imports +import argparse +import pathlib +import sys + +# local imports +from database_manager import DatabaseManager + +VERSION = "2.0.0" + + +def parse_arguments(): + """ parse CLI arguments + """ + parser = argparse.ArgumentParser( + prog="wg-meshconf", formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "-d", + "--database", + type=pathlib.Path, + help="path where the database file is stored", + default=pathlib.Path("database.json"), + ) + + # add subparsers for commands + subparsers = parser.add_subparsers(dest="command") + + # add new peer + addpeer = subparsers.add_parser("addpeer") + addpeer.add_argument("name", help="Name used to identify this node") + addpeer.add_argument( + "--address", help="address of the server", action="append", required=True + ) + addpeer.add_argument("--endpoint", help="peer's public endpoint address") + addpeer.add_argument("--privatekey", help="private key of server interface") + addpeer.add_argument("--listenport", help="port to listen on", default=51820) + addpeer.add_argument("--fwmark", help="fwmark for outgoing packets") + addpeer.add_argument("--dns", help="server interface DNS servers") + addpeer.add_argument("--mtu", help="server interface MTU") + addpeer.add_argument("--table", help="server routing table") + addpeer.add_argument("--preup", help="command to run before interface is up") + addpeer.add_argument("--postup", help="command to run after interface is up") + addpeer.add_argument("--predown", help="command to run before interface is down") + addpeer.add_argument("--postdown", help="command to run after interface is down") + addpeer.add_argument( + "--saveconfig", + action="store_true", + help="save server interface to config upon shutdown", + default=None, + ) + + # update existing peer information + updatepeer = subparsers.add_parser("updatepeer") + updatepeer.add_argument("name", help="Name used to identify this node") + updatepeer.add_argument("--address", help="address of the server", action="append") + updatepeer.add_argument("--endpoint", help="peer's public endpoint address") + updatepeer.add_argument("--privatekey", help="private key of server interface") + updatepeer.add_argument("--listenport", help="port to listen on") + updatepeer.add_argument("--fwmark", help="fwmark for outgoing packets") + updatepeer.add_argument("--dns", help="server interface DNS servers") + updatepeer.add_argument("--mtu", help="server interface MTU") + updatepeer.add_argument("--table", help="server routing table") + updatepeer.add_argument("--preup", help="command to run before interface is up") + updatepeer.add_argument("--postup", help="command to run after interface is up") + updatepeer.add_argument("--predown", help="command to run before interface is down") + updatepeer.add_argument("--postdown", help="command to run after interface is down") + updatepeer.add_argument( + "--saveconfig", + action="store_true", + help="save server interface to config upon shutdown", + default=None, + ) + + # delpeer deletes a peer form the database + delpeer = subparsers.add_parser("delpeer") + delpeer.add_argument("name", help="Name of peer to delete") + + # showpeers prints a table of all peers and their configurations + showpeers = subparsers.add_parser("showpeers") + showpeers.add_argument( + "name", help="Name of the peer to query", nargs="?", + ) + showpeers.add_argument( + "--style", + choices=["table", "text"], + help="peers information printing style", + default="table", + ) + showpeers.add_argument( + "-s", + "--simplify", + help="do not print columns that are all None", + action="store_true", + ) + + # generate config + genconfig = subparsers.add_parser("genconfig") + genconfig.add_argument( + "name", + help="Name of the peer to generate configuration for, \ + configuration for all peers are generated if omitted", + nargs="?", + ) + genconfig.add_argument( + "-o", + "--output", + help="configuration file output directory", + type=pathlib.Path, + default=pathlib.Path(__file__).parent.absolute() / "output", + ) + + return parser.parse_args() + + +# if the file is not being imported +if __name__ == "__main__": + + args = parse_arguments() + + database_manager = DatabaseManager(args.database) + + if args.command == "addpeer": + database_manager.addpeer( + args.name, + args.address, + args.endpoint, + args.listenport, + args.fwmark, + args.privatekey, + args.dns, + args.mtu, + args.table, + args.preup, + args.postup, + args.predown, + args.postdown, + args.saveconfig, + ) + + elif args.command == "updatepeer": + database_manager.updatepeer( + args.name, + args.address, + args.endpoint, + args.listenport, + args.fwmark, + args.privatekey, + args.dns, + args.mtu, + args.table, + args.preup, + args.postup, + args.predown, + args.postdown, + args.saveconfig, + ) + + elif args.command == "delpeer": + database_manager.delpeer(args.name) + + elif args.command == "showpeers": + database_manager.showpeers(args.name, args.style, args.simplify) + + elif args.command == "genconfig": + database_manager.genconfig(args.name, args.output) + + # if no commands are specified + else: + print( + f"No command specified\nUse {__file__} --help to see available commands", + file=sys.stderr, + ) diff --git a/src/wireguard.py b/src/wireguard.py new file mode 100755 index 0000000..922dfd8 --- /dev/null +++ b/src/wireguard.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Name: WireGuard Python Bindings +Dev: K4YT3X +Date Created: October 11, 2019 +Last Modified: July 19, 2020 +""" + +# built-in imports +import pathlib +import subprocess + + +class WireGuard: + """ WireGuard utility controller + + This class handles the interactions with the wg binary, + including: + + - genkey + - pubkey + - genpsk + """ + + def __init__(self, wg_binary=pathlib.Path("/usr/bin/wg")): + """ + Keyword Arguments: + wg_binary {pathlib.Path} -- path of wg binary (default: {pathlib.Path("/usr/bin/wg")}) + + Since the script might have to be run as root, it is bad practice to find wg using + pathlib.Path(shutil.which("wg") since a malicious binary named wg can be under the current + directory to intercept root privilege if SUID permission is given to the script. + """ + self.wg_binary = wg_binary + + def genkey(self): + """ generate WG private key + + Generate a new wireguard private key via + wg command. + """ + return ( + subprocess.run( + [str(self.wg_binary.absolute()), "genkey"], + check=True, + stdout=subprocess.PIPE, + ) + .stdout.decode() + .strip() + ) + + def pubkey(self, privkey: str) -> str: + """ convert WG private key into public key + + Uses wg pubkey command to convert the wg private + key into a public key. + + Arguments: + privkey {str} -- wg privkey + + Returns: + str -- pubkey derived from privkey + """ + return ( + subprocess.run( + [str(self.wg_binary.absolute()), "pubkey"], + check=True, + stdout=subprocess.PIPE, + input=privkey.encode("utf-8"), + ) + .stdout.decode() + .strip() + ) + + def genpsk(self): + """ generate a random base64 PSK + """ + return ( + subprocess.run( + [str(self.wg_binary.absolute()), "genpsk"], + check=True, + stdout=subprocess.PIPE, + ) + .stdout.decode() + .strip() + ) diff --git a/wireguard_mesh_configurator.py b/wireguard_mesh_configurator.py deleted file mode 100755 index d9ef1be..0000000 --- a/wireguard_mesh_configurator.py +++ /dev/null @@ -1,543 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Name: Wireguard Mesh Configurator -Dev: K4YT3X -Date Created: October 10, 2018 -Last Modified: May 16, 2019 - -Licensed under the GNU General Public License Version 3 (GNU GPL v3), - available at: https://www.gnu.org/licenses/gpl-3.0.txt -(C) 2018-2019 K4YT3X -""" -from avalon_framework import Avalon -import json -import os -import pickle -import re -import readline -import subprocess -import sys -import traceback - -VERSION = '1.2.0' -COMMANDS = [ - 'Interactive', - 'ShowPeers', - 'JSONLoadProfile', - 'JSONSaveProfile', - 'PickleLoadProfile', - 'PickleSaveProfile', - 'NewProfile', - 'AddPeer', - 'DeletePeer', - 'GenerateConfigs', - 'Exit', - 'Quit', -] - - -class Utilities: - """ Useful utilities - - This class contains a number of utility tools. - """ - - @staticmethod - def execute(command, input_value=''): - process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE) - output = process.communicate(input=input_value)[0] - return output.decode().replace('\n', '') - - -class ShellCompleter(object): - """ A Cisco-IOS-like shell completer - - This is a Cisco-IOS-like shell completer, that is not - case-sensitive. If the command typed is not ambiguous, - then execute the only command that matches. User does - not have to enter the entire command. - """ - - def __init__(self, options): - self.options = sorted(options) - - def complete(self, text, state): - if state == 0: - if text: - self.matches = [s for s in self.options if s and s.lower().startswith(text.lower())] - else: - self.matches = self.options[:] - try: - return self.matches[state] - except IndexError: - return None - - -class Peer: - """ Peer class - - Each object of this class represents a peer in - the wireguard mesh network. - """ - - def __init__(self, address, public_address, listen_port, private_key, keep_alive, preshared_key=None, alias=None, description=None): - self.address = address - self.public_address = public_address - self.listen_port = listen_port - self.private_key = private_key - self.keep_alive = keep_alive - self.preshared_key = preshared_key - self.alias = alias - self.description = description - - -class WireGuard: - """ WireGuard utility controller - - This class handles the interactions with the wg binary, - including: - - - genkey - - pubkey - - genpsk - """ - - def __init__(self): - pass - - def genkey(self): - """ Generate WG private key - - Generate a new wireguard private key via - wg command. - """ - return Utilities.execute(['wg', 'genkey']) - - def pubkey(self, public_key): - """ Convert WG private key into public key - - Uses wg pubkey command to convert the wg private - key into a public key. - """ - return Utilities.execute(['wg', 'pubkey'], input_value=public_key.encode('utf-8')) - - def genpsk(self): - """ Generate a random base64 psk - """ - return Utilities.execute(['wg', 'genpsk']) - - -class ProfileManager(object): - """ Profile manager - - Each instance of this class represents a profile, - which is a complete topology of a mesh / c/s network. - """ - - def __init__(self): - """ Initialize peers list - """ - self.peers = [] - - def pickle_load_profile(self, profile_path): - """ Load profile from a file - - Open the pickle file, deserialize the content and - load it back into the profile manager. - """ - self.peers = [] - Avalon.debug_info(f'Loading profile from: {profile_path}') - with open(profile_path, 'rb') as profile: - pm.peers = pickle.load(profile) - profile.close() - - def pickle_save_profile(self, profile_path): - """ Save current profile to a file - - Serializes the current profile with pickle - and dumps it into a file. - """ - - # If profile already exists (file or link), ask the user if - # we should overwrite it. - if os.path.isfile(profile_path) or os.path.islink(profile_path): - if not Avalon.ask('File already exists. Overwrite?', True): - Avalon.warning('Aborted saving profile') - return 1 - - # Abort if profile_path points to a directory - if os.path.isdir(profile_path): - Avalon.warning('Destination path is a directory') - Avalon.warning('Aborted saving profile') - return 1 - - # Finally, write the profile into the destination file - Avalon.debug_info(f'Writing profile to: {profile_path}') - with open(profile_path, 'wb') as profile: - pickle.dump(pm.peers, profile) - profile.close() - - def json_load_profile(self, profile_path): - """ Load profile to JSON file - - Dumps each peer's __dict__ to JSON file. - """ - self.peers = [] - Avalon.debug_info(f'Loading profile from: {profile_path}') - with open(profile_path, 'rb') as profile: - loaded_profiles = json.load(profile) - profile.close() - - for p in loaded_profiles['peers']: - peer = Peer(p['address'], p['public_address'], p['listen_port'], p['private_key'], keep_alive=p['keep_alive'], alias=p['alias'], description=p['description']) - pm.peers.append(peer) - - def json_save_profile(self, profile_path): - """ Save current profile to a JSON file - """ - - # If profile already exists (file or link), ask the user if - # we should overwrite it. - if os.path.isfile(profile_path) or os.path.islink(profile_path): - if not Avalon.ask('File already exists. Overwrite?', True): - Avalon.warning('Aborted saving profile') - return 1 - - # Abort if profile_path points to a directory - if os.path.isdir(profile_path): - Avalon.warning('Destination path is a directory') - Avalon.warning('Aborted saving profile') - return 1 - - # Finally, write the profile into the destination file - Avalon.debug_info(f'Writing profile to: {profile_path}') - - peers_dict = {} - peers_dict['peers'] = [] - - for peer in pm.peers: - peers_dict['peers'].append(peer.__dict__) - - with open(profile_path, 'w') as profile: - json.dump(peers_dict, profile, indent=4) - profile.close() - - def new_profile(self): - """ Create new profile and flush the peers list - """ - - # Warn the user before flushing configurations - Avalon.warning('This will flush the currently loaded profile!') - if len(self.peers) != 0: - if not Avalon.ask('Continue?', False): - return - - # Reset self.peers and start enrolling new peer data - self.peers = [] - - -def print_welcome(): - """ Print program name and legal information - """ - print(f'WireGuard Mesh Configurator {VERSION}') - print('(C) 2018-2019 K4YT3X') - print('Licensed under GNU GPL v3') - - -def print_peer_config(peer): - """ Print the configuration of a specific peer - - Input takes one Peer object. - """ - if peer.alias: - Avalon.info(f'{peer.alias} information summary:') - else: - Avalon.info(f'{peer.address} information summary:') - if peer.description: - print(f'Description: {peer.description}') - if peer.address: - print(f'Address: {peer.address}') - if peer.public_address: - print(f'Public Address: {peer.public_address}') - if peer.listen_port: - print(f'Listen Port: {peer.listen_port}') - print(f'Private Key: {peer.private_key}') - if peer.keep_alive: - print(f'Keep Alive: {peer.keep_alive}') - # print(f'Preshared Key: {peer.preshared_key}') - - -def add_peer(): - """ Enroll a new peer - - Gets all the information needed to generate a - new Peer class object. - """ - - # Get peer tunnel address - while True: - address = Avalon.gets('Address (leave empty if client only) [IP/CIDR]: ') - if re.match('^(?:\d{1,3}\.){3}\d{1,3}/{1}(?:\d\d?)?$', address) is None: - Avalon.error('Invalid address entered') - Avalon.error('Please use CIDR notation (e.g. 10.0.0.0/8)') - continue - break - - # Get peer public IP address - while True: - public_address = Avalon.gets('Public address (leave empty if client only) [IP|FQDN]: ') - - # Check if public_address is valid IP or FQDN - valid_address = False - if re.match('^(?:\d{1,3}\.){3}\d{1,3}(?:/\d\d?)?$', public_address) is not None: - valid_address = True - if re.match('(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(? 0: - Avalon.warning(f'Ambiguous command \"{commands[1]}\"') - print('Use \"Help\" command to list available commands') - result = 1 - else: - Avalon.error('Invalid command') - print('Use \"Help\" command to list available commands') - result = 1 - return result - except IndexError: - Avalon.error('Invalid arguments') - print('Use \"Help\" command to list available commands') - result = 0 - - -def main(): - """ WireGuard Mesh Configurator main function - - This function controls the main flow of this program. - """ - - try: - if sys.argv[1].lower() == 'help': - print_help() - exit(0) - except IndexError: - pass - - # Begin command interpreting - try: - if sys.argv[1].lower() == 'interactive' or sys.argv[1].lower() == 'int': - print_welcome() - # Set command completer - completer = ShellCompleter(COMMANDS) - readline.set_completer(completer.complete) - readline.parse_and_bind('tab: complete') - # Launch interactive trojan shell - prompt = f'{Avalon.FM.BD}[WGC]> {Avalon.FM.RST}' - while True: - command_interpreter([''] + input(prompt).split(' ')) - else: - # Return to shell with command return value - exit(command_interpreter(sys.argv[0:])) - except IndexError: - Avalon.warning('No commands specified') - print_help() - exit(0) - except (KeyboardInterrupt, EOFError): - Avalon.warning('Exiting') - exit(0) - except Exception: - Avalon.error('Exception caught') - traceback.print_exc() - exit(1) - - -if __name__ == '__main__': - # Create global object for WireGuard handler - wg = WireGuard() - - # Create global object for profile manager - pm = ProfileManager() - - # Launch main function - main()