Browse Source

* Initial version of client-mode.

* A lot of small fixes
* Added gui for client mode, where Clients are read-only (may change in the future)
* NEED FEEBACK On this :)
pull/29/head
Per-Arne Andersen 5 years ago
parent
commit
df1c94b8e9
  1. 88
      README.md
  2. 43
      docker-compose.yaml
  3. 8
      wg_dashboard_backend/const.py
  4. 27
      wg_dashboard_backend/db/api_key.py
  5. 4
      wg_dashboard_backend/db/user.py
  6. 94
      wg_dashboard_backend/db/wireguard.py
  7. 19
      wg_dashboard_backend/main.py
  8. 34
      wg_dashboard_backend/migrations/versions/007_create_read_only_client.py
  9. 2
      wg_dashboard_backend/models.py
  10. 2
      wg_dashboard_backend/requirements.txt
  11. 26
      wg_dashboard_backend/routers/v1/peer.py
  12. 53
      wg_dashboard_backend/routers/v1/server.py
  13. 8
      wg_dashboard_backend/schemas.py
  14. 195
      wg_dashboard_backend/script/wireguard.py
  15. 1
      wg_dashboard_frontend/src/app/interfaces/peer.ts
  16. 1
      wg_dashboard_frontend/src/app/interfaces/server.ts
  17. 1
      wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts
  18. 102
      wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html
  19. 3
      wg_dashboard_frontend/src/app/page/dashboard/server/server.component.scss
  20. 6
      wg_dashboard_frontend/src/app/page/dashboard/server/server.component.ts
  21. 3
      wg_dashboard_frontend/src/app/services/config.service.ts
  22. 34
      wg_dashboard_frontend/src/app/services/server.service.ts

88
README.md

@ -16,6 +16,7 @@ The features of wg-manager includes:
* Bandwidth usage statistics
* Export by QRCode, Text
* Authentication via API-Keys for automation (Created in GUI)
* Automatic setup using docker
**General**
* Modify Admin User
@ -94,7 +95,81 @@ The API docs is found [here](./docs/api.md).
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/add/configuration" -H "accept: application/json" -H "Content-Type: application/json" -H "X-API-Key: <api-key-here>" -d "{\"server_interface\":\"wg0\"}"`
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 |
|------------------|---------------------------------------------------------------------------|-------------|
@ -110,6 +185,17 @@ The API docs is found [here](./docs/api.md).
| 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 |
# Showcase
![Illustration](docs/images/0.png)

43
docker-compose.yaml

@ -14,11 +14,48 @@ services:
- 11820:11820/udp
- 51800-51900:51800-51900/udp
- 8888:8888
volumes:
- ./wg-manager:/config
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
WEB_CONCURRENCY: 2
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

8
wg_dashboard_backend/const.py

@ -19,6 +19,14 @@ 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):
db_user = sess.query(models.User)\
.filter_by(username="admin")\
.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()

94
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,7 +47,6 @@ def peer_dns_set(sess: Session, peer: schemas.WGPeer) -> schemas.WGPeer:
def peer_remove(sess: Session, peer: schemas.WGPeer) -> bool:
db_peers = sess.query(models.WGPeer).filter_by(id=peer.id).all()
for db_peer in db_peers:
@ -132,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:

19
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,6 +40,7 @@ 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:
@ -48,14 +55,14 @@ if not database_exists(engine.url):
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,14 @@ 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:
db.api_key.add_initial_api_key_for_admin(_db, const.SERVER_STARTUP_API_KEY)
_db.close()

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

2
wg_dashboard_backend/models.py

@ -41,6 +41,7 @@ class WGServer(Base):
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)
@ -62,6 +63,7 @@ class WGPeer(Base):
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,4 +12,4 @@ jinja2
sqlalchemy_utils
sqlalchemy-migrate
requests
uvicorn
uvicorn

26
wg_dashboard_backend/routers/v1/peer.py

@ -45,7 +45,7 @@ def generate_ip_address(server: schemas.WGServer, v6):
@router.post("/add", response_model=schemas.WGPeer)
def add_peer(
peer_add: schemas.WGPeerAdd,
peer_add: schemas.WGPeerConfigAdd,
sess: Session = Depends(middleware.get_db)
):
server = schemas.WGServer(interface=peer_add.server_interface).from_db(sess)
@ -68,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
@ -91,8 +91,26 @@ def add_peer(
return schemas.WGPeer.from_orm(db_peer)
@router.post("/add/configuration")
def add_peer_get_config(peer_add: schemas.WGPeerAdd,
@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)

53
wg_dashboard_backend/routers/v1/server.py

@ -33,58 +33,7 @@ def add_interface(
sess: Session = Depends(middleware.get_db)
):
# 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))
return server
return db.wireguard.server_add(server, sess)
@router.post("/stop", response_model=schemas.WGServer)

8
wg_dashboard_backend/schemas.py

@ -169,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'] = []
@ -188,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

195
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__)
@ -30,6 +35,7 @@ class WGPermissionsError(Exception):
class WGPortAlreadyInUse(Exception):
pass
class TempServerFile():
def __init__(self, server: schemas.WGServer):
self.server = server
@ -78,10 +84,10 @@ 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:
@ -113,7 +119,7 @@ def restart_interface(server: schemas.WGServer):
def is_running(server: schemas.WGServer):
try:
output = _run_wg(server, ["show", server.interface])
if output is None:
if output is None or b'Unable to access interface: No such device' in output:
return False
except Exception as e:
print(e.output)
@ -217,3 +223,182 @@ def generate_config(obj: typing.Union[typing.Dict[schemas.WGPeer, schemas.WGServ
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):
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 = 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)
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)
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()

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;

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

@ -14,5 +14,6 @@ export interface Server {
post_down: string;
configuration: string;
subnet: number;
read_only: number;
peers: Peer[];
}

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

@ -64,6 +64,7 @@ export class AddServerComponent implements OnInit {
public_key: new FormControl('' ),
post_up: new FormControl(''),
post_down: new FormControl(''),
read_only: new FormControl(''),
// Unused on backend
configuration: new FormControl(''),

102
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: <b>{{server.address}}/{{server.subnet}}</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>
@ -169,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;
}

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

@ -101,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]
}
}

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) {

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

@ -31,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> {
return this.http.post(this.peerURL + '/edit', peer);
return this.http.post(this.peerURL + '/edit', peer)
.pipe(catchError(this.config.handleError.bind(this)));
}
public getServers(): Observable<Server[]> {
@ -53,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> {
@ -76,37 +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): Subscribable<string> {
return this.http.get(this.serverURL + '/config/' + server.id.toString());
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');
return this.http.get(this.apiKeyURL + '/add')
.pipe(catchError(this.config.handleError.bind(this)));
}
public getAPIKeys() {
return this.http.get(this.apiKeyURL + '/list');
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)));
}
}

Loading…
Cancel
Save