Merge pull request #1 from h44z/master

Merge upstream changes
This commit is contained in:
Sundog Jones 2021-04-05 15:45:02 -07:00 committed by GitHub
commit c30bdbfa53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 2136 additions and 1423 deletions

View File

@ -1,6 +1,6 @@
# WireGuard Portal on Raspberry Pi # WireGuard Portal on Raspberry Pi
This readme only contains a detailed explanation of how to setup the WireGuard Portal service on a raspberry pi (>= 3). This readme only contains a detailed explanation of how to set up the WireGuard Portal service on a raspberry pi (>= 3).
## Setup ## Setup

View File

@ -29,6 +29,7 @@ It also supports LDAP (Active Directory or OpenLDAP) as authentication provider.
* Responsive template * Responsive template
* One single binary * One single binary
* Can be used with existing WireGuard setups * Can be used with existing WireGuard setups
* Support for multiple WireGuard interfaces
![Screenshot](screenshot.png) ![Screenshot](screenshot.png)
@ -54,14 +55,21 @@ services:
ports: ports:
- '8123:8123' - '8123:8123'
environment: environment:
# WireGuard Settings
- WG_DEVICES=wg0
- WG_DEFAULT_DEVICE=wg0
- WG_CONFIG_PATH=/etc/wireguard
# Core Settings
- EXTERNAL_URL=https://vpn.company.com - EXTERNAL_URL=https://vpn.company.com
- WEBSITE_TITLE=WireGuard VPN - WEBSITE_TITLE=WireGuard VPN
- COMPANY_NAME=Your Company Name - COMPANY_NAME=Your Company Name
- MAIL_FROM=WireGuard VPN <noreply+wireguard@company.com>
- ADMIN_USER=admin@domain.com - ADMIN_USER=admin@domain.com
- ADMIN_PASS=supersecret - ADMIN_PASS=supersecret
# Mail Settings
- MAIL_FROM=WireGuard VPN <noreply+wireguard@company.com>
- EMAIL_HOST=10.10.10.10 - EMAIL_HOST=10.10.10.10
- EMAIL_PORT=25 - EMAIL_PORT=25
# LDAP Settings
- LDAP_ENABLED=true - LDAP_ENABLED=true
- LDAP_URL=ldap://srv-ad01.company.local:389 - LDAP_URL=ldap://srv-ad01.company.local:389
- LDAP_BASEDN=DC=COMPANY,DC=LOCAL - LDAP_BASEDN=DC=COMPANY,DC=LOCAL
@ -71,7 +79,7 @@ services:
``` ```
Please note that mapping ```/etc/wireguard``` to ```/etc/wireguard``` inside the docker, will erase your host's current configuration. Please note that mapping ```/etc/wireguard``` to ```/etc/wireguard``` inside the docker, will erase your host's current configuration.
If needed, please make sure to backup your files from ```/etc/wireguard```. If needed, please make sure to backup your files from ```/etc/wireguard```.
For a full list of configuration options take a look at the source file [internal/common/configuration.go](internal/common/configuration.go#L57). For a full list of configuration options take a look at the source file [internal/server/configuration.go](internal/server/configuration.go#L56).
### Standalone ### Standalone
For a standalone application, use the Makefile provided in the repository to build the application. For a standalone application, use the Makefile provided in the repository to build the application.
@ -90,6 +98,7 @@ A detailed description for using this software with a raspberry pi can be found
* Generation or application of any `iptables` or `nftables` rules * Generation or application of any `iptables` or `nftables` rules
* Setting up or changing IP-addresses of the WireGuard interface on operating systems other than linux * Setting up or changing IP-addresses of the WireGuard interface on operating systems other than linux
* Importing private keys of an existing WireGuard setup
## Application stack ## Application stack

View File

@ -80,4 +80,24 @@ pre{background:#f7f7f9}iframe{overflow:hidden;border:none}@media (min-width: 768
.form-group.required label:after { .form-group.required label:after {
content:"*"; content:"*";
color:red; color:red;
}
a.advanced-settings:before {
content: "Hide";
}
a.advanced-settings.collapsed:before {
content: "Show";
}
.form-group.global-config label:after, .custom-control.global-config label:after {
content: "g";
color: #0057bb;
font-size: xx-small;
top: -5px;
position: absolute;
}
.text-blue {
color: #0057bb;
} }

View File

@ -0,0 +1,3 @@
.navbar {
padding: 0.5rem 1rem;
}

View File

@ -25,6 +25,11 @@
} }
}); });
}); });
$(function() {
$('select.device-selector').change(function() {
this.form.submit();
});
});
})(jQuery); // End of use strict })(jQuery); // End of use strict

View File

@ -20,16 +20,17 @@
<h2>Enter valid LDAP user email addresses to quickly create new accounts.</h2> <h2>Enter valid LDAP user email addresses to quickly create new accounts.</h2>
{{template "prt_flashes.html" .}} {{template "prt_flashes.html" .}}
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
<div class="form-row"> <div class="form-row">
<div class="form-group required col-md-12"> <div class="form-group required col-md-12">
<label for="inputEmail">Email Addresses</label> <label for="inputEmail">Email Addresses</label>
<input type="text" name="email" class="form-control" id="inputEmail" value="{{.FormData.Emails}}"> <input type="text" name="email" class="form-control" id="inputEmail" value="{{.FormData.Emails}}" required>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group required col-md-12"> <div class="form-group required col-md-12">
<label for="inputIdentifier">Client Friendly Name (will be added as suffix to the name of the user)</label> <label for="inputIdentifier">Client Friendly Name (will be added as suffix to the name of the user)</label>
<input type="text" name="identifier" class="form-control" id="inputIdentifier" value="{{.FormData.Identifier}}"> <input type="text" name="identifier" class="form-control" id="inputIdentifier" value="{{.FormData.Identifier}}" required>
</div> </div>
</div> </div>

View File

@ -13,33 +13,39 @@
<body id="page-top" class="d-flex flex-column min-vh-100"> <body id="page-top" class="d-flex flex-column min-vh-100">
{{template "prt_nav.html" .}} {{template "prt_nav.html" .}}
<div class="container mt-5"> <div class="container mt-5">
{{template "prt_flashes.html" .}}
<!-- server mode -->
{{if eq .Device.Type "server"}}
{{if .Peer.IsNew}} {{if .Peer.IsNew}}
<h1>Create a new client</h1> <h1>Create a new client</h1>
{{else}} {{else}}
<h1>Edit client <strong>{{.Peer.Identifier}}</strong></h1> <h1>Edit client: <strong>{{.Peer.Identifier}}</strong></h1>
{{end}} {{end}}
{{template "prt_flashes.html" .}}
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
<input type="hidden" name="uid" value="{{.Peer.UID}}"> <input type="hidden" name="uid" value="{{.Peer.UID}}">
<input type="hidden" name="devicetype" value="{{.Device.Type}}">
<input type="hidden" name="device" value="{{.Device.DeviceName}}">
<input type="hidden" name="endpoint" value="{{.Peer.Endpoint}}">
{{if .EditableKeys}} {{if .EditableKeys}}
<div class="form-row"> <div class="form-row">
<div class="form-group col-md-12"> <div class="form-group col-md-12">
<label for="inputServerPrivateKey">Private Key</label> <label for="server_PrivateKey">Private Key</label>
<input type="text" name="privkey" class="form-control" id="inputServerPrivateKey" value="{{.Peer.PrivateKey}}"> <input type="text" name="privkey" class="form-control" id="server_PrivateKey" value="{{.Peer.PrivateKey}}">
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group required col-md-12"> <div class="form-group required col-md-12">
<label for="inputServerPublicKey">Public Key</label> <label for="server_PublicKey">Public Key</label>
<input type="text" name="pubkey" class="form-control" id="inputServerPublicKey" value="{{.Peer.PublicKey}}"> <input type="text" name="pubkey" class="form-control" id="server_PublicKey" value="{{.Peer.PublicKey}}" required>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group col-md-12"> <div class="form-group col-md-12">
<label for="inputServerPresharedKey">Preshared Key</label> <label for="server_PresharedKey">Preshared Key</label>
<input type="text" name="presharedkey" class="form-control" id="inputServerPresharedKey" value="{{.Peer.PresharedKey}}"> <input type="text" name="presharedkey" class="form-control" id="server_PresharedKey" value="{{.Peer.PresharedKey}}" required>
</div> </div>
</div> </div>
{{else}} {{else}}
@ -47,48 +53,64 @@
<input type="hidden" name="presharedkey" value="{{.Peer.PresharedKey}}"> <input type="hidden" name="presharedkey" value="{{.Peer.PresharedKey}}">
<div class="form-row"> <div class="form-row">
<div class="form-group col-md-12"> <div class="form-group col-md-12">
<label for="inputServerPublicKey">Public Key</label> <label for="server_ro_PublicKey">Public Key</label>
<input type="text" name="pubkey" readonly class="form-control" id="inputServerPublicKey" value="{{.Peer.PublicKey}}"> <input type="text" name="pubkey" readonly class="form-control" id="server_ro_PublicKey" value="{{.Peer.PublicKey}}">
</div> </div>
</div> </div>
{{end}} {{end}}
<div class="form-row"> <div class="form-row">
<div class="form-group required col-md-12"> <div class="form-group required col-md-12">
<label for="inputIdentifier">Client Friendly Name</label> <label for="server_Identifier">Client Friendly Name</label>
<input type="text" name="identifier" class="form-control" id="inputIdentifier" value="{{.Peer.Identifier}}"> <input type="text" name="identifier" class="form-control" id="server_Identifier" value="{{.Peer.Identifier}}" required>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group required col-md-12"> <div class="form-group required col-md-12">
<label for="inputEmail">Client Email Address</label> <label for="server_Email">Client Email Address</label>
<input type="email" name="mail" class="form-control" id="inputEmail" value="{{.Peer.Email}}"> <input type="email" name="mail" class="form-control" id="server_Email" value="{{.Peer.Email}}" required>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group required col-md-12"> <div class="form-group required col-md-12">
<label for="inputIP">Client IP Address</label> <label for="server_IP">Client IP Address</label>
<input type="text" name="ip" class="form-control" id="inputIP" value="{{.Peer.IPsStr}}"> <input type="text" name="ip" class="form-control" id="server_IP" value="{{.Peer.IPsStr}}" required>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group required col-md-12"> <div class="form-group col-md-12 global-config">
<label for="inputAllowedIP">Allowed IPs</label> <label for="server_AllowedIP">Allowed IPs</label>
<input type="text" name="allowedip" class="form-control" id="inputAllowedIP" value="{{.Peer.AllowedIPsStr}}"> <input type="text" name="allowedip" class="form-control" id="server_AllowedIP" value="{{.Peer.AllowedIPsStr}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12 global-config">
<label for="server_DNS">Client DNS Servers</label>
<input type="text" name="dns" class="form-control" id="server_DNS" value="{{.Peer.DNSStr}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6 global-config">
<label for="server_PersistentKeepalive">Persistent Keepalive (0 = off)</label>
<input type="number" name="keepalive" class="form-control" id="server_PersistentKeepalive" placeholder="16" value="{{.Peer.PersistentKeepalive}}">
</div>
<div class="form-group col-md-6 global-config">
<label for="server_MTU">Client MTU (0 = default)</label>
<input type="number" name="mtu" class="form-control" id="server_MTU" placeholder="" value="{{.Peer.Mtu}}">
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group col-md-12"> <div class="form-group col-md-12">
<div class="custom-control custom-switch"> <div class="custom-control custom-switch">
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="inputDisabled" {{if .Peer.DeactivatedAt}}checked{{end}}> <input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="server_Disabled" {{if .Peer.DeactivatedAt}}checked{{end}}>
<label class="custom-control-label" for="inputDisabled"> <label class="custom-control-label" for="server_Disabled">
Disabled Disabled
</label> </label>
</div> </div>
<div class="custom-control custom-switch"> <div class="custom-control custom-switch">
<input class="custom-control-input" name="ignorekeepalive" type="checkbox" value="true" id="inputIgnoreKeepalive" {{if .Peer.IgnorePersistentKeepalive}}checked{{end}}> <input class="custom-control-input" name="ignoreglobalsettings" type="checkbox" value="true" id="server_IgnoreGlobalSettings" {{if .Peer.IgnoreGlobalSettings}}checked{{end}}>
<label class="custom-control-label" for="inputIgnoreKeepalive"> <label class="custom-control-label" for="server_IgnoreGlobalSettings">
Ignore persistent keepalive Ignore global settings (<span class="text-blue">g</span>)
</label> </label>
</div> </div>
</div> </div>
@ -98,6 +120,80 @@
<button type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary">Save</button>
<a href="/admin" class="btn btn-secondary">Cancel</a> <a href="/admin" class="btn btn-secondary">Cancel</a>
</form> </form>
{{end}}
<!-- client mode -->
{{if eq .Device.Type "client"}}
{{if .Peer.IsNew}}
<h1>Create a new remote endpoint</h1>
{{else}}
<h1>Edit remote endpoint: <strong>{{.Peer.Identifier}}</strong></h1>
{{end}}
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
<input type="hidden" name="uid" value="{{.Peer.UID}}">
<input type="hidden" name="mail" value="{{.AdminEmail}}">
<input type="hidden" name="devicetype" value="{{.Device.Type}}">
<input type="hidden" name="device" value="{{.Device.DeviceName}}">
<input type="hidden" name="privkey" value="{{.Peer.PrivateKey}}">
<div class="form-row">
<div class="form-group required col-md-12">
<label for="client_Identifier">Endpoint Friendly Name</label>
<input type="text" name="identifier" class="form-control" id="client_Identifier" value="{{.Peer.Identifier}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group required col-md-12">
<label for="client_Endpoint">Endpoint Address</label>
<input type="text" name="endpoint" class="form-control" id="client_Endpoint" value="{{.Peer.Endpoint}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group required col-md-12">
<label for="client_PublicKey">Endpoint Public Key</label>
<input type="text" name="pubkey" class="form-control" id="client_PublicKey" value="{{.Peer.PublicKey}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="client_PresharedKey">Preshared Key</label>
<input type="text" name="presharedkey" class="form-control" id="client_PresharedKey" value="{{.Peer.PresharedKey}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="client_AllowedIP">Allowed IPs</label>
<input type="text" name="allowedip" class="form-control" id="client_AllowedIP" value="{{.Peer.AllowedIPsStr}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="client_PersistentKeepalive">Persistent Keepalive (0 = off)</label>
<input type="number" name="keepalive" class="form-control" id="client_PersistentKeepalive" placeholder="16" value="{{.Peer.PersistentKeepalive}}">
</div>
<div class="form-group col-md-6">
<label for="client_IP">Ping-Check IP Address</label>
<input type="text" name="ip" class="form-control" id="client_IP" value="{{.Peer.IPsStr}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<div class="custom-control custom-switch">
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="client_Disabled" {{if .Peer.DeactivatedAt}}checked{{end}}>
<label class="custom-control-label" for="client_Disabled">
Disabled
</label>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Save</button>
<a href="/admin" class="btn btn-secondary">Cancel</a>
</form>
{{end}}
</div> </div>
{{template "prt_footer.html" .}} {{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script> <script src="/js/jquery.min.js"></script>

View File

@ -16,98 +16,240 @@
<h1>Edit interface <strong>{{.Device.DeviceName}}</strong></h1> <h1>Edit interface <strong>{{.Device.DeviceName}}</strong></h1>
{{template "prt_flashes.html" .}} {{template "prt_flashes.html" .}}
<form method="post" enctype="multipart/form-data"> <ul class="nav nav-tabs">
<input type="hidden" name="device" value="{{.Device.DeviceName}}"> <li class="nav-item">
<h3>Server's interface configuration</h3> <a class="nav-link {{if eq .Device.Type "server"}}active{{end}}" data-toggle="tab" href="#server">Server Mode</a>
{{if .EditableKeys}} </li>
<div class="form-row"> <li class="nav-item">
<div class="form-group required col-md-12"> <a class="nav-link {{if eq .Device.Type "client"}}active{{end}}" data-toggle="tab" href="#client">Client Mode</a>
<label for="inputServerPrivateKey">Private Key</label> </li>
<input type="text" name="privkey" class="form-control" id="inputServerPrivateKey" value="{{.Device.PrivateKey}}"> </ul>
</div>
</div> <div id="configContent" class="tab-content">
<div class="form-row"> <!-- server mode -->
<div class="form-group required col-md-12"> <div class="tab-pane fade {{if eq .Device.Type "server"}}active show{{end}}" id="server">
<label for="inputServerPublicKey">Public Key</label> <form method="post" enctype="multipart/form-data">
<input type="text" name="pubkey" class="form-control" id="inputServerPublicKey" value="{{.Device.PublicKey}}"> <input type="hidden" name="_csrf" value="{{.Csrf}}">
</div> <input type="hidden" name="device" value="{{.Device.DeviceName}}">
</div> <input type="hidden" name="devicetype" value="server">
{{else}} <h3>Server's interface configuration</h3>
<input type="hidden" name="privkey" value="{{.Device.PrivateKey}}"> <div class="form-row">
<div class="form-row"> <div class="form-group col-md-12">
<div class="form-group col-md-12"> <label for="server_DisplayName">Display Name</label>
<label for="inputServerPublicKey">Public Key</label> <input type="text" name="displayname" class="form-control" id="server_DisplayName" value="{{.Device.DisplayName}}">
<input type="text" name="pubkey" readonly class="form-control" id="inputServerPublicKey" value="{{.Device.PublicKey}}"> </div>
</div> </div>
</div> {{if .EditableKeys}}
{{end}} <div class="form-row">
<div class="form-row"> <div class="form-group required col-md-12">
<div class="form-group required col-md-6"> <label for="server_PrivateKey">Private Key</label>
<label for="inputListenPort">Listen port</label> <input type="text" name="privkey" class="form-control" id="server_PrivateKey" value="{{.Device.PrivateKey}}" required>
<input type="number" name="port" class="form-control" id="inputListenPort" placeholder="51820" value="{{.Device.ListenPort}}"> </div>
</div> </div>
<div class="form-group required col-md-6"> <div class="form-row">
<label for="inputIPs">Server IP address</label> <div class="form-group required col-md-12">
<input type="text" name="ip" class="form-control" id="inputIPs" placeholder="10.6.6.1/24" value="{{.Device.IPsStr}}"> <label for="server_PublicKey">Public Key</label>
</div> <input type="text" name="pubkey" class="form-control" id="server_PublicKey" value="{{.Device.PublicKey}}" required>
</div> </div>
<h3>Client's global configuration</h3> </div>
<div class="form-row"> {{else}}
<div class="form-group required col-md-12"> <input type="hidden" name="privkey" value="{{.Device.PrivateKey}}">
<label for="inputPublicEndpoint">Public Endpoint for Clients</label> <div class="form-row">
<input type="text" name="endpoint" class="form-control" id="inputPublicEndpoint" placeholder="vpn.company.com:51820" value="{{.Device.Endpoint}}"> <div class="form-group col-md-12">
</div> <label for="server_ro_PublicKey">Public Key</label>
</div> <input type="text" name="pubkey" readonly class="form-control" id="server_ro_PublicKey" value="{{.Device.PublicKey}}">
<div class="form-row"> </div>
<div class="form-group col-md-6"> </div>
<label for="inputDNS">DNS Servers</label> {{end}}
<input type="text" name="dns" class="form-control" id="inputDNS" placeholder="1.1.1.1" value="{{.Device.DNSStr}}"> <div class="form-row">
</div> <div class="form-group required col-md-6">
<div class="form-group col-md-6"> <label for="server_ListenPort">Listen port</label>
<label for="inputAllowedIP">Default allowed IPs</label> <input type="number" name="port" class="form-control" id="server_ListenPort" placeholder="51820" value="{{.Device.ListenPort}}" required>
<input type="text" name="allowedip" class="form-control" id="inputAllowedIP" placeholder="10.6.6.0/24" value="{{.Device.AllowedIPsStr}}"> </div>
</div> <div class="form-group required col-md-6">
</div> <label for="server_IPs">Server IP address</label>
<div class="form-row"> <input type="text" name="ip" class="form-control" id="server_IPs" placeholder="10.6.6.1/24" value="{{.Device.IPsStr}}" required>
<div class="form-group col-md-6"> </div>
<label for="inputMTU">Global MTU</label> </div>
<input type="number" name="mtu" class="form-control" id="inputMTU" placeholder="0" value="{{.Device.Mtu}}"> <h3>Client's global configuration (<span class="text-blue">g</span>)</h3>
</div> <div class="form-row">
<div class="form-group col-md-6"> <div class="form-group required col-md-12">
<label for="inputPersistentKeepalive">Persistent Keepalive</label> <label for="server_PublicEndpoint">Public Endpoint for Clients</label>
<input type="number" name="keepalive" class="form-control" id="inputPersistentKeepalive" placeholder="16" value="{{.Device.PersistentKeepalive}}"> <input type="text" name="endpoint" class="form-control" id="server_PublicEndpoint" placeholder="vpn.company.com:51820" value="{{.Device.DefaultEndpoint}}" required>
</div> </div>
</div> </div>
<h3>Interface configuration hooks</h3> <div class="form-row">
<div class="form-row"> <div class="form-group col-md-6">
<div class="form-group col-md-12"> <label for="server_DNS">DNS Servers</label>
<label for="inputPreUp">Pre Up</label> <input type="text" name="dns" class="form-control" id="server_DNS" placeholder="1.1.1.1" value="{{.Device.DNSStr}}">
<input type="text" name="preup" class="form-control" id="inputPreUp" value="{{.Device.PreUp}}"> </div>
</div> <div class="form-group col-md-6">
</div> <label for="server_AllowedIP">Default allowed IPs</label>
<div class="form-row"> <input type="text" name="allowedip" class="form-control" id="server_AllowedIP" placeholder="10.6.6.0/24" value="{{.Device.DefaultAllowedIPsStr}}">
<div class="form-group col-md-12"> </div>
<label for="inputPostUp">Post Up</label> </div>
<input type="text" name="postup" class="form-control" id="inputPostUp" value="{{.Device.PostUp}}"> <div class="form-row">
</div> <div class="form-group col-md-6">
</div> <label for="server_MTU">MTU (also used for the server interface, 0 = default)</label>
<div class="form-row"> <input type="number" name="mtu" class="form-control" id="server_MTU" placeholder="" value="{{.Device.Mtu}}">
<div class="form-group col-md-12"> </div>
<label for="inputPreDown">Pre Down</label> <div class="form-group col-md-6">
<input type="text" name="predown" class="form-control" id="inputPreDown" value="{{.Device.PreDown}}"> <label for="server_PersistentKeepalive">Persistent Keepalive (0 = off)</label>
</div> <input type="number" name="keepalive" class="form-control" id="server_PersistentKeepalive" placeholder="16" value="{{.Device.DefaultPersistentKeepalive}}">
</div> </div>
<div class="form-row"> </div>
<div class="form-group col-md-12"> <h3>Interface configuration hooks</h3>
<label for="inputPostDown">Post Down</label> <div class="form-row">
<input type="text" name="postdown" class="form-control" id="inputPostDown" value="{{.Device.PostDown}}"> <div class="form-group col-md-12">
</div> <label for="server_PreUp">Pre Up</label>
<input type="text" name="preup" class="form-control" id="server_PreUp" value="{{.Device.PreUp}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="server_PostUp">Post Up</label>
<input type="text" name="postup" class="form-control" id="server_PostUp" value="{{.Device.PostUp}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="server_PreDown">Pre Down</label>
<input type="text" name="predown" class="form-control" id="server_PreDown" value="{{.Device.PreDown}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="server_PostDown">Post Down</label>
<input type="text" name="postdown" class="form-control" id="server_PostDown" value="{{.Device.PostDown}}">
</div>
</div>
<div class="form-row">
<div class="d-flex align-items-center">
<a href="#" class="advanced-settings btn btn-link collapsed" data-toggle="collapse" data-target="#collapseAdvancedServer" aria-expanded="false" aria-controls="collapseAdvancedServer">
Advanced Settings
</a>
</div>
</div>
<div id="collapseAdvancedServer" class="collapse" aria-labelledby="collapseAdvancedServer">
<div class="form-row">
<div class="form-group col-md-6">
<label for="server_FirewallMark">Firewall Mark (0 = default or off)</label>
<input type="number" name="firewallmark" class="form-control" id="server_FirewallMark" placeholder="" value="{{.Device.FirewallMark}}">
</div>
<div class="form-group col-md-6">
<label for="server_RoutingTable">Routing Table (empty = default or auto)</label>
<input type="text" name="routingtable" class="form-control" id="server_RoutingTable" placeholder="auto" value="{{.Device.RoutingTable}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<div class="custom-control custom-switch">
<input class="custom-control-input" name="saveconfig" type="checkbox" value="true" id="server_SaveConfig" {{if .Peer.SaveConfig}}checked{{end}}>
<label class="custom-control-label" for="server_SaveConfig">
Save Configuration (if interface was edited via WireGuard configuration tool)
</label>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Save</button>
<a href="/admin" class="btn btn-secondary">Cancel</a>
<a href="/admin/device/applyglobals" class="btn btn-dark float-right">Apply Global Settings (<span class="text-blue">g</span>) to clients</a>
</form>
</div> </div>
<button type="submit" class="btn btn-primary">Save</button> <!-- client mode -->
<a href="/admin" class="btn btn-secondary">Cancel</a> <div class="tab-pane fade {{if eq .Device.Type "client"}}active show{{end}}" id="client">
<a href="/admin/device/applyglobals" class="btn btn-dark float-right">Apply Allowed IP's to clients</a> <form method="post" enctype="multipart/form-data">
</form> <input type="hidden" name="_csrf" value="{{.Csrf}}">
<input type="hidden" name="device" value="{{.Device.DeviceName}}">
<input type="hidden" name="devicetype" value="client">
<h3>Client's interface configuration</h3>
<div class="form-row">
<div class="form-group col-md-12">
<label for="client_DisplayName">Display Name</label>
<input type="text" name="displayname" class="form-control" id="client_DisplayName" value="{{.Device.DisplayName}}">
</div>
</div>
{{if .EditableKeys}}
<div class="form-row">
<div class="form-group required col-md-12">
<label for="client_PrivateKey">Private Key</label>
<input type="text" name="privkey" class="form-control" id="client_PrivateKey" value="{{.Device.PrivateKey}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group required col-md-12">
<label for="client_PublicKey">Public Key</label>
<input type="text" name="pubkey" class="form-control" id="client_PublicKey" value="{{.Device.PublicKey}}" required>
</div>
</div>
{{else}}
<input type="hidden" name="privkey" value="{{.Device.PrivateKey}}">
<div class="form-row">
<div class="form-group col-md-12">
<label for="client_ro_PublicKey">Public Key</label>
<input type="text" name="pubkey" readonly class="form-control" id="client_ro_PublicKey" value="{{.Device.PublicKey}}">
</div>
</div>
{{end}}
<div class="form-row">
<div class="form-group required col-md-6">
<label for="client_IPs">Client IP address</label>
<input type="text" name="ip" class="form-control" id="client_IPs" placeholder="10.6.6.1/24" value="{{.Device.IPsStr}}" required>
</div>
<div class="form-group col-md-6">
<label for="client_DNS">DNS Servers</label>
<input type="text" name="dns" class="form-control" id="client_DNS" placeholder="1.1.1.1" value="{{.Device.DNSStr}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-4">
<label for="client_MTU">MTU (0 = default)</label>
<input type="number" name="mtu" class="form-control" id="client_MTU" placeholder="" value="{{.Device.Mtu}}">
</div>
<div class="form-group col-md-4">
<label for="client_FirewallMark">Firewall Mark (0 = default or off)</label>
<input type="number" name="firewallmark" class="form-control" id="client_FirewallMark" placeholder="" value="{{.Device.FirewallMark}}">
</div>
<div class="form-group col-md-4">
<label for="client_RoutingTable">Routing Table (empty = default or auto)</label>
<input type="text" name="routingtable" class="form-control" id="client_RoutingTable" placeholder="auto" value="{{.Device.RoutingTable}}">
</div>
</div>
<h3>Interface configuration hooks</h3>
<div class="form-row">
<div class="form-group col-md-12">
<label for="client_PreUp">Pre Up</label>
<input type="text" name="preup" class="form-control" id="client_PreUp" value="{{.Device.PreUp}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="client_PostUp">Post Up</label>
<input type="text" name="postup" class="form-control" id="client_PostUp" value="{{.Device.PostUp}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="client_PreDown">Pre Down</label>
<input type="text" name="predown" class="form-control" id="client_PreDown" value="{{.Device.PreDown}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="client_PostDown">Post Down</label>
<input type="text" name="postdown" class="form-control" id="client_PostDown" value="{{.Device.PostDown}}">
</div>
</div>
<button type="submit" class="btn btn-primary">Save</button>
<a href="/admin" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>
</div> </div>
{{template "prt_footer.html" .}} {{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script> <script src="/js/jquery.min.js"></script>

View File

@ -22,11 +22,12 @@
{{template "prt_flashes.html" .}} {{template "prt_flashes.html" .}}
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
{{if eq .User.CreatedAt .Epoch}} {{if eq .User.CreatedAt .Epoch}}
<div class="form-row"> <div class="form-row">
<div class="form-group required col-md-12"> <div class="form-group required col-md-12">
<label for="inputEmail">Email</label> <label for="inputEmail">Email</label>
<input type="text" name="email" class="form-control" id="inputEmail" value="{{.User.Email}}"> <input type="text" name="email" class="form-control" id="inputEmail" value="{{.User.Email}}" required>
</div> </div>
</div> </div>
{{else}} {{else}}
@ -35,13 +36,13 @@
<div class="form-row"> <div class="form-row">
<div class="form-group required col-md-12"> <div class="form-group required col-md-12">
<label for="inputFirstname">Firstname</label> <label for="inputFirstname">Firstname</label>
<input type="text" name="firstname" class="form-control" id="inputFirstname" value="{{.User.Firstname}}"> <input type="text" name="firstname" class="form-control" id="inputFirstname" value="{{.User.Firstname}}" required>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group required col-md-12"> <div class="form-group required col-md-12">
<label for="inputLastname">Lastname</label> <label for="inputLastname">Lastname</label>
<input type="text" name="lastname" class="form-control" id="inputLastname" value="{{.User.Lastname}}"> <input type="text" name="lastname" class="form-control" id="inputLastname" value="{{.User.Lastname}}" required>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
@ -53,7 +54,7 @@
<div class="form-row"> <div class="form-row">
<div class="form-group col-md-12 {{if eq .User.CreatedAt .Epoch}}required{{end}}"> <div class="form-group col-md-12 {{if eq .User.CreatedAt .Epoch}}required{{end}}">
<label for="inputPassword">Password</label> <label for="inputPassword">Password</label>
<input type="password" name="password" class="form-control" id="inputPassword"> <input type="password" name="password" class="form-control" id="inputPassword" {{if eq .User.CreatedAt .Epoch}}required{{end}}>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">

View File

@ -18,7 +18,9 @@
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<span class="mr-auto">Interface status for <strong>{{.Device.DeviceName}}</strong></span> <span class="mr-auto">Interface status for <strong>{{.Device.DeviceName}}</strong> {{if eq $.Device.Type "server"}}(server mode){{end}}{{if eq $.Device.Type "client"}}(client mode){{end}}</span>
<a href="/admin/device/write?dev={{.Device.DeviceName}}" title="Write interface configuration"><i class="fas fa-save"></i></a>
&nbsp;&nbsp;&nbsp;
<a href="/admin/device/download?dev={{.Device.DeviceName}}" title="Download interface configuration"><i class="fas fa-download"></i></a> <a href="/admin/device/download?dev={{.Device.DeviceName}}" title="Download interface configuration"><i class="fas fa-download"></i></a>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;
<a href="/admin/device/edit?dev={{.Device.DeviceName}}" title="Edit interface settings"><i class="fas fa-cog"></i></a> <a href="/admin/device/edit?dev={{.Device.DeviceName}}" title="Edit interface settings"><i class="fas fa-cog"></i></a>
@ -26,6 +28,7 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
{{if eq $.Device.Type "server"}}
<div class="col-sm-6"> <div class="col-sm-6">
<table class="table table-sm table-borderless device-status-table"> <table class="table table-sm table-borderless device-status-table">
<tbody> <tbody>
@ -35,7 +38,7 @@
</tr> </tr>
<tr> <tr>
<td>Public Endpoint:</td> <td>Public Endpoint:</td>
<td>{{.Device.Endpoint}}</td> <td>{{.Device.DefaultEndpoint}}</td>
</tr> </tr>
<tr> <tr>
<td>Listening Port:</td> <td>Listening Port:</td>
@ -61,7 +64,7 @@
</tr> </tr>
<tr> <tr>
<td>Default allowed IP's:</td> <td>Default allowed IP's:</td>
<td>{{.Device.AllowedIPsStr}}</td> <td>{{.Device.DefaultAllowedIPsStr}}</td>
</tr> </tr>
<tr> <tr>
<td>Default DNS servers:</td> <td>Default DNS servers:</td>
@ -73,22 +76,68 @@
</tr> </tr>
<tr> <tr>
<td>Default Keepalive Interval:</td> <td>Default Keepalive Interval:</td>
<td>{{.Device.PersistentKeepalive}}</td> <td>{{.Device.DefaultPersistentKeepalive}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
{{end}}
{{if eq $.Device.Type "client"}}
<div class="col-sm-6">
<table class="table table-sm table-borderless device-status-table">
<tbody>
<tr>
<td>Public Key:</td>
<td>{{.Device.PublicKey}}</td>
</tr>
<tr>
<td>Enabled Endpoints:</td>
<td>{{len .Device.Interface.Peers}}</td>
</tr>
<tr>
<td>Total Endpoints:</td>
<td>{{.TotalPeers}}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-sm-6">
<table class="table table-sm table-borderless device-status-table">
<tbody>
<tr>
<td>IP Address:</td>
<td>{{.Device.IPsStr}}</td>
</tr>
<tr>
<td>DNS servers:</td>
<td>{{.Device.DNSStr}}</td>
</tr>
<tr>
<td>Default MTU:</td>
<td>{{.Device.Mtu}}</td>
</tr>
</tbody>
</table>
</div>
{{end}}
</div> </div>
</div> </div>
</div> </div>
<div class="mt-4 row"> <div class="mt-4 row">
<div class="col-sm-10 col-12"> <div class="col-sm-10 col-12">
{{if eq $.Device.Type "server"}}
<h2 class="mt-2">Current VPN Peers</h2> <h2 class="mt-2">Current VPN Peers</h2>
{{end}}
{{if eq $.Device.Type "client"}}
<h2 class="mt-2">Current VPN Endpoints</h2>
{{end}}
</div> </div>
<div class="col-sm-2 col-12 text-right"> <div class="col-sm-2 col-12 text-right">
{{if eq $.Device.Type "server"}}
<a href="/admin/peer/createldap" title="Add multiple peers" class="btn btn-primary"><i class="fa fa-fw fa-user-plus"></i></a> <a href="/admin/peer/createldap" title="Add multiple peers" class="btn btn-primary"><i class="fa fa-fw fa-user-plus"></i></a>
<a href="/admin/peer/create" title="Manually add a peer" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i>M</a> {{end}}
<a href="/admin/peer/create" title="Add a peer" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i>M</a>
</div> </div>
</div> </div>
<div class="mt-2 table-responsive"> <div class="mt-2 table-responsive">
@ -98,14 +147,22 @@
<th scope="col" class="list-image-cell"></th><!-- Status and expand --> <th scope="col" class="list-image-cell"></th><!-- Status and expand -->
<th scope="col"><a href="?sort=id">Identifier <i class="fa fa-fw {{.Session.GetSortIcon "peers" "id"}}"></i></a></th> <th scope="col"><a href="?sort=id">Identifier <i class="fa fa-fw {{.Session.GetSortIcon "peers" "id"}}"></i></a></th>
<th scope="col"><a href="?sort=pubKey">Public Key <i class="fa fa-fw {{.Session.GetSortIcon "peers" "pubKey"}}"></i></a></th> <th scope="col"><a href="?sort=pubKey">Public Key <i class="fa fa-fw {{.Session.GetSortIcon "peers" "pubKey"}}"></i></a></th>
{{if eq $.Device.Type "server"}}
<th scope="col"><a href="?sort=mail">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "peers" "mail"}}"></i></a></th> <th scope="col"><a href="?sort=mail">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "peers" "mail"}}"></i></a></th>
{{end}}
{{if eq $.Device.Type "server"}}
<th scope="col"><a href="?sort=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "peers" "ip"}}"></i></a></th> <th scope="col"><a href="?sort=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "peers" "ip"}}"></i></a></th>
{{end}}
{{if eq $.Device.Type "client"}}
<th scope="col"><a href="?sort=endpoint">Endpoint <i class="fa fa-fw {{.Session.GetSortIcon "peers" "endpoint"}}"></i></a></th>
{{end}}
<th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "peers" "handshake"}}"></i></a></th> <th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "peers" "handshake"}}"></i></a></th>
<th scope="col"></th><!-- Actions --> <th scope="col"></th><!-- Actions -->
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{range $i, $p :=.Peers}} {{range $i, $p :=.Peers}}
{{$peerUser:=(userForEmail $.Users $p.Email)}}
<tr id="user-pos-{{$i}}" {{if $p.DeactivatedAt}}class="disabled-peer"{{end}}> <tr id="user-pos-{{$i}}" {{if $p.DeactivatedAt}}class="disabled-peer"{{end}}>
<th scope="row" class="list-image-cell"> <th scope="row" class="list-image-cell">
<a href="#{{$p.UID}}" data-toggle="collapse" class="collapse-indicator collapsed"></a> <a href="#{{$p.UID}}" data-toggle="collapse" class="collapse-indicator collapsed"></a>
@ -114,8 +171,15 @@
</th> </th>
<td>{{$p.Identifier}}</td> <td>{{$p.Identifier}}</td>
<td>{{$p.PublicKey}}</td> <td>{{$p.PublicKey}}</td>
{{if eq $.Device.Type "server"}}
<td>{{$p.Email}}</td> <td>{{$p.Email}}</td>
{{end}}
{{if eq $.Device.Type "server"}}
<td>{{$p.IPsStr}}</td> <td>{{$p.IPsStr}}</td>
{{end}}
{{if eq $.Device.Type "client"}}
<td>{{$p.Endpoint}}</td>
{{end}}
<td><span data-toggle="tooltip" data-placement="left" title="" data-original-title="{{$p.LastHandshakeTime}}">{{$p.LastHandshake}}</span></td> <td><span data-toggle="tooltip" data-placement="left" title="" data-original-title="{{$p.LastHandshakeTime}}">{{$p.LastHandshake}}</span></td>
<td> <td>
{{if eq $.Session.IsAdmin true}} {{if eq $.Session.IsAdmin true}}
@ -132,9 +196,11 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#t1{{$p.UID}}">Personal</a> <a class="nav-link active" data-toggle="tab" href="#t1{{$p.UID}}">Personal</a>
</li> </li>
{{if eq $.Device.Type "server"}}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#t2{{$p.UID}}">Configuration</a> <a class="nav-link" data-toggle="tab" href="#t2{{$p.UID}}">Configuration</a>
</li> </li>
{{end}}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#t3{{$p.UID}}">Danger Zone</a> <a class="nav-link" data-toggle="tab" href="#t3{{$p.UID}}">Danger Zone</a>
</li> </li>
@ -142,14 +208,14 @@
<div class="tab-content" id="tabContent{{$p.UID}}"> <div class="tab-content" id="tabContent{{$p.UID}}">
<div id="t1{{$p.UID}}" class="tab-pane fade active show"> <div id="t1{{$p.UID}}" class="tab-pane fade active show">
<h4>User details</h4> <h4>User details</h4>
{{if not $p.User}} {{if not $peerUser}}
<p>No user information available...</p> <p>No user information available...</p>
{{else}} {{else}}
<ul> <ul>
<li>Firstname: {{$p.User.Firstname}}</li> <li>Firstname: {{$peerUser.Firstname}}</li>
<li>Lastname: {{$p.User.Lastname}}</li> <li>Lastname: {{$peerUser.Lastname}}</li>
<li>Phone: {{$p.User.Phone}}</li> <li>Phone: {{$peerUser.Phone}}</li>
<li>Mail: {{$p.User.Email}}</li> <li>Mail: {{$peerUser.Email}}</li>
</ul> </ul>
{{end}} {{end}}
<h4>Connection / Traffic</h4> <h4>Connection / Traffic</h4>
@ -160,22 +226,28 @@
<p class="ml-4">{{if $p.DeactivatedAt}}-{{else}}<i class="fas fa-long-arrow-alt-down" title="Download"></i> {{formatBytes $p.Peer.ReceiveBytes}} / <i class="fas fa-long-arrow-alt-up" title="Upload"></i> {{formatBytes $p.Peer.TransmitBytes}}{{end}}</p> <p class="ml-4">{{if $p.DeactivatedAt}}-{{else}}<i class="fas fa-long-arrow-alt-down" title="Download"></i> {{formatBytes $p.Peer.ReceiveBytes}} / <i class="fas fa-long-arrow-alt-up" title="Upload"></i> {{formatBytes $p.Peer.TransmitBytes}}{{end}}</p>
{{end}} {{end}}
</div> </div>
{{if eq $.Device.Type "server"}}
<div id="t2{{$p.UID}}" class="tab-pane fade"> <div id="t2{{$p.UID}}" class="tab-pane fade">
<pre>{{$p.Config}}</pre> <pre>{{$p.Config}}</pre>
</div> </div>
{{end}}
<div id="t3{{$p.UID}}" class="tab-pane fade"> <div id="t3{{$p.UID}}" class="tab-pane fade">
<a href="/admin/peer/delete?pkey={{$p.PublicKey}}" class="btn btn-danger" title="Delete peer">Delete</a> <a href="/admin/peer/delete?pkey={{$p.PublicKey}}" class="btn btn-danger" title="Delete peer">Delete</a>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{{if eq $.Device.Type "server"}}
<img class="list-image-large" src="/user/qrcode?pkey={{$p.PublicKey}}"/> <img class="list-image-large" src="/user/qrcode?pkey={{$p.PublicKey}}"/>
{{end}}
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{{if eq $.Device.Type "server"}}
<div class="float-right mt-5"> <div class="float-right mt-5">
<a href="/admin/peer/download?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Download configuration">Download</a> <a href="/admin/peer/download?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Download configuration">Download</a>
<a href="/admin/peer/email?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Send configuration via Email">Email</a> <a href="/admin/peer/email?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Send configuration via Email">Email</a>
</div> </div>
{{end}}
</div> </div>
</div> </div>
</div> </div>

View File

@ -92,7 +92,7 @@
<th class="column-top" width="210" style="font-size:0pt; line-height:0pt; padding:0; margin:0; font-weight:normal; vertical-align:top;"> <th class="column-top" width="210" style="font-size:0pt; line-height:0pt; padding:0; margin:0; font-weight:normal; vertical-align:top;">
<table width="100%" border="0" cellspacing="0" cellpadding="0"> <table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr> <tr>
<td class="fluid-img" style="font-size:0pt; line-height:0pt; text-align:left;"><img src="cid:{{.QrcodePngName}}" width="210" height="210" border="0" alt="" /></td> <td class="fluid-img" style="font-size:0pt; line-height:0pt; text-align:left;"><img src="cid:{{$.QrcodePngName}}" width="210" height="210" border="0" alt="" /></td>
</tr> </tr>
</table> </table>
</th> </th>
@ -100,14 +100,14 @@
<th class="column-top" width="280" style="font-size:0pt; line-height:0pt; padding:0; margin:0; font-weight:normal; vertical-align:top;"> <th class="column-top" width="280" style="font-size:0pt; line-height:0pt; padding:0; margin:0; font-weight:normal; vertical-align:top;">
<table width="100%" border="0" cellspacing="0" cellpadding="0"> <table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr> <tr>
{{if .Client.LdapUser}} {{if $.User}}
<td class="h4 pb20" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:20px; line-height:28px; text-align:left; padding-bottom:20px;">Hello {{.Client.LdapUser.Firstname}} {{.Client.LdapUser.Lastname}}</td> <td class="h4 pb20" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:20px; line-height:28px; text-align:left; padding-bottom:20px;">Hello {{$.User.Firstname}} {{$.User.Lastname}}</td>
{{else}} {{else}}
<td class="h4 pb20" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:20px; line-height:28px; text-align:left; padding-bottom:20px;">Hello</td> <td class="h4 pb20" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:20px; line-height:28px; text-align:left; padding-bottom:20px;">Hello</td>
{{end}} {{end}}
</tr> </tr>
<tr> <tr>
<td class="text pb20" style="color:#000000; font-family:Arial,sans-serif; font-size:14px; line-height:26px; text-align:left; padding-bottom:20px;">You or your administrator probably requested this VPN configuration. Scan the Qrcode or open the attached configuration file ({{.Client.GetConfigFileName}}) in the WireGuard VPN client to establish a secure VPN connection.</td> <td class="text pb20" style="color:#000000; font-family:Arial,sans-serif; font-size:14px; line-height:26px; text-align:left; padding-bottom:20px;">You or your administrator probably requested this VPN configuration. Scan the Qrcode or open the attached configuration file ({{$.Peer.GetConfigFileName}}) in the WireGuard VPN client to establish a secure VPN connection.</td>
</tr> </tr>
</table> </table>
</th> </th>
@ -170,7 +170,7 @@
<td class="text-footer1 pb10" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:16px; line-height:20px; text-align:center; padding-bottom:10px;">This mail was generated using WireGuard Portal.</td> <td class="text-footer1 pb10" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:16px; line-height:20px; text-align:center; padding-bottom:10px;">This mail was generated using WireGuard Portal.</td>
</tr> </tr>
<tr> <tr>
<td class="text-footer2" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:12px; line-height:26px; text-align:center;"><a href="{{.PortalUrl}}" target="_blank" rel="noopener noreferrer" class="link" style="color:#000000; text-decoration:none;"><span class="link" style="color:#000000; text-decoration:none;">Visit WireGuard Portal</span></a></td> <td class="text-footer2" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:12px; line-height:26px; text-align:center;"><a href="{{$.PortalUrl}}" target="_blank" rel="noopener noreferrer" class="link" style="color:#000000; text-decoration:none;"><span class="link" style="color:#000000; text-decoration:none;">Visit WireGuard Portal</span></a></td>
</tr> </tr>
</table> </table>
</td> </td>

View File

@ -13,12 +13,22 @@
<link rel="stylesheet" href="/css/signin.css"> <link rel="stylesheet" href="/css/signin.css">
</head> </head>
<body class="bg-gradient-primary"> <body id="page-top" class="d-flex flex-column min-vh-100">
<div class="container"> <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#topNavbar" aria-controls="topNavbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand" href="/"><img src="{{$.static.WebsiteLogo}}" alt="{{$.static.CompanyName}}"/></a>
<div id="topNavbar" class="navbar-collapse collapse">
</div><!--/.navbar-collapse -->
</nav>
<div class="container mt-1">
<div class="card mt-5"> <div class="card mt-5">
<div class="card-header">Please sign in</div> <div class="card-header">Please sign in</div>
<div class="card-body"> <div class="card-body">
<form class="form-signin" method="post"> <form class="form-signin" method="post">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
<div class="form-group"> <div class="form-group">
<label for="inputUsername">Email</label> <label for="inputUsername">Email</label>
<input type="text" name="username" class="form-control" id="inputUsername" aria-describedby="usernameHelp" placeholder="Enter email"> <input type="text" name="username" class="form-control" id="inputUsername" aria-describedby="usernameHelp" placeholder="Enter email">
@ -27,15 +37,16 @@
<label for="inputPassword">Password</label> <label for="inputPassword">Password</label>
<input type="password" name="password" class="form-control" id="inputPassword" placeholder="Password"> <input type="password" name="password" class="form-control" id="inputPassword" placeholder="Password">
</div> </div>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button> <button class="btn btn-lg btn-primary btn-block mt-5" type="submit">Sign in</button>
{{ if eq .error true }} {{ if eq .error true }}
<hr> <div class="alert alert-danger mt-3" role="alert">
<span class="text-danger">{{.message}}</span> {{.message}}
</div>
{{end}} {{end}}
</form> </form>
<div class="card shadow-lg o-hidden border-0 my-5"> <div class="card o-hidden border-0 my-5">
<div class="card-body p-0"> <div class="card-body p-0">
<a href="/" class="btn btn-white btn-block text-primary btn-user">Go Home</a> <a href="/" class="btn btn-white btn-block text-primary btn-user">Go Home</a>
</div> </div>

View File

@ -22,6 +22,19 @@
{{end}} {{end}}
{{end}}{{end}} {{end}}{{end}}
</ul> </ul>
{{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}
{{with startsWith $.Route "/admin/"}}
<form class="form-inline my-2 my-lg-0" method="get">
<div class="form-group mr-sm-2">
<select name="device" id="inputDevice" class="form-control device-selector">
{{range $d, $dn := $.DeviceNames}}
<option value="{{$d}}" {{if eq $d $.Session.DeviceName}}selected{{end}}>{{$d}} {{if and (ne $dn "") (ne $d $dn)}}({{$dn}}){{end}}</option>
{{end}}
</select>
</div>
</form>
{{end}}
{{end}}{{end}}
{{if eq $.Session.LoggedIn true}} {{if eq $.Session.LoggedIn true}}
<div class="nav-item dropdown"> <div class="nav-item dropdown">
<a href="#" class="navbar-text dropdown-toggle" data-toggle="dropdown">{{$.Session.Firstname}} {{$.Session.Lastname}} <span class="caret"></span></a> <a href="#" class="navbar-text dropdown-toggle" data-toggle="dropdown">{{$.Session.Firstname}} {{$.Session.Lastname}} <span class="caret"></span></a>
@ -43,6 +56,6 @@
</nav> </nav>
{{if not $.Device.IsValid}} {{if not $.Device.IsValid}}
<div class="container"> <div class="container">
<div class="alert alert-danger">Warning: WireGuard Interface is not fully configured! Configurations may be incomplete and non functional!</div> <div class="alert alert-danger">Warning: WireGuard Interface {{$.Device.DeviceName}} is not fully configured! Configurations may be incomplete and non functional!</div>
</div> </div>
{{end}} {{end}}

View File

@ -30,6 +30,7 @@
</thead> </thead>
<tbody> <tbody>
{{range $i, $p :=.Peers}} {{range $i, $p :=.Peers}}
{{$peerUser:=(userForEmail $.Users $p.Email)}}
<tr id="user-pos-{{$i}}" {{if $p.DeactivatedAt}}class="disabled-peer"{{end}}> <tr id="user-pos-{{$i}}" {{if $p.DeactivatedAt}}class="disabled-peer"{{end}}>
<th scope="row" class="list-image-cell"> <th scope="row" class="list-image-cell">
<a href="#{{$p.UID}}" data-toggle="collapse" class="collapse-indicator collapsed"></a> <a href="#{{$p.UID}}" data-toggle="collapse" class="collapse-indicator collapsed"></a>
@ -58,14 +59,14 @@
<div class="tab-content" id="tabContent{{$p.UID}}"> <div class="tab-content" id="tabContent{{$p.UID}}">
<div id="t1{{$p.UID}}" class="tab-pane fade active show"> <div id="t1{{$p.UID}}" class="tab-pane fade active show">
<h4>User details</h4> <h4>User details</h4>
{{if not $p.User}} {{if not $peerUser}}
<p>No user information available...</p> <p>No user information available...</p>
{{else}} {{else}}
<ul> <ul>
<li>Firstname: {{$p.User.Firstname}}</li> <li>Firstname: {{$peerUser.Firstname}}</li>
<li>Lastname: {{$p.User.Lastname}}</li> <li>Lastname: {{$peerUser.Lastname}}</li>
<li>Phone: {{$p.User.Phone}}</li> <li>Phone: {{$peerUser.Phone}}</li>
<li>Mail: {{$p.User.Email}}</li> <li>Mail: {{$peerUser.Email}}</li>
</ul> </ul>
{{end}} {{end}}
<h4>Traffic</h4> <h4>Traffic</h4>

19
go.mod
View File

@ -6,22 +6,21 @@ require (
github.com/gin-contrib/sessions v0.0.3 github.com/gin-contrib/sessions v0.0.3
github.com/gin-gonic/gin v1.6.3 github.com/gin-gonic/gin v1.6.3
github.com/go-ldap/ldap/v3 v3.2.4 github.com/go-ldap/ldap/v3 v3.2.4
github.com/go-playground/validator/v10 v10.2.0 github.com/go-playground/validator/v10 v10.4.1
github.com/gorilla/sessions v1.2.1 // indirect github.com/gorilla/sessions v1.2.1 // indirect
github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible
github.com/kelseyhightower/envconfig v1.4.0 github.com/kelseyhightower/envconfig v1.4.0
github.com/milosgajdos/tenus v0.0.3 github.com/milosgajdos/tenus v0.0.3
github.com/mitchellh/gox v1.0.1 // indirect
github.com/necrose99/gox v0.4.0 // indirect
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.7.0 github.com/sirupsen/logrus v1.8.1
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
github.com/toorop/gin-logrus v0.0.0-20200831135515-d2ee50d38dae github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200609130330-bd2cb7843e1b golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200609130330-bd2cb7843e1b
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
gorm.io/driver/mysql v1.0.4 gorm.io/driver/mysql v1.0.5
gorm.io/driver/sqlite v1.1.3 gorm.io/driver/sqlite v1.1.4
gorm.io/gorm v1.20.12 gorm.io/gorm v1.21.6
) )

View File

@ -182,7 +182,7 @@ func (provider Provider) open() (*ldap.Conn, error) {
if provider.config.StartTLS { if provider.config.StartTLS {
// Reconnect with TLS // Reconnect with TLS
err = conn.StartTLS(&tls.Config{InsecureSkipVerify: true}) err = conn.StartTLS(&tls.Config{InsecureSkipVerify: !provider.config.CertValidation})
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -7,6 +7,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/h44z/wg-portal/internal/common"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/h44z/wg-portal/internal/authentication" "github.com/h44z/wg-portal/internal/authentication"
"github.com/h44z/wg-portal/internal/users" "github.com/h44z/wg-portal/internal/users"
@ -22,11 +24,11 @@ type Provider struct {
db *gorm.DB db *gorm.DB
} }
func New(cfg *users.Config) (*Provider, error) { func New(cfg *common.DatabaseConfig) (*Provider, error) {
p := &Provider{} p := &Provider{}
var err error var err error
p.db, err = users.GetDatabaseForConfig(cfg) p.db, err = common.GetDatabaseForConfig(cfg)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to setup authentication database %s", cfg.Database) return nil, errors.Wrapf(err, "failed to setup authentication database %s", cfg.Database)
} }

109
internal/common/db.go Normal file
View File

@ -0,0 +1,109 @@
package common
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type SupportedDatabase string
const (
SupportedDatabaseMySQL SupportedDatabase = "mysql"
SupportedDatabaseSQLite SupportedDatabase = "sqlite"
)
type DatabaseConfig struct {
Typ SupportedDatabase `yaml:"typ" envconfig:"DATABASE_TYPE"` //mysql or sqlite
Host string `yaml:"host" envconfig:"DATABASE_HOST"`
Port int `yaml:"port" envconfig:"DATABASE_PORT"`
Database string `yaml:"database" envconfig:"DATABASE_NAME"` // On SQLite: the database file-path, otherwise the database name
User string `yaml:"user" envconfig:"DATABASE_USERNAME"`
Password string `yaml:"password" envconfig:"DATABASE_PASSWORD"`
}
func GetDatabaseForConfig(cfg *DatabaseConfig) (db *gorm.DB, err error) {
switch cfg.Typ {
case SupportedDatabaseSQLite:
if _, err = os.Stat(filepath.Dir(cfg.Database)); os.IsNotExist(err) {
if err = os.MkdirAll(filepath.Dir(cfg.Database), 0700); err != nil {
return
}
}
db, err = gorm.Open(sqlite.Open(cfg.Database), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true})
if err != nil {
return
}
case SupportedDatabaseMySQL:
connectionString := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
db, err = gorm.Open(mysql.Open(connectionString), &gorm.Config{})
if err != nil {
return
}
sqlDB, _ := db.DB()
sqlDB.SetConnMaxLifetime(time.Minute * 5)
sqlDB.SetMaxIdleConns(2)
sqlDB.SetMaxOpenConns(10)
err = sqlDB.Ping() // This DOES open a connection if necessary. This makes sure the database is accessible
if err != nil {
return nil, errors.Wrap(err, "failed to ping mysql authentication database")
}
}
// Enable Logger (logrus)
logCfg := logger.Config{
SlowThreshold: time.Second, // all slower than one second
Colorful: false,
LogLevel: logger.Silent, // default: log nothing
}
if logrus.StandardLogger().GetLevel() == logrus.TraceLevel {
logCfg.LogLevel = logger.Info
logCfg.SlowThreshold = 500 * time.Millisecond // all slower than half a second
}
db.Config.Logger = logger.New(logrus.StandardLogger(), logCfg)
return
}
type DatabaseMigrationInfo struct {
Version string `gorm:"primaryKey"`
Applied time.Time
}
func MigrateDatabase(db *gorm.DB, version string) error {
if err := db.AutoMigrate(&DatabaseMigrationInfo{}); err != nil {
return errors.Wrap(err, "failed to migrate version database")
}
newVersion := DatabaseMigrationInfo{
Version: version,
Applied: time.Now(),
}
existingMigration := DatabaseMigrationInfo{}
db.Where("version = ?", version).FirstOrInit(&existingMigration)
if existingMigration.Version == "" {
lastVersion := DatabaseMigrationInfo{}
db.Order("applied desc, version desc").FirstOrInit(&lastVersion)
// TODO: migrate database
res := db.Create(&newVersion)
if res.Error != nil {
return errors.Wrap(res.Error, "failed to write version to database")
}
}
return nil
}

View File

@ -71,9 +71,9 @@ func SendEmailWithAttachments(cfg MailConfig, sender, replyTo, subject, body str
} }
} }
if cfg.CertValidation { if cfg.TLS {
return e.Send(hostname, auth) return e.SendWithStartTLS(hostname, auth, &tls.Config{InsecureSkipVerify: !cfg.CertValidation})
} else { } else {
return e.SendWithStartTLS(hostname, auth, &tls.Config{InsecureSkipVerify: true}) return e.Send(hostname, auth)
} }
} }

View File

@ -60,6 +60,16 @@ func ListToString(lst []string) string {
return strings.Join(lst, ", ") return strings.Join(lst, ", ")
} }
// ListContains checks if a needle exists in the given list.
func ListContains(lst []string, needle string) bool {
for _, entry := range lst {
if entry == needle {
return true
}
}
return false
}
// https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/ // https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/
func ByteCountSI(b int64) string { func ByteCountSI(b int64) string {
const unit = 1000 const unit = 1000

View File

@ -8,11 +8,12 @@ const (
) )
type Config struct { type Config struct {
URL string `yaml:"url" envconfig:"LDAP_URL"` URL string `yaml:"url" envconfig:"LDAP_URL"`
StartTLS bool `yaml:"startTLS" envconfig:"LDAP_STARTTLS"` StartTLS bool `yaml:"startTLS" envconfig:"LDAP_STARTTLS"`
BaseDN string `yaml:"dn" envconfig:"LDAP_BASEDN"` CertValidation bool `yaml:"certcheck" envconfig:"LDAP_CERT_VALIDATION"`
BindUser string `yaml:"user" envconfig:"LDAP_USER"` BaseDN string `yaml:"dn" envconfig:"LDAP_BASEDN"`
BindPass string `yaml:"pass" envconfig:"LDAP_PASSWORD"` BindUser string `yaml:"user" envconfig:"LDAP_USER"`
BindPass string `yaml:"pass" envconfig:"LDAP_PASSWORD"`
Type Type `yaml:"typ" envconfig:"LDAP_TYPE"` // AD for active directory, OpenLDAP for OpenLDAP Type Type `yaml:"typ" envconfig:"LDAP_TYPE"` // AD for active directory, OpenLDAP for OpenLDAP
UserClass string `yaml:"userClass" envconfig:"LDAP_USER_CLASS"` UserClass string `yaml:"userClass" envconfig:"LDAP_USER_CLASS"`

View File

@ -23,7 +23,7 @@ func Open(cfg *Config) (*ldap.Conn, error) {
if cfg.StartTLS { if cfg.StartTLS {
// Reconnect with TLS // Reconnect with TLS
err = conn.StartTLS(&tls.Config{InsecureSkipVerify: true}) err = conn.StartTLS(&tls.Config{InsecureSkipVerify: !cfg.CertValidation})
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to star TLS on connection") return nil, errors.Wrap(err, "failed to star TLS on connection")
} }
@ -92,7 +92,7 @@ func IsActiveDirectoryUserDisabled(userAccountControl string) bool {
return false return false
} }
uacInt, err := strconv.Atoi(userAccountControl) uacInt, err := strconv.ParseInt(userAccountControl, 10, 32)
if err != nil { if err != nil {
return true return true
} }

View File

@ -1,12 +1,12 @@
package common package server
import ( import (
"os" "os"
"reflect" "reflect"
"runtime" "runtime"
"github.com/h44z/wg-portal/internal/common"
"github.com/h44z/wg-portal/internal/ldap" "github.com/h44z/wg-portal/internal/ldap"
"github.com/h44z/wg-portal/internal/users"
"github.com/h44z/wg-portal/internal/wireguard" "github.com/h44z/wg-portal/internal/wireguard"
"github.com/kelseyhightower/envconfig" "github.com/kelseyhightower/envconfig"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -65,11 +65,12 @@ type Config struct {
EditableKeys bool `yaml:"editableKeys" envconfig:"EDITABLE_KEYS"` EditableKeys bool `yaml:"editableKeys" envconfig:"EDITABLE_KEYS"`
CreateDefaultPeer bool `yaml:"createDefaultPeer" envconfig:"CREATE_DEFAULT_PEER"` CreateDefaultPeer bool `yaml:"createDefaultPeer" envconfig:"CREATE_DEFAULT_PEER"`
LdapEnabled bool `yaml:"ldapEnabled" envconfig:"LDAP_ENABLED"` LdapEnabled bool `yaml:"ldapEnabled" envconfig:"LDAP_ENABLED"`
SessionSecret string `yaml:"sessionSecret" envconfig:"SESSION_SECRET"`
} `yaml:"core"` } `yaml:"core"`
Database users.Config `yaml:"database"` Database common.DatabaseConfig `yaml:"database"`
Email MailConfig `yaml:"email"` Email common.MailConfig `yaml:"email"`
LDAP ldap.Config `yaml:"ldap"` LDAP ldap.Config `yaml:"ldap"`
WG wireguard.Config `yaml:"wg"` WG wireguard.Config `yaml:"wg"`
} }
func NewConfig() *Config { func NewConfig() *Config {
@ -84,6 +85,8 @@ func NewConfig() *Config {
cfg.Core.AdminUser = "admin@wgportal.local" cfg.Core.AdminUser = "admin@wgportal.local"
cfg.Core.AdminPassword = "wgportal" cfg.Core.AdminPassword = "wgportal"
cfg.Core.LdapEnabled = false cfg.Core.LdapEnabled = false
cfg.Core.EditableKeys = true
cfg.Core.SessionSecret = "secret"
cfg.Database.Typ = "sqlite" cfg.Database.Typ = "sqlite"
cfg.Database.Database = "data/wg_portal.db" cfg.Database.Database = "data/wg_portal.db"
@ -103,8 +106,9 @@ func NewConfig() *Config {
cfg.LDAP.DisabledAttribute = "userAccountControl" cfg.LDAP.DisabledAttribute = "userAccountControl"
cfg.LDAP.AdminLdapGroup = "CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL" cfg.LDAP.AdminLdapGroup = "CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL"
cfg.WG.DeviceName = "wg0" cfg.WG.DeviceNames = []string{"wg0"}
cfg.WG.WireGuardConfig = "/etc/wireguard/wg0.conf" cfg.WG.DefaultDeviceName = "wg0"
cfg.WG.ConfigDirectoryPath = "/etc/wireguard"
cfg.WG.ManageIPAddresses = true cfg.WG.ManageIPAddresses = true
cfg.Email.Host = "127.0.0.1" cfg.Email.Host = "127.0.0.1"
cfg.Email.Port = 25 cfg.Email.Port = 25

View File

@ -4,6 +4,8 @@ import (
"net/http" "net/http"
"strings" "strings"
csrf "github.com/utrack/gin-csrf"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/h44z/wg-portal/internal/authentication" "github.com/h44z/wg-portal/internal/authentication"
"github.com/h44z/wg-portal/internal/users" "github.com/h44z/wg-portal/internal/users"
@ -31,6 +33,7 @@ func (s *Server) GetLogin(c *gin.Context) {
"error": authError != "", "error": authError != "",
"message": errMsg, "message": errMsg,
"static": s.getStaticData(), "static": s.getStaticData(),
"Csrf": csrf.GetToken(c),
}) })
} }
@ -98,7 +101,7 @@ func (s *Server) PostLogin(c *gin.Context) {
Firstname: userData.Firstname, Firstname: userData.Firstname,
Lastname: userData.Lastname, Lastname: userData.Lastname,
Phone: userData.Phone, Phone: userData.Phone,
}); err != nil { }, s.wg.Cfg.GetDefaultDeviceName()); err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "login error", "failed to update user data") s.GetHandleError(c, http.StatusInternalServerError, "login error", "failed to update user data")
return return
} }
@ -121,9 +124,10 @@ func (s *Server) PostLogin(c *gin.Context) {
sessionData.Email = user.Email sessionData.Email = user.Email
sessionData.Firstname = user.Firstname sessionData.Firstname = user.Firstname
sessionData.Lastname = user.Lastname sessionData.Lastname = user.Lastname
sessionData.DeviceName = s.wg.Cfg.DeviceNames[0]
// Check if user already has a peer setup, if not create one // Check if user already has a peer setup, if not create one
if err := s.CreateUserDefaultPeer(user.Email); err != nil { if err := s.CreateUserDefaultPeer(user.Email, s.wg.Cfg.GetDefaultDeviceName()); err != nil {
// Not a fatal error, just log it... // Not a fatal error, just log it...
logrus.Errorf("failed to automatically create vpn peer for %s: %v", sessionData.Email, err) logrus.Errorf("failed to automatically create vpn peer for %s: %v", sessionData.Email, err)
} }

View File

@ -4,37 +4,39 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"github.com/pkg/errors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/h44z/wg-portal/internal/common"
"github.com/h44z/wg-portal/internal/users"
"github.com/pkg/errors"
) )
func (s *Server) GetHandleError(c *gin.Context, code int, message, details string) { func (s *Server) GetHandleError(c *gin.Context, code int, message, details string) {
currentSession := GetSessionData(c)
c.HTML(code, "error.html", gin.H{ c.HTML(code, "error.html", gin.H{
"Data": gin.H{ "Data": gin.H{
"Code": strconv.Itoa(code), "Code": strconv.Itoa(code),
"Message": message, "Message": message,
"Details": details, "Details": details,
}, },
"Route": c.Request.URL.Path, "Route": c.Request.URL.Path,
"Session": GetSessionData(c), "Session": GetSessionData(c),
"Static": s.getStaticData(), "Static": s.getStaticData(),
"Device": s.peers.GetDevice(currentSession.DeviceName),
"DeviceNames": s.GetDeviceNames(),
}) })
} }
func (s *Server) GetIndex(c *gin.Context) { func (s *Server) GetIndex(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", struct { currentSession := GetSessionData(c)
Route string
Alerts []FlashData c.HTML(http.StatusOK, "index.html", gin.H{
Session SessionData "Route": c.Request.URL.Path,
Static StaticData "Alerts": GetFlashes(c),
Device Device "Session": currentSession,
}{ "Static": s.getStaticData(),
Route: c.Request.URL.Path, "Device": s.peers.GetDevice(currentSession.DeviceName),
Alerts: GetFlashes(c), "DeviceNames": s.GetDeviceNames(),
Session: GetSessionData(c),
Static: s.getStaticData(),
Device: s.peers.GetDevice(),
}) })
} }
@ -74,25 +76,35 @@ func (s *Server) GetAdminIndex(c *gin.Context) {
return return
} }
device := s.peers.GetDevice() deviceName := c.Query("device")
users := s.peers.GetFilteredAndSortedPeers(currentSession.SortedBy["peers"], currentSession.SortDirection["peers"], currentSession.Search["peers"]) if deviceName != "" {
if !common.ListContains(s.wg.Cfg.DeviceNames, deviceName) {
s.GetHandleError(c, http.StatusInternalServerError, "device selection error", "no such device")
return
}
currentSession.DeviceName = deviceName
c.HTML(http.StatusOK, "admin_index.html", struct { if err := UpdateSessionData(c, currentSession); err != nil {
Route string s.GetHandleError(c, http.StatusInternalServerError, "device selection error", "failed to save session")
Alerts []FlashData return
Session SessionData }
Static StaticData c.Redirect(http.StatusSeeOther, "/admin/")
Peers []Peer return
TotalPeers int }
Device Device
}{ device := s.peers.GetDevice(currentSession.DeviceName)
Route: c.Request.URL.Path, users := s.peers.GetFilteredAndSortedPeers(currentSession.DeviceName, currentSession.SortedBy["peers"], currentSession.SortDirection["peers"], currentSession.Search["peers"])
Alerts: GetFlashes(c),
Session: currentSession, c.HTML(http.StatusOK, "admin_index.html", gin.H{
Static: s.getStaticData(), "Route": c.Request.URL.Path,
Peers: users, "Alerts": GetFlashes(c),
TotalPeers: len(s.peers.GetAllPeers()), "Session": currentSession,
Device: device, "Static": s.getStaticData(),
"Peers": users,
"TotalPeers": len(s.peers.GetAllPeers(currentSession.DeviceName)),
"Users": s.users.GetUsers(),
"Device": device,
"DeviceNames": s.GetDeviceNames(),
}) })
} }
@ -120,25 +132,18 @@ func (s *Server) GetUserIndex(c *gin.Context) {
return return
} }
device := s.peers.GetDevice() peers := s.peers.GetSortedPeersForEmail(currentSession.SortedBy["userpeers"], currentSession.SortDirection["userpeers"], currentSession.Email)
users := s.peers.GetSortedPeersForEmail(currentSession.SortedBy["userpeers"], currentSession.SortDirection["userpeers"], currentSession.Email)
c.HTML(http.StatusOK, "user_index.html", struct { c.HTML(http.StatusOK, "user_index.html", gin.H{
Route string "Route": c.Request.URL.Path,
Alerts []FlashData "Alerts": GetFlashes(c),
Session SessionData "Session": currentSession,
Static StaticData "Static": s.getStaticData(),
Peers []Peer "Peers": peers,
TotalPeers int "TotalPeers": len(peers),
Device Device "Users": []users.User{*s.users.GetUser(currentSession.Email)},
}{ "Device": s.peers.GetDevice(currentSession.DeviceName),
Route: c.Request.URL.Path, "DeviceNames": s.GetDeviceNames(),
Alerts: GetFlashes(c),
Session: currentSession,
Static: s.getStaticData(),
Peers: users,
TotalPeers: len(users),
Device: device,
}) })
} }
@ -155,10 +160,11 @@ func (s *Server) updateFormInSession(c *gin.Context, formData interface{}) error
func (s *Server) setNewPeerFormInSession(c *gin.Context) (SessionData, error) { func (s *Server) setNewPeerFormInSession(c *gin.Context) (SessionData, error) {
currentSession := GetSessionData(c) currentSession := GetSessionData(c)
// If session does not contain a peer form ignore update // If session does not contain a peer form ignore update
// If url contains a formerr parameter reset the form // If url contains a formerr parameter reset the form
if currentSession.FormData == nil || c.Query("formerr") == "" { if currentSession.FormData == nil || c.Query("formerr") == "" {
user, err := s.PrepareNewPeer() user, err := s.PrepareNewPeer(currentSession.DeviceName)
if err != nil { if err != nil {
return currentSession, errors.WithMessage(err, "failed to prepare new peer") return currentSession, errors.WithMessage(err, "failed to prepare new peer")
} }

View File

@ -1,47 +1,42 @@
package server package server
import ( import (
"fmt"
"net/http" "net/http"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/h44z/wg-portal/internal/common" "github.com/h44z/wg-portal/internal/common"
"github.com/h44z/wg-portal/internal/wireguard"
csrf "github.com/utrack/gin-csrf"
) )
func (s *Server) GetAdminEditInterface(c *gin.Context) { func (s *Server) GetAdminEditInterface(c *gin.Context) {
device := s.peers.GetDevice() currentSession := GetSessionData(c)
users := s.peers.GetAllPeers() device := s.peers.GetDevice(currentSession.DeviceName)
currentSession, err := s.setFormInSession(c, device) currentSession, err := s.setFormInSession(c, device)
if err != nil { if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error())
return return
} }
c.HTML(http.StatusOK, "admin_edit_interface.html", struct { c.HTML(http.StatusOK, "admin_edit_interface.html", gin.H{
Route string "Route": c.Request.URL.Path,
Alerts []FlashData "Alerts": GetFlashes(c),
Session SessionData "Session": currentSession,
Static StaticData "Static": s.getStaticData(),
Peers []Peer "Device": currentSession.FormData.(wireguard.Device),
Device Device "EditableKeys": s.config.Core.EditableKeys,
EditableKeys bool "DeviceNames": s.GetDeviceNames(),
}{ "Csrf": csrf.GetToken(c),
Route: c.Request.URL.Path,
Alerts: GetFlashes(c),
Session: currentSession,
Static: s.getStaticData(),
Peers: users,
Device: currentSession.FormData.(Device),
EditableKeys: s.config.Core.EditableKeys,
}) })
} }
func (s *Server) PostAdminEditInterface(c *gin.Context) { func (s *Server) PostAdminEditInterface(c *gin.Context) {
currentSession := GetSessionData(c) currentSession := GetSessionData(c)
var formDevice Device var formDevice wireguard.Device
if currentSession.FormData != nil { if currentSession.FormData != nil {
formDevice = currentSession.FormData.(Device) formDevice = currentSession.FormData.(wireguard.Device)
} }
if err := c.ShouldBind(&formDevice); err != nil { if err := c.ShouldBind(&formDevice); err != nil {
_ = s.updateFormInSession(c, formDevice) _ = s.updateFormInSession(c, formDevice)
@ -50,12 +45,20 @@ func (s *Server) PostAdminEditInterface(c *gin.Context) {
return return
} }
// Clean list input // Clean list input
formDevice.IPs = common.ParseStringList(formDevice.IPsStr) formDevice.IPsStr = common.ListToString(common.ParseStringList(formDevice.IPsStr))
formDevice.AllowedIPs = common.ParseStringList(formDevice.AllowedIPsStr) formDevice.DefaultAllowedIPsStr = common.ListToString(common.ParseStringList(formDevice.DefaultAllowedIPsStr))
formDevice.DNS = common.ParseStringList(formDevice.DNSStr) formDevice.DNSStr = common.ListToString(common.ParseStringList(formDevice.DNSStr))
formDevice.IPsStr = common.ListToString(formDevice.IPs)
formDevice.AllowedIPsStr = common.ListToString(formDevice.AllowedIPs) // Clean interface parameters based on interface type
formDevice.DNSStr = common.ListToString(formDevice.DNS) switch formDevice.Type {
case wireguard.DeviceTypeClient:
formDevice.ListenPort = 0
formDevice.DefaultEndpoint = ""
formDevice.DefaultAllowedIPsStr = ""
formDevice.DefaultPersistentKeepalive = 0
formDevice.SaveConfig = false
case wireguard.DeviceTypeServer:
}
// Update WireGuard device // Update WireGuard device
err := s.wg.UpdateDevice(formDevice.DeviceName, formDevice.GetConfig()) err := s.wg.UpdateDevice(formDevice.DeviceName, formDevice.GetConfig())
@ -76,7 +79,7 @@ func (s *Server) PostAdminEditInterface(c *gin.Context) {
} }
// Update WireGuard config file // Update WireGuard config file
err = s.WriteWireGuardConfigFile() err = s.WriteWireGuardConfigFile(currentSession.DeviceName)
if err != nil { if err != nil {
_ = s.updateFormInSession(c, formDevice) _ = s.updateFormInSession(c, formDevice)
SetFlashMessage(c, "Failed to update WireGuard config-file: "+err.Error(), "danger") SetFlashMessage(c, "Failed to update WireGuard config-file: "+err.Error(), "danger")
@ -86,12 +89,12 @@ func (s *Server) PostAdminEditInterface(c *gin.Context) {
// Update interface IP address // Update interface IP address
if s.config.WG.ManageIPAddresses { if s.config.WG.ManageIPAddresses {
if err := s.wg.SetIPAddress(formDevice.IPs); err != nil { if err := s.wg.SetIPAddress(currentSession.DeviceName, formDevice.GetIPAddresses()); err != nil {
_ = s.updateFormInSession(c, formDevice) _ = s.updateFormInSession(c, formDevice)
SetFlashMessage(c, "Failed to update ip address: "+err.Error(), "danger") SetFlashMessage(c, "Failed to update ip address: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update") c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update")
} }
if err := s.wg.SetMTU(formDevice.Mtu); err != nil { if err := s.wg.SetMTU(currentSession.DeviceName, formDevice.Mtu); err != nil {
_ = s.updateFormInSession(c, formDevice) _ = s.updateFormInSession(c, formDevice)
SetFlashMessage(c, "Failed to update MTU: "+err.Error(), "danger") SetFlashMessage(c, "Failed to update MTU: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update") c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update")
@ -106,9 +109,10 @@ func (s *Server) PostAdminEditInterface(c *gin.Context) {
} }
func (s *Server) GetInterfaceConfig(c *gin.Context) { func (s *Server) GetInterfaceConfig(c *gin.Context) {
device := s.peers.GetDevice() currentSession := GetSessionData(c)
users := s.peers.GetActivePeers() device := s.peers.GetDevice(currentSession.DeviceName)
cfg, err := device.GetConfigFile(users) peers := s.peers.GetActivePeers(device.DeviceName)
cfg, err := device.GetConfigFile(peers)
if err != nil { if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
return return
@ -121,20 +125,53 @@ func (s *Server) GetInterfaceConfig(c *gin.Context) {
return return
} }
func (s *Server) GetApplyGlobalConfig(c *gin.Context) { func (s *Server) GetSaveConfig(c *gin.Context) {
device := s.peers.GetDevice() currentSession := GetSessionData(c)
users := s.peers.GetAllPeers()
for _, user := range users { err := s.WriteWireGuardConfigFile(currentSession.DeviceName)
user.AllowedIPs = device.AllowedIPs if err != nil {
user.AllowedIPsStr = device.AllowedIPsStr SetFlashMessage(c, "Failed to save WireGuard config-file: "+err.Error(), "danger")
if err := s.peers.UpdatePeer(user); err != nil { c.Redirect(http.StatusSeeOther, "/admin/")
SetFlashMessage(c, err.Error(), "danger") return
c.Redirect(http.StatusSeeOther, "/admin/device/edit")
}
} }
SetFlashMessage(c, "Allowed IP's updated for all clients.", "success") SetFlashMessage(c, "Updated WireGuard config-file", "success")
c.Redirect(http.StatusSeeOther, "/admin/")
return
}
func (s *Server) GetApplyGlobalConfig(c *gin.Context) {
currentSession := GetSessionData(c)
device := s.peers.GetDevice(currentSession.DeviceName)
peers := s.peers.GetAllPeers(device.DeviceName)
if device.Type == wireguard.DeviceTypeClient {
SetFlashMessage(c, "Cannot apply global configuration while interface is in client mode.", "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit")
return
}
updateCounter := 0
for _, peer := range peers {
if peer.IgnoreGlobalSettings {
continue
}
peer.AllowedIPsStr = device.DefaultAllowedIPsStr
peer.Endpoint = device.DefaultEndpoint
peer.PersistentKeepalive = device.DefaultPersistentKeepalive
peer.DNSStr = device.DNSStr
peer.Mtu = device.Mtu
if err := s.peers.UpdatePeer(peer); err != nil {
SetFlashMessage(c, err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit")
return
}
updateCounter++
}
SetFlashMessage(c, fmt.Sprintf("Global configuration updated for %d clients.", updateCounter), "success")
c.Redirect(http.StatusSeeOther, "/admin/device/edit") c.Redirect(http.StatusSeeOther, "/admin/device/edit")
return return
} }

View File

@ -11,8 +11,10 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/h44z/wg-portal/internal/common" "github.com/h44z/wg-portal/internal/common"
"github.com/h44z/wg-portal/internal/users" "github.com/h44z/wg-portal/internal/users"
"github.com/h44z/wg-portal/internal/wireguard"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/tatsushid/go-fastping" "github.com/tatsushid/go-fastping"
csrf "github.com/utrack/gin-csrf"
) )
type LdapCreateForm struct { type LdapCreateForm struct {
@ -21,7 +23,6 @@ type LdapCreateForm struct {
} }
func (s *Server) GetAdminEditPeer(c *gin.Context) { func (s *Server) GetAdminEditPeer(c *gin.Context) {
device := s.peers.GetDevice()
peer := s.peers.GetPeerByKey(c.Query("pkey")) peer := s.peers.GetPeerByKey(c.Query("pkey"))
currentSession, err := s.setFormInSession(c, peer) currentSession, err := s.setFormInSession(c, peer)
@ -30,22 +31,17 @@ func (s *Server) GetAdminEditPeer(c *gin.Context) {
return return
} }
c.HTML(http.StatusOK, "admin_edit_client.html", struct { c.HTML(http.StatusOK, "admin_edit_client.html", gin.H{
Route string "Route": c.Request.URL.Path,
Alerts []FlashData "Alerts": GetFlashes(c),
Session SessionData "Session": currentSession,
Static StaticData "Static": s.getStaticData(),
Peer Peer "Peer": currentSession.FormData.(wireguard.Peer),
Device Device "EditableKeys": s.config.Core.EditableKeys,
EditableKeys bool "Device": s.peers.GetDevice(currentSession.DeviceName),
}{ "DeviceNames": s.GetDeviceNames(),
Route: c.Request.URL.Path, "AdminEmail": s.config.Core.AdminUser,
Alerts: GetFlashes(c), "Csrf": csrf.GetToken(c),
Session: currentSession,
Static: s.getStaticData(),
Peer: currentSession.FormData.(Peer),
Device: device,
EditableKeys: s.config.Core.EditableKeys,
}) })
} }
@ -54,9 +50,9 @@ func (s *Server) PostAdminEditPeer(c *gin.Context) {
urlEncodedKey := url.QueryEscape(c.Query("pkey")) urlEncodedKey := url.QueryEscape(c.Query("pkey"))
currentSession := GetSessionData(c) currentSession := GetSessionData(c)
var formPeer Peer var formPeer wireguard.Peer
if currentSession.FormData != nil { if currentSession.FormData != nil {
formPeer = currentSession.FormData.(Peer) formPeer = currentSession.FormData.(wireguard.Peer)
} }
if err := c.ShouldBind(&formPeer); err != nil { if err := c.ShouldBind(&formPeer); err != nil {
_ = s.updateFormInSession(c, formPeer) _ = s.updateFormInSession(c, formPeer)
@ -66,10 +62,8 @@ func (s *Server) PostAdminEditPeer(c *gin.Context) {
} }
// Clean list input // Clean list input
formPeer.IPs = common.ParseStringList(formPeer.IPsStr) formPeer.IPsStr = common.ListToString(common.ParseStringList(formPeer.IPsStr))
formPeer.AllowedIPs = common.ParseStringList(formPeer.AllowedIPsStr) formPeer.AllowedIPsStr = common.ListToString(common.ParseStringList(formPeer.AllowedIPsStr))
formPeer.IPsStr = common.ListToString(formPeer.IPs)
formPeer.AllowedIPsStr = common.ListToString(formPeer.AllowedIPs)
disabled := c.PostForm("isdisabled") != "" disabled := c.PostForm("isdisabled") != ""
now := time.Now() now := time.Now()
@ -92,37 +86,30 @@ func (s *Server) PostAdminEditPeer(c *gin.Context) {
} }
func (s *Server) GetAdminCreatePeer(c *gin.Context) { func (s *Server) GetAdminCreatePeer(c *gin.Context) {
device := s.peers.GetDevice()
currentSession, err := s.setNewPeerFormInSession(c) currentSession, err := s.setNewPeerFormInSession(c)
if err != nil { if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error())
return return
} }
c.HTML(http.StatusOK, "admin_edit_client.html", struct { c.HTML(http.StatusOK, "admin_edit_client.html", gin.H{
Route string "Route": c.Request.URL.Path,
Alerts []FlashData "Alerts": GetFlashes(c),
Session SessionData "Session": currentSession,
Static StaticData "Static": s.getStaticData(),
Peer Peer "Peer": currentSession.FormData.(wireguard.Peer),
Device Device "EditableKeys": s.config.Core.EditableKeys,
EditableKeys bool "Device": s.peers.GetDevice(currentSession.DeviceName),
}{ "DeviceNames": s.GetDeviceNames(),
Route: c.Request.URL.Path, "AdminEmail": s.config.Core.AdminUser,
Alerts: GetFlashes(c), "Csrf": csrf.GetToken(c),
Session: currentSession,
Static: s.getStaticData(),
Peer: currentSession.FormData.(Peer),
Device: device,
EditableKeys: s.config.Core.EditableKeys,
}) })
} }
func (s *Server) PostAdminCreatePeer(c *gin.Context) { func (s *Server) PostAdminCreatePeer(c *gin.Context) {
currentSession := GetSessionData(c) currentSession := GetSessionData(c)
var formPeer Peer var formPeer wireguard.Peer
if currentSession.FormData != nil { if currentSession.FormData != nil {
formPeer = currentSession.FormData.(Peer) formPeer = currentSession.FormData.(wireguard.Peer)
} }
if err := c.ShouldBind(&formPeer); err != nil { if err := c.ShouldBind(&formPeer); err != nil {
_ = s.updateFormInSession(c, formPeer) _ = s.updateFormInSession(c, formPeer)
@ -132,10 +119,8 @@ func (s *Server) PostAdminCreatePeer(c *gin.Context) {
} }
// Clean list input // Clean list input
formPeer.IPs = common.ParseStringList(formPeer.IPsStr) formPeer.IPsStr = common.ListToString(common.ParseStringList(formPeer.IPsStr))
formPeer.AllowedIPs = common.ParseStringList(formPeer.AllowedIPsStr) formPeer.AllowedIPsStr = common.ListToString(common.ParseStringList(formPeer.AllowedIPsStr))
formPeer.IPsStr = common.ListToString(formPeer.IPs)
formPeer.AllowedIPsStr = common.ListToString(formPeer.AllowedIPs)
disabled := c.PostForm("isdisabled") != "" disabled := c.PostForm("isdisabled") != ""
now := time.Now() now := time.Now()
@ -143,7 +128,7 @@ func (s *Server) PostAdminCreatePeer(c *gin.Context) {
formPeer.DeactivatedAt = &now formPeer.DeactivatedAt = &now
} }
if err := s.CreatePeer(formPeer); err != nil { if err := s.CreatePeer(currentSession.DeviceName, formPeer); err != nil {
_ = s.updateFormInSession(c, formPeer) _ = s.updateFormInSession(c, formPeer)
SetFlashMessage(c, "failed to add user: "+err.Error(), "danger") SetFlashMessage(c, "failed to add user: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/create?formerr=create") c.Redirect(http.StatusSeeOther, "/admin/peer/create?formerr=create")
@ -161,22 +146,16 @@ func (s *Server) GetAdminCreateLdapPeers(c *gin.Context) {
return return
} }
c.HTML(http.StatusOK, "admin_create_clients.html", struct { c.HTML(http.StatusOK, "admin_create_clients.html", gin.H{
Route string "Route": c.Request.URL.Path,
Alerts []FlashData "Alerts": GetFlashes(c),
Session SessionData "Session": currentSession,
Static StaticData "Static": s.getStaticData(),
Users []users.User "Users": s.users.GetFilteredAndSortedUsers("lastname", "asc", ""),
FormData LdapCreateForm "FormData": currentSession.FormData.(LdapCreateForm),
Device Device "Device": s.peers.GetDevice(currentSession.DeviceName),
}{ "DeviceNames": s.GetDeviceNames(),
Route: c.Request.URL.Path, "Csrf": csrf.GetToken(c),
Alerts: GetFlashes(c),
Session: currentSession,
Static: s.getStaticData(),
Users: s.users.GetFilteredAndSortedUsers("lastname", "asc", ""),
FormData: currentSession.FormData.(LdapCreateForm),
Device: s.peers.GetDevice(),
}) })
} }
@ -196,7 +175,7 @@ func (s *Server) PostAdminCreateLdapPeers(c *gin.Context) {
emails := common.ParseStringList(formData.Emails) emails := common.ParseStringList(formData.Emails)
for i := range emails { for i := range emails {
// TODO: also check email addr for validity? // TODO: also check email addr for validity?
if !strings.ContainsRune(emails[i], '@') || s.users.GetUser(emails[i]) == nil { if !strings.ContainsRune(emails[i], '@') {
_ = s.updateFormInSession(c, formData) _ = s.updateFormInSession(c, formData)
SetFlashMessage(c, "invalid email address: "+emails[i], "danger") SetFlashMessage(c, "invalid email address: "+emails[i], "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=mail") c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=mail")
@ -207,7 +186,7 @@ func (s *Server) PostAdminCreateLdapPeers(c *gin.Context) {
logrus.Infof("creating %d ldap peers", len(emails)) logrus.Infof("creating %d ldap peers", len(emails))
for i := range emails { for i := range emails {
if err := s.CreatePeerByEmail(emails[i], formData.Identifier, false); err != nil { if err := s.CreatePeerByEmail(currentSession.DeviceName, emails[i], formData.Identifier, false); err != nil {
_ = s.updateFormInSession(c, formData) _ = s.updateFormInSession(c, formData)
SetFlashMessage(c, "failed to add user: "+err.Error(), "danger") SetFlashMessage(c, "failed to add user: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=create") c.Redirect(http.StatusSeeOther, "/admin/peer/createldap?formerr=create")
@ -220,24 +199,24 @@ func (s *Server) PostAdminCreateLdapPeers(c *gin.Context) {
} }
func (s *Server) GetAdminDeletePeer(c *gin.Context) { func (s *Server) GetAdminDeletePeer(c *gin.Context) {
currentUser := s.peers.GetPeerByKey(c.Query("pkey")) currentPeer := s.peers.GetPeerByKey(c.Query("pkey"))
if err := s.DeletePeer(currentUser); err != nil { if err := s.DeletePeer(currentPeer); err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "Deletion error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "Deletion error", err.Error())
return return
} }
SetFlashMessage(c, "user deleted successfully", "success") SetFlashMessage(c, "peer deleted successfully", "success")
c.Redirect(http.StatusSeeOther, "/admin") c.Redirect(http.StatusSeeOther, "/admin")
} }
func (s *Server) GetPeerQRCode(c *gin.Context) { func (s *Server) GetPeerQRCode(c *gin.Context) {
user := s.peers.GetPeerByKey(c.Query("pkey")) peer := s.peers.GetPeerByKey(c.Query("pkey"))
currentSession := GetSessionData(c) currentSession := GetSessionData(c)
if !currentSession.IsAdmin && user.Email != currentSession.Email { if !currentSession.IsAdmin && peer.Email != currentSession.Email {
s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!") s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
return return
} }
png, err := user.GetQRCode() png, err := peer.GetQRCode()
if err != nil { if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "QRCode error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "QRCode error", err.Error())
return return
@ -247,38 +226,40 @@ func (s *Server) GetPeerQRCode(c *gin.Context) {
} }
func (s *Server) GetPeerConfig(c *gin.Context) { func (s *Server) GetPeerConfig(c *gin.Context) {
user := s.peers.GetPeerByKey(c.Query("pkey")) peer := s.peers.GetPeerByKey(c.Query("pkey"))
currentSession := GetSessionData(c) currentSession := GetSessionData(c)
if !currentSession.IsAdmin && user.Email != currentSession.Email { if !currentSession.IsAdmin && peer.Email != currentSession.Email {
s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!") s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
return return
} }
cfg, err := user.GetConfigFile(s.peers.GetDevice()) cfg, err := peer.GetConfigFile(s.peers.GetDevice(currentSession.DeviceName))
if err != nil { if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
return return
} }
c.Header("Content-Disposition", "attachment; filename="+user.GetConfigFileName()) c.Header("Content-Disposition", "attachment; filename="+peer.GetConfigFileName())
c.Data(http.StatusOK, "application/config", cfg) c.Data(http.StatusOK, "application/config", cfg)
return return
} }
func (s *Server) GetPeerConfigMail(c *gin.Context) { func (s *Server) GetPeerConfigMail(c *gin.Context) {
user := s.peers.GetPeerByKey(c.Query("pkey")) peer := s.peers.GetPeerByKey(c.Query("pkey"))
currentSession := GetSessionData(c) currentSession := GetSessionData(c)
if !currentSession.IsAdmin && user.Email != currentSession.Email { if !currentSession.IsAdmin && peer.Email != currentSession.Email {
s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!") s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
return return
} }
cfg, err := user.GetConfigFile(s.peers.GetDevice()) user := s.users.GetUser(peer.Email)
cfg, err := peer.GetConfigFile(s.peers.GetDevice(currentSession.DeviceName))
if err != nil { if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "ConfigFile error", err.Error())
return return
} }
png, err := user.GetQRCode() png, err := peer.GetQRCode()
if err != nil { if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "QRCode error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "QRCode error", err.Error())
return return
@ -286,11 +267,13 @@ func (s *Server) GetPeerConfigMail(c *gin.Context) {
// Apply mail template // Apply mail template
var tplBuff bytes.Buffer var tplBuff bytes.Buffer
if err := s.mailTpl.Execute(&tplBuff, struct { if err := s.mailTpl.Execute(&tplBuff, struct {
Client Peer Peer wireguard.Peer
User *users.User
QrcodePngName string QrcodePngName string
PortalUrl string PortalUrl string
}{ }{
Client: user, Peer: peer,
User: user,
QrcodePngName: "wireguard-config.png", QrcodePngName: "wireguard-config.png",
PortalUrl: s.config.Core.ExternalUrl, PortalUrl: s.config.Core.ExternalUrl,
}); err != nil { }); err != nil {
@ -301,7 +284,7 @@ func (s *Server) GetPeerConfigMail(c *gin.Context) {
// Send mail // Send mail
attachments := []common.MailAttachment{ attachments := []common.MailAttachment{
{ {
Name: user.GetConfigFileName(), Name: peer.GetConfigFileName(),
ContentType: "application/config", ContentType: "application/config",
Data: bytes.NewReader(cfg), Data: bytes.NewReader(cfg),
}, },
@ -314,24 +297,28 @@ func (s *Server) GetPeerConfigMail(c *gin.Context) {
if err := common.SendEmailWithAttachments(s.config.Email, s.config.Core.MailFrom, "", "WireGuard VPN Configuration", if err := common.SendEmailWithAttachments(s.config.Email, s.config.Core.MailFrom, "", "WireGuard VPN Configuration",
"Your mail client does not support HTML. Please find the configuration attached to this mail.", tplBuff.String(), "Your mail client does not support HTML. Please find the configuration attached to this mail.", tplBuff.String(),
[]string{user.Email}, attachments); err != nil { []string{peer.Email}, attachments); err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "Email error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "Email error", err.Error())
return return
} }
SetFlashMessage(c, "mail sent successfully", "success") SetFlashMessage(c, "mail sent successfully", "success")
c.Redirect(http.StatusSeeOther, "/admin") if strings.HasPrefix(c.Request.URL.Path, "/user") {
c.Redirect(http.StatusSeeOther, "/user/profile")
} else {
c.Redirect(http.StatusSeeOther, "/admin")
}
} }
func (s *Server) GetPeerStatus(c *gin.Context) { func (s *Server) GetPeerStatus(c *gin.Context) {
user := s.peers.GetPeerByKey(c.Query("pkey")) peer := s.peers.GetPeerByKey(c.Query("pkey"))
currentSession := GetSessionData(c) currentSession := GetSessionData(c)
if !currentSession.IsAdmin && user.Email != currentSession.Email { if !currentSession.IsAdmin && peer.Email != currentSession.Email {
s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!") s.GetHandleError(c, http.StatusUnauthorized, "No permissions", "You don't have permissions to view this resource!")
return return
} }
if user.Peer == nil { // no peer means disabled if peer.Peer == nil { // no peer means disabled
c.JSON(http.StatusOK, false) c.JSON(http.StatusOK, false)
return return
} }
@ -339,7 +326,7 @@ func (s *Server) GetPeerStatus(c *gin.Context) {
isOnline := false isOnline := false
ping := make(chan bool) ping := make(chan bool)
defer close(ping) defer close(ping)
for _, cidr := range user.IPs { for _, cidr := range peer.GetIPAddresses() {
ip, _, _ := net.ParseCIDR(cidr) ip, _, _ := net.ParseCIDR(cidr)
var ra *net.IPAddr var ra *net.IPAddr
if common.IsIPv6(ip.String()) { if common.IsIPv6(ip.String()) {

View File

@ -7,6 +7,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/h44z/wg-portal/internal/users" "github.com/h44z/wg-portal/internal/users"
csrf "github.com/utrack/gin-csrf"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -49,22 +50,15 @@ func (s *Server) GetAdminUsersIndex(c *gin.Context) {
dbUsers := s.users.GetFilteredAndSortedUsersUnscoped(currentSession.SortedBy["users"], currentSession.SortDirection["users"], currentSession.Search["users"]) dbUsers := s.users.GetFilteredAndSortedUsersUnscoped(currentSession.SortedBy["users"], currentSession.SortDirection["users"], currentSession.Search["users"])
c.HTML(http.StatusOK, "admin_user_index.html", struct { c.HTML(http.StatusOK, "admin_user_index.html", gin.H{
Route string "Route": c.Request.URL.Path,
Alerts []FlashData "Alerts": GetFlashes(c),
Session SessionData "Session": currentSession,
Static StaticData "Static": s.getStaticData(),
Users []users.User "Users": dbUsers,
TotalUsers int "TotalUsers": len(s.users.GetUsers()),
Device Device "Device": s.peers.GetDevice(currentSession.DeviceName),
}{ "DeviceNames": s.GetDeviceNames(),
Route: c.Request.URL.Path,
Alerts: GetFlashes(c),
Session: currentSession,
Static: s.getStaticData(),
Users: dbUsers,
TotalUsers: len(s.users.GetUsers()),
Device: s.peers.GetDevice(),
}) })
} }
@ -77,21 +71,16 @@ func (s *Server) GetAdminUsersEdit(c *gin.Context) {
return return
} }
c.HTML(http.StatusOK, "admin_edit_user.html", struct { c.HTML(http.StatusOK, "admin_edit_user.html", gin.H{
Route string "Route": c.Request.URL.Path,
Alerts []FlashData "Alerts": GetFlashes(c),
Session SessionData "Session": currentSession,
Static StaticData "Static": s.getStaticData(),
User users.User "User": currentSession.FormData.(users.User),
Device Device "Device": s.peers.GetDevice(currentSession.DeviceName),
Epoch time.Time "DeviceNames": s.GetDeviceNames(),
}{ "Epoch": time.Time{},
Route: c.Request.URL.Path, "Csrf": csrf.GetToken(c),
Alerts: GetFlashes(c),
Session: currentSession,
Static: s.getStaticData(),
User: currentSession.FormData.(users.User),
Device: s.peers.GetDevice(),
}) })
} }
@ -160,21 +149,16 @@ func (s *Server) GetAdminUsersCreate(c *gin.Context) {
return return
} }
c.HTML(http.StatusOK, "admin_edit_user.html", struct { c.HTML(http.StatusOK, "admin_edit_user.html", gin.H{
Route string "Route": c.Request.URL.Path,
Alerts []FlashData "Alerts": GetFlashes(c),
Session SessionData "Session": currentSession,
Static StaticData "Static": s.getStaticData(),
User users.User "User": currentSession.FormData.(users.User),
Device Device "Device": s.peers.GetDevice(currentSession.DeviceName),
Epoch time.Time "DeviceNames": s.GetDeviceNames(),
}{ "Epoch": time.Time{},
Route: c.Request.URL.Path, "Csrf": csrf.GetToken(c),
Alerts: GetFlashes(c),
Session: currentSession,
Static: s.getStaticData(),
User: currentSession.FormData.(users.User),
Device: s.peers.GetDevice(),
}) })
} }
@ -218,7 +202,7 @@ func (s *Server) PostAdminUsersCreate(c *gin.Context) {
formUser.IsAdmin = c.PostForm("isadmin") == "true" formUser.IsAdmin = c.PostForm("isadmin") == "true"
formUser.Source = users.UserSourceDatabase formUser.Source = users.UserSourceDatabase
if err := s.CreateUser(formUser); err != nil { if err := s.CreateUser(formUser, currentSession.DeviceName); err != nil {
_ = s.updateFormInSession(c, formUser) _ = s.updateFormInSession(c, formUser)
SetFlashMessage(c, "failed to add user: "+err.Error(), "danger") SetFlashMessage(c, "failed to add user: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/users/create?formerr=create") c.Redirect(http.StatusSeeOther, "/admin/users/create?formerr=create")

View File

@ -1,724 +0,0 @@
package server
import (
"bytes"
"crypto/md5"
"fmt"
"net"
"reflect"
"regexp"
"sort"
"strings"
"text/template"
"time"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
"github.com/h44z/wg-portal/internal/common"
"github.com/h44z/wg-portal/internal/users"
"github.com/h44z/wg-portal/internal/wireguard"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/skip2/go-qrcode"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"gorm.io/gorm"
)
//
// CUSTOM VALIDATORS ----------------------------------------------------------------------------
//
var cidrList validator.Func = func(fl validator.FieldLevel) bool {
cidrListStr := fl.Field().String()
cidrList := common.ParseStringList(cidrListStr)
for i := range cidrList {
_, _, err := net.ParseCIDR(cidrList[i])
if err != nil {
return false
}
}
return true
}
var ipList validator.Func = func(fl validator.FieldLevel) bool {
ipListStr := fl.Field().String()
ipList := common.ParseStringList(ipListStr)
for i := range ipList {
ip := net.ParseIP(ipList[i])
if ip == nil {
return false
}
}
return true
}
func init() {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
_ = v.RegisterValidation("cidrlist", cidrList)
_ = v.RegisterValidation("iplist", ipList)
}
}
//
// PEER ----------------------------------------------------------------------------------------
//
type Peer struct {
Peer *wgtypes.Peer `gorm:"-"` // WireGuard peer
User *users.User `gorm:"-"` // user reference for the peer
Config string `gorm:"-"`
UID string `form:"uid" binding:"alphanum"` // uid for html identification
IsOnline bool `gorm:"-"`
IsNew bool `gorm:"-"`
Identifier string `form:"identifier" binding:"required,lt=64"` // Identifier AND Email make a WireGuard peer unique
Email string `gorm:"index" form:"mail" binding:"required,email"`
LastHandshake string `gorm:"-"`
LastHandshakeTime string `gorm:"-"`
IgnorePersistentKeepalive bool `form:"ignorekeepalive"`
PresharedKey string `form:"presharedkey" binding:"omitempty,base64"`
AllowedIPsStr string `form:"allowedip" binding:"cidrlist"`
IPsStr string `form:"ip" binding:"cidrlist"`
AllowedIPs []string `gorm:"-"` // IPs that are used in the client config file
IPs []string `gorm:"-"` // The IPs of the client
PrivateKey string `form:"privkey" binding:"omitempty,base64"`
PublicKey string `gorm:"primaryKey" form:"pubkey" binding:"required,base64"`
DeactivatedAt *time.Time
CreatedBy string
UpdatedBy string
CreatedAt time.Time
UpdatedAt time.Time
}
func (p Peer) GetConfig() wgtypes.PeerConfig {
publicKey, _ := wgtypes.ParseKey(p.PublicKey)
var presharedKey *wgtypes.Key
if p.PresharedKey != "" {
presharedKeyTmp, _ := wgtypes.ParseKey(p.PresharedKey)
presharedKey = &presharedKeyTmp
}
cfg := wgtypes.PeerConfig{
PublicKey: publicKey,
Remove: false,
UpdateOnly: false,
PresharedKey: presharedKey,
Endpoint: nil,
PersistentKeepaliveInterval: nil,
ReplaceAllowedIPs: true,
AllowedIPs: make([]net.IPNet, len(p.IPs)),
}
for i, ip := range p.IPs {
_, ipNet, err := net.ParseCIDR(ip)
if err == nil {
cfg.AllowedIPs[i] = *ipNet
}
}
return cfg
}
func (p Peer) GetConfigFile(device Device) ([]byte, error) {
tpl, err := template.New("client").Funcs(template.FuncMap{"StringsJoin": strings.Join}).Parse(wireguard.ClientCfgTpl)
if err != nil {
return nil, errors.Wrap(err, "failed to parse client template")
}
var tplBuff bytes.Buffer
err = tpl.Execute(&tplBuff, struct {
Client Peer
Server Device
}{
Client: p,
Server: device,
})
if err != nil {
return nil, errors.Wrap(err, "failed to execute client template")
}
return tplBuff.Bytes(), nil
}
func (p Peer) GetQRCode() ([]byte, error) {
png, err := qrcode.Encode(p.Config, qrcode.Medium, 250)
if err != nil {
logrus.WithFields(logrus.Fields{
"err": err,
}).Error("failed to create qrcode")
return nil, errors.Wrap(err, "failed to encode qrcode")
}
return png, nil
}
func (p Peer) IsValid() bool {
if p.PublicKey == "" {
return false
}
return true
}
func (p Peer) ToMap() map[string]string {
out := make(map[string]string)
v := reflect.ValueOf(p)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
typ := v.Type()
for i := 0; i < v.NumField(); i++ {
// gets us a StructField
fi := typ.Field(i)
if tagv := fi.Tag.Get("form"); tagv != "" {
// set key of map to value in struct field
out[tagv] = v.Field(i).String()
}
}
return out
}
func (p Peer) GetConfigFileName() string {
reg := regexp.MustCompile("[^a-zA-Z0-9_-]+")
return reg.ReplaceAllString(strings.ReplaceAll(p.Identifier, " ", "-"), "") + ".conf"
}
//
// DEVICE --------------------------------------------------------------------------------------
//
type Device struct {
Interface *wgtypes.Device `gorm:"-"`
DeviceName string `form:"device" gorm:"primaryKey" binding:"required,alphanum"`
PrivateKey string `form:"privkey" binding:"required,base64"`
PublicKey string `form:"pubkey" binding:"required,base64"`
PersistentKeepalive int `form:"keepalive" binding:"gte=0"`
ListenPort int `form:"port" binding:"required,gt=0"`
Mtu int `form:"mtu" binding:"gte=0,lte=1500"`
Endpoint string `form:"endpoint" binding:"required,hostname_port"`
AllowedIPsStr string `form:"allowedip" binding:"cidrlist"`
IPsStr string `form:"ip" binding:"required,cidrlist"`
AllowedIPs []string `gorm:"-"` // IPs that are used in the client config file
IPs []string `gorm:"-"` // The IPs of the client
DNSStr string `form:"dns" binding:"iplist"`
DNS []string `gorm:"-"` // The DNS servers of the client
PreUp string `form:"preup"`
PostUp string `form:"postup"`
PreDown string `form:"predown"`
PostDown string `form:"postdown"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (d Device) IsValid() bool {
if d.PublicKey == "" {
return false
}
if len(d.IPs) == 0 {
return false
}
if d.Endpoint == "" {
return false
}
return true
}
func (d Device) GetConfig() wgtypes.Config {
var privateKey *wgtypes.Key
if d.PrivateKey != "" {
pKey, _ := wgtypes.ParseKey(d.PrivateKey)
privateKey = &pKey
}
cfg := wgtypes.Config{
PrivateKey: privateKey,
ListenPort: &d.ListenPort,
}
return cfg
}
func (d Device) GetConfigFile(peers []Peer) ([]byte, error) {
tpl, err := template.New("server").Funcs(template.FuncMap{"StringsJoin": strings.Join}).Parse(wireguard.DeviceCfgTpl)
if err != nil {
return nil, errors.Wrap(err, "failed to parse server template")
}
var tplBuff bytes.Buffer
err = tpl.Execute(&tplBuff, struct {
Clients []Peer
Server Device
}{
Clients: peers,
Server: d,
})
if err != nil {
return nil, errors.Wrap(err, "failed to execute server template")
}
return tplBuff.Bytes(), nil
}
//
// PEER-MANAGER --------------------------------------------------------------------------------
//
type PeerManager struct {
db *gorm.DB
wg *wireguard.Manager
users *users.Manager
}
func NewPeerManager(cfg *common.Config, wg *wireguard.Manager, userDB *users.Manager) (*PeerManager, error) {
um := &PeerManager{wg: wg, users: userDB}
var err error
um.db, err = users.GetDatabaseForConfig(&cfg.Database)
if err != nil {
return nil, errors.WithMessage(err, "failed to open peer database")
}
err = um.db.AutoMigrate(&Peer{}, &Device{})
if err != nil {
return nil, errors.WithMessage(err, "failed to migrate peer database")
}
return um, nil
}
func (u *PeerManager) InitFromCurrentInterface() error {
peers, err := u.wg.GetPeerList()
if err != nil {
return errors.Wrapf(err, "failed to get peer list")
}
device, err := u.wg.GetDeviceInfo()
if err != nil {
return errors.Wrapf(err, "failed to get device info")
}
var ipAddresses []string
var mtu int
if u.wg.Cfg.ManageIPAddresses {
if ipAddresses, err = u.wg.GetIPAddress(); err != nil {
return errors.Wrapf(err, "failed to get ip address")
}
if mtu, err = u.wg.GetMTU(); err != nil {
return errors.Wrapf(err, "failed to get MTU")
}
}
// Check if entries already exist in database, if not create them
for _, peer := range peers {
if err := u.validateOrCreatePeer(peer); err != nil {
return errors.WithMessagef(err, "failed to validate peer %s", peer.PublicKey)
}
}
if err := u.validateOrCreateDevice(*device, ipAddresses, mtu); err != nil {
return errors.WithMessagef(err, "failed to validate device %s", device.Name)
}
return nil
}
func (u *PeerManager) validateOrCreatePeer(wgPeer wgtypes.Peer) error {
peer := Peer{}
u.db.Where("public_key = ?", wgPeer.PublicKey.String()).FirstOrInit(&peer)
if peer.PublicKey == "" { // peer not found, create
peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(wgPeer.PublicKey.String())))
peer.PublicKey = wgPeer.PublicKey.String()
peer.PrivateKey = "" // UNKNOWN
if wgPeer.PresharedKey != (wgtypes.Key{}) {
peer.PresharedKey = wgPeer.PresharedKey.String()
}
peer.Email = "autodetected@example.com"
peer.Identifier = "Autodetected (" + peer.PublicKey[0:8] + ")"
peer.UpdatedAt = time.Now()
peer.CreatedAt = time.Now()
peer.AllowedIPs = make([]string, 0) // UNKNOWN
peer.IPs = make([]string, len(wgPeer.AllowedIPs))
for i, ip := range wgPeer.AllowedIPs {
peer.IPs[i] = ip.String()
}
peer.AllowedIPsStr = strings.Join(peer.AllowedIPs, ", ")
peer.IPsStr = strings.Join(peer.IPs, ", ")
res := u.db.Create(&peer)
if res.Error != nil {
return errors.Wrapf(res.Error, "failed to create autodetected peer %s", peer.PublicKey)
}
}
return nil
}
func (u *PeerManager) validateOrCreateDevice(dev wgtypes.Device, ipAddresses []string, mtu int) error {
device := Device{}
u.db.Where("device_name = ?", dev.Name).FirstOrInit(&device)
if device.PublicKey == "" { // device not found, create
device.PublicKey = dev.PublicKey.String()
device.PrivateKey = dev.PrivateKey.String()
device.DeviceName = dev.Name
device.ListenPort = dev.ListenPort
device.Mtu = 0
device.PersistentKeepalive = 16 // Default
device.IPsStr = strings.Join(ipAddresses, ", ")
if mtu == wireguard.DefaultMTU {
mtu = 0
}
device.Mtu = mtu
res := u.db.Create(&device)
if res.Error != nil {
return errors.Wrapf(res.Error, "failed to create autodetected device")
}
}
return nil
}
func (u *PeerManager) populatePeerData(peer *Peer) {
peer.AllowedIPs = strings.Split(peer.AllowedIPsStr, ", ")
peer.IPs = strings.Split(peer.IPsStr, ", ")
// Set config file
tmpCfg, _ := peer.GetConfigFile(u.GetDevice())
peer.Config = string(tmpCfg)
// set data from WireGuard interface
peer.Peer, _ = u.wg.GetPeer(peer.PublicKey)
peer.LastHandshake = "never"
peer.LastHandshakeTime = "Never connected, or user is disabled."
if peer.Peer != nil {
since := time.Since(peer.Peer.LastHandshakeTime)
sinceSeconds := int(since.Round(time.Second).Seconds())
sinceMinutes := int(sinceSeconds / 60)
sinceSeconds -= sinceMinutes * 60
if sinceMinutes > 2*10080 { // 2 weeks
peer.LastHandshake = "a while ago"
} else if sinceMinutes > 10080 { // 1 week
peer.LastHandshake = "a week ago"
} else {
peer.LastHandshake = fmt.Sprintf("%02dm %02ds", sinceMinutes, sinceSeconds)
}
peer.LastHandshakeTime = peer.Peer.LastHandshakeTime.Format(time.UnixDate)
}
peer.IsOnline = false
// set user data
peer.User = u.users.GetUser(peer.Email)
}
func (u *PeerManager) populateDeviceData(device *Device) {
device.AllowedIPs = strings.Split(device.AllowedIPsStr, ", ")
device.IPs = strings.Split(device.IPsStr, ", ")
device.DNS = strings.Split(device.DNSStr, ", ")
// set data from WireGuard interface
device.Interface, _ = u.wg.GetDeviceInfo()
}
func (u *PeerManager) GetAllPeers() []Peer {
peers := make([]Peer, 0)
u.db.Find(&peers)
for i := range peers {
u.populatePeerData(&peers[i])
}
return peers
}
func (u *PeerManager) GetActivePeers() []Peer {
peers := make([]Peer, 0)
u.db.Where("deactivated_at IS NULL").Find(&peers)
for i := range peers {
u.populatePeerData(&peers[i])
}
return peers
}
func (u *PeerManager) GetFilteredAndSortedPeers(sortKey, sortDirection, search string) []Peer {
peers := make([]Peer, 0)
u.db.Find(&peers)
filteredPeers := make([]Peer, 0, len(peers))
for i := range peers {
u.populatePeerData(&peers[i])
if search == "" ||
strings.Contains(peers[i].Email, search) ||
strings.Contains(peers[i].Identifier, search) ||
strings.Contains(peers[i].PublicKey, search) {
filteredPeers = append(filteredPeers, peers[i])
}
}
sort.Slice(filteredPeers, func(i, j int) bool {
var sortValueLeft string
var sortValueRight string
switch sortKey {
case "id":
sortValueLeft = filteredPeers[i].Identifier
sortValueRight = filteredPeers[j].Identifier
case "pubKey":
sortValueLeft = filteredPeers[i].PublicKey
sortValueRight = filteredPeers[j].PublicKey
case "mail":
sortValueLeft = filteredPeers[i].Email
sortValueRight = filteredPeers[j].Email
case "ip":
sortValueLeft = filteredPeers[i].IPsStr
sortValueRight = filteredPeers[j].IPsStr
case "handshake":
if filteredPeers[i].Peer == nil {
return false
} else if filteredPeers[j].Peer == nil {
return true
}
sortValueLeft = filteredPeers[i].Peer.LastHandshakeTime.Format(time.RFC3339)
sortValueRight = filteredPeers[j].Peer.LastHandshakeTime.Format(time.RFC3339)
}
if sortDirection == "asc" {
return sortValueLeft < sortValueRight
} else {
return sortValueLeft > sortValueRight
}
})
return filteredPeers
}
func (u *PeerManager) GetSortedPeersForEmail(sortKey, sortDirection, email string) []Peer {
peers := make([]Peer, 0)
u.db.Where("email = ?", email).Find(&peers)
for i := range peers {
u.populatePeerData(&peers[i])
}
sort.Slice(peers, func(i, j int) bool {
var sortValueLeft string
var sortValueRight string
switch sortKey {
case "id":
sortValueLeft = peers[i].Identifier
sortValueRight = peers[j].Identifier
case "pubKey":
sortValueLeft = peers[i].PublicKey
sortValueRight = peers[j].PublicKey
case "mail":
sortValueLeft = peers[i].Email
sortValueRight = peers[j].Email
case "ip":
sortValueLeft = peers[i].IPsStr
sortValueRight = peers[j].IPsStr
case "handshake":
if peers[i].Peer == nil {
return true
} else if peers[j].Peer == nil {
return false
}
sortValueLeft = peers[i].Peer.LastHandshakeTime.Format(time.RFC3339)
sortValueRight = peers[j].Peer.LastHandshakeTime.Format(time.RFC3339)
}
if sortDirection == "asc" {
return sortValueLeft < sortValueRight
} else {
return sortValueLeft > sortValueRight
}
})
return peers
}
func (u *PeerManager) GetDevice() Device {
devices := make([]Device, 0, 1)
u.db.Find(&devices)
for i := range devices {
u.populateDeviceData(&devices[i])
}
return devices[0] // use first device for now... more to come?
}
func (u *PeerManager) GetPeerByKey(publicKey string) Peer {
peer := Peer{}
u.db.Where("public_key = ?", publicKey).FirstOrInit(&peer)
u.populatePeerData(&peer)
return peer
}
func (u *PeerManager) GetPeersByMail(mail string) []Peer {
var peers []Peer
u.db.Where("email = ?", mail).Find(&peers)
for i := range peers {
u.populatePeerData(&peers[i])
}
return peers
}
func (u *PeerManager) CreatePeer(peer Peer) error {
peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey)))
peer.UpdatedAt = time.Now()
peer.CreatedAt = time.Now()
peer.AllowedIPsStr = strings.Join(peer.AllowedIPs, ", ")
peer.IPsStr = strings.Join(peer.IPs, ", ")
res := u.db.Create(&peer)
if res.Error != nil {
logrus.Errorf("failed to create peer: %v", res.Error)
return errors.Wrap(res.Error, "failed to create peer")
}
return nil
}
func (u *PeerManager) UpdatePeer(peer Peer) error {
peer.UpdatedAt = time.Now()
peer.AllowedIPsStr = strings.Join(peer.AllowedIPs, ", ")
peer.IPsStr = strings.Join(peer.IPs, ", ")
res := u.db.Save(&peer)
if res.Error != nil {
logrus.Errorf("failed to update peer: %v", res.Error)
return errors.Wrap(res.Error, "failed to update peer")
}
return nil
}
func (u *PeerManager) DeletePeer(peer Peer) error {
res := u.db.Delete(&peer)
if res.Error != nil {
logrus.Errorf("failed to delete peer: %v", res.Error)
return errors.Wrap(res.Error, "failed to delete peer")
}
return nil
}
func (u *PeerManager) UpdateDevice(device Device) error {
device.UpdatedAt = time.Now()
device.AllowedIPsStr = strings.Join(device.AllowedIPs, ", ")
device.IPsStr = strings.Join(device.IPs, ", ")
device.DNSStr = strings.Join(device.DNS, ", ")
res := u.db.Save(&device)
if res.Error != nil {
logrus.Errorf("failed to update device: %v", res.Error)
return errors.Wrap(res.Error, "failed to update device")
}
return nil
}
func (u *PeerManager) GetAllReservedIps() ([]string, error) {
reservedIps := make([]string, 0)
peers := u.GetAllPeers()
for _, user := range peers {
for _, cidr := range user.IPs {
if cidr == "" {
continue
}
ip, _, err := net.ParseCIDR(cidr)
if err != nil {
return nil, errors.Wrap(err, "failed to parse cidr")
}
reservedIps = append(reservedIps, ip.String())
}
}
device := u.GetDevice()
for _, cidr := range device.IPs {
if cidr == "" {
continue
}
ip, _, err := net.ParseCIDR(cidr)
if err != nil {
return nil, errors.Wrap(err, "failed to parse cidr")
}
reservedIps = append(reservedIps, ip.String())
}
return reservedIps, nil
}
func (u *PeerManager) IsIPReserved(cidr string) bool {
reserved, err := u.GetAllReservedIps()
if err != nil {
return true // in case something failed, assume the ip is reserved
}
ip, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
return true
}
// this two addresses are not usable
broadcastAddr := common.BroadcastAddr(ipnet).String()
networkAddr := ipnet.IP.String()
address := ip.String()
if address == broadcastAddr || address == networkAddr {
return true
}
for _, r := range reserved {
if address == r {
return true
}
}
return false
}
// GetAvailableIp search for an available ip in cidr against a list of reserved ips
func (u *PeerManager) GetAvailableIp(cidr string) (string, error) {
reserved, err := u.GetAllReservedIps()
if err != nil {
return "", errors.WithMessage(err, "failed to get all reserved IP addresses")
}
ip, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
return "", errors.Wrap(err, "failed to parse cidr")
}
// this two addresses are not usable
broadcastAddr := common.BroadcastAddr(ipnet).String()
networkAddr := ipnet.IP.String()
for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); common.IncreaseIP(ip) {
ok := true
address := ip.String()
for _, r := range reserved {
if address == r {
ok = false
break
}
}
if ok && address != networkAddr && address != broadcastAddr {
netMask := "/32"
if common.IsIPv6(address) {
netMask = "/128"
}
return address + netMask, nil
}
}
return "", errors.New("no more available address from cidr")
}

View File

@ -4,14 +4,14 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
wg_portal "github.com/h44z/wg-portal" wgportal "github.com/h44z/wg-portal"
) )
func SetupRoutes(s *Server) { func SetupRoutes(s *Server) {
// Startpage // Startpage
s.server.GET("/", s.GetIndex) s.server.GET("/", s.GetIndex)
s.server.GET("/favicon.ico", func(c *gin.Context) { s.server.GET("/favicon.ico", func(c *gin.Context) {
file, _ := wg_portal.Statics.ReadFile("assets/img/favicon.ico") file, _ := wgportal.Statics.ReadFile("assets/img/favicon.ico")
c.Data( c.Data(
http.StatusOK, http.StatusOK,
"image/x-icon", "image/x-icon",
@ -32,6 +32,7 @@ func SetupRoutes(s *Server) {
admin.GET("/device/edit", s.GetAdminEditInterface) admin.GET("/device/edit", s.GetAdminEditInterface)
admin.POST("/device/edit", s.PostAdminEditInterface) admin.POST("/device/edit", s.PostAdminEditInterface)
admin.GET("/device/download", s.GetInterfaceConfig) admin.GET("/device/download", s.GetInterfaceConfig)
admin.GET("/device/write", s.GetSaveConfig)
admin.GET("/device/applyglobals", s.GetApplyGlobalConfig) admin.GET("/device/applyglobals", s.GetApplyGlobalConfig)
admin.GET("/peer/edit", s.GetAdminEditPeer) admin.GET("/peer/edit", s.GetAdminEditPeer)
admin.POST("/peer/edit", s.PostAdminEditPeer) admin.POST("/peer/edit", s.PostAdminEditPeer)

View File

@ -11,12 +11,13 @@ import (
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/memstore" "github.com/gin-contrib/sessions/memstore"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
wg_portal "github.com/h44z/wg-portal" wgportal "github.com/h44z/wg-portal"
ldapprovider "github.com/h44z/wg-portal/internal/authentication/providers/ldap" ldapprovider "github.com/h44z/wg-portal/internal/authentication/providers/ldap"
passwordprovider "github.com/h44z/wg-portal/internal/authentication/providers/password" passwordprovider "github.com/h44z/wg-portal/internal/authentication/providers/password"
"github.com/h44z/wg-portal/internal/common" "github.com/h44z/wg-portal/internal/common"
@ -25,6 +26,8 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
ginlogrus "github.com/toorop/gin-logrus" ginlogrus "github.com/toorop/gin-logrus"
csrf "github.com/utrack/gin-csrf"
"gorm.io/gorm"
) )
const SessionIdentifier = "wgPortalSession" const SessionIdentifier = "wgPortalSession"
@ -32,18 +35,19 @@ const SessionIdentifier = "wgPortalSession"
func init() { func init() {
gob.Register(SessionData{}) gob.Register(SessionData{})
gob.Register(FlashData{}) gob.Register(FlashData{})
gob.Register(Peer{}) gob.Register(wireguard.Peer{})
gob.Register(Device{}) gob.Register(wireguard.Device{})
gob.Register(LdapCreateForm{}) gob.Register(LdapCreateForm{})
gob.Register(users.User{}) gob.Register(users.User{})
} }
type SessionData struct { type SessionData struct {
LoggedIn bool LoggedIn bool
IsAdmin bool IsAdmin bool
Firstname string Firstname string
Lastname string Lastname string
Email string Email string
DeviceName string
SortedBy map[string]string SortedBy map[string]string
SortDirection map[string]string SortDirection map[string]string
@ -69,14 +73,15 @@ type StaticData struct {
type Server struct { type Server struct {
ctx context.Context ctx context.Context
config *common.Config config *Config
server *gin.Engine server *gin.Engine
mailTpl *template.Template mailTpl *template.Template
auth *AuthManager auth *AuthManager
db *gorm.DB
users *users.Manager users *users.Manager
wg *wireguard.Manager wg *wireguard.Manager
peers *PeerManager peers *wireguard.PeerManager
} }
func (s *Server) Setup(ctx context.Context) error { func (s *Server) Setup(ctx context.Context) error {
@ -90,9 +95,19 @@ func (s *Server) Setup(ctx context.Context) error {
// Init rand // Init rand
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
s.config = common.NewConfig() s.config = NewConfig()
s.ctx = ctx s.ctx = ctx
// Setup database connection
s.db, err = common.GetDatabaseForConfig(&s.config.Database)
if err != nil {
return errors.WithMessage(err, "database setup failed")
}
err = common.MigrateDatabase(s.db, Version)
if err != nil {
return errors.WithMessage(err, "database migration failed")
}
// Setup http server // Setup http server
gin.SetMode(gin.DebugMode) gin.SetMode(gin.DebugMode)
gin.DefaultWriter = ioutil.Discard gin.DefaultWriter = ioutil.Discard
@ -101,27 +116,43 @@ func (s *Server) Setup(ctx context.Context) error {
s.server.Use(ginlogrus.Logger(logrus.StandardLogger())) s.server.Use(ginlogrus.Logger(logrus.StandardLogger()))
} }
s.server.Use(gin.Recovery()) s.server.Use(gin.Recovery())
s.server.Use(sessions.Sessions("authsession", memstore.NewStore([]byte(s.config.Core.SessionSecret))))
s.server.Use(csrf.Middleware(csrf.Options{
Secret: s.config.Core.SessionSecret,
ErrorFunc: func(c *gin.Context) {
c.String(400, "CSRF token mismatch")
c.Abort()
},
}))
s.server.SetFuncMap(template.FuncMap{ s.server.SetFuncMap(template.FuncMap{
"formatBytes": common.ByteCountSI, "formatBytes": common.ByteCountSI,
"urlEncode": url.QueryEscape, "urlEncode": url.QueryEscape,
"startsWith": strings.HasPrefix,
"userForEmail": func(users []users.User, email string) *users.User {
for i := range users {
if users[i].Email == email {
return &users[i]
}
}
return nil
},
}) })
// Setup templates // Setup templates
templates := template.Must(template.New("").Funcs(s.server.FuncMap).ParseFS(wg_portal.Templates, "assets/tpl/*.html")) templates := template.Must(template.New("").Funcs(s.server.FuncMap).ParseFS(wgportal.Templates, "assets/tpl/*.html"))
s.server.SetHTMLTemplate(templates) s.server.SetHTMLTemplate(templates)
s.server.Use(sessions.Sessions("authsession", memstore.NewStore([]byte("secret")))) // TODO: change key?
// Serve static files // Serve static files
s.server.StaticFS("/css", http.FS(fsMust(fs.Sub(wg_portal.Statics, "assets/css")))) s.server.StaticFS("/css", http.FS(fsMust(fs.Sub(wgportal.Statics, "assets/css"))))
s.server.StaticFS("/js", http.FS(fsMust(fs.Sub(wg_portal.Statics, "assets/js")))) s.server.StaticFS("/js", http.FS(fsMust(fs.Sub(wgportal.Statics, "assets/js"))))
s.server.StaticFS("/img", http.FS(fsMust(fs.Sub(wg_portal.Statics, "assets/img")))) s.server.StaticFS("/img", http.FS(fsMust(fs.Sub(wgportal.Statics, "assets/img"))))
s.server.StaticFS("/fonts", http.FS(fsMust(fs.Sub(wg_portal.Statics, "assets/fonts")))) s.server.StaticFS("/fonts", http.FS(fsMust(fs.Sub(wgportal.Statics, "assets/fonts"))))
// Setup all routes // Setup all routes
SetupRoutes(s) SetupRoutes(s)
// Setup user database (also needed for database authentication) // Setup user database (also needed for database authentication)
s.users, err = users.NewManager(&s.config.Database) s.users, err = users.NewManager(s.db)
if err != nil { if err != nil {
return errors.WithMessage(err, "user-manager initialization failed") return errors.WithMessage(err, "user-manager initialization failed")
} }
@ -153,18 +184,18 @@ func (s *Server) Setup(ctx context.Context) error {
} }
// Setup peer manager // Setup peer manager
if s.peers, err = NewPeerManager(s.config, s.wg, s.users); err != nil { if s.peers, err = wireguard.NewPeerManager(s.db, s.wg); err != nil {
return errors.WithMessage(err, "unable to setup peer manager") return errors.WithMessage(err, "unable to setup peer manager")
} }
if err = s.peers.InitFromCurrentInterface(); err != nil {
return errors.WithMessage(err, "unable to initialize peer manager") for _, deviceName := range s.wg.Cfg.DeviceNames {
} if err = s.RestoreWireGuardInterface(deviceName); err != nil {
if err = s.RestoreWireGuardInterface(); err != nil { return errors.WithMessagef(err, "unable to restore WireGuard state for %s", deviceName)
return errors.WithMessage(err, "unable to restore WireGuard state") }
} }
// Setup mail template // Setup mail template
s.mailTpl, err = template.New("email.html").ParseFS(wg_portal.Templates, "assets/tpl/email.html") s.mailTpl, err = template.New("email.html").ParseFS(wgportal.Templates, "assets/tpl/email.html")
if err != nil { if err != nil {
return errors.Wrap(err, "unable to pare mail template") return errors.Wrap(err, "unable to pare mail template")
} }
@ -174,6 +205,8 @@ func (s *Server) Setup(ctx context.Context) error {
} }
func (s *Server) Run() { func (s *Server) Run() {
logrus.Infof("starting web service on %s", s.config.Core.ListeningAddress)
// Start ldap sync // Start ldap sync
if s.config.Core.LdapEnabled { if s.config.Core.LdapEnabled {
go s.SyncLdapWithUserDatabase() go s.SyncLdapWithUserDatabase()
@ -233,11 +266,12 @@ func GetSessionData(c *gin.Context) SessionData {
} else { } else {
sessionData = SessionData{ sessionData = SessionData{
Search: map[string]string{"peers": "", "userpeers": "", "users": ""}, Search: map[string]string{"peers": "", "userpeers": "", "users": ""},
SortedBy: map[string]string{"peers": "mail", "userpeers": "mail", "users": "email"}, SortedBy: map[string]string{"peers": "handshake", "userpeers": "id", "users": "email"},
SortDirection: map[string]string{"peers": "asc", "userpeers": "asc", "users": "asc"}, SortDirection: map[string]string{"peers": "desc", "userpeers": "asc", "users": "asc"},
Email: "", Email: "",
Firstname: "", Firstname: "",
Lastname: "", Lastname: "",
DeviceName: "",
IsAdmin: false, IsAdmin: false,
LoggedIn: false, LoggedIn: false,
} }

View File

@ -4,104 +4,104 @@ import (
"crypto/md5" "crypto/md5"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"path"
"syscall" "syscall"
"time" "time"
"github.com/h44z/wg-portal/internal/common"
"github.com/h44z/wg-portal/internal/users" "github.com/h44z/wg-portal/internal/users"
"github.com/h44z/wg-portal/internal/wireguard"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"gorm.io/gorm" "gorm.io/gorm"
) )
func (s *Server) PrepareNewPeer() (Peer, error) { // PrepareNewPeer initiates a new peer for the given WireGuard device.
device := s.peers.GetDevice() func (s *Server) PrepareNewPeer(device string) (wireguard.Peer, error) {
dev := s.peers.GetDevice(device)
deviceIPs := dev.GetIPAddresses()
peer := Peer{} peer := wireguard.Peer{}
peer.IsNew = true peer.IsNew = true
peer.AllowedIPsStr = device.AllowedIPsStr
peer.IPs = make([]string, len(device.IPs)) switch dev.Type {
for i := range device.IPs { case wireguard.DeviceTypeServer:
freeIP, err := s.peers.GetAvailableIp(device.IPs[i]) peerIPs := make([]string, len(deviceIPs))
if err != nil { for i := range deviceIPs {
return Peer{}, errors.WithMessage(err, "failed to get available IP addresses") freeIP, err := s.peers.GetAvailableIp(device, deviceIPs[i])
if err != nil {
return wireguard.Peer{}, errors.WithMessage(err, "failed to get available IP addresses")
}
peerIPs[i] = freeIP
} }
peer.IPs[i] = freeIP peer.SetIPAddresses(peerIPs...)
psk, err := wgtypes.GenerateKey()
if err != nil {
return wireguard.Peer{}, errors.Wrap(err, "failed to generate key")
}
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
return wireguard.Peer{}, errors.Wrap(err, "failed to generate private key")
}
peer.PresharedKey = psk.String()
peer.PrivateKey = key.String()
peer.PublicKey = key.PublicKey().String()
peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey)))
peer.Endpoint = dev.DefaultEndpoint
peer.DNSStr = dev.DNSStr
peer.PersistentKeepalive = dev.DefaultPersistentKeepalive
peer.AllowedIPsStr = dev.DefaultAllowedIPsStr
peer.Mtu = dev.Mtu
case wireguard.DeviceTypeClient:
peer.UID = "newendpoint"
} }
peer.IPsStr = common.ListToString(peer.IPs)
psk, err := wgtypes.GenerateKey()
if err != nil {
return Peer{}, errors.Wrap(err, "failed to generate key")
}
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
return Peer{}, errors.Wrap(err, "failed to generate private key")
}
peer.PresharedKey = psk.String()
peer.PrivateKey = key.String()
peer.PublicKey = key.PublicKey().String()
peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey)))
return peer, nil return peer, nil
} }
func (s *Server) CreatePeerByEmail(email, identifierSuffix string, disabled bool) error { // CreatePeerByEmail creates a new peer for the given email.
user, err := s.users.GetOrCreateUser(email) func (s *Server) CreatePeerByEmail(device, email, identifierSuffix string, disabled bool) error {
if err != nil { user := s.users.GetUser(email)
return errors.WithMessagef(err, "failed to load/create related user %s", email)
}
device := s.peers.GetDevice() peer, err := s.PrepareNewPeer(device)
peer := Peer{}
peer.User = user
peer.AllowedIPsStr = device.AllowedIPsStr
peer.IPs = make([]string, len(device.IPs))
for i := range device.IPs {
freeIP, err := s.peers.GetAvailableIp(device.IPs[i])
if err != nil {
return errors.WithMessage(err, "failed to get available IP addresses")
}
peer.IPs[i] = freeIP
}
peer.IPsStr = common.ListToString(peer.IPs)
psk, err := wgtypes.GenerateKey()
if err != nil { if err != nil {
return errors.Wrap(err, "failed to generate key") return errors.WithMessage(err, "failed to prepare new peer")
} }
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
return errors.Wrap(err, "failed to generate private key")
}
peer.PresharedKey = psk.String()
peer.PrivateKey = key.String()
peer.PublicKey = key.PublicKey().String()
peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey)))
peer.Email = email peer.Email = email
peer.Identifier = fmt.Sprintf("%s %s (%s)", user.Firstname, user.Lastname, identifierSuffix) if user != nil {
peer.Identifier = fmt.Sprintf("%s %s (%s)", user.Firstname, user.Lastname, identifierSuffix)
} else {
peer.Identifier = fmt.Sprintf("%s (%s)", email, identifierSuffix)
}
now := time.Now() now := time.Now()
if disabled { if disabled {
peer.DeactivatedAt = &now peer.DeactivatedAt = &now
} }
return s.CreatePeer(peer) return s.CreatePeer(device, peer)
} }
func (s *Server) CreatePeer(peer Peer) error { // CreatePeer creates the new peer in the database. If the peer has no assigned ip addresses, a new one will be assigned
device := s.peers.GetDevice() // automatically. Also, if the private key is empty, a new key-pair will be generated.
peer.AllowedIPsStr = device.AllowedIPsStr // This function also configures the new peer on the physical WireGuard interface if the peer is not deactivated.
if peer.IPs == nil || len(peer.IPs) == 0 { func (s *Server) CreatePeer(device string, peer wireguard.Peer) error {
peer.IPs = make([]string, len(device.IPs)) dev := s.peers.GetDevice(device)
for i := range device.IPs { deviceIPs := dev.GetIPAddresses()
freeIP, err := s.peers.GetAvailableIp(device.IPs[i]) peerIPs := peer.GetIPAddresses()
peer.AllowedIPsStr = dev.DefaultAllowedIPsStr
if len(peerIPs) == 0 && dev.Type == wireguard.DeviceTypeServer {
peerIPs = make([]string, len(deviceIPs))
for i := range deviceIPs {
freeIP, err := s.peers.GetAvailableIp(device, deviceIPs[i])
if err != nil { if err != nil {
return errors.WithMessage(err, "failed to get available IP addresses") return errors.WithMessage(err, "failed to get available IP addresses")
} }
peer.IPs[i] = freeIP peerIPs[i] = freeIP
} }
peer.IPsStr = common.ListToString(peer.IPs) peer.SetIPAddresses(peerIPs...)
} }
if peer.PrivateKey == "" { // if private key is empty create a new one if peer.PrivateKey == "" && dev.Type == wireguard.DeviceTypeServer { // if private key is empty create a new one
psk, err := wgtypes.GenerateKey() psk, err := wgtypes.GenerateKey()
if err != nil { if err != nil {
return errors.Wrap(err, "failed to generate key") return errors.Wrap(err, "failed to generate key")
@ -114,11 +114,12 @@ func (s *Server) CreatePeer(peer Peer) error {
peer.PrivateKey = key.String() peer.PrivateKey = key.String()
peer.PublicKey = key.PublicKey().String() peer.PublicKey = key.PublicKey().String()
} }
peer.DeviceName = dev.DeviceName
peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey))) peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey)))
// Create WireGuard interface // Create WireGuard interface
if peer.DeactivatedAt == nil { if peer.DeactivatedAt == nil {
if err := s.wg.AddPeer(peer.GetConfig()); err != nil { if err := s.wg.AddPeer(device, peer.GetConfig(&dev)); err != nil {
return errors.WithMessage(err, "failed to add WireGuard peer") return errors.WithMessage(err, "failed to add WireGuard peer")
} }
} }
@ -128,37 +129,42 @@ func (s *Server) CreatePeer(peer Peer) error {
return errors.WithMessage(err, "failed to create peer") return errors.WithMessage(err, "failed to create peer")
} }
return s.WriteWireGuardConfigFile() return s.WriteWireGuardConfigFile(device)
} }
func (s *Server) UpdatePeer(peer Peer, updateTime time.Time) error { // UpdatePeer updates the physical WireGuard interface and the database.
func (s *Server) UpdatePeer(peer wireguard.Peer, updateTime time.Time) error {
currentPeer := s.peers.GetPeerByKey(peer.PublicKey) currentPeer := s.peers.GetPeerByKey(peer.PublicKey)
dev := s.peers.GetDevice(peer.DeviceName)
// Update WireGuard device // Update WireGuard device
var err error var err error
switch { switch {
case peer.DeactivatedAt == &updateTime: case peer.DeactivatedAt != nil && *peer.DeactivatedAt == updateTime:
err = s.wg.RemovePeer(peer.PublicKey) err = s.wg.RemovePeer(peer.DeviceName, peer.PublicKey)
case peer.DeactivatedAt == nil && currentPeer.Peer != nil: case peer.DeactivatedAt == nil && currentPeer.Peer != nil:
err = s.wg.UpdatePeer(peer.GetConfig()) err = s.wg.UpdatePeer(peer.DeviceName, peer.GetConfig(&dev))
case peer.DeactivatedAt == nil && currentPeer.Peer == nil: case peer.DeactivatedAt == nil && currentPeer.Peer == nil:
err = s.wg.AddPeer(peer.GetConfig()) err = s.wg.AddPeer(peer.DeviceName, peer.GetConfig(&dev))
} }
if err != nil { if err != nil {
return errors.WithMessage(err, "failed to update WireGuard peer") return errors.WithMessage(err, "failed to update WireGuard peer")
} }
peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey)))
// Update in database // Update in database
if err := s.peers.UpdatePeer(peer); err != nil { if err := s.peers.UpdatePeer(peer); err != nil {
return errors.WithMessage(err, "failed to update peer") return errors.WithMessage(err, "failed to update peer")
} }
return s.WriteWireGuardConfigFile() return s.WriteWireGuardConfigFile(peer.DeviceName)
} }
func (s *Server) DeletePeer(peer Peer) error { // DeletePeer removes the peer from the physical WireGuard interface and the database.
func (s *Server) DeletePeer(peer wireguard.Peer) error {
// Delete WireGuard peer // Delete WireGuard peer
if err := s.wg.RemovePeer(peer.PublicKey); err != nil { if err := s.wg.RemovePeer(peer.DeviceName, peer.PublicKey); err != nil {
return errors.WithMessage(err, "failed to remove WireGuard peer") return errors.WithMessage(err, "failed to remove WireGuard peer")
} }
@ -167,15 +173,17 @@ func (s *Server) DeletePeer(peer Peer) error {
return errors.WithMessage(err, "failed to remove peer") return errors.WithMessage(err, "failed to remove peer")
} }
return s.WriteWireGuardConfigFile() return s.WriteWireGuardConfigFile(peer.DeviceName)
} }
func (s *Server) RestoreWireGuardInterface() error { // RestoreWireGuardInterface restores the state of the physical WireGuard interface from the database.
activePeers := s.peers.GetActivePeers() func (s *Server) RestoreWireGuardInterface(device string) error {
activePeers := s.peers.GetActivePeers(device)
dev := s.peers.GetDevice(device)
for i := range activePeers { for i := range activePeers {
if activePeers[i].Peer == nil { if activePeers[i].Peer == nil {
if err := s.wg.AddPeer(activePeers[i].GetConfig()); err != nil { if err := s.wg.AddPeer(device, activePeers[i].GetConfig(&dev)); err != nil {
return errors.WithMessage(err, "failed to add WireGuard peer") return errors.WithMessage(err, "failed to add WireGuard peer")
} }
} }
@ -184,26 +192,29 @@ func (s *Server) RestoreWireGuardInterface() error {
return nil return nil
} }
func (s *Server) WriteWireGuardConfigFile() error { // WriteWireGuardConfigFile writes the configuration file for the physical WireGuard interface.
if s.config.WG.WireGuardConfig == "" { func (s *Server) WriteWireGuardConfigFile(device string) error {
if s.config.WG.ConfigDirectoryPath == "" {
return nil // writing disabled return nil // writing disabled
} }
if err := syscall.Access(s.config.WG.WireGuardConfig, syscall.O_RDWR); err != nil { if err := syscall.Access(s.config.WG.ConfigDirectoryPath, syscall.O_RDWR); err != nil {
return errors.Wrap(err, "failed to check WireGuard config access rights") return errors.Wrap(err, "failed to check WireGuard config access rights")
} }
device := s.peers.GetDevice() dev := s.peers.GetDevice(device)
cfg, err := device.GetConfigFile(s.peers.GetActivePeers()) cfg, err := dev.GetConfigFile(s.peers.GetActivePeers(device))
if err != nil { if err != nil {
return errors.WithMessage(err, "failed to get config file") return errors.WithMessage(err, "failed to get config file")
} }
if err := ioutil.WriteFile(s.config.WG.WireGuardConfig, cfg, 0644); err != nil { filePath := path.Join(s.config.WG.ConfigDirectoryPath, dev.DeviceName+".conf")
if err := ioutil.WriteFile(filePath, cfg, 0644); err != nil {
return errors.Wrap(err, "failed to write WireGuard config file") return errors.Wrap(err, "failed to write WireGuard config file")
} }
return nil return nil
} }
func (s *Server) CreateUser(user users.User) error { // CreateUser creates the user in the database and optionally adds a default WireGuard peer for the user.
func (s *Server) CreateUser(user users.User, device string) error {
if user.Email == "" { if user.Email == "" {
return errors.New("cannot create user with empty email address") return errors.New("cannot create user with empty email address")
} }
@ -220,9 +231,11 @@ func (s *Server) CreateUser(user users.User) error {
} }
// Check if user already has a peer setup, if not, create one // Check if user already has a peer setup, if not, create one
return s.CreateUserDefaultPeer(user.Email) return s.CreateUserDefaultPeer(user.Email, device)
} }
// UpdateUser updates the user in the database. If the user is marked as deleted, it will get remove from the database.
// Also, if the user is re-enabled, all it's linked WireGuard peers will be activated again.
func (s *Server) UpdateUser(user users.User) error { func (s *Server) UpdateUser(user users.User) error {
if user.DeletedAt.Valid { if user.DeletedAt.Valid {
return s.DeleteUser(user) return s.DeleteUser(user)
@ -249,6 +262,8 @@ func (s *Server) UpdateUser(user users.User) error {
return nil return nil
} }
// DeleteUser removes the user from the database.
// Also, if the user has linked WireGuard peers, they will be deactivated.
func (s *Server) DeleteUser(user users.User) error { func (s *Server) DeleteUser(user users.User) error {
currentUser := s.users.GetUserUnscoped(user.Email) currentUser := s.users.GetUserUnscoped(user.Email)
@ -271,7 +286,7 @@ func (s *Server) DeleteUser(user users.User) error {
return nil return nil
} }
func (s *Server) CreateUserDefaultPeer(email string) error { func (s *Server) CreateUserDefaultPeer(email, device string) error {
// Check if user is active, if not, quit // Check if user is active, if not, quit
var existingUser *users.User var existingUser *users.User
if existingUser = s.users.GetUser(email); existingUser == nil { if existingUser = s.users.GetUser(email); existingUser == nil {
@ -282,7 +297,7 @@ func (s *Server) CreateUserDefaultPeer(email string) error {
if s.config.Core.CreateDefaultPeer { if s.config.Core.CreateDefaultPeer {
peers := s.peers.GetPeersByMail(email) peers := s.peers.GetPeersByMail(email)
if len(peers) == 0 { // Create default vpn peer if len(peers) == 0 { // Create default vpn peer
if err := s.CreatePeer(Peer{ if err := s.CreatePeer(device, wireguard.Peer{
Identifier: existingUser.Firstname + " " + existingUser.Lastname + " (Default)", Identifier: existingUser.Firstname + " " + existingUser.Lastname + " (Default)",
Email: existingUser.Email, Email: existingUser.Email,
CreatedBy: existingUser.Email, CreatedBy: existingUser.Email,
@ -295,3 +310,14 @@ func (s *Server) CreateUserDefaultPeer(email string) error {
return nil return nil
} }
func (s *Server) GetDeviceNames() map[string]string {
devNames := make(map[string]string, len(s.wg.Cfg.DeviceNames))
for _, devName := range s.wg.Cfg.DeviceNames {
dev := s.peers.GetDevice(devName)
devNames[devName] = dev.DisplayName
}
return devNames
}

View File

@ -0,0 +1,3 @@
package server
var Version = "1.0.5"

View File

@ -1,17 +0,0 @@
package users
type SupportedDatabase string
const (
SupportedDatabaseMySQL SupportedDatabase = "mysql"
SupportedDatabaseSQLite SupportedDatabase = "sqlite"
)
type Config struct {
Typ SupportedDatabase `yaml:"typ" envconfig:"DATABASE_TYPE"` //mysql or sqlite
Host string `yaml:"host" envconfig:"DATABASE_HOST"`
Port int `yaml:"port" envconfig:"DATABASE_PORT"`
Database string `yaml:"database" envconfig:"DATABASE_NAME"` // On SQLite: the database file-path, otherwise the database name
User string `yaml:"user" envconfig:"DATABASE_USERNAME"`
Password string `yaml:"password" envconfig:"DATABASE_PASSWORD"`
}

View File

@ -1,9 +1,6 @@
package users package users
import ( import (
"fmt"
"os"
"path/filepath"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@ -11,69 +8,15 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger"
) )
func GetDatabaseForConfig(cfg *Config) (db *gorm.DB, err error) {
switch cfg.Typ {
case SupportedDatabaseSQLite:
if _, err = os.Stat(filepath.Dir(cfg.Database)); os.IsNotExist(err) {
if err = os.MkdirAll(filepath.Dir(cfg.Database), 0700); err != nil {
return
}
}
db, err = gorm.Open(sqlite.Open(cfg.Database), &gorm.Config{})
if err != nil {
return
}
case SupportedDatabaseMySQL:
connectionString := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
db, err = gorm.Open(mysql.Open(connectionString), &gorm.Config{})
if err != nil {
return
}
sqlDB, _ := db.DB()
sqlDB.SetConnMaxLifetime(time.Minute * 5)
sqlDB.SetMaxIdleConns(2)
sqlDB.SetMaxOpenConns(10)
err = sqlDB.Ping() // This DOES open a connection if necessary. This makes sure the database is accessible
if err != nil {
return nil, errors.Wrap(err, "failed to ping mysql authentication database")
}
}
// Enable Logger (logrus)
logCfg := logger.Config{
SlowThreshold: time.Second, // all slower than one second
Colorful: false,
LogLevel: logger.Silent, // default: log nothing
}
if logrus.StandardLogger().GetLevel() == logrus.TraceLevel {
logCfg.LogLevel = logger.Info
logCfg.SlowThreshold = 500 * time.Millisecond // all slower than half a second
}
db.Config.Logger = logger.New(logrus.StandardLogger(), logCfg)
return
}
type Manager struct { type Manager struct {
db *gorm.DB db *gorm.DB
} }
func NewManager(cfg *Config) (*Manager, error) { func NewManager(db *gorm.DB) (*Manager, error) {
m := &Manager{} m := &Manager{db: db}
var err error
m.db, err = GetDatabaseForConfig(cfg)
if err != nil {
return nil, errors.Wrapf(err, "failed to setup user database %s", cfg.Database)
}
// check if old user table exists (from version <= 1.0.2), if so rename it to peers. // check if old user table exists (from version <= 1.0.2), if so rename it to peers.
if m.db.Migrator().HasTable("users") && !m.db.Migrator().HasTable("peers") { if m.db.Migrator().HasTable("users") && !m.db.Migrator().HasTable("peers") {
@ -84,14 +27,11 @@ func NewManager(cfg *Config) (*Manager, error) {
} }
} }
return m, m.MigrateUserDB()
}
func (m Manager) MigrateUserDB() error {
if err := m.db.AutoMigrate(&User{}); err != nil { if err := m.db.AutoMigrate(&User{}); err != nil {
return errors.Wrap(err, "failed to migrate user database") return nil, errors.Wrap(err, "failed to migrate user database")
} }
return nil
return m, nil
} }
func (m Manager) GetUsers() []User { func (m Manager) GetUsers() []User {

View File

@ -1,7 +1,17 @@
package wireguard package wireguard
import "github.com/h44z/wg-portal/internal/common"
type Config struct { type Config struct {
DeviceName string `yaml:"device" envconfig:"WG_DEVICE"` DeviceNames []string `yaml:"devices" envconfig:"WG_DEVICES"` // managed devices
WireGuardConfig string `yaml:"configFile" envconfig:"WG_CONFIG_FILE"` // optional, if set, updates will be written to this file DefaultDeviceName string `yaml:"devices" envconfig:"WG_DEFAULT_DEVICE"` // this device is used for auto-created peers, use GetDefaultDeviceName() to access this field
ManageIPAddresses bool `yaml:"manageIPAddresses" envconfig:"MANAGE_IPS"` // handle ip-address setup of interface ConfigDirectoryPath string `yaml:"configDirectory" envconfig:"WG_CONFIG_PATH"` // optional, if set, updates will be written to this path, filename: <devicename>.conf
ManageIPAddresses bool `yaml:"manageIPAddresses" envconfig:"MANAGE_IPS"` // handle ip-address setup of interface
}
func (c Config) GetDefaultDeviceName() string {
if c.DefaultDeviceName == "" || !common.ListContains(c.DeviceNames, c.DefaultDeviceName) {
return c.DeviceNames[0]
}
return c.DefaultDeviceName
} }

View File

@ -9,6 +9,7 @@ import (
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
) )
// Manager offers a synchronized management interface to the real WireGuard interface.
type Manager struct { type Manager struct {
Cfg *Config Cfg *Config
wg *wgctrl.Client wg *wgctrl.Client
@ -25,8 +26,8 @@ func (m *Manager) Init() error {
return nil return nil
} }
func (m *Manager) GetDeviceInfo() (*wgtypes.Device, error) { func (m *Manager) GetDeviceInfo(device string) (*wgtypes.Device, error) {
dev, err := m.wg.Device(m.Cfg.DeviceName) dev, err := m.wg.Device(device)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "could not get WireGuard device") return nil, errors.Wrap(err, "could not get WireGuard device")
} }
@ -34,11 +35,11 @@ func (m *Manager) GetDeviceInfo() (*wgtypes.Device, error) {
return dev, nil return dev, nil
} }
func (m *Manager) GetPeerList() ([]wgtypes.Peer, error) { func (m *Manager) GetPeerList(device string) ([]wgtypes.Peer, error) {
m.mux.RLock() m.mux.RLock()
defer m.mux.RUnlock() defer m.mux.RUnlock()
dev, err := m.wg.Device(m.Cfg.DeviceName) dev, err := m.wg.Device(device)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "could not get WireGuard device") return nil, errors.Wrap(err, "could not get WireGuard device")
} }
@ -46,7 +47,7 @@ func (m *Manager) GetPeerList() ([]wgtypes.Peer, error) {
return dev.Peers, nil return dev.Peers, nil
} }
func (m *Manager) GetPeer(pubKey string) (*wgtypes.Peer, error) { func (m *Manager) GetPeer(device string, pubKey string) (*wgtypes.Peer, error) {
m.mux.RLock() m.mux.RLock()
defer m.mux.RUnlock() defer m.mux.RUnlock()
@ -55,7 +56,7 @@ func (m *Manager) GetPeer(pubKey string) (*wgtypes.Peer, error) {
return nil, errors.Wrap(err, "invalid public key") return nil, errors.Wrap(err, "invalid public key")
} }
peers, err := m.GetPeerList() peers, err := m.GetPeerList(device)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "could not get WireGuard peers") return nil, errors.Wrap(err, "could not get WireGuard peers")
} }
@ -69,11 +70,11 @@ func (m *Manager) GetPeer(pubKey string) (*wgtypes.Peer, error) {
return nil, errors.Errorf("could not find WireGuard peer: %s", pubKey) return nil, errors.Errorf("could not find WireGuard peer: %s", pubKey)
} }
func (m *Manager) AddPeer(cfg wgtypes.PeerConfig) error { func (m *Manager) AddPeer(device string, cfg wgtypes.PeerConfig) error {
m.mux.Lock() m.mux.Lock()
defer m.mux.Unlock() defer m.mux.Unlock()
err := m.wg.ConfigureDevice(m.Cfg.DeviceName, wgtypes.Config{Peers: []wgtypes.PeerConfig{cfg}}) err := m.wg.ConfigureDevice(device, wgtypes.Config{Peers: []wgtypes.PeerConfig{cfg}})
if err != nil { if err != nil {
return errors.Wrap(err, "could not configure WireGuard device") return errors.Wrap(err, "could not configure WireGuard device")
} }
@ -81,12 +82,12 @@ func (m *Manager) AddPeer(cfg wgtypes.PeerConfig) error {
return nil return nil
} }
func (m *Manager) UpdatePeer(cfg wgtypes.PeerConfig) error { func (m *Manager) UpdatePeer(device string, cfg wgtypes.PeerConfig) error {
m.mux.Lock() m.mux.Lock()
defer m.mux.Unlock() defer m.mux.Unlock()
cfg.UpdateOnly = true cfg.UpdateOnly = true
err := m.wg.ConfigureDevice(m.Cfg.DeviceName, wgtypes.Config{Peers: []wgtypes.PeerConfig{cfg}}) err := m.wg.ConfigureDevice(device, wgtypes.Config{Peers: []wgtypes.PeerConfig{cfg}})
if err != nil { if err != nil {
return errors.Wrap(err, "could not configure WireGuard device") return errors.Wrap(err, "could not configure WireGuard device")
} }
@ -94,7 +95,7 @@ func (m *Manager) UpdatePeer(cfg wgtypes.PeerConfig) error {
return nil return nil
} }
func (m *Manager) RemovePeer(pubKey string) error { func (m *Manager) RemovePeer(device string, pubKey string) error {
m.mux.Lock() m.mux.Lock()
defer m.mux.Unlock() defer m.mux.Unlock()
@ -108,7 +109,7 @@ func (m *Manager) RemovePeer(pubKey string) error {
Remove: true, Remove: true,
} }
err = m.wg.ConfigureDevice(m.Cfg.DeviceName, wgtypes.Config{Peers: []wgtypes.PeerConfig{peer}}) err = m.wg.ConfigureDevice(device, wgtypes.Config{Peers: []wgtypes.PeerConfig{peer}})
if err != nil { if err != nil {
return errors.Wrap(err, "could not configure WireGuard device") return errors.Wrap(err, "could not configure WireGuard device")
} }
@ -116,6 +117,6 @@ func (m *Manager) RemovePeer(pubKey string) error {
return nil return nil
} }
func (m *Manager) UpdateDevice(name string, cfg wgtypes.Config) error { func (m *Manager) UpdateDevice(device string, cfg wgtypes.Config) error {
return m.wg.ConfigureDevice(name, cfg) return m.wg.ConfigureDevice(device, cfg)
} }

View File

@ -11,10 +11,10 @@ import (
const DefaultMTU = 1420 const DefaultMTU = 1420
func (m *Manager) GetIPAddress() ([]string, error) { func (m *Manager) GetIPAddress(device string) ([]string, error) {
wgInterface, err := tenus.NewLinkFrom(m.Cfg.DeviceName) wgInterface, err := tenus.NewLinkFrom(device)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "could not retrieve WireGuard interface %s", m.Cfg.DeviceName) return nil, errors.Wrapf(err, "could not retrieve WireGuard interface %s", device)
} }
// Get golang net.interface // Get golang net.interface
@ -52,14 +52,14 @@ func (m *Manager) GetIPAddress() ([]string, error) {
return ipAddresses, nil return ipAddresses, nil
} }
func (m *Manager) SetIPAddress(cidrs []string) error { func (m *Manager) SetIPAddress(device string, cidrs []string) error {
wgInterface, err := tenus.NewLinkFrom(m.Cfg.DeviceName) wgInterface, err := tenus.NewLinkFrom(device)
if err != nil { if err != nil {
return errors.Wrapf(err, "could not retrieve WireGuard interface %s", m.Cfg.DeviceName) return errors.Wrapf(err, "could not retrieve WireGuard interface %s", device)
} }
// First remove existing IP addresses // First remove existing IP addresses
existingIPs, err := m.GetIPAddress() existingIPs, err := m.GetIPAddress(device)
if err != nil { if err != nil {
return errors.Wrap(err, "could not retrieve IP addresses") return errors.Wrap(err, "could not retrieve IP addresses")
} }
@ -89,10 +89,10 @@ func (m *Manager) SetIPAddress(cidrs []string) error {
return nil return nil
} }
func (m *Manager) GetMTU() (int, error) { func (m *Manager) GetMTU(device string) (int, error) {
wgInterface, err := tenus.NewLinkFrom(m.Cfg.DeviceName) wgInterface, err := tenus.NewLinkFrom(device)
if err != nil { if err != nil {
return 0, errors.Wrapf(err, "could not retrieve WireGuard interface %s", m.Cfg.DeviceName) return 0, errors.Wrapf(err, "could not retrieve WireGuard interface %s", device)
} }
// Get golang net.interface // Get golang net.interface
@ -104,10 +104,10 @@ func (m *Manager) GetMTU() (int, error) {
return iface.MTU, nil return iface.MTU, nil
} }
func (m *Manager) SetMTU(mtu int) error { func (m *Manager) SetMTU(device string, mtu int) error {
wgInterface, err := tenus.NewLinkFrom(m.Cfg.DeviceName) wgInterface, err := tenus.NewLinkFrom(device)
if err != nil { if err != nil {
return errors.Wrapf(err, "could not retrieve WireGuard interface %s", m.Cfg.DeviceName) return errors.Wrapf(err, "could not retrieve WireGuard interface %s", device)
} }
if mtu == 0 { if mtu == 0 {
@ -115,7 +115,7 @@ func (m *Manager) SetMTU(mtu int) error {
} }
if err := wgInterface.SetLinkMTU(mtu); err != nil { if err := wgInterface.SetLinkMTU(mtu); err != nil {
return errors.Wrapf(err, "could not set MTU on interface %s", m.Cfg.DeviceName) return errors.Wrapf(err, "could not set MTU on interface %s", device)
} }
return nil return nil

View File

@ -0,0 +1,844 @@
package wireguard
// WireGuard documentation: https://manpages.debian.org/unstable/wireguard-tools/wg.8.en.html
import (
"bytes"
"crypto/md5"
"fmt"
"net"
"regexp"
"sort"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
"github.com/h44z/wg-portal/internal/common"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/skip2/go-qrcode"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"gorm.io/gorm"
)
//
// CUSTOM VALIDATORS ----------------------------------------------------------------------------
//
var cidrList validator.Func = func(fl validator.FieldLevel) bool {
cidrListStr := fl.Field().String()
cidrList := common.ParseStringList(cidrListStr)
for i := range cidrList {
_, _, err := net.ParseCIDR(cidrList[i])
if err != nil {
return false
}
}
return true
}
var ipList validator.Func = func(fl validator.FieldLevel) bool {
ipListStr := fl.Field().String()
ipList := common.ParseStringList(ipListStr)
for i := range ipList {
ip := net.ParseIP(ipList[i])
if ip == nil {
return false
}
}
return true
}
func init() {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
_ = v.RegisterValidation("cidrlist", cidrList)
_ = v.RegisterValidation("iplist", ipList)
}
}
//
// PEER ----------------------------------------------------------------------------------------
//
type Peer struct {
Peer *wgtypes.Peer `gorm:"-"` // WireGuard peer
Device *Device `gorm:"foreignKey:DeviceName" binding:"-"` // linked WireGuard device
Config string `gorm:"-"`
UID string `form:"uid" binding:"required,alphanum"` // uid for html identification
DeviceName string `gorm:"index" form:"device" binding:"required"`
DeviceType DeviceType `gorm:"-" form:"devicetype" binding:"required,oneof=client server"`
Identifier string `form:"identifier" binding:"required,max=64"` // Identifier AND Email make a WireGuard peer unique
Email string `gorm:"index" form:"mail" binding:"required,email"`
IgnoreGlobalSettings bool `form:"ignoreglobalsettings"`
IsOnline bool `gorm:"-"`
IsNew bool `gorm:"-"`
LastHandshake string `gorm:"-"`
LastHandshakeTime string `gorm:"-"`
// Core WireGuard Settings
PublicKey string `gorm:"primaryKey" form:"pubkey" binding:"required,base64"` // the public key of the peer itself
PresharedKey string `form:"presharedkey" binding:"omitempty,base64"`
AllowedIPsStr string `form:"allowedip" binding:"cidrlist"` // a comma separated list of IPs that are used in the client config file
Endpoint string `form:"endpoint" binding:"omitempty,hostname_port"`
PersistentKeepalive int `form:"keepalive" binding:"gte=0"`
// Misc. WireGuard Settings
PrivateKey string `form:"privkey" binding:"omitempty,base64"`
IPsStr string `form:"ip" binding:"cidrlist,required_if=DeviceType server"` // a comma separated list of IPs of the client
DNSStr string `form:"dns" binding:"iplist"` // comma separated list of the DNS servers for the client
// Global Device Settings (can be ignored, only make sense if device is in server mode)
Mtu int `form:"mtu" binding:"gte=0,lte=1500"`
DeactivatedAt *time.Time
CreatedBy string
UpdatedBy string
CreatedAt time.Time
UpdatedAt time.Time
}
func (p *Peer) SetIPAddresses(addresses ...string) {
p.IPsStr = common.ListToString(addresses)
}
func (p Peer) GetIPAddresses() []string {
return common.ParseStringList(p.IPsStr)
}
func (p *Peer) SetDNSServers(addresses ...string) {
p.DNSStr = common.ListToString(addresses)
}
func (p Peer) GetDNSServers() []string {
return common.ParseStringList(p.DNSStr)
}
func (p *Peer) SetAllowedIPs(addresses ...string) {
p.AllowedIPsStr = common.ListToString(addresses)
}
func (p Peer) GetAllowedIPs() []string {
return common.ParseStringList(p.AllowedIPsStr)
}
func (p Peer) GetConfig(_ *Device) wgtypes.PeerConfig {
publicKey, _ := wgtypes.ParseKey(p.PublicKey)
var presharedKey *wgtypes.Key
if p.PresharedKey != "" {
presharedKeyTmp, _ := wgtypes.ParseKey(p.PresharedKey)
presharedKey = &presharedKeyTmp
}
var endpoint *net.UDPAddr
if p.Endpoint != "" {
addr, err := net.ResolveUDPAddr("udp", p.Endpoint)
if err == nil {
endpoint = addr
}
}
var keepAlive *time.Duration
if p.PersistentKeepalive != 0 {
keepAliveDuration := time.Duration(p.PersistentKeepalive) * time.Second
keepAlive = &keepAliveDuration
}
peerAllowedIPs := p.GetAllowedIPs()
allowedIPs := make([]net.IPNet, len(peerAllowedIPs))
for i, ip := range peerAllowedIPs {
_, ipNet, err := net.ParseCIDR(ip)
if err == nil {
allowedIPs[i] = *ipNet
}
}
cfg := wgtypes.PeerConfig{
PublicKey: publicKey,
Remove: false,
UpdateOnly: false,
PresharedKey: presharedKey,
Endpoint: endpoint,
PersistentKeepaliveInterval: keepAlive,
ReplaceAllowedIPs: true,
AllowedIPs: allowedIPs,
}
return cfg
}
func (p Peer) GetConfigFile(device Device) ([]byte, error) {
var tplBuff bytes.Buffer
err := templateCache.ExecuteTemplate(&tplBuff, "peer.tpl", gin.H{
"Peer": p,
"Interface": device,
})
if err != nil {
return nil, errors.Wrap(err, "failed to execute client template")
}
return tplBuff.Bytes(), nil
}
func (p Peer) GetQRCode() ([]byte, error) {
png, err := qrcode.Encode(p.Config, qrcode.Medium, 250)
if err != nil {
logrus.WithFields(logrus.Fields{
"err": err,
}).Error("failed to create qrcode")
return nil, errors.Wrap(err, "failed to encode qrcode")
}
return png, nil
}
func (p Peer) IsValid() bool {
if p.PublicKey == "" {
return false
}
return true
}
func (p Peer) GetConfigFileName() string {
reg := regexp.MustCompile("[^a-zA-Z0-9_-]+")
return reg.ReplaceAllString(strings.ReplaceAll(p.Identifier, " ", "-"), "") + ".conf"
}
//
// DEVICE --------------------------------------------------------------------------------------
//
type DeviceType string
const (
DeviceTypeServer DeviceType = "server"
DeviceTypeClient DeviceType = "client"
)
type Device struct {
Interface *wgtypes.Device `gorm:"-"`
Type DeviceType `form:"devicetype" binding:"required,oneof=client server"`
DeviceName string `form:"device" gorm:"primaryKey" binding:"required,alphanum"`
DisplayName string `form:"displayname" binding:"omitempty,max=200"`
// Core WireGuard Settings (Interface section)
PrivateKey string `form:"privkey" binding:"required,base64"`
ListenPort int `form:"port" binding:"required_if=Type server,omitempty,gt=0,lt=65535"`
FirewallMark int32 `form:"firewallmark" binding:"gte=0"`
// Misc. WireGuard Settings
PublicKey string `form:"pubkey" binding:"required,base64"`
Mtu int `form:"mtu" binding:"gte=0,lte=1500"` // the interface MTU, wg-quick addition
IPsStr string `form:"ip" binding:"required,cidrlist"` // comma separated list of the IPs of the client, wg-quick addition
DNSStr string `form:"dns" binding:"iplist"` // comma separated list of the DNS servers of the client, wg-quick addition
RoutingTable string `form:"routingtable"` // the routing table, wg-quick addition
PreUp string `form:"preup"` // pre up script, wg-quick addition
PostUp string `form:"postup"` // post up script, wg-quick addition
PreDown string `form:"predown"` // pre down script, wg-quick addition
PostDown string `form:"postdown"` // post down script, wg-quick addition
SaveConfig bool `form:"saveconfig"` // if set to `true', the configuration is saved from the current state of the interface upon shutdown, wg-quick addition
// Settings that are applied to all peer by default
DefaultEndpoint string `form:"endpoint" binding:"required_if=Type server,omitempty,hostname_port"`
DefaultAllowedIPsStr string `form:"allowedip" binding:"cidrlist"` // comma separated list of IPs that are used in the client config file
DefaultPersistentKeepalive int `form:"keepalive" binding:"gte=0"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (d Device) IsValid() bool {
switch d.Type {
case DeviceTypeServer:
if d.PublicKey == "" {
return false
}
if len(d.GetIPAddresses()) == 0 {
return false
}
if d.DefaultEndpoint == "" {
return false
}
case DeviceTypeClient:
if d.PublicKey == "" {
return false
}
if len(d.GetIPAddresses()) == 0 {
return false
}
}
return true
}
func (d *Device) SetIPAddresses(addresses ...string) {
d.IPsStr = common.ListToString(addresses)
}
func (d Device) GetIPAddresses() []string {
return common.ParseStringList(d.IPsStr)
}
func (d *Device) SetDNSServers(addresses ...string) {
d.DNSStr = common.ListToString(addresses)
}
func (d Device) GetDNSServers() []string {
return common.ParseStringList(d.DNSStr)
}
func (d *Device) SetDefaultAllowedIPs(addresses ...string) {
d.DefaultAllowedIPsStr = common.ListToString(addresses)
}
func (d Device) GetDefaultAllowedIPs() []string {
return common.ParseStringList(d.DefaultAllowedIPsStr)
}
func (d Device) GetConfig() wgtypes.Config {
var privateKey *wgtypes.Key
if d.PrivateKey != "" {
pKey, _ := wgtypes.ParseKey(d.PrivateKey)
privateKey = &pKey
}
fwMark := int(d.FirewallMark)
cfg := wgtypes.Config{
PrivateKey: privateKey,
ListenPort: &d.ListenPort,
FirewallMark: &fwMark,
}
return cfg
}
func (d Device) GetConfigFile(peers []Peer) ([]byte, error) {
var tplBuff bytes.Buffer
err := templateCache.ExecuteTemplate(&tplBuff, "interface.tpl", gin.H{
"Peers": peers,
"Interface": d,
})
if err != nil {
return nil, errors.Wrap(err, "failed to execute server template")
}
return tplBuff.Bytes(), nil
}
//
// PEER-MANAGER --------------------------------------------------------------------------------
//
type PeerManager struct {
db *gorm.DB
wg *Manager
}
func NewPeerManager(db *gorm.DB, wg *Manager) (*PeerManager, error) {
pm := &PeerManager{db: db, wg: wg}
// check if old device table exists (from version <= 1.0.3), if so migrate it.
if db.Migrator().HasColumn(&Device{}, "endpoint") {
if err := db.Migrator().RenameColumn(&Device{}, "endpoint", "default_endpoint"); err != nil {
return nil, errors.Wrapf(err, "failed to migrate old database structure for column endpoint")
}
}
if db.Migrator().HasColumn(&Device{}, "allowed_ips_str") {
if err := db.Migrator().RenameColumn(&Device{}, "allowed_ips_str", "default_allowed_ips_str"); err != nil {
return nil, errors.Wrapf(err, "failed to migrate old database structure for column allowed_ips_str")
}
}
if db.Migrator().HasColumn(&Device{}, "persistent_keepalive") {
if err := db.Migrator().RenameColumn(&Device{}, "persistent_keepalive", "default_persistent_keepalive"); err != nil {
return nil, errors.Wrapf(err, "failed to migrate old database structure for column persistent_keepalive")
}
}
if err := pm.db.AutoMigrate(&Peer{}, &Device{}); err != nil {
return nil, errors.WithMessage(err, "failed to migrate peer database")
}
if err := pm.initFromPhysicalInterface(); err != nil {
return nil, errors.WithMessagef(err, "unable to initialize peer manager")
}
// check if peers without device name exist (from version <= 1.0.3), if so assign them to the default device.
peers := make([]Peer, 0)
pm.db.Find(&peers)
for i := range peers {
if peers[i].DeviceName == "" {
peers[i].DeviceName = wg.Cfg.GetDefaultDeviceName()
pm.db.Save(&peers[i])
}
}
// validate and update existing peers if needed
for _, deviceName := range wg.Cfg.DeviceNames {
dev := pm.GetDevice(deviceName)
peers := pm.GetAllPeers(deviceName)
for i := range peers {
if err := pm.fixPeerDefaultData(&peers[i], &dev); err != nil {
return nil, errors.WithMessagef(err, "unable to fix peers for interface %s", deviceName)
}
}
}
return pm, nil
}
// initFromPhysicalInterface read all WireGuard peers from the WireGuard interface configuration. If a peer does not
// exist in the local database, it gets created.
func (m *PeerManager) initFromPhysicalInterface() error {
for _, deviceName := range m.wg.Cfg.DeviceNames {
peers, err := m.wg.GetPeerList(deviceName)
if err != nil {
return errors.Wrapf(err, "failed to get peer list for device %s", deviceName)
}
device, err := m.wg.GetDeviceInfo(deviceName)
if err != nil {
return errors.Wrapf(err, "failed to get device info for device %s", deviceName)
}
var ipAddresses []string
var mtu int
if m.wg.Cfg.ManageIPAddresses {
if ipAddresses, err = m.wg.GetIPAddress(deviceName); err != nil {
return errors.Wrapf(err, "failed to get ip address for device %s", deviceName)
}
if mtu, err = m.wg.GetMTU(deviceName); err != nil {
return errors.Wrapf(err, "failed to get MTU for device %s", deviceName)
}
}
// Check if device already exists in database, if not, create it
if err := m.validateOrCreateDevice(*device, ipAddresses, mtu); err != nil {
return errors.WithMessagef(err, "failed to validate device %s", device.Name)
}
// Check if entries already exist in database, if not, create them
for _, peer := range peers {
if err := m.validateOrCreatePeer(deviceName, peer); err != nil {
return errors.WithMessagef(err, "failed to validate peer %s for device %s", peer.PublicKey, deviceName)
}
}
}
return nil
}
// validateOrCreatePeer checks if the given WireGuard peer already exists in the database, if not, the peer entry will be created
// assumption: server mode is used
func (m *PeerManager) validateOrCreatePeer(device string, wgPeer wgtypes.Peer) error {
peer := Peer{}
m.db.Where("public_key = ?", wgPeer.PublicKey.String()).FirstOrInit(&peer)
dev := m.GetDevice(device)
if peer.PublicKey == "" { // peer not found, create
peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(wgPeer.PublicKey.String())))
if dev.Type == DeviceTypeServer {
peer.PublicKey = wgPeer.PublicKey.String()
peer.Identifier = "Autodetected Client (" + peer.PublicKey[0:8] + ")"
} else if dev.Type == DeviceTypeClient {
peer.PublicKey = wgPeer.PublicKey.String()
if wgPeer.Endpoint != nil {
peer.Endpoint = wgPeer.Endpoint.String()
}
peer.Identifier = "Autodetected Endpoint (" + peer.PublicKey[0:8] + ")"
}
if wgPeer.PresharedKey != (wgtypes.Key{}) {
peer.PresharedKey = wgPeer.PresharedKey.String()
}
peer.Email = "autodetected@example.com"
peer.UpdatedAt = time.Now()
peer.CreatedAt = time.Now()
IPs := make([]string, len(wgPeer.AllowedIPs)) // use allowed IP's as the peer IP's
for i, ip := range wgPeer.AllowedIPs {
IPs[i] = ip.String()
}
peer.SetIPAddresses(IPs...)
peer.DeviceName = device
res := m.db.Create(&peer)
if res.Error != nil {
return errors.Wrapf(res.Error, "failed to create autodetected peer %s", peer.PublicKey)
}
}
if peer.DeviceName == "" {
peer.DeviceName = device
res := m.db.Save(&peer)
if res.Error != nil {
return errors.Wrapf(res.Error, "failed to update autodetected peer %s", peer.PublicKey)
}
}
return nil
}
// validateOrCreateDevice checks if the given WireGuard device already exists in the database, if not, the peer entry will be created
func (m *PeerManager) validateOrCreateDevice(dev wgtypes.Device, ipAddresses []string, mtu int) error {
device := Device{}
m.db.Where("device_name = ?", dev.Name).FirstOrInit(&device)
if device.PublicKey == "" { // device not found, create
device.Type = DeviceTypeServer // imported device, we assume that server mode is used
device.PublicKey = dev.PublicKey.String()
device.PrivateKey = dev.PrivateKey.String()
device.DeviceName = dev.Name
device.ListenPort = dev.ListenPort
device.FirewallMark = int32(dev.FirewallMark)
device.Mtu = 0
device.DefaultPersistentKeepalive = 16 // Default
device.IPsStr = strings.Join(ipAddresses, ", ")
if mtu == DefaultMTU {
mtu = 0
}
device.Mtu = mtu
res := m.db.Create(&device)
if res.Error != nil {
return errors.Wrapf(res.Error, "failed to create autodetected device")
}
}
if device.Type == "" {
device.Type = DeviceTypeServer // from version <= 1.0.3, only server mode devices were supported
res := m.db.Save(&device)
if res.Error != nil {
return errors.Wrapf(res.Error, "failed to update autodetected device")
}
}
return nil
}
// populatePeerData enriches the peer struct with WireGuard live data like last handshake, ...
func (m *PeerManager) populatePeerData(peer *Peer) {
// Set config file
tmpCfg, _ := peer.GetConfigFile(m.GetDevice(peer.DeviceName))
peer.Config = string(tmpCfg)
// set data from WireGuard interface
peer.Peer, _ = m.wg.GetPeer(peer.DeviceName, peer.PublicKey)
peer.LastHandshake = "never"
peer.LastHandshakeTime = "Never connected, or user is disabled."
if peer.Peer != nil {
since := time.Since(peer.Peer.LastHandshakeTime)
sinceSeconds := int(since.Round(time.Second).Seconds())
sinceMinutes := sinceSeconds / 60
sinceSeconds -= sinceMinutes * 60
if sinceMinutes > 2*10080 { // 2 weeks
peer.LastHandshake = "a while ago"
} else if sinceMinutes > 10080 { // 1 week
peer.LastHandshake = "a week ago"
} else {
peer.LastHandshake = fmt.Sprintf("%02dm %02ds", sinceMinutes, sinceSeconds)
}
peer.LastHandshakeTime = peer.Peer.LastHandshakeTime.Format(time.UnixDate)
}
peer.IsOnline = false
}
// fixPeerDefaultData tries to fill all required fields for the given peer
// also tries to migrate data if the database schema changed
func (m *PeerManager) fixPeerDefaultData(peer *Peer, device *Device) error {
updatePeer := false
switch device.Type {
case DeviceTypeServer:
if peer.Endpoint == "" {
peer.Endpoint = device.DefaultEndpoint
updatePeer = true
}
case DeviceTypeClient:
}
if updatePeer {
return m.UpdatePeer(*peer)
}
return nil
}
// populateDeviceData enriches the device struct with WireGuard live data like interface information
func (m *PeerManager) populateDeviceData(device *Device) {
// set data from WireGuard interface
device.Interface, _ = m.wg.GetDeviceInfo(device.DeviceName)
}
func (m *PeerManager) GetAllPeers(device string) []Peer {
peers := make([]Peer, 0)
m.db.Where("device_name = ?", device).Find(&peers)
for i := range peers {
m.populatePeerData(&peers[i])
}
return peers
}
func (m *PeerManager) GetActivePeers(device string) []Peer {
peers := make([]Peer, 0)
m.db.Where("device_name = ? AND deactivated_at IS NULL", device).Find(&peers)
for i := range peers {
m.populatePeerData(&peers[i])
}
return peers
}
func (m *PeerManager) GetFilteredAndSortedPeers(device, sortKey, sortDirection, search string) []Peer {
peers := make([]Peer, 0)
m.db.Where("device_name = ?", device).Find(&peers)
filteredPeers := make([]Peer, 0, len(peers))
for i := range peers {
m.populatePeerData(&peers[i])
if search == "" ||
strings.Contains(peers[i].Email, search) ||
strings.Contains(peers[i].Identifier, search) ||
strings.Contains(peers[i].PublicKey, search) {
filteredPeers = append(filteredPeers, peers[i])
}
}
sortPeers(sortKey, sortDirection, filteredPeers)
return filteredPeers
}
func (m *PeerManager) GetSortedPeersForEmail(sortKey, sortDirection, email string) []Peer {
peers := make([]Peer, 0)
m.db.Where("email = ?", email).Find(&peers)
for i := range peers {
m.populatePeerData(&peers[i])
}
sortPeers(sortKey, sortDirection, peers)
return peers
}
func sortPeers(sortKey string, sortDirection string, peers []Peer) {
sort.Slice(peers, func(i, j int) bool {
var sortValueLeft string
var sortValueRight string
switch sortKey {
case "id":
sortValueLeft = peers[i].Identifier
sortValueRight = peers[j].Identifier
case "pubKey":
sortValueLeft = peers[i].PublicKey
sortValueRight = peers[j].PublicKey
case "mail":
sortValueLeft = peers[i].Email
sortValueRight = peers[j].Email
case "ip":
sortValueLeft = peers[i].IPsStr
sortValueRight = peers[j].IPsStr
case "endpoint":
sortValueLeft = peers[i].Endpoint
sortValueRight = peers[j].Endpoint
case "handshake":
if peers[i].Peer == nil {
return true
} else if peers[j].Peer == nil {
return false
}
sortValueLeft = peers[i].Peer.LastHandshakeTime.Format(time.RFC3339)
sortValueRight = peers[j].Peer.LastHandshakeTime.Format(time.RFC3339)
}
if sortDirection == "asc" {
return sortValueLeft < sortValueRight
} else {
return sortValueLeft > sortValueRight
}
})
}
func (m *PeerManager) GetDevice(device string) Device {
dev := Device{}
m.db.Where("device_name = ?", device).First(&dev)
m.populateDeviceData(&dev)
return dev
}
func (m *PeerManager) GetPeerByKey(publicKey string) Peer {
peer := Peer{}
m.db.Where("public_key = ?", publicKey).FirstOrInit(&peer)
m.populatePeerData(&peer)
return peer
}
func (m *PeerManager) GetPeersByMail(mail string) []Peer {
var peers []Peer
m.db.Where("email = ?", mail).Find(&peers)
for i := range peers {
m.populatePeerData(&peers[i])
}
return peers
}
// ---- Database helpers -----
func (m *PeerManager) CreatePeer(peer Peer) error {
peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey)))
peer.UpdatedAt = time.Now()
peer.CreatedAt = time.Now()
res := m.db.Create(&peer)
if res.Error != nil {
logrus.Errorf("failed to create peer: %v", res.Error)
return errors.Wrap(res.Error, "failed to create peer")
}
return nil
}
func (m *PeerManager) UpdatePeer(peer Peer) error {
peer.UpdatedAt = time.Now()
res := m.db.Save(&peer)
if res.Error != nil {
logrus.Errorf("failed to update peer: %v", res.Error)
return errors.Wrap(res.Error, "failed to update peer")
}
return nil
}
func (m *PeerManager) DeletePeer(peer Peer) error {
res := m.db.Delete(&peer)
if res.Error != nil {
logrus.Errorf("failed to delete peer: %v", res.Error)
return errors.Wrap(res.Error, "failed to delete peer")
}
return nil
}
func (m *PeerManager) UpdateDevice(device Device) error {
device.UpdatedAt = time.Now()
res := m.db.Save(&device)
if res.Error != nil {
logrus.Errorf("failed to update device: %v", res.Error)
return errors.Wrap(res.Error, "failed to update device")
}
return nil
}
// ---- IP helpers ----
func (m *PeerManager) GetAllReservedIps(device string) ([]string, error) {
reservedIps := make([]string, 0)
peers := m.GetAllPeers(device)
for _, user := range peers {
for _, cidr := range user.GetIPAddresses() {
if cidr == "" {
continue
}
ip, _, err := net.ParseCIDR(cidr)
if err != nil {
return nil, errors.Wrap(err, "failed to parse cidr")
}
reservedIps = append(reservedIps, ip.String())
}
}
dev := m.GetDevice(device)
for _, cidr := range dev.GetIPAddresses() {
if cidr == "" {
continue
}
ip, _, err := net.ParseCIDR(cidr)
if err != nil {
return nil, errors.Wrap(err, "failed to parse cidr")
}
reservedIps = append(reservedIps, ip.String())
}
return reservedIps, nil
}
func (m *PeerManager) IsIPReserved(device string, cidr string) bool {
reserved, err := m.GetAllReservedIps(device)
if err != nil {
return true // in case something failed, assume the ip is reserved
}
ip, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
return true
}
// this two addresses are not usable
broadcastAddr := common.BroadcastAddr(ipnet).String()
networkAddr := ipnet.IP.String()
address := ip.String()
if address == broadcastAddr || address == networkAddr {
return true
}
for _, r := range reserved {
if address == r {
return true
}
}
return false
}
// GetAvailableIp search for an available ip in cidr against a list of reserved ips
func (m *PeerManager) GetAvailableIp(device string, cidr string) (string, error) {
reserved, err := m.GetAllReservedIps(device)
if err != nil {
return "", errors.WithMessagef(err, "failed to get all reserved IP addresses for %s", device)
}
ip, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
return "", errors.Wrap(err, "failed to parse cidr")
}
// this two addresses are not usable
broadcastAddr := common.BroadcastAddr(ipnet).String()
networkAddr := ipnet.IP.String()
for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); common.IncreaseIP(ip) {
ok := true
address := ip.String()
for _, r := range reserved {
if address == r {
ok = false
break
}
}
if ok && address != networkAddr && address != broadcastAddr {
netMask := "/32"
if common.IsIPv6(address) {
netMask = "/128"
}
return address + netMask, nil
}
}
return "", errors.New("no more available address from cidr")
}

View File

@ -1,53 +1,20 @@
package wireguard package wireguard
var ( import (
ClientCfgTpl = `#{{ .Client.Identifier }} "embed"
[Interface] "strings"
Address = {{ .Client.IPsStr }} "text/template"
PrivateKey = {{ .Client.PrivateKey }}
{{- if .Server.DNSStr}}
DNS = {{ .Server.DNSStr }}
{{- end}}
{{- if ne .Server.Mtu 0}}
MTU = {{.Server.Mtu}}
{{- end}}
[Peer]
PublicKey = {{ .Server.PublicKey }}
{{- if .Client.PresharedKey}}
PresharedKey = {{ .Client.PresharedKey }}
{{- end}}
AllowedIPs = {{ .Client.AllowedIPsStr }}
Endpoint = {{ .Server.Endpoint }}
{{- if and (ne .Server.PersistentKeepalive 0) (not .Client.IgnorePersistentKeepalive)}}
PersistentKeepalive = {{.Server.PersistentKeepalive}}
{{- end}}
`
DeviceCfgTpl = `# AUTOGENERATED FILE - DO NOT EDIT
# Updated: {{ .Server.UpdatedAt }} / Created: {{ .Server.CreatedAt }}
[Interface]
{{- range .Server.IPs}}
Address = {{ . }}
{{- end}}
ListenPort = {{ .Server.ListenPort }}
PrivateKey = {{ .Server.PrivateKey }}
{{- if ne .Server.Mtu 0}}
MTU = {{.Server.Mtu}}
{{- end}}
PreUp = {{ .Server.PreUp }}
PostUp = {{ .Server.PostUp }}
PreDown = {{ .Server.PreDown }}
PostDown = {{ .Server.PostDown }}
{{range .Clients}}
{{if not .DeactivatedAt -}}
# {{.Identifier}} / {{.Email}} / Updated: {{.UpdatedAt}} / Created: {{.CreatedAt}}
[Peer]
PublicKey = {{ .PublicKey }}
{{- if .PresharedKey}}
PresharedKey = {{ .PresharedKey }}
{{- end}}
AllowedIPs = {{ StringsJoin .IPs ", " }}
{{- end}}
{{end}}`
) )
//go:embed tpl/*
var Templates embed.FS
var templateCache *template.Template
func init() {
var err error
templateCache, err = template.New("server").Funcs(template.FuncMap{"StringsJoin": strings.Join}).ParseFS(Templates, "tpl/*.tpl")
if err != nil {
panic(err)
}
}

View File

@ -0,0 +1,78 @@
# AUTOGENERATED FILE - DO NOT EDIT
# -WGP- Interface: {{ .Interface.DeviceName }} / Updated: {{ .Interface.UpdatedAt }} / Created: {{ .Interface.CreatedAt }}
# -WGP- Interface display name: {{ .Interface.DisplayName }}
# -WGP- Interface mode: {{ .Interface.Type }}
# -WGP- PublicKey = {{ .Interface.PublicKey }}
[Interface]
# Core settings
PrivateKey = {{ .Interface.PrivateKey }}
Address = {{ .Interface.IPsStr }}
# Misc. settings (optional)
{{- if ne .Interface.ListenPort 0}}
ListenPort = {{ .Interface.ListenPort }}
{{- end}}
{{- if ne .Interface.Mtu 0}}
MTU = {{.Interface.Mtu}}
{{- end}}
{{- if and (ne .Interface.DNSStr "") (eq $.Interface.Type "client")}}
DNS = {{ .Interface.DNSStr }}
{{- end}}
{{- if ne .Interface.FirewallMark 0}}
FwMark = {{.Interface.FirewallMark}}
{{- end}}
{{- if ne .Interface.RoutingTable ""}}
Table = {{.Interface.RoutingTable}}
{{- end}}
{{- if .Interface.SaveConfig}}
SaveConfig = true
{{- end}}
# Interface hooks (optional)
{{- if .Interface.PreUp}}
PreUp = {{ .Interface.PreUp }}
{{- end}}
{{- if .Interface.PostUp}}
PostUp = {{ .Interface.PostUp }}
{{- end}}
{{- if .Interface.PreDown}}
PreDown = {{ .Interface.PreDown }}
{{- end}}
{{- if .Interface.PostDown}}
PostDown = {{ .Interface.PostDown }}
{{- end}}
#
# Peers
#
{{range .Peers}}
{{- if not .DeactivatedAt}}
# -WGP- Peer: {{.Identifier}} / Updated: {{.UpdatedAt}} / Created: {{.CreatedAt}}
# -WGP- Peer email: {{.Email}}
{{- if .PrivateKey}}
# -WGP- PrivateKey: {{.PrivateKey}}
{{- end}}
[Peer]
PublicKey = {{ .PublicKey }}
{{- if .PresharedKey}}
PresharedKey = {{ .PresharedKey }}
{{- end}}
{{- if eq $.Interface.Type "server"}}
AllowedIPs = {{ .IPsStr }}
{{- end}}
{{- if eq $.Interface.Type "client"}}
{{- if .AllowedIPsStr}}
AllowedIPs = {{ .AllowedIPsStr }}
{{- end}}
{{- end}}
{{- if and (ne .Endpoint "") (eq $.Interface.Type "client")}}
Endpoint = {{ .Endpoint }}
{{- end}}
{{- if ne .PersistentKeepalive 0}}
PersistentKeepalive = {{ .PersistentKeepalive }}
{{- end}}
{{- end}}
{{end}}

View File

@ -0,0 +1,30 @@
# AUTOGENERATED FILE - PROVIDED BY WIREGUARD PORTAL
# WireGuard configuration: {{ .Peer.Identifier }}
# -WGP- PublicKey: {{ .Peer.PublicKey }}
[Interface]
# Core settings
PrivateKey = {{ .Peer.PrivateKey }}
Address = {{ .Peer.IPsStr }}
# Misc. settings (optional)
{{- if .Peer.DNSStr}}
DNS = {{ .Peer.DNSStr }}
{{- end}}
{{- if ne .Peer.Mtu 0}}
MTU = {{.Peer.Mtu}}
{{- end}}
[Peer]
PublicKey = {{ .Interface.PublicKey }}
Endpoint = {{ .Peer.Endpoint }}
{{- if .Peer.AllowedIPsStr}}
AllowedIPs = {{ .Peer.AllowedIPsStr }}
{{- end}}
{{- if .Peer.PresharedKey}}
PresharedKey = {{ .Peer.PresharedKey }}
{{- end}}
{{- if ne .Peer.PersistentKeepalive 0}}
PersistentKeepalive = {{.Peer.PersistentKeepalive}}
{{- end}}

View File

@ -1,6 +1,9 @@
LISTENING_ADDRESS=:8080 LISTENING_ADDRESS=:8080
WG_DEVICES=wg0
WG_DEFAULT_DEVICE=wg0
WG_CONFIG_PATH=/etc/wireguard
EXTERNAL_URL=https://vpn.company.com EXTERNAL_URL=https://vpn.company.com
WEBSITE_TITLE=WireGuard VPN WEBSITE_TITLE=WireGuard VPN
COMPANY_NAME=Your Company Name COMPANY_NAME=Your Company Name
ADMIN_USER=admin ADMIN_USER=admin@wgportal.local
ADMIN_PASS=supersecret ADMIN_PASS=supersecret