resolved 2.0.0 to master merge conflict

This commit is contained in:
K4YT3X 2020-11-15 21:40:33 -05:00
parent 8632583a4b
commit 7b616e9a2a
8 changed files with 763 additions and 737 deletions

42
CHANGELOG.md Normal file
View File

@ -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

342
README.md
View File

@ -1,224 +1,184 @@
# WireGuard Mesh Configurator # wg-meshconf
## On the Horizon: `wg-dynamic` ## 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. 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.
- 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
## Introduction ## 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 ## Prerequisites
### System Packages - Python 3
- prettytable (optional)
|Package|Explanation|Example| 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.
|-|-|-|
|ncurses dev package|Required by the installation of the Python `readline` library.|`libncurses5-dev` on Debian|
### 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| 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.
|-|-|
|`avalon_framework`|Command line I/O library|
|`readline`|For better interactive command line interface|
|`netaddr`|For calculating IP addresses|
## 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. ```shell
./wg-meshconf addpeer NAME --address IP_ADDRESS --address IP_ADDRESS_2 --endpoint ENDPOINT
```
$ python3 wireguard_mesh_configurator.py int
``` ```
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 ## 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} ...
``` positional arguments:
$ git clone https://github.com/K4YT3X/wireguard-mesh-configurator.git {addpeer,updatepeer,delpeer,showpeers,genconfig}
$ cd wireguard-mesh-configurator/
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.
``` ```shell
$ sudo pip3 install -r requirements.txt $./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
Run the tool. positional arguments:
name Name used to identify this node
```
$ python3 wireguard_mesh_configurator.py interactive optional arguments:
``` -h, --help show this help message and exit
--address ADDRESS address of the server
or --endpoint ENDPOINT peer's public endpoint address
--privatekey PRIVATEKEY
``` private key of server interface
$ python3 wireguard_mesh_configurator.py int --listenport LISTENPORT
``` port to listen on
--fwmark FWMARK fwmark for outgoing packets
### Creating A Profile --dns DNS server interface DNS servers
--mtu MTU server interface MTU
Run the `NewProfile` command to create a new profile. --table TABLE server routing table
--preup PREUP command to run before interface is up
``` --postup POSTUP command to run after interface is up
[WGC]> NewProfile # Create new profile --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
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]
``` ```

View File

@ -1,3 +0,0 @@
avalon_framework
readline
netaddr

295
src/database_manager.py Executable file
View File

@ -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"])
)
)

1
src/requirements.txt Normal file
View File

@ -0,0 +1 @@
prettytable

187
src/wg-meshconf Executable file
View File

@ -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,
)

87
src/wireguard.py Executable file
View File

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

View File

@ -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}(?<!-)\.)+[a-zA-Z]{2,63}$)', public_address) is not None:
valid_address = True
if not valid_address and public_address != '': # field not required
Avalon.error('Invalid public address address entered')
Avalon.error('Please enter an IP address or FQDN')
continue
break
# Get peer listening port
listen_port = Avalon.gets('Listen port (leave empty for client) [1-65535]: ')
# Get peer private key
private_key = Avalon.gets('Private key (leave empty for auto generation): ')
if private_key == '':
private_key = wg.genkey()
# Ask if this peer needs to be actively connected
# if peer is behind NAT and needs to be accessed actively
# PersistentKeepalive must be turned on (!= 0)
keep_alive = Avalon.ask('Keep alive?', False)
"""
preshared_key = False
if Avalon.ask('Use a preshared key?', True):
preshared_key = Avalon.gets('Preshared Key (leave empty for auto generation): ')
if preshared_key == '':
preshared_key = wg.genpsk()
peer = Peer(address, private_key, keep_alive, listen_port, preshared_key)
"""
# Get peer alias
alias = Avalon.gets('Alias (optional): ')
# Get peer description
description = Avalon.gets('Description (optional): ')
# Create peer and append peer into the peers list
peer = Peer(address, public_address, listen_port, private_key, keep_alive=keep_alive, alias=alias, description=description)
pm.peers.append(peer)
print_peer_config(peer)
def delete_peer(address):
""" Delete a peer
Delete a specific peer from the peer list.
"""
for peer in pm.peers:
if peer.address == address:
pm.peers.remove(peer)
def generate_configs(output_path):
""" Generate configuration file for every peer
This function reads the PEERS list, generates a
configuration file for every peer, and export into
the CONFIG_OUTPUT directory.
"""
if len(pm.peers) == 0:
Avalon.warning('No peers configured, exiting')
exit(0)
if len(pm.peers) == 1:
Avalon.warning('Only one peer configured')
Avalon.info('Generating configuration files')
# Abort is destination is a file / link
if os.path.isfile(output_path) or os.path.islink(output_path):
Avalon.warning('Destination path is a file / link')
Avalon.warning('Aborting configuration generation')
return 1
# Ask if user wants to create the output directory if it doesn't exist
if not os.path.isdir(output_path):
if Avalon.ask('Output directory doesn\'t exist. Create output directory?', True):
os.mkdir(output_path)
else:
Avalon.warning('Aborting configuration generation')
return 1
# Iterate through all peers and generate configuration for each peer
for peer in pm.peers:
Avalon.debug_info(f'Generating configuration file for {peer.address}')
with open(f'{output_path}/{peer.address.split("/")[0]}.conf', 'w') as config:
# Write Interface configuration
config.write('[Interface]\n')
if peer.alias:
config.write(f'# Alias: {peer.alias}\n')
if peer.description:
config.write(f'# Description: {peer.description}\n')
config.write(f'PrivateKey = {peer.private_key}\n')
if peer.address != '':
config.write(f'Address = {peer.address}\n')
if peer.listen_port != '':
config.write(f'ListenPort = {peer.listen_port}\n')
# Write peers' information
for p in pm.peers:
if p.address == peer.address:
# Skip if peer is self
continue
config.write('\n[Peer]\n')
print(p.private_key)
if p.alias:
config.write(f'# Alias: {p.alias}\n')
if p.description:
config.write(f'# Description: {p.description}\n')
config.write(f'PublicKey = {wg.pubkey(p.private_key)}\n')
config.write(f'AllowedIPs = {p.address}\n')
if p.public_address != '':
config.write(f'Endpoint = {p.public_address}:{p.listen_port}\n')
if peer.keep_alive:
config.write('PersistentKeepalive = 25\n')
if p.preshared_key:
config.write(f'PresharedKey = {p.preshared_key}\n')
def print_help():
""" Print help messages
"""
help_lines = [
f'\n{Avalon.FM.BD}Commands are not case-sensitive{Avalon.FM.RST}',
'Interactive // launch interactive shell',
'ShowPeers // show all peer information',
'JSONLoadProfile [profile path] // load profile from profile_path (JSON format)',
'JSONSaveProfile [profile path] // save profile to profile_path (JSON format)',
'PickleLoadProfile [profile path] // load profile from profile_path (Pickle format)',
'PickleSaveProfile [profile path] // save profile to profile_path (Pickle format)',
'NewProfile // create new profile',
'AddPeers // add a new peer into the current profile',
'DeletePeer // delete a peer from the current profile',
'GenerateConfigs [output directory] // generate configuration files',
'Exit',
'Quit',
'',
]
for line in help_lines:
print(line)
def command_interpreter(commands):
""" WGC shell command interpreter
This function interprets commands from CLI or
the interactive shell, and passes the parameters
to the corresponding functions.
"""
try:
# Try to guess what the user is saying
possibilities = [s for s in COMMANDS if s.lower().startswith(commands[1])]
if len(possibilities) == 1:
commands[1] = possibilities[0]
if commands[1].replace(' ', '') == '':
result = 0
elif commands[1].lower() == 'help':
print_help()
result = 0
elif commands[1].lower() == 'showpeers':
for peer in pm.peers:
print_peer_config(peer)
result = 0
elif commands[1].lower() == 'jsonloadprofile':
result = pm.json_load_profile(commands[2])
elif commands[1].lower() == 'jsonsaveprofile':
result = pm.json_save_profile(commands[2])
elif commands[1].lower() == 'pickleloadprofile':
result = pm.pickle_load_profile(commands[2])
elif commands[1].lower() == 'picklesaveprofile':
result = pm.pickle_save_profile(commands[2])
elif commands[1].lower() == 'newprofile':
result = pm.new_profile()
elif commands[1].lower() == 'addpeer':
result = add_peer()
elif commands[1].lower() == 'deletepeer':
result = delete_peer(commands[2])
elif commands[1].lower() == 'generateconfigs':
result = generate_configs(commands[2])
elif commands[1].lower() == 'exit' or commands[1].lower() == 'quit':
Avalon.warning('Exiting')
exit(0)
elif len(possibilities) > 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()