From 89b41bcf697f50a9d72e5bc39289d8e9dc778efa Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Mon, 20 Jul 2020 23:52:51 +0200 Subject: [PATCH 1/3] * Clearified error message when no ADMIN_USERNAME, and ADMIN_PASSWORD env variable is set. #27 --- wg_dashboard_backend/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wg_dashboard_backend/main.py b/wg_dashboard_backend/main.py index a876841..2b4857b 100644 --- a/wg_dashboard_backend/main.py +++ b/wg_dashboard_backend/main.py @@ -37,12 +37,12 @@ _db: Session = SessionLocal() 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_USERNAME 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) From df1c94b8e9b04e12f27779b8a5c158f14bc904a6 Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Tue, 21 Jul 2020 02:27:07 +0200 Subject: [PATCH 2/3] * 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 :) --- README.md | 88 +++++++- docker-compose.yaml | 43 +++- wg_dashboard_backend/const.py | 8 + wg_dashboard_backend/db/api_key.py | 27 +++ wg_dashboard_backend/db/user.py | 4 +- wg_dashboard_backend/db/wireguard.py | 94 ++++++++- wg_dashboard_backend/main.py | 19 +- .../versions/007_create_read_only_client.py | 34 +++ wg_dashboard_backend/models.py | 2 + wg_dashboard_backend/requirements.txt | 2 +- wg_dashboard_backend/routers/v1/peer.py | 26 ++- wg_dashboard_backend/routers/v1/server.py | 53 +---- wg_dashboard_backend/schemas.py | 8 +- wg_dashboard_backend/script/wireguard.py | 195 +++++++++++++++++- .../src/app/interfaces/peer.ts | 1 + .../src/app/interfaces/server.ts | 1 + .../add-server/add-server.component.ts | 1 + .../dashboard/server/server.component.html | 102 ++++++++- .../dashboard/server/server.component.scss | 3 + .../page/dashboard/server/server.component.ts | 6 + .../src/app/services/config.service.ts | 3 +- .../src/app/services/server.service.ts | 34 +-- 22 files changed, 663 insertions(+), 91 deletions(-) create mode 100644 wg_dashboard_backend/db/api_key.py create mode 100644 wg_dashboard_backend/migrations/versions/007_create_read_only_client.py diff --git a/README.md b/README.md index e544b9b..8869786 100644 --- a/README.md +++ b/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: " http://:/api/v1/users/api-key/list` -5. Example 2: `curl -X POST "http://:/api/v1/peer/add/configuration" -H "accept: application/json" -H "Content-Type: application/json" -H "X-API-Key: " -d "{\"server_interface\":\"wg0\"}"` +5. Example 2: `curl -X POST "http://:/api/v1/peer/configuration/add" -H "accept: application/json" -H "Content-Type: application/json" -H "X-API-Key: " -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) diff --git a/docker-compose.yaml b/docker-compose.yaml index 19ee233..10da013 100644 --- a/docker-compose.yaml +++ b/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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/wg_dashboard_backend/const.py b/wg_dashboard_backend/const.py index 1ea55ab..bfd8e89 100644 --- a/wg_dashboard_backend/const.py +++ b/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 diff --git a/wg_dashboard_backend/db/api_key.py b/wg_dashboard_backend/db/api_key.py new file mode 100644 index 0000000..898a4dc --- /dev/null +++ b/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 diff --git a/wg_dashboard_backend/db/user.py b/wg_dashboard_backend/db/user.py index b5f5509..ea5634c 100644 --- a/wg_dashboard_backend/db/user.py +++ b/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() diff --git a/wg_dashboard_backend/db/wireguard.py b/wg_dashboard_backend/db/wireguard.py index 71187fe..0fb93d4 100644 --- a/wg_dashboard_backend/db/wireguard.py +++ b/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: diff --git a/wg_dashboard_backend/main.py b/wg_dashboard_backend/main.py index 2b4857b..f9d9a59 100644 --- a/wg_dashboard_backend/main.py +++ b/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() diff --git a/wg_dashboard_backend/migrations/versions/007_create_read_only_client.py b/wg_dashboard_backend/migrations/versions/007_create_read_only_client.py new file mode 100644 index 0000000..4ae29b9 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/wg_dashboard_backend/models.py b/wg_dashboard_backend/models.py index 13fc980..0c7ef68 100644 --- a/wg_dashboard_backend/models.py +++ b/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")) diff --git a/wg_dashboard_backend/requirements.txt b/wg_dashboard_backend/requirements.txt index 30347ba..d81d475 100644 --- a/wg_dashboard_backend/requirements.txt +++ b/wg_dashboard_backend/requirements.txt @@ -12,4 +12,4 @@ jinja2 sqlalchemy_utils sqlalchemy-migrate requests -uvicorn \ No newline at end of file +uvicorn diff --git a/wg_dashboard_backend/routers/v1/peer.py b/wg_dashboard_backend/routers/v1/peer.py index 4fe642b..7bc8949 100644 --- a/wg_dashboard_backend/routers/v1/peer.py +++ b/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) diff --git a/wg_dashboard_backend/routers/v1/server.py b/wg_dashboard_backend/routers/v1/server.py index 938881d..4bf4d8d 100644 --- a/wg_dashboard_backend/routers/v1/server.py +++ b/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) diff --git a/wg_dashboard_backend/schemas.py b/wg_dashboard_backend/schemas.py index 7a16513..2e71529 100644 --- a/wg_dashboard_backend/schemas.py +++ b/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 \ No newline at end of file diff --git a/wg_dashboard_backend/script/wireguard.py b/wg_dashboard_backend/script/wireguard.py index 8286df4..e252bb6 100644 --- a/wg_dashboard_backend/script/wireguard.py +++ b/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() diff --git a/wg_dashboard_frontend/src/app/interfaces/peer.ts b/wg_dashboard_frontend/src/app/interfaces/peer.ts index f8ca8aa..c96cb1d 100644 --- a/wg_dashboard_frontend/src/app/interfaces/peer.ts +++ b/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; diff --git a/wg_dashboard_frontend/src/app/interfaces/server.ts b/wg_dashboard_frontend/src/app/interfaces/server.ts index dd8a801..2105223 100644 --- a/wg_dashboard_frontend/src/app/interfaces/server.ts +++ b/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[]; } diff --git a/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts b/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts index 77b5ac7..0227145 100644 --- a/wg_dashboard_frontend/src/app/page/dashboard/add-server/add-server.component.ts +++ b/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(''), diff --git a/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html b/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html index b6f3843..db8b7a0 100644 --- a/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html +++ b/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.html @@ -1,5 +1,5 @@ - + @@ -15,6 +15,7 @@ - Endpoint: {{server.endpoint}}:{{server.listen_port}} - Address: {{server.address}}/{{server.subnet}} + Endpoint: {{server.endpoint}}:{{server.listen_port}} - Address: {{server.address}}/{{server.subnet}} + + + + Address: {{server.peers[0].address}}, {{server.peers[0].v6_address}} +
DNS: {{server.peers[0].dns}} +
Allowed-IP's {{server.peers[0].allowed_ips}} +
Endpoint: {{ getEndpointFromConfig(server.peers[0].configuration)}} +
- + - +
@@ -169,3 +180,86 @@ + + + + + + + + + check_circle + {{srv_peer.name}} + + + + + + + + + + + + + + + + + + + + + + + + Address: {{srv_peer.address}}, {{srv_peer.v6_address}} +
DNS: {{srv_peer.dns}} +
Allowed-IP's {{srv_peer.allowed_ips}} +
Endpoint: {{ getEndpointFromConfig(srv_peer.configuration)}} +
+
+ + + + + + + +
+ +
diff --git a/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.scss b/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.scss index 9aa55a2..770ec8c 100644 --- a/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.scss +++ b/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.scss @@ -11,3 +11,6 @@ table { font-size: 20px; } +.dashboard-card{ + margin-bottom: 10px; +} diff --git a/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.ts b/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.ts index dffb40c..3581eca 100644 --- a/wg_dashboard_frontend/src/app/page/dashboard/server/server.component.ts +++ b/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] + } } diff --git a/wg_dashboard_frontend/src/app/services/config.service.ts b/wg_dashboard_frontend/src/app/services/config.service.ts index cc87719..5564fe0 100644 --- a/wg_dashboard_frontend/src/app/services/config.service.ts +++ b/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) { diff --git a/wg_dashboard_frontend/src/app/services/server.service.ts b/wg_dashboard_frontend/src/app/services/server.service.ts index fafe45a..ea69dc9 100644 --- a/wg_dashboard_frontend/src/app/services/server.service.ts +++ b/wg_dashboard_frontend/src/app/services/server.service.ts @@ -31,15 +31,17 @@ export class ServerService { public serverPerformAction(action: string, item: any): Subscribable { 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 { - 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 { - 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 { @@ -53,7 +55,7 @@ export class ServerService { } public startServer(item: Server): Subscribable { - return this.serverPerformAction('start', item); + return this.serverPerformAction('start', item) } public stopServer(item: Server): Subscribable { @@ -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 { - 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))); } } From a2a5aca1bcf3b3518862a71d66c4ba9bb98b5539 Mon Sep 17 00:00:00 2001 From: Per-Arne Andersen Date: Tue, 21 Jul 2020 02:29:27 +0200 Subject: [PATCH 3/3] * docs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8869786..88b0001 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ The features of wg-manager includes: **General** * Modify Admin User +* Create and manage API-Keys # Dependencies * Linux >= 5.6 *(Alternatively: wireguard-dkms)*
Name