parent
929034a0b1
commit
067715f48a
|
|
@ -6,9 +6,13 @@ on:
|
|||
pull_request:
|
||||
branches: [main, master]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
|
|
@ -66,6 +70,8 @@ jobs:
|
|||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-and-test
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
|
|
@ -84,6 +90,7 @@ jobs:
|
|||
needs: build-and-test
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Finalize Coveralls
|
||||
uses: coverallsapp/github-action@v2
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ on:
|
|||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
|
|
@ -16,6 +14,8 @@ env:
|
|||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
|
|
@ -48,6 +48,9 @@ jobs:
|
|||
docker:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
|
|
@ -87,7 +90,7 @@ jobs:
|
|||
with:
|
||||
push: true
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
|
|
@ -100,6 +103,8 @@ jobs:
|
|||
binaries:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
|
@ -108,17 +113,10 @@ jobs:
|
|||
goarch: amd64
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
- goos: linux
|
||||
goarch: arm
|
||||
goarm: "7"
|
||||
- goos: linux
|
||||
goarch: "386"
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- goos: darwin
|
||||
goarch: arm64
|
||||
- goos: freebsd
|
||||
goarch: amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
|
|
@ -142,17 +140,17 @@ jobs:
|
|||
CGO_ENABLED: "0"
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
run: |
|
||||
BINARY_NAME="wireguard-ui-${{ matrix.goos }}-${{ matrix.goarch }}"
|
||||
if [ -n "${{ matrix.goarm }}" ]; then
|
||||
BINARY_NAME="${BINARY_NAME}v${{ matrix.goarm }}"
|
||||
fi
|
||||
go build -ldflags="-X 'main.appVersion=${{ github.ref_name }}' -X 'main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)' -X 'main.gitCommit=${{ github.sha }}'" \
|
||||
-o "${BINARY_NAME}" .
|
||||
tar czf "${BINARY_NAME}.tar.gz" "${BINARY_NAME}" init.sh
|
||||
ARCHIVE="wireguard-ui-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz"
|
||||
go build -trimpath \
|
||||
-ldflags="-s -w -X 'main.appVersion=${{ github.ref_name }}' -X 'main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)' -X 'main.gitCommit=${{ github.sha }}'" \
|
||||
-o wireguard-ui .
|
||||
tar czf "${ARCHIVE}" wireguard-ui
|
||||
sha256sum "${ARCHIVE}" >> checksums.txt
|
||||
|
||||
- name: Upload release asset
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
files: wireguard-ui-*.tar.gz
|
||||
files: |
|
||||
wireguard-ui-*.tar.gz
|
||||
checksums.txt
|
||||
|
|
|
|||
|
|
@ -32,8 +32,9 @@ COPY . .
|
|||
COPY --from=frontend /src/assets ./assets/
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
|
||||
-trimpath \
|
||||
-ldflags="-s -w -X 'main.appVersion=${APP_VERSION}' -X 'main.buildTime=${BUILD_TIME}' -X 'main.gitCommit=${GIT_COMMIT}'" \
|
||||
-a -o wg-ui .
|
||||
-o wg-ui .
|
||||
|
||||
# Stage 3: Runtime
|
||||
FROM alpine:3.23
|
||||
|
|
|
|||
4
Makefile
4
Makefile
|
|
@ -5,7 +5,7 @@ GO_PACKAGES := $(shell go list ./... | grep -v 'wireguard-ui$$' | grep -v node_m
|
|||
VERSION ?= dev
|
||||
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "N/A")
|
||||
BUILD_TIME := $(shell date -u '+%Y-%m-%d %H:%M:%S')
|
||||
LDFLAGS := -X 'main.appVersion=$(VERSION)' -X 'main.buildTime=$(BUILD_TIME)' -X 'main.gitCommit=$(GIT_COMMIT)'
|
||||
LDFLAGS := -s -w -X 'main.appVersion=$(VERSION)' -X 'main.buildTime=$(BUILD_TIME)' -X 'main.gitCommit=$(GIT_COMMIT)'
|
||||
|
||||
.PHONY: help build build-frontend build-backend test test-verbose coverage lint lint-go lint-frontend fmt vet clean dev
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ build-frontend: ## Build the React frontend
|
|||
npm ci && npm run build
|
||||
|
||||
build-backend: ## Build the Go binary
|
||||
CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o $(APP_NAME) .
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags="$(LDFLAGS)" -o $(APP_NAME) .
|
||||
|
||||
## ---- Test ----
|
||||
|
||||
|
|
|
|||
169
README.md
169
README.md
|
|
@ -1,28 +1,26 @@
|
|||
# digitaltolk/wireguard-ui
|
||||
|
||||
[](https://github.com/DigitalTolk/wireguard-ui/actions/workflows/ci.yml)
|
||||
[](https://coveralls.io/github/DigitalTolk/wireguard-ui)
|
||||
|
||||
A web user interface to manage your WireGuard setup.
|
||||
A modern web interface to manage your WireGuard VPN setup.
|
||||
|
||||
Fork of [ngoduykhanh/wireguard-ui](https://github.com/ngoduykhanh/wireguard-ui).
|
||||
Fork of [ngoduykhanh/wireguard-ui](https://github.com/ngoduykhanh/wireguard-ui) by [Khanh Ngo](https://github.com/ngoduykhanh).
|
||||
|
||||
## Features
|
||||
|
||||
- Modern React frontend built with shadcn/ui components (WCAG accessible)
|
||||
- Single Sign-On via OpenID Connect (OIDC)
|
||||
- Audit logging of all administrative actions
|
||||
- SQLite database backend
|
||||
- Client management with QR codes, email delivery, and Excel export
|
||||
- Multi-platform Docker images (amd64, arm64, armv7)
|
||||
- Modern React frontend with [shadcn/ui](https://ui.shadcn.com/) components
|
||||
- WCAG accessible, auto dark mode, mobile-friendly
|
||||
- Single Sign-On via OpenID Connect (Microsoft Entra ID / any OIDC provider)
|
||||
- Audit logging with Excel export (ISO 27001 evidence)
|
||||
- SQLite database (pure Go, no CGO)
|
||||
- Client management: QR codes, config download, email delivery
|
||||
- Server-side search and filtering with bookmarkable URLs
|
||||
- Input validation (frontend + backend)
|
||||
- Multi-platform Docker images (linux/amd64, linux/arm64)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Docker
|
||||
|
||||
```sh
|
||||
docker pull digitaltolk/wireguard-ui
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```yaml
|
||||
|
|
@ -34,51 +32,156 @@ services:
|
|||
- NET_ADMIN
|
||||
network_mode: host
|
||||
environment:
|
||||
- WGUI_USERNAME=admin
|
||||
- WGUI_PASSWORD=admin
|
||||
- OIDC_ISSUER_URL=https://login.microsoftonline.com/{tenant-id}/v2.0
|
||||
- OIDC_CLIENT_ID=your-app-client-id
|
||||
- OIDC_CLIENT_SECRET=your-app-client-secret
|
||||
- OIDC_REDIRECT_URL=https://vpn.example.com/api/v1/auth/oidc/callback
|
||||
- SESSION_SECRET=change-me-to-a-random-string
|
||||
- WGUI_MANAGE_START=true
|
||||
- WGUI_MANAGE_RESTART=true
|
||||
volumes:
|
||||
- ./db:/app/db
|
||||
- /etc/wireguard:/etc/wireguard
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
> The default username and password are `admin`. Change them to secure your setup.
|
||||
### Docker
|
||||
|
||||
See [`examples/docker-compose`](examples/docker-compose) for more configurations
|
||||
(LinuxServer, BoringTun, host networking).
|
||||
```sh
|
||||
docker pull digitaltolk/wireguard-ui:latest
|
||||
# or from GitHub Container Registry:
|
||||
docker pull ghcr.io/digitaltolk/wireguard-ui:latest
|
||||
```
|
||||
|
||||
### Binary
|
||||
|
||||
Download the binary from the [Releases](https://github.com/DigitalTolk/wireguard-ui/releases) page:
|
||||
Download from [Releases](https://github.com/DigitalTolk/wireguard-ui/releases):
|
||||
|
||||
```sh
|
||||
./wireguard-ui
|
||||
tar xzf wireguard-ui-linux-amd64.tar.gz -C /usr/local/bin
|
||||
wireguard-ui
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Refer to the environment variable tables in the upstream documentation or inspect
|
||||
the source for a full list. Key variables:
|
||||
### Application
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `BIND_ADDRESS` | Listen address | `0.0.0.0:80` |
|
||||
| `SESSION_SECRET` | Secret for session cookies | N/A |
|
||||
| `WGUI_USERNAME` | Initial admin username | `admin` |
|
||||
| `WGUI_PASSWORD` | Initial admin password | `admin` |
|
||||
| `WGUI_CONFIG_FILE_PATH` | WireGuard config path | `/etc/wireguard/wg0.conf` |
|
||||
| `WGUI_MANAGE_START` | Start WireGuard with container | `false` |
|
||||
| `WGUI_MANAGE_RESTART` | Restart WireGuard on config apply | `false` |
|
||||
| `BIND_ADDRESS` | Listen address and port | `0.0.0.0:5000` |
|
||||
| `BASE_PATH` | URL base path (for reverse proxy) | `` |
|
||||
| `SESSION_SECRET` | Secret key for session cookies | random |
|
||||
| `SESSION_SECRET_FILE` | File containing session secret | |
|
||||
| `SESSION_MAX_DURATION` | Max session lifetime in days | `90` |
|
||||
| `DISABLE_LOGIN` | Disable authentication (development only) | `false` |
|
||||
| `WGUI_LOG_LEVEL` | Log level: DEBUG, INFO, WARN, ERROR, OFF | `INFO` |
|
||||
| `WGUI_FAVICON_FILE_PATH` | Custom favicon file path | |
|
||||
|
||||
## Build
|
||||
### OIDC / SSO (required for production)
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `OIDC_ISSUER_URL` | OIDC provider URL (e.g. `https://login.microsoftonline.com/{tenant}/v2.0`) | |
|
||||
| `OIDC_CLIENT_ID` | OAuth2 client ID | |
|
||||
| `OIDC_CLIENT_SECRET` | OAuth2 client secret | |
|
||||
| `OIDC_CLIENT_SECRET_FILE` | File containing client secret | |
|
||||
| `OIDC_REDIRECT_URL` | Callback URL (e.g. `https://vpn.example.com/api/v1/auth/oidc/callback`) | |
|
||||
| `OIDC_SCOPES` | Comma-separated scopes | `openid,profile,email` |
|
||||
| `OIDC_AUTO_PROVISION` | Auto-create users on first OIDC login | `true` |
|
||||
| `OIDC_ADMIN_GROUPS` | Comma-separated group UUIDs for auto-admin | |
|
||||
|
||||
### WireGuard Server
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `WGUI_ENDPOINT_ADDRESS` | Public endpoint address for clients | auto-detected |
|
||||
| `WGUI_SERVER_INTERFACE_ADDRESSES` | Server interface CIDR addresses | `10.252.1.0/24` |
|
||||
| `WGUI_SERVER_LISTEN_PORT` | WireGuard listen port | `51820` |
|
||||
| `WGUI_SERVER_POST_UP_SCRIPT` | Post-up script | |
|
||||
| `WGUI_SERVER_POST_DOWN_SCRIPT` | Post-down script | |
|
||||
| `WGUI_DNS` | DNS servers pushed to clients (comma-separated) | `1.1.1.1` |
|
||||
| `WGUI_MTU` | MTU size | `1450` |
|
||||
| `WGUI_PERSISTENT_KEEPALIVE` | Keepalive interval in seconds | `15` |
|
||||
| `WGUI_FIREWALL_MARK` | Firewall mark (hex) | `0xca6c` |
|
||||
| `WGUI_TABLE` | Routing table | `auto` |
|
||||
| `WGUI_CONFIG_FILE_PATH` | Path to write wg0.conf | `/etc/wireguard/wg0.conf` |
|
||||
| `WG_CONF_TEMPLATE` | Custom wg.conf template path | built-in |
|
||||
| `SUBNET_RANGES` | Named subnet ranges (e.g. `LAN:10.0.0.0/24;REMOTE:192.168.0.0/24`) | |
|
||||
|
||||
### Client Defaults
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `WGUI_DEFAULT_CLIENT_ALLOWED_IPS` | Default allowed IPs for new clients | `0.0.0.0/0` |
|
||||
| `WGUI_DEFAULT_CLIENT_EXTRA_ALLOWED_IPS` | Default extra allowed IPs | |
|
||||
| `WGUI_DEFAULT_CLIENT_USE_SERVER_DNS` | Use server DNS by default | `true` |
|
||||
| `WGUI_DEFAULT_CLIENT_ENABLE_AFTER_CREATION` | Enable client after creation | `true` |
|
||||
|
||||
### Email (SMTP)
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `SMTP_HOSTNAME` | SMTP server hostname | `127.0.0.1` |
|
||||
| `SMTP_PORT` | SMTP server port | `25` |
|
||||
| `SMTP_USERNAME` | SMTP username | |
|
||||
| `SMTP_PASSWORD` | SMTP password | |
|
||||
| `SMTP_PASSWORD_FILE` | File containing SMTP password | |
|
||||
| `SMTP_HELO` | SMTP HELO hostname | `localhost` |
|
||||
| `SMTP_ENCRYPTION` | NONE, SSL, SSLTLS, TLS, STARTTLS | `STARTTLS` |
|
||||
| `SMTP_AUTH_TYPE` | PLAIN, LOGIN, NONE | `NONE` |
|
||||
| `EMAIL_FROM_ADDRESS` | Sender email address | |
|
||||
| `EMAIL_FROM_NAME` | Sender display name | `WireGuard UI` |
|
||||
|
||||
### Email (SendGrid)
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `SENDGRID_API_KEY` | SendGrid API key | |
|
||||
| `SENDGRID_API_KEY_FILE` | File containing SendGrid API key | |
|
||||
|
||||
### Container Management
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `WGUI_MANAGE_START` | Start WireGuard when container starts | `false` |
|
||||
| `WGUI_MANAGE_RESTART` | Restart WireGuard when config changes | `false` |
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
# Build everything (frontend + Go binary)
|
||||
# Install dependencies
|
||||
make deps && make deps-frontend
|
||||
|
||||
# Start Go backend (port 5000)
|
||||
make dev
|
||||
|
||||
# Start frontend dev server with hot reload (port 5173)
|
||||
make dev-frontend
|
||||
|
||||
# Run all tests
|
||||
make test
|
||||
|
||||
# Run linters
|
||||
make lint
|
||||
|
||||
# Build everything
|
||||
make build
|
||||
|
||||
# Build Docker image
|
||||
docker build -t wireguard-ui .
|
||||
make docker-build
|
||||
```
|
||||
|
||||
### Available Make Targets
|
||||
|
||||
```
|
||||
make help Show all commands
|
||||
make build Build frontend + Go binary
|
||||
make test Run all tests with coverage
|
||||
make lint Run Go + frontend linters
|
||||
make dev Start Go backend
|
||||
make dev-frontend Start Vite dev server
|
||||
make docker-build Build Docker image
|
||||
make coverage-html Open coverage report in browser
|
||||
```
|
||||
|
||||
## License
|
||||
|
|
|
|||
|
|
@ -80,14 +80,13 @@ func TestAddressField_WithoutName(t *testing.T) {
|
|||
// --- NewSmtpMail tests ---
|
||||
|
||||
func TestNewSmtpMail(t *testing.T) {
|
||||
s := NewSmtpMail("smtp.example.com", 587, "user", "pass", "helo.example.com", true, "PLAIN", "Sender", "sender@example.com", "TLS")
|
||||
s := NewSmtpMail("smtp.example.com", 587, "user", "pass", "helo.example.com", "PLAIN", "Sender", "sender@example.com", "TLS")
|
||||
|
||||
assert.Equal(t, "smtp.example.com", s.hostname)
|
||||
assert.Equal(t, 587, s.port)
|
||||
assert.Equal(t, "user", s.username)
|
||||
assert.Equal(t, "pass", s.password)
|
||||
assert.Equal(t, "helo.example.com", s.smtpHelo)
|
||||
assert.True(t, s.noTLSCheck)
|
||||
assert.Equal(t, mail.AuthPlain, s.authType)
|
||||
assert.Equal(t, mail.EncryptionTLS, s.encryption)
|
||||
assert.Equal(t, "Sender", s.fromName)
|
||||
|
|
@ -95,11 +94,10 @@ func TestNewSmtpMail(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewSmtpMail_Defaults(t *testing.T) {
|
||||
s := NewSmtpMail("host", 25, "", "", "", false, "", "", "from@test.com", "")
|
||||
s := NewSmtpMail("host", 25, "", "", "", "", "", "from@test.com", "")
|
||||
|
||||
assert.Equal(t, mail.AuthNone, s.authType)
|
||||
assert.Equal(t, mail.EncryptionSTARTTLS, s.encryption)
|
||||
assert.False(t, s.noTLSCheck)
|
||||
}
|
||||
|
||||
// --- NewSendgridApiMail tests ---
|
||||
|
|
@ -123,14 +121,14 @@ func TestNewSendgridApiMail_Empty(t *testing.T) {
|
|||
// --- SmtpMail.Send tests (connection error path) ---
|
||||
|
||||
func TestSmtpMail_Send_ConnectionError(t *testing.T) {
|
||||
s := NewSmtpMail("127.0.0.1", 1, "user", "pass", "", false, "PLAIN", "Sender", "from@test.com", "NONE")
|
||||
s := NewSmtpMail("127.0.0.1", 1, "user", "pass", "", "PLAIN", "Sender", "from@test.com", "NONE")
|
||||
|
||||
err := s.Send("Recipient", "to@test.com", "Subject", "<p>Body</p>", nil)
|
||||
assert.Error(t, err, "Send should fail when SMTP server is unreachable")
|
||||
}
|
||||
|
||||
func TestSmtpMail_Send_ConnectionError_WithTLSCheck(t *testing.T) {
|
||||
s := NewSmtpMail("127.0.0.1", 1, "", "", "helo.test", true, "LOGIN", "From Name", "from@test.com", "SSL")
|
||||
func TestSmtpMail_Send_ConnectionError_SSL(t *testing.T) {
|
||||
s := NewSmtpMail("127.0.0.1", 1, "", "", "helo.test", "LOGIN", "From Name", "from@test.com", "SSL")
|
||||
|
||||
err := s.Send("Recipient", "to@test.com", "Subject", "Body", []Attachment{
|
||||
{Name: "test.txt", Data: []byte("hello")},
|
||||
|
|
@ -139,7 +137,7 @@ func TestSmtpMail_Send_ConnectionError_WithTLSCheck(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSmtpMail_Send_ConnectionError_STARTTLS(t *testing.T) {
|
||||
s := NewSmtpMail("127.0.0.1", 1, "", "", "", false, "", "", "from@test.com", "STARTTLS")
|
||||
s := NewSmtpMail("127.0.0.1", 1, "", "", "", "", "", "from@test.com", "STARTTLS")
|
||||
|
||||
err := s.Send("", "to@test.com", "Test", "Content", nil)
|
||||
assert.Error(t, err)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package emailer
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -17,7 +16,6 @@ type SmtpMail struct {
|
|||
smtpHelo string
|
||||
authType mail.AuthType
|
||||
encryption mail.Encryption
|
||||
noTLSCheck bool
|
||||
fromName string
|
||||
from string
|
||||
}
|
||||
|
|
@ -48,8 +46,8 @@ func encryptionType(encryptionType string) mail.Encryption {
|
|||
}
|
||||
}
|
||||
|
||||
func NewSmtpMail(hostname string, port int, username string, password string, SmtpHelo string, noTLSCheck bool, auth string, fromName, from string, encryption string) *SmtpMail {
|
||||
ans := SmtpMail{hostname: hostname, port: port, username: username, password: password, smtpHelo: SmtpHelo, noTLSCheck: noTLSCheck, fromName: fromName, from: from, authType: authType(auth), encryption: encryptionType(encryption)}
|
||||
func NewSmtpMail(hostname string, port int, username string, password string, SmtpHelo string, auth string, fromName, from string, encryption string) *SmtpMail {
|
||||
ans := SmtpMail{hostname: hostname, port: port, username: username, password: password, smtpHelo: SmtpHelo, fromName: fromName, from: from, authType: authType(auth), encryption: encryptionType(encryption)}
|
||||
return &ans
|
||||
}
|
||||
|
||||
|
|
@ -74,10 +72,6 @@ func (o *SmtpMail) Send(toName string, to string, subject string, content string
|
|||
server.ConnectTimeout = 10 * time.Second
|
||||
server.SendTimeout = 10 * time.Second
|
||||
|
||||
if o.noTLSCheck {
|
||||
server.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
smtpClient, err := server.Connect()
|
||||
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
boringtun:
|
||||
image: ghcr.io/ntkme/boringtun:edge
|
||||
command:
|
||||
- wg0
|
||||
container_name: boringtun
|
||||
# use the network of the 'wireguard-ui' service. this enables to show active clients in the status page
|
||||
network_mode: service:wireguard-ui
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
|
|
@ -20,12 +17,14 @@ services:
|
|||
cap_add:
|
||||
- NET_ADMIN
|
||||
environment:
|
||||
- OIDC_ISSUER_URL=https://login.microsoftonline.com/{tenant-id}/v2.0
|
||||
- OIDC_CLIENT_ID=your-client-id
|
||||
- OIDC_CLIENT_SECRET=your-client-secret
|
||||
- OIDC_REDIRECT_URL=https://vpn.example.com/api/v1/auth/oidc/callback
|
||||
- SESSION_SECRET=change-me
|
||||
- SENDGRID_API_KEY
|
||||
- EMAIL_FROM_ADDRESS
|
||||
- EMAIL_FROM_NAME
|
||||
- SESSION_SECRET
|
||||
- WGUI_USERNAME=admin
|
||||
- WGUI_PASSWORD=admin
|
||||
- WG_CONF_TEMPLATE
|
||||
- WGUI_MANAGE_START=true
|
||||
- WGUI_MANAGE_RESTART=true
|
||||
|
|
@ -37,7 +36,6 @@ services:
|
|||
- ./db:/app/db
|
||||
- ./config:/etc/wireguard
|
||||
ports:
|
||||
# port for wireguard-ui
|
||||
- "5000:5000"
|
||||
# port of the wireguard server. this must be set here as the `boringtun` container joins the network of this container and hasn't its own network over which it could publish the ports
|
||||
- "51820:51820/udp"
|
||||
restart: unless-stopped
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
wireguard:
|
||||
image: linuxserver/wireguard:latest
|
||||
|
|
@ -9,9 +7,7 @@ services:
|
|||
volumes:
|
||||
- ./config:/config
|
||||
ports:
|
||||
# port for wireguard-ui. this must be set here as the `wireguard-ui` container joins the network of this container and hasn't its own network over which it could publish the ports
|
||||
- "5000:5000"
|
||||
# port of the wireguard server
|
||||
- "51820:51820/udp"
|
||||
|
||||
wireguard-ui:
|
||||
|
|
@ -21,15 +17,16 @@ services:
|
|||
- wireguard
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
# use the network of the 'wireguard' service. this enables to show active clients in the status page
|
||||
network_mode: service:wireguard
|
||||
environment:
|
||||
- OIDC_ISSUER_URL=https://login.microsoftonline.com/{tenant-id}/v2.0
|
||||
- OIDC_CLIENT_ID=your-client-id
|
||||
- OIDC_CLIENT_SECRET=your-client-secret
|
||||
- OIDC_REDIRECT_URL=https://vpn.example.com/api/v1/auth/oidc/callback
|
||||
- SESSION_SECRET=change-me
|
||||
- SENDGRID_API_KEY
|
||||
- EMAIL_FROM_ADDRESS
|
||||
- EMAIL_FROM_NAME
|
||||
- SESSION_SECRET
|
||||
- WGUI_USERNAME=admin
|
||||
- WGUI_PASSWORD=admin
|
||||
- WG_CONF_TEMPLATE
|
||||
- WGUI_MANAGE_START=true
|
||||
- WGUI_MANAGE_RESTART=true
|
||||
|
|
@ -40,3 +37,4 @@ services:
|
|||
volumes:
|
||||
- ./db:/app/db
|
||||
- ./config:/etc/wireguard
|
||||
restart: unless-stopped
|
||||
|
|
|
|||
|
|
@ -1,23 +1,22 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
wireguard-ui:
|
||||
image: digitaltolk/wireguard-ui:latest
|
||||
container_name: wireguard-ui
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
# required to show active clients. with this set, you don't need to expose the ui port (5000) anymore
|
||||
network_mode: host
|
||||
environment:
|
||||
- OIDC_ISSUER_URL=https://login.microsoftonline.com/{tenant-id}/v2.0
|
||||
- OIDC_CLIENT_ID=your-client-id
|
||||
- OIDC_CLIENT_SECRET=your-client-secret
|
||||
- OIDC_REDIRECT_URL=https://vpn.example.com/api/v1/auth/oidc/callback
|
||||
- SESSION_SECRET=change-me
|
||||
- SENDGRID_API_KEY
|
||||
- EMAIL_FROM_ADDRESS
|
||||
- EMAIL_FROM_NAME
|
||||
- SESSION_SECRET
|
||||
- WGUI_USERNAME=admin
|
||||
- WGUI_PASSWORD=admin
|
||||
- WG_CONF_TEMPLATE
|
||||
- WGUI_MANAGE_START=false
|
||||
- WGUI_MANAGE_RESTART=false
|
||||
- WGUI_MANAGE_START=true
|
||||
- WGUI_MANAGE_RESTART=true
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
|
|
@ -25,3 +24,4 @@ services:
|
|||
volumes:
|
||||
- ./db:/app/db
|
||||
- /etc/wireguard:/etc/wireguard
|
||||
restart: unless-stopped
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -13,7 +13,7 @@ require (
|
|||
github.com/sendgrid/sendgrid-go v3.16.1+incompatible
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/xhit/go-simple-mail/v2 v2.16.0
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
//golang.zx2c4.com/wireguard v0.0.20200121 // indirect
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||
gopkg.in/go-playground/validator.v9 v9.31.0
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/DigitalTolk/wireguard-ui/model"
|
||||
"github.com/DigitalTolk/wireguard-ui/util"
|
||||
)
|
||||
|
||||
|
|
@ -176,6 +178,10 @@ func TestAPIGetMe_WithSession(t *testing.T) {
|
|||
env := setupTestEnv(t)
|
||||
util.DisableLogin = false
|
||||
|
||||
// Create the user that the session will reference
|
||||
now := time.Now().UTC()
|
||||
env.db.SaveUser(model.User{Username: "admin", Email: "admin@test.com", Admin: true, CreatedAt: now, UpdatedAt: now})
|
||||
|
||||
// Create a route that first creates a session and then calls APIGetMe
|
||||
env.echo.GET("/setup-and-getme", func(c echo.Context) error {
|
||||
// Create a session for admin user
|
||||
|
|
|
|||
|
|
@ -130,6 +130,10 @@ func TestFindOrCreateOIDCUser_NewUser_AdminViaGroups(t *testing.T) {
|
|||
|
||||
func TestFindOrCreateOIDCUser_NewUser_NotAdmin(t *testing.T) {
|
||||
env := setupTestEnv(t)
|
||||
now := time.Now().UTC()
|
||||
|
||||
// pre-create a user so the next one isn't the first (first user auto-gets admin)
|
||||
env.db.SaveUser(model.User{Username: "existing", OIDCSub: "sub-existing", Admin: true, CreatedAt: now, UpdatedAt: now})
|
||||
|
||||
origAutoProvision := util.OIDCAutoProvision
|
||||
util.OIDCAutoProvision = true
|
||||
|
|
|
|||
5
main.go
5
main.go
|
|
@ -39,7 +39,6 @@ var (
|
|||
flagSmtpUsername string
|
||||
flagSmtpPassword string
|
||||
flagSmtpAuthType = "NONE"
|
||||
flagSmtpNoTLSCheck = false
|
||||
flagSmtpEncryption = "STARTTLS"
|
||||
flagSmtpHelo = "localhost"
|
||||
flagSendgridApiKey string
|
||||
|
|
@ -78,7 +77,6 @@ func init() {
|
|||
flag.IntVar(&flagSmtpPort, "smtp-port", util.LookupEnvOrInt("SMTP_PORT", flagSmtpPort), "SMTP Port")
|
||||
flag.StringVar(&flagSmtpHelo, "smtp-helo", util.LookupEnvOrString("SMTP_HELO", flagSmtpHelo), "SMTP HELO Hostname")
|
||||
flag.StringVar(&flagSmtpUsername, "smtp-username", util.LookupEnvOrString("SMTP_USERNAME", flagSmtpUsername), "SMTP Username")
|
||||
flag.BoolVar(&flagSmtpNoTLSCheck, "smtp-no-tls-check", util.LookupEnvOrBool("SMTP_NO_TLS_CHECK", flagSmtpNoTLSCheck), "Disable TLS verification for SMTP. This is potentially dangerous.")
|
||||
flag.StringVar(&flagSmtpEncryption, "smtp-encryption", util.LookupEnvOrString("SMTP_ENCRYPTION", flagSmtpEncryption), "SMTP Encryption : NONE, SSL, SSLTLS, TLS or STARTTLS (by default)")
|
||||
flag.StringVar(&flagSmtpAuthType, "smtp-auth-type", util.LookupEnvOrString("SMTP_AUTH_TYPE", flagSmtpAuthType), "SMTP Auth Type : PLAIN, LOGIN or NONE.")
|
||||
flag.StringVar(&flagEmailFrom, "email-from", util.LookupEnvOrString("EMAIL_FROM_ADDRESS", flagEmailFrom), "'From' email address.")
|
||||
|
|
@ -122,7 +120,6 @@ func init() {
|
|||
util.SmtpUsername = flagSmtpUsername
|
||||
util.SmtpPassword = flagSmtpPassword
|
||||
util.SmtpAuthType = flagSmtpAuthType
|
||||
util.SmtpNoTLSCheck = flagSmtpNoTLSCheck
|
||||
util.SmtpEncryption = flagSmtpEncryption
|
||||
util.SendgridApiKey = flagSendgridApiKey
|
||||
util.EmailFrom = flagEmailFrom
|
||||
|
|
@ -214,7 +211,7 @@ func main() {
|
|||
if util.SendgridApiKey != "" {
|
||||
sendmail = emailer.NewSendgridApiMail(util.SendgridApiKey, util.EmailFromName, util.EmailFrom)
|
||||
} else {
|
||||
sendmail = emailer.NewSmtpMail(util.SmtpHostname, util.SmtpPort, util.SmtpUsername, util.SmtpPassword, util.SmtpHelo, util.SmtpNoTLSCheck, util.SmtpAuthType, util.EmailFromName, util.EmailFrom, util.SmtpEncryption)
|
||||
sendmail = emailer.NewSmtpMail(util.SmtpHostname, util.SmtpPort, util.SmtpUsername, util.SmtpPassword, util.SmtpHelo, util.SmtpAuthType, util.EmailFromName, util.EmailFrom, util.SmtpEncryption)
|
||||
}
|
||||
|
||||
// set up Echo with session middleware
|
||||
|
|
|
|||
|
|
@ -4,14 +4,11 @@ import "time"
|
|||
|
||||
// User model
|
||||
type User struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
// PasswordHash takes precedence over Password.
|
||||
PasswordHash string `json:"password_hash,omitempty"`
|
||||
Email string `json:"email"`
|
||||
DisplayName string `json:"display_name"`
|
||||
OIDCSub string `json:"oidc_sub,omitempty"`
|
||||
Admin bool `json:"admin"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
DisplayName string `json:"display_name"`
|
||||
OIDCSub string `json:"oidc_sub,omitempty"`
|
||||
Admin bool `json:"admin"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,23 +134,7 @@ func (o *SqliteDB) Init() error {
|
|||
o.db.Exec(`INSERT INTO hashes (id, client, server) VALUES (1, 'none', 'none')`)
|
||||
}
|
||||
|
||||
// default user
|
||||
var userCount int
|
||||
o.db.QueryRow("SELECT COUNT(*) FROM users").Scan(&userCount)
|
||||
if userCount == 0 {
|
||||
username := util.LookupEnvOrString(util.UsernameEnvVar, util.DefaultUsername)
|
||||
now := time.Now().UTC()
|
||||
|
||||
_, err := o.db.Exec(
|
||||
`INSERT INTO users (username, admin, created_at, updated_at) VALUES (?, ?, ?, ?)`,
|
||||
username, true, now, now,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create default user: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// init caches
|
||||
// init caches (first OIDC login auto-provisions admin user)
|
||||
users, err := o.GetUsers()
|
||||
if err == nil {
|
||||
util.DBUsersToCRC32Mutex.Lock()
|
||||
|
|
|
|||
|
|
@ -423,10 +423,10 @@ func TestInit_CreatesDefaults(t *testing.T) {
|
|||
err := db.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
// should have created default user
|
||||
// no default user — first OIDC login creates admin
|
||||
users, err := db.GetUsers()
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(users), 1)
|
||||
assert.Len(t, users, 0)
|
||||
|
||||
// should have created server config
|
||||
server, err := db.GetServer()
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ var (
|
|||
SmtpPort int
|
||||
SmtpUsername string
|
||||
SmtpPassword string
|
||||
SmtpNoTLSCheck bool
|
||||
SmtpEncryption string
|
||||
SmtpAuthType string
|
||||
SmtpHelo string
|
||||
|
|
@ -40,9 +39,6 @@ var (
|
|||
)
|
||||
|
||||
const (
|
||||
DefaultUsername = "admin"
|
||||
DefaultPassword = "admin"
|
||||
DefaultIsAdmin = true
|
||||
DefaultServerAddress = "10.252.1.0/24"
|
||||
DefaultServerPort = 51820
|
||||
DefaultDNS = "1.1.1.1"
|
||||
|
|
@ -51,11 +47,6 @@ const (
|
|||
DefaultFirewallMark = "0xca6c" // i.e. 51820
|
||||
DefaultTable = "auto"
|
||||
DefaultConfigFilePath = "/etc/wireguard/wg0.conf"
|
||||
UsernameEnvVar = "WGUI_USERNAME"
|
||||
PasswordEnvVar = "WGUI_PASSWORD"
|
||||
PasswordFileEnvVar = "WGUI_PASSWORD_FILE"
|
||||
PasswordHashEnvVar = "WGUI_PASSWORD_HASH"
|
||||
PasswordHashFileEnvVar = "WGUI_PASSWORD_HASH_FILE"
|
||||
FaviconFilePathEnvVar = "WGUI_FAVICON_FILE_PATH"
|
||||
EndpointAddressEnvVar = "WGUI_ENDPOINT_ADDRESS"
|
||||
DNSEnvVar = "WGUI_DNS"
|
||||
|
|
|
|||
32
util/hash.go
32
util/hash.go
|
|
@ -1,32 +0,0 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func HashPassword(plaintext string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(plaintext), 14)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot hash password: %w", err)
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
func VerifyHash(base64Hash string, plaintext string) (bool, error) {
|
||||
hash, err := base64.StdEncoding.DecodeString(base64Hash)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("cannot decode base64 hash: %w", err)
|
||||
}
|
||||
err = bcrypt.CompareHashAndPassword(hash, []byte(plaintext))
|
||||
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("cannot verify password: %w", err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHashPassword(t *testing.T) {
|
||||
hash, err := HashPassword("mypassword")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, hash)
|
||||
|
||||
// hash is base64 encoded
|
||||
assert.Greater(t, len(hash), 20)
|
||||
|
||||
// different calls produce different hashes (bcrypt salt)
|
||||
hash2, err := HashPassword("mypassword")
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, hash, hash2)
|
||||
}
|
||||
|
||||
func TestVerifyHash(t *testing.T) {
|
||||
hash, err := HashPassword("testpassword")
|
||||
require.NoError(t, err)
|
||||
|
||||
match, err := VerifyHash(hash, "testpassword")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, match)
|
||||
|
||||
match, err = VerifyHash(hash, "wrongpassword")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, match)
|
||||
}
|
||||
|
||||
func TestVerifyHash_InvalidBase64(t *testing.T) {
|
||||
_, err := VerifyHash("not-base64!!!", "password")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestVerifyHash_InvalidBcryptHash(t *testing.T) {
|
||||
// Valid base64 but not a valid bcrypt hash
|
||||
_, err := VerifyHash("aGVsbG93b3JsZA==", "password")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestHashPassword_EmptyPassword(t *testing.T) {
|
||||
hash, err := HashPassword("")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, hash)
|
||||
}
|
||||
Loading…
Reference in New Issue