Initial public commit

This commit is contained in:
Henri 2020-11-19 21:55:45 +01:00
parent 45f5f64235
commit 3cde7d371d
16 changed files with 922 additions and 0 deletions

23
CHANGELOG.md Normal file
View File

@ -0,0 +1,23 @@
# Changelog
All notable changes to this project are documented in this file.
## [Unreleased]
### Added
- n/a
### Changed
- n/a
### Fixed
- n/a
## [0.1.0] - 2020-11-19
### Added
- First public release to Github.

113
README.md Normal file
View File

@ -0,0 +1,113 @@
# wgfrontend
A simple web frontend for configuring peers within a WireGuard configuration file to thus administer road warrior clients.
There are lot of user interfaces for administering WireGuard configuration files available. However, many of them have a bunch of dependencies, require root privileges to operate, or are a hassle to set up. "wgfrontend" provides a user interface that can be easily installed by just installing a package from Python's package repository PyPi (i.e. using pip).
IMPORTANT NOTE: This tool is in an early development stage. Be warned. It is already working but looking "ugly" (the user interface does not have any nice formatting or CSS yet).
This little tool is independent of the Towalink site connectivity solution (see https://towalink.readthedocs.io).
---
## Features
- Web frontend for adding, modifying, and deleting WireGuard peers
- Config files for WireGuard peers can be downloaded
- Config files for WireGuard peers are shown as QR Code
- Assistant for initial set-up
- Web frontend does not run with root privileges
- Simple installation
---
## Installation
Install using PyPi:
```shell
pip3 install wgfrontend
```
---
## Quickstart
After installing "wgfrontend" as shown above, just execute the tool with root permissions to get started:
```shell
wgfrontend
```
An interactive set-up assistant queries for the needed configuration data and sets up the environment.
Once everything is configured, "wgfrontend" drops root privileges and runs a small web server on port 8080 to serve the web frontend.
Note that the changes done by wgfrontend in the WireGuard configuration file do not take affect automatically. The new configuration needs to be taken over to the WireGuard interface manually. Automating this is on the roadmap.
---
## Details
### The wgfrontend configuration file
The interactive set-up assistant creates a configuration file with the desired information. It is located at "/etc/wgfrontend/wgfrontend.conf".
Here is an example:
```
### Config file of the Towalink WireGuard Frontend ###
[general]
# The WireGuard config file to read and write
wg_configfile = /etc/wireguard/wg_rw.conf
# The system user to be used for the frontend
user = wgfrontend
[users]
admin = dc524e423d9762830649d4d9e18f4b47a56c92f96646104dd06c71b26b54f732e8318d5b60a6b2b01b4f269407771496e879c9bf65ca9ef4f55a243ff358fc8dfea0bd9d30d766320857093eb95022822f71b098215f26f6d2644033d956bfdd
```
### Add an additional frontend user
Create a password hash using the following command:
```shell
wgfrontend-password
```
Using this, you can add another user to the [users] section in the wgfrontend configuration file.
### A note on security
Don't expose the web frontend to the Internet without another layer of protection.
The wgfrontend web server does not run with root permissions. That's a start and better than many other WireGuard frontends. But the web server user has the permission to write to a WireGuard configuration file. This file may reference scripts that are run with root permissions when wg-quick is run. In case of a vulnerability in wgfrontend, this can be abused for privilege escalation. Thus add an additional safeguard layer of protection.
---
## Reporting bugs
In case you encounter any bugs, please report the expected behavior and the actual behavior so that the issue can be reproduced and fixed.
---
## Developers
### Clone repository
Clone this repo to your local machine using `https://github.com/towalink/wgfrontend.git`
Install the module temporarily to make it available in your Python installation:
```shell
pip3 install -e <path to root of "src" directory>
```
---
## License
[![License](http://img.shields.io/:license-agpl3-blue.svg?style=flat-square)](https://opensource.org/licenses/AGPL-3.0)
- **[AGPL3 license](https://opensource.org/licenses/AGPL-3.0)**
- Copyright 2020 © <a href="https://github.com/towalink/wgfrontend" target="_blank">Dirk Henrici</a>.
- [WireGuard](https://www.wireguard.com/) is a registered trademark of Jason A. Donenfeld.

54
setup.py Normal file
View File

@ -0,0 +1,54 @@
import os
import setuptools
with open('README.md', 'r') as f:
long_description = f.read()
setup_kwargs = {
'name': 'wgfrontend',
'version': '0.1.0',
'author': 'The Towalink Project',
'author_email': 'pypi.wgfrontend@towalink.net',
'description': 'web-based user interface for configuring WireGuard for roadwarriors',
'long_description': long_description,
'long_description_content_type': 'text/markdown',
'url': 'https://www.github.com/towalink/wgfrontend',
'packages': setuptools.find_packages('src'),
'package_dir': {'': 'src'},
'include_package_data': True,
'install_requires': ['cherrypy',
'jinja2',
'qrcode',
'wgconfig'
],
'entry_points': '''
[console_scripts]
wgfrontend=wgfrontend:main
wgfrontend-password=pwdtools:hash_password_interactively
''',
'classifiers': [
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)',
'Operating System :: POSIX :: Linux',
'Development Status :: 3 - Alpha',
#'Development Status :: 4 - Beta',
#'Development Status :: 5 - Production/Stable',
'Intended Audience :: System Administrators',
'Intended Audience :: Information Technology',
'Intended Audience :: Telecommunications Industry',
'Topic :: System :: Networking'
],
'python_requires': '>=3.6',
'keywords': 'Towalink VPN webfrontend WireGuard',
'project_urls': {
'Project homepage': 'https://www.towalink.net',
'Repository': 'https://www.github.com/towalink/wgfrontend',
'Documentation': 'https://towalink.readthedocs.io',
},
}
if __name__ == '__main__':
setuptools.setup(**setup_kwargs)

108
src/config.py Normal file
View File

@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
import configparser
import logging
import os
import textwrap
import pwdtools
logger = logging.getLogger(__name__)
config_filename = '/etc/wgfrontend/wgfrontend.conf'
class Configuration():
"""Class for reading/writing the configuration file"""
_config = None
def exists(self):
"""Checks whether the config file exists"""
return os.path.isfile(self.filename)
def read_config(self):
"""Reads the config file"""
try:
logger.debug('Attempting to read config file [{0}]'.format(self.filename))
cfg = configparser.ConfigParser()
cfg.read(self.filename)
self._config = dict(cfg['general'])
self._users = dict(cfg['users'])
except Exception as e:
logger.warning('Config file [{0}] could not be read [{1}], using defaults'.format(self.filename, str(e)))
self._config = dict()
def write_config(self, wg_configfile='', user='', users={}):
"""Writes a new config file with the given attributes"""
# Set default values
if not wg_configfile.strip():
wg_configfile = '/etc/wireguard/wg_rw.conf'
if not user.strip():
user = 'wgfrontend'
users = { username if username.strip() else 'admin': password for username, password in users.items() }
username = next(iter(users.keys()))
password = pwdtools.hash_password(users[username])
# Config file content
config_content = textwrap.dedent(f'''\
### Config file of the Towalink WireGuard Frontend ###
[general]
# The WireGuard config file to read and write
wg_configfile = {wg_configfile}
# The system user to be used for the frontend
user = {user}
[users]
{username} = {password}
''')
# Write to file system
try:
with open(self.filename, 'w') as config_file:
config_file.write(config_content)
except OSError as e:
logger.error('Could not write config file [{0}], [{1}]'.format(self.filename, str(e)))
@property
def filename(self):
"""Return the name of the config file (incl. path)"""
return config_filename
@property
def config(self):
"""Return the config dictionary"""
if self._config is None:
self.read_config()
return self._config
@property
def users(self):
"""Return the users dictionary"""
if self._users is None:
self.read_config()
return self._users
@property
def wg_configfile(self):
"""The filename incl. path of the config file for the WireGuard interface"""
return self.config.get('wg_configfile', '/etc/wireguard/wg_rw.conf')
@property
def sslcertfile(self):
"""The filename incl. path for the server certificate"""
return os.path.join(os.path.dirname(self.filename), 'server.pem')
@property
def sslkeyfile(self):
"""The filename incl. path for the server private key"""
return os.path.join(os.path.dirname(self.filename), 'key.pem')
@property
def libdir(self):
"""The directory for the generated config files"""
return '/var/lib/wgfrontend'
@property
def user(self):
"""The configured name for the wgfrontend system user"""
return self.config.get('user', 'wgfrontend')

32
src/pwdtools.py Normal file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Thx to https://www.vitoshacademy.com/hashing-passwords-in-python/
import hashlib, binascii, os
def hash_password(password):
"""Hash a password for storing."""
salt = hashlib.sha256(os.urandom(60)).hexdigest().encode('ascii')
pwdhash = hashlib.pbkdf2_hmac('sha512', password.encode('utf-8'), salt, 100000)
pwdhash = binascii.hexlify(pwdhash)
return (salt + pwdhash).decode('ascii')
def verify_password(stored_password, provided_password):
"""Verify a stored password against one provided by user"""
salt = stored_password[:64]
stored_password = stored_password[64:]
pwdhash = hashlib.pbkdf2_hmac('sha512', provided_password.encode('utf-8'), salt.encode('ascii'), 100000)
pwdhash = binascii.hexlify(pwdhash).decode('ascii')
return pwdhash == stored_password
def hash_password_interactively():
"""Ask for a password and print it in hashed form"""
pwd = input('Please enter password to hash: ')
print('Hashed password:')
print(hash_password(pwd))
if __name__ == '__main__':
hash_password_interactively()

178
src/setupenv.py Normal file
View File

@ -0,0 +1,178 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import getpass
import grp
import os
import pwd
import string
import wgconfig
import wgconfig.wgexec as wgexec
import config
def is_root():
"""Returns whether this script is run with user is 0 (root)"""
return os.getuid() == 0
def get_user():
"""Returns the effective user that executes this script"""
return getpass.getuser()
def check_user(username):
"""Returns a boolean that indicates if the given user exists on the system"""
try:
pwd.getpwnam(username)
return True
except KeyError:
return False
def create_user(username):
"""Create the given user on the system"""
if os.path.exists('/usr/sbin/useradd'):
os.system(f'useradd {username}')
else:
os.system(f'adduser {username} -D')
def ensure_user(username):
"""Ensures that the given user exists on the system"""
if not check_user(username):
create_user(username)
def check_wg():
"""Check whether the wg tool is present"""
return os.path.isfile('/usr/bin/wg')
def check_wgquick():
"""Check whether the wg-quick tool is present"""
return os.path.isfile('/usr/bin/wg-quick')
def touch_file(filename, perm=0o640):
"""Touch the given file with the provided permissions"""
if not os.path.exists(os.path.dirname(filename)):
os.makedirs(os.path.dirname(filename))
with os.fdopen(os.open(filename, os.O_WRONLY | os.O_CREAT, perm), 'w') as handle:
pass
def check_validcharacters(value, valid_chars=string.ascii_letters):
"""Checks whether the provided string only contains the listed characters"""
invalid = [ k for k in value if k not in valid_chars ]
if invalid:
return False
return True
def chown(username, path):
"""Change file/path ownership to given user"""
uid = pwd.getpwnam(username).pw_uid
os.chown(path, uid, -1)
def drop_privileges(uid_name='nobody', gid_name='nogroup'):
""""""
if not is_root():
raise ValueError('No privileges present to drop')
uid = pwd.getpwnam(uid_name).pw_uid
gid = grp.getgrnam(gid_name).gr_gid
# os.setgroups([]) # remove group privileges
os.setgid(gid)
os.setuid(uid)
def setup_environment():
"""Environment setup assistant"""
cfg = config.Configuration()
if is_root():
print('Welcome to Towalink WireGuard Frontend')
print('======================================')
print('You are executing "wgfrontend" as root user. We\'ll now make sure that everything is properly installed.')
if check_wg():
print(f'1a) Wireguard (wg) is available. Ok.')
else:
print(f'1a) Wireguard (wg) is not available. FAIL.')
if check_wg():
print(f'1b) Wireguard (wg-quick) is available. Ok.')
else:
print(f'1b) Wireguard (wg-quick) is not available. FAIL.')
if cfg.exists():
print(f'2) Config file {cfg.filename} already exists. Ok.')
else:
print(f'2) Config file {cfg.filename} does not yet exist. Let\'s create one...')
print(f' Press enter to select defaults.')
wg_configfile = input(f'2a) Please specify the WireGuard config file to be used [/etc/wireguard/wg_rw.conf]: ')
user = input(f'2b) Please specify the system user for the web frontend [wgfrontend]: ')
ok = False
while not ok:
username = input(f'2c) Please specify the username for your web frontend user [admin]: ')
if check_validcharacters(username, string.ascii_letters + '_'):
ok = True
else:
print(' Username must only contain letters and underscores. Please enter anew.')
ok = False
while not ok:
password = input(f'2d) Please specify the password for your web frontend user: ')
if len(password) >= 8:
ok = True
else:
print(' Password must have at least eight characters. Please enter anew.')
touch_file(cfg.filename, perm=0o640) # create without world read permissions
cfg.write_config(wg_configfile=wg_configfile, user=user, users={username: password})
print(' Config file written. Ok.')
print(f'3) Ensuring that system user "{cfg.user}" exists.')
ensure_user(cfg.user)
print(f'4) Ensuring ownership of config file {cfg.filename}.')
chown(cfg.user, cfg.filename)
if os.path.exists(cfg.libdir):
print(f'5) Directory {cfg.libdir} already exists. Ok.')
else:
print(f'5) Directory {cfg.libdir} does not yet exist. Let\'s create it...')
os.makedirs(cfg.libdir, mode=0o640, exist_ok=True)
print(' Directory created. Ok.')
print(f'6) Ensuring ownership of directory {cfg.libdir}.')
chown(cfg.user, cfg.libdir)
if os.path.exists(cfg.wg_configfile):
print(f'7) WireGuard config file {cfg.wg_configfile} already exists. Ok.')
else:
print(f'7) WireGuard config file {cfg.wg_configfile} does not yet exist. Let\'s create one...')
wg_listenport = input(f'7a) Please specify the listen port of the WireGuard interface [51820]: ')
if not wg_listenport.strip():
wg_listenport = 51820
ok = False
while not ok:
endpoint = input(f'7b) Please specify the endpoint hostname (and optionally port) to reach your WireGuard server: ')
if len(endpoint) > 0:
ok = True
else:
print(' You need to enter an endpoint hostname.')
wg_address = input(f'7c) Please specify the IP address of the WireGuard interface incl. prefix length [192.168.0.1/24]: ')
if not wg_address.strip():
wg_address = '192.168.0.1/24'
wg_networks = input(f'7d) Please specify the network ranges that the clients shall route to the WireGuard server [192.168.0.0/16]: ')
if not wg_networks.strip():
wg_networks = '192.168.0.0/16'
wc = wgconfig.WGConfig(cfg.wg_configfile)
wc.initialize_file('# This file has been created and is managed by wgfrontend. Only change manually if you know what you\'re doing.')
if not ':' in endpoint:
endpoint += ':51820'
wc.add_attr(None, 'ListenPort', wg_listenport, '# Endpoint = ' + endpoint, append_as_line=True)
wc.add_attr(None, 'PrivateKey', wgexec.generate_privatekey())
wc.add_attr(None, 'Address', wg_address, '# Networks = ' + wg_networks, append_as_line=True)
wc.write_file()
print(' Config file written. Ok.')
print(f'8a) Ensuring list permission of WireGuard config directory {os.path.dirname(cfg.wg_configfile)}.')
os.chmod(os.path.dirname(cfg.wg_configfile), 0o711)
print(f'8b) Ensuring ownership of WireGuard config file {cfg.wg_configfile}.')
chown(cfg.user, cfg.wg_configfile)
print(f'8c) Ensuring ownership of server certificate file {cfg.sslcertfile} in case it exists.')
if os.path.exists(cfg.sslcertfile):
chown(cfg.user, cfg.sslcertfile)
print(f'8d) Ensuring ownership of server private key file {cfg.sslkeyfile} in case it exists.')
if os.path.exists(cfg.sslkeyfile):
chown(cfg.user, cfg.sslkeyfile)
print(f'9) Dropping root privileges to user/group "{cfg.user}".')
drop_privileges(cfg.user, cfg.user)
print(f'Attempting to start web frontend...')
return cfg
if __name__ == '__main__':
setup_environment()

15
src/templates/base.html Normal file
View File

@ -0,0 +1,15 @@
<html>
<head>
<meta http-equiv='content-type' content='text/html; charset=utf-8' />
<title>Towalink WireGuard Frontend</title>
<link rel='stylesheet' media='screen' href='/static/layout.css' />
</head>
<body>
<div class='container'>
<div class='header'>
{% include 'part_header.html' %}
</div>
<div class='content'>{% block content %}{% endblock %}</div>
</div>
</body>
</html>

24
src/templates/config.html Normal file
View File

@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% block content %}
<h3>Client Config</h3>
<div class='instructions'>
<a href='https://www.wireguard.com/install/' target="_blank">WireGuard client installation instructions</a>
</div>
<div class='span-18 last'>
<form method="get" action="download">
<table>
<tr>
<td>
{{ peerdata['Description'] }}<br>
{{ peerdata['Address'] }}
</td>
<td>
<img src="/configs/{{ peerdata['Id'] }}.png" alt="QR Code">
<button type="submit" name="id" value="{{ peerdata['Id'] }}">Download Config</button>
<button type="submit" formaction=".." }}">Return</button>
</td>
</tr>
</table>
</form>
</div>
{% endblock %}

22
src/templates/edit.html Normal file
View File

@ -0,0 +1,22 @@
{% extends 'base.html' %}
{% block content %}
<h3>Configured Clients</h3>
<div class='span-18 last'>
<form method="get" action="edit">
<table>
<tr>
<td>
<input type="hidden" name="id" value="{{ peerdata['Id'] }}" />
<input type="text" name="description" value="{{ peerdata['Description'] }}" size="40" /><br>
{{ peerdata['Address'] }}
</td>
<td>
<button type="submit" name="action" value="save">Save Data</button>
<button type="submit" name="action" value="delete" formaction="/" onclick="return confirm('Do you really want to delete this client?')">Delete Client</button>
<button type="submit" formaction="/">Return to List</button>
</td>
</tr>
</table>
</form>
</div>
{% endblock %}

23
src/templates/index.html Normal file
View File

@ -0,0 +1,23 @@
{% extends 'base.html' %}
{% block content %}
<h3>Configured Clients</h3>
<div class='span-18 last'>
<form method="get" action="edit">
<button type="submit" name="action" value="new">Add Client</button>
<table>
{%- for peer, peerdata in peers.items()|sort(attribute='1.Description') %}
<tr>
<td>
{{ peerdata['Description'] }}<br>
{{ peerdata['Address'] }}
</td>
<td>
<button type="submit" name="id" value="{{ peerdata['Id'] }}">Edit Client</button>
<button type="submit" name="id" value="{{ peerdata['Id'] }}" formaction="config">Get Config</button>
</td>
</tr>
{%- endfor %}
</table>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,3 @@
<div class='heading'>
<h2><a href='/'>Towalink WireGuard Frontend</a></h2>
</div>

151
src/webapp.py Normal file
View File

@ -0,0 +1,151 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import cherrypy
import jinja2
import os
import random
import string
import pwdtools
import wgcfg
def login_screen(from_page='..', username='', error_msg='', **kwargs):
"""Based on https://docs.cherrypy.org/en/latest/_modules/cherrypy/lib/cptools.html"""
content="""
<form method="post" action="do_login">
<div align=center>
<span class=errormsg>%s</span>
<table>
<tr>
<td>
Login:
</td>
<td>
<input type="text" name="username" value="%s" size="40" />
</td>
<tr>
<td>
Password:
</td>
<td>
<input type="password" name="password" size="40" />
<input type="hidden" name="from_page" value="%s" />
</td>
</tr>
<tr>
<td colspan=2 align=right>
<input type="submit" value="Login" />
</td>
</tr>
</table>
</div>
</form>
""" % (error_msg, username, from_page)
title='Login'
return ('<html><body>' + content + '</body></html>').encode('utf-8')
# return cherrypy.tools.encode('<html><body>' + content + '</body></html>')
class WebApp():
def __init__(self, cfg):
"""Instance initialization"""
self.cfg = cfg
self.jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader('templates'))
self.wg = wgcfg.WGCfg(self.cfg.wg_configfile, self.cfg.libdir)
@cherrypy.expose
def index(self, action=None, id=None, description=None):
if (action == 'delete') and id:
peer, peerdata = self.wg.get_peer_byid(id)
self.wg.delete_peer(peer)
peers = self.wg.get_peers()
tmpl = self.jinja_env.get_template('index.html')
return tmpl.render(peers=peers)
@cherrypy.expose
def config(self, id):
peer, peerdata = self.wg.get_peer_byid(id)
tmpl = self.jinja_env.get_template('config.html')
return tmpl.render(peerdata=peerdata)
@cherrypy.expose
def edit(self, action='edit', id=None, description=None):
if id: # existing client
peer, peerdata = self.wg.get_peer_byid(id)
if description:
peerdata = self.wg.update_peer(peer, description)
else:
if not description:
description = 'My new client'
if action == 'new': # default values for new client
peerdata = { 'Description': description, 'Id': '' }
else: # save changes
peer = self.wg.create_peer(description)
peerdata = self.wg.get_peer(peer)
tmpl = self.jinja_env.get_template('edit.html')
return tmpl.render(peerdata=peerdata)
@cherrypy.expose
def download(self, id):
peer, peerdata = self.wg.get_peer_byid(id)
config, peerdata = self.wg.get_peerconfig(peer)
cherrypy.response.headers['Content-Disposition'] = f'attachment; filename=wg_{id}.conf'
cherrypy.response.headers['Content-Type'] = 'text/plain' # 'application/x-download' 'application/octet-stream'
return config.encode('utf-8')
@cherrypy.expose
def logout(self):
username = cherrypy.session['username']
cherrypy.session.clear()
return '"{0}" has been logged out'.format(username)
def run_webapp(cfg):
def check_username_and_password(username, password):
"""Check whether provided username and password are valid when authenticating"""
if (username in cfg.users) and (pwdtools.verify_password(cfg.users[username], password)):
return
return 'invalid username/password'
script_path = os.path.dirname(os.path.abspath(__file__))
conf = {
'/': {
'tools.sessions.on': True,
'tools.staticdir.root': os.path.join(script_path, 'webroot'),
# 'tools.session_auth.on': True,
'tools.session_auth.login_screen': login_screen,
'tools.session_auth.check_username_and_password': check_username_and_password,
},
'/configs': {
'tools.staticdir.on': True,
'tools.staticdir.root': None,
'tools.staticdir.dir': cfg.libdir
},
'/static': {
'tools.staticdir.on': True,
'tools.staticdir.dir': 'static'
},
'/favicon.ico':
{
'tools.staticfile.on': True,
'tools.staticfile.filename': os.path.join(script_path, 'webroot', 'static', 'favicon.ico')
}
}
if os.path.exists(cfg.sslcertfile) and os.path.exists(cfg.sslkeyfile):
cherrypy.server.ssl_module = 'builtin'
cherrypy.server.ssl_certificate = cfg.sslcertfile
cherrypy.server.ssl_private_key = cfg.sslkeyfile
cherrypy.config.update({'server.socket_host': '0.0.0.0',
'server.socket_port': 8080,
})
cherrypy.quickstart(WebApp(cfg), '/', conf)
if __name__ == '__main__':
pass

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1 @@
TEST

160
src/wgcfg.py Normal file
View File

@ -0,0 +1,160 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import ipaddress
import logging
import os
import qrcode
import textwrap
import wgconfig
import wgconfig.wgexec as wgexec
logger = logging.getLogger(__name__)
class WGCfg():
"""Class for reading/writing the WireGuard configuration file"""
def __init__(self, filename, libdir):
"""Initialize instance for the given config file"""
self.filename = filename
self.libdir = libdir
self.wc = wgconfig.WGConfig(self.filename)
self.wc.read_file()
def get_interface(self):
"""Get WireGuard interface data"""
return self.wc.interface
def transform_to_clientdata(self, peer, peerdata):
"""Transform data of a single peer from server into a dictionary of client config data"""
result = dict()
description = peerdata['_rawdata'][0]
if description[0] == '#':
description = description[2:]
else:
description = 'Peer: ' + peer
result['Description'] = description
for item in peerdata['_rawdata']:
if item.startswith('# PrivateKey = '):
result['PrivateKey'] = item[15:]
result['PublicKey'] = peer
result['PresharedKey'] = peerdata['PresharedKey']
address = peerdata['AllowedIPs'].partition(',')[0] # get first allowed ip range
address = address.partition('/')[0] + '/' + self.get_interface()['Address'].partition('/')[2] # take prefix length from interface address
result['Address'] = address
result['Id'] = address.partition('/')[0].replace('.', '-')
result['QRCode'] = os.path.join(self.libdir, result['Id'] + '.png')
return result
def get_peer(self, peer):
"""Get data of the given WireGuard peer"""
return self.transform_to_clientdata(peer, self.wc.peers[peer])
def get_peers(self):
"""Get data of all WireGuard peers"""
return { peer: self.get_peer(peer) for peer in self.wc.peers.keys() }
def get_peer_byid(self, id):
"""Get data WireGuard peer with the given id"""
peer = next(peer for peer, peerdata in self.get_peers().items() if peerdata['Id'] == id)
return peer, self.get_peer(peer)
def get_peerconfig(self, peer):
"""Get config for the given WireGuard peer"""
peerdata = self.get_peer(peer)
for item in self.get_interface()['_rawdata']:
if item.startswith('# Endpoint = '):
endpoint = item[13:]
if item.startswith('# Networks = '):
allowed_ips = item[13:]
public_key = wgexec.get_publickey(peerdata['PrivateKey'])
public_key_server = wgexec.get_publickey(self.get_interface()['PrivateKey'])
config = textwrap.dedent(f'''\
# {peerdata['Description']}
[Interface]
ListenPort = 51820
PrivateKey = {peerdata['PrivateKey']}
# PublicKey = {public_key}
Address = {peerdata['Address']}
[Peer]
Endpoint = {endpoint}
PublicKey = {public_key_server}
PresharedKey = {peerdata['PresharedKey']}
AllowedIPs = {allowed_ips}
PersistentKeepalive = 25
''')
return config, peerdata
def create_peer(self, description, ip=None):
"""Create peer with the given description"""
if ip is None:
ip = self.find_free_ip()
private_key = wgexec.generate_privatekey()
peer = wgexec.get_publickey(private_key)
self.wc.add_peer(peer, '# ' + description)
comment = '# PrivateKey = ' + private_key
self.wc.add_attr(peer, 'PresharedKey', wgexec.generate_presharedkey(), comment, append_as_line=True)
self.wc.add_attr(peer, 'AllowedIPs', ip + '/32')
self.wc.add_attr(peer, 'PersistentKeepalive', 25)
self.wc.write_file()
self.write_qrcode(peer)
return peer
def update_peer(self, peer, description):
"""Update the given peer"""
peerdata = self.wc.peers[peer]
first_line = peerdata['_index_firstline']
if self.wc.lines[first_line][0] != '#':
raise ValueError(f'Comment expected in first line of config for peer [{peerdata}]')
self.wc.lines[first_line] = '# ' + description
self.wc.invalidate_data()
self.wc.write_file()
return self.get_peer(peer)
def delete_peer(self, peer):
"""Delete the given peer"""
self.wc.del_peer(peer)
self.wc.write_file()
def find_free_ip(self):
"""Find the first free address in the given network"""
interface_address = ipaddress.ip_interface(self.get_interface()['Address'])
network = interface_address.network
interface_address = interface_address.ip
addresses = [ ipaddress.ip_interface(peerdata['Address']).ip for peer, peerdata in self.get_peers().items() ]
print(addresses) # ***
ip = None
for addr in network.hosts():
if addr == interface_address:
continue
if addr in addresses:
continue
ip = addr
break
if ip is None:
raise ValueError('No free IP address available any more')
return str(ip)
def write_qrcode(self, peer):
"""Generate a QRCode for the given peers configuration file and store in lib directory"""
config, peerdata = self.get_peerconfig(peer)
#img = qrcode.make(config)
qr = qrcode.QRCode(version=15, error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=2, border=5)
qr.add_data(config)
qr.make(fit=True)
img = qr.make_image(fill_color='black', back_color='white')
img.save(peerdata['QRCode'])
if __name__ == '__main__':
import pprint
wg = WGCfg('/etc/wireguard/wg_rw.conf', '/var/lib/wgfrontend')
peer = wg.create_peer('This is a first test peer')
peer2 = wg.create_peer('This is a second test peer')
wg.update_peer(peer2, 'CHANGED')
print(wg.get_peerconfig(peer2)[0])
wg.delete_peer(peer)
wg.delete_peer(peer2)

15
src/wgfrontend.py Normal file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import setupenv
import webapp
def main():
cfg = setupenv.setup_environment()
webapp.run_webapp(cfg)
if __name__ == '__main__':
main()