mirror of
https://github.com/h44z/wg-portal.git
synced 2025-10-06 16:36:18 +00:00
Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
dab1e13c54 | ||
|
51fb9b4139 | ||
|
bda8c9a3d1 | ||
|
54716f7f53 | ||
|
e97fb38bd5 | ||
|
2796433973 | ||
|
3e2208c8f6 | ||
|
09a9af245c | ||
|
979cec7d83 | ||
|
0f33871850 | ||
|
c43e8d7ca2 | ||
|
4a0e773d96 | ||
|
6f4af97024 | ||
|
0d5b895174 | ||
|
fe3247bdc1 | ||
|
e4b927bc45 | ||
|
383fc8cb58 | ||
|
ab7f19bb55 | ||
|
49c7109c61 | ||
|
352c689623 | ||
|
e6a8e2f2cf | ||
|
12717987a6 |
@@ -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.*/
|
|
30
.github/workflows/docker-publish.yml
vendored
30
.github/workflows/docker-publish.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -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
|
||||||
|
22
Dockerfile
22
Dockerfile
@@ -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
161
Makefile
@@ -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)
|
||||||
|
@@ -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.
|
||||||
|
144
README.md
144
README.md
@@ -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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -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.
|
||||||
@@ -108,61 +131,64 @@ For example: `CONFIG_FILE=/home/test/config.yml ./wg-portal-amd64`.
|
|||||||
### Configuration Options
|
### Configuration Options
|
||||||
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. |
|
||||||
| COMPANY_NAME | company | core | WireGuard Portal | The company name (for branding). |
|
| COMPANY_NAME | company | core | WireGuard Portal | The company name (for branding). |
|
||||||
| MAIL_FROM | mailFrom | core | WireGuard VPN <noreply@company.com> | The email address from which emails are sent. |
|
| MAIL_FROM | mailFrom | core | WireGuard VPN <noreply@company.com> | The email address from which emails are sent. |
|
||||||
| LOGO_URL | logoUrl | core | /img/header-logo.png | The logo displayed in the page's header. |
|
| LOGO_URL | logoUrl | core | /img/header-logo.png | The logo displayed in the page's header. |
|
||||||
| ADMIN_USER | adminUser | core | admin@wgportal.local | The administrator user. Must be a valid email address. |
|
| ADMIN_USER | adminUser | core | admin@wgportal.local | The administrator user. Must be a valid email address. |
|
||||||
| ADMIN_PASS | adminPass | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. |
|
| ADMIN_PASS | adminPass | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. |
|
||||||
| EDITABLE_KEYS | editableKeys | core | true | Allow to edit key-pairs in the UI. |
|
| EDITABLE_KEYS | editableKeys | core | true | Allow to edit key-pairs in the UI. |
|
||||||
| CREATE_DEFAULT_PEER | createDefaultPeer | core | false | If an LDAP user logs in for the first time, a new WireGuard peer will be created on the WG_DEFAULT_DEVICE if this option is enabled. |
|
| CREATE_DEFAULT_PEER | createDefaultPeer | core | false | If an LDAP user logs in for the first time, a new WireGuard peer will be created on the WG_DEFAULT_DEVICE if this option is enabled. |
|
||||||
| SELF_PROVISIONING | selfProvisioning | core | false | Allow registered users to automatically create peers via the RESTful API. |
|
| SELF_PROVISIONING | selfProvisioning | core | false | Allow registered users to automatically create peers via the RESTful API. |
|
||||||
| 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. |
|
||||||
| DATABASE_TYPE | typ | database | sqlite | Either mysql or sqlite. |
|
| BACKGROUND_TASK_INTERVAL | backgroundTaskInterval | core | 900 | The interval (in seconds) for the background tasks (like peer expiry check). |
|
||||||
| DATABASE_HOST | host | database | | The mysql server address. |
|
| EXPIRY_REENABLE | expiryReEnable | core | false | Reactivate expired peers if the expiration date is in the future. |
|
||||||
| DATABASE_PORT | port | database | | The mysql server port. |
|
| DATABASE_TYPE | typ | database | sqlite | Either mysql or sqlite. |
|
||||||
| DATABASE_NAME | database | database | data/wg_portal.db | For sqlite database: the database file-path, otherwise the database name. |
|
| DATABASE_HOST | host | database | | The mysql server address. |
|
||||||
| DATABASE_USERNAME | user | database | | The mysql user. |
|
| DATABASE_PORT | port | database | | The mysql server port. |
|
||||||
| DATABASE_PASSWORD | password | database | | The mysql password. |
|
| DATABASE_NAME | database | database | data/wg_portal.db | For sqlite database: the database file-path, otherwise the database name. |
|
||||||
| EMAIL_HOST | host | email | 127.0.0.1 | The email server address. |
|
| DATABASE_USERNAME | user | database | | The mysql user. |
|
||||||
| EMAIL_PORT | port | email | 25 | The email server port. |
|
| DATABASE_PASSWORD | password | database | | The mysql password. |
|
||||||
| EMAIL_TLS | tls | email | false | Use STARTTLS. DEPRECATED: use EMAIL_ENCRYPTION instead. |
|
| EMAIL_HOST | host | email | 127.0.0.1 | The email server address. |
|
||||||
| EMAIL_ENCRYPTION | encryption | email | none | Either none, tls or starttls. |
|
| EMAIL_PORT | port | email | 25 | The email server port. |
|
||||||
| EMAIL_CERT_VALIDATION | certcheck | email | false | Validate the email server certificate. |
|
| EMAIL_TLS | tls | email | false | Use STARTTLS. DEPRECATED: use EMAIL_ENCRYPTION instead. |
|
||||||
| EMAIL_USERNAME | user | email | | An optional username for SMTP authentication. |
|
| EMAIL_ENCRYPTION | encryption | email | none | Either none, tls or starttls. |
|
||||||
| EMAIL_PASSWORD | pass | email | | An optional password for SMTP authentication. |
|
| EMAIL_CERT_VALIDATION | certcheck | email | false | Validate the email server certificate. |
|
||||||
| EMAIL_AUTHTYPE | auth | email | plain | Either plain, login or crammd5. If username and password are empty, this value is ignored. |
|
| EMAIL_USERNAME | user | email | | An optional username for SMTP authentication. |
|
||||||
| WG_DEVICES | devices | wg | wg0 | A comma separated list of WireGuard devices. |
|
| EMAIL_PASSWORD | pass | email | | An optional password for SMTP authentication. |
|
||||||
| WG_DEFAULT_DEVICE | defaultDevice | wg | wg0 | This device is used for auto-created peers (if CREATE_DEFAULT_PEER is enabled). |
|
| EMAIL_AUTHTYPE | auth | email | plain | Either plain, login or crammd5. If username and password are empty, this value is ignored. |
|
||||||
| WG_CONFIG_PATH | configDirectory | wg | /etc/wireguard | If set, interface configuration updates will be written to this path, filename: <devicename>.conf. |
|
| WG_DEVICES | devices | wg | wg0 | A comma separated list of WireGuard devices. |
|
||||||
| MANAGE_IPS | manageIPAddresses | wg | true | Handle IP address setup of interface, only available on linux. |
|
| WG_DEFAULT_DEVICE | defaultDevice | wg | wg0 | This device is used for auto-created peers (if CREATE_DEFAULT_PEER is enabled). |
|
||||||
| LDAP_URL | url | ldap | ldap://srv-ad01.company.local:389 | The LDAP server url. |
|
| WG_CONFIG_PATH | configDirectory | wg | /etc/wireguard | If set, interface configuration updates will be written to this path, filename: <devicename>.conf. |
|
||||||
| LDAP_STARTTLS | startTLS | ldap | true | Use STARTTLS. |
|
| MANAGE_IPS | manageIPAddresses | wg | true | Handle IP address setup of interface, only available on linux. |
|
||||||
| LDAP_CERT_VALIDATION | certcheck | ldap | false | Validate the LDAP server certificate. |
|
| USER_MANAGE_PEERS | userManagePeers | wg | false | Logged in user can create or update peers (partially). |
|
||||||
| LDAP_BASEDN | dn | ldap | DC=COMPANY,DC=LOCAL | The base DN for searching users. |
|
| LDAP_URL | url | ldap | ldap://srv-ad01.company.local:389 | The LDAP server url. |
|
||||||
| LDAP_USER | user | ldap | company\\\\ldap_wireguard | The bind user. |
|
| LDAP_STARTTLS | startTLS | ldap | true | Use STARTTLS. |
|
||||||
| LDAP_PASSWORD | pass | ldap | SuperSecret | The bind password. |
|
| LDAP_CERT_VALIDATION | certcheck | ldap | false | Validate the LDAP server certificate. |
|
||||||
| LDAP_LOGIN_FILTER | loginFilter | ldap | (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2)) | {{login_identifier}} will be replaced with the login email address. |
|
| LDAP_BASEDN | dn | ldap | DC=COMPANY,DC=LOCAL | The base DN for searching users. |
|
||||||
| LDAP_SYNC_FILTER | syncFilter | ldap | (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*)) | The filter string for the LDAP synchronization service. |
|
| LDAP_USER | user | ldap | company\\\\ldap_wireguard | The bind user. |
|
||||||
| LDAP_ADMIN_GROUP | adminGroup | ldap | CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL | Users in this group are marked as administrators. |
|
| LDAP_PASSWORD | pass | ldap | SuperSecret | The bind password. |
|
||||||
| LDAP_ATTR_EMAIL | attrEmail | ldap | mail | User email attribute. |
|
| LDAP_LOGIN_FILTER | loginFilter | ldap | (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2)) | {{login_identifier}} will be replaced with the login email address. |
|
||||||
| LDAP_ATTR_FIRSTNAME | attrFirstname | ldap | givenName | User firstname attribute. |
|
| LDAP_SYNC_FILTER | syncFilter | ldap | (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*)) | The filter string for the LDAP synchronization service. |
|
||||||
| LDAP_ATTR_LASTNAME | attrLastname | ldap | sn | User lastname attribute. |
|
| LDAP_ADMIN_GROUP | adminGroup | ldap | CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL | Users in this group are marked as administrators. |
|
||||||
| LDAP_ATTR_PHONE | attrPhone | ldap | telephoneNumber | User phone number attribute. |
|
| LDAP_ATTR_EMAIL | attrEmail | ldap | mail | User email attribute. |
|
||||||
| LDAP_ATTR_GROUPS | attrGroups | ldap | memberOf | User groups attribute. |
|
| LDAP_ATTR_FIRSTNAME | attrFirstname | ldap | givenName | User firstname attribute. |
|
||||||
| LDAP_CERT_CONN | ldapCertConn | ldap | false | Allow connection with certificate against LDAP server without user/password |
|
| LDAP_ATTR_LASTNAME | attrLastname | ldap | sn | User lastname attribute. |
|
||||||
| LDAPTLS_CERT | ldapTlsCert | ldap | | The LDAP cert's path |
|
| LDAP_ATTR_PHONE | attrPhone | ldap | telephoneNumber | User phone number attribute. |
|
||||||
| LDAPTLS_KEY | ldapTlsKey | ldap | | The LDAP key's path |
|
| LDAP_ATTR_GROUPS | attrGroups | ldap | memberOf | User groups attribute. |
|
||||||
| LOG_LEVEL | | | debug | Specify log level, one of: trace, debug, info, off. |
|
| LDAP_CERT_CONN | ldapCertConn | ldap | false | Allow connection with certificate against LDAP server without user/password |
|
||||||
| LOG_JSON | | | false | Format log output as JSON. |
|
| LDAPTLS_CERT | ldapTlsCert | ldap | | The LDAP cert's path |
|
||||||
| LOG_COLOR | | | true | Colorize log output. |
|
| LDAPTLS_KEY | ldapTlsKey | ldap | | The LDAP key's path |
|
||||||
| CONFIG_FILE | | | config.yml | The config file path. |
|
| LOG_LEVEL | | | debug | Specify log level, one of: trace, debug, info, off. |
|
||||||
|
| LOG_JSON | | | false | Format log output as JSON. |
|
||||||
|
| LOG_COLOR | | | true | Colorize log output. |
|
||||||
|
| CONFIG_FILE | | | config.yml | The config file path. |
|
||||||
|
|
||||||
### Sample yaml configuration
|
### Sample yaml configuration
|
||||||
config.yml:
|
config.yml:
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
@@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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>
|
||||||
|
@@ -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
35
cmd/hc/main.go
Normal 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
|
||||||
|
}
|
@@ -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":
|
||||||
|
@@ -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
8
go.mod
@@ -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
21
go.sum
@@ -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=
|
||||||
|
@@ -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 {
|
||||||
|
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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.
|
||||||
@@ -21,7 +22,7 @@ func BroadcastAddr(n *net.IPNet) net.IP {
|
|||||||
return broadcast
|
return broadcast
|
||||||
}
|
}
|
||||||
|
|
||||||
// http://play.golang.org/p/m8TNTtygK0
|
// http://play.golang.org/p/m8TNTtygK0
|
||||||
func IncreaseIP(ip net.IP) {
|
func IncreaseIP(ip net.IP) {
|
||||||
for j := len(ip) - 1; j >= 0; j-- {
|
for j := len(ip) - 1; j >= 0; j-- {
|
||||||
ip[j]++
|
ip[j]++
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
@@ -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"`
|
||||||
|
@@ -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 {
|
||||||
|
|
||||||
|
@@ -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()})
|
||||||
|
@@ -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"
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -135,16 +135,16 @@ func (s *Server) GetUserIndex(c *gin.Context) {
|
|||||||
peers := s.peers.GetSortedPeersForEmail(currentSession.SortedBy["userpeers"], currentSession.SortDirection["userpeers"], currentSession.Email)
|
peers := s.peers.GetSortedPeersForEmail(currentSession.SortedBy["userpeers"], currentSession.SortDirection["userpeers"], currentSession.Email)
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "user_index.html", gin.H{
|
c.HTML(http.StatusOK, "user_index.html", gin.H{
|
||||||
"Route": c.Request.URL.Path,
|
"Route": c.Request.URL.Path,
|
||||||
"Alerts": GetFlashes(c),
|
"Alerts": GetFlashes(c),
|
||||||
"Session": currentSession,
|
"Session": currentSession,
|
||||||
"Static": s.getStaticData(),
|
"Static": s.getStaticData(),
|
||||||
"Peers": peers,
|
"Peers": peers,
|
||||||
"TotalPeers": len(peers),
|
"TotalPeers": len(peers),
|
||||||
"Users": []users.User{*s.users.GetUser(currentSession.Email)},
|
"Users": []users.User{*s.users.GetUser(currentSession.Email)},
|
||||||
"Device": s.peers.GetDevice(currentSession.DeviceName),
|
"Device": s.peers.GetDevice(currentSession.DeviceName),
|
||||||
"DeviceNames": s.GetDeviceNames(),
|
"DeviceNames": s.GetDeviceNames(),
|
||||||
"UserManagePeers": s.config.WG.UserManagePeers,
|
"UserManagePeers": s.config.WG.UserManagePeers,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
var Version = "testbuild"
|
var Version = "testbuild"
|
||||||
var DatabaseVersion = "1.0.8"
|
var DatabaseVersion = "1.0.9"
|
||||||
|
@@ -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
|
||||||
|
@@ -3,11 +3,11 @@ package wireguard
|
|||||||
import "github.com/h44z/wg-portal/internal/common"
|
import "github.com/h44z/wg-portal/internal/common"
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DeviceNames []string `yaml:"devices" envconfig:"WG_DEVICES"` // managed devices
|
DeviceNames []string `yaml:"devices" envconfig:"WG_DEVICES"` // managed devices
|
||||||
DefaultDeviceName string `yaml:"defaultDevice" envconfig:"WG_DEFAULT_DEVICE"` // this device is used for auto-created peers, use GetDefaultDeviceName() to access this field
|
DefaultDeviceName string `yaml:"defaultDevice" envconfig:"WG_DEFAULT_DEVICE"` // this device is used for auto-created peers, use GetDefaultDeviceName() to access this field
|
||||||
ConfigDirectoryPath string `yaml:"configDirectory" envconfig:"WG_CONFIG_PATH"` // optional, if set, updates will be written to this path, filename: <devicename>.conf
|
ConfigDirectoryPath string `yaml:"configDirectory" envconfig:"WG_CONFIG_PATH"` // optional, if set, updates will be written to this path, filename: <devicename>.conf
|
||||||
ManageIPAddresses bool `yaml:"manageIPAddresses" envconfig:"MANAGE_IPS"` // handle ip-address setup of interface
|
ManageIPAddresses bool `yaml:"manageIPAddresses" envconfig:"MANAGE_IPS"` // handle ip-address setup of interface
|
||||||
UserManagePeers bool `yaml:"userManagePeers" envconfig:"USER_MANAGE_PEERS"` // user can manage own peers
|
UserManagePeers bool `yaml:"userManagePeers" envconfig:"USER_MANAGE_PEERS"` // user can manage own peers
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Config) GetDefaultDeviceName() string {
|
func (c Config) GetDefaultDeviceName() string {
|
||||||
|
@@ -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,15 +116,19 @@ 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"`
|
||||||
CreatedBy string
|
DeactivatedReason string `json:",omitempty"`
|
||||||
UpdatedBy string
|
|
||||||
CreatedAt time.Time
|
ExpiresAt *time.Time `json:",omitempty" form:"expires_at" binding:"omitempty" time_format:"2006-01-02"`
|
||||||
UpdatedAt time.Time
|
|
||||||
|
CreatedBy string
|
||||||
|
UpdatedBy string
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Peer) SetIPAddresses(addresses ...string) {
|
func (p *Peer) SetIPAddresses(addresses ...string) {
|
||||||
@@ -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
|
||||||
|
Reference in New Issue
Block a user