Compare commits

...

22 Commits

Author SHA1 Message Date
Christoph Haas
dab1e13c54 fix circle ci config 2022-11-11 19:18:10 +01:00
Christoph Haas
51fb9b4139 cleanup code warnings, update RaspberryPi readme 2022-11-11 18:17:38 +01:00
Christoph Haas
bda8c9a3d1 fix migration issue for mysql/mariadb (#128) 2022-11-11 18:07:48 +01:00
h44z
54716f7f53 Multiarch Docker Build (#104) (#129)
* Improved Makefile
* Multiarch Docker build (amd64, arm64 and armv7)
* closes #104
2022-11-11 17:10:41 +01:00
Christoph Haas
e97fb38bd5 fix issue where newly created peers expire 2022-11-08 18:02:00 +01:00
Christoph Haas
2796433973 expiry feature: automatically re-enable peers if date is in the future 2022-11-01 10:51:17 +01:00
Christoph Haas
3e2208c8f6 ensure that db index is re-created (avoids invalid DDL errors), update gorm 2022-10-29 15:24:13 +02:00
Christoph Haas
09a9af245c prepare new release 2022-10-29 14:27:56 +02:00
h44z
979cec7d83 Merge pull request #127 from h44z/feat_exp
Expiry Date for Peers
2022-10-29 14:20:36 +02:00
Christoph Haas
0f33871850 peer expiry feature: update api docs and readme 2022-10-29 13:18:32 +02:00
Christoph Haas
c43e8d7ca2 peer expiry feature: re-activate expired peers 2022-10-29 13:03:05 +02:00
Christoph Haas
4a0e773d96 peer expiry feature: expiration check 2022-10-29 11:21:04 +02:00
Christoph Haas
6f4af97024 peer expiry feature: frontend updates 2022-10-29 10:12:42 +02:00
Christoph Haas
0d5b895174 lazy load qr code (if browser supports it) 2022-10-29 10:06:58 +02:00
Christoph Haas
fe3247bdc1 peer expiry feature: database model, frontend updates 2022-10-28 23:21:37 +02:00
Christoph Haas
e4b927bc45 use go-playground/validator instead of asaskevich/govalidator (#46) 2022-10-28 21:48:44 +02:00
philippderdiedas
383fc8cb58 Merge branch 'h44z:master' into master 2022-10-28 20:42:35 +02:00
Christoph Haas
ab7f19bb55 only remove private key if a custom public key was specified (#112) 2022-10-28 18:40:06 +02:00
Philipp Harms
49c7109c61 Fix DNSStr validator 2022-10-28 18:31:20 +02:00
Fabian Schultis
352c689623 Remove as in https://github.com/h44z/wg-portal/issues/112 2022-10-28 17:54:13 +02:00
skodapilot
e6a8e2f2cf Fixed possibility to save clients without preshared key (#114) 2022-09-19 22:39:34 +02:00
dada513
12717987a6 Add config option to make everyone admin (#106) 2022-09-19 22:26:11 +02:00
36 changed files with 662 additions and 367 deletions

View File

@@ -10,7 +10,7 @@ jobs:
- run: - run:
name: Install Dependencies name: Install Dependencies
command: | command: |
make dep make build-dependencies
- save_cache: - save_cache:
key: go-mod-latest-v4-{{ checksum "go.sum" }} key: go-mod-latest-v4-{{ checksum "go.sum" }}
paths: paths:
@@ -20,31 +20,38 @@ jobs:
command: | command: |
VERSION=$CIRCLE_BRANCH VERSION=$CIRCLE_BRANCH
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build-amd64
- run: - run:
name: Install Cross-Platform Dependencies name: Install Cross-Platform Dependencies
command: | command: |
sudo apt-get update sudo apt-get update
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install gcc-arm-linux-gnueabi libc6-dev-armel-cross gcc-arm-linux-gnueabihf libc6-dev-armhf-cross gcc-aarch64-linux-gnu libc6-dev-arm64-cross sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install gcc-aarch64-linux-gnu libc6-dev-arm64-cross
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install gcc-arm-linux-gnueabi libc6-dev-armel-cross gcc-arm-linux-gnueabihf libc6-dev-armhf-cross
sudo ln -s /usr/include/asm-generic /usr/include/asm sudo ln -s /usr/include/asm-generic /usr/include/asm
- run:
name: Build ARM64
command: |
VERSION=$CIRCLE_BRANCH
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build-arm64
- run: - run:
name: Build ARM name: Build ARM
command: | command: |
VERSION=$CIRCLE_BRANCH VERSION=$CIRCLE_BRANCH
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build-cross-plat make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build-arm
- store_artifacts: - store_artifacts:
path: ~/repo/dist path: ~/repo/dist
- run: - run:
name: "Publish Release on GitHub" name: "Publish Release on GitHub"
command: | command: |
if [ ! -z "${CIRCLE_TAG}" ]; then if [ ! -z "${CIRCLE_TAG}" ]; then
go get github.com/tcnksm/ghr go install github.com/tcnksm/ghr@latest
ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -replace $CIRCLE_TAG ~/repo/dist ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -replace $CIRCLE_TAG ~/repo/dist
fi fi
working_directory: ~/repo working_directory: ~/repo
docker: docker:
- image: cimg/go:1.17 - image: cimg/go:1.19
build-116: # just to validate compatibility with minimum go version build-116: # just to validate compatibility with minimum go version
steps: steps:
- checkout - checkout
@@ -54,13 +61,13 @@ jobs:
- run: - run:
name: Install Dependencies name: Install Dependencies
command: | command: |
make dep make build-dependencies
- save_cache: - save_cache:
key: go-mod-116-v4-{{ checksum "go.sum" }} key: go-mod-116-v4-{{ checksum "go.sum" }}
paths: paths:
- "~/go/pkg/mod" - "~/go/pkg/mod"
- run: - run:
name: Build AMD64 name: Build
command: | command: |
VERSION=$CIRCLE_BRANCH VERSION=$CIRCLE_BRANCH
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
@@ -68,54 +75,6 @@ jobs:
working_directory: ~/repo116 working_directory: ~/repo116
docker: docker:
- image: cimg/go:1.16 - image: cimg/go:1.16
build-legacy:
steps:
- checkout
- restore_cache:
keys:
- go-mod-legacy-v4-{{ checksum "go.sum" }}
- run:
name: Install Dependencies
command: |
make dep
- save_cache:
key: go-mod-legacy-v4-{{ checksum "go.sum" }}
paths:
- "/go/pkg/mod"
- run:
name: Build AMD64
command: |
VERSION=$CIRCLE_BRANCH
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build
- run:
name: Install Cross-Platform Dependencies
command: |
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install gcc-arm-linux-gnueabi libc6-dev-armel-cross gcc-arm-linux-gnueabihf libc6-dev-armhf-cross gcc-aarch64-linux-gnu libc6-dev-arm64-cross
sudo ln -s /usr/include/asm-generic /usr/include/asm
- run:
name: Build ARM
command: |
VERSION=$CIRCLE_BRANCH
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build-cross-plat
- store_artifacts:
path: ~/repolegacy/dist
- run:
name: "Publish Legacy Release on GitHub"
command: |
rm ~/repolegacy/dist/wg-portal.service ~/repolegacy/dist/wg-portal.env
mv ~/repolegacy/dist/wg-portal-amd64 ~/repolegacy/dist/wg-portal-amd64-legacy
mv ~/repolegacy/dist/wg-portal-arm ~/repolegacy/dist/wg-portal-arm-legacy
mv ~/repolegacy/dist/wg-portal-arm64 ~/repolegacy/dist/wg-portal-arm64-legacy
if [ ! -z "${CIRCLE_TAG}" ]; then
go get github.com/tcnksm/ghr
ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} $CIRCLE_TAG ~/repolegacy/dist
fi
working_directory: ~/repolegacy
docker:
- image: circleci/golang:1.16-stretch
workflows: workflows:
build-and-release: build-and-release:
@@ -131,9 +90,3 @@ workflows:
filters: filters:
tags: tags:
only: /^v.*/ only: /^v.*/
- build-legacy:
requires:
- build-latest
filters:
tags:
only: /^v.*/

View File

@@ -24,7 +24,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Get Version - name: Get Version
shell: bash shell: bash
@@ -35,14 +41,14 @@ jobs:
- name: Log in to Docker Hub - name: Log in to Docker Hub
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v3 uses: docker/metadata-action@v4
with: with:
images: h44z/wg-portal images: h44z/wg-portal
flavor: | flavor: |
@@ -55,12 +61,13 @@ jobs:
type=semver,pattern={{version}} type=semver,pattern={{version}}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v2 uses: docker/build-push-action@v3
with: with:
context: . context: .
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
build-args: | build-args: |
BUILD_IDENTIFIER=${{ steps.get_version.outputs.identifier }} BUILD_IDENTIFIER=${{ steps.get_version.outputs.identifier }}
BUILD_VERSION=${{ steps.get_version.outputs.hash }} BUILD_VERSION=${{ steps.get_version.outputs.hash }}
@@ -74,7 +81,13 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Get Version - name: Get Version
shell: bash shell: bash
@@ -87,7 +100,7 @@ jobs:
# https://github.com/docker/login-action # https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }} - name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -97,7 +110,7 @@ jobs:
# https://github.com/docker/metadata-action # https://github.com/docker/metadata-action
- name: Extract Docker metadata - name: Extract Docker metadata
id: meta id: meta
uses: docker/metadata-action@v3 uses: docker/metadata-action@v4
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: | flavor: |
@@ -112,12 +125,13 @@ jobs:
# Build and push Docker image with Buildx (don't push on PR) # Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action # https://github.com/docker/build-push-action
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v2 uses: docker/build-push-action@v3
with: with:
context: . context: .
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
build-args: | build-args: |
BUILD_IDENTIFIER=${{ steps.get_version.outputs.identifier }} BUILD_IDENTIFIER=${{ steps.get_version.outputs.identifier }}
BUILD_VERSION=${{ steps.get_version.outputs.hash }} BUILD_VERSION=${{ steps.get_version.outputs.hash }}

1
.gitignore vendored
View File

@@ -28,6 +28,7 @@
out/ out/
dist/ dist/
data/ data/
docker_images/
ssh.key ssh.key
.testCoverage.txt .testCoverage.txt
wg_portal.db wg_portal.db

View File

@@ -12,6 +12,10 @@ ENV ENV_BUILD_IDENTIFIER=$BUILD_IDENTIFIER
ARG BUILD_VERSION ARG BUILD_VERSION
ENV ENV_BUILD_VERSION=$BUILD_VERSION ENV ENV_BUILD_VERSION=$BUILD_VERSION
# populated by BuildKit
ARG TARGETPLATFORM
ENV ENV_TARGETPLATFORM=$TARGETPLATFORM
RUN mkdir /build RUN mkdir /build
# Copy the source from the current directory to the Working Directory inside the container # Copy the source from the current directory to the Working Directory inside the container
@@ -20,16 +24,8 @@ ADD . /build/
# Set the Current Working Directory inside the container # Set the Current Working Directory inside the container
WORKDIR /build WORKDIR /build
# Workaround for failing travis-ci builds
RUN rm -rf ~/go; rm -rf go.sum
# Download dependencies
RUN curl -L https://git.prolicht.digital/pub/healthcheck/-/releases/v1.0.1/downloads/binaries/hc -o /build/hc; \
chmod +rx /build/hc; \
echo "Building version: $ENV_BUILD_IDENTIFIER-$ENV_BUILD_VERSION"
# Build the Go app # Build the Go app
RUN go clean -modcache; go mod tidy; make build-docker RUN echo "Building version '$ENV_BUILD_IDENTIFIER-$ENV_BUILD_VERSION' for platform $ENV_TARGETPLATFORM"; make build
######- ######-
# Here starts the main image # Here starts the main image
@@ -44,16 +40,14 @@ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /etc/passwd /etc/passwd COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group COPY --from=builder /etc/group /etc/group
# Import healthcheck binary
COPY --from=builder /build/hc /app/hc
# Copy binaries # Copy binaries
COPY --from=builder /build/dist/wgportal /app/wgportal COPY --from=builder /build/dist/wg-portal /app/wg-portal
COPY --from=builder /build/dist/hc /app/hc
# Set the Current Working Directory inside the container # Set the Current Working Directory inside the container
WORKDIR /app WORKDIR /app
# Command to run the executable # Command to run the executable
CMD [ "/app/wgportal" ] CMD [ "/app/wg-portal" ]
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 CMD [ "/app/hc", "http://localhost:11223/health" ] HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 CMD [ "/app/hc", "http://localhost:11223/health" ]

161
Makefile
View File

@@ -6,62 +6,119 @@ BUILDDIR=dist
BINARIES=$(subst cmd/,,$(wildcard cmd/*)) BINARIES=$(subst cmd/,,$(wildcard cmd/*))
IMAGE=h44z/wg-portal IMAGE=h44z/wg-portal
.PHONY: all test clean phony all: help
all: dep build .PHONY: help
help:
@echo "Usage:"
@sed -n 's/^#>//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' # user commands (#>)
@echo ""
@echo "Advanced commands:"
@sed -n 's/^#<//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' # internal commands (#<)
build: dep $(addsuffix -amd64,$(addprefix $(BUILDDIR)/,$(BINARIES))) ########################################################################################
cp scripts/wg-portal.service $(BUILDDIR) ##
cp scripts/wg-portal.env $(BUILDDIR) ## DEVELOPER / USER TARGETS
##
########################################################################################
build-cross-plat: dep build $(addsuffix -arm,$(addprefix $(BUILDDIR)/,$(BINARIES))) $(addsuffix -arm64,$(addprefix $(BUILDDIR)/,$(BINARIES))) #> codegen: Re-generate autogenerated files (like API docs)
cp scripts/wg-portal.service $(BUILDDIR) .PHONY: codegen
cp scripts/wg-portal.env $(BUILDDIR) codegen: $(SUBDIRS)
cd internal; swag init --propertyStrategy pascalcase --parseInternal --generalInfo server/api.go --output server/docs/
build-docker: dep
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(GOCMD) build -o $(BUILDDIR)/wgportal -ldflags "-w -s -linkmode external -extldflags \"-static\" -X github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}" -tags netgo cmd/wg-portal/main.go
dep:
$(GOCMD) mod download
validate: dep
$(GOCMD) fmt $(GOFILES)
$(GOCMD) vet $(GOFILES)
$(GOCMD) test -race $(GOFILES)
coverage: dep
$(GOCMD) fmt $(GOFILES)
$(GOCMD) test $(GOFILES) -v -coverprofile .testCoverage.txt
$(GOCMD) tool cover -func=.testCoverage.txt # use total:\s+\(statements\)\s+(\d+.\d+\%) as Gitlab CI regextotal:\s+\(statements\)\s+(\d+.\d+\%)
coverage-html: coverage
$(GOCMD) tool cover -html=.testCoverage.txt
test: dep
$(GOCMD) test $(MODULENAME)/... -v -count=1
clean:
$(GOCMD) clean $(GOFILES)
rm -rf .testCoverage.txt
rm -rf $(BUILDDIR)
docker-build:
docker build -t $(IMAGE) .
docker-push:
docker push $(IMAGE)
api-docs:
cd internal/server; swag init --propertyStrategy pascalcase --parseDependency --parseInternal --generalInfo api.go
$(GOCMD) fmt internal/server/docs/docs.go $(GOCMD) fmt internal/server/docs/docs.go
$(BUILDDIR)/%-amd64: cmd/%/main.go dep phony #> update: Update all dependencies
GOOS=linux GOARCH=amd64 $(GOCMD) build -ldflags "-X github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}" -o $@ $< .PHONY: update
update:
@ $(GOCMD) get -u ./...
@ $(GOCMD) mod tidy
# On arch-linux install aarch64-linux-gnu-gcc to crosscompile for arm64 #> format: Re-format the code
$(BUILDDIR)/%-arm64: cmd/%/main.go dep phony .PHONY: format
CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 $(GOCMD) build -ldflags "-linkmode external -extldflags \"-static\" -X github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}" -o $@ $< format:
@echo "Formatting code..."
@ $(GOCMD) fmt $(GOFILES)
# 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-gnueabi-gcc GOOS=linux GOARCH=arm GOARM=7 $(GOCMD) build -ldflags "-linkmode external -extldflags \"-static\" -X github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}" -o $@ $< ## TESTING / CODE QUALITY TARGETS
##
########################################################################################
#> test: Run all kinds of tests, except for integration tests
.PHONY: test
test: test-vet test-race
#< test-vet: Static code analysis
.PHONY: test-vet
test-vet: build-dependencies
@$(GOCMD) vet $(GOFILES)
#< test-race: Race condition test
.PHONY: test-race
test-race: build-dependencies
@$(GOCMD) test -race -short $(GOFILES)
########################################################################################
##
## CI TARGETS
##
########################################################################################
#< clean: Delete all generated executables and test files
.PHONY: clean
clean:
@rm -rf $(BUILDDIR)
#< build: Build all executables (architecture depends on build system)
.PHONY: build
build: build-dependencies
CGO_ENABLED=1 $(GOCMD) build -o $(BUILDDIR)/wg-portal \
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
cmd/wg-portal/main.go
CGO_ENABLED=0 $(GOCMD) build -o $(BUILDDIR)/hc \
-ldflags "-w -s -extldflags \"-static\"" \
cmd/hc/main.go
#< build: Build all executables for AMD64
.PHONY: build-amd64
build-amd64: build-dependencies
CGO_ENABLED=1 $(GOCMD) build -o $(BUILDDIR)/wg-portal-amd64 \
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
cmd/wg-portal/main.go
CGO_ENABLED=0 $(GOCMD) build -o $(BUILDDIR)/hc-amd64 \
-ldflags "-w -s -extldflags \"-static\"" \
cmd/hc/main.go
#< build-arm64: Build all executables for ARM64
.PHONY: build-arm64
build-arm64: build-dependencies
CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 $(GOCMD) build -o $(BUILDDIR)/wg-portal-arm64 \
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
cmd/wg-portal/main.go
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GOCMD) build -o $(BUILDDIR)/hc-arm64 \
-ldflags "-w -s -extldflags \"-static\"" \
cmd/hc/main.go
#< build-arm: Build all executables for ARM32
.PHONY: build-arm
build-arm: build-dependencies
CGO_ENABLED=1 CC=arm-linux-gnueabi-gcc GOOS=linux GOARCH=arm GOARM=7 $(GOCMD) build -o $(BUILDDIR)/wg-portal-arm \
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
cmd/wg-portal/main.go
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 $(GOCMD) build -o $(BUILDDIR)/hc-arm \
-ldflags "-w -s -extldflags \"-static\"" \
cmd/hc/main.go
#< build-dependencies: Generate the output directory for compiled executables and download dependencies
.PHONY: build-dependencies
build-dependencies:
@$(GOCMD) mod download -x
@mkdir -p $(BUILDDIR)
cp scripts/wg-portal.service $(BUILDDIR)
cp scripts/wg-portal.env $(BUILDDIR)

View File

@@ -4,16 +4,21 @@ This readme only contains a detailed explanation of how to set up the WireGuard
## Setup ## 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, You can either download prebuild binaries from the [release page](https://github.com/h44z/wg-portal/releases) or use Docker images for ARM.
use the following instructions: If you want to build the binary yourself, use the following building instructions.
### Building ### Building
This section describes how to build the WireGuard Portal code. This section describes how to build the WireGuard Portal code.
To compile the final binary, use the Makefile provided in the repository. To compile the final binary, use the Makefile provided in the repository.
As WireGuard Portal is written in Go, **golang >= 1.16** must be installed prior to building. As WireGuard Portal is written in Go, **golang >= 1.16** must be installed prior to building.
If you want to cross compile ARM binaries from AMD64 systems, install *arm-linux-gnueabi-gcc* (armv7) or *aarch64-linux-gnu-gcc* (arm64).
``` ```
make build-cross-plat # for 64 bit OS
make build-arm64
# for 32 bit OS
make build-arm
``` ```
The compiled binary and all necessary assets will be located in the dist folder. The compiled binary and all necessary assets will be located in the dist folder.

View File

@@ -31,6 +31,7 @@ It also supports LDAP (Active Directory or OpenLDAP) as authentication provider.
* Can be used with existing WireGuard setups * Can be used with existing WireGuard setups
* Support for multiple WireGuard interfaces * Support for multiple WireGuard interfaces
* REST API for management and client deployment * REST API for management and client deployment
* Peer Expiry Feature
![Screenshot](screenshot.png) ![Screenshot](screenshot.png)
@@ -84,21 +85,43 @@ 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 back up your files from ```/etc/wireguard```. If needed, please make sure to back up your files from ```/etc/wireguard```.
For a full list of configuration options take a look at the source file [internal/server/configuration.go](internal/server/configuration.go#L56). For a full list of configuration options take a look at the source file [internal/server/configuration.go](internal/server/configuration.go#L58).
### Standalone ### Standalone
For a standalone application, use the Makefile provided in the repository to build the application. Go version 1.16 or higher has to be installed to build WireGuard Portal. For a standalone application, use the Makefile provided in the repository to build the application. Go version 1.16 or higher has to be installed to build WireGuard Portal.
``` ```shell
# show all possible make commands
make make
# To build for arm architecture as well use: # build wg-portal for current system architecture
make build-cross-plat make build
``` ```
The compiled binary will be located in the dist folder. The compiled binary 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). A detailed description for using this software with a raspberry pi can be found in the [README-RASPBERRYPI.md](README-RASPBERRYPI.md).
To build the Docker image, Docker (> 20.x) with buildx is required. If you want to build cross-platform images, you need to install qemu.
On arch linux for example install: `docker-buildx qemu-user-static qemu-user-static-binfmt`.
Once the Docker setup is completed, create a new buildx builder:
```shell
docker buildx create --name wgportalbuilder --platform linux/arm/v7,linux/arm64,linux/amd64
docker buildx use wgportalbuilder
docker buildx inspect --bootstrap
```
Now you can compile the Docker image:
```shell
# multi platform build, can only be exported to tar archives
docker buildx build --platform linux/arm/v7,linux/arm64,linux/amd64 --output type=local,dest=docker_images \
--build-arg BUILD_IDENTIFIER=dev --build-arg BUILD_VERSION=0.1 -t h44z/wg-portal .
# image for current platform only (same as docker build)
docker buildx build --load \
--build-arg BUILD_IDENTIFIER=dev --build-arg BUILD_VERSION=0.1 -t h44z/wg-portal .
```
## Configuration ## Configuration
You can configure WireGuard Portal using either environment variables or a yaml configuration file. You can configure WireGuard Portal using either environment variables or a yaml configuration file.
The filepath of the yaml configuration file defaults to **config.yml** in the working directory of the executable. The filepath of the yaml configuration file defaults to **config.yml** in the working directory of the executable.
@@ -109,7 +132,7 @@ For example: `CONFIG_FILE=/home/test/config.yml ./wg-portal-amd64`.
The following configuration options are available: The following configuration options are available:
| environment | yaml | yaml_parent | default_value | description | | environment | yaml | yaml_parent | default_value | description |
|----------------------------|-------------------------|-------------|-------------------------------------------------|-------------------------------------------------------------------------------------------| |----------------------------|-------------------------|-------------|-----------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
| LISTENING_ADDRESS | listeningAddress | core | :8123 | The address on which the web server is listening. Optional IP address and port, e.g.: 127.0.0.1:8080. | | LISTENING_ADDRESS | listeningAddress | core | :8123 | The address on which the web server is listening. Optional IP address and port, e.g.: 127.0.0.1:8080. |
| EXTERNAL_URL | externalUrl | core | http://localhost:8123 | The external URL where the web server is reachable. This link is used in emails that are created by the WireGuard Portal. | | EXTERNAL_URL | externalUrl | core | http://localhost:8123 | The external URL where the web server is reachable. This link is used in emails that are created by the WireGuard Portal. |
| WEBSITE_TITLE | title | core | WireGuard VPN | The website title. | | WEBSITE_TITLE | title | core | WireGuard VPN | The website title. |
@@ -124,6 +147,8 @@ The following configuration options are available:
| WG_EXPORTER_FRIENDLY_NAMES | wgExporterFriendlyNames | core | false | Enable integration with [prometheus_wireguard_exporter friendly name](https://github.com/MindFlavor/prometheus_wireguard_exporter#friendly-tags). | | WG_EXPORTER_FRIENDLY_NAMES | wgExporterFriendlyNames | core | false | Enable integration with [prometheus_wireguard_exporter friendly name](https://github.com/MindFlavor/prometheus_wireguard_exporter#friendly-tags). |
| LDAP_ENABLED | ldapEnabled | core | false | Enable or disable the LDAP backend. | | LDAP_ENABLED | ldapEnabled | core | false | Enable or disable the LDAP backend. |
| SESSION_SECRET | sessionSecret | core | secret | Use a custom secret to encrypt session data. | | SESSION_SECRET | sessionSecret | core | secret | Use a custom secret to encrypt session data. |
| BACKGROUND_TASK_INTERVAL | backgroundTaskInterval | core | 900 | The interval (in seconds) for the background tasks (like peer expiry check). |
| EXPIRY_REENABLE | expiryReEnable | core | false | Reactivate expired peers if the expiration date is in the future. |
| DATABASE_TYPE | typ | database | sqlite | Either mysql or sqlite. | | DATABASE_TYPE | typ | database | sqlite | Either mysql or sqlite. |
| DATABASE_HOST | host | database | | The mysql server address. | | DATABASE_HOST | host | database | | The mysql server address. |
| DATABASE_PORT | port | database | | The mysql server port. | | DATABASE_PORT | port | database | | The mysql server port. |
@@ -142,6 +167,7 @@ The following configuration options are available:
| WG_DEFAULT_DEVICE | defaultDevice | wg | wg0 | This device is used for auto-created peers (if CREATE_DEFAULT_PEER is enabled). | | WG_DEFAULT_DEVICE | defaultDevice | wg | wg0 | This device is used for auto-created peers (if CREATE_DEFAULT_PEER is enabled). |
| WG_CONFIG_PATH | configDirectory | wg | /etc/wireguard | If set, interface configuration updates will be written to this path, filename: <devicename>.conf. | | WG_CONFIG_PATH | configDirectory | wg | /etc/wireguard | If set, interface configuration updates will be written to this path, filename: <devicename>.conf. |
| MANAGE_IPS | manageIPAddresses | wg | true | Handle IP address setup of interface, only available on linux. | | MANAGE_IPS | manageIPAddresses | wg | true | Handle IP address setup of interface, only available on linux. |
| USER_MANAGE_PEERS | userManagePeers | wg | false | Logged in user can create or update peers (partially). |
| LDAP_URL | url | ldap | ldap://srv-ad01.company.local:389 | The LDAP server url. | | LDAP_URL | url | ldap | ldap://srv-ad01.company.local:389 | The LDAP server url. |
| LDAP_STARTTLS | startTLS | ldap | true | Use STARTTLS. | | LDAP_STARTTLS | startTLS | ldap | true | Use STARTTLS. |
| LDAP_CERT_VALIDATION | certcheck | ldap | false | Validate the LDAP server certificate. | | LDAP_CERT_VALIDATION | certcheck | ldap | false | Validate the LDAP server certificate. |

View File

@@ -73,6 +73,10 @@ pre{background:#f7f7f9}iframe{overflow:hidden;border:none}@media (min-width: 768
color: #d03131; color: #d03131;
} }
.expiring-peer {
color: #d09d12;
}
.tokenfield .token { .tokenfield .token {
border-radius: 0px; border-radius: 0px;
border: 1px solid #1a1a1a; border: 1px solid #1a1a1a;
@@ -106,3 +110,9 @@ a.advanced-settings.collapsed:before {
.text-blue { .text-blue {
color: #0057bb; color: #0057bb;
} }
@media (min-width: 992px) {
.pull-right-lg {
float: right;
}
}

View File

@@ -45,7 +45,7 @@
<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_PresharedKey">Preshared Key</label> <label for="server_PresharedKey">Preshared Key</label>
<input type="text" name="presharedkey" class="form-control" id="server_PresharedKey" value="{{.Peer.PresharedKey}}" required> <input type="text" name="presharedkey" class="form-control" id="server_PresharedKey" value="{{.Peer.PresharedKey}}">
</div> </div>
</div> </div>
{{else}} {{else}}
@@ -106,7 +106,7 @@
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group col-md-12"> <div class="form-group col-md-6">
<div class="custom-control custom-switch"> <div class="custom-control custom-switch">
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="server_Disabled" {{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="server_Disabled"> <label class="custom-control-label" for="server_Disabled">
@@ -120,6 +120,10 @@
</label> </label>
</div> </div>
</div> </div>
<div class="form-group col-md-6">
<label for="expires_at">Expires At</label>
<input type="date" name="expires_at" pattern="\d{4}-\d{2}-\d{2}" class="form-control" id="expires_at" placeholder="" value="{{formatDate .Peer.ExpiresAt}}" min="2022-01-01">
</div>
</div> </div>
@@ -185,7 +189,7 @@
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group col-md-12"> <div class="form-group col-md-6">
<div class="custom-control custom-switch"> <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}}> <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"> <label class="custom-control-label" for="client_Disabled">
@@ -193,6 +197,10 @@
</label> </label>
</div> </div>
</div> </div>
<div class="form-group col-md-6">
<label for="expires_at">Expires At</label>
<input type="date" name="expires_at" pattern="\d{4}-\d{2}-\d{2}" class="form-control" id="expires_at" placeholder="" value="{{formatDate .Peer.ExpiresAt}}" min="2022-01-01">
</div>
</div> </div>

View File

@@ -170,7 +170,7 @@
<!-- online check --> <!-- online check -->
<span title="Online status" class="online-status" id="online-{{$p.UID}}" data-pkey="{{$p.PublicKey}}"><i class="fas fa-unlink"></i></span> <span title="Online status" class="online-status" id="online-{{$p.UID}}" data-pkey="{{$p.PublicKey}}"><i class="fas fa-unlink"></i></span>
</th> </th>
<td>{{$p.Identifier}}</td> <td>{{$p.Identifier}}{{if $p.WillExpire}} <i class="fas fa-hourglass-end expiring-peer" data-toggle="tooltip" data-placement="right" title="" data-original-title="Expires at: {{formatDate $p.ExpiresAt}}"></i>{{end}}</td>
<td>{{$p.PublicKey}}</td> <td>{{$p.PublicKey}}</td>
{{if eq $.Device.Type "server"}} {{if eq $.Device.Type "server"}}
<td>{{$p.Email}}</td> <td>{{$p.Email}}</td>
@@ -239,12 +239,18 @@
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{{if eq $.Device.Type "server"}} {{if eq $.Device.Type "server"}}
<img class="list-image-large" src="/user/qrcode?pkey={{$p.PublicKey}}"/> <img class="list-image-large" loading="lazy" alt="Configuration QR Code" src="/user/qrcode?pkey={{$p.PublicKey}}"/>
{{end}} {{end}}
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
{{if $p.DeactivatedAt}}
<div class="pull-right-lg mt-lg-5 disabled-peer">Peer is disabled! <i class="fas fa-comment-dots" data-toggle="tooltip" data-placement="left" title="" data-original-title="Reason: {{$p.DeactivatedReason}}"></i></div>
{{end}}
{{if $p.WillExpire}}
<div class="pull-right-lg mt-lg-5 expiring-peer"><i class="fas fa-exclamation-triangle"></i> Peer will expire on {{ formatDate $p.ExpiresAt}}</div>
{{end}}
{{if eq $.Device.Type "server"}} {{if eq $.Device.Type "server"}}
<div class="float-right mt-5"> <div class="pull-right-lg mt-lg-5 mt-md-3">
<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>

View File

@@ -49,7 +49,7 @@
<!-- online check --> <!-- online check -->
<span class="online-status" id="online-{{$p.UID}}" data-pkey="{{$p.PublicKey}}"><i class="fas fa-unlink"></i></span> <span class="online-status" id="online-{{$p.UID}}" data-pkey="{{$p.PublicKey}}"><i class="fas fa-unlink"></i></span>
</th> </th>
<td>{{$p.Identifier}}</td> <td>{{$p.Identifier}}{{if $p.WillExpire}} <i class="fas fa-hourglass-end expiring-peer" data-toggle="tooltip" data-placement="right" title="" data-original-title="Expires at: {{formatDate $p.ExpiresAt}}"></i>{{end}}</td>
<td>{{$p.PublicKey}}</td> <td>{{$p.PublicKey}}</td>
<td>{{$p.Email}}</td> <td>{{$p.Email}}</td>
<td>{{$p.IPsStr}}</td> <td>{{$p.IPsStr}}</td>
@@ -102,7 +102,13 @@
<img class="list-image-large" src="/user/qrcode?pkey={{$p.PublicKey}}"/> <img class="list-image-large" src="/user/qrcode?pkey={{$p.PublicKey}}"/>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="float-right mt-5"> {{if $p.DeactivatedAt}}
<div class="pull-right-lg mt-lg-5 disabled-peer">Peer is disabled! <i class="fas fa-comment-dots" data-toggle="tooltip" data-placement="left" title="" data-original-title="Reason: {{$p.DeactivatedReason}}"></i></div>
{{end}}
{{if $p.WillExpire}}
<div class="pull-right-lg mt-lg-5 expiring-peer"><i class="fas fa-exclamation-triangle"></i> Profile expires on {{ formatDate $p.ExpiresAt}}</div>
{{end}}
<div class="pull-right-lg mt-lg-5 mt-md-3">
<a href="/user/download?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Download configuration">Download</a> <a href="/user/download?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Download configuration">Download</a>
<a href="/user/email?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Send configuration via Email">Email</a> <a href="/user/email?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Send configuration via Email">Email</a>
</div> </div>

35
cmd/hc/main.go Normal file
View File

@@ -0,0 +1,35 @@
// source taken from https://git.prolicht.digital/golib/healthcheck/-/blob/master/cmd/hc/main.go
package main
import (
"net/http"
"os"
"time"
)
// main checks the given URL, if the response is not 200, it will return with exit code 1
// on success, exit code 0 will be returned
func main() {
os.Exit(checkWebEndpointFromArgs())
}
func checkWebEndpointFromArgs() int {
if len(os.Args) < 2 {
return 1
}
if status := checkWebEndpoint(os.Args[1]); !status {
return 1
}
return 0
}
func checkWebEndpoint(url string) bool {
client := &http.Client{
Timeout: time.Second * 2,
}
if resp, err := client.Get(url); err != nil || resp.StatusCode < 200 || resp.StatusCode > 299 {
return false
}
return true
}

View File

@@ -2,13 +2,14 @@ package main
import ( import (
"context" "context"
"io/ioutil" "io"
"os" "os"
"os/signal" "os/signal"
"runtime"
"syscall" "syscall"
"time" "time"
"git.prolicht.digital/pub/healthcheck" "git.prolicht.digital/golib/healthcheck"
"github.com/h44z/wg-portal/internal/server" "github.com/h44z/wg-portal/internal/server"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@@ -19,6 +20,7 @@ func main() {
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
logrus.Infof("sysinfo: os=%s, arch=%s", runtime.GOOS, runtime.GOARCH)
logrus.Infof("starting WireGuard Portal Server [%s]...", server.Version) logrus.Infof("starting WireGuard Portal Server [%s]...", server.Version)
// Context for clean shutdown // Context for clean shutdown
@@ -26,7 +28,7 @@ func main() {
defer cancel() defer cancel()
// start health check service on port 11223 // start health check service on port 11223
healthcheck.New(healthcheck.WithContext(ctx)).Start() healthcheck.New(healthcheck.ListenOn(":11223")).StartWithContext(ctx)
service := server.Server{} service := server.Server{}
if err := service.Setup(ctx); err != nil { if err := service.Setup(ctx); err != nil {
@@ -72,7 +74,7 @@ func setupLogger(logger *logrus.Logger) error {
switch level { switch level {
case "off": case "off":
logger.SetOutput(ioutil.Discard) logger.SetOutput(io.Discard)
case "info": case "info":
logger.SetLevel(logrus.InfoLevel) logger.SetLevel(logrus.InfoLevel)
case "debug": case "debug":

View File

@@ -1,7 +1,7 @@
version: '3.6' version: '3.6'
services: services:
wg-portal: wg-portal:
image: h44z/wg-portal:1.0.6 image: h44z/wg-portal:1.0.16
container_name: wg-portal container_name: wg-portal
restart: unless-stopped restart: unless-stopped
logging: logging:

8
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/h44z/wg-portal
go 1.16 go 1.16
require ( require (
git.prolicht.digital/pub/healthcheck v1.0.1 git.prolicht.digital/golib/healthcheck v1.1.1
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 // indirect github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 // indirect
github.com/evanphx/json-patch v0.5.2 github.com/evanphx/json-patch v0.5.2
github.com/gin-contrib/sessions v0.0.5 github.com/gin-contrib/sessions v0.0.5
@@ -35,7 +35,7 @@ require (
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220504211119-3d4a969bb56b golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220504211119-3d4a969bb56b
google.golang.org/protobuf v1.28.0 // indirect google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v3 v3.0.0 gopkg.in/yaml.v3 v3.0.0
gorm.io/driver/mysql v1.3.3 gorm.io/driver/mysql v1.4.3
gorm.io/driver/sqlite v1.3.2 gorm.io/driver/sqlite v1.4.3
gorm.io/gorm v1.23.5 gorm.io/gorm v1.24.0
) )

21
go.sum
View File

@@ -1,5 +1,5 @@
git.prolicht.digital/pub/healthcheck v1.0.1 h1:cdNgcSyQL9oveFBC9V+XE4OVbfMEwqPqGdShH79sZ98= git.prolicht.digital/golib/healthcheck v1.1.1 h1:bdx0MuGqAq0PCooPpiuPXsr4/Ok+yfJwq8P9ITq2eLI=
git.prolicht.digital/pub/healthcheck v1.0.1/go.mod h1:5CVsGrijfedtLaYv3KJkfvM0nmzpgndC9MgBjC1tom4= git.prolicht.digital/golib/healthcheck v1.1.1/go.mod h1:wEqVrqHJ8NsSx5qlFGUlw74wJ/wDSKaA34QoyvsEkdc=
github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e h1:ZU22z/2YRFLyf/P4ZwUYSdNCWsMEI0VeyrFoI2rAhJQ= github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e h1:ZU22z/2YRFLyf/P4ZwUYSdNCWsMEI0VeyrFoI2rAhJQ=
github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -206,7 +206,7 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mdlayher/genetlink v1.2.0 h1:4yrIkRV5Wfk1WfpWTcoOlGmsWgQj3OtQN9ZsbrE+XtU= github.com/mdlayher/genetlink v1.2.0 h1:4yrIkRV5Wfk1WfpWTcoOlGmsWgQj3OtQN9ZsbrE+XtU=
@@ -453,16 +453,15 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.0.4/go.mod h1:MEgp8tk2n60cSBCq5iTcPDw3ns8Gs+zOva9EUhkknTs= gorm.io/driver/mysql v1.0.4/go.mod h1:MEgp8tk2n60cSBCq5iTcPDw3ns8Gs+zOva9EUhkknTs=
gorm.io/driver/mysql v1.3.3 h1:jXG9ANrwBc4+bMvBcSl8zCfPBaVoPyBEBshA8dA93X8= gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k=
gorm.io/driver/mysql v1.3.3/go.mod h1:ChK6AHbHgDCFZyJp0F+BmVGb06PSIoh9uVYKAlRbb2U= gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg=
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
gorm.io/driver/sqlite v1.3.2 h1:nWTy4cE52K6nnMhv23wLmur9Y3qWbZvOBz+V4PrGAxg= gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
gorm.io/driver/sqlite v1.3.2/go.mod h1:B+8GyC9K7VgzJAcrcXMRPdnMcck+8FgJynEehEPM16U= gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74=
gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM= gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=

View File

@@ -2,7 +2,7 @@ package ldap
import ( import (
"crypto/tls" "crypto/tls"
"io/ioutil" "os"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -48,8 +48,8 @@ func (Provider) GetPriority() int {
return 1 // LDAP password provider return 1 // LDAP password provider
} }
func (provider Provider) SetupRoutes(routes *gin.RouterGroup) { func (provider Provider) SetupRoutes(_ *gin.RouterGroup) {
// nothing todo here // nothing here
} }
func (provider Provider) Login(ctx *authentication.AuthContext) (string, error) { func (provider Provider) Login(ctx *authentication.AuthContext) (string, error) {
@@ -97,8 +97,8 @@ func (provider Provider) Login(ctx *authentication.AuthContext) (string, error)
return sr.Entries[0].GetAttributeValue(provider.config.EmailAttribute), nil return sr.Entries[0].GetAttributeValue(provider.config.EmailAttribute), nil
} }
func (provider Provider) Logout(context *authentication.AuthContext) error { func (provider Provider) Logout(_ *authentication.AuthContext) error {
return nil // nothing todo here return nil // nothing here
} }
func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authentication.User, error) { func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authentication.User, error) {
@@ -159,23 +159,23 @@ func (provider Provider) open() (*ldap.Conn, error) {
if provider.config.LdapCertConn { if provider.config.LdapCertConn {
cert_plain, err := ioutil.ReadFile(provider.config.LdapTlsCert) certPlain, err := os.ReadFile(provider.config.LdapTlsCert)
if err != nil { if err != nil {
return nil, errors.WithMessage(err, "failed to load the certificate") return nil, errors.WithMessage(err, "failed to load the certificate")
} }
key, err := ioutil.ReadFile(provider.config.LdapTlsKey) key, err := os.ReadFile(provider.config.LdapTlsKey)
if err != nil { if err != nil {
return nil, errors.WithMessage(err, "failed to load the key") return nil, errors.WithMessage(err, "failed to load the key")
} }
cert_x509, err := tls.X509KeyPair(cert_plain, key) certX509, err := tls.X509KeyPair(certPlain, key)
if err != nil { if err != nil {
return nil, errors.WithMessage(err, "failed X509") return nil, errors.WithMessage(err, "failed X509")
} }
tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert_x509}} tlsConfig = &tls.Config{Certificates: []tls.Certificate{certX509}}
} else { } else {

View File

@@ -50,8 +50,8 @@ func (Provider) GetPriority() int {
return 0 // DB password provider = highest prio return 0 // DB password provider = highest prio
} }
func (provider Provider) SetupRoutes(routes *gin.RouterGroup) { func (provider Provider) SetupRoutes(_ *gin.RouterGroup) {
// nothing todo here // nothing here
} }
func (provider Provider) Login(ctx *authentication.AuthContext) (string, error) { func (provider Provider) Login(ctx *authentication.AuthContext) (string, error) {
@@ -79,8 +79,8 @@ func (provider Provider) Login(ctx *authentication.AuthContext) (string, error)
return user.Email, nil return user.Email, nil
} }
func (provider Provider) Logout(context *authentication.AuthContext) error { func (provider Provider) Logout(_ *authentication.AuthContext) error {
return nil // nothing todo here return nil // nothing here
} }
func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authentication.User, error) { func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authentication.User, error) {

View File

@@ -5,6 +5,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"strings"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -36,6 +37,40 @@ func init() {
return nil return nil
}, },
}) })
migrations = append(migrations, Migration{
version: "1.0.9",
migrateFn: func(db *gorm.DB) error {
if db.Dialector.Name() != (sqlite.Dialector{}).Name() {
logrus.Infof("upgraded database format to version 1.0.9")
return nil // only perform migration for sqlite
}
type sqlIndex struct {
Name string `gorm:"column:name"`
Table string `gorm:"column:tbl_name"`
}
var indices []sqlIndex
if err := db.Raw("SELECT name, tbl_name FROM sqlite_master WHERE type == 'index'").Scan(&indices).Error; err != nil {
return errors.Wrap(err, "failed to fetch indices")
}
for _, index := range indices {
if index.Table != "devices" && index.Table != "peers" && index.Table != "users" {
continue
}
if strings.Contains(index.Name, "autoindex") {
continue
}
if err := db.Exec("DROP INDEX " + index.Name).Error; err != nil {
return errors.Wrap(err, "failed to drop index "+index.Name)
}
}
logrus.Infof("upgraded database format to version 1.0.9")
return nil
},
})
} }
type SupportedDatabase string type SupportedDatabase string

View File

@@ -3,7 +3,6 @@ package common
import ( import (
"crypto/tls" "crypto/tls"
"io" "io"
"io/ioutil"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -96,7 +95,7 @@ func SendEmailWithAttachments(cfg MailConfig, sender, replyTo, subject, body, ht
email.AddAlternative(mail.TextPlain, body) email.AddAlternative(mail.TextPlain, body)
for _, attachment := range attachments { for _, attachment := range attachments {
attachmentData, err := ioutil.ReadAll(attachment.Data) attachmentData, err := io.ReadAll(attachment.Data)
if err != nil { if err != nil {
return errors.Wrapf(err, "failed to read attachment data for %s", attachment.Name) return errors.Wrapf(err, "failed to read attachment data for %s", attachment.Name)
} }

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"net" "net"
"strings" "strings"
"time"
) )
// BroadcastAddr returns the last address in the given network, or the broadcast address. // BroadcastAddr returns the last address in the given network, or the broadcast address.
@@ -84,3 +85,11 @@ func ByteCountSI(b int64) string {
return fmt.Sprintf("%.1f %cB", return fmt.Sprintf("%.1f %cB",
float64(b)/float64(div), "kMGTPE"[exp]) float64(b)/float64(div), "kMGTPE"[exp])
} }
func FormatDateHTML(t *time.Time) string {
if t == nil {
return ""
}
return t.Format("2006-01-02")
}

View File

@@ -29,6 +29,7 @@ type Config struct {
SyncFilter string `yaml:"syncFilter" envconfig:"LDAP_SYNC_FILTER"` SyncFilter string `yaml:"syncFilter" envconfig:"LDAP_SYNC_FILTER"`
AdminLdapGroup string `yaml:"adminGroup" envconfig:"LDAP_ADMIN_GROUP"` // Members of this group receive admin rights in WG-Portal AdminLdapGroup string `yaml:"adminGroup" envconfig:"LDAP_ADMIN_GROUP"` // Members of this group receive admin rights in WG-Portal
AdminLdapGroup_ *gldap.DN `yaml:"-"` AdminLdapGroup_ *gldap.DN `yaml:"-"`
EveryoneAdmin bool `yaml:"everyoneAdmin" envconfig:"LDAP_EVERYONE_ADMIN"`
LdapCertConn bool `yaml:"ldapCertConn" envconfig:"LDAP_CERT_CONN"` LdapCertConn bool `yaml:"ldapCertConn" envconfig:"LDAP_CERT_CONN"`
LdapTlsCert string `yaml:"ldapTlsCert" envconfig:"LDAPTLS_CERT"` LdapTlsCert string `yaml:"ldapTlsCert" envconfig:"LDAPTLS_CERT"`
LdapTlsKey string `yaml:"ldapTlsKey" envconfig:"LDAPTLS_KEY"` LdapTlsKey string `yaml:"ldapTlsKey" envconfig:"LDAPTLS_KEY"`

View File

@@ -2,7 +2,7 @@ package ldap
import ( import (
"crypto/tls" "crypto/tls"
"io/ioutil" "os"
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -19,23 +19,23 @@ func Open(cfg *Config) (*ldap.Conn, error) {
if cfg.LdapCertConn { if cfg.LdapCertConn {
cert_plain, err := ioutil.ReadFile(cfg.LdapTlsCert) certPlain, err := os.ReadFile(cfg.LdapTlsCert)
if err != nil { if err != nil {
return nil, errors.WithMessage(err, "failed to load the certificate") return nil, errors.WithMessage(err, "failed to load the certificate")
} }
key, err := ioutil.ReadFile(cfg.LdapTlsKey) key, err := os.ReadFile(cfg.LdapTlsKey)
if err != nil { if err != nil {
return nil, errors.WithMessage(err, "failed to load the key") return nil, errors.WithMessage(err, "failed to load the key")
} }
cert_x509, err := tls.X509KeyPair(cert_plain, key) certX509, err := tls.X509KeyPair(certPlain, key)
if err != nil { if err != nil {
return nil, errors.WithMessage(err, "failed X509") return nil, errors.WithMessage(err, "failed X509")
} }
tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert_x509}} tlsConfig = &tls.Config{Certificates: []tls.Certificate{certX509}}
} else { } else {

View File

@@ -439,6 +439,7 @@ func (s *ApiServer) PutPeer(c *gin.Context) {
now := time.Now() now := time.Now()
if updatePeer.DeactivatedAt != nil { if updatePeer.DeactivatedAt != nil {
updatePeer.DeactivatedAt = &now updatePeer.DeactivatedAt = &now
updatePeer.DeactivatedReason = wireguard.DeactivatedReasonApiEdit
} }
if err := s.s.UpdatePeer(updatePeer, now); err != nil { if err := s.s.UpdatePeer(updatePeer, now); err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
@@ -516,6 +517,7 @@ func (s *ApiServer) PatchPeer(c *gin.Context) {
now := time.Now() now := time.Now()
if mergedPeer.DeactivatedAt != nil { if mergedPeer.DeactivatedAt != nil {
mergedPeer.DeactivatedAt = &now mergedPeer.DeactivatedAt = &now
mergedPeer.DeactivatedReason = wireguard.DeactivatedReasonApiEdit
} }
if err := s.s.UpdatePeer(mergedPeer, now); err != nil { if err := s.s.UpdatePeer(mergedPeer, now); err != nil {
c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})

View File

@@ -67,10 +67,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"`
SelfProvisioningAllowed bool `yaml:"selfProvisioning" envconfig:"SELF_PROVISIONING"` SelfProvisioningAllowed bool `yaml:"selfProvisioning" envconfig:"SELF_PROVISIONING"`
WGExoprterFriendlyNames bool `yaml:"wgExporterFriendlyNames" envconfig:"WG_EXPORTER_FRIENDLY_NAMES"` WGExporterFriendlyNames bool `yaml:"wgExporterFriendlyNames" envconfig:"WG_EXPORTER_FRIENDLY_NAMES"`
LdapEnabled bool `yaml:"ldapEnabled" envconfig:"LDAP_ENABLED"` LdapEnabled bool `yaml:"ldapEnabled" envconfig:"LDAP_ENABLED"`
SessionSecret string `yaml:"sessionSecret" envconfig:"SESSION_SECRET"` SessionSecret string `yaml:"sessionSecret" envconfig:"SESSION_SECRET"`
LogoUrl string `yaml:"logoUrl" envconfig:"LOGO_URL"` LogoUrl string `yaml:"logoUrl" envconfig:"LOGO_URL"`
BackgroundTaskInterval int `yaml:"backgroundTaskInterval" envconfig:"BACKGROUND_TASK_INTERVAL"` // in seconds
ExpiryReEnable bool `yaml:"expiryReEnable" envconfig:"EXPIRY_REENABLE"`
} `yaml:"core"` } `yaml:"core"`
Database common.DatabaseConfig `yaml:"database"` Database common.DatabaseConfig `yaml:"database"`
Email common.MailConfig `yaml:"email"` Email common.MailConfig `yaml:"email"`
@@ -92,8 +94,9 @@ func NewConfig() *Config {
cfg.Core.AdminPassword = "wgportal" cfg.Core.AdminPassword = "wgportal"
cfg.Core.LdapEnabled = false cfg.Core.LdapEnabled = false
cfg.Core.EditableKeys = true cfg.Core.EditableKeys = true
cfg.Core.WGExoprterFriendlyNames = false cfg.Core.WGExporterFriendlyNames = false
cfg.Core.SessionSecret = "secret" cfg.Core.SessionSecret = "secret"
cfg.Core.BackgroundTaskInterval = 15 * 60 // 15 minutes
cfg.Database.Typ = "sqlite" cfg.Database.Typ = "sqlite"
cfg.Database.Database = "data/wg_portal.db" cfg.Database.Database = "data/wg_portal.db"

View File

@@ -1,17 +1,10 @@
// Package docs GENERATED BY THE COMMAND ABOVE; DO NOT EDIT // Package docs GENERATED BY SWAG; DO NOT EDIT
// This file was generated by swaggo/swag // This file was generated by swaggo/swag
package docs package docs
import ( import "github.com/swaggo/swag"
"bytes"
"encoding/json"
"strings"
"text/template"
"github.com/swaggo/swag" const docTemplate = `{
)
var doc = `{
"schemes": {{ marshal .Schemes }}, "schemes": {{ marshal .Schemes }},
"swagger": "2.0", "swagger": "2.0",
"info": { "info": {
@@ -1267,10 +1260,13 @@ var doc = `{
"type": "string" "type": "string"
}, },
"Mtu": { "Mtu": {
"type": "integer" "type": "integer",
"maximum": 1500,
"minimum": 0
}, },
"PersistentKeepalive": { "PersistentKeepalive": {
"type": "integer" "type": "integer",
"minimum": 0
} }
} }
}, },
@@ -1344,16 +1340,19 @@ var doc = `{
"type": "string" "type": "string"
}, },
"DefaultPersistentKeepalive": { "DefaultPersistentKeepalive": {
"type": "integer" "type": "integer",
"minimum": 0
}, },
"DeviceName": { "DeviceName": {
"type": "string" "type": "string"
}, },
"DisplayName": { "DisplayName": {
"type": "string" "type": "string",
"maxLength": 200
}, },
"FirewallMark": { "FirewallMark": {
"type": "integer" "type": "integer",
"minimum": 0
}, },
"IPsStr": { "IPsStr": {
"description": "comma separated list of the IPs of the client, wg-quick addition", "description": "comma separated list of the IPs of the client, wg-quick addition",
@@ -1364,7 +1363,9 @@ var doc = `{
}, },
"Mtu": { "Mtu": {
"description": "the interface MTU, wg-quick addition", "description": "the interface MTU, wg-quick addition",
"type": "integer" "type": "integer",
"maximum": 1500,
"minimum": 0
}, },
"PostDown": { "PostDown": {
"description": "post down script, wg-quick addition", "description": "post down script, wg-quick addition",
@@ -1399,7 +1400,11 @@ var doc = `{
"type": "boolean" "type": "boolean"
}, },
"Type": { "Type": {
"type": "string" "type": "string",
"enum": [
"client",
"server"
]
}, },
"UpdatedAt": { "UpdatedAt": {
"type": "string" "type": "string"
@@ -1438,11 +1443,18 @@ var doc = `{
"DeactivatedAt": { "DeactivatedAt": {
"type": "string" "type": "string"
}, },
"DeactivatedReason": {
"type": "string"
},
"DeviceName": { "DeviceName": {
"type": "string" "type": "string"
}, },
"DeviceType": { "DeviceType": {
"type": "string" "type": "string",
"enum": [
"client",
"server"
]
}, },
"Email": { "Email": {
"type": "string" "type": "string"
@@ -1450,23 +1462,30 @@ var doc = `{
"Endpoint": { "Endpoint": {
"type": "string" "type": "string"
}, },
"ExpiresAt": {
"type": "string"
},
"IPsStr": { "IPsStr": {
"description": "a comma separated list of IPs of the client", "description": "a comma separated list of IPs of the client",
"type": "string" "type": "string"
}, },
"Identifier": { "Identifier": {
"description": "Identifier AND Email make a WireGuard peer unique", "description": "Identifier AND Email make a WireGuard peer unique",
"type": "string" "type": "string",
"maxLength": 64
}, },
"IgnoreGlobalSettings": { "IgnoreGlobalSettings": {
"type": "boolean" "type": "boolean"
}, },
"Mtu": { "Mtu": {
"description": "Global Device Settings (can be ignored, only make sense if device is in server mode)", "description": "Global Device Settings (can be ignored, only make sense if device is in server mode)",
"type": "integer" "type": "integer",
"maximum": 1500,
"minimum": 0
}, },
"PersistentKeepalive": { "PersistentKeepalive": {
"type": "integer" "type": "integer",
"minimum": 0
}, },
"PresharedKey": { "PresharedKey": {
"type": "string" "type": "string"
@@ -1502,56 +1521,18 @@ var doc = `{
} }
}` }`
type swaggerInfo struct {
Version string
Host string
BasePath string
Schemes []string
Title string
Description string
}
// SwaggerInfo holds exported Swagger Info so clients can modify it // SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = swaggerInfo{ var SwaggerInfo = &swag.Spec{
Version: "1.0", Version: "1.0",
Host: "", Host: "",
BasePath: "/api/v1", BasePath: "/api/v1",
Schemes: []string{}, Schemes: []string{},
Title: "WireGuard Portal API", Title: "WireGuard Portal API",
Description: "WireGuard Portal API for managing users and peers.", Description: "WireGuard Portal API for managing users and peers.",
} InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
type s struct{}
func (s *s) ReadDoc() string {
sInfo := SwaggerInfo
sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1)
t, err := template.New("swagger_info").Funcs(template.FuncMap{
"marshal": func(v interface{}) string {
a, _ := json.Marshal(v)
return string(a)
},
"escape": func(v interface{}) string {
// escape tabs
str := strings.Replace(v.(string), "\t", "\\t", -1)
// replace " with \", and if that results in \\", replace that with \\\"
str = strings.Replace(str, "\"", "\\\"", -1)
return strings.Replace(str, "\\\\\"", "\\\\\\\"", -1)
},
}).Parse(doc)
if err != nil {
return doc
}
var tpl bytes.Buffer
if err := t.Execute(&tpl, sInfo); err != nil {
return doc
}
return tpl.String()
} }
func init() { func init() {
swag.Register(swag.Name, &s{}) swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
} }

View File

@@ -112,7 +112,7 @@ func (s *Server) GetInterfaceConfig(c *gin.Context) {
currentSession := GetSessionData(c) currentSession := GetSessionData(c)
device := s.peers.GetDevice(currentSession.DeviceName) device := s.peers.GetDevice(currentSession.DeviceName)
peers := s.peers.GetActivePeers(device.DeviceName) peers := s.peers.GetActivePeers(device.DeviceName)
cfg, err := device.GetConfigFile(peers, s.config.Core.WGExoprterFriendlyNames) cfg, err := device.GetConfigFile(peers, s.config.Core.WGExporterFriendlyNames)
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

View File

@@ -71,8 +71,15 @@ func (s *Server) PostAdminEditPeer(c *gin.Context) {
now := time.Now() now := time.Now()
if disabled && currentPeer.DeactivatedAt == nil { if disabled && currentPeer.DeactivatedAt == nil {
formPeer.DeactivatedAt = &now formPeer.DeactivatedAt = &now
formPeer.DeactivatedReason = wireguard.DeactivatedReasonAdminEdit
} else if !disabled { } else if !disabled {
formPeer.DeactivatedAt = nil formPeer.DeactivatedAt = nil
formPeer.DeactivatedReason = ""
// If a peer was deactivated due to expiry, remove the expires-at date to avoid
// unwanted re-expiry.
if currentPeer.DeactivatedReason == wireguard.DeactivatedReasonExpired {
formPeer.ExpiresAt = nil
}
} }
// Update in database // Update in database
@@ -129,6 +136,7 @@ func (s *Server) PostAdminCreatePeer(c *gin.Context) {
now := time.Now() now := time.Now()
if disabled { if disabled {
formPeer.DeactivatedAt = &now formPeer.DeactivatedAt = &now
formPeer.DeactivatedReason = wireguard.DeactivatedReasonAdminCreate
} }
if err := s.CreatePeer(currentSession.DeviceName, formPeer); err != nil { if err := s.CreatePeer(currentSession.DeviceName, formPeer); err != nil {
@@ -189,7 +197,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(currentSession.DeviceName, emails[i], formData.Identifier, false); err != nil { if err := s.CreatePeerByEmail(currentSession.DeviceName, emails[i], formData.Identifier); 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")
@@ -420,10 +428,9 @@ func (s *Server) PostUserCreatePeer(c *gin.Context) {
formPeer = currentSession.FormData.(wireguard.Peer) formPeer = currentSession.FormData.(wireguard.Peer)
} }
formPeer.Email = currentSession.Email; formPeer.Email = currentSession.Email
formPeer.Identifier = currentSession.Email; formPeer.Identifier = currentSession.Email
formPeer.DeviceType = wireguard.DeviceTypeServer; formPeer.DeviceType = wireguard.DeviceTypeServer
formPeer.PrivateKey = "";
if err := c.ShouldBind(&formPeer); err != nil { if err := c.ShouldBind(&formPeer); err != nil {
_ = s.updateFormInSession(c, formPeer) _ = s.updateFormInSession(c, formPeer)
@@ -432,10 +439,16 @@ func (s *Server) PostUserCreatePeer(c *gin.Context) {
return return
} }
// if public key was manually set, remove the incorrect private key
if formPeer.PublicKey != currentSession.FormData.(wireguard.Peer).PublicKey {
formPeer.PrivateKey = ""
}
disabled := c.PostForm("isdisabled") != "" disabled := c.PostForm("isdisabled") != ""
now := time.Now() now := time.Now()
if disabled { if disabled {
formPeer.DeactivatedAt = &now formPeer.DeactivatedAt = &now
formPeer.DeactivatedReason = wireguard.DeactivatedReasonUserCreate
} }
if err := s.CreatePeer(currentSession.DeviceName, formPeer); err != nil { if err := s.CreatePeer(currentSession.DeviceName, formPeer); err != nil {
@@ -452,7 +465,6 @@ func (s *Server) PostUserCreatePeer(c *gin.Context) {
func (s *Server) GetUserEditPeer(c *gin.Context) { func (s *Server) GetUserEditPeer(c *gin.Context) {
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)
if err != nil { if err != nil {
s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error()) s.GetHandleError(c, http.StatusInternalServerError, "Session error", err.Error())
@@ -461,7 +473,7 @@ func (s *Server) GetUserEditPeer(c *gin.Context) {
if peer.Email != currentSession.Email { if 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
} }
c.HTML(http.StatusOK, "user_edit_client.html", gin.H{ c.HTML(http.StatusOK, "user_edit_client.html", gin.H{
@@ -486,13 +498,14 @@ func (s *Server) PostUserEditPeer(c *gin.Context) {
if currentPeer.Email != currentSession.Email { if currentPeer.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
} }
disabled := c.PostForm("isdisabled") != "" disabled := c.PostForm("isdisabled") != ""
now := time.Now() now := time.Now()
if disabled && currentPeer.DeactivatedAt == nil { if disabled && currentPeer.DeactivatedAt == nil {
currentPeer.DeactivatedAt = &now currentPeer.DeactivatedAt = &now
currentPeer.DeactivatedReason = wireguard.DeactivatedReasonUserEdit
} }
// Update in database // Update in database

View File

@@ -4,6 +4,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/h44z/wg-portal/internal/wireguard"
"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/users"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@@ -45,6 +47,9 @@ func (s *Server) SyncLdapWithUserDatabase() {
} }
func (s Server) userIsInAdminGroup(ldapData *ldap.RawLdapData) bool { func (s Server) userIsInAdminGroup(ldapData *ldap.RawLdapData) bool {
if s.config.LDAP.EveryoneAdmin {
return true
}
if s.config.LDAP.AdminLdapGroup_ == nil { if s.config.LDAP.AdminLdapGroup_ == nil {
return false return false
} }
@@ -109,6 +114,7 @@ func (s *Server) disableMissingLdapUsers(ldapUsers []ldap.RawLdapData) {
for _, peer := range s.peers.GetPeersByMail(activeUsers[i].Email) { for _, peer := range s.peers.GetPeersByMail(activeUsers[i].Email) {
now := time.Now() now := time.Now()
peer.DeactivatedAt = &now peer.DeactivatedAt = &now
peer.DeactivatedReason = wireguard.DeactivatedReasonLdapMissing
if err := s.UpdatePeer(peer, now); err != nil { if err := s.UpdatePeer(peer, now); err != nil {
logrus.Errorf("failed to update deactivated peer %s: %v", peer.PublicKey, err) logrus.Errorf("failed to update deactivated peer %s: %v", peer.PublicKey, err)
} }
@@ -138,6 +144,7 @@ func (s *Server) updateLdapUsers(ldapUsers []ldap.RawLdapData) {
for _, peer := range s.peers.GetPeersByMail(user.Email) { for _, peer := range s.peers.GetPeersByMail(user.Email) {
now := time.Now() now := time.Now()
peer.DeactivatedAt = nil peer.DeactivatedAt = nil
peer.DeactivatedReason = ""
if err = s.UpdatePeer(peer, now); err != nil { if err = s.UpdatePeer(peer, now); err != nil {
logrus.Errorf("failed to update activated peer %s: %v", peer.PublicKey, err) logrus.Errorf("failed to update activated peer %s: %v", peer.PublicKey, err)
} }

View File

@@ -4,8 +4,8 @@ import (
"context" "context"
"encoding/gob" "encoding/gob"
"html/template" "html/template"
"io"
"io/fs" "io/fs"
"io/ioutil"
"math/rand" "math/rand"
"net/http" "net/http"
"net/url" "net/url"
@@ -110,7 +110,7 @@ func (s *Server) Setup(ctx context.Context) error {
// Setup http server // Setup http server
gin.SetMode(gin.DebugMode) gin.SetMode(gin.DebugMode)
gin.DefaultWriter = ioutil.Discard gin.DefaultWriter = io.Discard
s.server = gin.New() s.server = gin.New()
if logrus.GetLevel() == logrus.TraceLevel { if logrus.GetLevel() == logrus.TraceLevel {
s.server.Use(ginlogrus.Logger(logrus.StandardLogger())) s.server.Use(ginlogrus.Logger(logrus.StandardLogger()))
@@ -127,6 +127,7 @@ func (s *Server) Setup(ctx context.Context) error {
}) })
s.server.Use(sessions.Sessions("authsession", cookieStore)) s.server.Use(sessions.Sessions("authsession", cookieStore))
s.server.SetFuncMap(template.FuncMap{ s.server.SetFuncMap(template.FuncMap{
"formatDate": common.FormatDateHTML,
"formatBytes": common.ByteCountSI, "formatBytes": common.ByteCountSI,
"urlEncode": url.QueryEscape, "urlEncode": url.QueryEscape,
"startsWith": strings.HasPrefix, "startsWith": strings.HasPrefix,
@@ -215,6 +216,8 @@ func (s *Server) Run() {
go s.SyncLdapWithUserDatabase() go s.SyncLdapWithUserDatabase()
} }
go s.RunBackgroundTasks(s.ctx)
// Run web service // Run web service
srv := &http.Server{ srv := &http.Server{
Addr: s.config.Core.ListeningAddress, Addr: s.config.Core.ListeningAddress,

View File

@@ -1,9 +1,10 @@
package server package server
import ( import (
"context"
"crypto/md5" "crypto/md5"
"fmt" "fmt"
"io/ioutil" "os"
"path" "path"
"syscall" "syscall"
"time" "time"
@@ -62,7 +63,7 @@ func (s *Server) PrepareNewPeer(device string) (wireguard.Peer, error) {
} }
// CreatePeerByEmail creates a new peer for the given email. // CreatePeerByEmail creates a new peer for the given email.
func (s *Server) CreatePeerByEmail(device, email, identifierSuffix string, disabled bool) error { func (s *Server) CreatePeerByEmail(device, email, identifierSuffix string) error {
user := s.users.GetUser(email) user := s.users.GetUser(email)
peer, err := s.PrepareNewPeer(device) peer, err := s.PrepareNewPeer(device)
@@ -75,10 +76,6 @@ func (s *Server) CreatePeerByEmail(device, email, identifierSuffix string, disab
} else { } else {
peer.Identifier = fmt.Sprintf("%s (%s)", email, identifierSuffix) peer.Identifier = fmt.Sprintf("%s (%s)", email, identifierSuffix)
} }
now := time.Now()
if disabled {
peer.DeactivatedAt = &now
}
return s.CreatePeer(device, peer) return s.CreatePeer(device, peer)
} }
@@ -123,6 +120,9 @@ func (s *Server) CreatePeer(device string, peer wireguard.Peer) error {
} }
peer.DeviceName = dev.DeviceName 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)))
if peer.ExpiresAt != nil && peer.ExpiresAt.IsZero() { // convert 01-01-0001 to nil
peer.ExpiresAt = nil
}
// Create WireGuard interface // Create WireGuard interface
if peer.DeactivatedAt == nil { if peer.DeactivatedAt == nil {
@@ -144,6 +144,13 @@ 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) dev := s.peers.GetDevice(peer.DeviceName)
// Check if expiry date is in the future, an reactivate the peer in case.
if s.config.Core.ExpiryReEnable && currentPeer.DeactivatedReason == wireguard.DeactivatedReasonExpired &&
peer.ExpiresAt != nil && peer.ExpiresAt.After(time.Now()) {
peer.DeactivatedAt = nil
peer.DeactivatedReason = ""
}
// Update WireGuard device // Update WireGuard device
var err error var err error
switch { switch {
@@ -159,6 +166,9 @@ func (s *Server) UpdatePeer(peer wireguard.Peer, updateTime time.Time) error {
} }
peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey))) peer.UID = fmt.Sprintf("u%x", md5.Sum([]byte(peer.PublicKey)))
if peer.ExpiresAt != nil && peer.ExpiresAt.IsZero() { // convert 01-01-0001 to nil
peer.ExpiresAt = nil
}
// Update in database // Update in database
if err := s.peers.UpdatePeer(peer); err != nil { if err := s.peers.UpdatePeer(peer); err != nil {
@@ -209,12 +219,12 @@ func (s *Server) WriteWireGuardConfigFile(device string) error {
} }
dev := s.peers.GetDevice(device) dev := s.peers.GetDevice(device)
cfg, err := dev.GetConfigFile(s.peers.GetActivePeers(device), s.config.Core.WGExoprterFriendlyNames) cfg, err := dev.GetConfigFile(s.peers.GetActivePeers(device), s.config.Core.WGExporterFriendlyNames)
if err != nil { if err != nil {
return errors.WithMessage(err, "failed to get config file") return errors.WithMessage(err, "failed to get config file")
} }
filePath := path.Join(s.config.WG.ConfigDirectoryPath, dev.DeviceName+".conf") filePath := path.Join(s.config.WG.ConfigDirectoryPath, dev.DeviceName+".conf")
if err := ioutil.WriteFile(filePath, cfg, 0644); err != nil { if err := os.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
@@ -281,6 +291,7 @@ func (s *Server) UpdateUser(user users.User) error {
for _, peer := range s.peers.GetPeersByMail(user.Email) { for _, peer := range s.peers.GetPeersByMail(user.Email) {
now := time.Now() now := time.Now()
peer.DeactivatedAt = nil peer.DeactivatedAt = nil
peer.DeactivatedReason = ""
if err := s.UpdatePeer(peer, now); err != nil { if err := s.UpdatePeer(peer, now); err != nil {
logrus.Errorf("failed to update (re)activated peer %s for %s: %v", peer.PublicKey, user.Email, err) logrus.Errorf("failed to update (re)activated peer %s for %s: %v", peer.PublicKey, user.Email, err)
} }
@@ -302,6 +313,7 @@ func (s *Server) DeleteUser(user users.User) error {
for _, peer := range s.peers.GetPeersByMail(user.Email) { for _, peer := range s.peers.GetPeersByMail(user.Email) {
now := time.Now() now := time.Now()
peer.DeactivatedAt = &now peer.DeactivatedAt = &now
peer.DeactivatedReason = wireguard.DeactivatedReasonUserMissing
if err := s.UpdatePeer(peer, now); err != nil { if err := s.UpdatePeer(peer, now); err != nil {
logrus.Errorf("failed to update deactivated peer %s for %s: %v", peer.PublicKey, user.Email, err) logrus.Errorf("failed to update deactivated peer %s for %s: %v", peer.PublicKey, user.Email, err)
} }
@@ -376,3 +388,60 @@ func (s *Server) GetDeviceNames() map[string]string {
return devNames return devNames
} }
func (s *Server) RunBackgroundTasks(ctx context.Context) {
running := true
for running {
select {
case <-ctx.Done():
running = false
continue
case <-time.After(time.Duration(s.config.Core.BackgroundTaskInterval) * time.Second):
// sleep completed, select will stop blocking
}
logrus.Debug("running periodic background tasks...")
err := s.checkExpiredPeers()
if err != nil {
logrus.Errorf("failed to check expired peers: %v", err)
}
}
}
func (s *Server) checkExpiredPeers() error {
now := time.Now()
for _, devName := range s.wg.Cfg.DeviceNames {
changed := false
peers := s.peers.GetAllPeers(devName)
for _, peer := range peers {
if peer.IsExpired() && !peer.IsDeactivated() {
changed = true
peer.UpdatedAt = now
peer.DeactivatedAt = &now
peer.DeactivatedReason = wireguard.DeactivatedReasonExpired
res := s.db.Save(&peer)
if res.Error != nil {
return fmt.Errorf("failed save expired peer %s: %w", peer.PublicKey, res.Error)
}
err := s.wg.RemovePeer(peer.DeviceName, peer.PublicKey)
if err != nil {
return fmt.Errorf("failed to expire peer %s: %w", peer.PublicKey, err)
}
}
}
if changed {
err := s.WriteWireGuardConfigFile(devName)
if err != nil {
return fmt.Errorf("failed to persist config for interface %s: %w", devName, err)
}
}
}
return nil
}

View File

@@ -1,4 +1,4 @@
package server package server
var Version = "testbuild" var Version = "testbuild"
var DatabaseVersion = "1.0.8" var DatabaseVersion = "1.0.9"

View File

@@ -11,7 +11,6 @@ type UserSource string
const ( const (
UserSourceLdap UserSource = "ldap" // LDAP / ActiveDirectory UserSourceLdap UserSource = "ldap" // LDAP / ActiveDirectory
UserSourceDatabase UserSource = "db" // sqlite / mysql database UserSourceDatabase UserSource = "db" // sqlite / mysql database
UserSourceOIDC UserSource = "oidc" // open id connect, TODO: implement
) )
type PrivateString string type PrivateString string

View File

@@ -23,9 +23,19 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// const (
DeactivatedReasonExpired = "expired"
DeactivatedReasonUserEdit = "user edit action"
DeactivatedReasonUserCreate = "user create action"
DeactivatedReasonAdminEdit = "admin edit action"
DeactivatedReasonAdminCreate = "admin create action"
DeactivatedReasonApiEdit = "api edit action"
DeactivatedReasonApiCreate = "api create action"
DeactivatedReasonLdapMissing = "missing in ldap"
DeactivatedReasonUserMissing = "missing user"
)
// CUSTOM VALIDATORS ---------------------------------------------------------------------------- // CUSTOM VALIDATORS ----------------------------------------------------------------------------
//
var cidrList validator.Func = func(fl validator.FieldLevel) bool { var cidrList validator.Func = func(fl validator.FieldLevel) bool {
cidrListStr := fl.Field().String() cidrListStr := fl.Field().String()
@@ -39,6 +49,22 @@ var cidrList validator.Func = func(fl validator.FieldLevel) bool {
return true return true
} }
var dnsList validator.Func = func(fl validator.FieldLevel) bool {
dnsListStr := fl.Field().String()
dnsList := common.ParseStringList(dnsListStr)
validate := binding.Validator.Engine().(*validator.Validate)
for i := range dnsList {
ip := net.ParseIP(dnsList[i])
if ip == nil {
err := validate.Var(dnsList[i], "fqdn")
if err != nil {
return false
}
}
}
return true
}
var ipList validator.Func = func(fl validator.FieldLevel) bool { var ipList validator.Func = func(fl validator.FieldLevel) bool {
ipListStr := fl.Field().String() ipListStr := fl.Field().String()
ipList := common.ParseStringList(ipListStr) ipList := common.ParseStringList(ipListStr)
@@ -55,6 +81,7 @@ func init() {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok { if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
_ = v.RegisterValidation("cidrlist", cidrList) _ = v.RegisterValidation("cidrlist", cidrList)
_ = v.RegisterValidation("iplist", ipList) _ = v.RegisterValidation("iplist", ipList)
_ = v.RegisterValidation("dnsList", dnsList)
} }
} }
@@ -89,11 +116,15 @@ type Peer struct {
// Misc. WireGuard Settings // Misc. WireGuard Settings
PrivateKey string `form:"privkey" binding:"omitempty,base64"` 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 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 DNSStr string `form:"dns" binding:"dnsList"` // 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) // Global Device Settings (can be ignored, only make sense if device is in server mode)
Mtu int `form:"mtu" binding:"gte=0,lte=1500"` Mtu int `form:"mtu" binding:"gte=0,lte=1500"`
DeactivatedAt *time.Time `json:",omitempty"` DeactivatedAt *time.Time `json:",omitempty"`
DeactivatedReason string `json:",omitempty"`
ExpiresAt *time.Time `json:",omitempty" form:"expires_at" binding:"omitempty" time_format:"2006-01-02"`
CreatedBy string CreatedBy string
UpdatedBy string UpdatedBy string
CreatedAt time.Time CreatedAt time.Time
@@ -223,6 +254,33 @@ func (p Peer) IsValid() bool {
return true return true
} }
func (p Peer) WillExpire() bool {
if p.ExpiresAt == nil {
return false
}
if p.DeactivatedAt != nil {
return false // already deactivated...
}
if p.ExpiresAt.After(time.Now()) {
return true
}
return false
}
func (p Peer) IsExpired() bool {
if p.ExpiresAt == nil {
return false
}
if p.ExpiresAt.Before(time.Now()) {
return true
}
return false
}
func (p Peer) IsDeactivated() bool {
return p.DeactivatedAt != nil
}
func (p Peer) GetConfigFileName() string { func (p Peer) GetConfigFileName() string {
reg := regexp.MustCompile("[^a-zA-Z0-9_-]+") reg := regexp.MustCompile("[^a-zA-Z0-9_-]+")
return reg.ReplaceAllString(strings.ReplaceAll(p.Identifier, " ", "-"), "") + ".conf" return reg.ReplaceAllString(strings.ReplaceAll(p.Identifier, " ", "-"), "") + ".conf"
@@ -244,7 +302,7 @@ type Device struct {
Peers []Peer `gorm:"foreignKey:DeviceName" binding:"-" json:"-"` // linked WireGuard peers Peers []Peer `gorm:"foreignKey:DeviceName" binding:"-" json:"-"` // linked WireGuard peers
Type DeviceType `form:"devicetype" binding:"required,oneof=client server"` Type DeviceType `form:"devicetype" binding:"required,oneof=client server"`
DeviceName string `form:"device" gorm:"primaryKey" binding:"required" validator:"regexp=[0-9a-zA-Z\-]+"` DeviceName string `form:"device" gorm:"primaryKey" binding:"required" validator:"regexp=[0-9a-zA-Z\\-]+"`
DisplayName string `form:"displayname" binding:"omitempty,max=200"` DisplayName string `form:"displayname" binding:"omitempty,max=200"`
// Core WireGuard Settings (Interface section) // Core WireGuard Settings (Interface section)
@@ -255,7 +313,7 @@ type Device struct {
PublicKey string `form:"pubkey" binding:"required,base64"` PublicKey string `form:"pubkey" binding:"required,base64"`
Mtu int `form:"mtu" binding:"gte=0,lte=1500"` // the interface MTU, wg-quick addition 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 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 DNSStr string `form:"dns" binding:"dnsList"` // comma separated list of the DNS servers of the client, wg-quick addition
RoutingTable string `form:"routingtable"` // the routing table, wg-quick addition RoutingTable string `form:"routingtable"` // the routing table, wg-quick addition
PreUp string `form:"preup"` // pre up script, wg-quick addition PreUp string `form:"preup"` // pre up script, wg-quick addition
PostUp string `form:"postup"` // post up script, wg-quick addition PostUp string `form:"postup"` // post up script, wg-quick addition