diff --git a/.gitignore b/.gitignore index d22c6f0..9968f75 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ __pycache__ src/wg-dashboard.ini src/static/pic.xd *.conf +private_key.txt +public_key.txt diff --git a/README.md b/README.md index fcb4a84..5a4894c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@
- +
Monitoring WireGuard is not convinient, need to login into server and type wg show
. That's why this platform is being created, to view all configurations and manage them in a easier way.
Latest Version: V2.1
+Latest Version: v2.2
-All these settings will be able to configure within the dashboard in **Settings** on the sidebar, without changing the actual file. **Except `version` and `auth_req` due to security consideration.** +**Except `auth_req` due to security consideration.** + +#### Generating QR code and peer configuration file (.conf) + +Starting version 2.2, dashboard can now generate QR code and configuration file for each peer. Here is a template of what each QR code encoded with and the same content will be inside the file: + +```ini +[Interface] +PrivateKey = QWERTYUIOPO234567890YUSDAKFH10E1B12JE129U21= +Address = 0.0.0.0/32 +DNS = 1.1.1.1 + +[Peer] +PublicKey = QWERTYUIOPO234567890YUSDAKFH10E1B12JE129U21= +AllowedIPs = 0.0.0.0/0 +Endpoint = 0.0.0.0:51820 +``` + +| | Description | Default Value | Available in Peer setting | +| ----------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------- | +| **`[Interface]`** | | | | +| `PrivateKey` | The private key of this peer | Private key generated by WireGuard (`wg genkey`) or provided by user | Yes | +| `Address` | The `allowed_ips` of your peer | N/A | Yes | +| `DNS` | The DNS server your peer will use | `1.1.1.1` - Cloud flare DNS, you can change it when you adding the peer or in the peer setting. | Yes | +| **`[Peer]`** | | | | +| `PublicKey` | The public key of your server | N/A | No | +| `AllowedIPs` | IP ranges for which a peer will route traffic | `0.0.0.0/0` - Indicated a default route to send all internet and VPN traffic through that peer. | No | +| `Endpoint` | Your wireguard server ip and port, the dashboard will search for your server's default interface's ip. | `Sign In
-Index Page
+![Index Image](img/HomePage.png) +Home
-![Signin Image](https://github.com/donaldzou/Wireguard-Dashboard/raw/main/src/static/signin.png) +![Configuration](img/Configuration.png) +Configuration
-Signin Page
+![Add Peer](img/AddPeer.png) +Add Peer
-![Configuration Image](https://github.com/donaldzou/Wireguard-Dashboard/raw/main/src/static/configuration.png) +![Edit Peer](img/EditPeer.png) +Edit Peer
-Configuration Page
+![Delete Peer](img/DeletePeer.png) +Delete Peer
-![Settings Image](https://github.com/donaldzou/Wireguard-Dashboard/raw/main/src/static/settings.png) +![Dashboard Setting](img/DashboardSetting.png) +Dashboard Setting
-Settings Page
+![Ping](img/Ping.png) +Ping
+ +![Traceroute](img/Traceroute.png) +Traceroute
+## β° Changelog + +#### v2.1 - Jul 2, 2021 + +- Added **Ping** and **Traceroute** tools! +- Adjusted the calculation of data usage on each peers +- Added refresh interval of the dashboard +- Bug fixed when no configuration on fresh install ([#23](https://github.com/donaldzou/wireguard-dashboard/issues/23)) +- Fixed crash when too many peers ([#22](https://github.com/donaldzou/wireguard-dashboard/issues/22)) + +#### v2.0 - May 5, 2021 + +- Added login function to dashboard + - ***I'm not using the most ideal way to store the username and password, feel free to provide a better way to do this if you any good idea!*** +- Added a config file to the dashboard +- Dashboard config can be change within the **Setting** tab on the side bar +- Adjusted UI +- And much more! + +#### v1.1.2 - Apr 3, 2021 + +- Resolved issue [#3](https://github.com/donaldzou/wireguard-dashboard/issues/3). + +#### v1.1.1 - Apr 2, 2021 + +- Able to add a friendly name to each peer. Thanks [#2](https://github.com/donaldzou/wireguard-dashboard/issues/2) ! + +#### v1.0 - Dec 27, 2020 + +- Added the function to remove peers + ## π Dependencies - CSS/JS @@ -185,14 +410,13 @@ All these settings will be able to configure within the dashboard in **Settings* - [Bootstrap Icon](https://icons.getbootstrap.com) `v1.4.0` - [jQuery](https://jquery.com) `v3.5.1` - Python - - [Flask](https://pypi.org/project/Flask/) `v1.1.2` + - [Flask](https://pypi.org/project/Flask/) `v2.0.1` - [TinyDB](https://pypi.org/project/tinydb/) `v4.3.0` - [ifcfg](https://pypi.org/project/ifcfg/) `v0.21` - [icmplib](https://pypi.org/project/icmplib/) `v2.1.1` + - [flask-qrcode](https://pypi.org/project/Flask-QRcode/) `v3.0.0` - - -## Contributors β¨ +## β¨ Contributors [![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#contributors-) diff --git a/img/AddPeer.png b/img/AddPeer.png new file mode 100644 index 0000000..e130b01 Binary files /dev/null and b/img/AddPeer.png differ diff --git a/img/Configuration.png b/img/Configuration.png new file mode 100644 index 0000000..393c4bf Binary files /dev/null and b/img/Configuration.png differ diff --git a/img/DashboardSetting.png b/img/DashboardSetting.png new file mode 100644 index 0000000..b09ced3 Binary files /dev/null and b/img/DashboardSetting.png differ diff --git a/img/DeletePeer.png b/img/DeletePeer.png new file mode 100644 index 0000000..11f15fe Binary files /dev/null and b/img/DeletePeer.png differ diff --git a/img/EditPeer.png b/img/EditPeer.png new file mode 100644 index 0000000..9bb9858 Binary files /dev/null and b/img/EditPeer.png differ diff --git a/img/Group 2.png b/img/Group 2.png deleted file mode 100644 index 2dcf9d1..0000000 Binary files a/img/Group 2.png and /dev/null differ diff --git a/img/Group 3.png b/img/Group 3.png deleted file mode 100644 index 281bbc7..0000000 Binary files a/img/Group 3.png and /dev/null differ diff --git a/img/Group 3@2x.png b/img/Group 3@2x.png deleted file mode 100644 index 888a51a..0000000 Binary files a/img/Group 3@2x.png and /dev/null differ diff --git a/img/HomePage.png b/img/HomePage.png new file mode 100644 index 0000000..41c64dc Binary files /dev/null and b/img/HomePage.png differ diff --git a/img/Ping.png b/img/Ping.png new file mode 100644 index 0000000..8592964 Binary files /dev/null and b/img/Ping.png differ diff --git a/img/QRCode.png b/img/QRCode.png new file mode 100644 index 0000000..004e27b Binary files /dev/null and b/img/QRCode.png differ diff --git a/img/SignIn.png b/img/SignIn.png new file mode 100644 index 0000000..340e706 Binary files /dev/null and b/img/SignIn.png differ diff --git a/img/Traceroute.png b/img/Traceroute.png new file mode 100644 index 0000000..b5fa9aa Binary files /dev/null and b/img/Traceroute.png differ diff --git a/img/Wg-dashboard-logo.png b/img/Wg-dashboard-logo.png deleted file mode 100644 index f8ba3a1..0000000 Binary files a/img/Wg-dashboard-logo.png and /dev/null differ diff --git a/img/Wg-dashboard-logo@2x.png b/img/Wg-dashboard-logo@2x.png deleted file mode 100644 index 4be3e71..0000000 Binary files a/img/Wg-dashboard-logo@2x.png and /dev/null differ diff --git a/img/Group 2@2x.png b/img/logo.png similarity index 100% rename from img/Group 2@2x.png rename to img/logo.png diff --git a/src/dashboard.py b/src/dashboard.py index 87280dd..974b22f 100644 --- a/src/dashboard.py +++ b/src/dashboard.py @@ -13,19 +13,24 @@ import configparser import re # PIP installed library import ifcfg +from flask_qrcode import QRcode from tinydb import TinyDB, Query from icmplib import ping, multiping, traceroute, resolve, Host, Hop # Dashboard Version -dashboard_version = 'v2.1' +dashboard_version = 'v2.2' # Dashboard Config Name dashboard_conf = 'wg-dashboard.ini' +# Default Wireguard IP +wg_ip = ifcfg.default_interface()['inet'] # Upgrade Required update = "" # Flask App Configuration app = Flask("Wireguard Dashboard") app.secret_key = secrets.token_urlsafe(16) app.config['TEMPLATES_AUTO_RELOAD'] = True +# Enable QR Code Generator +QRcode(app) def get_conf_peer_key(config_name): @@ -34,7 +39,8 @@ def get_conf_peer_key(config_name): peer_key = peer_key.decode("UTF-8").split() return peer_key except Exception: - return config_name+" is not running." + return config_name + " is not running." + def get_conf_running_peer_number(config_name): running = 0 @@ -59,6 +65,24 @@ def is_match(regex, text): pattern = re.compile(regex) return pattern.search(text) is not None + +def read_conf_file_interface(config_name): + conf_location = wg_conf_path + "/" + config_name + ".conf" + f = open(conf_location, 'r') + file = f.read().split("\n") + data = {} + peers_start = 0 + for i in range(len(file)): + if not is_match("#(.*)", file[i]): + if len(file[i]) > 0: + if file[i] != "[Interface]": + tmp = re.split(r'\s*=\s*', file[i], 1) + if len(tmp) == 2: + data[tmp[0]] = tmp[1] + f.close() + return data + + def read_conf_file(config_name): # Read Configuration File Start conf_location = wg_conf_path + "/" + config_name + ".conf" @@ -70,7 +94,7 @@ def read_conf_file(config_name): } peers_start = 0 for i in range(len(file)): - if not is_match("^#(.*)",file[i]): + if not is_match("#(.*)", file[i]): if file[i] == "[Peer]": peers_start = i break @@ -121,6 +145,7 @@ def get_latest_handshake(config_name, db, peers): db.update({"latest_handshake": "(None)", "status": status}, peers.id == data_usage[count]) count += 2 + def get_transfer(config_name, db, peers): # Get transfer try: @@ -155,6 +180,7 @@ def get_transfer(config_name, db, peers): count += 3 + def get_endpoint(config_name, db, peers): # Get endpoint try: @@ -167,22 +193,26 @@ def get_endpoint(config_name, db, peers): db.update({"endpoint": data_usage[count + 1]}, peers.id == data_usage[count]) count += 2 + def get_allowed_ip(config_name, db, peers, conf_peer_data): # Get allowed ip for i in conf_peer_data["Peers"]: db.update({"allowed_ip": i.get('AllowedIPs', '(None)')}, peers.id == i["PublicKey"]) - -def get_conf_peers_data(config_name): +def get_all_peers_data(config_name): db = TinyDB('db/' + config_name + '.json') peers = Query() conf_peer_data = read_conf_file(config_name) for i in conf_peer_data['Peers']: - if not db.search(peers.id == i['PublicKey']): + search = db.search(peers.id == i['PublicKey']) + if not search: db.insert({ "id": i['PublicKey'], + "private_key": "", + "DNS": "1.1.1.1", + "endpoint_allowed_ip": "0.0.0.0/0", "name": "", "total_receive": 0, "total_sent": 0, @@ -193,6 +223,16 @@ def get_conf_peers_data(config_name): "allowed_ip": 0, "traffic": [] }) + else: + # Update database since V2.2 + update_db = {} + if "private_key" not in search[0]: + update_db['private_key'] = '' + if "DNS" not in search[0]: + update_db['DNS'] = '1.1.1.1' + if "endpoint_allowed_ip" not in search[0]: + update_db['endpoint_allowed_ip'] = '0.0.0.0/0' + db.update(update_db, peers.id == i['PublicKey']) tic = time.perf_counter() get_latest_handshake(config_name, db, peers) @@ -204,15 +244,16 @@ def get_conf_peers_data(config_name): db.close() - - - - -def get_peers(config_name): - get_conf_peers_data(config_name) +def get_peers(config_name, search, sort_t): + get_all_peers_data(config_name) db = TinyDB('db/' + config_name + '.json') - result = db.all() - result = sorted(result, key=lambda d: d['status']) + peer = Query() + print(search) + if len(search) == 0: + result = db.all() + else: + result = db.search(peer.name.matches('(.*)(' + re.escape(search) + ')(.*)')) + result = sorted(result, key=lambda d: d[sort_t]) db.close() return result @@ -262,20 +303,101 @@ def get_conf_status(config_name): def get_conf_list(): conf = [] for i in os.listdir(wg_conf_path): - if not i.startswith('.'): - if ".conf" in i: - i = i.replace('.conf', '') - temp = {"conf": i, "status": get_conf_status(i), "public_key": get_conf_pub_key(i)} - if temp['status'] == "running": - temp['checked'] = 'checked' - else: - temp['checked'] = "" - conf.append(temp) + if is_match("^(.{1,}).(conf)$", i): + i = i.replace('.conf', '') + temp = {"conf": i, "status": get_conf_status(i), "public_key": get_conf_pub_key(i)} + if temp['status'] == "running": + temp['checked'] = 'checked' + else: + temp['checked'] = "" + conf.append(temp) if len(conf) > 0: conf = sorted(conf, key=itemgetter('conf')) return conf +def genKeys(): + gen = subprocess.check_output('wg genkey > private_key.txt && wg pubkey < private_key.txt > public_key.txt', + shell=True) + private = open('private_key.txt') + private_key = private.readline().strip() + public = open('public_key.txt') + public_key = public.readline().strip() + data = {"private_key": private_key, "public_key": public_key} + private.close() + public.close() + os.remove('private_key.txt') + os.remove('public_key.txt') + return data + + +def genPubKey(private_key): + pri_key_file = open('private_key.txt', 'w') + pri_key_file.write(private_key) + pri_key_file.close() + try: + check = subprocess.check_output("wg pubkey < private_key.txt > public_key.txt", shell=True) + public = open('public_key.txt') + public_key = public.readline().strip() + os.remove('private_key.txt') + os.remove('public_key.txt') + return {"status": 'success', "msg": "", "data": public_key} + except subprocess.CalledProcessError as exc: + os.remove('private_key.txt') + return {"status": 'failed', "msg": "Key is not the correct length or format", "data": ""} + + +def checkKeyMatch(private_key, public_key, config_name): + result = genPubKey(private_key) + if result['status'] == 'failed': + return result + else: + db = TinyDB('db/' + config_name + '.json') + peers = Query() + match = db.search(peers.id == result['data']) + if len(match) != 1 or result['data'] != public_key: + return {'status': 'failed', 'msg': 'Please check your private key, it does not match with the public key.'} + else: + return {'status': 'success'} + + +def checkAllowedIP(public_key, ip, config_name): + db = TinyDB('db/' + config_name + '.json') + peers = Query() + peer = db.search(peers.id == public_key) + if len(peer) != 1: + return {'status': 'failed', 'msg': 'Peer does not exist'} + else: + existed_ip = db.search((peers.id != public_key) & (peers.allowed_ip == ip)) + if len(existed_ip) != 0: + return {'status': 'failed', 'msg': "Allowed IP already taken by another peer."} + else: + return {'status': 'success'} + + +def checkIp(ip): + return is_match("((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4}", ip) + + +def cleanIp(ip): + return ip.replace(' ', '') + + +def cleanIpWithRange(ip): + return cleanIp(ip).split(',') + + +def checkIpWithRange(ip): + return is_match("((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|\/)){4}(0|8|16|24|32)(,|$)", ip) + + +def checkAllowedIPs(ip): + ip = cleanIpWithRange(ip) + for i in ip: + if not checkIpWithRange(i): return False + return True + + @app.before_request def auth_req(): conf = configparser.ConfigParser(strict=False) @@ -289,8 +411,11 @@ def auth_req(): request.endpoint != "signout" and \ request.endpoint != "auth" and \ "username" not in session: - print("User not loggedin - Attemped access: "+str(request.endpoint)) - session['message'] = "You need to sign in first!" + print("User not loggedin - Attemped access: " + str(request.endpoint)) + if request.endpoint != "index": + session['message'] = "You need to sign in first!" + else: + session['message'] = "" return redirect(url_for("signin")) else: if request.endpoint in ['signin', 'signout', 'auth', 'settings', 'update_acct', 'update_pwd', @@ -329,7 +454,9 @@ def settings(): required_auth = config.get("Server", "auth_req") return render_template('settings.html', conf=get_conf_list(), message=message, status=status, app_ip=config.get("Server", "app_ip"), app_port=config.get("Server", "app_port"), - required_auth=required_auth, wg_conf_path=config.get("Server", "wg_conf_path")) + required_auth=required_auth, wg_conf_path=config.get("Server", "wg_conf_path"), + peer_global_DNS=config.get("Peers", "peer_global_DNS"), + peer_endpoint_allowed_ip=config.get("Peers", "peer_endpoint_allowed_ip")) @app.route('/auth', methods=['POST']) @@ -350,6 +477,10 @@ def auth(): @app.route('/update_acct', methods=['POST']) def update_acct(): + if len(request.form['username']) == 0: + session['message'] = "Username cannot be empty." + session['message_status'] = "danger" + return redirect(url_for("settings")) config = configparser.ConfigParser(strict=False) config.read(dashboard_conf) config.set("Account", "username", request.form['username']) @@ -367,6 +498,45 @@ def update_acct(): return redirect(url_for("settings")) +@app.route('/update_peer_default_config', methods=['POST']) +def update_peer_default_config(): + config = configparser.ConfigParser(strict=False) + config.read(dashboard_conf) + if len(request.form['peer_endpoint_allowed_ip']) == 0 or len(request.form['peer_global_DNS']) == 0: + session['message'] = "Peer DNS or Peer Endpoint Allowed IP cannot be empty." + session['message_status'] = "danger" + return redirect(url_for("settings")) + # Check DNS Format + DNS = request.form['peer_global_DNS'] + DNS = cleanIp(DNS) + if not checkIp(DNS): + session['message'] = "Peer DNS Format Incorrect. Example: 1.1.1.1" + session['message_status'] = "danger" + return redirect(url_for("settings")) + + # Check Endpoint Allowed IPs + ip = request.form['peer_endpoint_allowed_ip'] + if not checkAllowedIPs(ip): + session[ + 'message'] = "Peer Endpoint Allowed IPs Format Incorrect. Example: 192.168.1.1/32 or 192.168.1.1/32,192.168.1.2/32" + session['message_status'] = "danger" + return redirect(url_for("settings")) + + config.set("Peers", "peer_endpoint_allowed_ip", ','.join(cleanIpWithRange(ip))) + config.set("Peers", "peer_global_DNS", request.form['peer_global_DNS']) + try: + config.write(open(dashboard_conf, "w")) + session['message'] = "DNS and Enpoint Allowed IP update successfully!" + session['message_status'] = "success" + config.clear() + return redirect(url_for("settings")) + except Exception: + session['message'] = "DNS and Enpoint Allowed IP update failed." + session['message_status'] = "danger" + config.clear() + return redirect(url_for("settings")) + + @app.route('/update_pwd', methods=['POST']) def update_pwd(): config = configparser.ConfigParser(strict=False) @@ -420,6 +590,22 @@ def update_wg_conf_path(): config.clear() os.system('bash wgd.sh restart') + +@app.route('/update_dashboard_sort', methods=['POST']) +def update_dashbaord_sort(): + config = configparser.ConfigParser(strict=False) + config.read(dashboard_conf) + data = request.get_json() + sort_tag = ['name', 'status', 'allowed_ip'] + if data['sort'] in sort_tag: + config.set("Server", "dashboard_sort", data['sort']) + else: + config.set("Server", "dashboard_sort", 'status') + config.write(open(dashboard_conf, "w")) + config.clear() + return "true" + + @app.route('/update_dashboard_refresh_interval', methods=['POST']) def update_dashboard_refresh_interval(): config = configparser.ConfigParser(strict=False) @@ -429,28 +615,30 @@ def update_dashboard_refresh_interval(): config.clear() return "true" + @app.route('/get_ping_ip', methods=['POST']) def get_ping_ip(): config = request.form['config'] db = TinyDB('db/' + config + '.json') html = "" for i in db.all(): - html += '