forked from extern/wg-meshconf
resolved 2.0.0 to master merge conflict
This commit is contained in:
parent
8632583a4b
commit
7b616e9a2a
42
CHANGELOG.md
Normal file
42
CHANGELOG.md
Normal 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
342
README.md
@ -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]
|
|
||||||
```
|
```
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
avalon_framework
|
|
||||||
readline
|
|
||||||
netaddr
|
|
295
src/database_manager.py
Executable file
295
src/database_manager.py
Executable 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
1
src/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
prettytable
|
187
src/wg-meshconf
Executable file
187
src/wg-meshconf
Executable 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
87
src/wireguard.py
Executable 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()
|
||||||
|
)
|
@ -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()
|
|
Loading…
Reference in New Issue
Block a user