diff --git a/README.md b/README.md index 3865a62..e975649 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,9 @@ ## 📣 What's New: Version v2.2 - 🎉 **New Features** - - **QR Code**: You can add the private key in peer setting of your existed peer to create a QR code. Or just create a new one, dashboard will now be able to auto generate a private key and public key ;) Don't worry, all keys will be generated on your machine, and **will delete all key files after they got generated**. [❤️ in [#29](https://github.com/donaldzou/wireguard-dashboard/issues/15)] - - **Autostart on boot**: Added a tutorial on how to start the dashboard to on boot! Please read the [tutorial below](#autostart-wireguard-dashboard-on-boot). [❤️ in [#29](https://github.com/donaldzou/wireguard-dashboard/issues/29)] + - **QR Code:** You can add the private key in peer setting of your existed peer to create a QR code. Or just create a new one, dashboard will now be able to auto generate a private key and public key ;) Don't worry, all keys will be generated on your machine, and **will delete all key files after they got generated**. [❤️ in [#29](https://github.com/donaldzou/wireguard-dashboard/issues/29)] + - **Peer configuration file download:** Same as QR code, you now can download the peer configuration file, so you don't need to manually input all the details on the peer machine! [❤️ in [#40](https://github.com/donaldzou/wireguard-dashboard/issues/40)] + - **Autostart on boot:** Added a tutorial on how to start the dashboard to on boot! Please read the [tutorial below](#autostart-wireguard-dashboard-on-boot). [❤️ in [#29](https://github.com/donaldzou/wireguard-dashboard/issues/29)] - 🪚 **Bug Fixed** - When there are comments in the wireguard config file, will cause the dashboard to crash. - Used regex to search for config files. @@ -78,11 +79,11 @@ 1. **Download Wireguard Dashboard** ```shell - $ git clone -b v2.2 https://github.com/donaldzou/Wireguard-Dashboard.git + $ git clone -b v2.2 https://github.com/donaldzou/wireguard-dashboard.git 2. **Install Python Dependencies** ```shell - $ cd Wireguard-Dashboard/src + $ cd wireguard-dashboard/src $ python3 -m pip install -r requirements.txt ``` @@ -234,9 +235,7 @@ In the `src` folder, it contained a file called `wg-dashboard.service`, we can u $ sudo systemctl restart wg-dashboard.service # <-- To restart the service ``` -8. And now you can reboot your system, and use the command at step 6 to see if it will auto start after the reboot. If you have any questions or problem, please report a bug. - -⚠️ **For first time user please also read the next section.** +8. **And now you can reboot your system, and use the command at step 6 to see if it will auto start after the reboot. If you have any questions or problem, please report a bug.** ## ✂️ Dashboard Configuration @@ -261,9 +260,9 @@ Since version 2.0, Wireguard Dashboard will be using a configuration file called 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.** -#### Generating QR code +#### Generating QR code and peer configuration file (.conf) -Starting version 2.2, dashboard can now generate QR code for each peer. Here is a template of what each QR code encoded with: +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: ``` [Interface] @@ -282,10 +281,10 @@ Endpoint = 0.0.0.0:51820 | **`[Interface]`** | | | | `PrivateKey` | The private key of this peer | N/A | | `Address` | The `allowed_ips` of your peer | N/A | -| `DNS` | The DNS server your peer will use | `1.1.1.1` - Cloud flare DNS, you can switch it to Google DNS - `8.8.8.8`, or use your own DNS, you can edit it later in the WireGuard phone app. | +| `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. | | **`[Peer]`** | | | | `PublicKey` | The public key of your server | N/A | -| `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 | +| `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. | | `Endpoint` | Your wireguard server ip and port, the dashboard will search for your server's default interface's ip. | `:` | ## ❓ How to update the dashboard? @@ -309,7 +308,7 @@ Endpoint = 0.0.0.0:51820 ### ⚠️ **Update from v1.x.x** 1. Stop the dashboard if it is running. -2. You can use `git pull https://github.com/donaldzou/Wireguard-Dashboard.git v2.2` to get the new update inside `Wireguard-Dashboard` directory. +2. You can use `git pull https://github.com/donaldzou/wireguard-dashboard.git v2.2` to get the new update inside `Wireguard-Dashboard` directory. 3. Proceed **Step 2 & 3** in the [Install](#-install) step down below. ## 🔍 Screenshot diff --git a/src/dashboard.py b/src/dashboard.py index 2019e20..f71c2d9 100644 --- a/src/dashboard.py +++ b/src/dashboard.py @@ -18,7 +18,7 @@ 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 @@ -186,6 +186,7 @@ def get_conf_peers_data(config_name): db.insert({ "id": i['PublicKey'], "private_key": "", + "DNS":"1.1.1.1", "name": "", "total_receive": 0, "total_sent": 0, @@ -198,8 +199,12 @@ def get_conf_peers_data(config_name): }) else: # Update database since V2.2 + update_db = {} if "private_key" not in search[0]: - db.update({'private_key':''}, peers.id == i['PublicKey']) + update_db['private_key'] = '' + if "DNS" not in search[0]: + update_db['DNS'] = '1.1.1.1' + db.update(update_db, peers.id == i['PublicKey']) tic = time.perf_counter() get_latest_handshake(config_name, db, peers) @@ -271,6 +276,61 @@ def get_conf_list(): 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'} + @app.before_request def auth_req(): conf = configparser.ConfigParser(strict=False) @@ -285,7 +345,10 @@ def auth_req(): 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!" + 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', @@ -509,6 +572,7 @@ def get_conf(config_name): "public_key": get_conf_pub_key(config_name), "listen_port": get_conf_listen_port(config_name), "running_peer": get_conf_running_peer_number(config_name), + } if conf_data['status'] == "stopped": # return redirect('/') @@ -540,6 +604,8 @@ def switch(config_name): @app.route('/add_peer/', methods=['POST']) def add_peer(config_name): + db = TinyDB("db/" + config_name + ".json") + peers = Query() data = request.get_json() public_key = data['public_key'] allowed_ips = data['allowed_ips'] @@ -548,6 +614,8 @@ def add_peer(config_name): return config_name+" is not running." if public_key in keys: return "Public key already exist." + if len(db.search(peers.allowed_ip.matches(allowed_ips))) != 0: + return "Allowed IP already taken by another peer." else: status = "" try: @@ -555,12 +623,11 @@ def add_peer(config_name): "wg set " + config_name + " peer " + public_key + " allowed-ips " + allowed_ips, shell=True, stderr=subprocess.STDOUT) status = subprocess.check_output("wg-quick save " + config_name, shell=True, stderr=subprocess.STDOUT) get_conf_peers_data(config_name) - db = TinyDB("db/" + config_name + ".json") - peers = Query() - db.update({"name": data['name'], "private_key": data['private_key']}, peers.id == public_key) + db.update({"name": data['name'], "private_key": data['private_key'], "DNS": data['DNS']}, peers.id == public_key) db.close() return "true" except subprocess.CalledProcessError as exc: + db.close() return exc.output.strip() @@ -590,19 +657,47 @@ def remove_peer(config_name): return exc.output.strip() -@app.route('/save_peer_name/', methods=['POST']) -def save_peer_name(config_name): +@app.route('/save_peer_setting/', methods=['POST']) +def save_peer_setting(config_name): data = request.get_json() id = data['id'] name = data['name'] + private_key = data['private_key'] + DNS = data['DNS'] + allowed_ip = data['allowed_ip'] db = TinyDB("db/" + config_name + ".json") peers = Query() - db.update({"name": name}, peers.id == id) - db.close() - return id + " " + name + if len(db.search(peers.id == id)) == 1: + check_ip = checkAllowedIP(id, allowed_ip, config_name) + if private_key != "": + check_key = checkKeyMatch(private_key, id, config_name) + if check_key['status'] == "failed": + return jsonify(check_key) + if check_ip['status'] == "failed": + return jsonify(check_ip) + + try: + if allowed_ip == "": + allowed_ip = '""' + change_ip = subprocess.check_output('wg set '+config_name+" peer "+id+" allowed-ips "+allowed_ip, shell=True, stderr=subprocess.STDOUT) + save_change_ip = subprocess.check_output('wg-quick save '+ config_name, shell=True,stderr=subprocess.STDOUT) + if change_ip.decode("UTF-8") != "": + return jsonify({"status":"failed", "msg": change_ip.decode("UTF-8")}) + + db.update({"name": name, "private_key": private_key, "DNS": DNS}, peers.id == id) + db.close() + return jsonify({"status": "success", "msg": ""}) + except subprocess.CalledProcessError as exc: + return jsonify({"status":"failed", "msg": str(exc.output.decode("UTF-8").strip())}) -@app.route('/get_peer_name/', methods=['POST']) + + + else: + return jsonify({"status":"failed","msg":"This peer does not exist."}) + + +@app.route('/get_peer_data/', methods=['POST']) def get_peer_name(config_name): data = request.get_json() id = data['id'] @@ -610,39 +705,55 @@ def get_peer_name(config_name): peers = Query() result = db.search(peers.id == id) db.close() - return result[0]['name'] + data = {"name": result[0]['name'], "allowed_ip":result[0]['allowed_ip'], "DNS": result[0]['DNS'], "private_key": result[0]['private_key']} + return jsonify(data) @app.route('/generate_peer', methods=['GET']) def generate_peer(): - 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 jsonify(data) + return jsonify(genKeys()) @app.route('/generate_public_key', methods=['POST']) def generate_public_key(): data = request.get_json() private_key = data['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 jsonify({"status":'success', "msg":"", "data":public_key}) - except subprocess.CalledProcessError as exc: - os.remove('private_key.txt') - return jsonify({"status":'failed', "msg":"Key is not the correct length or format", "data":""}) + return jsonify(genPubKey(private_key)) + +@app.route('/check_key_match/', methods=['POST']) +def check_key_match(config_name): + data = request.get_json() + private_key = data['private_key'] + public_key = data['public_key'] + return jsonify(checkKeyMatch(private_key,public_key, config_name)) + +@app.route('/download/', methods=['GET']) +def download(config_name): + id = request.args.get('id') + db = TinyDB("db/" + config_name + ".json") + peers = Query() + print(id) + get_peer = db.search(peers.id == id) + print(get_peer) + if len(get_peer) == 1: + peer = get_peer[0] + if peer['private_key'] != "": + public_key = get_conf_pub_key(config_name) + listen_port = get_conf_listen_port(config_name) + endpoint = wg_ip+":"+listen_port + private_key = peer['private_key'] + allowed_ip = peer['allowed_ip'] + DNS = peer['DNS'] + name = "".join(peer['name'].split(' ')) + if name == "": name = public_key + def generate(private_key, allowed_ip, DNS, public_key, endpoint): + yield "[Interface]\nPrivateKey = "+private_key+"\nAddress = "+allowed_ip+"\nDNS = "+DNS+"\n\n[Peer]\nPublicKey = "+public_key+"\nAllowedIPs = 0.0.0.0/0\nEndpoint = "+endpoint + + return app.response_class(generate(private_key,allowed_ip,DNS, public_key,endpoint), mimetype='text/conf', headers={"Content-Disposition":"attachment;filename="+name+".conf"}) + else: + return redirect("/configuration/" + config_name) + + + + def init_dashboard(): # Set Default INI File diff --git a/src/requirements.txt b/src/requirements.txt index 1d4c08a..e6e826d 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,4 +1,5 @@ Flask==1.1.2 tinydb==4.3.0 ifcfg==0.21 -icmplib==2.1.1 \ No newline at end of file +icmplib==2.1.1 +flask-qrcode==3.0.0 \ No newline at end of file diff --git a/src/static/configuration.js b/src/static/configuration.js new file mode 100644 index 0000000..97b35d1 --- /dev/null +++ b/src/static/configuration.js @@ -0,0 +1,228 @@ +// Config Toggle +$("body").on("click", ".switch", function (){ + $(this).siblings($(".spinner-border")).css("display", "inline-block"); + $(this).remove() + location.replace("/switch/"+$(this).attr('id')); +}) + +// Generating Keys +function generate_key(){ + $.ajax({ + "url": "/generate_peer", + "method": "GET", + }).done(function(res){ + $("#private_key").val(res.private_key) + $("#public_key").val(res.public_key) + $("#add_peer_alert").addClass("d-none"); + $("#re_generate_key i").removeClass("rotating") + }) +} + +function generate_public_key(){ + $.ajax({ + "url": "/generate_public_key", + "method": "POST", + "headers":{"Content-Type": "application/json"}, + "data": JSON.stringify({"private_key": $("#private_key").val()}) + }).done(function(res){ + if(res['status'] === "failed"){ + $("#add_peer_alert").html(res['msg']+$("#add_peer_alert").html()); + $("#add_peer_alert").removeClass("d-none"); + }else{ + $("#add_peer_alert").addClass("d-none"); + } + $("#public_key").val(res['data']) + $("#re_generate_key i").removeClass("rotating") + }) +} + +// Add Peer +$("#private_key").change(function(){ + if ($("#private_key").val().length > 0){ + $("#re_generate_key i").addClass("rotating") + generate_public_key() + }else{ + $("#public_key").removeAttr("disabled") + $("#public_key").val("") + } +}) + +$('#add_modal').on('show.bs.modal', function (event) { + generate_key() +}) + +$("#re_generate_key").click(function (){ + $("#public_key").attr("disabled","disabled") + $("#re_generate_key i").addClass("rotating") + generate_key() +}) + +$("#save_peer").click(function(){ + if ($("#allowed_ips") !== "" && $("#public_key") !== ""){ + var conf = $(this).attr('conf_id') + $.ajax({ + method: "POST", + url: "/add_peer/"+conf, + headers:{ + "Content-Type": "application/json" + }, + data: JSON.stringify({ + "private_key":$("#private_key").val(), + "public_key":$("#public_key").val(), + "allowed_ips": $("#allowed_ips").val(), + "name":$("#new_add_name").val(), + "DNS": $("#DNS").val() + }), + success: function (response){ + if(response != "true"){ + $("#add_peer_alert").html(response+$("#add_peer_alert").html()); + $("#add_peer_alert").removeClass("d-none"); + } + else{ + location.reload(); + } + } + }) + } +}) +var qrcodeModal = new bootstrap.Modal(document.getElementById('qrcode_modal'), { + keyboard: false +}) + +// QR Code +$("body").on("click", ".btn-qrcode-peer", function (){ + qrcodeModal.toggle(); + $("#qrcode_img").attr('src', $(this).attr('img_src')) +}) + +// Delete Peer Modal +var deleteModal = new bootstrap.Modal(document.getElementById('delete_modal'), { + keyboard: false +}); + +$("body").on("click", ".btn-delete-peer", function(){ + var peer_id = $(this).attr("id"); + $("#delete_peer").attr("peer_id", peer_id); + deleteModal.toggle(); +}) + +$("#delete_peer").click(function(){ + var peer_id = $(this).attr("peer_id"); + var config = $(this).attr("conf_id"); + $.ajax({ + method: "POST", + url: "/remove_peer/"+config, + headers:{ + "Content-Type": "application/json" + }, + data: JSON.stringify({"action": "delete", "peer_id": peer_id}), + success: function (response){ + if(response !== "true"){ + $("#remove_peer_alert").html(response+$("#add_peer_alert").html()); + $("#remove_peer_alert").removeClass("d-none"); + } + else{ + deleteModal.toggle(); + load_data(); + $('#alertToast').toast('show'); + $('#alertToast .toast-body').html("Peer deleted!"); + } + } + }) +}); + +// Peer Setting Modal +var settingModal = new bootstrap.Modal(document.getElementById('setting_modal'), { + keyboard: false +}) +$("body").on("click", ".btn-setting-peer", function(){ + settingModal.toggle(); + var peer_id = $(this).attr("id"); + $("#save_peer_setting").attr("peer_id", peer_id); + $.ajax({ + method: "POST", + url: "/get_peer_data/"+$("#setting_modal").attr("conf_id"), + headers:{ + "Content-Type": "application/json" + }, + data: JSON.stringify({"id": peer_id}), + success: function(response){ + let peer_name = ((response['name'] === "") ? "Untitled Peer" : response['name']); + $("#setting_modal .peer_name").html(peer_name); + $("#setting_modal #peer_name_textbox").val(peer_name) + $("#setting_modal #peer_private_key_textbox").val(response['private_key']) + $("#setting_modal #peer_DNS_textbox").val(response['DNS']) + $("#setting_modal #peer_allowed_ip_textbox").val(response['allowed_ip']) + } + }) +}); + +$('#setting_modal').on('hidden.bs.modal', function (event) { + $("#setting_peer_alert").addClass("d-none"); +}) + +$("#peer_private_key_textbox").change(function(){ + if ($(this).val().length > 0){ + $.ajax({ + "url": "/check_key_match/"+$("#save_peer_setting").attr("conf_id"), + "method": "POST", + "headers":{"Content-Type": "application/json"}, + "data": JSON.stringify({ + "private_key": $("#peer_private_key_textbox").val(), + "public_key": $("#save_peer_setting").attr("peer_id") + }) + }).done(function(res){ + if(res['status'] == "failed"){ + $("#setting_peer_alert").html(res['msg']); + $("#setting_peer_alert").removeClass("d-none"); + }else{ + $("#setting_peer_alert").addClass("d-none"); + } + }) + } +}) + +$("#save_peer_setting").click(function (){ + $(this).attr("disabled","disabled") + $(this).html("Saving...") + if ($("#peer_DNS_textbox").val() !== "" && $("#peer_allowed_ip_textbox").val() !== ""){ + var peer_id = $(this).attr("peer_id"); + var conf_id = $(this).attr("conf_id"); + $.ajax({ + method: "POST", + url: "/save_peer_setting/"+conf_id, + headers:{ + "Content-Type": "application/json" + }, + data: JSON.stringify({ + id: peer_id, + name: $("#peer_name_textbox").val(), + DNS: $("#peer_DNS_textbox").val(), + private_key: $("#peer_private_key_textbox").val(), + allowed_ip: $("#peer_allowed_ip_textbox").val() + }), + success: function (response){ + if (response['status'] === "failed"){ + $("#setting_peer_alert").html(response['msg']); + $("#setting_peer_alert").removeClass("d-none"); + }else{ + settingModal.toggle(); + load_data(); + $('#alertToast').toast('show'); + $('#alertToast .toast-body').html("Peer Saved!"); + } + $("#save_peer_setting").removeAttr("disabled") + $("#save_peer_setting").html("Save") + } + }) + } + + +}) + +$(".peer_private_key_textbox_switch").click(function (){ + let mode = (($("#peer_private_key_textbox").attr('type') === 'password') ? "text":"password") + let icon = (($("#peer_private_key_textbox").attr('type') === 'password') ? "bi bi-eye-slash-fill":"bi bi-eye-fill") + $("#peer_private_key_textbox").attr('type',mode) + $(".peer_private_key_textbox_switch i").removeClass().addClass(icon) +}) \ No newline at end of file diff --git a/src/static/dashboard.css b/src/static/dashboard.css index 3d7c51f..cadf23c 100644 --- a/src/static/dashboard.css +++ b/src/static/dashboard.css @@ -130,8 +130,11 @@ body { .btn-control{ border: none !important; - padding: 0; - padding-right: 0.5rem; + padding: 0 1rem 0 0; +} + +.share_peer_btn_group .btn-control{ + padding: 0 0 0 1rem; } .btn-control:hover{ @@ -146,6 +149,10 @@ body { color:#007bff } +.btn-download-peer:hover{ + color: #17a2b8; +} + .login-container{ padding: 2rem; } @@ -231,4 +238,12 @@ main{ -ms-animation: rotating 0.75s linear infinite; -o-animation: rotating 0.75s linear infinite; animation: rotating 0.75s linear infinite; +} + +.peer_private_key_textbox_switch{ + position: absolute; + right: 2rem; + transform: translateY(-28px); + font-size: 1.2rem; + cursor: pointer; } \ No newline at end of file diff --git a/src/templates/configuration.html b/src/templates/configuration.html index 0e0114c..0c56d55 100644 --- a/src/templates/configuration.html +++ b/src/templates/configuration.html @@ -8,7 +8,7 @@