├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── deploy-nightly.yml │ ├── deploy.yml │ ├── lint.yml │ └── stale.yml ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── assets ├── screenshot.png └── wg-easy.sketch ├── docker-compose.dev.yml ├── docker-compose.yml ├── docs └── changelog.json ├── package-lock.json ├── package.json └── src ├── .eslintrc.json ├── .gitignore ├── config.js ├── lib ├── Server.js ├── ServerError.js ├── Util.js └── WireGuard.js ├── package-lock.json ├── package.json ├── server.js ├── services ├── Server.js └── WireGuard.js └── www ├── css └── vendor │ └── tailwind.min.css ├── img ├── apple-touch-icon.png ├── favicon.png └── logo.png ├── index.html ├── js ├── api.js ├── app.js └── vendor │ ├── md5.min.js │ ├── timeago.min.js │ └── vue.min.js └── manifest.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: weejewel 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/workflows/deploy-nightly.yml: -------------------------------------------------------------------------------- 1 | name: Build & Publish Nightly Docker Image to GitHub Container Registry 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 12 * * *" 7 | 8 | jobs: 9 | deploy: 10 | name: Build & Deploy 11 | runs-on: ubuntu-latest 12 | permissions: 13 | packages: write 14 | contents: read 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | ref: production 19 | 20 | - name: Set up QEMU 21 | uses: docker/setup-qemu-action@v1 22 | 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v1 25 | 26 | - name: Login to GitHub Container Registry 27 | uses: docker/login-action@v3 28 | with: 29 | registry: ghcr.io 30 | username: ${{ github.actor }} 31 | password: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | - name: Set environment variables 34 | run: echo RELEASE=$(cat ./src/package.json | jq -r .release) >> $GITHUB_ENV 35 | 36 | - name: Build & Publish Docker Image 37 | uses: docker/build-push-action@v5 38 | with: 39 | push: true 40 | platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8 41 | tags: ghcr.io/wg-easy/wg-easy:nightly, ghcr.io/wg-easy/wg-easy:${{ env.RELEASE }}-nightly 42 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build & Publish Docker Image to GitHub Container Registry 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - production 8 | 9 | jobs: 10 | deploy: 11 | name: Build & Deploy 12 | runs-on: ubuntu-latest 13 | permissions: 14 | packages: write 15 | contents: read 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | ref: production 20 | 21 | - name: Set up QEMU 22 | uses: docker/setup-qemu-action@v1 23 | 24 | - name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v1 26 | 27 | - name: Login to GitHub Container Registry 28 | uses: docker/login-action@v3 29 | with: 30 | registry: ghcr.io 31 | username: ${{ github.actor }} 32 | password: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - name: Set environment variables 35 | run: echo RELEASE=$(cat ./src/package.json | jq -r .release) >> $GITHUB_ENV 36 | 37 | - name: Build & Publish Docker Image 38 | uses: docker/build-push-action@v5 39 | with: 40 | push: true 41 | platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8 42 | tags: ghcr.io/wg-easy/wg-easy:latest, ghcr.io/wg-easy/wg-easy:${{ env.RELEASE }} 43 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - production 8 | pull_request: 9 | 10 | jobs: 11 | lint: 12 | name: Lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: '18' 19 | 20 | - run: | 21 | cd src 22 | npm ci 23 | npm run lint 24 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | workflow_dispatch: 10 | schedule: 11 | - cron: '*/5 * * * *' 12 | 13 | jobs: 14 | stale: 15 | 16 | runs-on: ubuntu-latest 17 | permissions: 18 | issues: write 19 | pull-requests: write 20 | 21 | steps: 22 | - uses: actions/stale@v5 23 | with: 24 | days-before-issue-stale: 14 25 | days-before-issue-close: 7 26 | stale-issue-label: "stale" 27 | stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." 28 | close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." 29 | days-before-pr-stale: 30 30 | days-before-pr-close: 14 31 | stale-pr-message: "This PR is stale because it has been open for 30 days with no activity." 32 | close-pr-message: "This PR was closed because it has been inactive for 14 days since being marked as stale." 33 | repo-token: ${{ secrets.GITHUB_TOKEN }} 34 | operations-per-run: 100 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /config 2 | /wg0.conf 3 | /wg0.json -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/library/node:18-alpine AS build_node_modules 2 | 3 | # Copy Web UI 4 | COPY src/ /app/ 5 | WORKDIR /app 6 | RUN npm ci --production 7 | 8 | # Copy build result to a new image. 9 | # This saves a lot of disk space. 10 | FROM docker.io/library/node:18-alpine 11 | COPY --from=build_node_modules /app /app 12 | 13 | # Move node_modules one directory up, so during development 14 | # we don't have to mount it in a volume. 15 | # This results in much faster reloading! 16 | # 17 | # Also, some node_modules might be native, and 18 | # the architecture & OS of your development machine might differ 19 | # than what runs inside of docker. 20 | RUN mv /app/node_modules /node_modules 21 | 22 | # Install Linux packages 23 | RUN apk add -U --no-cache \ 24 | iptables \ 25 | wireguard-tools \ 26 | dumb-init 27 | 28 | # Expose Ports 29 | EXPOSE 51820/udp 30 | EXPOSE 51821/tcp 31 | 32 | # Set Environment 33 | ENV DEBUG=Server,WireGuard 34 | 35 | # Run Web UI 36 | WORKDIR /app 37 | CMD ["/usr/bin/dumb-init", "node", "server.js"] 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | **You may:** 2 | 3 | * Use this software for yourself; 4 | * Use this software for a company; 5 | * Modify this software, as long as you: 6 | * Publish the changes on GitHub as an open-source & linked fork; 7 | * Don't remove any links to the original project or donation pages; 8 | 9 | **You may not:** 10 | 11 | * Use this software in a commercial product without a license from the original author; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WireGuard Easy 2 | 3 | [](https://github.com/wg-easy/wg-easy/actions/workflows/deploy.yml) 4 | [](https://github.com/wg-easy/wg-easy/actions/workflows/lint.yml) 5 |  6 | [](https://github.com/sponsors/WeeJeWel) 7 |  8 | 9 | You have found the easiest way to install & manage WireGuard on any Linux host! 10 | 11 |
12 |
13 |
50 | $ docker run -d \ 51 | --name=wg-easy \ 52 | -e WG_HOST=🚨YOUR_SERVER_IP \ 53 | -e PASSWORD=🚨YOUR_ADMIN_PASSWORD \ 54 | -v ~/.wg-easy:/etc/wireguard \ 55 | -p 51820:51820/udp \ 56 | -p 51821:51821/tcp \ 57 | --cap-add=NET_ADMIN \ 58 | --cap-add=SYS_MODULE \ 59 | --sysctl="net.ipv4.conf.all.src_valid_mark=1" \ 60 | --sysctl="net.ipv4.ip_forward=1" \ 61 | --restart unless-stopped \ 62 | ghcr.io/wg-easy/wg-easy 63 |64 | 65 | > 💡 Replace `YOUR_SERVER_IP` with your WAN IP, or a Dynamic DNS hostname. 66 | > 67 | > 💡 Replace `YOUR_ADMIN_PASSWORD` with a password to log in on the Web UI. 68 | 69 | The Web UI will now be available on `http://0.0.0.0:51821`. 70 | 71 | > 💡 Your configuration files will be saved in `~/.wg-easy` 72 | 73 | ### 3. Sponsor 74 | 75 | Are you enjoying this project? [Buy me a beer!](https://github.com/sponsors/WeeJeWel) 🍻 76 | 77 | ## Options 78 | 79 | These options can be configured by setting environment variables using `-e KEY="VALUE"` in the `docker run` command. 80 | 81 | | Env | Default | Example | Description | 82 | | - | - | - | - | 83 | | `PASSWORD` | - | `foobar123` | When set, requires a password when logging in to the Web UI. | 84 | | `WG_HOST` | - | `vpn.myserver.com` | The public hostname of your VPN server. | 85 | | `WG_DEVICE` | `eth0` | `ens6f0` | Ethernet device the wireguard traffic should be forwarded through. | 86 | | `WG_PORT` | `51820` | `12345` | The public UDP port of your VPN server. WireGuard will always listen on `51820` inside the Docker container. | 87 | | `WG_MTU` | `null` | `1420` | The MTU the clients will use. Server uses default WG MTU. | 88 | | `WG_PERSISTENT_KEEPALIVE` | `0` | `25` | Value in seconds to keep the "connection" open. If this value is 0, then connections won't be kept alive. | 89 | | `WG_DEFAULT_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range. | 90 | | `WG_DEFAULT_DNS` | `1.1.1.1` | `8.8.8.8, 8.8.4.4` | DNS server clients will use. | 91 | | `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. | 92 | | `WG_PRE_UP` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L19) for the default value. | 93 | | `WG_POST_UP` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L20) for the default value. | 94 | | `WG_PRE_DOWN` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L27) for the default value. | 95 | | `WG_POST_DOWN` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L28) for the default value. | 96 | 97 | > If you change `WG_PORT`, make sure to also change the exposed port. 98 | 99 | ## Updating 100 | 101 | To update to the latest version, simply run: 102 | 103 | ```bash 104 | docker stop wg-easy 105 | docker rm wg-easy 106 | docker pull ghcr.io/wg-easy/wg-easy 107 | ``` 108 | 109 | And then run the `docker run -d \ ...` command above again. 110 | 111 | ## Common Use Cases 112 | 113 | * [Using WireGuard-Easy with Pi-Hole](https://github.com/wg-easy/wg-easy/wiki/Using-WireGuard-Easy-with-Pi-Hole) 114 | * [Using WireGuard-Easy with nginx/SSL](https://github.com/wg-easy/wg-easy/wiki/Using-WireGuard-Easy-with-nginx-SSL) 115 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeeJeWel/wg-easy/83ac4ff4cf96b23ec4e1d8019413311834083d7a/assets/screenshot.png -------------------------------------------------------------------------------- /assets/wg-easy.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeeJeWel/wg-easy/83ac4ff4cf96b23ec4e1d8019413311834083d7a/assets/wg-easy.sketch -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | wg-easy: 4 | image: wg-easy 5 | command: npm run serve 6 | volumes: 7 | - ./src/:/app/ 8 | environment: 9 | # - PASSWORD=p 10 | - WG_HOST=192.168.1.233 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | wg-easy: 4 | environment: 5 | # ⚠️ Required: 6 | # Change this to your host's public address 7 | - WG_HOST=raspberrypi.local 8 | 9 | # Optional: 10 | # - PASSWORD=foobar123 11 | # - WG_PORT=51820 12 | # - WG_DEFAULT_ADDRESS=10.8.0.x 13 | # - WG_DEFAULT_DNS=1.1.1.1 14 | # - WG_MTU=1420 15 | # - WG_ALLOWED_IPS=192.168.15.0/24, 10.0.1.0/24 16 | # - WG_PRE_UP=echo "Pre Up" > /etc/wireguard/pre-up.txt 17 | # - WG_POST_UP=echo "Post Up" > /etc/wireguard/post-up.txt 18 | # - WG_PRE_DOWN=echo "Pre Down" > /etc/wireguard/pre-down.txt 19 | # - WG_POST_DOWN=echo "Post Down" > /etc/wireguard/post-down.txt 20 | 21 | image: ghcr.io/wg-easy/wg-easy 22 | container_name: wg-easy 23 | volumes: 24 | - .:/etc/wireguard 25 | ports: 26 | - "51820:51820/udp" 27 | - "51821:51821/tcp" 28 | restart: unless-stopped 29 | cap_add: 30 | - NET_ADMIN 31 | - SYS_MODULE 32 | sysctls: 33 | - net.ipv4.ip_forward=1 34 | - net.ipv4.conf.all.src_valid_mark=1 35 | -------------------------------------------------------------------------------- /docs/changelog.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": "Initial version. Enjoy!", 3 | "2": "You can now rename a client, and update the address. Enjoy!", 4 | "3": "Many improvements and small changes. Enjoy!", 5 | "4": "Now with pretty charts for client's network speed. Enjoy!", 6 | "5": "Many small improvements & feature requests. Enjoy!", 7 | "6": "Many small performance improvements & bug fixes. Enjoy!", 8 | "7": "Improved the look & performance of the upload/download chart.", 9 | "8": "Updated to Node.js v18." 10 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "lockfileVersion": 1 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "scripts": { 4 | "build": "DOCKER_BUILDKIT=1 docker build --tag wg-easy .", 5 | "serve": "docker-compose -f docker-compose.yml -f docker-compose.dev.yml up", 6 | "start": "docker run --env WG_HOST=0.0.0.0 --name wg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/wireguard -p 51820:51820/udp -p 51821:51821/tcp wg-easy" 7 | } 8 | } -------------------------------------------------------------------------------- /src/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "athom", 3 | "ignorePatterns": [ 4 | "**/vendor/*.js" 5 | ], 6 | "rules": { 7 | "consistent-return": "off", 8 | "no-shadow": "off", 9 | "max-len": "off" 10 | } 11 | } -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { release } = require('./package.json'); 4 | 5 | module.exports.RELEASE = release; 6 | module.exports.PORT = process.env.PORT || 51821; 7 | module.exports.PASSWORD = process.env.PASSWORD; 8 | module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/'; 9 | module.exports.WG_DEVICE = process.env.WG_DEVICE || 'eth0'; 10 | module.exports.WG_HOST = process.env.WG_HOST; 11 | module.exports.WG_PORT = process.env.WG_PORT || 51820; 12 | module.exports.WG_MTU = process.env.WG_MTU || null; 13 | module.exports.WG_PERSISTENT_KEEPALIVE = process.env.WG_PERSISTENT_KEEPALIVE || 0; 14 | module.exports.WG_DEFAULT_ADDRESS = process.env.WG_DEFAULT_ADDRESS || '10.8.0.x'; 15 | module.exports.WG_DEFAULT_DNS = typeof process.env.WG_DEFAULT_DNS === 'string' 16 | ? process.env.WG_DEFAULT_DNS 17 | : '1.1.1.1'; 18 | module.exports.WG_ALLOWED_IPS = process.env.WG_ALLOWED_IPS || '0.0.0.0/0, ::/0'; 19 | 20 | module.exports.WG_PRE_UP = process.env.WG_PRE_UP || ''; 21 | module.exports.WG_POST_UP = process.env.WG_POST_UP || ` 22 | iptables -t nat -A POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ${module.exports.WG_DEVICE} -j MASQUERADE; 23 | iptables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT; 24 | iptables -A FORWARD -i wg0 -j ACCEPT; 25 | iptables -A FORWARD -o wg0 -j ACCEPT; 26 | `.split('\n').join(' '); 27 | 28 | module.exports.WG_PRE_DOWN = process.env.WG_PRE_DOWN || ''; 29 | module.exports.WG_POST_DOWN = process.env.WG_POST_DOWN || ''; 30 | -------------------------------------------------------------------------------- /src/lib/Server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | const express = require('express'); 6 | const expressSession = require('express-session'); 7 | const debug = require('debug')('Server'); 8 | 9 | const Util = require('./Util'); 10 | const ServerError = require('./ServerError'); 11 | const WireGuard = require('../services/WireGuard'); 12 | 13 | const { 14 | PORT, 15 | RELEASE, 16 | PASSWORD, 17 | } = require('../config'); 18 | 19 | module.exports = class Server { 20 | 21 | constructor() { 22 | // Express 23 | this.app = express() 24 | .disable('etag') 25 | .use('/', express.static(path.join(__dirname, '..', 'www'))) 26 | .use(express.json()) 27 | .use(expressSession({ 28 | secret: String(Math.random()), 29 | resave: true, 30 | saveUninitialized: true, 31 | })) 32 | 33 | .get('/api/release', (Util.promisify(async () => { 34 | return RELEASE; 35 | }))) 36 | 37 | // Authentication 38 | .get('/api/session', Util.promisify(async req => { 39 | const requiresPassword = !!process.env.PASSWORD; 40 | const authenticated = requiresPassword 41 | ? !!(req.session && req.session.authenticated) 42 | : true; 43 | 44 | return { 45 | requiresPassword, 46 | authenticated, 47 | }; 48 | })) 49 | .post('/api/session', Util.promisify(async req => { 50 | const { 51 | password, 52 | } = req.body; 53 | 54 | if (typeof password !== 'string') { 55 | throw new ServerError('Missing: Password', 401); 56 | } 57 | 58 | if (password !== PASSWORD) { 59 | throw new ServerError('Incorrect Password', 401); 60 | } 61 | 62 | req.session.authenticated = true; 63 | req.session.save(); 64 | 65 | debug(`New Session: ${req.session.id}`); 66 | })) 67 | 68 | // WireGuard 69 | .use((req, res, next) => { 70 | if (!PASSWORD) { 71 | return next(); 72 | } 73 | 74 | if (req.session && req.session.authenticated) { 75 | return next(); 76 | } 77 | 78 | return res.status(401).json({ 79 | error: 'Not Logged In', 80 | }); 81 | }) 82 | .delete('/api/session', Util.promisify(async req => { 83 | const sessionId = req.session.id; 84 | 85 | req.session.destroy(); 86 | 87 | debug(`Deleted Session: ${sessionId}`); 88 | })) 89 | .get('/api/wireguard/client', Util.promisify(async req => { 90 | return WireGuard.getClients(); 91 | })) 92 | .get('/api/wireguard/client/:clientId/qrcode.svg', Util.promisify(async (req, res) => { 93 | const { clientId } = req.params; 94 | const svg = await WireGuard.getClientQRCodeSVG({ clientId }); 95 | res.header('Content-Type', 'image/svg+xml'); 96 | res.send(svg); 97 | })) 98 | .get('/api/wireguard/client/:clientId/configuration', Util.promisify(async (req, res) => { 99 | const { clientId } = req.params; 100 | const client = await WireGuard.getClient({ clientId }); 101 | const config = await WireGuard.getClientConfiguration({ clientId }); 102 | const configName = client.name 103 | .replace(/[^a-zA-Z0-9_=+.-]/g, '-') 104 | .replace(/(-{2,}|-$)/g, '-') 105 | .replace(/-$/, '') 106 | .substring(0, 32); 107 | res.header('Content-Disposition', `attachment; filename="${configName || clientId}.conf"`); 108 | res.header('Content-Type', 'text/plain'); 109 | res.send(config); 110 | })) 111 | .post('/api/wireguard/client', Util.promisify(async req => { 112 | const { name } = req.body; 113 | return WireGuard.createClient({ name }); 114 | })) 115 | .delete('/api/wireguard/client/:clientId', Util.promisify(async req => { 116 | const { clientId } = req.params; 117 | return WireGuard.deleteClient({ clientId }); 118 | })) 119 | .post('/api/wireguard/client/:clientId/enable', Util.promisify(async req => { 120 | const { clientId } = req.params; 121 | return WireGuard.enableClient({ clientId }); 122 | })) 123 | .post('/api/wireguard/client/:clientId/disable', Util.promisify(async req => { 124 | const { clientId } = req.params; 125 | return WireGuard.disableClient({ clientId }); 126 | })) 127 | .put('/api/wireguard/client/:clientId/name', Util.promisify(async req => { 128 | const { clientId } = req.params; 129 | const { name } = req.body; 130 | return WireGuard.updateClientName({ clientId, name }); 131 | })) 132 | .put('/api/wireguard/client/:clientId/address', Util.promisify(async req => { 133 | const { clientId } = req.params; 134 | const { address } = req.body; 135 | return WireGuard.updateClientAddress({ clientId, address }); 136 | })) 137 | 138 | .listen(PORT, () => { 139 | debug(`Listening on http://0.0.0.0:${PORT}`); 140 | }); 141 | } 142 | 143 | }; 144 | -------------------------------------------------------------------------------- /src/lib/ServerError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = class ServerError extends Error { 4 | 5 | constructor(message, statusCode = 500) { 6 | super(message); 7 | this.statusCode = statusCode; 8 | } 9 | 10 | }; 11 | -------------------------------------------------------------------------------- /src/lib/Util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const childProcess = require('child_process'); 4 | 5 | module.exports = class Util { 6 | 7 | static isValidIPv4(str) { 8 | const blocks = str.split('.'); 9 | if (blocks.length !== 4) return false; 10 | 11 | for (let value of blocks) { 12 | value = parseInt(value, 10); 13 | if (Number.isNaN(value)) return false; 14 | if (value < 0 || value > 255) return false; 15 | } 16 | 17 | return true; 18 | } 19 | 20 | static promisify(fn) { 21 | // eslint-disable-next-line func-names 22 | return function(req, res) { 23 | Promise.resolve().then(async () => fn(req, res)) 24 | .then(result => { 25 | if (res.headersSent) return; 26 | 27 | if (typeof result === 'undefined') { 28 | return res 29 | .status(204) 30 | .end(); 31 | } 32 | 33 | return res 34 | .status(200) 35 | .json(result); 36 | }) 37 | .catch(error => { 38 | if (typeof error === 'string') { 39 | error = new Error(error); 40 | } 41 | 42 | // eslint-disable-next-line no-console 43 | console.error(error); 44 | 45 | return res 46 | .status(error.statusCode || 500) 47 | .json({ 48 | error: error.message || error.toString(), 49 | stack: error.stack, 50 | }); 51 | }); 52 | }; 53 | } 54 | 55 | static async exec(cmd, { 56 | log = true, 57 | } = {}) { 58 | if (typeof log === 'string') { 59 | // eslint-disable-next-line no-console 60 | console.log(`$ ${log}`); 61 | } else if (log === true) { 62 | // eslint-disable-next-line no-console 63 | console.log(`$ ${cmd}`); 64 | } 65 | 66 | if (process.platform !== 'linux') { 67 | return ''; 68 | } 69 | 70 | return new Promise((resolve, reject) => { 71 | childProcess.exec(cmd, { 72 | shell: 'bash', 73 | }, (err, stdout) => { 74 | if (err) return reject(err); 75 | return resolve(String(stdout).trim()); 76 | }); 77 | }); 78 | } 79 | 80 | }; 81 | -------------------------------------------------------------------------------- /src/lib/WireGuard.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs').promises; 4 | const path = require('path'); 5 | 6 | const debug = require('debug')('WireGuard'); 7 | const uuid = require('uuid'); 8 | const QRCode = require('qrcode'); 9 | 10 | const Util = require('./Util'); 11 | const ServerError = require('./ServerError'); 12 | 13 | const { 14 | WG_PATH, 15 | WG_HOST, 16 | WG_PORT, 17 | WG_MTU, 18 | WG_DEFAULT_DNS, 19 | WG_DEFAULT_ADDRESS, 20 | WG_PERSISTENT_KEEPALIVE, 21 | WG_ALLOWED_IPS, 22 | WG_PRE_UP, 23 | WG_POST_UP, 24 | WG_PRE_DOWN, 25 | WG_POST_DOWN, 26 | } = require('../config'); 27 | 28 | module.exports = class WireGuard { 29 | 30 | async getConfig() { 31 | if (!this.__configPromise) { 32 | this.__configPromise = Promise.resolve().then(async () => { 33 | if (!WG_HOST) { 34 | throw new Error('WG_HOST Environment Variable Not Set!'); 35 | } 36 | 37 | debug('Loading configuration...'); 38 | let config; 39 | try { 40 | config = await fs.readFile(path.join(WG_PATH, 'wg0.json'), 'utf8'); 41 | config = JSON.parse(config); 42 | debug('Configuration loaded.'); 43 | } catch (err) { 44 | const privateKey = await Util.exec('wg genkey'); 45 | const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`, { 46 | log: 'echo ***hidden*** | wg pubkey', 47 | }); 48 | const address = WG_DEFAULT_ADDRESS.replace('x', '1'); 49 | 50 | config = { 51 | server: { 52 | privateKey, 53 | publicKey, 54 | address, 55 | }, 56 | clients: {}, 57 | }; 58 | debug('Configuration generated.'); 59 | } 60 | 61 | await this.__saveConfig(config); 62 | await Util.exec('wg-quick down wg0').catch(() => { }); 63 | await Util.exec('wg-quick up wg0').catch(err => { 64 | if (err && err.message && err.message.includes('Cannot find device "wg0"')) { 65 | throw new Error('WireGuard exited with the error: Cannot find device "wg0"\nThis usually means that your host\'s kernel does not support WireGuard!'); 66 | } 67 | 68 | throw err; 69 | }); 70 | // await Util.exec(`iptables -t nat -A POSTROUTING -s ${WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o eth0 -j MASQUERADE`); 71 | // await Util.exec('iptables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT'); 72 | // await Util.exec('iptables -A FORWARD -i wg0 -j ACCEPT'); 73 | // await Util.exec('iptables -A FORWARD -o wg0 -j ACCEPT'); 74 | await this.__syncConfig(); 75 | 76 | return config; 77 | }); 78 | } 79 | 80 | return this.__configPromise; 81 | } 82 | 83 | async saveConfig() { 84 | const config = await this.getConfig(); 85 | await this.__saveConfig(config); 86 | await this.__syncConfig(); 87 | } 88 | 89 | async __saveConfig(config) { 90 | let result = ` 91 | # Note: Do not edit this file directly. 92 | # Your changes will be overwritten! 93 | 94 | # Server 95 | [Interface] 96 | PrivateKey = ${config.server.privateKey} 97 | Address = ${config.server.address}/24 98 | ListenPort = 51820 99 | PreUp = ${WG_PRE_UP} 100 | PostUp = ${WG_POST_UP} 101 | PreDown = ${WG_PRE_DOWN} 102 | PostDown = ${WG_POST_DOWN} 103 | `; 104 | 105 | for (const [clientId, client] of Object.entries(config.clients)) { 106 | if (!client.enabled) continue; 107 | 108 | result += ` 109 | 110 | # Client: ${client.name} (${clientId}) 111 | [Peer] 112 | PublicKey = ${client.publicKey} 113 | PresharedKey = ${client.preSharedKey} 114 | AllowedIPs = ${client.address}/32`; 115 | } 116 | 117 | debug('Config saving...'); 118 | await fs.writeFile(path.join(WG_PATH, 'wg0.json'), JSON.stringify(config, false, 2), { 119 | mode: 0o660, 120 | }); 121 | await fs.writeFile(path.join(WG_PATH, 'wg0.conf'), result, { 122 | mode: 0o600, 123 | }); 124 | debug('Config saved.'); 125 | } 126 | 127 | async __syncConfig() { 128 | debug('Config syncing...'); 129 | await Util.exec('wg syncconf wg0 <(wg-quick strip wg0)'); 130 | debug('Config synced.'); 131 | } 132 | 133 | async getClients() { 134 | const config = await this.getConfig(); 135 | const clients = Object.entries(config.clients).map(([clientId, client]) => ({ 136 | id: clientId, 137 | name: client.name, 138 | enabled: client.enabled, 139 | address: client.address, 140 | publicKey: client.publicKey, 141 | createdAt: new Date(client.createdAt), 142 | updatedAt: new Date(client.updatedAt), 143 | allowedIPs: client.allowedIPs, 144 | 145 | persistentKeepalive: null, 146 | latestHandshakeAt: null, 147 | transferRx: null, 148 | transferTx: null, 149 | })); 150 | 151 | // Loop WireGuard status 152 | const dump = await Util.exec('wg show wg0 dump', { 153 | log: false, 154 | }); 155 | dump 156 | .trim() 157 | .split('\n') 158 | .slice(1) 159 | .forEach(line => { 160 | const [ 161 | publicKey, 162 | preSharedKey, // eslint-disable-line no-unused-vars 163 | endpoint, // eslint-disable-line no-unused-vars 164 | allowedIps, // eslint-disable-line no-unused-vars 165 | latestHandshakeAt, 166 | transferRx, 167 | transferTx, 168 | persistentKeepalive, 169 | ] = line.split('\t'); 170 | 171 | const client = clients.find(client => client.publicKey === publicKey); 172 | if (!client) return; 173 | 174 | client.latestHandshakeAt = latestHandshakeAt === '0' 175 | ? null 176 | : new Date(Number(`${latestHandshakeAt}000`)); 177 | client.transferRx = Number(transferRx); 178 | client.transferTx = Number(transferTx); 179 | client.persistentKeepalive = persistentKeepalive; 180 | }); 181 | 182 | return clients; 183 | } 184 | 185 | async getClient({ clientId }) { 186 | const config = await this.getConfig(); 187 | const client = config.clients[clientId]; 188 | if (!client) { 189 | throw new ServerError(`Client Not Found: ${clientId}`, 404); 190 | } 191 | 192 | return client; 193 | } 194 | 195 | async getClientConfiguration({ clientId }) { 196 | const config = await this.getConfig(); 197 | const client = await this.getClient({ clientId }); 198 | 199 | return ` 200 | [Interface] 201 | PrivateKey = ${client.privateKey} 202 | Address = ${client.address}/24 203 | ${WG_DEFAULT_DNS ? `DNS = ${WG_DEFAULT_DNS}` : ''} 204 | ${WG_MTU ? `MTU = ${WG_MTU}` : ''} 205 | 206 | [Peer] 207 | PublicKey = ${config.server.publicKey} 208 | PresharedKey = ${client.preSharedKey} 209 | AllowedIPs = ${WG_ALLOWED_IPS} 210 | PersistentKeepalive = ${WG_PERSISTENT_KEEPALIVE} 211 | Endpoint = ${WG_HOST}:${WG_PORT}`; 212 | } 213 | 214 | async getClientQRCodeSVG({ clientId }) { 215 | const config = await this.getClientConfiguration({ clientId }); 216 | return QRCode.toString(config, { 217 | type: 'svg', 218 | width: 512, 219 | }); 220 | } 221 | 222 | async createClient({ name }) { 223 | if (!name) { 224 | throw new Error('Missing: Name'); 225 | } 226 | 227 | const config = await this.getConfig(); 228 | 229 | const privateKey = await Util.exec('wg genkey'); 230 | const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`); 231 | const preSharedKey = await Util.exec('wg genpsk'); 232 | 233 | // Calculate next IP 234 | let address; 235 | for (let i = 2; i < 255; i++) { 236 | const client = Object.values(config.clients).find(client => { 237 | return client.address === WG_DEFAULT_ADDRESS.replace('x', i); 238 | }); 239 | 240 | if (!client) { 241 | address = WG_DEFAULT_ADDRESS.replace('x', i); 242 | break; 243 | } 244 | } 245 | 246 | if (!address) { 247 | throw new Error('Maximum number of clients reached.'); 248 | } 249 | 250 | // Create Client 251 | const clientId = uuid.v4(); 252 | const client = { 253 | name, 254 | address, 255 | privateKey, 256 | publicKey, 257 | preSharedKey, 258 | 259 | createdAt: new Date(), 260 | updatedAt: new Date(), 261 | 262 | enabled: true, 263 | }; 264 | 265 | config.clients[clientId] = client; 266 | 267 | await this.saveConfig(); 268 | 269 | return client; 270 | } 271 | 272 | async deleteClient({ clientId }) { 273 | const config = await this.getConfig(); 274 | 275 | if (config.clients[clientId]) { 276 | delete config.clients[clientId]; 277 | await this.saveConfig(); 278 | } 279 | } 280 | 281 | async enableClient({ clientId }) { 282 | const client = await this.getClient({ clientId }); 283 | 284 | client.enabled = true; 285 | client.updatedAt = new Date(); 286 | 287 | await this.saveConfig(); 288 | } 289 | 290 | async disableClient({ clientId }) { 291 | const client = await this.getClient({ clientId }); 292 | 293 | client.enabled = false; 294 | client.updatedAt = new Date(); 295 | 296 | await this.saveConfig(); 297 | } 298 | 299 | async updateClientName({ clientId, name }) { 300 | const client = await this.getClient({ clientId }); 301 | 302 | client.name = name; 303 | client.updatedAt = new Date(); 304 | 305 | await this.saveConfig(); 306 | } 307 | 308 | async updateClientAddress({ clientId, address }) { 309 | const client = await this.getClient({ clientId }); 310 | 311 | if (!Util.isValidIPv4(address)) { 312 | throw new ServerError(`Invalid Address: ${address}`, 400); 313 | } 314 | 315 | client.address = address; 316 | client.updatedAt = new Date(); 317 | 318 | await this.saveConfig(); 319 | } 320 | 321 | }; 322 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "release": 8, 3 | "name": "wg-easy", 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "server.js", 7 | "scripts": { 8 | "serve": "DEBUG=Server,WireGuard node --watch server.js", 9 | "serve-with-password": "PASSWORD=wg npm run serve", 10 | "lint": "eslint ." 11 | }, 12 | "author": "Emile Nijssen", 13 | "license": "GPL", 14 | "dependencies": { 15 | "debug": "^4.3.1", 16 | "express": "^4.17.1", 17 | "express-session": "^1.17.1", 18 | "qrcode": "^1.4.4", 19 | "uuid": "^8.3.2" 20 | }, 21 | "devDependencies": { 22 | "eslint": "^7.27.0", 23 | "eslint-config-athom": "^2.1.0" 24 | }, 25 | "engines": { 26 | "node": "18" 27 | } 28 | } -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./services/Server'); 4 | 5 | const WireGuard = require('./services/WireGuard'); 6 | 7 | WireGuard.getConfig() 8 | .catch(err => { 9 | // eslint-disable-next-line no-console 10 | console.error(err); 11 | 12 | // eslint-disable-next-line no-process-exit 13 | process.exit(1); 14 | }); 15 | -------------------------------------------------------------------------------- /src/services/Server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Server = require('../lib/Server'); 4 | 5 | module.exports = new Server(); 6 | -------------------------------------------------------------------------------- /src/services/WireGuard.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const WireGuard = require('../lib/WireGuard'); 4 | 5 | module.exports = new WireGuard(); 6 | -------------------------------------------------------------------------------- /src/www/img/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeeJeWel/wg-easy/83ac4ff4cf96b23ec4e1d8019413311834083d7a/src/www/img/apple-touch-icon.png -------------------------------------------------------------------------------- /src/www/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeeJeWel/wg-easy/83ac4ff4cf96b23ec4e1d8019413311834083d7a/src/www/img/favicon.png -------------------------------------------------------------------------------- /src/www/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeeJeWel/wg-easy/83ac4ff4cf96b23ec4e1d8019413311834083d7a/src/www/img/logo.png -------------------------------------------------------------------------------- /src/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
There is an update available!
47 |{{latestRelease.changelog}}
48 |Clients
61 |There are no clients yet.
284 |
293 |
370 | 372 |
373 |444 | Are you sure you want to delete {{clientDelete.name}}? 445 | This action cannot be undone. 446 |
447 |Made by Emile Nijssen · Donate · GitHub
517 | 518 | 519 |(0===i?9:1)&&(i+=1),d[n](t,i)[agoin].replace("%s",t)}function r(e,n){return n=n?t(n):new Date,(n-t(e))/1e3}function i(t){for(var e=1,n=0,r=Math.abs(t);t>=l[n]&&n
1&&(n+="s"),[t+" "+n+" ago","in "+t+" "+n]},zh_CN:function(t,e){if(0===e)return["刚刚","片刻后"];var n=s[parseInt(e/2)];return[t+n+"前",t+n+"后"]}},l=[60,60,24,7,365/7/12,12],p=6,h="datetime";return u.register=function(t,e){d[t]=e},u});
--------------------------------------------------------------------------------
/src/www/js/vendor/vue.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Vue.js v2.6.12
3 | * (c) 2014-2020 Evan You
4 | * Released under the MIT License.
5 | */
6 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).Vue=t()}(this,function(){"use strict";var e=Object.freeze({});function t(e){return null==e}function n(e){return null!=e}function r(e){return!0===e}function i(e){return"string"==typeof e||"number"==typeof e||"symbol"==typeof e||"boolean"==typeof e}function o(e){return null!==e&&"object"==typeof e}var a=Object.prototype.toString;function s(e){return"[object Object]"===a.call(e)}function c(e){var t=parseFloat(String(e));return t>=0&&Math.floor(t)===t&&isFinite(e)}function u(e){return n(e)&&"function"==typeof e.then&&"function"==typeof e.catch}function l(e){return null==e?"":Array.isArray(e)||s(e)&&e.toString===a?JSON.stringify(e,null,2):String(e)}function f(e){var t=parseFloat(e);return isNaN(t)?e:t}function p(e,t){for(var n=Object.create(null),r=e.split(","),i=0;i