Compare commits

...

10 Commits

Author SHA1 Message Date
Christoph Haas
d978fd560d add asterisk to required fields, allow editing of device keys 2020-12-18 22:26:36 +01:00
Christoph Haas
ec60dd136a fix default mtu handling 2020-12-18 22:07:55 +01:00
Christoph Haas
6fd4089766 update raspi readme 2020-12-18 22:00:01 +01:00
Christoph Haas
4dd7f7b14b fix Dockerfile 2020-12-18 21:56:54 +01:00
Christoph Haas
10defaa2ba ip and mtu updates (linux only) 2020-12-18 21:54:57 +01:00
Christoph Haas
a95fe42efe fix ci and docker build 2020-12-17 16:23:55 +01:00
Christoph Haas
262e8e2047 fix a few bugs, add instructions for raspberry pi 2020-12-17 16:10:05 +01:00
Christoph Haas
edd09a9e13 fix newlines in template 2020-12-17 14:01:03 +01:00
Christoph Haas
814f57d357 fix newlines in template 2020-12-17 13:47:46 +01:00
Christoph Haas
793c2fc27e remove unnecessary script and css files 2020-12-02 19:26:18 +01:00
30 changed files with 332 additions and 38641 deletions

View File

@@ -37,7 +37,7 @@ RUN apt-get update && apt-get upgrade -y && \
chmod +rx /usr/local/bin/goss && \
goss --version
COPY --from=builder /build/dist/wg-portal /app/wgportal
COPY --from=builder /build/dist/wg-portal-amd64 /app/wgportal
COPY --from=builder /build/dist/assets /app/assets
COPY --from=builder /build/scripts /app/

View File

@@ -8,10 +8,17 @@ IMAGE=h44z/wg-portal
.PHONY: all test clean phony
all: dep test build
all: dep build
build: dep $(addprefix $(BUILDDIR)/,$(BINARIES))
build: dep $(addsuffix -amd64,$(addprefix $(BUILDDIR)/,$(BINARIES)))
cp -r assets $(BUILDDIR)
cp scripts/wg-portal.service $(BUILDDIR)
cp scripts/wg-portal.env $(BUILDDIR)
build-cross-plat: dep build $(addsuffix -arm,$(addprefix $(BUILDDIR)/,$(BINARIES))) $(addsuffix -arm64,$(addprefix $(BUILDDIR)/,$(BINARIES)))
cp -r assets $(BUILDDIR)
cp scripts/wg-portal.service $(BUILDDIR)
cp scripts/wg-portal.env $(BUILDDIR)
dep:
$(GOCMD) mod download
@@ -43,5 +50,13 @@ docker-build:
docker-push:
docker push $(IMAGE)
$(BUILDDIR)/%: cmd/%/main.go dep phony
$(GOCMD) build -o $@ $<
$(BUILDDIR)/%-amd64: cmd/%/main.go dep phony
GOOS=linux GOARCH=amd64 $(GOCMD) build -o $@ $<
# On arch-linux install aarch64-linux-gnu-gcc to crosscompile for arm64
$(BUILDDIR)/%-arm64: cmd/%/main.go dep phony
CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 $(GOCMD) build -ldflags "-linkmode external -extldflags -static" -o $@ $<
# On arch-linux install arm-linux-gnueabihf-gcc to crosscompile for arm
$(BUILDDIR)/%-arm: cmd/%/main.go dep phony
CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc GOOS=linux GOARCH=arm GOARM=7 $(GOCMD) build -ldflags "-linkmode external -extldflags -static" -o $@ $<

46
README-RASPBERRYPI.md Normal file
View File

@@ -0,0 +1,46 @@
# 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).
## Setup
You can download prebuild binaries from the [release page](https://github.com/h44z/wg-portal/releases). If you want to build the binary yourself,
use the following instructions:
### Building
This section describes how to build the WireGuard Portal code.
To compile the final binary, use the Makefile provided in the repository.
As WireGuard Portal is written in Go, **golang >= 1.14** must be installed prior to building.
```
make build-cross-plat
```
The compiled binary and all necessary assets will be located in the dist folder.
### Service setup
- Copy the contents from the dist folder (or from the downloaded zip file) to `/opt/wg-portal`. You can choose a different path as well, but make sure to update the systemd service file accordingly.
- Update the provided systemd `wg-portal.service` file:
- Make sure that the binary matches the system architecture.
- There are three pre-build binaries available: wg-portal-**amd64**, wg-portal-**arm64** and wg-portal-**arm**.
- For a raspberry pi use the arm binary if you are using armv7l architecture. If armv8 is used, the arm64 version should work.
- Make sure that the paths to the binary and the working directory are set correctly (defaults to /opt/wg-portal/wg-portal-amd64):
- ConditionPathExists
- WorkingDirectory
- ExecStart
- EnvironmentFile
- Update environment variables in the `wg-portal.env` file to fit your needs
- Make sure that the binary application file is executable
- `sudo chmod +x /opt/wg-portal/wg-portal-*`
- Link the system service file to the correct folder:
- `sudo ln -s /opt/wg-portal/wg-portal.service /etc/systemd/system/wg-portal.service`
- Reload the systemctl daemon:
- `sudo systemctl daemon-reload`
### Manage the service
Once the service has been setup, you can simply manage the service using `systemctl`:
- Enable on startup: `systemctl enable wg-portal.service`
- Start: `systemctl start wg-portal.service`
- Stop: `systemctl stop wg-portal.service`
- Status: `systemctl status wg-portal.service`

View File

@@ -34,7 +34,7 @@ will only be available in combination with LDAP.
### Docker
The easiest way to run WireGuard Portal is using the provided docker image.
Docker compose snippet, used for demo server
Docker compose snippet with sample values:
```
version: '3.6'
services:
@@ -74,14 +74,18 @@ For a standalone application, use the Makefile provided in the repository to bui
```
make
# To build for arm architecture as well use:
make build-cross-plat
```
The compiled binary and all necessary assets will be located in the dist folder.
A detailed description for using this software with a raspberry pi can be found in the [README-RASPBERRYPI.md](README-RASPBERRYPI.md).
## What is out of scope
* Generation or application of any `iptables` or `nftables` rules
* Setting up or changing IP-addresses of the WireGuard interface
* Setting up or changing IP-addresses of the WireGuard interface on operating systems other than linux
## Application stack

View File

@@ -1,209 +0,0 @@
/*!
* bootstrap-tokenfield
* https://github.com/sliptree/bootstrap-tokenfield
* Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
*/
@-webkit-keyframes 'blink' {
0% {
border-color: #ededed;
}
100% {
border-color: #b94a48;
}
}
@-moz-keyframes 'blink' {
0% {
border-color: #ededed;
}
100% {
border-color: #b94a48;
}
}
@keyframes 'blink' {
0% {
border-color: #ededed;
}
100% {
border-color: #b94a48;
}
}
.tokenfield {
height: auto;
min-height: 34px;
padding-bottom: 0px;
}
.tokenfield.focus {
border-color: #66afe9;
outline: 0;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);
}
.tokenfield .token {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
display: inline-block;
border: 1px solid #d9d9d9;
background-color: #ededed;
white-space: nowrap;
margin: -1px 5px 5px 0;
height: 22px;
vertical-align: top;
cursor: default;
}
.tokenfield .token:hover {
border-color: #b9b9b9;
}
.tokenfield .token.active {
border-color: #52a8ec;
border-color: rgba(82, 168, 236, 0.8);
}
.tokenfield .token.duplicate {
border-color: #ebccd1;
-webkit-animation-name: blink;
animation-name: blink;
-webkit-animation-duration: 0.1s;
animation-duration: 0.1s;
-webkit-animation-direction: normal;
animation-direction: normal;
-webkit-animation-timing-function: ease;
animation-timing-function: ease;
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.tokenfield .token.invalid {
background: none;
border: 1px solid transparent;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
border-bottom: 1px dotted #d9534f;
}
.tokenfield .token.invalid.active {
background: #ededed;
border: 1px solid #ededed;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
.tokenfield .token .token-label {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
padding-left: 4px;
vertical-align: top;
}
.tokenfield .token .close {
font-family: Arial;
display: inline-block;
line-height: 100%;
font-size: 1.1em;
line-height: 1.49em;
margin-left: 5px;
float: none;
height: 100%;
vertical-align: top;
padding-right: 4px;
}
.tokenfield .token-input {
background: none;
width: 60px;
min-width: 60px;
border: 0;
height: 20px;
padding: 0;
margin-bottom: 6px;
-webkit-box-shadow: none;
box-shadow: none;
}
.tokenfield .token-input:focus {
border-color: transparent;
outline: 0;
/* IE6-9 */
-webkit-box-shadow: none;
box-shadow: none;
}
.tokenfield.disabled {
cursor: not-allowed;
background-color: #eeeeee;
}
.tokenfield.disabled .token-input {
cursor: not-allowed;
}
.tokenfield.disabled .token:hover {
cursor: not-allowed;
border-color: #d9d9d9;
}
.tokenfield.disabled .token:hover .close {
cursor: not-allowed;
opacity: 0.2;
filter: alpha(opacity=20);
}
.has-warning .tokenfield.focus {
border-color: #66512c;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
}
.has-error .tokenfield.focus {
border-color: #843534;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
}
.has-success .tokenfield.focus {
border-color: #2b542c;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
}
.tokenfield.input-sm,
.input-group-sm .tokenfield {
min-height: 30px;
padding-bottom: 0px;
}
.input-group-sm .token,
.tokenfield.input-sm .token {
height: 20px;
margin-bottom: 4px;
}
.input-group-sm .token-input,
.tokenfield.input-sm .token-input {
height: 18px;
margin-bottom: 5px;
}
.tokenfield.input-lg,
.input-group-lg .tokenfield {
min-height: 45px;
padding-bottom: 4px;
}
.input-group-lg .token,
.tokenfield.input-lg .token {
height: 25px;
}
.input-group-lg .token-label,
.tokenfield.input-lg .token-label {
line-height: 23px;
}
.input-group-lg .token .close,
.tokenfield.input-lg .token .close {
line-height: 1.3em;
}
.input-group-lg .token-input,
.tokenfield.input-lg .token-input {
height: 23px;
line-height: 23px;
margin-bottom: 6px;
vertical-align: top;
}
.tokenfield.rtl {
direction: rtl;
text-align: right;
}
.tokenfield.rtl .token {
margin: -1px 0 5px 5px;
}
.tokenfield.rtl .token .token-label {
padding-left: 0px;
padding-right: 4px;
}

10150
assets/css/bootstrap.css vendored

File diff suppressed because it is too large Load Diff

View File

@@ -65,4 +65,9 @@ pre{background:#f7f7f9}iframe{overflow:hidden;border:none}@media (min-width: 768
background-color: #f7f7f9;
margin: -4px 5px 5px 0;
height: 22px;
}
.form-group.required label:after {
content:"*";
color:red;
}

1312
assets/css/jquery-ui.css vendored

File diff suppressed because it is too large Load Diff

View File

@@ -1,141 +0,0 @@
/*!
* bootstrap-tokenfield
* https://github.com/sliptree/bootstrap-tokenfield
* Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
*/
/* General Typeahead styling, from http://jsfiddle.net/ragulka/Dy9au/1/ */
.twitter-typeahead {
width: 100%;
position: relative;
vertical-align: top;
}
.twitter-typeahead .tt-input,
.twitter-typeahead .tt-hint {
margin: 0;
width: 100%;
vertical-align: middle;
background-color: #ffffff;
}
.twitter-typeahead .tt-hint {
color: #999999;
z-index: 1;
border: 1px solid transparent;
}
.twitter-typeahead .tt-input {
color: #555555;
z-index: 2;
}
.twitter-typeahead .tt-input,
.twitter-typeahead .tt-hint {
height: 34px;
padding: 6px 12px;
font-size: 14px;
line-height: 1.428571429;
}
.twitter-typeahead .input-sm.tt-input,
.twitter-typeahead .hint-sm.tt-hint {
border-radius: 3px;
}
.twitter-typeahead .input-lg.tt-input,
.twitter-typeahead .hint-lg.tt-hint {
border-radius: 6px;
}
.input-group .twitter-typeahead:first-child .tt-input,
.input-group .twitter-typeahead:first-child .tt-hint {
border-radius: 4px 0 0 4px !important;
}
.input-group .twitter-typeahead:last-child .tt-input,
.input-group .twitter-typeahead:last-child .tt-hint {
border-radius: 0 4px 4px 0 !important;
}
.input-group.input-group-sm .twitter-typeahead:first-child .tt-input,
.input-group.input-group-sm .twitter-typeahead:first-child .tt-hint {
border-radius: 3px 0 0 3px !important;
}
.input-group.input-group-sm .twitter-typeahead:last-child .tt-input,
.input-group.input-group-sm .twitter-typeahead:last-child .tt-hint {
border-radius: 0 3px 3px 0 !important;
}
.input-sm.tt-input,
.hint-sm.tt-hint,
.input-group.input-group-sm .tt-input,
.input-group.input-group-sm .tt-hint {
height: 30px;
padding: 5px 10px;
font-size: 12px;
line-height: 1.5;
}
.input-group.input-group-lg .twitter-typeahead:first-child .tt-input,
.input-group.input-group-lg .twitter-typeahead:first-child .tt-hint {
border-radius: 6px 0 0 6px !important;
}
.input-group.input-group-lg .twitter-typeahead:last-child .tt-input,
.input-group.input-group-lg .twitter-typeahead:last-child .tt-hint {
border-radius: 0 6px 6px 0 !important;
}
.input-lg.tt-input,
.hint-lg.tt-hint,
.input-group.input-group-lg .tt-input,
.input-group.input-group-lg .tt-hint {
height: 45px;
padding: 10px 16px;
font-size: 18px;
line-height: 1.33;
}
.tt-dropdown-menu {
width: 100%;
min-width: 160px;
margin-top: 2px;
padding: 5px 0;
background-color: #ffffff;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, 0.15);
*border-right-width: 2px;
*border-bottom-width: 2px;
border-radius: 6px;
-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
-webkit-background-clip: padding-box;
-moz-background-clip: padding;
background-clip: padding-box;
}
.tt-suggestion {
display: block;
padding: 3px 20px;
}
.tt-suggestion.tt-cursor {
color: #262626;
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
}
.tt-suggestion.tt-cursor a {
color: #ffffff;
}
.tt-suggestion p {
margin: 0;
}
/* Tokenfield-specific Typeahead styling */
.tokenfield .twitter-typeahead {
width: auto;
}
.tokenfield .twitter-typeahead .tt-hint {
padding: 0;
height: 20px;
}
.tokenfield.input-sm .twitter-typeahead .tt-input,
.tokenfield.input-sm .twitter-typeahead .tt-hint {
height: 18px;
font-size: 12px;
line-height: 1.5;
}
.tokenfield.input-lg .twitter-typeahead .tt-input,
.tokenfield.input-lg .twitter-typeahead .tt-hint {
height: 23px;
font-size: 18px;
line-height: 1.33;
}
.tokenfield .twitter-typeahead .tt-suggestions {
font-size: 14px;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

18706
assets/js/jquery-ui.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -21,13 +21,13 @@
{{template "prt_flashes.html" .}}
<form method="post" enctype="multipart/form-data">
<div class="form-row">
<div class="form-group col-md-12">
<div class="form-group required col-md-12">
<label for="inputEmail">Email Addresses</label>
<input type="text" name="email" class="form-control" id="inputEmail" value="{{.FormData.Emails}}">
</div>
</div>
<div class="form-row">
<div class="form-group 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>
<input type="text" name="identifier" class="form-control" id="inputIdentifier" value="{{.FormData.Identifier}}">
</div>

View File

@@ -31,7 +31,7 @@
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<div class="form-group required col-md-12">
<label for="inputServerPublicKey">Public Key</label>
<input type="text" name="pubkey" class="form-control" id="inputServerPublicKey" value="{{.Peer.PublicKey}}">
</div>
@@ -53,25 +53,25 @@
</div>
{{end}}
<div class="form-row">
<div class="form-group col-md-12">
<div class="form-group required col-md-12">
<label for="inputIdentifier">Client Friendly Name</label>
<input type="text" name="identifier" class="form-control" id="inputIdentifier" value="{{.Peer.Identifier}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<div class="form-group required col-md-12">
<label for="inputEmail">Client Email Address</label>
<input type="email" name="mail" class="form-control" id="inputEmail" value="{{.Peer.Email}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<div class="form-group required col-md-12">
<label for="inputIP">Client IP Address</label>
<input type="text" name="ip" class="form-control" id="inputIP" value="{{.Peer.IPsStr}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<div class="form-group required col-md-12">
<label for="inputAllowedIP">Allowed IPs</label>
<input type="text" name="allowedip" class="form-control" id="inputAllowedIP" value="{{.Peer.AllowedIPsStr}}">
</div>

View File

@@ -18,27 +18,42 @@
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="device" value="{{.Device.DeviceName}}">
<input type="hidden" name="privkey" value="{{.Device.PrivateKey}}">
<h3>Server's interface configuration</h3>
{{if .EditableKeys}}
<div class="form-row">
<div class="form-group required col-md-12">
<label for="inputServerPrivateKey">Private Key</label>
<input type="text" name="privkey" class="form-control" id="inputServerPrivateKey" value="{{.Device.PrivateKey}}">
</div>
</div>
<div class="form-row">
<div class="form-group required col-md-12">
<label for="inputServerPublicKey">Public Key</label>
<input type="text" name="pubkey" class="form-control" id="inputServerPublicKey" value="{{.Device.PublicKey}}">
</div>
</div>
{{else}}
<input type="hidden" name="privkey" value="{{.Device.PrivateKey}}">
<div class="form-row">
<div class="form-group col-md-12">
<label for="inputServerPublicKey">Public Key</label>
<input type="text" name="pubkey" readonly class="form-control" id="inputServerPublicKey" value="{{.Device.PublicKey}}">
</div>
</div>
{{end}}
<div class="form-row">
<div class="form-group col-md-6">
<div class="form-group required col-md-6">
<label for="inputListenPort">Listen port</label>
<input type="number" name="port" class="form-control" id="inputListenPort" placeholder="51820" value="{{.Device.ListenPort}}">
</div>
<div class="form-group col-md-6">
<div class="form-group required col-md-6">
<label for="inputIPs">Server IP address</label>
<input type="text" name="ip" class="form-control" id="inputIPs" placeholder="10.6.6.1/24" value="{{.Device.IPsStr}}">
</div>
</div>
<h3>Client's global configuration</h3>
<div class="form-row">
<div class="form-group col-md-12">
<div class="form-group required col-md-12">
<label for="inputPublicEndpoint">Public Enpoint for Clients</label>
<input type="text" name="endpoint" class="form-control" id="inputPublicEndpoint" placeholder="vpn.company.com:51820" value="{{.Device.Endpoint}}">
</div>
@@ -91,7 +106,7 @@
<button type="submit" class="btn btn-primary">Save</button>
<a href="/admin" class="btn btn-secondary">Cancel</a>
<a href="/admin/applyglobals" class="btn btn-dark float-right">Apply Allowed IP's to clients</a>
<a href="/admin/device/applyglobals" class="btn btn-dark float-right">Apply Allowed IP's to clients</a>
</form>
</div>
{{template "prt_footer.html" .}}

View File

@@ -100,7 +100,7 @@
<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">
<tr>
{{if not .Client.LdapUser}}
{{if .Client.LdapUser}}
<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>
{{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>

View File

@@ -48,7 +48,6 @@
<script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/jquery.easing.js"></script>
<script src="/js/jquery.fancybox.min.js"></script>
<script src="/js/custom.js"></script>
</body>

1
go.mod
View File

@@ -10,6 +10,7 @@ require (
github.com/gorilla/sessions v1.2.1 // indirect
github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible
github.com/kelseyhightower/envconfig v1.4.0
github.com/milosgajdos/tenus v0.0.3
github.com/sirupsen/logrus v1.7.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e

View File

@@ -4,6 +4,7 @@ import (
"errors"
"os"
"reflect"
"runtime"
"github.com/h44z/wg-portal/internal/wireguard"
@@ -91,6 +92,7 @@ func NewConfig() *Config {
cfg.LDAP.BindPass = "SuperSecret"
cfg.WG.DeviceName = "wg0"
cfg.WG.WireGuardConfig = "/etc/wireguard/wg0.conf"
cfg.WG.ManageIPAddresses = true
cfg.AdminLdapGroup = "CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL"
cfg.Email.Host = "127.0.0.1"
cfg.Email.Port = 25
@@ -109,5 +111,10 @@ func NewConfig() *Config {
log.Warnf("unable to load environment config: %v", err)
}
if cfg.WG.ManageIPAddresses && runtime.GOOS != "linux" {
log.Warnf("Managing IP addresses only works on linux! Feature disabled.")
cfg.WG.ManageIPAddresses = false
}
return cfg
}

View File

@@ -19,19 +19,21 @@ func (s *Server) GetAdminEditInterface(c *gin.Context) {
}
c.HTML(http.StatusOK, "admin_edit_interface.html", struct {
Route string
Alerts []FlashData
Session SessionData
Static StaticData
Peers []User
Device Device
Route string
Alerts []FlashData
Session SessionData
Static StaticData
Peers []User
Device Device
EditableKeys bool
}{
Route: c.Request.URL.Path,
Alerts: s.getFlashes(c),
Session: currentSession,
Static: s.getStaticData(),
Peers: users,
Device: currentSession.FormData.(Device),
Route: c.Request.URL.Path,
Alerts: s.getFlashes(c),
Session: currentSession,
Static: s.getStaticData(),
Peers: users,
Device: currentSession.FormData.(Device),
EditableKeys: s.config.Core.EditableKeys,
})
}
@@ -73,8 +75,33 @@ func (s *Server) PostAdminEditInterface(c *gin.Context) {
return
}
// Update WireGuard config file
err = s.WriteWireGuardConfigFile()
if err != nil {
_ = s.updateFormInSession(c, formDevice)
s.setFlashMessage(c, "Failed to update wireguard config-file: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update")
return
}
// Update interface IP address
if s.config.WG.ManageIPAddresses {
if err := s.wg.SetIPAddress(formDevice.IPs); err != nil {
_ = s.updateFormInSession(c, formDevice)
s.setFlashMessage(c, "Failed to update ip address: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update")
}
if err := s.wg.SetMTU(formDevice.Mtu); err != nil {
_ = s.updateFormInSession(c, formDevice)
s.setFlashMessage(c, "Failed to update MTU: "+err.Error(), "danger")
c.Redirect(http.StatusSeeOther, "/admin/device/edit?formerr=update")
}
}
s.setFlashMessage(c, "Changes applied successfully!", "success")
s.setFlashMessage(c, "WireGuard must be restarted to apply ip changes.", "warning")
if !s.config.WG.ManageIPAddresses {
s.setFlashMessage(c, "WireGuard must be restarted to apply ip changes.", "warning")
}
c.Redirect(http.StatusSeeOther, "/admin/device/edit")
}

View File

@@ -202,7 +202,7 @@ type Device struct {
Interface *wgtypes.Device `gorm:"-"`
DeviceName string `form:"device" gorm:"primaryKey" binding:"required,alphanum"`
PrivateKey string `form:"privkey" binding:"base64"`
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"`
@@ -319,6 +319,18 @@ func (u *UserManager) InitFromCurrentInterface() error {
log.Errorf("failed to init user-manager from device: %v", err)
return err
}
var ipAddresses []string
var mtu int
if u.wg.Cfg.ManageIPAddresses {
if ipAddresses, err = u.wg.GetIPAddress(); err != nil {
log.Errorf("failed to init user-manager from device: %v", err)
return err
}
if mtu, err = u.wg.GetMTU(); err != nil {
log.Errorf("failed to init user-manager from device: %v", err)
return err
}
}
// Check if entries already exist in database, if not create them
for _, peer := range peers {
@@ -326,7 +338,7 @@ func (u *UserManager) InitFromCurrentInterface() error {
return err
}
}
if err := u.validateOrCreateDevice(*device); err != nil {
if err := u.validateOrCreateDevice(*device, ipAddresses, mtu); err != nil {
return err
}
@@ -366,7 +378,7 @@ func (u *UserManager) validateOrCreateUserForPeer(peer wgtypes.Peer) error {
return nil
}
func (u *UserManager) validateOrCreateDevice(dev wgtypes.Device) error {
func (u *UserManager) validateOrCreateDevice(dev wgtypes.Device, ipAddresses []string, mtu int) error {
device := Device{}
u.db.Where("device_name = ?", dev.Name).FirstOrInit(&device)
@@ -377,6 +389,11 @@ func (u *UserManager) validateOrCreateDevice(dev wgtypes.Device) error {
device.ListenPort = dev.ListenPort
device.Mtu = 0
device.PersistentKeepalive = 16 // Default
device.IPsStr = strings.Join(ipAddresses, ", ")
if mtu == wireguard.WireGuardDefaultMTU {
mtu = 0
}
device.Mtu = mtu
res := u.db.Create(&device)
if res.Error != nil {

View File

@@ -1,6 +1,7 @@
package wireguard
type Config struct {
DeviceName string `yaml:"device" envconfig:"WG_DEVICE"`
WireGuardConfig string `yaml:"configFile" envconfig:"WG_CONFIG_FILE"` // optional, if set, updates will be written to this file
DeviceName string `yaml:"device" envconfig:"WG_DEVICE"`
WireGuardConfig string `yaml:"configFile" envconfig:"WG_CONFIG_FILE"` // optional, if set, updates will be written to this file
ManageIPAddresses bool `yaml:"manageIPAddresses" envconfig:"MANAGE_IPS"` // handle ip-address setup of interface
}

120
internal/wireguard/net.go Normal file
View File

@@ -0,0 +1,120 @@
package wireguard
import (
"fmt"
"net"
"github.com/milosgajdos/tenus"
)
const WireGuardDefaultMTU = 1420
func (m *Manager) GetIPAddress() ([]string, error) {
wgInterface, err := tenus.NewLinkFrom(m.Cfg.DeviceName)
if err != nil {
return nil, fmt.Errorf("could not retrieve WireGuard interface %s: %w", m.Cfg.DeviceName, err)
}
// Get golang net.interface
iface := wgInterface.NetInterface()
if iface == nil { // Not sure if this check is really necessary
return nil, fmt.Errorf("could not retrieve WireGuard net.interface: %w", err)
}
addrs, err := iface.Addrs()
if err != nil {
return nil, fmt.Errorf("could not retrieve WireGuard ip addresses: %w", err)
}
ipAddresses := make([]string, 0, len(addrs))
for _, addr := range addrs {
var ip net.IP
var mask net.IPMask
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
mask = v.Mask
case *net.IPAddr:
ip = v.IP
mask = ip.DefaultMask()
}
if ip == nil {
continue // something is wrong?
}
maskSize, _ := mask.Size()
cidr := fmt.Sprintf("%s/%d", ip.String(), maskSize)
ipAddresses = append(ipAddresses, cidr)
}
return ipAddresses, nil
}
func (m *Manager) SetIPAddress(cidrs []string) error {
wgInterface, err := tenus.NewLinkFrom(m.Cfg.DeviceName)
if err != nil {
return fmt.Errorf("could not retrieve WireGuard interface %s: %w", m.Cfg.DeviceName, err)
}
// First remove existing IP addresses
existingIPs, err := m.GetIPAddress()
if err != nil {
return err
}
for _, cidr := range existingIPs {
wgIp, wgIpNet, err := net.ParseCIDR(cidr)
if err != nil {
return fmt.Errorf("unable to parse cidr %s: %w", cidr, err)
}
if err := wgInterface.UnsetLinkIp(wgIp, wgIpNet); err != nil {
return fmt.Errorf("failed to unset ip %s: %w", cidr, err)
}
}
// Next set new IP adrresses
for _, cidr := range cidrs {
wgIp, wgIpNet, err := net.ParseCIDR(cidr)
if err != nil {
return fmt.Errorf("unable to parse cidr %s: %w", cidr, err)
}
if err := wgInterface.SetLinkIp(wgIp, wgIpNet); err != nil {
return fmt.Errorf("failed to set ip %s: %w", cidr, err)
}
}
return nil
}
func (m *Manager) GetMTU() (int, error) {
wgInterface, err := tenus.NewLinkFrom(m.Cfg.DeviceName)
if err != nil {
return 0, fmt.Errorf("could not retrieve WireGuard interface %s: %w", m.Cfg.DeviceName, err)
}
// Get golang net.interface
iface := wgInterface.NetInterface()
if iface == nil { // Not sure if this check is really necessary
return 0, fmt.Errorf("could not retrieve WireGuard net.interface: %w", err)
}
return iface.MTU, nil
}
func (m *Manager) SetMTU(mtu int) error {
wgInterface, err := tenus.NewLinkFrom(m.Cfg.DeviceName)
if err != nil {
return fmt.Errorf("could not retrieve WireGuard interface %s: %w", m.Cfg.DeviceName, err)
}
if mtu == 0 {
mtu = WireGuardDefaultMTU
}
if err := wgInterface.SetLinkMTU(mtu); err != nil {
return fmt.Errorf("could not set MTU on interface %s: %w", m.Cfg.DeviceName, err)
}
return nil
}

View File

@@ -5,35 +5,35 @@ var (
[Interface]
Address = {{ .Client.IPsStr }}
PrivateKey = {{ .Client.PrivateKey }}
{{if .Server.DNSStr -}}
{{- if .Server.DNSStr}}
DNS = {{ .Server.DNSStr }}
{{- end}}
{{- if ne .Server.Mtu 0 -}}
{{- if ne .Server.Mtu 0}}
MTU = {{.Server.Mtu}}
{{- end}}
[Peer]
PublicKey = {{ .Server.PublicKey }}
{{- if .Client.PresharedKey -}}
{{- if .Client.PresharedKey}}
PresharedKey = {{ .Client.PresharedKey }}
{{- end -}}
{{- end}}
AllowedIPs = {{ .Client.AllowedIPsStr }}
Endpoint = {{ .Server.Endpoint }}
{{if and (ne .Server.PersistentKeepalive 0) (not .Client.IgnorePersistentKeepalive) -}}
{{- 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 }}
{{- range .Server.IPs}}
Address = {{ . }}
{{- end}}
ListenPort = {{ .Server.ListenPort }}
PrivateKey = {{ .Server.PrivateKey }}
{{- if ne .Server.Mtu 0 -}}
{{- if ne .Server.Mtu 0}}
MTU = {{.Server.Mtu}}
{{- end -}}
{{- end}}
PreUp = {{ .Server.PreUp }}
PostUp = {{ .Server.PostUp }}
PreDown = {{ .Server.PreDown }}
@@ -44,9 +44,9 @@ PostDown = {{ .Server.PostDown }}
# {{.Identifier}} / {{.Email}} / Updated: {{.UpdatedAt}} / Created: {{.CreatedAt}}
[Peer]
PublicKey = {{ .PublicKey }}
{{- if .PresharedKey -}}
{{- if .PresharedKey}}
PresharedKey = {{ .PresharedKey }}
{{- end -}}
{{- end}}
AllowedIPs = {{ StringsJoin .IPs ", " }}
{{- end}}
{{end}}`

6
scripts/wg-portal.env Normal file
View File

@@ -0,0 +1,6 @@
LISTENING_ADDRESS=:8080
EXTERNAL_URL=https://vpn.company.com
WEBSITE_TITLE=WireGuard VPN
COMPANY_NAME=Your Company Name
ADMIN_USER=admin
ADMIN_PASS=supersecret

19
scripts/wg-portal.service Normal file
View File

@@ -0,0 +1,19 @@
[Unit]
Description=WireGuard Portal
ConditionPathExists=/opt/wg-portal/wg-portal-amd64
After=network.target
[Service]
Type=simple
User=root
Group=root
Restart=on-failure
RestartSec=10
WorkingDirectory=/opt/wg-portal
ExecStart=/opt/wg-portal/wg-portal-amd64
EnvironmentFile=/opt/wg-portal/wg-portal.env
[Install]
WantedBy=multi-user.target