Initial commit

This commit is contained in:
vx3r 2020-01-30 15:45:49 +09:00
commit 024d2b4ebb
32 changed files with 11342 additions and 0 deletions

6
.env Normal file
View File

@ -0,0 +1,6 @@
SERVER=0.0.0.0
PORT=8080
GIN_MODE=debug

WG_CONF_DIR=./wireguard
WG_INTERFACE_NAME=wg0.conf

34
.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# Vendor
vendor/*

*.exe

.idea/*

wireguard

ui/.idea/*
ui/dist/*

.DS_Store
ui/node_modules
/dist

# local env files
ui/.env.local
ui/.env.*.local

.idea

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

35
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,35 @@
stages:
- build
- deploy

build-back:
stage: build
image: golang:latest
script:
- GOOS=linux GOARCH=amd64 go build -o ${CI_PROJECT_NAME}-linux-amd64
artifacts:
paths:
- ${CI_PROJECT_NAME}-linux-amd64

build-front:
stage: build
image: node:10-alpine
script:
- cd ./ui
- npm install
- npm run build
- cd ..
artifacts:
paths:
- ui/dist

deploy:
stage: deploy
image: docker:latest
only:
- master
script:
- docker login -u ${CI_REGISTRY_USER} -p ${REGISTRY_PASSWORD} ${CI_REGISTRY}
- docker build --tag ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA} --tag ${CI_REGISTRY_IMAGE}:latest .
- docker push ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}
- docker push ${CI_REGISTRY_IMAGE}:latest

13
Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM debian:stable-slim

WORKDIR /app

ADD wg-gen-web-linux-amd64 .
ADD .env .
ADD ui/dist ui/dist

RUN chmod +x ./wg-gen-web-linux-amd64

EXPOSE 8080

CMD ["/app/wg-gen-web-linux-amd64"]

13
LICENSE-WTFPL Normal file
View File

@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004

Copyright (C) 2013 Stephen Mathieson <me@stephenmathieson.com>

Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.

DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

0. You just DO WHAT THE FUCK YOU WANT TO.

60
README.md Normal file
View File

@ -0,0 +1,60 @@
# Wg Gen Web

Simple Web based configuration generator for [WireGuard](https://wireguard.com).

## Whay another one ?

All WireGuard UI implementation are trying to manage the WireGuard by applying configurations or creation network rules.
This implementation only generate configuration and its up to you to create network rules and apply configuration to WireGuard.
For example by monituring generated directory with [inotifywait](https://github.com/inotify-tools/inotify-tools/wiki).

The goal is to run Wg Gen Web in a container and WireGuard on host system.

## Features

* Self-serve and web based
* Automatically select IP from networks chosen for client
* QR-Code for convenient mobile client configuration
* Enable / Disable client
* Generation of `wg0.conf` after any modification
* Dockerized
* Pretty cool look
![Screenshot](Wg-Gen-Web.png)

## Running

The easiest way to run wireguard-ui is using the container image
```
docker run --rm -it -v /tmp/wireguard:/data -p 8080:8080 -e "WG_CONF_DIR=/data" vx3r/wg-gen-web:latest
```
docker compose snipped
```
version: '3.6'
services:
wg-gen-web:
image: vx3r/wg-gen-web:latest
container_name: wg-gen-web
restart: unless-stopped
expose:
- "8080/tcp"
environment:
- WG_CONF_DIR=/data
- WG_INTERFACE_NAME=wg0.conf
volumes:
- /mnt/raid-lv-data/docker-persistent-data/wg-gen-web:/data
```

## What is out of scope

* Generation or application of any `iptables` or `nftables` rules
* Application of configuration to WireGuard

## TODO

* Full setup example with `inotifywait` and `systemd`
* Multi-user support behind [Authelia](https://github.com/authelia/authelia) (suggestions / thoughts are welcome)
* Send configs by email to client
## License

* Do What the Fuck You Want to Public License. [LICENSE-WTFPL](LICENSE-WTFPL) or http://www.wtfpl.net

BIN
Wg-Gen-Web.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

188
api/api.go Normal file
View File

@ -0,0 +1,188 @@
package api

import (
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"github.com/skip2/go-qrcode"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/repository"
"net/http"
)

// ApplyRoutes applies router to gin Router
func ApplyRoutes(r *gin.Engine) {
client := r.Group("/api/v1.0/client")
{

client.POST("", createClient)
client.GET("/:id", readClient)
client.PATCH("/:id", updateClient)
client.DELETE("/:id", deleteClient)
client.GET("", readClients)
client.GET("/:id/config", configClient)
}

server := r.Group("/api/v1.0/server")
{
server.GET("", readServer)
server.PATCH("", updateServer)
}
}


func createClient(c *gin.Context) {
var data model.Client

if err := c.ShouldBindJSON(&data); err != nil {
log.WithFields(log.Fields{
"err": err,
}).Error("failed to bind")
c.AbortWithStatus(http.StatusUnprocessableEntity)
return
}

client, err := repository.CreateClient(&data)
if err != nil {
log.WithFields(log.Fields{
"err": err,
}).Error("failed to create client")
c.AbortWithStatus(http.StatusInternalServerError)
return
}

c.JSON(http.StatusOK, client)
}

func readClient(c *gin.Context) {
id := c.Param("id")

client, err := repository.ReadClient(id)
if err != nil {
log.WithFields(log.Fields{
"err": err,
}).Error("failed to read client")
c.AbortWithStatus(http.StatusInternalServerError)
return
}

c.JSON(http.StatusOK, client)
}

func updateClient(c *gin.Context) {
var data model.Client
id := c.Param("id")

if err := c.ShouldBindJSON(&data); err != nil {
log.WithFields(log.Fields{
"err": err,
}).Error("failed to bind")
c.AbortWithStatus(http.StatusUnprocessableEntity)
return
}

client, err := repository.UpdateClient(id, &data)
if err != nil {
log.WithFields(log.Fields{
"err": err,
}).Error("failed to update client")
c.AbortWithStatus(http.StatusInternalServerError)
return
}

c.JSON(http.StatusOK, client)
}

func deleteClient(c *gin.Context) {
id := c.Param("id")

err := repository.DeleteClient(id)
if err != nil {
log.WithFields(log.Fields{
"err": err,
}).Error("failed to remove client")
c.AbortWithStatus(http.StatusInternalServerError)
return
}

c.JSON(http.StatusOK, gin.H{})
}

func readClients(c *gin.Context) {
clients, err := repository.ReadClients()
if err != nil {
log.WithFields(log.Fields{
"err": err,
}).Error("failed to list clients")
c.AbortWithStatus(http.StatusInternalServerError)
return
}

c.JSON(http.StatusOK, clients)
}

func configClient(c *gin.Context) {
configData, err := repository.ReadClientConfig(c.Param("id"))
if err != nil {
log.WithFields(log.Fields{
"err": err,
}).Error("failed to read client config")
c.AbortWithStatus(http.StatusInternalServerError)
return
}

formatQr := c.DefaultQuery("qrcode", "false")
if formatQr == "false" {
// return config as txt file
c.Header("Content-Disposition", "attachment; filename=wg0.conf")
c.Data(http.StatusOK, "application/config", configData)
return
} else {
// return config as png qrcode
png, err := qrcode.Encode(string(configData), qrcode.Medium, 220)
if err != nil {
log.WithFields(log.Fields{
"err": err,
}).Error("failed to create qrcode")
c.AbortWithStatus(http.StatusInternalServerError)
return
}
c.Data(http.StatusOK, "image/png", png)
return
}
}

func readServer(c *gin.Context) {
client, err := repository.ReadServer()
if err != nil {
log.WithFields(log.Fields{
"err": err,
}).Error("failed to read client")
c.AbortWithStatus(http.StatusInternalServerError)
return
}

c.JSON(http.StatusOK, client)
}

func updateServer(c *gin.Context) {
var data model.Server

if err := c.ShouldBindJSON(&data); err != nil {
log.WithFields(log.Fields{
"err": err,
}).Error("failed to bind")
c.AbortWithStatus(http.StatusUnprocessableEntity)
return
}

client, err := repository.UpdateServer(&data)
if err != nil {
log.WithFields(log.Fields{
"err": err,
}).Error("failed to update client")
c.AbortWithStatus(http.StatusInternalServerError)
return
}

c.JSON(http.StatusOK, client)
}

16
go.mod Normal file
View File

@ -0,0 +1,16 @@
module gitlab.127-0-0-1.fr/vx3r/wg-gen-web

go 1.13

require (
github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e
github.com/gin-contrib/cors v1.3.0
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2
github.com/gin-gonic/gin v1.5.0
github.com/joho/godotenv v1.3.0
github.com/satori/go.uuid v1.2.0
github.com/sirupsen/logrus v1.4.2
github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200114203027-fcfc50b29cbb
)

113
go.sum Normal file
View File

@ -0,0 +1,113 @@
github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e h1:5jVSh2l/ho6ajWhSPNN84eHEdq3dp0T7+f6r3Tc6hsk=
github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e/go.mod h1:IJgIiGUARc4aOr4bOQ85klmjsShkEEfiRc6q/yBSfo8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/gin-contrib/cors v1.3.0 h1:PolezCc89peu+NgkIWt9OB01Kbzt6IP0J/JvkG6xxlg=
github.com/gin-contrib/cors v1.3.0/go.mod h1:artPvLlhkF7oG06nK8v3U8TNz6IeX+w1uzCSEId5/Vc=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2 h1:xLG16iua01X7Gzms9045s2Y2niNpvSY/Zb1oBwgNYZY=
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2/go.mod h1:VhW/Ch/3FhimwZb8Oj+qJmdMmoB8r7lmJ5auRjm50oQ=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc=
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
github.com/mdlayher/netlink v1.0.0 h1:vySPY5Oxnn/8lxAPn2cK6kAzcZzYJl3KriSLO46OT18=
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 h1:RYiqpb2ii2Z6J4x0wxK46kvPBbFuZcdhS+CIztmYgZs=
github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg=
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191218084908-4a24b4065292 h1:Y8q0zsdcgAd+JU8VUA8p8Qv2YhuY9zevDG2ORt5qBUI=
golang.org/x/sys v0.0.0-20191218084908-4a24b4065292/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.zx2c4.com/wireguard v0.0.20191012 h1:sdX+y3hrHkW8KJkjY7ZgzpT5Tqo8XnBkH55U1klphko=
golang.zx2c4.com/wireguard v0.0.20191012/go.mod h1:P2HsVp8SKwZEufsnezXZA4GRX/T49/HlU7DGuelXsU4=
golang.zx2c4.com/wireguard v0.0.20200121 h1:vcswa5Q6f+sylDfjqyrVNNrjsFUUbPsgAQTBCAg/Qf8=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200114203027-fcfc50b29cbb h1:EZFZIHfDUPApqlA2wgF5LBAXKIKAxNckrehUTPYYAHc=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200114203027-fcfc50b29cbb/go.mod h1:vpFXH8L2bfaEJ/8I7DZ0CVOHsVydo6KeW9Iqh3qMa4g=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

79
main.go Normal file
View File

@ -0,0 +1,79 @@
package main

import (
"fmt"
"github.com/danielkov/gin-helmet"
"github.com/gin-contrib/cors"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
log "github.com/sirupsen/logrus"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
"os"
"path/filepath"
)

func init() {
log.SetFormatter(&log.TextFormatter{})
log.SetOutput(os.Stderr)
log.SetLevel(log.DebugLevel)
}

func main() {
// load .env environment variables
err := godotenv.Load()
if err != nil {
log.WithFields(log.Fields{
"err": err,
}).Fatal("failed to initialize env")
}

// check directories or create it
if !util.DirectoryExists(filepath.Join(os.Getenv("WG_CONF_DIR"))) {
err = os.Mkdir(filepath.Join(os.Getenv("WG_CONF_DIR")), 0755)
if err != nil {
log.WithFields(log.Fields{
"err": err,
"dir": filepath.Join(os.Getenv("WG_CONF_DIR")),
}).Fatal("failed to mkdir")
}
}

if os.Getenv("GIN_MODE") == "release" {
// set gin release mode
gin.SetMode(gin.ReleaseMode)
// disable console color
gin.DisableConsoleColor()
}

// creates a gin router with default middleware: logger and recovery (crash-free) middleware
app := gin.Default()

// same as
config := cors.DefaultConfig()
config.AllowAllOrigins = true
app.Use(cors.New(config))
//app.Use(cors.Default())

// protection
app.Use(helmet.Default())

// no route redirect to frontend app
app.NoRoute(func(c *gin.Context) {
c.Redirect(301, "/index.html")
})

// serve static files
app.Use(static.Serve("/", static.LocalFile("./ui/dist", false)))

// apply api router
api.ApplyRoutes(app)

err = app.Run(fmt.Sprintf("%s:%s", os.Getenv("SERVER"), os.Getenv("PORT")))
if err != nil {
log.WithFields(log.Fields{
"err": err,
}).Fatal("failed to start server")
}
}

16
model/client.go Normal file
View File

@ -0,0 +1,16 @@
package model

import "time"

type Client struct {
Id string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Enable bool `json:"enable"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
AllowedIPs string `json:"allowedIPs"`
Address string `json:"address"`
PrivateKey string `json:"privateKey"`
PublicKey string `json:"publicKey"`
}

17
model/server.go Normal file
View File

@ -0,0 +1,17 @@
package model

import "time"

type Server struct {
Name string `json:"name"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Address string `json:"address"`
ListenPort int `json:"listenPort"`
PrivateKey string `json:"privateKey"`
PublicKey string `json:"publicKey"`
PresharedKey string `json:"presharedKey"`
Endpoint string `json:"endpoint"`
PersistentKeepalive int `json:"persistentKeepalive"`
Dns string `json:"dns"`
}

335
repository/repository.go Normal file
View File

@ -0,0 +1,335 @@
package repository

import (
"encoding/json"
"errors"
uuid "github.com/satori/go.uuid"
log "github.com/sirupsen/logrus"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"time"
)

func CreateClient(client *model.Client) (*model.Client, error) {
u := uuid.NewV4()
client.Id = u.String()

key, err := wgtypes.GeneratePrivateKey()
if err != nil {
return nil, err
}
client.PrivateKey = key.String()
client.PublicKey = key.PublicKey().String()

// find available IP address from selected networks
clients, err := ReadClients()
if err != nil {
return nil, err
}

reserverIps := make([]string, 0)
for _, client := range clients {
ips := strings.Split(client.Address, ",")
for i := range ips {
if util.IsIPv6(ips[i]){
ips[i] = strings.ReplaceAll(strings.TrimSpace(ips[i]), "/128","")
} else {
ips[i] = strings.ReplaceAll(strings.TrimSpace(ips[i]), "/32","")
}
}
reserverIps = append(reserverIps, ips...)
}

networks := strings.Split(client.Address, ",")
for i := range networks {
networks[i] = strings.TrimSpace(networks[i])
}
ips := make([]string, 0)
for _, network := range networks {
ip, err := util.GetAvailableIp(network, reserverIps)
if err != nil {
return nil, err
}
if util.IsIPv6(ip){
ip = ip + "/128"
} else {
ip = ip + "/32"
}
ips = append(ips, ip)
}
client.Address = strings.Join(ips, ",")
client.Created = time.Now().UTC()
client.Updated = client.Created

err = serialize(client.Id, client)
if err != nil {
return nil, err
}

v, err := deserialize(client.Id)
if err != nil {
return nil, err
}
client = v.(*model.Client)

return client, nil
}

func ReadClient(id string) (*model.Client, error) {
v, err := deserialize(id)
if err != nil {
return nil, err
}
client := v.(*model.Client)

return client, nil
}

func ReadClientConfig(id string) ([]byte, error) {
client, err := ReadClient(id)
if err != nil {
return nil, err
}

server, err := ReadServer()
if err != nil {
return nil, err
}

configDataWg, err := util.DumpClient(client, server)
if err != nil {
return nil, err
}

return configDataWg.Bytes(), nil
}

func UpdateClient(Id string, client *model.Client) (*model.Client, error) {
v, err := deserialize(Id)
if err != nil {
return nil, err
}
current := v.(*model.Client)

if current.Id != client.Id {
return nil, errors.New("records Id mismatch")
}
// keep keys
client.PrivateKey = current.PrivateKey
client.PublicKey = current.PublicKey

client.Updated = time.Now().UTC()

err = serialize(client.Id, client)
if err != nil {
return nil, err
}

v, err = deserialize(Id)
if err != nil {
return nil, err
}
client = v.(*model.Client)

return client, nil
}

func DeleteClient(id string) error {
path := filepath.Join(os.Getenv("WG_CONF_DIR"), id)
err := os.Remove(path)
if err != nil {
return err
}

// data modified, dump new config
return generateWgConfig()
}

func ReadClients() ([]*model.Client, error) {
clients := make([]*model.Client, 0)

files, err := ioutil.ReadDir(filepath.Join(os.Getenv("WG_CONF_DIR")))
if err != nil {
return nil, err
}

for _, f := range files {
// clients file name is an uuid
_, err := uuid.FromString(f.Name())
if err == nil {
c, err := deserialize(f.Name())
if err != nil {
log.WithFields(log.Fields{
"err": err,
"path": f.Name(),
}).Error("failed to deserialize client")
} else {
clients = append(clients, c.(*model.Client))
}
}
}

sort.Slice(clients, func(i, j int) bool {
return clients[i].Created.After(clients[j].Created)
})

return clients, nil
}

/*
* Return server object, create default one
*/
func ReadServer() (*model.Server, error) {
if !util.FileExists(filepath.Join(os.Getenv("WG_CONF_DIR"), "server.json")) {
server := &model.Server{}

key, err := wgtypes.GeneratePrivateKey()
if err != nil {
return nil, err
}
server.PrivateKey = key.String()
server.PublicKey = key.PublicKey().String()

presharedKey, err := wgtypes.GenerateKey()
if err != nil {
return nil, err
}
server.PresharedKey = presharedKey.String()

server.Name = "Created with default values"
server.Endpoint = "wireguard.example.com:123"
server.ListenPort = 51820
server.Address = "fd9f:6666::10:6:6:1/112, 10.6.6.1/24"
server.Dns = "fd9f::10:0:0:2, 10.0.0.2"
server.PersistentKeepalive = 16
server.Created = time.Now().UTC()
server.Updated = server.Created

err = serialize("server.json", server)
if err != nil {
return nil, err
}
}

c, err := deserialize("server.json")
if err != nil {
return nil, err
}

return c.(*model.Server), nil
}

/*
* Update server, keep private values from existing one
*/
func UpdateServer(server *model.Server) (*model.Server, error) {
current, err := deserialize("server.json")
if err != nil {
return nil, err
}
server.PrivateKey = current.(*model.Server).PrivateKey
server.PublicKey = current.(*model.Server).PublicKey
server.PresharedKey = current.(*model.Server).PresharedKey

server.Updated = time.Now().UTC()

err = serialize("server.json", server)
if err != nil {
return nil, err
}

v, err := deserialize("server.json")
if err != nil {
return nil, err
}
server = v.(*model.Server)

return server, nil
}
/*
* Write object to disk
*/
func serialize(id string, c interface{}) error {
b, err := json.Marshal(c)
if err != nil {
return err
}

err = util.WriteFile(filepath.Join(os.Getenv("WG_CONF_DIR"), id), b)
if err != nil {
return err
}

// data modified, dump new config
return generateWgConfig()
}
/*
* Read client from disc
*/
func deserializeClient(data []byte) (*model.Client, error) {
var c *model.Client
err := json.Unmarshal(data, &c)
if err != nil {
return nil, err
}

return c, nil
}
/*
* Read server from disc
*/
func deserializeServer(data []byte) (*model.Server, error) {
var c *model.Server
err := json.Unmarshal(data, &c)
if err != nil {
return nil, err
}

return c, nil
}
func deserialize(id string) (interface{}, error) {
path := filepath.Join(os.Getenv("WG_CONF_DIR"), id)

b, err := util.ReadFile(path)
if err != nil {
return nil, err
}
if id == "server.json" {
return deserializeServer(b)
}

return deserializeClient(b)
}
/*
* Generate Wireguard interface configuration
*/
func generateWgConfig() error {
clients, err := ReadClients()
if err != nil {
return err
}

server, err := ReadServer()
if err != nil {
return err
}

configDataWg, err := util.DumpServerWg(clients, server)
if err != nil {
return err
}

err = util.WriteFile(filepath.Join(os.Getenv("WG_CONF_DIR"), os.Getenv("WG_INTERFACE_NAME")), configDataWg.Bytes())
if err != nil {
return err
}

return nil
}

2
ui/.browserslistrc Normal file
View File

@ -0,0 +1,2 @@
> 1%
last 2 versions

19
ui/README.md Normal file
View File

@ -0,0 +1,19 @@
# ui

## Project setup
```
npm install
```

### Compiles and hot-reloads for development
```
npm run serve
```

### Compiles and minifies for production
```
npm run build
```

### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

9622
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
ui/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "ui",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
"dependencies": {
"axios": "^0.19.2",
"moment": "^2.24.0",
"vue": "^2.6.10",
"vue-moment": "^4.1.0",
"vue-plugin-axios": "^1.0.14",
"vue-router": "^3.1.3",
"vuetify": "^2.1.0"
},
"devDependencies": {
"@vue/cli-plugin-router": "^4.1.0",
"@vue/cli-service": "^4.1.0",
"sass": "^1.19.0",
"sass-loader": "^8.0.0",
"vue-cli-plugin-vuetify": "^2.0.3",
"vue-template-compiler": "^2.6.10",
"vuetify-loader": "^1.3.0"
}
}

BIN
ui/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

19
ui/public/index.html Normal file
View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>ui</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
</head>
<body>
<noscript>
<strong>We're sorry but ui doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

33
ui/src/App.vue Normal file
View File

@ -0,0 +1,33 @@
<template>
<v-app id="inspire">

<v-app-bar app>
<v-toolbar-title>Wg Gen Web</v-toolbar-title>
</v-app-bar>

<v-content>
<v-container>
<router-view />
</v-container>
</v-content>

<v-footer app>
<span>Copyright <a class="pr-1 pl-1" href="http://www.wtfpl.net/" target="_blank">WTFPL</a> &copy; {{ new Date().getFullYear() }} Created with</span><v-icon class="pr-1 pl-1">mdi-heart</v-icon><span>by</span><a class="pr-1 pl-1" href="mailto:vx3r@127-0-0-1.fr">vx3r</a>
</v-footer>

</v-app>
</template>

<script>
export default {
name: 'App',

data: () => ({
//
}),

created () {
this.$vuetify.theme.dark = true
},
};
</script>

BIN
ui/src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

1
ui/src/assets/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg>

After

Width:  |  Height:  |  Size: 539 B

14
ui/src/main.js Normal file
View File

@ -0,0 +1,14 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import vuetify from './plugins/vuetify';
import './plugins/axios';
import './plugins/moment';

Vue.config.productionTip = false

new Vue({
router,
vuetify,
render: function (h) { return h(App) }
}).$mount('#app')

13
ui/src/plugins/axios.js Normal file
View File

@ -0,0 +1,13 @@
import Vue from 'vue'
import VueAxios from 'vue-plugin-axios'
import axios from 'axios'

export const myVar = 'This is my variable'

// https://www.npmjs.com/package/vue-cli-plugin-vuetify
Vue.use(VueAxios, {
axios,
config: {
baseURL: process.env.VUE_APP_API_BASE_URL || '/api/v1.0',
},
});

15
ui/src/plugins/moment.js Normal file
View File

@ -0,0 +1,15 @@
import Vue from 'vue'
import moment from 'moment';
import VueMoment from 'vue-moment'

moment.locale('es');

Vue.use(VueMoment, {
moment
});
// $moment() accessible in project

Vue.filter('formatDate', function (value) {
if (!value) return '';
return moment(String(value)).format('YYYY-MM-DD HH:mm')
});

View File

@ -0,0 +1,7 @@
import Vue from 'vue';
import Vuetify from 'vuetify/lib';

Vue.use(Vuetify);

export default new Vuetify({
});

21
ui/src/router/index.js Normal file
View File

@ -0,0 +1,21 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
{
path: '/',
name: 'home',
component: Home
}
]

const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})

export default router

434
ui/src/views/Home.vue Normal file
View File

@ -0,0 +1,434 @@
<template>
<v-content>
<v-row v-if="server">
<v-col cols="12">
<v-card dark>
<v-list-item>
<v-list-item-content>
<v-list-item-title class="headline">Server configurations</v-list-item-title>
</v-list-item-content>
</v-list-item>
<div class="d-flex flex-no-wrap justify-space-between">
<v-col cols="6">
<v-text-field
v-model="server.name"
:rules="[
v => !!v || 'Name is required',
]"
label="Friendly server name"
required
/>
<v-text-field
type="number"
v-model="server.persistentKeepalive"
label="Persistent keepalive for clients"
:rules="[
v => !!v || 'Persistent keepalive is required',
]"
required
/>
<v-text-field
v-model="server.endpoint"
label="Endpoint for clients to connect to"
:rules="[
v => !!v || 'Endpoint is required',
]"
required
/>
<v-text-field
v-model="server.address"
label="Server interface addresses"
:rules="[
v => !!v || 'Server interface address is required',
]"
required
/>
</v-col>
<v-col cols="6">
<v-text-field
v-model="server.publicKey"
label="Server public key"
disabled
/>
<v-text-field
v-model="server.presharedKey"
label="Preshared Key key"
disabled
/>
<v-text-field
v-model="server.dns"
label="DNS servers for clients"
:rules="[
v => !!v || 'DNS server is required',
]"
required
/>
<v-text-field
v-model="server.listenPort"
type="number"
:rules="[
v => !!v || 'Listen port is required',
]"
label="Server listen port"
required
/>
</v-col>
</div>

<v-card-actions>
<v-spacer/>
<v-btn
color="warning"
@click="updateServer"
>
Update server configuration
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
<v-divider dark/>
<v-row>
<v-col cols="12">
<v-card dark>
<v-list-item>
<v-list-item-content>
<v-list-item-title class="headline">Clients</v-list-item-title>
</v-list-item-content>
<v-btn
color="success"
@click.stop="dialogAddClient = true"
>
Add new client
</v-btn>
</v-list-item>
<v-row>
<v-col
v-for="(client, i) in clients"
:key="i"
cols="6"
>
<v-card
color="#1F7087"
class="mx-auto"
raised
shaped
>
<v-list-item>
<v-list-item-content>
<v-list-item-title class="headline">{{ client.name }}</v-list-item-title>
<v-list-item-subtitle>{{ client.email }}</v-list-item-subtitle>
<v-list-item-subtitle>Created: {{ client.created | formatDate }}</v-list-item-subtitle>
<v-list-item-subtitle>Updated: {{ client.updated | formatDate }}</v-list-item-subtitle>
</v-list-item-content>

<v-list-item-avatar
tile
size="150"
>
<v-img :src="getUrlToConfig(client.id, true)"/>
</v-list-item-avatar>
</v-list-item>

<v-card-text class="text--primary">
<v-chip
v-for="(ip, i) in client.address.split(',')"
:key="i"
color="indigo"
text-color="white"
>
<v-icon left>mdi-ip-network</v-icon>
{{ ip }}
</v-chip>
</v-card-text>
<v-card-actions>
<v-btn
text
:href="getUrlToConfig(client.id, false)"
>
Download configuration
<v-icon right dark>mdi-cloud-download</v-icon>
</v-btn>
<v-btn
text
@click.stop="dialogEditClient = true; clientToEdit = client"
>
Edit
<v-icon right dark>mdi-square-edit-outline</v-icon>
</v-btn>
<v-btn
text
@click="deleteClient(client.id)"
>
Delete
<v-icon right dark>mdi-trash-can-outline</v-icon>
</v-btn>
<v-spacer/>
<v-tooltip right>
<template v-slot:activator="{ on }">
<v-switch
dark
v-on="on"
color="success"
v-model="client.enable"
/>
</template>
<span>Enable or disable this client</span>
</v-tooltip>

</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-card>
</v-col>
</v-row>
<v-dialog
v-model="dialogAddClient"
max-width="550"
>
<v-card>
<v-card-title class="headline">Add new client</v-card-title>
<v-card-text>
<v-row>
<v-col
cols="12"
>
<v-form
ref="form"
v-model="valid"
>
<v-text-field
v-model="client.name"
label="Client friendly name"
:rules="[
v => !!v || 'Client name is required',
]"
required
/>
<v-text-field
v-model="client.email"
label="Client email"
:rules="[
v => !!v || 'E-mail is required',
v => /.+@.+\..+/.test(v) || 'E-mail must be valid',
]"
required
/>
<v-select
v-model="clientAddress"
:items="serverAddress"
label="Client IP will be chosen from these networks"
:rules="[
v => !!v || 'Network is required',
]"
multiple
chips
persistent-hint
required
/>
<v-switch
v-model="client.enable"
color="red"
inset
:label="client.enable ? 'Enable client after creation': 'Disable client after creation'"
/>
</v-form>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer/>
<v-btn
:disabled="!valid"
color="success"
@click="addClient()"
>
Submit
</v-btn>
<v-btn
color="primary"
@click="dialogAddClient = false"
>
Cancel
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog
v-if="clientToEdit"
v-model="dialogEditClient"
max-width="550"
>
<v-card>
<v-card-title class="headline">Edit client</v-card-title>
<v-card-text>
<v-row>
<v-col
cols="12"
>
<v-form
ref="form"
v-model="valid"
>
<v-text-field
v-model="clientToEdit.name"
label="Client friendly name"
:rules="[
v => !!v || 'Client name is required',
]"
required
/>
<v-text-field
v-model="clientToEdit.email"
label="Client email"
:rules="[
v => !!v || 'E-mail is required',
v => /.+@.+\..+/.test(v) || 'E-mail must be valid',
]"
required
/>
</v-form>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer/>
<v-btn
:disabled="!valid"
color="success"
@click="updateClient()"
>
Submit
</v-btn>
<v-btn
color="primary"
@click="dialogEditClient = false"
>
Cancel
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-snackbar
v-model="notification.show"
:right="true"
:top="true"
:color="notification.color"
>
{{ notification.text }}
<v-btn
dark
text
@click="notification.show = false"
>
Close
</v-btn>
</v-snackbar>
</v-content>
</template>

<script>
export default {
name: 'home',
mounted () {
this.getData()
},
data: () => ({
notification: {
show: false,
color: '',
text: '',
},
valid: true,
checkbox: false,
server: null,
clients: [],
ipDns: "",
ipAddress: "",
clientAddress: [],
serverAddress: [],
dialogAddClient: false,
dialogEditClient: false,
clientToEdit: null,
client: {
name: "",
email: "",
enable: true,
allowedIPs: "0.0.0.0/0,::/0",
address: "",
}
}),

methods: {
getData() {
this.$get('/server').then((res) => {
this.server = res;
this.clientAddress = this.serverAddress = this.server.address.split(',')
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});

this.$get('/client').then((res) => {
this.clients = res
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
},
updateServer () {
this.$patch('/server', this.server).then((res) => {
this.notify('success', "Server successfully updated");
this.getData()
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
},
addClient () {
this.dialogAddClient = false;
this.client.address = this.clientAddress.join(',');
this.$post('/client', this.client).then((res) => {
this.notify('success', "Client successfully added");
this.getData()
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
},
deleteClient(id) {
if(confirm("Do you really want to delete?")){
this.$delete(`/client/${id}`).then((res) => {
this.notify('success', "Client successfully deleted");
this.getData()
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
}
},
getUrlToConfig(id, qrcode){
let base = "/api/v1.0";
if (process.env.NODE_ENV === "development"){
base = process.env.VUE_APP_API_BASE_URL
}
if (qrcode){
return `${base}/client/${id}/config?qrcode=true`
} else {
return `${base}/client/${id}/config`
}
},
updateClient() {
this.dialogEditClient = false;
this.$patch(`/client/${this.clientToEdit.id}`, this.clientToEdit).then((res) => {
this.notify('success', "Client successfully updated");
this.getData()
}).catch((e) => {
this.notify('error', e.response.status + ' ' + e.response.statusText);
});
},
notify(color, msg) {
this.notification.show = true;
this.notification.color = color;
this.notification.text = msg;
},

},
}
</script>

9
ui/vue.config.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
devServer: {
port: 8081,
disableHostCheck: true,
},
"transpileDependencies": [
"vuetify"
]
};

88
util/tpl.go Normal file
View File

@ -0,0 +1,88 @@
package util

import (
"bytes"
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
"strings"
"text/template"
)

var (
clientTpl = `
[Interface]
Address = {{.Client.Address}}
PrivateKey = {{.Client.PrivateKey}}
DNS = {{.Server.Dns}}
[Peer]
PublicKey = {{.Server.PublicKey}}
PresharedKey = {{.Server.PresharedKey}}
AllowedIPs = {{.Client.AllowedIPs}}
Endpoint = {{.Server.Endpoint}}
PersistentKeepalive = {{.Server.PersistentKeepalive}}`

wgTpl = `
# {{.Server.Name}} / Updated: {{.Server.Updated}} / Created: {{.Server.Created}}
[Interface]
{{range .ServerAdresses}}
Address = {{.}}
{{end}}
ListenPort = {{.Server.ListenPort}}
PrivateKey = {{.Server.PrivateKey}}
{{$server := .Server}}
{{range .Clients}}
{{if .Enable}}
# {{.Name}} / {{.Email}} / Updated: {{.Updated}} / Created: {{.Created}}
[Peer]
PublicKey = {{.PublicKey}}
PresharedKey = {{$server.PresharedKey}}
AllowedIPs = {{.Address}}
{{end}}
{{end}}`
)

func DumpClient(client *model.Client, server *model.Server) (bytes.Buffer, error) {
var tplBuff bytes.Buffer

t, err := template.New("client").Parse(clientTpl)
if err != nil {
return tplBuff, err
}

return dump(t, struct {
Client *model.Client
Server *model.Server
}{
Client: client,
Server: server,
})
}

func DumpServerWg(clients []*model.Client, server *model.Server) (bytes.Buffer, error) {
var tplBuff bytes.Buffer

t, err := template.New("server").Parse(wgTpl)
if err != nil {
return tplBuff, err
}

return dump(t, struct {
Clients []*model.Client
Server *model.Server
ServerAdresses []string
}{
ServerAdresses: strings.Split(server.Address, ","),
Clients: clients,
Server: server,
})
}

func dump(tpl *template.Template , data interface{}) (bytes.Buffer, error) {
var tplBuff bytes.Buffer

err := tpl.Execute(&tplBuff, data)
if err != nil {
return tplBuff, err
}

return tplBuff, nil
}

93
util/util.go Normal file
View File

@ -0,0 +1,93 @@
package util

import (
"errors"
"io/ioutil"
"net"
"os"
"strings"
)

func ReadFile(path string) (bytes []byte, err error) {
bytes, err = ioutil.ReadFile(path)
if err != nil {
return nil, err
}

return bytes, nil
}

func WriteFile(path string, bytes []byte) (err error) {
err = ioutil.WriteFile(path, bytes, 0644)
if err != nil {
return err
}

return nil
}

func FileExists(name string) bool {
info, err := os.Stat(name)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}

func DirectoryExists(name string) bool {
info, err := os.Stat(name)
if os.IsNotExist(err) {
return false
}
return info.IsDir()
}

func GetAvailableIp(cidr string, reserved []string) (string, error) {
addresses, err := GetAllAddressesFromCidr(cidr)
if err != nil {
return "", err
}

for _, addresse := range addresses {
ok := true
for _, r := range reserved {
if addresse == r {
ok = false
break
}
}
if ok {
return addresse, nil
}
}

return "", errors.New("no more available address from cidr")
}

func GetAllAddressesFromCidr(cidr string) ([]string, error) {
ip, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
return nil, err
}

var ips []string
for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) {
ips = append(ips, ip.String())
}
// remove network address and broadcast address (and server ip .1)
return ips[2 : len(ips)-1], nil
}

// http://play.golang.org/p/m8TNTtygK0
func inc(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
ip[j]++
if ip[j] > 0 {
break
}
}
}

func IsIPv6(address string) bool {
return strings.Count(address, ":") >= 2
}