Browse Source

Port initial commit

pull/438/head
Yamzik 4 years ago
parent
commit
75d3d24fc2
  1. 2
      .dockerignore
  2. 6
      .gitignore
  3. 2
      Alpine.Wireguard.Dockerfile
  4. 9
      Application.Deploy.Dockerfile
  5. 7
      Application.Runtime.Dockerfile
  6. 4
      Builder.Dockerfile
  7. 32
      Builder.Former.dockerfile
  8. 2277
      Cargo.lock
  9. 22
      Cargo.toml
  10. 50
      Dockerfile
  11. 3
      Installer.Dockerfile
  12. 59
      README.md
  13. BIN
      assets/screenshot.png
  14. BIN
      assets/wg-easy.sketch
  15. 0
      data/www/css/vendor/tailwind.min.css
  16. 0
      data/www/img/apple-touch-icon.png
  17. 0
      data/www/img/favicon.png
  18. 0
      data/www/img/logo.png
  19. 2
      data/www/index.html
  20. 14
      data/www/index__.html
  21. 0
      data/www/js/api.js
  22. 10
      data/www/js/app.js
  23. 0
      data/www/js/vendor/md5.min.js
  24. 0
      data/www/js/vendor/timeago.min.js
  25. 0
      data/www/js/vendor/vue.min.js
  26. 0
      data/www/manifest.json
  27. 40
      docker-compose.deploy.yml
  28. 10
      docker-compose.dev.yml
  29. 9
      docker-compose.installer.yml
  30. 50
      docker-compose.yml
  31. 9
      docs/changelog.json
  32. 1
      image_deploy.sh
  33. 1
      image_runtime.sh
  34. 1
      install.sh
  35. 4
      package-lock.json
  36. 8
      package.json
  37. 1
      release.sh
  38. 11
      src/.eslintrc.json
  39. 1
      src/.gitignore
  40. 28
      src/config.js
  41. 143
      src/lib/Server.js
  42. 10
      src/lib/ServerError.js
  43. 80
      src/lib/Util.js
  44. 321
      src/lib/WireGuard.js
  45. 326
      src/main.rs
  46. 2678
      src/package-lock.json
  47. 33
      src/package.json
  48. 14
      src/server.js
  49. 5
      src/services/Server.js
  50. 5
      src/services/WireGuard.js
  51. 0
      src/test/serde_test.rs
  52. 2
      src/utils.rs
  53. 8
      src/utils/misc.rs
  54. 99
      src/utils/os.rs
  55. 2
      src/wireguard.rs
  56. 108
      src/wireguard/commands.rs
  57. 430
      src/wireguard/config.rs

2
.dockerignore

@ -0,0 +1,2 @@
/target
/.vscode

6
.gitignore

@ -1,3 +1,3 @@
/config
/wg0.conf
/wg0.json
/target
/application
/.vscode

2
Alpine.Wireguard.Dockerfile

@ -0,0 +1,2 @@
FROM alpine:3.17.0 AS runtime
RUN apk add -U --no-cache wireguard-tools dumb-init

9
Application.Deploy.Dockerfile

@ -0,0 +1,9 @@
FROM alpine:3.17.0 AS alpine_wg
RUN apk add -U --no-cache wireguard-tools dumb-init
FROM alpine_wg AS runtime
COPY application/bin/rs-wg /usr/local/cargo/bin/rs-wg
COPY data/wg /etc/wireguard
COPY data/www /etc/www
WORKDIR /etc/wireguard
EXPOSE 8080

7
Application.Runtime.Dockerfile

@ -0,0 +1,7 @@
FROM alpine:3.17.0 AS alpine_wg
RUN apk add -U --no-cache wireguard-tools dumb-init
FROM alpine_wg AS runtime
WORKDIR /etc/wireguard
EXPOSE 8080
USER root

4
Builder.Dockerfile

@ -0,0 +1,4 @@
FROM rs-wg-builder as builder
COPY . /usr/src/rs-wg
WORKDIR /usr/src/rs-wg
RUN cargo build --target x86_64-unknown-linux-musl --release

32
Builder.Former.dockerfile

@ -0,0 +1,32 @@
################
##### by Pascal Zwikirsch
##### https://levelup.gitconnected.com/create-an-optimized-rust-alpine-docker-image-1940db638a6c
##### Builder
FROM rust:1.65.0-slim as builder
WORKDIR /usr/src
# Create blank project
RUN USER=root cargo new medium-rust-dockerize
# We want dependencies cached, so copy those first.
COPY Cargo.toml Cargo.lock /usr/src/medium-rust-dockerize/
# Set the working directory
WORKDIR /usr/src/medium-rust-dockerize
## Install target platform (Cross-Compilation) --> Needed for Alpine
RUN rustup target add x86_64-unknown-linux-musl
# This is a dummy build to get the dependencies cached.
RUN cargo build --target x86_64-unknown-linux-musl --release
FROM alpine:3.16.0 AS runtime
# Copy application binary from builder image
COPY --from=builder /usr/src/medium-rust-dockerize/target/x86_64-unknown-linux-musl/release/medium-rust-dockerize /usr/local/bin
EXPOSE 3030
# Run the application
CMD ["/usr/local/bin/medium-rust-dockerize"]

2277
Cargo.lock

File diff suppressed because it is too large

22
Cargo.toml

@ -0,0 +1,22 @@
[package]
name = "rs-wg"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4"
env_logger = "0"
tokio = { version = "1", features = ["full"] }
actix-web = "4"
actix-files = "0"
actix-web-lab = "0"
actix-session = { version = "0", features = ["cookie-session"] }
futures-util = { version = "0", default-features = false, features = ["std"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = {version = "1", features = ["serde", "v4", "fast-rng", "macro-diagnostics"] }
chrono = {version = "0", features = ["serde"] }
qrcode-generator = "4"
fs2 = "0"

50
Dockerfile

@ -1,50 +0,0 @@
# There's an issue with node:16-alpine.
# On Raspberry Pi, the following crash happens:
# #FailureMessage Object: 0x7e87753c
# #
# # Fatal error in , line 0
# # unreachable code
# #
# #
# #
FROM docker.io/library/node:14-alpine@sha256:dc92f36e7cd917816fa2df041d4e9081453366381a00f40398d99e9392e78664 AS build_node_modules
# Copy Web UI
COPY src/ /app/
WORKDIR /app
RUN npm ci --production
# Copy build result to a new image.
# This saves a lot of disk space.
FROM docker.io/library/node:14-alpine@sha256:dc92f36e7cd917816fa2df041d4e9081453366381a00f40398d99e9392e78664
COPY --from=build_node_modules /app /app
# Move node_modules one directory up, so during development
# we don't have to mount it in a volume.
# This results in much faster reloading!
#
# Also, some node_modules might be native, and
# the architecture & OS of your development machine might differ
# than what runs inside of docker.
RUN mv /app/node_modules /node_modules
# Enable this to run `npm run serve`
RUN npm i -g nodemon
# Install Linux packages
RUN apk add -U --no-cache \
wireguard-tools \
dumb-init
# Expose Ports
EXPOSE 51820/udp
EXPOSE 51821/tcp
# Set Environment
ENV DEBUG=Server,WireGuard
# Run Web UI
WORKDIR /app
CMD ["/usr/bin/dumb-init", "node", "server.js"]

3
Installer.Dockerfile

@ -0,0 +1,3 @@
FROM rust:1-alpine
RUN rustup target add x86_64-unknown-linux-musl
WORKDIR /usr

59
README.md

@ -17,7 +17,7 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
* All-in-one: WireGuard + Web UI.
* Easy installation, simple to use.
* List, create, edit, delete, enable & disable clients.
* List, create, edit, delete, clients (enable & disable are currently broken).
* Show a client's QR code.
* Download a client's configuration file.
* Statistics for which clients are connected.
@ -29,6 +29,11 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
* A host with a kernel that supports WireGuard (all modern kernels).
* A host with Docker installed.
## Flaws
* Application state r/w race contitions due to this is stored on the disk with no caching
* Enabling/disabling clients is currently unavailable due to former flaw
## Installation
### 1. Install Docker
@ -48,32 +53,20 @@ And log in again.
To automatically install & run wg-easy, simply run:
<pre>
$ docker run -d \
--name=wg-easy \
-e WG_HOST=<b>🚨YOUR_SERVER_IP</b> \
-e PASSWORD=<b>🚨YOUR_ADMIN_PASSWORD</b> \
-v ~/.wg-easy:/etc/wireguard \
-p 51820:51820/udp \
-p 51821:51821/tcp \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
--sysctl="net.ipv4.conf.all.src_valid_mark=1" \
--sysctl="net.ipv4.ip_forward=1" \
--restart unless-stopped \
weejewel/wg-easy
$ docker compose -f docker-compose.deploy.yml up -d
</pre>
> 💡 Replace `YOUR_SERVER_IP` with your WAN IP, or a Dynamic DNS hostname.
> 💡 Replace `HOST` with your WAN IP, or a Dynamic DNS hostname.
>
> 💡 Replace `YOUR_ADMIN_PASSWORD` with a password to log in on the Web UI.
> 💡 Replace `PASSWORD` with a password to log in on the Web UI.
The Web UI will now be available on `http://0.0.0.0:51821`.
The Web UI will now be available on `http://0.0.0.0:8080`.
> 💡 Your configuration files will be saved in `~/.wg-easy`
> 💡 Your configuration files will be saved in `./data`
### 3. Sponsor
Are you enjoying this project? [Buy me a beer!](https://github.com/sponsors/WeeJeWel) 🍻
Are you enjoying this project? [Buy Weejewel a beer!](https://github.com/sponsors/WeeJeWel) 🍻
## Options
@ -82,17 +75,17 @@ These options can be configured by setting environment variables using `-e KEY="
| Env | Default | Example | Description |
| - | - | - | - |
| `PASSWORD` | - | `foobar123` | When set, requires a password when logging in to the Web UI. |
| `WG_HOST` | - | `vpn.myserver.com` | The public hostname of your VPN server. |
| `HOST` | - | `vpn.myserver.com` | The public hostname of your VPN server. |
| `WG_PORT` | `51820` | `12345` | The public UDP port of your VPN server. WireGuard will always listen on `51820` inside the Docker container. |
| `WG_MTU` | `null` | `1420` | The MTU the clients will use. Server uses default WG MTU. |
| `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. |
| `WG_DEFAULT_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range. |
| `WG_DEFAULT_DNS` | `1.1.1.1` | `8.8.8.8, 8.8.4.4` | DNS server clients will use. |
| `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. |
| `WG_PRE_UP` | `...` | - | See [config.js](https://github.com/WeeJeWel/wg-easy/blob/master/src/config.js#L19) for the default value. |
| `WG_POST_UP` | `...` | `iptables ...` | See [config.js](https://github.com/WeeJeWel/wg-easy/blob/master/src/config.js#L20) for the default value. |
| `WG_PRE_DOWN` | `...` | - | See [config.js](https://github.com/WeeJeWel/wg-easy/blob/master/src/config.js#L27) for the default value. |
| `WG_POST_DOWN` | `...` | `iptables ...` | See [config.js](https://github.com/WeeJeWel/wg-easy/blob/master/src/config.js#L28) for the default value. |
| `MTU` | `null` | `1420` | The MTU the clients will use. Server uses default WG MTU. |
| `PERSISTENT_KEEPALIVE` | `0` | `25` | Value in seconds to keep the "connection" open. If this value is 0, then connections won't be kept alive. |
| `DEFAULT_ADDRESS` | `10.8.0.1` | `10.6.0.1` | Clients IP address range. |
| `DEFAULT_DNS` | `1.1.1.1` | `8.8.8.8, 8.8.4.4` | DNS server clients will use. |
| `ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. |
| `PRE_UP` | `...` | - | See [config.js](https://github.com/WeeJeWel/wg-easy/blob/master/src/config.js#L19) for the default value. |
| `OST_UP` | `...` | `iptables ...` | See [config.js](https://github.com/WeeJeWel/wg-easy/blob/master/src/config.js#L20) for the default value. |
| `PRE_DOWN` | `...` | - | See [config.js](https://github.com/WeeJeWel/wg-easy/blob/master/src/config.js#L27) for the default value. |
| `POST_DOWN` | `...` | `iptables ...` | See [config.js](https://github.com/WeeJeWel/wg-easy/blob/master/src/config.js#L28) for the default value. |
> If you change `WG_PORT`, make sure to also change the exposed port.
@ -101,12 +94,12 @@ These options can be configured by setting environment variables using `-e KEY="
To update to the latest version, simply run:
```bash
docker stop wg-easy
docker rm wg-easy
docker pull weejewel/wg-easy
docker stop rs-wg
docker rm rs-wg
docker pull yamzeg/rs-wg
```
And then run the `docker run -d \ ...` command above again.
And then run the `docker compose -f \ ...` command above again.
## Common Use Cases

BIN
assets/screenshot.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

BIN
assets/wg-easy.sketch

Binary file not shown.

0
src/www/css/vendor/tailwind.min.css → data/www/css/vendor/tailwind.min.css

0
src/www/img/apple-touch-icon.png → data/www/img/apple-touch-icon.png

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

0
src/www/img/favicon.png → data/www/img/favicon.png

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

0
src/www/img/logo.png → data/www/img/logo.png

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

2
src/www/index.html → data/www/index.html

@ -6,7 +6,7 @@
<link href="./css/vendor/tailwind.min.css" rel="stylesheet">
<link rel="manifest" href="./manifest.json">
<link rel="icon" type="image/png" href="./img/favicon.png">
<link rel="apple-touch-icon" href="./img/apple-touch-icon.png">
<link rel="apple-touch-icon" href="./img/apple-touch -icon.png">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
</head>

14
data/www/index__.html

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="manifest" href="./manifest.json">
<link rel="icon" type="image/png" href="./img/favicon.png">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Hello!</h1>
</body>
</html>

0
src/www/js/api.js → data/www/js/api.js

10
src/www/js/app.js → data/www/js/app.js

@ -112,15 +112,7 @@ new Vue({
},
},
methods: {
dateTime: value => {
return new Intl.DateTimeFormat(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
}).format(value);
},
dateTime: value => new Date(value),
async refresh({
updateCharts = false,
} = {}) {

0
src/www/js/vendor/md5.min.js → data/www/js/vendor/md5.min.js

0
src/www/js/vendor/timeago.min.js → data/www/js/vendor/timeago.min.js

0
src/www/js/vendor/vue.min.js → data/www/js/vendor/vue.min.js

0
src/www/manifest.json → data/www/manifest.json

40
docker-compose.deploy.yml

@ -0,0 +1,40 @@
version: "3.8"
services:
rs-wg:
image: yamzeg/rs-wg
container_name: rs-wg
environment:
- WIREGUARD_SETTINGS=/etc/wireguard/settings.json
- WIREGUARD_PATH=/etc/wireguard/
- STATIC_DIR=/etc/www/
- RUST_LOG=actix_web=warn,debug
# optional parameters are commented out
# - INTERFACE_NAME=wg0
# - RELEASE=rs0
# - API_PORT=8080
- PASSWORD=123321
- HOST=
# - WG_PORT=51820
# - MTU=0
# - PERSISTENT_KEEPALIVE=25
# - DEFAULT_ADDRESS=10.8.0.1
# - DEFAULT_ADDRESS_BASE=10.8.0
# - DEFAULT_DNS=1.1.1.1
# - ALLOWED_IPS=0.0.0.0/0, ::/0
# - PRE_UP=
# - POST_UP=
# - PRE_DOWN=
# - POST_DOWN=
volumes:
- ./data:/etc/wireguard
ports:
- "51820:51820/udp" # should be same as WG_PORT
- "8080:8080/tcp"
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
entrypoint: dumb-init /usr/local/cargo/bin/rs-wg

10
docker-compose.dev.yml

@ -1,10 +0,0 @@
version: "3.8"
services:
wg-easy:
image: wg-easy
command: npm run serve
volumes:
- ./src/:/app/
environment:
# - PASSWORD=p
- WG_HOST=192.168.1.233

9
docker-compose.installer.yml

@ -0,0 +1,9 @@
version: "3.8"
services:
rs-wg-installer:
image: rs-wg-installer
container_name: rs-wg-installer
entrypoint: cargo
volumes:
- .:/usr
- ./application:/usr/local/cargo/bin/rs-wg

50
docker-compose.yml

@ -1,34 +1,42 @@
version: "3.8"
services:
wg-easy:
rs-wg:
image: rs-wg-local
container_name: rs-wg
environment:
# ⚠️ Required:
# Change this to your host's public address
- WG_HOST=raspberrypi.local
- WIREGUARD_SETTINGS=/etc/wireguard/settings.json
- WIREGUARD_PATH=/etc/wireguard/
- STATIC_DIR=/etc/wireguard/www
- RUST_LOG=actix_web=warn,debug
# Optional:
# - PASSWORD=foobar123
# - WG_PORT=51820
# - WG_DEFAULT_ADDRESS=10.8.0.x
# - WG_DEFAULT_DNS=1.1.1.1
# - WG_MTU=1420
# - WG_ALLOWED_IPS=192.168.15.0/24, 10.0.1.0/24
# - WG_PRE_UP=echo "Pre Up" > /etc/wireguard/pre-up.txt
# - WG_POST_UP=echo "Post Up" > /etc/wireguard/post-up.txt
# - WG_PRE_DOWN=echo "Pre Down" > /etc/wireguard/pre-down.txt
# - WG_POST_DOWN=echo "Post Down" > /etc/wireguard/post-down.txt
# optional parameters are commented out
# - INTERFACE_NAME=wg0
# - RELEASE=rs0
# - API_PORT=8080
# - PASSWORD=123321
- HOST=localhost
# - WG_PORT=51825
# - MTU=0
# - PERSISTENT_KEEPALIVE=25
# - DEFAULT_ADDRESS=10.8.0.1
# - DEFAULT_ADDRESS_BASE=10.8.0
# - DEFAULT_DNS=1.1.1.1
# - ALLOWED_IPS=0.0.0.0/0, ::/0
# - PRE_UP=
# - POST_UP=
# - PRE_DOWN=
# - POST_DOWN=
image: weejewel/wg-easy
container_name: wg-easy
volumes:
- .:/etc/wireguard
- ./data:/etc/wireguard
- ./application/bin:/usr/local/cargo/bin
ports:
- "51820:51820/udp"
- "51821:51821/tcp"
restart: unless-stopped
- "8081:51820/udp"
- "8080:8080/tcp"
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
entrypoint: dumb-init /usr/local/cargo/bin/rs-wg

9
docs/changelog.json

@ -1,9 +0,0 @@
{
"1": "Initial version. Enjoy!",
"2": "You can now rename a client, and update the address. Enjoy!",
"3": "Many improvements and small changes. Enjoy!",
"4": "Now with pretty charts for client's network speed. Enjoy!",
"5": "Many small improvements & feature requests. Enjoy!",
"6": "Many small performance improvements & bug fixes. Enjoy!",
"7": "Improved the look & performance of the upload/download chart."
}

1
image_deploy.sh

@ -0,0 +1 @@
docker build -t yamzeg/rs-wg -f Application.Deploy.Dockerfile .

1
image_runtime.sh

@ -0,0 +1 @@
docker build -t rs-wg-local -f Application.Runtime.Dockerfile .

1
install.sh

@ -0,0 +1 @@
cargo install --target x86_64-unknown-linux-musl --path . --root application

4
package-lock.json

@ -1,4 +0,0 @@
{
"version": "1.0.0",
"lockfileVersion": 1
}

8
package.json

@ -1,8 +0,0 @@
{
"version": "1.0.0",
"scripts": {
"build": "DOCKER_BUILDKIT=1 docker build --tag wg-easy .",
"serve": "docker-compose -f docker-compose.yml -f docker-compose.dev.yml up",
"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"
}
}

1
release.sh

@ -0,0 +1 @@
cargo build --target x86_64-unknown-linux-musl --release

11
src/.eslintrc.json

@ -1,11 +0,0 @@
{
"extends": "athom",
"ignorePatterns": [
"**/vendor/*.js"
],
"rules": {
"consistent-return": "off",
"no-shadow": "off",
"max-len": "off"
}
}

1
src/.gitignore

@ -1 +0,0 @@
/node_modules

28
src/config.js

@ -1,28 +0,0 @@
'use strict';
const { release } = require('./package.json');
module.exports.RELEASE = release;
module.exports.PORT = process.env.PORT || 51821;
module.exports.PASSWORD = process.env.PASSWORD;
module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/';
module.exports.WG_HOST = process.env.WG_HOST;
module.exports.WG_PORT = process.env.WG_PORT || 51820;
module.exports.WG_MTU = process.env.WG_MTU || null;
module.exports.WG_PERSISTENT_KEEPALIVE = process.env.WG_PERSISTENT_KEEPALIVE || 0;
module.exports.WG_DEFAULT_ADDRESS = process.env.WG_DEFAULT_ADDRESS || '10.8.0.x';
module.exports.WG_DEFAULT_DNS = typeof process.env.WG_DEFAULT_DNS === 'string'
? process.env.WG_DEFAULT_DNS
: '1.1.1.1';
module.exports.WG_ALLOWED_IPS = process.env.WG_ALLOWED_IPS || '0.0.0.0/0, ::/0';
module.exports.WG_PRE_UP = process.env.WG_PRE_UP || '';
module.exports.WG_POST_UP = process.env.WG_POST_UP || `
iptables -t nat -A POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o eth0 -j MASQUERADE;
iptables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT;
iptables -A FORWARD -i wg0 -j ACCEPT;
iptables -A FORWARD -o wg0 -j ACCEPT;
`.split('\n').join(' ');
module.exports.WG_PRE_DOWN = process.env.WG_PRE_DOWN || '';
module.exports.WG_POST_DOWN = process.env.WG_POST_DOWN || '';

143
src/lib/Server.js

@ -1,143 +0,0 @@
'use strict';
const path = require('path');
const express = require('express');
const expressSession = require('express-session');
const debug = require('debug')('Server');
const Util = require('./Util');
const ServerError = require('./ServerError');
const WireGuard = require('../services/WireGuard');
const {
PORT,
RELEASE,
PASSWORD,
} = require('../config');
module.exports = class Server {
constructor() {
// Express
this.app = express()
.disable('etag')
.use('/', express.static(path.join(__dirname, '..', 'www')))
.use(express.json())
.use(expressSession({
secret: String(Math.random()),
resave: true,
saveUninitialized: true,
}))
.get('/api/release', (Util.promisify(async () => {
return RELEASE;
})))
// Authentication
.get('/api/session', Util.promisify(async req => {
const requiresPassword = !!process.env.PASSWORD;
const authenticated = requiresPassword
? !!(req.session && req.session.authenticated)
: true;
return {
requiresPassword,
authenticated,
};
}))
.post('/api/session', Util.promisify(async req => {
const {
password,
} = req.body;
if (typeof password !== 'string') {
throw new ServerError('Missing: Password', 401);
}
if (password !== PASSWORD) {
throw new ServerError('Incorrect Password', 401);
}
req.session.authenticated = true;
req.session.save();
debug(`New Session: ${req.session.id}`);
}))
// WireGuard
.use((req, res, next) => {
if (!PASSWORD) {
return next();
}
if (req.session && req.session.authenticated) {
return next();
}
return res.status(401).json({
error: 'Not Logged In',
});
})
.delete('/api/session', Util.promisify(async req => {
const sessionId = req.session.id;
req.session.destroy();
debug(`Deleted Session: ${sessionId}`);
}))
.get('/api/wireguard/client', Util.promisify(async req => {
return WireGuard.getClients();
}))
.get('/api/wireguard/client/:clientId/qrcode.svg', Util.promisify(async (req, res) => {
const { clientId } = req.params;
const svg = await WireGuard.getClientQRCodeSVG({ clientId });
res.header('Content-Type', 'image/svg+xml');
res.send(svg);
}))
.get('/api/wireguard/client/:clientId/configuration', Util.promisify(async (req, res) => {
const { clientId } = req.params;
const client = await WireGuard.getClient({ clientId });
const config = await WireGuard.getClientConfiguration({ clientId });
const configName = client.name
.replace(/[^a-zA-Z0-9_=+.-]/g, '-')
.replace(/(-{2,}|-$)/g, '-')
.replace(/-$/, '')
.substring(0, 32);
res.header('Content-Disposition', `attachment; filename="${configName || clientId}.conf"`);
res.header('Content-Type', 'text/plain');
res.send(config);
}))
.post('/api/wireguard/client', Util.promisify(async req => {
const { name } = req.body;
return WireGuard.createClient({ name });
}))
.delete('/api/wireguard/client/:clientId', Util.promisify(async req => {
const { clientId } = req.params;
return WireGuard.deleteClient({ clientId });
}))
.post('/api/wireguard/client/:clientId/enable', Util.promisify(async req => {
const { clientId } = req.params;
return WireGuard.enableClient({ clientId });
}))
.post('/api/wireguard/client/:clientId/disable', Util.promisify(async req => {
const { clientId } = req.params;
return WireGuard.disableClient({ clientId });
}))
.put('/api/wireguard/client/:clientId/name', Util.promisify(async req => {
const { clientId } = req.params;
const { name } = req.body;
return WireGuard.updateClientName({ clientId, name });
}))
.put('/api/wireguard/client/:clientId/address', Util.promisify(async req => {
const { clientId } = req.params;
const { address } = req.body;
return WireGuard.updateClientAddress({ clientId, address });
}))
.listen(PORT, () => {
debug(`Listening on http://0.0.0.0:${PORT}`);
});
}
};

10
src/lib/ServerError.js

@ -1,10 +0,0 @@
'use strict';
module.exports = class ServerError extends Error {
constructor(message, statusCode = 500) {
super(message);
this.statusCode = statusCode;
}
};

80
src/lib/Util.js

@ -1,80 +0,0 @@
'use strict';
const childProcess = require('child_process');
module.exports = class Util {
static isValidIPv4(str) {
const blocks = str.split('.');
if (blocks.length !== 4) return false;
for (let value of blocks) {
value = parseInt(value, 10);
if (Number.isNaN(value)) return false;
if (value < 0 || value > 255) return false;
}
return true;
}
static promisify(fn) {
// eslint-disable-next-line func-names
return function(req, res) {
Promise.resolve().then(async () => fn(req, res))
.then(result => {
if (res.headersSent) return;
if (typeof result === 'undefined') {
return res
.status(204)
.end();
}
return res
.status(200)
.json(result);
})
.catch(error => {
if (typeof error === 'string') {
error = new Error(error);
}
// eslint-disable-next-line no-console
console.error(error);
return res
.status(error.statusCode || 500)
.json({
error: error.message || error.toString(),
stack: error.stack,
});
});
};
}
static async exec(cmd, {
log = true,
} = {}) {
if (typeof log === 'string') {
// eslint-disable-next-line no-console
console.log(`$ ${log}`);
} else if (log === true) {
// eslint-disable-next-line no-console
console.log(`$ ${cmd}`);
}
if (process.platform !== 'linux') {
return '';
}
return new Promise((resolve, reject) => {
childProcess.exec(cmd, {
shell: 'bash',
}, (err, stdout) => {
if (err) return reject(err);
return resolve(String(stdout).trim());
});
});
}
};

321
src/lib/WireGuard.js

@ -1,321 +0,0 @@
'use strict';
const fs = require('fs').promises;
const path = require('path');
const debug = require('debug')('WireGuard');
const uuid = require('uuid');
const QRCode = require('qrcode');
const Util = require('./Util');
const ServerError = require('./ServerError');
const {
WG_PATH,
WG_HOST,
WG_PORT,
WG_MTU,
WG_DEFAULT_DNS,
WG_DEFAULT_ADDRESS,
WG_PERSISTENT_KEEPALIVE,
WG_ALLOWED_IPS,
WG_PRE_UP,
WG_POST_UP,
WG_PRE_DOWN,
WG_POST_DOWN,
} = require('../config');
module.exports = class WireGuard {
async getConfig() {
if (!this.__configPromise) {
this.__configPromise = Promise.resolve().then(async () => {
if (!WG_HOST) {
throw new Error('WG_HOST Environment Variable Not Set!');
}
debug('Loading configuration...');
let config;
try {
config = await fs.readFile(path.join(WG_PATH, 'wg0.json'), 'utf8');
config = JSON.parse(config);
debug('Configuration loaded.');
} catch (err) {
const privateKey = await Util.exec('wg genkey');
const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`, {
log: 'echo ***hidden*** | wg pubkey',
});
const address = WG_DEFAULT_ADDRESS.replace('x', '1');
config = {
server: {
privateKey,
publicKey,
address,
},
clients: {},
};
debug('Configuration generated.');
}
await this.__saveConfig(config);
await Util.exec('wg-quick down wg0').catch(() => { });
await Util.exec('wg-quick up wg0').catch(err => {
if (err && err.message && err.message.includes('Cannot find device "wg0"')) {
throw new Error('WireGuard exited with the error: Cannot find device "wg0"\nThis usually means that your host\'s kernel does not support WireGuard!');
}
throw err;
});
// await Util.exec(`iptables -t nat -A POSTROUTING -s ${WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o eth0 -j MASQUERADE`);
// await Util.exec('iptables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT');
// await Util.exec('iptables -A FORWARD -i wg0 -j ACCEPT');
// await Util.exec('iptables -A FORWARD -o wg0 -j ACCEPT');
await this.__syncConfig();
return config;
});
}
return this.__configPromise;
}
async saveConfig() {
const config = await this.getConfig();
await this.__saveConfig(config);
await this.__syncConfig();
}
async __saveConfig(config) {
let result = `
# Note: Do not edit this file directly.
# Your changes will be overwritten!
# Server
[Interface]
PrivateKey = ${config.server.privateKey}
Address = ${config.server.address}/24
ListenPort = 51820
PreUp = ${WG_PRE_UP}
PostUp = ${WG_POST_UP}
PreDown = ${WG_PRE_DOWN}
PostDown = ${WG_POST_DOWN}
`;
for (const [clientId, client] of Object.entries(config.clients)) {
if (!client.enabled) continue;
result += `
# Client: ${client.name} (${clientId})
[Peer]
PublicKey = ${client.publicKey}
PresharedKey = ${client.preSharedKey}
AllowedIPs = ${client.address}/32`;
}
debug('Config saving...');
await fs.writeFile(path.join(WG_PATH, 'wg0.json'), JSON.stringify(config, false, 2), {
mode: 0o660,
});
await fs.writeFile(path.join(WG_PATH, 'wg0.conf'), result, {
mode: 0o600,
});
debug('Config saved.');
}
async __syncConfig() {
debug('Config syncing...');
await Util.exec('wg syncconf wg0 <(wg-quick strip wg0)');
debug('Config synced.');
}
async getClients() {
const config = await this.getConfig();
const clients = Object.entries(config.clients).map(([clientId, client]) => ({
id: clientId,
name: client.name,
enabled: client.enabled,
address: client.address,
publicKey: client.publicKey,
createdAt: new Date(client.createdAt),
updatedAt: new Date(client.updatedAt),
allowedIPs: client.allowedIPs,
persistentKeepalive: null,
latestHandshakeAt: null,
transferRx: null,
transferTx: null,
}));
// Loop WireGuard status
const dump = await Util.exec('wg show wg0 dump', {
log: false,
});
dump
.trim()
.split('\n')
.slice(1)
.forEach(line => {
const [
publicKey,
preSharedKey, // eslint-disable-line no-unused-vars
endpoint, // eslint-disable-line no-unused-vars
allowedIps, // eslint-disable-line no-unused-vars
latestHandshakeAt,
transferRx,
transferTx,
persistentKeepalive,
] = line.split('\t');
const client = clients.find(client => client.publicKey === publicKey);
if (!client) return;
client.latestHandshakeAt = latestHandshakeAt === '0'
? null
: new Date(Number(`${latestHandshakeAt}000`));
client.transferRx = Number(transferRx);
client.transferTx = Number(transferTx);
client.persistentKeepalive = persistentKeepalive;
});
return clients;
}
async getClient({ clientId }) {
const config = await this.getConfig();
const client = config.clients[clientId];
if (!client) {
throw new ServerError(`Client Not Found: ${clientId}`, 404);
}
return client;
}
async getClientConfiguration({ clientId }) {
const config = await this.getConfig();
const client = await this.getClient({ clientId });
return `
[Interface]
PrivateKey = ${client.privateKey}
Address = ${client.address}/24
${WG_DEFAULT_DNS ? `DNS = ${WG_DEFAULT_DNS}` : ''}
${WG_MTU ? `MTU = ${WG_MTU}` : ''}
[Peer]
PublicKey = ${config.server.publicKey}
PresharedKey = ${client.preSharedKey}
AllowedIPs = ${WG_ALLOWED_IPS}
PersistentKeepalive = ${WG_PERSISTENT_KEEPALIVE}
Endpoint = ${WG_HOST}:${WG_PORT}`;
}
async getClientQRCodeSVG({ clientId }) {
const config = await this.getClientConfiguration({ clientId });
return QRCode.toString(config, {
type: 'svg',
width: 512,
});
}
async createClient({ name }) {
if (!name) {
throw new Error('Missing: Name');
}
const config = await this.getConfig();
const privateKey = await Util.exec('wg genkey');
const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`);
const preSharedKey = await Util.exec('wg genpsk');
// Calculate next IP
let address;
for (let i = 2; i < 255; i++) {
const client = Object.values(config.clients).find(client => {
return client.address === WG_DEFAULT_ADDRESS.replace('x', i);
});
if (!client) {
address = WG_DEFAULT_ADDRESS.replace('x', i);
break;
}
}
if (!address) {
throw new Error('Maximum number of clients reached.');
}
// Create Client
const clientId = uuid.v4();
const client = {
name,
address,
privateKey,
publicKey,
preSharedKey,
createdAt: new Date(),
updatedAt: new Date(),
enabled: true,
};
config.clients[clientId] = client;
await this.saveConfig();
return client;
}
async deleteClient({ clientId }) {
const config = await this.getConfig();
if (config.clients[clientId]) {
delete config.clients[clientId];
await this.saveConfig();
}
}
async enableClient({ clientId }) {
const client = await this.getClient({ clientId });
client.enabled = true;
client.updatedAt = new Date();
await this.saveConfig();
}
async disableClient({ clientId }) {
const client = await this.getClient({ clientId });
client.enabled = false;
client.updatedAt = new Date();
await this.saveConfig();
}
async updateClientName({ clientId, name }) {
const client = await this.getClient({ clientId });
client.name = name;
client.updatedAt = new Date();
await this.saveConfig();
}
async updateClientAddress({ clientId, address }) {
const client = await this.getClient({ clientId });
if (!Util.isValidIPv4(address)) {
throw new ServerError(`Invalid Address: ${address}`, 400);
}
client.address = address;
client.updatedAt = new Date();
await this.saveConfig();
}
};

326
src/main.rs

@ -0,0 +1,326 @@
mod utils;
mod wireguard;
use actix_session::{storage::CookieSessionStore, Session, SessionMiddleware};
use actix_web::{
delete, get,
http::header::{self, ContentDisposition, DispositionParam},
middleware::Logger,
post, web, App, HttpResponse, HttpServer, Responder,
};
use serde::{Deserialize, Serialize};
use utils::{misc, os};
use uuid::Uuid;
use wireguard::config::{WireGuard, WireguardSettings};
#[derive(Deserialize)]
struct Name {
pub name: String,
}
struct Static {
pub dir: String,
pub index_page: String,
pub manifest: String,
}
struct Security {
pub password: String,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all(serialize = "camelCase"))]
#[serde(rename_all(deserialize = "camelCase"))]
struct AuthCheck {
pub requires_password: bool,
pub authenticated: bool,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all(serialize = "camelCase"))]
#[serde(rename_all(deserialize = "camelCase"))]
struct AuthRequest {
pub password: String,
}
#[get("/")]
async fn index(static_files: web::Data<Static>) -> impl Responder {
HttpResponse::Ok().body(static_files.index_page.to_string())
}
#[get("/manifest.json")]
async fn manifest(static_files: web::Data<Static>) -> impl Responder {
HttpResponse::Ok().body(static_files.manifest.to_string())
}
#[get("/js/{filename:.*}")]
async fn get_script(
name: web::Path<String>,
static_files: web::Data<Static>,
) -> actix_web::Result<impl Responder> {
let path = format!("{}/js/{}", static_files.dir, name);
Ok(actix_files::NamedFile::open_async(path).await?)
}
#[get("/css/{filename:.*}")]
async fn get_style(
name: web::Path<String>,
static_files: web::Data<Static>,
) -> actix_web::Result<impl Responder> {
let path = format!("{}/css/{}", static_files.dir, name);
Ok(actix_files::NamedFile::open_async(path).await?)
}
#[get("/img/{filename:.*}")]
async fn get_image(
name: web::Path<String>,
static_files: web::Data<Static>,
) -> actix_web::Result<impl Responder> {
let path = format!("{}/img/{}", static_files.dir, name);
Ok(actix_files::NamedFile::open_async(path).await?)
}
#[get("/api/release")]
async fn release() -> impl Responder {
HttpResponse::Ok().body("7")
}
#[get("/api/session")]
async fn session_check(session: Session, security: web::Data<Security>) -> impl Responder {
let mut auth_req = AuthCheck {
authenticated: true,
requires_password: true,
};
if security.password.is_empty() {
auth_req.requires_password = false;
} else {
match session.get::<bool>("authenticated") {
Ok(auth) => match auth {
Some(val) => {
auth_req.authenticated = val;
}
None => {
auth_req.authenticated = false;
}
},
Err(_) => {
auth_req.authenticated = false;
}
}
}
HttpResponse::Ok().body(serde_json::to_string_pretty(&auth_req).unwrap())
}
#[post("/api/session")]
async fn session_login(
session: Session,
request_body: web::Json<AuthRequest>,
security: web::Data<Security>,
) -> impl Responder {
if !request_body.password.eq(&security.password) {
return HttpResponse::Unauthorized();
}
_ = session.insert("authenticated", true);
HttpResponse::NoContent()
}
#[delete("/api/session")]
async fn session_logout(session: Session) -> impl Responder {
session.clear();
HttpResponse::NoContent()
}
#[get("/api/wireguard/client")]
async fn all_clients(wireguard: web::Data<WireGuard>) -> impl Responder {
let memento = wireguard.get_memento().await;
let clients = wireguard.get_clients(&memento).await;
HttpResponse::Ok().body(serde_json::to_string_pretty(&clients).unwrap())
}
#[get("/api/wireguard/client/{id}/qrcode.svg")]
async fn client_qr(id: web::Path<Uuid>, wireguard: web::Data<WireGuard>) -> impl Responder {
let memento = wireguard.get_memento().await;
let qr = wireguard.get_client_qrcode_svg(&memento, *id).await;
HttpResponse::Ok().body(qr)
}
#[get("/api/wireguard/client/{id}/configuration")]
async fn client_config(id: web::Path<Uuid>, wireguard: web::Data<WireGuard>) -> impl Responder {
let memento = wireguard.get_memento().await;
let (name, config) = wireguard.get_client_configuration(&memento, *id);
HttpResponse::Ok()
.content_type("text/plain")
.append_header(ContentDisposition {
disposition: header::DispositionType::Attachment,
parameters: vec![
DispositionParam::Name(String::from("file")),
DispositionParam::Filename(format!("{}.conf", name)),
],
})
.body(config)
}
#[post("/api/wireguard/client")]
async fn create_client(data: web::Json<Name>, wireguard: web::Data<WireGuard>) -> impl Responder {
let mut memento = wireguard.get_memento().await;
let client = wireguard
.create_client(&mut memento, data.name.to_string())
.await;
HttpResponse::Ok().body(serde_json::to_string_pretty(client).unwrap())
}
#[delete("/api/wireguard/client/{id}")]
async fn delete_client(id: web::Path<Uuid>, wireguard: web::Data<WireGuard>) -> impl Responder {
let mut memento = wireguard.get_memento().await;
wireguard.delete_client(&mut memento, *id).await;
HttpResponse::NoContent()
}
#[post("/api/wireguard/client/{id}/enable")]
async fn enable_client(id: web::Path<Uuid>, wireguard: web::Data<WireGuard>) -> impl Responder {
// todo here is a bug that kills whole app
// let mut memento = wireguard.get_memento().await;
// wireguard.enable_client(&mut memento, *id).await;
HttpResponse::NoContent()
}
#[post("/api/wireguard/client/{id}/disable")]
async fn disable_client(id: web::Path<Uuid>, wireguard: web::Data<WireGuard>) -> impl Responder {
// let mut memento = wireguard.get_memento().await;
// wireguard.disable_client(&mut memento, *id).await;
HttpResponse::NoContent()
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "actix_web=debug,debug");
}
if std::env::var("RUST_BACKTRACE").is_err() {
std::env::set_var("RUST_BACKTRACE", "0");
}
env_logger::init();
let wireguard_path = get_env("WIREGUARD_PATH", "./data/wireguard");
let static_dir = get_env("STATIC_DIR", "./data/www");
let password = get_env("PASSWORD", "123321");
let settings = get_wireguard_settings();
let wireguard = WireGuard::new("wg0", wireguard_path, settings);
wireguard.start().await;
let index_path = format!("{}/index.html", static_dir);
let mainfest_path = format!("{}/manifest.json", static_dir);
let (_, index_page) = os::load_and_read_file_unhandled(index_path.as_str()).await;
let (_, manifest_file) = os::load_and_read_file_unhandled(mainfest_path.as_str()).await;
let security = web::Data::new(Security { password });
let web_wireguard = web::Data::new(wireguard);
let static_files = web::Data::new(Static {
dir: static_dir,
index_page,
manifest: manifest_file,
});
let key = actix_web::cookie::Key::from(&[0; 64]);
HttpServer::new(move || {
App::new()
.app_data(web_wireguard.clone())
.app_data(static_files.clone())
.app_data(security.clone())
.wrap(Logger::default())
.wrap(Logger::new("%a %{User-Agent}i"))
.wrap(
SessionMiddleware::builder(CookieSessionStore::default(), key.clone())
.cookie_secure(false)
.build(),
)
.service(release)
.service(session_check)
.service(session_login)
.service(session_logout)
.service(index)
.service(manifest)
.service(get_script)
.service(get_style)
.service(get_image)
.service(create_client)
.service(all_clients)
.service(client_qr)
.service(client_config)
.service(create_client)
.service(delete_client)
.service(enable_client)
.service(disable_client)
})
.bind(("0.0.0.0", 8080))?
.workers(1)
.run()
.await
}
fn get_wireguard_settings() -> WireguardSettings {
let interface_name = get_env("INTERFACE_NAME", "wg0");
let release_ = get_env("RELEASE", "rs0");
let api_port = get_env("API_PORT", "8080");
let password = get_env("PASSWORD", "123321");
let host = std::env::var("HOST").expect("Must set HOST env var");
let wg_port = get_env("WG_PORT", "51820");
let mtu = get_env("MTU", "0");
let persistent_keepalive = get_env("PERSISTENT_KEEPALIVE", "25");
let default_address = get_env("DEFAULT_ADDRESS", "10.8.0.1");
let default_address_base = get_env("DEFAULT_ADDRESS_BASE", "10.8.0");
let default_dns = get_env("DEFAULT_DNS", "1.1.1.1");
let allowed_ips = get_env("ALLOWED_IPS", "0.0.0.0/0, ::/0");
let default_post_up = vec![
format!(
"iptables -t nat -A POSTROUTING -s {}.0/24 -o eth0 -j MASQUERADE; ",
default_address_base
),
format!(
"iptables -A INPUT -p udp -m udp --dport {} -j ACCEPT; ",
wg_port
),
format!("iptables -A FORWARD -i {} -j ACCEPT; ", interface_name),
format!("iptables -A FORWARD -o {} -j ACCEPT;", interface_name),
];
let pre_up = get_env("PRE_UP", "");
let post_up = get_env("POST_UP", misc::multi_line(&default_post_up).as_str());
let pre_down = get_env("PRE_DOWN", "");
let post_down = get_env("POST_DOWN", "");
WireguardSettings {
interface_name,
release: release_,
api_port: api_port.parse::<i32>().expect("Invalid api port was set"),
password,
host,
wg_port: wg_port.parse::<i32>().expect("Invalid wg port was set"),
mtu: mtu.parse::<i32>().expect("Invalid mtu was set"),
persistent_keepalive: persistent_keepalive
.parse::<i32>()
.expect("Invalid keepalive was set"),
default_address,
default_address_base,
default_dns,
allowed_ips,
pre_up,
post_up,
pre_down,
post_down,
}
}
fn get_env(name: &str, default: &str) -> String {
match std::env::var(name) {
Ok(path) => path,
Err(_) => {
log::info!("{} was not set. Using default: {}.", name, default);
String::from(default)
}
}
}

2678
src/package-lock.json

File diff suppressed because it is too large

33
src/package.json

@ -1,33 +0,0 @@
{
"release": 7,
"name": "wg-easy",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"serve": "DEBUG=Server,WireGuard nodemon server.js",
"serve-with-password": "PASSWORD=wg npm run serve",
"lint": "eslint ."
},
"author": "Emile Nijssen",
"license": "GPL",
"dependencies": {
"debug": "^4.3.1",
"express": "^4.17.1",
"express-session": "^1.17.1",
"qrcode": "^1.4.4",
"uuid": "^8.3.2"
},
"devDependencies": {
"eslint": "^7.27.0",
"eslint-config-athom": "^2.1.0"
},
"nodemonConfig": {
"ignore": [
"www/*"
]
},
"engines": {
"node": "14"
}
}

14
src/server.js

@ -1,14 +0,0 @@
'use strict';
require('./services/Server');
const WireGuard = require('./services/WireGuard');
WireGuard.getConfig()
.catch(err => {
// eslint-disable-next-line no-console
console.error(err);
// eslint-disable-next-line no-process-exit
process.exit(1);
});

5
src/services/Server.js

@ -1,5 +0,0 @@
'use strict';
const Server = require('../lib/Server');
module.exports = new Server();

5
src/services/WireGuard.js

@ -1,5 +0,0 @@
'use strict';
const WireGuard = require('../lib/WireGuard');
module.exports = new WireGuard();

0
src/test/serde_test.rs

2
src/utils.rs

@ -0,0 +1,2 @@
pub mod os;
pub mod misc;

8
src/utils/misc.rs

@ -0,0 +1,8 @@
pub fn multi_line(vec: &Vec<String>) -> String {
let mut result = String::new();
for line in vec {
result.push_str(line.as_str());
}
result
}

99
src/utils/os.rs

@ -0,0 +1,99 @@
use std::{ffi::OsStr, string::FromUtf8Error};
use log::{info, warn};
use serde::Serialize;
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
process::Command,
};
#[derive(Serialize)]
pub struct StdResult {
pub stdout: String,
pub stderr: String,
}
impl StdResult {
pub fn log(&self) {
if self.stdout.len() > 0 {
info!("{}", &self.stdout);
}
if self.stderr.len() > 0 {
warn!("{}", &self.stderr);
}
}
}
pub async fn load_file_unhandled(path: &str) -> tokio::fs::File {
match tokio::fs::OpenOptions::new()
.append(false)
.read(true)
.write(true)
.create(true)
.open(path)
.await
{
Ok(file) => file,
Err(error) => {
log::error!("Could not load file {}: {}", path, error);
panic!()
}
}
}
pub async fn load_and_read_file_unhandled(path: &str) -> (tokio::fs::File, String) {
let mut buffer = String::new();
let mut file = load_file_unhandled(path).await;
match file.read_to_string(&mut buffer).await {
Ok(_) => (file, buffer),
Err(error) => {
log::error!("Could not read file data: {}", error);
panic!()
}
}
}
pub async fn write_to_path_unhandled(path: &str, data: String) -> tokio::fs::File {
let mut file = load_file_unhandled(path).await;
match file.write(data.as_bytes()).await {
Ok(_) => file,
Err(error) => {
log::error!("Could not write to file: {}", error);
panic!()
}
}
}
pub async fn exec_sh<S: AsRef<OsStr>>(command: S) -> Result<StdResult, String> {
let res = match Command::new("sh").arg("-c").arg(command).output().await {
Ok(val) => val,
Err(err) => return Err(err.to_string()),
};
let stdout_b = &res.stdout.to_vec();
let stderr_b = &res.stderr.to_vec();
let stdout = match remove_escape(stdout_b) {
Ok(val) => val,
Err(err) => return Err(err.to_string()),
};
let stderr = match remove_escape(stderr_b) {
Ok(val) => val,
Err(err) => return Err(err.to_string()),
};
Ok(StdResult { stdout, stderr })
}
pub fn remove_escape(bytes: &Vec<u8>) -> Result<String, FromUtf8Error> {
match bytes.len() == 0 {
true => Ok(String::new()),
false => match match bytes[bytes.len() - 1] == '\n' as u8 {
true => String::from_utf8(bytes[0..bytes.len() - 1].to_vec()),
false => String::from_utf8(bytes.to_vec()),
} {
Ok(val) => Ok(val),
Err(err) => Err(err),
},
}
}

2
src/wireguard.rs

@ -0,0 +1,2 @@
pub mod commands;
pub mod config;

108
src/wireguard/commands.rs

@ -0,0 +1,108 @@
// use log::info;
// use super::config::WireguardSettings;
// use crate::{
// utils::{os::{self}, misc},
// wireguard::config::{self, WireguardMemento},
// };
// // pub async fn wg_start(settings: WireguardSettings) {
// // let (wg, quick) = get_config().await;
// // sync_config().await;
// // let config_str = format_config(wg, quick).await;
// // os::write_to_path_unhandled("/etc/wireguard/wg0.conf", config_str).await;
// // _ = os::exec_bash(format!("wg-quick down wg0")).await;
// // _ = os::exec_bash(format!("wg-quick up wg0")).await;
// // //todo Add error feedback if host kernel does not support wireguard
// // }
// pub async fn get_config() -> (WireguardMemento, WireguardSettings) {
// let quick = load_wireguard_settings("./quick_config.json").await;
// log::info!("Loading configuration...");
// let wg_config_path = &mut quick.wg_path.to_owned();
// wg_config_path.push_str("/wg0");
// let wg_config: WireguardMemento;
// let (mut wg_config_file, wg_config_str) =
// os::load_and_read_file_unhandled(wg_config_path).await;
// if !wg_config_str.is_empty() {
// wg_config = serde_json::from_str(wg_config_str.as_str()).unwrap();
// } else {
// info!("Creating new WireGuard configuration...");
// let private_key = os::exec_bash(format!("wg genkey")).await;
// let public_key = os::exec_bash(format!("echo {private_key} | wg pubkey")).await;
// let address = quick.wg_default_address.repeat(1);
// wg_config = WireguardMemento::new(config::Server {
// private_key,
// public_key,
// address,
// });
// info!("WireGuard configuration generated...");
// save_config_directly(&wg_config, &mut wg_config_file).await;
// }
// (wg_config, quick)
// }
// async fn save_config_directly(config: &WireguardMemento, file: &mut tokio::fs::File) {
// let wg_config_str =
// serde_json::to_string_pretty(config).expect("Could not serialize wg_config");
// os::write_to_file_unhandled(file, wg_config_str).await;
// }
// async fn load_wireguard_settings(config_path: &str) -> WireguardSettings {
// let (_, config_str) = os::load_and_read_file_unhandled(config_path).await;
// match serde_json::from_str(&config_str) {
// Ok(config) => config,
// Err(error) => {
// log::error!(
// "Could not parse settings {}: {}",
// config_path,
// error.to_string()
// );
// panic!()
// }
// }
// }
// async fn sync_config() {
// info!("Config syncing...");
// os::exec_bash(format!("wg syncconf wg0 <(wg-quick strip wg0)")).await;
// info!("Config synced...");
// os::exec_bash(format!("wg syncconf wg0 <(wg-quick strip wg0)")).await;
// }
// // async fn format_config(config: WireguardMemento, quick: WireguardSettings) -> String {
// // let result_vec = vec![
// // format!("# Note: Do not edit this file directly.\n"),
// // format!("# Your changes will be overwritten!\n"),
// // format!("\n"),
// // format!("# Server\n"),
// // format!("[Interface]\n"),
// // format!("PrivateKey = {}\n", config.server.private_key),
// // format!("Address = {}/24\n", config.server.address),
// // format!("ListenPort = 51820\n"),
// // format!("PreUp = {}\n", multi_line(quick.wg_pre_up)),
// // format!("PostUp = {}\n", multi_line(quick.wg_post_up)),
// // format!("PreDown = {}\n", multi_line(quick.wg_pre_down)),
// // format!("PostDown = {}\n", multi_line(quick.wg_post_down)),
// // ];
// // let mut result = misc::multi_line(&result_vec);
// // for (uid, client) in config.clients {
// // if !client.enabled {
// // continue;
// // }
// // let result_inner = vec![
// // format!("\n"),
// // format!("# Client: {} ({})\n", client.name, uid),
// // format!("[Peer]\n"),
// // format!("PublicKey = {}\n", client.public_key),
// // format!("PresharedKey = {}\n", client.pre_shared_key),
// // format!("AllowedIPs = {}/32\n", client.address),
// // ];
// // result.push_str(multi_line(result_inner).as_str());
// // }
// // result
// // }

430
src/wireguard/config.rs

@ -0,0 +1,430 @@
use std::collections::HashMap;
use chrono::{SecondsFormat, SubsecRound, Utc};
use serde::{Deserialize, Serialize};
use tokio::join;
use uuid::Uuid;
use crate::utils::{misc, os};
#[derive(Serialize, Deserialize, Clone)]
pub struct WireguardSettings {
pub interface_name: String,
pub release: String,
pub api_port: i32,
pub password: String,
pub host: String,
pub wg_port: i32,
pub mtu: i32,
pub persistent_keepalive: i32,
pub default_address: String,
pub default_address_base: String,
pub default_dns: String,
pub allowed_ips: String,
pub pre_up: String,
pub post_up: String,
pub pre_down: String,
pub post_down: String,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all(serialize = "camelCase"))]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct WireguardMemento {
pub server: Server,
pub clients: HashMap<uuid::Uuid, Client>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all(serialize = "camelCase"))]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct Server {
pub private_key: String,
pub public_key: String,
pub address: String,
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all(serialize = "camelCase"))]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct Client {
pub uuid: Uuid,
pub name: String,
pub address: String,
pub private_key: String,
pub public_key: String,
pub pre_shared_key: String,
pub created_at: String,
pub updated_at: String,
pub enabled: bool,
}
impl Client {
pub fn enable(&mut self) {
self.enabled = true;
self.updated_at = Utc::now()
.trunc_subsecs(3)
.to_rfc3339_opts(SecondsFormat::Millis, true)
}
pub fn disable(&mut self) {
self.enabled = false;
self.updated_at = Utc::now()
.trunc_subsecs(3)
.to_rfc3339_opts(SecondsFormat::Millis, true)
}
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all(serialize = "camelCase"))]
#[serde(rename_all(deserialize = "snake_case"))]
pub struct WebClient {
pub id: Uuid,
pub name: String,
pub enabled: bool,
pub address: String,
pub public_key: String,
pub created_at: String,
pub updated_at: String,
pub allowed_ips: String,
pub persistent_keepalive: String,
pub latest_handshake_at: String,
pub transfer_rx: i64,
pub transfer_tx: i64,
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all(serialize = "camelCase"))]
#[serde(rename_all(deserialize = "snake_case"))]
pub struct DumpClient {
pub public_key: String,
pub pre_shared_key: String,
pub endpoint: String,
pub allowed_ips: String,
pub latest_handshake_at: String,
pub transfer_rx: i64,
pub transfer_tx: i64,
pub persistent_keepalive: String,
}
#[derive(Clone)]
pub struct WireGuard {
pub name: String,
pub config_path: String,
pub memento_path: String,
pub settings: WireguardSettings,
}
impl WireguardMemento {
pub fn new(server: Server) -> Self {
Self {
server,
clients: HashMap::new(),
}
}
}
impl WireGuard {
pub fn new(name: &str, wg_path: String, settings: WireguardSettings) -> Self {
let path = format!("{}{}", wg_path, name);
let config_path = format!("{}.conf", path);
let memento_path = format!("{}.json", path);
let name = format!("{}", name);
Self {
name,
settings,
config_path,
memento_path,
}
}
pub async fn quick_up(&self) {
let result = os::exec_sh(format!("wg-quick up {}", &self.name))
.await
.expect("Error");
result.log();
}
pub async fn quick_down(&self) {
let result = os::exec_sh(format!("wg-quick down {}", &self.name))
.await
.expect("Error");
result.log();
}
pub async fn sync_config(&self) {
let result = os::exec_sh(format!(
"wg syncconf {} <(wg-quick strip {})",
self.name, self.name
))
.await
.expect("Error syncing");
result.log();
}
pub async fn get_memento(&self) -> WireguardMemento {
let (_, memento_str) = os::load_and_read_file_unhandled(self.memento_path.as_str()).await;
if !memento_str.is_empty() {
serde_json::from_str::<WireguardMemento>(memento_str.as_str()).unwrap()
} else {
log::info!("Creating new WireGuard configuration...");
let private_key = os::exec_sh("wg genkey").await.expect("error genkey").stdout;
let public_key = os::exec_sh(format!("echo {} | wg pubkey", private_key))
.await
.expect("error pubkey")
.stdout;
let address = self.settings.default_address.repeat(1);
let memento = WireguardMemento::new(Server {
private_key,
public_key,
address,
});
log::info!("WireGuard configuration generated...");
memento
}
}
pub async fn set_memento(&self, memento: &WireguardMemento) {
let data_memento =
serde_json::to_string_pretty(memento).expect("Could not serialize memento");
let data_config = self.format_config(memento);
join!(
os::write_to_path_unhandled(self.config_path.as_str(), data_config),
os::write_to_path_unhandled(self.memento_path.as_str(), data_memento.to_string())
);
}
pub async fn start(&self) {
let memento = self.get_memento().await;
self.set_memento(&memento).await;
self.quick_down().await;
self.quick_up().await;
self.sync_config().await;
}
pub async fn create_client<'a>(
&'a self,
memento: &'a mut WireguardMemento,
name: String,
) -> &Client {
let private_key = os::exec_sh("wg genkey").await.expect("error genkey").stdout;
let pub_cmd = format!("echo {} | wg pubkey", private_key);
let public_key = os::exec_sh(pub_cmd).await.expect("error pubkey").stdout;
let pre_shared_key = os::exec_sh("wg genpsk").await.expect("error genpsk").stdout;
let mut i: u8 = 2;
let address = loop {
match memento.clients.iter().find(|(_, client)| {
client.address == format!("{}.{}", self.settings.default_address_base, i)
}) {
Some(_) => {
if i == 255 {
break None;
}
i += 1;
}
None => break Some(format!("{}.{}", self.settings.default_address_base, i)),
}
}
.expect("Maximum number of clients reached");
let uuid = Uuid::new_v4();
let now = Utc::now().trunc_subsecs(3);
let client = Client {
uuid,
name,
address,
created_at: now.to_rfc3339_opts(SecondsFormat::Secs, true),
updated_at: now.to_rfc3339_opts(SecondsFormat::Secs, true),
enabled: true,
pre_shared_key,
private_key,
public_key,
};
memento.clients.insert(uuid, client.clone());
self.set_memento(&memento).await;
self.sync_config().await;
memento.clients.get(&uuid).unwrap()
}
pub fn get_client<'a>(&'a self, memento: &'a WireguardMemento, client_id: Uuid) -> &Client {
let client = memento
.clients
.get(&client_id)
.expect("Could not find user");
client
}
pub async fn get_clients(&self, memento: &WireguardMemento) -> Vec<WebClient> {
let res = os::exec_sh("wg show wg0 dump").await.unwrap();
let lines: Vec<Vec<String>> = res
.stdout
.trim()
.split('\n')
.skip(1)
.map(|line| line.split('\t').map(|x| x.to_string()).collect())
.collect();
let lines: Vec<DumpClient> = lines
.iter()
.map(|x| DumpClient {
public_key: x[0].to_string(),
pre_shared_key: x[1].to_string(),
endpoint: x[2].to_string(),
allowed_ips: x[3].to_string(),
latest_handshake_at: x[4].to_string(),
transfer_rx: x[5].parse::<i64>().unwrap(),
transfer_tx: x[6].parse::<i64>().unwrap(),
persistent_keepalive: x[7].to_string(),
})
.collect();
let clients: Vec<WebClient> = memento
.clients
.iter()
.map(|(uuid, value)| {
let uuid = uuid.to_owned();
let value = value.to_owned();
let client_conf;
match lines.iter().find(|x| x.public_key.eq(&value.public_key)) {
Some(conf) => client_conf = conf,
None => return None,
};
Some(WebClient {
id: uuid,
name: value.name,
enabled: value.enabled,
address: value.address,
public_key: value.public_key,
created_at: value.created_at,
updated_at: value.updated_at,
allowed_ips: client_conf.allowed_ips.to_string(),
persistent_keepalive: client_conf.persistent_keepalive.to_string(),
latest_handshake_at: client_conf.latest_handshake_at.to_string(),
transfer_rx: client_conf.transfer_rx,
transfer_tx: client_conf.transfer_tx,
})
})
.filter(|x| x.is_some())
.map(|x| x.unwrap())
.collect();
clients
}
pub fn get_client_configuration(
&self,
memento: &WireguardMemento,
client_id: Uuid,
) -> (String, String) {
let client = self.get_client(memento, client_id);
let result_vec = vec![
format!("[Interface]\n"),
format!("PrivateKey = {}\n", client.private_key),
format!("Address = {}/24\n", client.address),
format!("DNS = {}\n", self.settings.default_dns),
format!("MTU = {}\n", self.settings.mtu),
format!("\n"),
format!("[Peer]\n"),
format!("PublicKey = {}\n", memento.server.public_key),
format!("PresharedKey = {}\n", client.pre_shared_key),
format!("AllowedIPs = {}\n", self.settings.allowed_ips),
format!(
"PersistentKeepalive = {}\n",
self.settings.persistent_keepalive
),
format!(
"Endpoint = {}:{}\n",
self.settings.host, self.settings.wg_port
),
];
(client.name.to_string(), misc::multi_line(&result_vec))
}
pub async fn delete_client(&self, memento: &mut WireguardMemento, client_id: Uuid) {
if memento.clients.remove(&client_id).is_some() {
self.set_memento(&memento).await;
}
}
pub async fn enable_client(&self, memento: &mut WireguardMemento, client_id: Uuid) {
match memento.clients.get_mut(&client_id) {
Some(client) => {
client.enable();
self.set_memento(&memento).await;
}
None => return,
}
}
pub async fn disable_client(&self, memento: &mut WireguardMemento, client_id: Uuid) {
match memento.clients.get_mut(&client_id) {
Some(client) => {
client.disable();
self.set_memento(&memento).await;
}
None => return,
}
}
pub async fn get_client_qrcode_svg(
&self,
memento: &WireguardMemento,
client_id: Uuid,
) -> String {
let (_, config) = self.get_client_configuration(memento, client_id);
return qrcode_generator::to_svg_to_string_from_str(
config,
qrcode_generator::QrCodeEcc::Low,
512,
None::<&str>,
)
.unwrap();
}
fn format_config(&self, config: &WireguardMemento) -> String {
let result_vec = vec![
format!("# Note: Do not edit this file directly.\n"),
format!("# Your changes will be overwritten!\n"),
format!("\n"),
format!("# Server\n"),
format!("[Interface]\n"),
format!("PrivateKey = {}\n", config.server.private_key),
format!("Address = {}/24\n", config.server.address),
format!("ListenPort = 51820\n"),
format!("PreUp = {}\n", self.settings.pre_up),
format!("PostUp = {}\n", self.settings.post_up),
format!("PreDown = {}\n", self.settings.pre_down),
format!("PostDown = {}\n", self.settings.post_down),
];
let mut result = misc::multi_line(&result_vec);
for (uid, client) in &config.clients {
if !client.enabled {
continue;
}
let result_inner = vec![
format!("\n"),
format!("# Client: {} ({})\n", client.name, uid),
format!("[Peer]\n"),
format!("PublicKey = {}\n", client.public_key),
format!("PresharedKey = {}\n", client.pre_shared_key),
format!("AllowedIPs = {}/32\n", client.address),
];
result.push_str(misc::multi_line(&result_inner).as_str());
}
result
}
}
Loading…
Cancel
Save