Browse Source

Merge pull request #32 from perara/dev

Dev
pull/33/head
Per-Arne Andersen 4 years ago
committed by GitHub
parent
commit
4db48576d2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      .travis.yml
  2. 8
      Dockerfile
  3. 132
      README.md
  4. 16
      back/test.py
  5. 62
      docker-compose.yaml
  6. 5
      docker/push.sh
  7. 1
      docker/start.py
  8. 1779
      docs/api.md
  9. 1
      docs/guides/docker_configuration.md
  10. 24
      wg_dashboard_backend/const.py
  11. 27
      wg_dashboard_backend/db/api_key.py
  12. 4
      wg_dashboard_backend/db/user.py
  13. 126
      wg_dashboard_backend/db/wireguard.py
  14. 24
      wg_dashboard_backend/main.py
  15. 23
      wg_dashboard_backend/middleware.py
  16. 21
      wg_dashboard_backend/migrations/versions/004_create_server_subnet.py
  17. 32
      wg_dashboard_backend/migrations/versions/005_create_v6_address.py
  18. 21
      wg_dashboard_backend/migrations/versions/006_create_v6_subnet.py
  19. 34
      wg_dashboard_backend/migrations/versions/007_create_read_only_client.py
  20. 19
      wg_dashboard_backend/models.py
  21. 2
      wg_dashboard_backend/requirements.txt
  22. 110
      wg_dashboard_backend/routers/v1/peer.py
  23. 59
      wg_dashboard_backend/routers/v1/server.py
  24. 59
      wg_dashboard_backend/routers/v1/user.py
  25. 29
      wg_dashboard_backend/routers/v1/wg.py
  26. 27
      wg_dashboard_backend/schemas.py
  27. 56
      wg_dashboard_backend/script/obfuscate/__init__.py
  28. 30
      wg_dashboard_backend/script/obfuscate/obfs4.py
  29. 119
      wg_dashboard_backend/script/obfuscate/tor.py
  30. 209
      wg_dashboard_backend/script/wireguard.py
  31. 2
      wg_dashboard_backend/templates/peer.j2
  32. 8
      wg_dashboard_backend/templates/server.j2
  33. 4122
      wg_dashboard_frontend/package-lock.json
  34. 2
      wg_dashboard_frontend/package.json
  35. 1
      wg_dashboard_frontend/src/app/interfaces/peer.ts
  36. 3
      wg_dashboard_frontend/src/app/interfaces/server.ts
  37. 28
      wg_dashboard_frontend/src/app/layout/layout.module.ts
  38. 7
      wg_dashboard_frontend/src/app/layout/layout/layout.component.html
  39. 14
      wg_dashboard_frontend/src/app/layout/layout/layout.component.ts
  40. 2
      wg_dashboard_frontend/src/app/page/components/modal-confirm/modal-confirm.component.ts
  41. 66
      wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.html
  42. 60
      wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts
  43. 4
      wg_dashboard_frontend/src/app/page/dashboard/dashboard.module.ts
  44. 15
      wg_dashboard_frontend/src/app/page/dashboard/peer/peer.component.ts
  45. 111
      wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html
  46. 3
      wg_dashboard_frontend/src/app/page/dashboard/server/server.component.scss
  47. 19
      wg_dashboard_frontend/src/app/page/dashboard/server/server.component.ts
  48. 2
      wg_dashboard_frontend/src/app/page/page-routing.module.ts
  49. 5
      wg_dashboard_frontend/src/app/page/page.module.ts
  50. 43
      wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.html
  51. 0
      wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.scss
  52. 25
      wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.spec.ts
  53. 45
      wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.ts
  54. 7
      wg_dashboard_frontend/src/app/page/user/edit/edit.component.html
  55. 3
      wg_dashboard_frontend/src/app/page/user/edit/edit.component.scss
  56. 7
      wg_dashboard_frontend/src/app/page/user/edit/edit.component.ts
  57. 3
      wg_dashboard_frontend/src/app/services/config.service.ts
  58. 50
      wg_dashboard_frontend/src/app/services/server.service.ts
  59. 2
      wg_dashboard_frontend/src/app/validators/ip-address.validator.ts
  60. 3
      wg_dashboard_frontend/widdershins.json

15
.travis.yml

@ -0,0 +1,15 @@
dist: xenial
sudo: required
env:
global:
- DOCKER_REPO=perara/wg-manager
before_install:
- curl -fsSL https://get.docker.com | sh
- echo '{"experimental":"enabled"}' | sudo tee /etc/docker/daemon.json
- mkdir -p $HOME/.docker
- echo '{"experimental":"enabled"}' | sudo tee $HOME/.docker/config.json
- sudo service docker start
install:
- docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
- docker buildx create --name xbuilder --use
script: bash ci.sh

8
Dockerfile

@ -11,15 +11,15 @@ MAINTAINER per@sysx.no
ENV IS_DOCKER True
WORKDIR /app
# Install dependencies
RUN apk add --no-cache --update wireguard-tools py3-gunicorn python3 py3-pip
RUN apk add --no-cache --update wireguard-tools py3-gunicorn python3 py3-pip ip6tables
COPY wg_dashboard_backend /app
ENV LIBRARY_PATH=/lib:/usr/lib
# Install dependencies
RUN apk add --no-cache build-base python3-dev libffi-dev && \
RUN apk add --no-cache build-base python3-dev libffi-dev jpeg-dev zlib-dev && \
pip3 install uvicorn && \
pip3 install -r requirements.txt && \
apk del build-base python3-dev libffi-dev
apk del build-base python3-dev libffi-dev jpeg-dev zlib-dev
# Copy startup scripts
COPY docker/ ./startup

132
README.md

@ -5,35 +5,54 @@ The wg-manager provides an easy-to-use graphical web interface to import, setup,
The features of wg-manager includes:
**Server**
* IPv4 **and** IPv6 support
* Create/Delete/Modify
* Start/Stop/Restart server
* Import existing
* Import existing configurations
* Export server config, along with client config as zip.
**Peer**
* Create/Delete/Modify
* Bandwidth usage statistics
* Export by QRCode, Text
* Authentication via API-Keys for automation (Created in GUI)
* Automatic setup using docker
**General**
* Modify Admin User
* Create and manage API-Keys
# Dependencies
* Linux >= 5.6 *(Alternatively: wireguard-dkms)*
# Common Installation Steps
1. Enable ip forwarding with `sysctl -w net.ipv4.ip_forward=1`
* To make the forwarding persistent add `net.ipv4.ip_forward = 1` to `/etc/sysctl.d/99-sysctl.conf`
2. It is recommended to have a firewall protecting your servers
1. Enable ip forwarding:
```
sysctl -w net.ipv4.ip_forward=1 # IPV4 Support
sysctl -w net.ipv6.conf.all.forwarding=1 # IPV6 Support
```
2. For persistent configuration:
```
cat > /etc/sysctl.d/99-sysctl.conf << EOF
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding=1
EOF
```
3. It is recommended to have a firewall protecting your servers
## Notes
* A few people has experienced issues with running the dockerized method using bridged networking. To fix this, you can use `network_mode: host`. Note that you can no longer reverse-proxy the web interface from reverse proxies such as [jwilder/nginx-proxy](https://hub.docker.com/r/jwilder/nginx-proxy/).
## Method #1: Docker-compose
```yaml
version: "2.1"
services:
wireguard:
container_name: wg-manager
image: perara/wg-manager
restart: always
sysctls:
net.ipv6.conf.all.disable_ipv6: 0 # Required for IPV6
cap_add:
- NET_ADMIN
#network_mode: host # Alternatively
@ -69,6 +88,88 @@ build:
# Usage
When docker container/server has started, go to http://localhost:8888
# API Docs
The API docs is found [here](./docs/api.md).
# API-Keys
1. Login to wg-manager
2. Go to edit profile
3. Create API-Key and take note of the key. Use the X-API-Key header to authenticate.
4. Example: `curl -i -H "X-API-Key: <key-goes-here>" http://<host>:<port>/api/v1/users/api-key/list`
5. Example 2: `curl -X POST "http://<host>:<port>/api/v1/peer/configuration/add" -H "accept: application/json" -H "Content-Type: application/json" -H "X-API-Key: <api-key-here>" -d "{\"server_interface\":\"wg0\"}"`
# Client Mode
wg-manager can also run in client-mode, with near-automatic setup and connection. To automatically setup the client,
you will need:
1. wg-manager server url
2. name of the interface the client should run on
3. wg-manager server api key
You can setup multiple clients using the numbered environment variables. The following configuration runs a server and client automatically:
```dockerfile
version: "2.1"
services:
server:
container_name: wg-manager
build: .
restart: always
sysctls:
net.ipv6.conf.all.disable_ipv6: 0
cap_add:
- NET_ADMIN
#network_mode: host # Alternatively
ports:
- 11820:11820/udp
- 51800-51900:51800-51900/udp
- 8888:8888
environment:
HOST: 0.0.0.0
PORT: 8888
ADMIN_USERNAME: admin
ADMIN_PASSWORD: admin
WEB_CONCURRENCY: 2
SERVER_INIT_INTERFACE_START: 1
#endpoint dynamic variables: ||external|| , ||internal||
SERVER_INIT_INTERFACE: '{"address":"10.0.200.1","v6_address":"fd42:42:42::1","subnet":24,"v6_subnet":64,"interface":"wg0","listen_port":"51820","endpoint":"server","dns":"10.0.200.1,8.8.8.8","private_key":"","public_key":"","post_up":"","post_down":"","configuration":"","is_running":false,"peers":[]}'
SERVER_STARTUP_API_KEY: thisisasecretkeythatnobodyknows
networks:
- wg-manager-net
client:
container_name: wg-manager-server-with-client
build: .
restart: always
sysctls:
net.ipv6.conf.all.disable_ipv6: 0
cap_add:
- NET_ADMIN
ports:
- 8889:8889
privileged: true
environment:
HOST: 0.0.0.0 # Optional (For Accessing WEB-Gui)
PORT: 8889 # Optional (Web-GUI Listen Port)
WEB_CONCURRENCY: 1 # Optional
ADMIN_USERNAME: admin
ADMIN_PASSWORD: admin
INIT_SLEEP: 5 # If you run into concurrency issues
SERVER: 0 # If you want to host a server as well
CLIENT: 1 # If you want to connect to servers
CLIENT_START_AUTOMATICALLY: 1 # If you want the client to start automatically
CLIENT_1_NAME: "client-1" # Name of first client
CLIENT_1_SERVER_HOST: "http://server:8888" # Endpoint of first server
CLIENT_1_SERVER_INTERFACE: "wg0" # Interface of first server (to get config)
CLIENT_1_API_KEY: "thisisasecretkeythatnobodyknows" # API-Key of first server (to get config)
networks:
- wg-manager-net
networks:
wg-manager-net:
driver: bridge
```
# Environment variables
| Environment | Description | Recommended |
@ -81,6 +182,25 @@ When docker container/server has started, go to http://localhost:8888
| LOG_LEVEL | Logging level of gunicorn/python | info |
| ADMIN_USERNAME | Default admin username on database creation | admin |
| ADMIN_PASSWORD | Default admin password on database creation | admin |
| POST_UP | The POST_UP Command (version 4) | default |
| POST_DOWN | The POST_DOWN Command (version 4) | default |
| POST_UP_V6 | The POST_UP Command (version 6) | default |
| POST_DOWN_V6 | The POST_DOWN Command (version 6) | default |
| INIT_SLEEP | Sleep before bootstrap. Useful for delaying client boot | integer |
| SERVER_STARTUP_API_KEY | Create a initial, and known API key on server init | secret |
| SERVER_INIT_INTERFACE | Create a initial wireguard interface on server init. See docs | json |
| SERVER_INIT_INTERFACE_START | If the interface should start immediately | 1 or 0 |
| SERVER | If the container should enable server-mode | 1 or 0 |
| CLIENT | If the container should enable client-mode | 1 or 0 |
| CLIENT_START_AUTOMATICALLY | If client is enabled. should it start immediately? | 1 or 0 |
| CLIENT_X_NAME | Name of the automatically generated client. X = incremental number from 1 | string |
| CLIENT_X_SERVER_HOST | The url to wg-manager server e.g. "http://server:8888" See docs | url |
| CLIENT_X_SERVER_INTERFACE | The wg-interface to create client on e.g"wg0". See docs | string |
| CLIENT_X_API_KEY | A valid API-Key that is active on the server. Works well with SERVER_STARTUP_API_KEY | string |
# Q&A
I'm trying to start the device but recieve the message: `Perhaps ip6tables or your kernel needs to be upgraded.`.
Try: `modprobe ip6table_nat` on the host.
# Showcase
![Illustration](docs/images/0.png)
@ -106,7 +226,3 @@ When docker container/server has started, go to http://localhost:8888
- Implement multi-server support (setting up site-2-site servers from the GUI)
- Extending multi-server support to enable custom access lists (A peer can be assigned to multiple servers, as part of the ACL)
### Other
* Eventual bugfixes
* Improve Auth
* Improve everything...

16
back/test.py

@ -0,0 +1,16 @@
import requests
if __name__ == "__main__":
sess = requests.Session()
resp = sess.post("http://localhost:8888/api/v1/login", data={
"username": "admin",
"password": "admin"
})
print(resp.json())
sess.headers.update({
"Authorization": f"Bearer {resp.json()['access_token']}"
})
for _ in range(20):
print(sess.get("http://localhost:8888/api/v1/wg/generate_psk").json())

62
docker-compose.yaml

@ -0,0 +1,62 @@
version: "2.1"
services:
server:
container_name: wg-manager
build: .
restart: always
sysctls:
net.ipv6.conf.all.disable_ipv6: 0
cap_add:
- NET_ADMIN
#network_mode: host # Alternatively
ports:
- 11820:11820/udp
- 51800-51900:51800-51900/udp
- 8888:8888
environment:
HOST: 0.0.0.0
PORT: 8888
ADMIN_USERNAME: admin
ADMIN_PASSWORD: admin
WEB_CONCURRENCY: 2
SERVER_INIT_INTERFACE_START: 1
#endpoint dynamic variables: ||external|| , ||internal||
SERVER_INIT_INTERFACE: '{"address":"10.0.200.1","v6_address":"fd42:42:42::1","subnet":24,"v6_subnet":64,"interface":"wg0","listen_port":"51820","endpoint":"server","dns":"10.0.200.1,8.8.8.8","private_key":"","public_key":"","post_up":"","post_down":"","configuration":"","is_running":false,"peers":[]}'
SERVER_STARTUP_API_KEY: thisisasecretkeythatnobodyknows
networks:
- wg-manager-net
client:
container_name: wg-manager-server-with-client
build: .
restart: always
sysctls:
net.ipv6.conf.all.disable_ipv6: 0
cap_add:
- NET_ADMIN
ports:
- 8889:8889
privileged: true
environment:
HOST: 0.0.0.0 # Optional (For Accessing WEB-Gui)
PORT: 8889 # Optional (Web-GUI Listen Port)
WEB_CONCURRENCY: 1 # Optional
ADMIN_USERNAME: admin
ADMIN_PASSWORD: admin
INIT_SLEEP: 5 # If you run into concurrency issues
SERVER: 0 # If you want to host a server as well
CLIENT: 1 # If you want to connect to servers
CLIENT_START_AUTOMATICALLY: 1 # If you want the client to start automatically
CLIENT_1_NAME: "client-1" # Name of first client
CLIENT_1_ROUTES: "10.0.200.0/24"
CLIENT_1_SERVER_HOST: "http://server:8888" # Endpoint of first server
CLIENT_1_SERVER_INTERFACE: "wg0" # Interface of first server (to get config)
CLIENT_1_API_KEY: "thisisasecretkeythatnobodyknows" # API-Key of first server (to get config)
networks:
- wg-manager-net
networks:
wg-manager-net:
driver: bridge

5
docker/push.sh

@ -1,5 +1,6 @@
#!/usr/bin/env bash
cd ..
docker login
docker build -t perara/wg-manager .
docker push perara/wg-manager
docker build -t perara/wg-manager:dev .
docker push perara/wg-manager:dev

1
docker/start.py

@ -18,6 +18,7 @@ if __name__ == "__main__":
APP_MODULE = os.getenv("APP_MODULE", f"{MODULE_NAME}:{VARIABLE_NAME}")
GUNICORN_CONF = os.getenv("GUNICORN_CONF", get_location(DEFAULT_GUNICORN_CONF))
OPTIONS = [
"--preload",
"-k",
"uvicorn.workers.UvicornWorker",
"-c",

1779
docs/api.md

File diff suppressed because it is too large

1
docs/guides/docker_configuration.md

@ -1,6 +1,7 @@
# Docker Configuration
```bash
docker run -d \
--sysctl net.ipv6.conf.all.disable_ipv6=0 \
--cap-add NET_ADMIN \
--name wg-manager \
#--net host \

24
wg_dashboard_backend/const.py

@ -5,18 +5,36 @@ IS_DOCKER = os.getenv("IS_DOCKER", "False") == "True"
DATABASE_FILE = "/config/database.db" if IS_DOCKER else "./database.db"
DATABASE_URL = f"sqlite:///{DATABASE_FILE}"
os.makedirs("build", exist_ok=True)
OBFUSCATE_ENABLED = os.getenv("OBFUSCATION", "True") == "True" # TODO should be false by default
OBFUSCATE_MODE = os.getenv("OBFUSCATION_MODE", "obfs4")
OBFUSCATE_SOCKS_TOR_PORT = int(os.getenv("OBFUSCATE_SOCKS_TOR_PORT", "5555"))
OBFUSCATE_TOR_LISTEN_ADDR = int(os.getenv("OBFUSCATE_TOR_LISTEN_ADDR", "5556"))
OBFUSCATE_SUPPORTED = ["obfs4"]
assert OBFUSCATE_MODE in OBFUSCATE_SUPPORTED, "Invalid OBFUSCATE_MODE=%s, Valid options are: %s" % (OBFUSCATE_MODE,
OBFUSCATE_SUPPORTED)
DEFAULT_POST_UP = os.getenv("POST_UP", "iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE")
DEFAULT_POST_DOWN = os.getenv("POST_DOWN", "iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE")
os.makedirs("build", exist_ok=True)
DEFAULT_POST_UP = os.getenv("POST_UP", "iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;")
DEFAULT_POST_DOWN = os.getenv("POST_DOWN", "iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE;")
DEFAULT_POST_UP_v6 = os.getenv("POST_UP_V6", "ip6tables -A FORWARD -i %i -j ACCEPT; ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;")
DEFAULT_POST_DOWN_v6 = os.getenv("POST_DOWN_V6", "ip6tables -D FORWARD -i %i -j ACCEPT; ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE;")
SECRET_KEY = ''.join(random.choices(string.ascii_uppercase + string.digits, k=64))
ALGORITHM = "HS256"
API_KEY_LENGTH = 32
ACCESS_TOKEN_EXPIRE_MINUTES = 30
CMD_WG_COMMAND = ["wg"]
CMD_WG_QUICK = ["wg-quick"]
INIT_SLEEP = int(os.getenv("INIT_SLEEP", "0"))
SERVER_STARTUP_API_KEY = os.getenv("SERVER_STARTUP_API_KEY", None)
SERVER_INIT_INTERFACE = os.getenv("SERVER_INIT_INTERFACE", None)
SERVER_INIT_INTERFACE_START = os.getenv("SERVER_INIT_INTERFACE_START", "1") == "1"
SERVER = os.getenv("SERVER", "1") == "1"
CLIENT = os.getenv("CLIENT", "0") == "1"
CLIENT_START_AUTOMATICALLY = os.getenv("CLIENT_START_AUTOMATICALLY", "1") == "1"
if not IS_DOCKER:
CMD_WG_COMMAND = ["sudo"] + CMD_WG_COMMAND
CMD_WG_QUICK = ["sudo"] + CMD_WG_QUICK

27
wg_dashboard_backend/db/api_key.py

@ -0,0 +1,27 @@
from sqlalchemy.orm import Session
import models
def add_initial_api_key_for_admin(sess: Session, api_key, ADMIN_USERNAME):
db_user = sess.query(models.User)\
.filter_by(username=ADMIN_USERNAME)\
.one()
exists_api_key = sess.query(models.UserAPIKey)\
.filter_by(
user_id=db_user.id,
key=api_key
)\
.count()
if exists_api_key == 0:
db_api_key = models.UserAPIKey()
db_api_key.key = api_key
db_api_key.user_id = db_user.id
sess.add(db_api_key)
sess.commit()
return True

4
wg_dashboard_backend/db/user.py

@ -7,13 +7,11 @@ from passlib.context import CryptContext
import schemas
def update_user(sess: Session, form_data: schemas.UserInDB):
user = get_user_by_name(sess, form_data.username)
user.password = form_data.password
user.full_name = form_data.full_name
user.email = form_data.email # TOD this section should be updated
user.email = form_data.email # TODO this section should be updated
sess.add(user)
sess.commit()

126
wg_dashboard_backend/db/wireguard.py

@ -1,7 +1,11 @@
import ipaddress
import json
import os
import shutil
import typing
from starlette.exceptions import HTTPException
import const
import script.wireguard
from sqlalchemy import exists
@ -43,15 +47,38 @@ def peer_dns_set(sess: Session, peer: schemas.WGPeer) -> schemas.WGPeer:
def peer_remove(sess: Session, peer: schemas.WGPeer) -> bool:
db_peers = peer.filter_query(sess).all()
db_peers = sess.query(models.WGPeer).filter_by(id=peer.id).all()
for db_peer in db_peers:
sess.delete(db_peer)
sess.commit()
server_update_configuration(sess, peer.server_id)
return True
def peer_edit(sess: Session, peer: schemas.WGPeer):
# Retrieve server from db
server: models.WGServer = get_server_by_id(sess, peer.server_id)
# Generate peer configuration
peer.configuration = script.wireguard.generate_config(dict(
peer=peer,
server=server
))
# Update database record for Peer
sess.query(models.WGPeer) \
.filter_by(id=peer.id) \
.update(peer.dict(exclude={"id"}))
sess.commit()
server_update_configuration(sess, server.id)
return peer
def peer_key_pair_generate(sess: Session, peer: schemas.WGPeer) -> schemas.WGPeer:
db_peer: models.WGPeer = peer_query_get_by_address(sess, peer.address, peer.server).one()
private_key, public_key = script.wireguard.generate_keys()
@ -108,10 +135,97 @@ def server_update_field(sess: Session, interface: str, server: schemas.WGServer,
def server_get_all(sess: Session) -> typing.List[schemas.WGServer]:
db_interfaces = sess.query(models.WGServer).all()
db_interfaces = sess.query(models.WGServer) \
.all()
return [schemas.WGServer.from_orm(db_interface) for db_interface in db_interfaces]
def server_add_on_init(sess: Session):
"""
Routine for adding server from env variable.
:param server:
:param sess:
:return:
"""
try:
init_data = json.loads(const.SERVER_INIT_INTERFACE)
if init_data["endpoint"] == "||external||":
import requests
init_data["endpoint"] = requests.get("https://api.ipify.org").text
elif init_data["endpoint"] == "||internal||":
import socket
init_data["endpoint"] = socket.gethostbyname(socket.gethostname())
if sess.query(models.WGServer) \
.filter_by(endpoint=init_data["endpoint"], listen_port=init_data["listen_port"]) \
.count() == 0:
# Only add if it does not already exists.
server_add(schemas.WGServerAdd(**init_data), sess, start=const.SERVER_INIT_INTERFACE_START)
except Exception as e:
_LOGGER.warning("Failed to setup initial server interface with exception:")
_LOGGER.exception(e)
def server_add(server: schemas.WGServerAdd, sess: Session, start=False):
# Configure POST UP with defaults if not manually set.
if server.post_up == "":
server.post_up = const.DEFAULT_POST_UP
if server.v6_address is not None:
server.post_up += const.DEFAULT_POST_UP_v6
# Configure POST DOWN with defaults if not manually set.
if server.post_down == "":
server.post_down = const.DEFAULT_POST_DOWN
if server.v6_address is not None:
server.post_down += const.DEFAULT_POST_DOWN_v6
peers = server.peers if server.peers else []
# Public/Private key
try:
if sess.query(models.WGServer) \
.filter(
(models.WGServer.interface == server.interface) |
(models.WGServer.address == server.address) |
(models.WGServer.v6_address == server.v6_address)).count() != 0:
raise HTTPException(status_code=400,
detail="The server interface or ip %s already exists in the database" % server.interface)
if not server.private_key:
keys = script.wireguard.generate_keys()
server.private_key = keys["private_key"]
server.public_key = keys["public_key"]
server.configuration = script.wireguard.generate_config(server)
server.peers = []
server.sync(sess)
if len(peers) > 0:
server.from_db(sess)
for schemaPeer in peers:
schemaPeer.server_id = server.id
schemaPeer.configuration = script.wireguard.generate_config(dict(
peer=schemaPeer,
server=server
))
dbPeer = models.WGPeer(**schemaPeer.dict())
sess.add(dbPeer)
sess.commit()
server.from_db(sess)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
if start and not script.wireguard.is_running(server):
script.wireguard.start_interface(server)
return server
def server_remove(sess: Session, server: schemas.WGServer) -> bool:
db_server = server_query_get_by_interface(sess, server.interface).one()
if db_server is None:
@ -158,3 +272,11 @@ def server_post_down_set(sess: Session, server: schemas.WGServer) -> bool:
def server_endpoint_set(sess: Session, server: schemas.WGServer) -> bool:
return server_update_field(sess, server.interface, server, {"endpoint"})
def server_update_configuration(sess: Session, server_id: int) -> bool:
# Generate server configuration
server: models.WGServer = sess.query(models.WGServer).filter_by(id=server_id).one()
server.configuration = script.wireguard.generate_config(server)
sess.add(server)
sess.commit()

24
wg_dashboard_backend/main.py

@ -1,10 +1,14 @@
import logging
import os
import time
import typing
from sqlalchemy_utils import database_exists
from starlette.middleware.base import BaseHTTPMiddleware
import const
import db.wireguard
import db.api_key
import middleware
from database import engine, SessionLocal
from routers.v1 import user, server, peer, wg
@ -27,6 +31,8 @@ from migrate import DatabaseAlreadyControlledError
from migrate.versioning.shell import main
import models
# Sleep the wait timer.
time.sleep(const.INIT_SLEEP)
app = FastAPI()
app.add_middleware(BaseHTTPMiddleware, dispatch=middleware.db_session_middleware)
@ -34,28 +40,29 @@ app.add_middleware(BaseHTTPMiddleware, dispatch=middleware.db_session_middleware
_db: Session = SessionLocal()
# Ensure database existence
if not database_exists(engine.url):
ADMIN_USERNAME = os.getenv("ADMIN_USERNAME")
if not ADMIN_USERNAME:
raise RuntimeError("Database does not exist and no ADMIN_USER is set")
raise RuntimeError("Database does not exist and the environment variable ADMIN_USERNAME is set")
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD")
if not ADMIN_PASSWORD:
raise RuntimeError("Database does not exist and no ADMIN_PASSWORD is set")
raise RuntimeError("Database does not exist and the environment variable ADMIN_PASSWORD is set")
# Create database from metadata
models.Base.metadata.create_all(engine)
# Create default user
_db.add(models.User(
_db.merge(models.User(
username=ADMIN_USERNAME,
password=middleware.get_password_hash(ADMIN_PASSWORD),
full_name="Admin",
role="admin",
email=""
))
_db.commit()
_db.commit()
# Do migrations
@ -75,6 +82,15 @@ for s in servers:
except Exception as e:
print(e)
if const.CLIENT:
script.wireguard.load_environment_clients(_db)
if const.SERVER_INIT_INTERFACE is not None:
db.wireguard.server_add_on_init(_db)
if const.SERVER_STARTUP_API_KEY is not None:
ADMIN_USERNAME = os.getenv("ADMIN_USERNAME")
db.api_key.add_initial_api_key_for_admin(_db, const.SERVER_STARTUP_API_KEY, ADMIN_USERNAME)
_db.close()

23
wg_dashboard_backend/middleware.py

@ -11,6 +11,7 @@ from starlette.requests import Request
from starlette.responses import Response
import const
import models
import schemas
from database import SessionLocal
import db.user
@ -56,7 +57,13 @@ def create_access_token(*, data: dict, expires_delta: timedelta = None):
return encoded_jwt
def auth(token: str = Depends(oauth2_scheme), sess: Session = Depends(get_db)):
def retrieve_api_key(request: Request):
return request.headers.get("X-API-Key", None)
def auth(token: str = Depends(oauth2_scheme), api_key: str = Depends(retrieve_api_key), sess: Session = Depends(get_db)):
username = None
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@ -64,14 +71,22 @@ def auth(token: str = Depends(oauth2_scheme), sess: Session = Depends(get_db)):
headers={"WWW-Authenticate": "Bearer"},
)
# Attempt to authenticate using JWT
try:
payload = jwt.decode(token, const.SECRET_KEY, algorithms=[const.ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except PyJWTError:
pass
try:
db_user_api_key = sess.query(models.UserAPIKey).filter_by(key=api_key).one()
username = db_user_api_key.user.username
except Exception:
pass
if username is None:
raise credentials_exception
user = schemas.User.from_orm(
schemas.UserInDB(username=username, password="").from_db(sess)
)

21
wg_dashboard_backend/migrations/versions/004_create_server_subnet.py

@ -0,0 +1,21 @@
from sqlalchemy import *
from migrate import *
def upgrade(migrate_engine):
try:
meta = MetaData(bind=migrate_engine)
server = Table('server', meta, autoload=True)
subnet = Column('subnet', Integer, nullable=False)
subnet.create(server)
except:
pass
def downgrade(migrate_engine):
try:
meta = MetaData(bind=migrate_engine)
server = Table('server', meta, autoload=True)
server.c.subnet.drop()
except:
pass

32
wg_dashboard_backend/migrations/versions/005_create_v6_address.py

@ -0,0 +1,32 @@
from sqlalchemy import *
from migrate import *
def upgrade(migrate_engine):
try:
meta = MetaData(bind=migrate_engine)
server = Table('server', meta, autoload=True)
v6_address_server = Column('v6_address', VARCHAR, unique=True, nullable=True)
v6_address_server.create(server)
meta = MetaData(bind=migrate_engine)
peer = Table('peer', meta, autoload=True)
v6_address_peer = Column('v6_address', VARCHAR, nullable=True)
v6_address_peer.create(peer)
except:
pass
def downgrade(migrate_engine):
try:
meta = MetaData(bind=migrate_engine)
server = Table('server', meta, autoload=True)
server.c.v6_address.drop()
meta = MetaData(bind=migrate_engine)
peer = Table('peer', meta, autoload=True)
peer.c.v6_address.drop()
except:
pass

21
wg_dashboard_backend/migrations/versions/006_create_v6_subnet.py

@ -0,0 +1,21 @@
from sqlalchemy import *
from migrate import *
def upgrade(migrate_engine):
try:
meta = MetaData(bind=migrate_engine)
peer = Table('server', meta, autoload=True)
v6_subnet = Column('v6_subnet', INTEGER)
v6_subnet.create(peer)
except:
pass
def downgrade(migrate_engine):
try:
meta = MetaData(bind=migrate_engine)
peer = Table('server', meta, autoload=True)
peer.c.v6_subnet.drop()
except:
pass

34
wg_dashboard_backend/migrations/versions/007_create_read_only_client.py

@ -0,0 +1,34 @@
from sqlalchemy import *
from migrate import *
def upgrade(migrate_engine):
try:
meta = MetaData(bind=migrate_engine)
server = Table('server', meta, autoload=True)
read_only = Column('read_only', INTEGER, default=0)
read_only.create(server)
except:
pass
try:
meta = MetaData(bind=migrate_engine)
peer = Table('peer', meta, autoload=True)
read_only = Column('read_only', INTEGER, default=0)
read_only.create(peer)
except:
pass
def downgrade(migrate_engine):
try:
meta = MetaData(bind=migrate_engine)
server = Table('server', meta, autoload=True)
server.c.read_only.drop()
except:
pass
try:
meta = MetaData(bind=migrate_engine)
server = Table('peer', meta, autoload=True)
server.c.read_only.drop()
except:
pass

19
wg_dashboard_backend/models.py

@ -1,6 +1,8 @@
import datetime
import sqlalchemy
from sqlalchemy import Integer, Column
from sqlalchemy import Integer, Column, DateTime
from sqlalchemy.orm import relationship, backref
from database import Base
@ -16,17 +18,30 @@ class User(Base):
role = Column(sqlalchemy.String)
class UserAPIKey(Base):
__tablename__ = "api_keys"
id = Column(Integer, primary_key=True, autoincrement=True)
key = Column(sqlalchemy.String, unique=True)
user_id = Column(Integer, sqlalchemy.ForeignKey('users.id', ondelete="CASCADE", onupdate="CASCADE"))
user = relationship("User", foreign_keys=[user_id])
created_date = Column(DateTime, default=datetime.datetime.utcnow)
class WGServer(Base):
__tablename__ = "server"
id = Column(Integer, primary_key=True, index=True)
interface = Column(sqlalchemy.String, unique=True, index=True)
subnet = Column(sqlalchemy.Integer, nullable=False)
address = Column(sqlalchemy.String, unique=True)
v6_address = Column(sqlalchemy.String, unique=True)
v6_subnet = Column(sqlalchemy.Integer, nullable=False)
listen_port = Column(sqlalchemy.String, unique=True)
private_key = Column(sqlalchemy.String)
public_key = Column(sqlalchemy.String)
endpoint = Column(sqlalchemy.String)
dns = Column(sqlalchemy.String)
read_only = Column(sqlalchemy.Integer, default=0)
post_up = Column(sqlalchemy.String)
post_down = Column(sqlalchemy.String)
@ -42,11 +57,13 @@ class WGPeer(Base):
id = Column(Integer, primary_key=True, index=True)
name = Column(sqlalchemy.String, default="Unnamed")
address = Column(sqlalchemy.String)
v6_address = Column(sqlalchemy.String)
public_key = Column(sqlalchemy.String)
private_key = Column(sqlalchemy.String)
shared_key = Column(sqlalchemy.Text)
dns = Column(sqlalchemy.Text)
allowed_ips = Column(sqlalchemy.String)
read_only = Column(sqlalchemy.Integer, default=0)
server_id = Column(Integer, sqlalchemy.ForeignKey('server.id', ondelete="CASCADE", onupdate="CASCADE"))
server = relationship("WGServer", backref=backref("server"))

2
wg_dashboard_backend/requirements.txt

@ -12,3 +12,5 @@ jinja2
sqlalchemy_utils
sqlalchemy-migrate
requests
uvicorn
qrcode[pil]

110
wg_dashboard_backend/routers/v1/peer.py

@ -1,7 +1,8 @@
import ipaddress
import itertools
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from starlette.responses import PlainTextResponse
import const
import models
@ -13,20 +14,18 @@ import script.wireguard
router = APIRouter()
@router.post("/add", response_model=schemas.WGPeer)
def add_peer(
peer_add: schemas.WGPeerAdd,
sess: Session = Depends(middleware.get_db)
):
server = schemas.WGServer(interface=peer_add.server_interface).from_db(sess)
peer = schemas.WGPeer(server_id=server.id)
address_space = set(ipaddress.ip_network(server.address, strict=False).hosts())
def generate_ip_address(server: schemas.WGServer, v6):
if v6:
address_space = set(
itertools.islice(ipaddress.ip_network("fd42:42:42::1/64", strict=False).hosts(), 1, 1024)
)
else:
address_space = set(ipaddress.ip_network(f"{server.address}/{server.subnet}", strict=False).hosts())
occupied_space = set()
# Try add server IP to list.
try:
occupied_space.add(ipaddress.ip_address(server.address.split("/")[0]))
occupied_space.add(ipaddress.ip_address(server.v6_address if v6 else server.address))
except ValueError:
pass
@ -34,14 +33,31 @@ def add_peer(
# Try add peer ip to list.
try:
occupied_space.add(ipaddress.ip_address(p.address.split("/")[0]))
occupied_space.add(ipaddress.ip_address(p.v6_address if v6 else p.address))
except ValueError as e:
pass # Ignore invalid addresses. These are out of address_space
address_space -= occupied_space
# Select first available address
peer.address = str(list(sorted(address_space)).pop(0)) + "/32"
return str(list(sorted(address_space)).pop(0))
@router.post("/add", response_model=schemas.WGPeer)
def add_peer(
peer_add: schemas.WGPeerConfigAdd,
sess: Session = Depends(middleware.get_db)
):
server = schemas.WGServer(interface=peer_add.server_interface).from_db(sess)
if server is None:
raise HTTPException(500, detail="The server-interface '%s' does not exist!" % peer_add.server_interface)
peer = schemas.WGPeer(server_id=server.id)
if server.v6_address:
peer.v6_address = generate_ip_address(server, v6=True)
peer.address = generate_ip_address(server, v6=False)
# Private public key generation
keys = script.wireguard.generate_keys()
@ -52,7 +68,7 @@ def add_peer(
peer.allowed_ips = ', '.join(const.PEER_DEFAULT_ALLOWED_IPS)
# Set unnamed
peer.name = "Unnamed"
peer.name = "Unnamed" if not peer_add.name else peer_add.name
peer.dns = server.dns
@ -61,13 +77,45 @@ def add_peer(
server=server
))
peer.sync(sess)
db_peer = models.WGPeer(**peer.dict())
sess.add(db_peer)
sess.commit()
# If server is running. Add peer
if script.wireguard.is_running(server):
script.wireguard.add_peer(server, peer)
return peer
# Update server configuration
db.wireguard.server_update_configuration(sess, db_peer.server_id)
return schemas.WGPeer.from_orm(db_peer)
@router.post("/configuration/get_or_add")
def get_or_add_peer_return_config(peer_get: schemas.WGPeerConfigGetByName,
sess: Session = Depends(middleware.get_db)
):
server = sess.query(models.WGServer).filter_by(interface=peer_get.server_interface).one()
peer = sess.query(models.WGPeer).filter_by(name=peer_get.name, server_id=server.id).all()
if not peer:
return add_peer_get_config(schemas.WGPeerConfigAdd(
name=peer_get.name,
server_interface=peer_get.server_interface
), sess=sess)
peer = peer[0]
return PlainTextResponse(peer.configuration)
@router.post("/configuration/add")
def add_peer_get_config(peer_add: schemas.WGPeerConfigAdd,
sess: Session = Depends(middleware.get_db)
):
wg_peer: schemas.WGPeer = add_peer(peer_add, sess)
return PlainTextResponse(wg_peer.configuration)
@router.post("/delete", response_model=schemas.WGPeer)
@ -75,13 +123,13 @@ def delete_peer(
peer: schemas.WGPeer,
sess: Session = Depends(middleware.get_db)
):
peer.from_db(sess) # Sync full object
server = sess.query(models.WGServer).filter_by(id=peer.server_id).one()
if not db.wireguard.peer_remove(sess, peer):
raise HTTPException(400, detail="Were not able to delete peer %s (%s)" % (peer.name, peer.public_key))
server = schemas.WGServer(interface=peer.server_id)
if script.wireguard.is_running(server):
if script.wireguard.is_running(schemas.WGServer(interface=server.interface)):
script.wireguard.remove_peer(server, peer)
return peer
@ -92,27 +140,7 @@ def edit_peer(
peer: schemas.WGPeer,
sess: Session = Depends(middleware.get_db)
):
# Retrieve server from db
server: models.WGServer = db.wireguard.get_server_by_id(sess, peer.server_id)
# Generate peer configuration
peer.configuration = script.wireguard.generate_config(dict(
peer=peer,
server=server
))
# Update database record for Peer
sess.query(models.WGPeer)\
.filter_by(id=peer.id)\
.update(peer.dict(exclude={"id"}))
peer = db.wireguard.peer_edit(sess, peer)
# Generate server configuration
server.configuration = script.wireguard.generate_config(server)
sess.add(server)
sess.commit()
return dict(
peer=peer,
server_configuration=server.configuration
)
return peer

59
wg_dashboard_backend/routers/v1/server.py

@ -32,44 +32,8 @@ def add_interface(
server: schemas.WGServerAdd,
sess: Session = Depends(middleware.get_db)
):
server.post_up = server.post_up if server.post_up != "" else const.DEFAULT_POST_UP
server.post_down = server.post_up if server.post_up != "" else const.DEFAULT_POST_DOWN
peers = server.peers if server.peers else []
# Public/Private key
try:
if server.filter_query(sess).count() != 0:
raise HTTPException(status_code=400, detail="The server interface %s already exists in the database" % server.interface)
if not server.private_key:
keys = script.wireguard.generate_keys()
server.private_key = keys["private_key"]
server.public_key = keys["public_key"]
server.configuration = script.wireguard.generate_config(server)
server.peers = []
server.sync(sess)
if len(peers) > 0:
server.from_db(sess)
for schemaPeer in peers:
schemaPeer.server_id = server.id
schemaPeer.configuration = script.wireguard.generate_config(dict(
peer=schemaPeer,
server=server
))
dbPeer = models.WGPeer(**schemaPeer.dict())
sess.add(dbPeer)
sess.commit()
server.from_db(sess)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
return server
return db.wireguard.server_add(server, sess)
@router.post("/stop", response_model=schemas.WGServer)
@ -122,7 +86,11 @@ def delete_server(
@router.post("/stats", dependencies=[Depends(middleware.auth)])
def stats_server(server: schemas.WGServer):
stats = script.wireguard.get_stats(server)
if script.wireguard.is_running(server):
stats = script.wireguard.get_stats(server)
else:
stats = []
return JSONResponse(content=stats)
@ -149,12 +117,23 @@ def edit_server(
peer=peer,
server=server
))
peer.sync(sess)
db_peer = models.WGPeer(**peer.dict())
sess.merge(db_peer)
sess.commit()
script.wireguard.start_interface(server)
server.is_running = script.wireguard.is_running(server)
server.sync(sess)
server.sync(sess) # TODO - fix this sync mess.
server.from_db(sess)
return server
@router.get("/config/{server_id}", response_model=str)
def server_config(
server_id: int,
sess: Session = Depends(middleware.get_db)
):
return db.wireguard.get_server_by_id(sess, server_id=server_id).configuration

59
wg_dashboard_backend/routers/v1/user.py

@ -1,9 +1,12 @@
import os
from datetime import timedelta
from fastapi import APIRouter, HTTPException, Depends, Form
from fastapi import APIRouter, HTTPException, Depends, Form, Body
from fastapi.responses import PlainTextResponse, JSONResponse
import typing
from sqlalchemy.orm import Session
from starlette import status
from binascii import hexlify
import const
import db.user
import middleware
@ -28,6 +31,51 @@ def edit(form_data: schemas.UserInDB,
return form_data
@router.get("/users/api-key/add", response_model=schemas.UserAPIKeyFull)
def add_api_key(
user: schemas.UserInDB = Depends(middleware.auth),
sess: Session = Depends(middleware.get_db)
):
key = hexlify(os.urandom(const.API_KEY_LENGTH)).decode()
api_key = models.UserAPIKey(
user_id=user.id,
key=key,
)
sess.add(api_key)
sess.commit()
return schemas.UserAPIKeyFull.from_orm(api_key)
@router.post("/users/api-key/delete")
def delete_api_keys(
key_id: int = Body(None, embed=True),
user: schemas.UserInDB = Depends(middleware.auth),
sess: Session = Depends(middleware.get_db)
):
count = sess.query(models.UserAPIKey)\
.filter_by(id=key_id)\
.delete()
sess.commit()
return JSONResponse({
"message": "Key deleted OK" if count == 1 else "There was an error while deleting the api-key"
})
@router.get("/users/api-key/list", response_model=typing.List[schemas.UserAPIKey])
def get_api_keys(
user: schemas.UserInDB = Depends(middleware.auth),
sess: Session = Depends(middleware.get_db)
):
keys = [schemas.UserAPIKey.from_orm(x) for x in sess.query(models.UserAPIKey)
.filter(models.UserAPIKey.user_id == user.id).all()]
return keys
@router.post("/login", response_model=schemas.Token)
def login(*, username: str = Form(...), password: str = Form(...), sess: Session = Depends(middleware.get_db)):
user: schemas.UserInDB = schemas.UserInDB(username=username, password="").from_db(sess)
@ -77,10 +125,3 @@ def create_user(
role=form_data.role,
)):
raise HTTPException(status_code=400, detail="Could not create user")
return login_for_access_token(OAuth2PasswordRequestForm(
username=form_data.username,
password=form_data.password,
scope=""
), sess)

29
wg_dashboard_backend/routers/v1/wg.py

@ -1,8 +1,14 @@
from fastapi import APIRouter
from datetime import datetime
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from zipfile import ZipFile
from io import BytesIO
import middleware
from starlette.responses import StreamingResponse
import schemas
import script.wireguard
import db.wireguard
router = APIRouter()
@ -23,3 +29,24 @@ def generate_key_pair():
private_key=private_key,
public_key=public_key
)
@router.get("/dump")
def dump_database(
sess: Session = Depends(middleware.get_db)
):
in_memory = BytesIO()
zf = ZipFile(in_memory, mode="w")
for server in db.wireguard.server_get_all(sess):
zf.writestr(f"{server.interface}/{server.interface}.conf", server.configuration)
for peer in server.peers:
zf.writestr(f"{server.interface}/peers/{peer.name}_{peer.address.replace('.','-')}.conf", server.configuration)
zf.close()
in_memory.seek(0)
now = datetime.now().strftime("%m-%d-%Y-%H:%M:%S")
return StreamingResponse(in_memory, media_type="application/zip", headers={
"Content-Disposition": f'attachment; filename="wg-manager-dump-{now}.zip"'
})

27
wg_dashboard_backend/schemas.py

@ -1,3 +1,5 @@
from datetime import datetime
import pydantic
from pydantic import BaseModel, typing
from sqlalchemy.orm import Session, Query
@ -100,6 +102,15 @@ class User(GenericModel):
excludes = {"id"}
class UserAPIKey(GenericModel):
id: int
created_date: datetime
class UserAPIKeyFull(UserAPIKey):
key: str
class UserInDB(User):
password: str
@ -114,6 +125,7 @@ class WGPeer(GenericModel):
id: int = None
name: str = None
address: str = None
v6_address: str = None
private_key: str = None
public_key: str = None
shared_key: str = None
@ -124,8 +136,8 @@ class WGPeer(GenericModel):
class Meta:
model = models.WGPeer
key = "address"
excludes = {"id"}
key = "id"
excludes = {}
class WGPeerConfig(GenericModel):
@ -144,6 +156,9 @@ class PSK(GenericModel):
class WGServer(GenericModel):
id: int = None
address: str = None
v6_address: str = None
subnet: int = None
v6_subnet: int = None
interface: str
listen_port: int = None
endpoint: str = None
@ -154,6 +169,7 @@ class WGServer(GenericModel):
post_up: str = None
post_down: str = None
dns: str = None
read_only: int = None
peers: pydantic.typing.List['WGPeer'] = []
@ -173,6 +189,11 @@ class WGServerAdd(WGServer):
listen_port: int
class WGPeerAdd(GenericModel):
class WGPeerConfigAdd(GenericModel):
server_interface: str
name: str = None
class WGPeerConfigGetByName(GenericModel):
server_interface: str
name: str = None

56
wg_dashboard_backend/script/obfuscate/__init__.py

@ -0,0 +1,56 @@
import abc
from pathlib import Path
import subprocess
import shlex
class BaseObfuscation(abc.ABC):
def __init__(self, binary_name=None, binary_path=None, algorithm=None):
assert binary_name is not None or binary_path is not None
self.binary_name = binary_name if binary_name is not None else Path(self.binary_path).name
self.binary_path = binary_path if binary_path else ""
self.algorithm = algorithm
def ensure_installed(self):
# Attempt to find process by path
binary = Path(self.binary_path)
if not binary.is_file():
# Did not find by path, attempt to find using which
proc_which = subprocess.Popen(["which", self.binary_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
data = [x.decode().strip() for x in proc_which.communicate() if x != b''][0]
if proc_which.returncode != 0:
raise RuntimeError("Could not find binary '%s'" % data)
self.binary_path = data
def execute(self, *args, kill_first=False, override_command=None):
if kill_first:
# TODO try to delete by full name as we dont want to kill other processes.
pattern = self.binary_name
self.execute(*[pattern], override_command="pkill")
#pattern = self.binary_path + " " + ' '.join(args)
#print(pattern)
#kill_output, kill_code = self.execute(*[pattern], override_command="pkill")
command = override_command if override_command is not None else self.binary_path
print(shlex.join([command] + list(args)))
proc_which = subprocess.Popen([command] + list(args), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
raw_data = proc_which.communicate()
data = [x.decode().strip() for x in raw_data if x != b'']
if len(data) == 0:
data = ""
else:
data = data[0]
return data, proc_which.returncode

30
wg_dashboard_backend/script/obfuscate/obfs4.py

@ -0,0 +1,30 @@
from script.obfuscate import BaseObfuscation
import re
class ObfuscateOBFS4(BaseObfuscation):
def __init__(self):
super().__init__(
binary_name="obfs4proxy",
binary_path="/usr/bin/obfs4proxy",
algorithm="obfs4"
)
self.ensure_installed()
def ensure_installed(self):
super().ensure_installed()
output, code = self.execute("-version")
if re.match(f'{self.binary_name}-[0-9]+.[0-9]+.[0-9]+', output) and code == 0:
return True
else:
raise RuntimeError(f"Could not verify that {self.binary_name} is installed correctly.")
if __name__ == "__main__":
x = ObfuscateOBFS4()
x.ensure_installed()

119
wg_dashboard_backend/script/obfuscate/tor.py

@ -0,0 +1,119 @@
from pathlib import Path
import requests
import const
from script.obfuscate import BaseObfuscation
import re
import os
import qrcode
import socket
from script.obfuscate.obfs4 import ObfuscateOBFS4
class ObfuscationViaTOR(BaseObfuscation):
def __init__(self, algorithm: BaseObfuscation):
super().__init__(
binary_name="tor"
)
self.algorithm = algorithm
self.tor_data_dir = "/tmp/wg-manager-tor-proxy"
self.tor_config_file = "/tmp/wg-manager-tor-proxy/torrc"
self.tor_fingerprint_file = f"{self.tor_data_dir}/fingerprint"
self.tor_bridge_file = f"{self.tor_data_dir}/pt_state/obfs4_bridgeline.txt"
Path(self.tor_config_file).touch()
os.makedirs(self.tor_data_dir, exist_ok=True)
def __del__(self):
pass
def ensure_installed(self):
super().ensure_installed()
output, code = self.execute("--version")
if re.match(f'Tor version .*', output) and code == 0:
return True
else:
raise RuntimeError(f"Could not verify that {self.binary_name} is installed correctly.")
def start(self):
output, code = self.execute(
"-f", self.tor_config_file,
"--DataDirectory", self.tor_data_dir,
"--RunAsDaemon", "1",
"--ExitPolicy", "reject *:*",
"--ORPort", str(const.OBFUSCATE_SOCKS_TOR_PORT),
"--BridgeRelay", "1",
"--PublishServerDescriptor", "0",
"--ServerTransportPlugin", f"{self.algorithm.algorithm} exec {self.algorithm.binary_path}",
"--ServerTransportListenAddr", f"{self.algorithm.algorithm} 0.0.0.0:{const.OBFUSCATE_TOR_LISTEN_ADDR}",
"--ExtORPort", "auto",
"--ContactInfo", "wg-manager@github.com",
"--Nickname", "wgmanager",
kill_first=True
)
print(output)
def generate_bridge_line(self, local=False):
if local:
ip_address = socket.gethostbyname(socket.gethostname())
else:
ip_address = requests.get("https://api.ipify.org").text
with open(self.tor_fingerprint_file, "r") as f:
fingerprint = f.read().split(" ")
assert len(fingerprint) == 2, "Could not load fingerprint correctly. " \
"Should be a list of 2 items (name, fingerprint)"
fingerprint = fingerprint[1]
with open(self.tor_bridge_file, "r") as f:
bridge_line_raw = f.read()
bridge_line = re.search(r"^Bridge .*", bridge_line_raw, re.MULTILINE).group(0)
bridge_line = bridge_line\
.replace("<IP ADDRESS>", ip_address)\
.replace("<PORT>", str(const.OBFUSCATE_TOR_LISTEN_ADDR))\
.replace("<FINGERPRINT>", fingerprint)\
.replace("Bridge ", "bridge://")\
.replace("\n", "")
#bridge_line = f"bridge://{self.algorithm.algorithm} {ip_address}:{const.OBFUSCATE_SOCKS_TOR_PORT} {fingerprint}"
print(bridge_line)
return bridge_line
def output_qr(self, text, image=False):
qr = qrcode.QRCode(
version=10,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(text)
qr.make(fit=True)
if image:
img = qr.make_image(fill_color="black", back_color="white")
img.show()
else:
try:
qr.print_tty()
except:
qr.print_ascii()
if __name__ == "__main__":
x = ObfuscationViaTOR(
algorithm=ObfuscateOBFS4()
)
x.ensure_installed()
x.start()
bridge_line = x.generate_bridge_line(local=False)
x.output_qr(bridge_line, image=True)
#x.generate_bridge_line(local=False)

209
wg_dashboard_backend/script/wireguard.py

@ -1,16 +1,21 @@
import logging
import random
import subprocess
import tempfile
import requests
import typing
import configparser
from sqlalchemy.orm import Session
import const
import models
import schemas
import os
import re
import ipaddress
import util
from database import SessionLocal
_LOGGER = logging.getLogger(__name__)
@ -27,6 +32,10 @@ class WGPermissionsError(Exception):
pass
class WGPortAlreadyInUse(Exception):
pass
class TempServerFile():
def __init__(self, server: schemas.WGServer):
self.server = server
@ -48,7 +57,7 @@ def _run_wg(server: schemas.WGServer, command):
return output
except Exception as e:
if b'Operation not permitted' in e.output:
raise WGPermissionsError("The user has insufficientt permissions for interface %s" % server.interface)
raise WGPermissionsError("The user has insufficient permissions for interface %s" % server.interface)
def is_installed():
@ -75,15 +84,18 @@ def generate_psk():
return subprocess.check_output(const.CMD_WG_COMMAND + ["genpsk"]).decode("utf-8").strip()
def start_interface(server: schemas.WGServer):
def start_interface(server: typing.Union[schemas.WGServer, schemas.WGPeer]):
with TempServerFile(server) as server_file:
try:
#print(*const.CMD_WG_QUICK, "up", server_file)
# print(*const.CMD_WG_QUICK, "up", server_file)
output = subprocess.check_output(const.CMD_WG_QUICK + ["up", server_file], stderr=subprocess.STDOUT)
return output
except Exception as e:
print(e.output)
if b'already exists' in e.output:
raise WGAlreadyStartedError("The wireguard device %s is already started." % server.interface)
elif b'Address already in use' in e.output:
raise WGPortAlreadyInUse("The port %s is already used by another application." % server.listen_port)
def stop_interface(server: schemas.WGServer):
@ -92,7 +104,6 @@ def stop_interface(server: schemas.WGServer):
output = subprocess.check_output(const.CMD_WG_QUICK + ["down", server_file], stderr=subprocess.STDOUT)
return output
except Exception as e:
if b'is not a WireGuard interface' in e.output:
raise WGAlreadyStoppedError("The wireguard device %s is already stopped." % server.interface)
@ -111,6 +122,7 @@ def is_running(server: schemas.WGServer):
if output is None or b'Unable to access interface: No such device' in output:
return False
except Exception as e:
print(e.output)
if b'No such device' in e.output:
return False
return True
@ -197,14 +209,197 @@ def move_server_dir(interface, interface1):
def generate_config(obj: typing.Union[typing.Dict[schemas.WGPeer, schemas.WGServer], schemas.WGServer]):
if isinstance(obj, dict) and "server" in obj and "peer" in obj:
template = "peer.j2"
is_ipv6 = obj["server"].v6_address is not None
elif isinstance(obj, schemas.WGServer) or isinstance(obj, models.WGServer):
template = "server.j2"
is_ipv6 = obj.v6_address is not None
else:
raise ValueError("Incorrect input type. Should be WGPeer or WGServer")
result = util.jinja_env.get_template(template).render(
data=obj
data=obj,
is_ipv6=is_ipv6
)
return result
def retrieve_client_conf_from_server(
client_name,
server_interface,
server_host,
server_api_key
):
const.CLIENT_NAME = "client-1"
const.CLIENT_SERVER_INTERFACE = "wg0"
const.CLIENT_SERVER_HOST = "http://localhost:4200"
const.CLIENT_API_KEY = "8bae20143fb962930614952d80634822361fd5ab9488053866a56de5881f9d7b"
assert server_interface is not None and \
server_host is not None and \
server_api_key is not None, "Client configuration is invalid: %s, %s, api-key-is-null?: %s" % (
server_interface,
server_host,
server_api_key is None
)
api_get_or_add = f"{server_host}/api/v1/peer/configuration/get_or_add"
response = requests.post(api_get_or_add, json={
"server_interface": server_interface,
"name": client_name
}, headers={
"X-API-Key": server_api_key
})
if response.status_code != 200:
print(response.text)
raise RuntimeError("Could not retrieve config from server: %s" % (api_get_or_add,))
return response.text
def create_client_config(sess: Session, configuration, client_name, client_routes):
parser = configparser.ConfigParser()
parser.read_string(configuration)
public_key = parser["Peer"]["PublicKey"]
assert len(set(parser.sections()) - {"Interface", "Peer"}) == 0, "Missing Interface or Peer section"
# Parse Server
# Check if server already exists.
is_new_server = False
is_new_peer = False
try:
db_server = sess.query(models.WGServer).filter_by(
public_key=public_key,
read_only=1
).one()
except:
db_server = None
if db_server is None:
db_server = models.WGServer()
is_new_server = True
db_server.read_only = 1
db_server.public_key = parser["Peer"]["PublicKey"]
db_server.address = parser["Peer"]["Endpoint"]
db_server.listen_port = random.randint(69000, 19292009)
db_server.v6_address = "N/A"
db_server.v6_subnet = 0
db_server.address = "N/A"
db_server.subnet = 0
db_server.private_key = "N/A"
db_server.dns = "N/A"
db_server.post_up = "N/A"
db_server.post_down = "N/A"
db_server.is_running = False
db_server.configuration = "N/A"
# Parse client
try:
db_peer = sess.query(models.WGPeer).filter_by(
private_key=parser["Interface"]["PrivateKey"],
read_only=1
).one()
except:
db_peer = None
if db_peer is None:
db_peer = models.WGPeer()
is_new_peer = True
db_peer.read_only = 1
db_peer.name = client_name
addresses_split = parser["Interface"]["Address"].split(",")
assert len(addresses_split) > 0, "Must be at least one address"
for address_with_subnet in addresses_split:
addr, subnet = address_with_subnet.split("/")
if isinstance(ipaddress.ip_address(addr), ipaddress.IPv4Address):
db_peer.address = address_with_subnet
elif isinstance(ipaddress.ip_address(addr), ipaddress.IPv6Address):
db_peer.v6_address = address_with_subnet
else:
raise RuntimeError("Incorrect IP Address: %s, %s" % (addr, subnet))
db_peer.private_key = parser["Interface"]["PrivateKey"]
db_peer.public_key = "N/A"
db_peer.allowed_ips = client_routes if client_routes else parser["Peer"]["AllowedIPs"]
db_peer.configuration = configuration
db_server.interface = f"client_{db_peer.name}"
db_server.configuration = configuration
try:
db_peer.shared_key = parser["Interface"]["PrivateKey"]
except KeyError:
db_peer.shared_key = "N/A"
db_peer.dns = parser["Interface"]["DNS"]
db_peer.server = db_server
if is_new_server:
sess.add(db_server)
else:
sess.merge(db_server)
sess.commit()
if is_new_peer:
sess.add(db_peer)
else:
sess.merge(db_peer)
sess.commit()
if const.CLIENT_START_AUTOMATICALLY and not is_running(db_server):
start_interface(db_server)
def load_environment_clients(sess: Session):
i = 1
while True:
client_name = os.getenv(f"CLIENT_{i}_NAME", None)
client_server_interface = os.getenv(f"CLIENT_{i}_SERVER_INTERFACE", None)
client_server_host = os.getenv(f"CLIENT_{i}_SERVER_HOST", None)
client_api_key = os.getenv(f"CLIENT_{i}_API_KEY", None)
client_routes = os.getenv(f"CLIENT_{i}_ROUTES", None)
if client_api_key is None or \
client_server_interface is None or \
client_server_host is None or \
client_api_key is None:
break
_LOGGER.warning(
f"Found client configuration: name={client_name},siface={client_server_interface},shost={client_server_host}")
config = retrieve_client_conf_from_server(
client_name=client_name,
server_interface=client_server_interface,
server_host=client_server_host,
server_api_key=client_api_key
)
create_client_config(sess, configuration=config, client_name=client_name, client_routes=client_routes)
i += 1
if __name__ == "__main__":
os.environ["CLIENT_1_NAME"] = "client-1"
os.environ["CLIENT_1_SERVER_INTERFACE"] = "wg0"
os.environ["CLIENT_1_SERVER_HOST"] = "http://localhost:4200"
os.environ["CLIENT_1_API_KEY"] = "8bae20143fb962930614952d80634822361fd5ab9488053866a56de5881f9d7b"
os.environ["CLIENT_2_NAME"] = "client-2"
os.environ["CLIENT_2_SERVER_INTERFACE"] = "wg0"
os.environ["CLIENT_2_SERVER_HOST"] = "http://localhost:4200"
os.environ["CLIENT_2_API_KEY"] = "8bae20143fb962930614952d80634822361fd5ab9488053866a56de5881f9d7b"
sess: Session = SessionLocal()
load_environment_clients(sess)
sess.close()

2
wg_dashboard_backend/templates/peer.j2

@ -1,5 +1,5 @@
[Interface]
Address = {{ data.peer.address.replace("/32", "/24") }}
Address = {{ data.peer.address }}/{{ data.server.subnet }}{%- if is_ipv6 -%},{{ data.peer.v6_address }}/{{ data.server.v6_subnet }}{%- endif %}
PrivateKey = {{ data.peer.private_key }}
DNS = {{ data.peer.dns }}

8
wg_dashboard_backend/templates/server.j2

@ -1,10 +1,10 @@
[Interface]
Address = {{ data.address }}
Address = {{ data.address }}/{{ data.subnet }}{%- if is_ipv6 -%},{{ data.v6_address }}/{{ data.v6_subnet }}{%- endif %}
ListenPort = {{ data.listen_port }}
PrivateKey = {{ data.private_key }}
PostUp = {{ data.post_up }}
PostDown = {{ data.post_down }}
PostUp = {{ data.post_up }}{%- if is_ipv6 -%} {{ data.v6_post_up }}{%- endif %}
PostDown = {{ data.post_down }}{%- if is_ipv6 -%} {{ data.v6_post_down }}{%- endif %}
{% for peer in data.peers %}
[Peer]
@ -13,5 +13,5 @@ PublicKey = {{ peer.public_key }}
{%- if peer.shared_key %}
PresharedKey = {{ peer.shared_key }}
{%- endif %}
AllowedIPs = {{ peer.address }}
AllowedIPs = {{ peer.address }}/32{%- if is_ipv6 -%},{{ peer.v6_address }}/128{%- endif %}
{% endfor %}

4122
wg_dashboard_frontend/package-lock.json

File diff suppressed because it is too large

2
wg_dashboard_frontend/package.json

@ -8,6 +8,7 @@
"license": "MIT",
"scripts": {
"ng": "ng",
"md-doc": "npm install widdershins && wget http://127.0.0.1:8000/openapi.json -O /tmp/wg-openapi.json && widdershins /tmp/wg-openapi.json --environment widdershins.json --resolve=true --omitHeader=true --language_tabs 'python:Python' -o ../docs/api.md",
"start": "ng serve --host 0.0.0.0 --disable-host-check",
"build": "ng build",
"buildwatch": "ng build --watch --aot --outputPath=../wg_dashboard_backend/build/ --host 0.0.0.0 --disable-host-check",
@ -59,6 +60,7 @@
"rxjs": "6.5.5",
"tslib": "^1.10.0",
"web-animations-js": "^2.3.2",
"widdershins": "^4.0.1",
"zone.js": "^0.10.3"
},
"devDependencies": {

1
wg_dashboard_frontend/src/app/interfaces/peer.ts

@ -1,6 +1,7 @@
export interface Peer {
_stats: any;
address: string;
v6_address: string;
public_key: string;
private_key: string;
shared_key: string;

3
wg_dashboard_frontend/src/app/interfaces/server.ts

@ -1,6 +1,7 @@
import { Peer } from './peer';
export interface Server {
id: number;
address: string;
interface: string;
listen_port: string;
@ -12,5 +13,7 @@ export interface Server {
post_up: string;
post_down: string;
configuration: string;
subnet: number;
read_only: number;
peers: Peer[];
}

28
wg_dashboard_frontend/src/app/layout/layout.module.ts

@ -12,22 +12,24 @@ import { RouterModule } from '@angular/router';
import { MatMenuModule } from '@angular/material/menu';
import {MatSlideToggleModule} from "@angular/material/slide-toggle";
import {NotifierModule} from "angular-notifier";
import {FormsModule} from "@angular/forms";
@NgModule({
declarations: [LayoutComponent],
imports: [
CommonModule,
MatSidenavModule,
MatToolbarModule,
MatListModule,
MatIconModule,
MatButtonModule,
FlexLayoutModule,
RouterModule,
MatMenuModule,
MatSlideToggleModule,
NotifierModule,
],
imports: [
CommonModule,
MatSidenavModule,
MatToolbarModule,
MatListModule,
MatIconModule,
MatButtonModule,
FlexLayoutModule,
RouterModule,
MatMenuModule,
MatSlideToggleModule,
NotifierModule,
FormsModule,
],
exports: [
],

7
wg_dashboard_frontend/src/app/layout/layout/layout.component.html

@ -36,10 +36,11 @@
<mat-menu #themeMenu="matMenu">
<button mat-menu-item *ngFor="let theme of themes" (click)="setCurrentTheme(theme)">{{theme.name}}</button>
<mat-slide-toggle
style="margin-top: 10px;"
style="margin-top: 10px; padding: 0 16px;"
color="primary"
(click)="toggleDarkMode($event);"
[checked]="darkMode">
[(ngModel)]="darkMode"
(change)="toggleDarkMode()"
(click)="$event.stopPropagation();">
Dark
</mat-slide-toggle>
</mat-menu>

14
wg_dashboard_frontend/src/app/layout/layout/layout.component.ts

@ -40,22 +40,22 @@ export class LayoutComponent implements OnInit {
public auth: AuthService,
private comm: DataService,
private cookieService: CookieService
) {}
ngOnInit(): void {
console.log('Layout');
) {
this.darkMode = (this.cookieService.get("darkMode") === 'true');
if(this.cookieService.check("currentTheme")){
this.currentTheme = JSON.parse(this.cookieService.get("currentTheme"));
this.darkMode = (this.cookieService.get("darkMode") === 'true');
}else {
this.currentTheme = { ... this.themes[0]}
}
this.sendData()
}
ngOnInit(): void {
console.log('Layout');
}
toggleDarkMode($event){
$event.stopPropagation();
this.darkMode = !this.darkMode;
toggleDarkMode(){
this.cookieService.set("darkMode", String(this.darkMode));
this.sendData();
}

2
wg_dashboard_frontend/src/app/page/components/modal-confirm/modal-confirm.component.ts

@ -37,6 +37,7 @@ export class ModalConfirmComponent implements OnInit {
}
open($event){
$event.stopPropagation();
if (this.noConfirm) {
this.onConfirm.emit($event);
return true;
@ -54,6 +55,7 @@ export class ModalConfirmComponent implements OnInit {
}
cancel($event){
$event.stopPropagation();
this.onCancel.emit($event);
this.shown = false
}

66
wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.html

@ -20,41 +20,91 @@
<form [formGroup]="serverForm" class="add-server-form">
<p><b>Essentials</b></p>
<table class="add-server-full-width" cellspacing="0"><tr>
<td>
<mat-form-field class="add-server-full-width">
<mat-label>Interface</mat-label>
<input formControlName="interface" matInput placeholder="wg0">
<input formControlName="interface" matInput [placeholder]="defaultInterface">
</mat-form-field>
</td>
<td>
<mat-form-field class="add-server-full-width">
<mat-label>Address Scope</mat-label>
<input formControlName="address" matInput placeholder="10.0.200.1/24">
<mat-label>Endpoint</mat-label>
<input formControlName="endpoint" matInput placeholder="my-address.com">
</mat-form-field>
</td>
<td>
<mat-form-field class="add-server-full-width">
<mat-label>Port</mat-label>
<input formControlName="listen_port" matInput [placeholder]="defaultListenPort">
</mat-form-field>
</td>
</tr></table>
<table class="add-server-full-width" cellspacing="0"><tr>
<td>
<mat-form-field class="add-server-full-width">
<mat-label>Endpoint</mat-label>
<input formControlName="endpoint" matInput placeholder="my-address.com">
<mat-label>IPv4 Address</mat-label>
<input formControlName="address" matInput [placeholder]="defaultIPv4Address">
</mat-form-field>
</td>
<td>
<mat-form-field matLine class="add-server-full-width">
<mat-label>Subnet</mat-label>
<select matNativeControl formControlName="subnet">
<option *ngFor="let v4Subnet of v4Subnets" [value]="v4Subnet">/{{v4Subnet}}</option>
</select>
</mat-form-field>
</td>
</tr></table>
<table class="add-server-full-width" cellspacing="0"><tr>
<td>
<mat-checkbox [checked]="true" #hasIPV6Support (change)="ipv6SupportChanged($event)">IPv6 Support</mat-checkbox>
</td>
</tr></table>
<table *ngIf="hasIPV6Support.checked" class="add-server-full-width" cellspacing="0"><tr>
<td>
<mat-form-field class="add-server-full-width">
<mat-label>Port</mat-label>
<input formControlName="listen_port" matInput placeholder="51820">
<mat-label>IPv6 Address</mat-label>
<input formControlName="v6_address" matInput [placeholder]="defaultIPv6Address">
</mat-form-field>
</td>
<td>
<mat-form-field matLine class="add-server-full-width">
<mat-label>Subnet</mat-label>
<select matNativeControl formControlName="v6_subnet">
<option *ngFor="let v6Subnet of v6Subnets" [value]="v6Subnet">/{{v6Subnet}}</option>
</select>
</mat-form-field>
</td>
</tr></table>
<table class="add-server-full-width" cellspacing="0"><tr>
<td>
<mat-form-field class="add-server-full-width">
<mat-label>Default DNS</mat-label>
<input formControlName="dns" matInput placeholder="8.8.8.8">
<input formControlName="dns" matInput [placeholder]="defaultIPv4Address">
</mat-form-field>
</td>
</tr></table>

60
wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts

@ -1,15 +1,16 @@
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
import {Component, Input, OnInit, ViewChild, ViewEncapsulation} from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { IPValidator } from '../../../validators/ip-address.validator';
import { NumberValidator } from '../../../validators/number.validator';
import { Server } from '../../../interfaces/server';
import { ServerService } from '../../../services/server.service';
import { DataService } from '../../../services/data.service';
import Parser, {Property, Section, Sections} from "@jedmao/ini-parser";
import Parser, {Section} from "@jedmao/ini-parser";
import {Peer} from "../../../interfaces/peer";
import {forkJoin, from, Observable, of} from "rxjs";
import {concatAll, concatMap, filter, map, mergeAll, mergeMap, switchMap} from "rxjs/operators";
import {forkJoin, from} from "rxjs";
import {map, mergeMap} from "rxjs/operators";
import {NotifierService} from "angular-notifier";
import {MatCheckboxChange} from "@angular/material/checkbox";
@Component({
selector: 'app-add-server',
templateUrl: './add-server.component.html',
@ -34,21 +35,36 @@ export class AddServerComponent implements OnInit {
"DNS": "dns"
}
v4Subnets = [];
v6Subnets = [];
defaultListenPort = "51820"
defaultInterface = "wg0"
defaultIPv4Subnet = 24;
defaultIPv6Subnet = 64;
defaultIPv4Address = "10.0.200.1"
defaultDNS = this.defaultIPv4Address + ",8.8.8.8"
defaultIPv6Address = "fd42:42:42::1"
serverForm: FormGroup = null;
isEdit = false;
editServer: Server = null;
initForm(){
this.serverForm = new FormGroup({
address: new FormControl('', [IPValidator.isIPAddress]),
interface: new FormControl('', [Validators.required, Validators.minLength(3)]),
listen_port: new FormControl('', [Validators.required, NumberValidator.stringIsNumber]),
address: new FormControl(this.defaultIPv4Address, [Validators.required, IPValidator.isIPAddress]),
v6_address: new FormControl(this.defaultIPv6Address, [Validators.required, IPValidator.isIPAddress]),
subnet: new FormControl(this.defaultIPv4Subnet, [Validators.required, Validators.min(1), Validators.max(32)]),
v6_subnet: new FormControl(this.defaultIPv6Subnet, [Validators.required, Validators.min(1), Validators.max(64)]),
interface: new FormControl(this.defaultInterface, [Validators.required, Validators.minLength(3)]),
listen_port: new FormControl(this.defaultListenPort, [Validators.required, NumberValidator.stringIsNumber]),
endpoint: new FormControl('', Validators.required),
dns: new FormControl(''),
dns: new FormControl(this.defaultDNS),
private_key: new FormControl('' ),
public_key: new FormControl('' ),
post_up: new FormControl(''),
post_down: new FormControl(''),
read_only: new FormControl(''),
// Unused on backend
configuration: new FormControl(''),
@ -57,11 +73,25 @@ export class AddServerComponent implements OnInit {
});
}
ipv6SupportChanged($event: MatCheckboxChange){
let v6AddressControl = this.serverForm.get("v6_address");
let v6SubnetControl = this.serverForm.get("v6_subnet");
if($event.checked){
v6AddressControl.enable()
v6SubnetControl.enable()
}else {
v6AddressControl.disable()
v6SubnetControl.disable()
}
}
constructor(private serverAPI: ServerService, private comm: DataService, private notify: NotifierService) {
}
ngOnInit(): void {
this.v4Subnets = Array(32).fill(1).map((x,i)=>i+1);
this.v6Subnets = Array(64).fill(1).map((x,i)=>i+1);
this.initForm();
this.comm.on('server-edit').subscribe((data: Server) => {
@ -140,11 +170,14 @@ export class AddServerComponent implements OnInit {
return false;
}
iFace.nodes["subnet"] = iFace.nodes["address"].split("/")[1];
iFace.nodes["address"] = iFace.nodes["address"].split("/")[0];
iFace.nodes["peers"] = sPeers
.map( x => x.nodes)
.map( x => {
x.server_id = -1;
x.address = x.allowed_ips; // Allowed_ips in server is the address of the peer (Seen from server perspective)
x.address = x.allowed_ips.split("/")[0]; // Allowed_ips in server is the address of the peer (Seen from server perspective)
x.allowed_ips = null; // This should be retrieved from peer data config
return x;
})
@ -200,10 +233,9 @@ export class AddServerComponent implements OnInit {
});
}
this.isEdit = false;
this.editServer = null;
this.serverForm.reset();
this.serverForm.clearValidators();
this.resetForm();
}
getKeyPair() {
@ -220,7 +252,7 @@ export class AddServerComponent implements OnInit {
resetForm() {
this.isEdit = false;
this.editServer = null;
this.serverForm.clearValidators();
this.initForm()
}
}

4
wg_dashboard_frontend/src/app/page/dashboard/dashboard.module.ts

@ -18,6 +18,8 @@ import { MatTableModule } from '@angular/material/table';
import { PeerComponent } from './peer/peer.component';
import { QRCodeModule } from 'angularx-qrcode';
import {MatTooltipModule} from "@angular/material/tooltip";
import {MatSelectModule} from "@angular/material/select";
import {MatCheckboxModule} from "@angular/material/checkbox";
@NgModule({
declarations: [
@ -43,6 +45,8 @@ import {MatTooltipModule} from "@angular/material/tooltip";
FormsModule,
QRCodeModule,
MatTooltipModule,
MatSelectModule,
MatCheckboxModule,
],
})

15
wg_dashboard_frontend/src/app/page/dashboard/peer/peer.component.ts

@ -16,6 +16,7 @@ export class PeerComponent implements OnInit {
@Input('server') server: Server;
@Input('selectedPeer') selectedPeer: Peer;
@Input('onEvent') editPeerEmitter: EventEmitter<any> = new EventEmitter<any>();
@Input('cbOnPeerUpdate') cbOnPeerUpdate: Function;
constructor(public serverAPI: ServerService) { }
@ -28,6 +29,7 @@ export class PeerComponent implements OnInit {
if (msg.type === 'edit') {
this.edit();
} else if (msg.type == 'delete') {
this.delete();
}
@ -41,11 +43,13 @@ export class PeerComponent implements OnInit {
// Submit the edit (True -> False)
const idx = this.server.peers.indexOf(this.peer);
this.serverAPI.editPeer(this.peer).subscribe((data) => {
this.server.configuration = data.server_configuration;
Object.keys(data.peer).forEach(k => {
this.server.peers[idx][k] = data.peer[k];
this.serverAPI.editPeer(this.peer).subscribe((peer) => {
Object.keys(peer).forEach(k => {
this.server.peers[idx][k] = peer[k];
});
// Query server for server configuration update
this.cbOnPeerUpdate();
});
} else if (!this.peer._edit) {
@ -63,6 +67,9 @@ export class PeerComponent implements OnInit {
const idx = this.server.peers.indexOf(this.peer);
this.serverAPI.deletePeer(this.peer).subscribe((apiServer) => {
this.server.peers.splice(idx, 1);
// Query server for server configuration update
this.cbOnPeerUpdate();
});
}

111
wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html

@ -1,5 +1,5 @@
<mat-card class="dashboard-card">
<mat-card class="dashboard-card" *ngIf="server.read_only == 0">
<mat-card-header>
<mat-card-title class="card-container-left">
@ -15,6 +15,7 @@
<mat-card-title class="card-container-right">
<app-modal-confirm
*ngIf="server.read_only == 0"
[noConfirm]="true"
(onConfirm)="downloadServerConfig()"
area="true"
@ -33,6 +34,7 @@
</app-modal-confirm>
<app-modal-confirm
*ngIf="server.read_only == 0"
[noConfirm]="true"
(onConfirm)="addPeer()"
icon="person_add"
@ -67,6 +69,7 @@
</app-modal-confirm>
<app-modal-confirm
*ngIf="server.read_only == 0"
[noConfirm]="true"
(onConfirm)="edit()"
icon="edit"
@ -83,13 +86,21 @@
</mat-card-title>
<mat-card-subtitle style="margin-top: 2px;">Endpoint: <b>{{server.endpoint}}:{{server.listen_port}}</b> - Address Space: <b>{{server.address}}</b></mat-card-subtitle>
<mat-card-subtitle *ngIf="server.read_only == 0" style="margin-top: 2px;">Endpoint: <b>{{server.endpoint}}:{{server.listen_port}}</b> - Address: <b>{{server.address}}/{{server.subnet}}</b></mat-card-subtitle>
<!-- Read-only, usually client "fake-server" -->
<mat-card-subtitle *ngIf="server.read_only == 1" style="margin-top: 2px;">
Address: <b>{{server.peers[0].address}}, {{server.peers[0].v6_address}}</b>
<br>DNS: <b>{{server.peers[0].dns}}</b>
<br>Allowed-IP's <b>{{server.peers[0].allowed_ips}}</b>
<br>Endpoint: <b>{{ getEndpointFromConfig(server.peers[0].configuration)}}</b>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content class="dashboard-card-content">
<mat-card-content *ngIf="server.read_only == 0" class="dashboard-card-content">
<table class="table">
<table class="table" >
<thead>
<tr>
<th>Name</th>
@ -127,7 +138,7 @@
<!-- Edit buttons -->
<app-modal-confirm
[noConfirm]="true"
(onConfirm)="selectedPeer=peer; this.editPeerEmitter.emit({type: 'edit', peer: peer}); $event.stopPropagation();"
(onConfirm)="selectedPeer=peer; this.editPeerEmitter.emit({type: 'edit', peer: peer});"
icon="edit"
hover="Edit {{peer.name}}">
</app-modal-confirm>
@ -148,7 +159,12 @@
</tr>
<tr [hidden]="peer !== selectedPeer">
<td colspan="6">
<app-peer [onEvent]="this.editPeerEmitter" [(peer)]="server.peers[idx]" [(server)]="server"></app-peer>
<app-peer
[cbOnPeerUpdate]="onPeerUpdate"
[onEvent]="this.editPeerEmitter"
[(peer)]="server.peers[idx]"
[(server)]="server">
</app-peer>
</td>
</tr>
@ -164,3 +180,86 @@
<mat-card-actions>
</mat-card-actions>
</mat-card>
<ng-container *ngIf="server.read_only == 1">
<mat-card class="dashboard-card" *ngFor="let srv_peer of server.peers">
<mat-card-header>
<mat-card-title class="card-container-left">
<mat-icon
class="app-material-icon-valign"
[class]="{'green': server.is_running, 'red': !server.is_running}"
matTooltip="Indicates if the server is online or offline"
>check_circle</mat-icon>
{{srv_peer.name}}
</mat-card-title>
<mat-card-title class="card-container-right">
<app-modal-confirm
[qrCode]="true"
[noConfirm]="false"
area="true"
icon="settings"
title="Configuration"
[text]="server.configuration"
hover="Show config for {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
*ngIf="!server.is_running"
[noConfirm]="true"
(onConfirm)="start()"
icon="play_arrow"
hover="Start {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
*ngIf="server.is_running"
[noConfirm]="false"
(onConfirm)="stop()"
title="Stop server {{server.interface}}?"
text="Are you sure you want to stop this server? This may cause you or your clients to lose connection to the server."
icon="stop"
hover="Stop {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
[noConfirm]="false"
(onConfirm)="restart()"
title="Restart server {{server.interface}}?"
text="Are you sure you want to restart this server? This may cause you or your clients to lose connection to the server."
icon="autorenew"
hover="Restart {{server.interface}}">
</app-modal-confirm>
<app-modal-confirm
(onConfirm)="delete()"
title="Delete {{server.interface}}"
text="Are you sure you want to delete {{server.interface}}"
icon="delete"
hover="Delete {{server.interface}}">
</app-modal-confirm>
</mat-card-title>
<!-- Read-only, usually client "fake-server" -->
<mat-card-subtitle style="margin-top: 2px;">
Address: <b>{{srv_peer.address}}, {{srv_peer.v6_address}}</b>
<br>DNS: <b>{{srv_peer.dns}}</b>
<br>Allowed-IP's <b>{{srv_peer.allowed_ips}}</b>
<br>Endpoint: <b>{{ getEndpointFromConfig(srv_peer.configuration)}}</b>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content class="dashboard-card-content">
</mat-card-content>
<mat-card-actions>
</mat-card-actions>
</mat-card>
</ng-container>

3
wg_dashboard_frontend/src/app/page/dashboard/server/server.component.scss

@ -11,3 +11,6 @@ table {
font-size: 20px;
}
.dashboard-card{
margin-bottom: 10px;
}

19
wg_dashboard_frontend/src/app/page/dashboard/server/server.component.ts

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnInit, ViewEncapsulation } from '@angular/core';
import { Component, EventEmitter, Input, OnInit } from '@angular/core';
import { Server } from '../../../interfaces/server';
import { ServerService } from '../../../services/server.service';
import { DataService } from '../../../services/data.service';
@ -47,6 +47,9 @@ export class ServerComponent implements OnInit {
server_interface: this.server.interface
}).subscribe((peer) => {
this.server.peers.push(peer);
// Query server for server configuration update
this.onPeerUpdate();
});
}
@ -58,7 +61,7 @@ export class ServerComponent implements OnInit {
delete() {
const index = this.servers.indexOf(this.server);
this.serverAPI.deleteServer(this.server).subscribe((apiServer) => {
this.serverAPI.deleteServer(this.server).subscribe(() => {
this.servers.splice(index, 1);
});
}
@ -72,6 +75,12 @@ export class ServerComponent implements OnInit {
this.editPeerEmitter.emit({ type: 'open', peer });
}
onPeerUpdate(){
this.serverAPI.serverConfig(this.server).subscribe((configuration) => {
this.server.configuration = configuration
})
}
pInt(string: string) {
return parseInt(string);
}
@ -92,4 +101,10 @@ export class ServerComponent implements OnInit {
saveAs(content, `${this.server.interface}_${this.server.address}.zip`);
});
}
getEndpointFromConfig(config){
console.log(config)
let res = config.match("Endpoint = (.*)") // TODO handle whitespace
return res[1]
}
}

2
wg_dashboard_frontend/src/app/page/page-routing.module.ts

@ -16,7 +16,7 @@ const routes: Routes = [
},
{ path: 'user', component: LayoutComponent, children:
[
{ path: 'edit', component: EditComponent, pathMatch: 'full' },
{ path: 'edit', component: EditComponent, pathMatch: 'full', canActivate: [AuthGuard]},
{ path: 'login', component: LoginComponent, pathMatch: 'full' },
],
},

5
wg_dashboard_frontend/src/app/page/page.module.ts

@ -9,9 +9,11 @@ import { MatInputModule } from '@angular/material/input';
import { FlexModule } from '@angular/flex-layout';
import { EditComponent } from './user/edit/edit.component';
import { MatButtonModule } from '@angular/material/button';
import {MatTableModule} from "@angular/material/table";
import { ApiKeyComponent } from './user/edit/api-key/api-key.component';
@NgModule({
declarations: [LoginComponent, EditComponent],
declarations: [LoginComponent, EditComponent, ApiKeyComponent],
imports: [
CommonModule,
PageRoutingModule,
@ -22,6 +24,7 @@ import { MatButtonModule } from '@angular/material/button';
MatInputModule,
FlexModule,
MatButtonModule,
MatTableModule,
],
})

43
wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.html

@ -0,0 +1,43 @@
<mat-card>
<mat-card-title>
API Keys
</mat-card-title>
<mat-card-content>
You can use API-Keys to perform authenticated actions. These are less secure than using OAuth2, but at the gain for increased convenience.
<br><b>Note:</b> A newly created API Key will only show <b>once</b>. This means that you have to take note of the key and safe it somewhere safe.
<table mat-table [dataSource]="dataSource" style="width: 100%">
<!-- Id Column -->
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef> ID. </th>
<td mat-cell *matCellDef="let element"> {{element.id}} </td>
</ng-container>
<!-- Key Column -->
<ng-container matColumnDef="key">
<th mat-header-cell *matHeaderCellDef> API-Key </th>
<td mat-cell *matCellDef="let element"> {{(element.key) ? element.key : "[HIDDEN]"}} </td>
</ng-container>
<!-- Created_At Column -->
<ng-container matColumnDef="created_at">
<th mat-header-cell *matHeaderCellDef> Creation Date </th>
<td mat-cell *matCellDef="let element"> {{element.created_date | date:'medium'}} </td>
</ng-container>
<!-- Delete Column -->
<ng-container matColumnDef="delete">
<th mat-header-cell *matHeaderCellDef> Delete </th>
<td mat-cell *matCellDef="let element"> <button mat-flat-button color="warn" (click)="deleteAPIKey(element)">Delete</button></td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<button mat-flat-button color="primary" (click)="createAPIKey()">New Key</button>
</mat-card-content>
</mat-card>

0
wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.scss

25
wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.spec.ts

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ApiKeyComponent } from './api-key.component';
describe('ApiKeyComponent', () => {
let component: ApiKeyComponent;
let fixture: ComponentFixture<ApiKeyComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ApiKeyComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ApiKeyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

45
wg_dashboard_frontend/src/app/page/user/edit/api-key/api-key.component.ts

@ -0,0 +1,45 @@
import {Component, OnInit} from '@angular/core';
import {ServerService} from "../../../../services/server.service";
@Component({
selector: 'app-api-key',
templateUrl: './api-key.component.html',
styleUrls: ['./api-key.component.scss']
})
export class ApiKeyComponent implements OnInit {
displayedColumns: string[] = ['id', 'key', 'created_at', 'delete'];
dataSource = [];
constructor(private serverService: ServerService
) { }
ngOnInit(): void {
this.serverService.getAPIKeys().subscribe((apiKeys: Array<any>) => {
this.dataSource = [...apiKeys]
console.log(this.dataSource)
})
}
deleteAPIKey(elem){
let idx = this.dataSource.indexOf(elem);
this.serverService.deleteAPIKey(elem.id).subscribe(x => {
this.dataSource.splice(idx, 1);
this.dataSource = [...this.dataSource]
})
}
createAPIKey(){
this.serverService.addAPIKey().subscribe(key => {
this.dataSource.push(key)
this.dataSource = [...this.dataSource]
})
}
}

7
wg_dashboard_frontend/src/app/page/user/edit/edit.component.html

@ -1,5 +1,5 @@
<div flex fxFill fxLayout="row" fxLayoutAlign="center center" >
<div fxFlex="33">
<div flex fxFill fxLayout="row" fxLayoutAlign="left top">
<div fxFlex="33" class="user-edit-component">
<mat-card>
<mat-card-title>
@ -47,6 +47,9 @@
</mat-card>
</div>
<div fxFlex="66" class="user-edit-component">
<app-api-key></app-api-key>
</div>

3
wg_dashboard_frontend/src/app/page/user/edit/edit.component.scss

@ -0,0 +1,3 @@
.user-edit-component{
padding: 20px;
}

7
wg_dashboard_frontend/src/app/page/user/edit/edit.component.ts

@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { AuthService } from '@services/*';
import { Router } from '@angular/router';
import {ServerService} from "../../../services/server.service";
@Component({
selector: 'app-edit',
@ -23,8 +24,10 @@ export class EditComponent implements OnInit {
public user: any;
public error: string;
constructor(private authService: AuthService,
private router: Router) {
constructor(
private authService: AuthService,
private router: Router
) {
}

3
wg_dashboard_frontend/src/app/services/config.service.ts

@ -10,7 +10,8 @@ export class ConfigService {
public applicationName = 'WireGuard Manager';
constructor(private notify: NotifierService) { }
constructor(private notify: NotifierService) {
}
public handleError(error: HttpErrorResponse) {
if (error.error instanceof ErrorEvent) {

50
wg_dashboard_frontend/src/app/services/server.service.ts

@ -16,6 +16,9 @@ export class ServerService {
public serverURL = this.base + "server";
public peerURL = this.base + "peer";
public wgURL = this.base + "wg";
public userURL = this.base + "users";
public apiKeyURL = this.userURL + "/api-key"
constructor(private config: ConfigService, private http: HttpClient, private notify: NotifierService) {
@ -28,15 +31,17 @@ export class ServerService {
public serverPerformAction(action: string, item: any): Subscribable<Server> {
return this.http.post(this.serverURL + '/' + action, item)
.pipe(catchError(this.config.handleError));
.pipe(catchError(this.config.handleError.bind(this)));
}
public addPeer(server_interface: any): Subscribable<Peer> {
return this.http.post(this.peerURL + '/add', server_interface);
return this.http.post(this.peerURL + '/add', server_interface)
.pipe(catchError(this.config.handleError.bind(this)));
}
public editPeer(peer: Peer): Subscribable<{ peer: Peer, server_configuration: string }> {
return this.http.post(this.peerURL + '/edit', peer);
public editPeer(peer: Peer): Subscribable<Peer> {
return this.http.post(this.peerURL + '/edit', peer)
.pipe(catchError(this.config.handleError.bind(this)));
}
public getServers(): Observable<Server[]> {
@ -50,7 +55,7 @@ export class ServerService {
}
public startServer(item: Server): Subscribable<Server> {
return this.serverPerformAction('start', item);
return this.serverPerformAction('start', item)
}
public stopServer(item: Server): Subscribable<Server> {
@ -73,22 +78,45 @@ export class ServerService {
}
public getKeyPair() {
return this.http.get(this.wgURL + '/generate_keypair');
return this.http.get(this.wgURL + '/generate_keypair')
.pipe(catchError(this.config.handleError.bind(this)));
}
public getPSK() {
return this.http.get(this.wgURL + '/generate_psk');
return this.http.get(this.wgURL + '/generate_psk')
.pipe(catchError(this.config.handleError.bind(this)));
}
public peerConfig(peer: Peer) {
return this.http.post(this.peerURL + '/config', peer);
return this.http.post(this.peerURL + '/config', peer)
.pipe(catchError(this.config.handleError.bind(this)));
}
public serverConfig(server: Server) {
return this.http.post(this.serverURL + '/config', server);
public serverConfig(server: Server): Subscribable<string> {
return this.http.get(this.serverURL + '/config/' + server.id.toString())
.pipe(catchError(this.config.handleError.bind(this)));
}
public serverStats(server: Server) {
return this.http.post(this.serverURL + '/stats', server);
return this.http.post(this.serverURL + '/stats', server)
.pipe(catchError(this.config.handleError.bind(this)));
}
public addAPIKey() {
return this.http.get(this.apiKeyURL + '/add')
.pipe(catchError(this.config.handleError.bind(this)));
}
public getAPIKeys() {
return this.http.get(this.apiKeyURL + '/list')
.pipe(catchError(this.config.handleError.bind(this)));
}
public deleteAPIKey(api_key_id: { id: number }) {
return this.http.post(this.apiKeyURL + '/delete', {
key_id: api_key_id
})
.pipe(catchError(this.config.handleError.bind(this)));
}
}

2
wg_dashboard_frontend/src/app/validators/ip-address.validator.ts

@ -4,7 +4,7 @@ import * as IPCIDR from 'ip-cidr';
export class IPValidator {
static isIPAddress(control: AbstractControl): ValidationErrors | null {
if (!control.value || !(new IPCIDR(control.value).isValid()) || !control.value.includes('/')) {
if (!control.value || !(new IPCIDR(control.value).isValid())) {
return { validIP: true };
}
return null;

3
wg_dashboard_frontend/widdershins.json

@ -0,0 +1,3 @@
{
"language_tabs": [{ "python": "Python" }]
}
Loading…
Cancel
Save