@ -0,0 +1,2 @@ |
|||
/target |
|||
/.vscode |
|||
@ -1,3 +1,3 @@ |
|||
/config |
|||
/wg0.conf |
|||
/wg0.json |
|||
/target |
|||
/application |
|||
/.vscode |
|||
@ -0,0 +1,2 @@ |
|||
FROM alpine:3.17.0 AS runtime |
|||
RUN apk add -U --no-cache wireguard-tools dumb-init |
|||
@ -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 |
|||
@ -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 |
|||
@ -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 |
|||
@ -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"] |
|||
@ -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" |
|||
@ -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"] |
|||
@ -0,0 +1,3 @@ |
|||
FROM rust:1-alpine |
|||
RUN rustup target add x86_64-unknown-linux-musl |
|||
WORKDIR /usr |
|||
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
@ -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,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 |
|||
@ -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 |
|||
@ -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 |
|||
@ -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 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= |
|||
|
|||
# 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 |
|||
|
|||
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 |
|||
@ -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." |
|||
} |
|||
@ -0,0 +1 @@ |
|||
docker build -t yamzeg/rs-wg -f Application.Deploy.Dockerfile . |
|||
@ -0,0 +1 @@ |
|||
docker build -t rs-wg-local -f Application.Runtime.Dockerfile . |
|||
@ -0,0 +1 @@ |
|||
cargo install --target x86_64-unknown-linux-musl --path . --root application |
|||
@ -1,4 +0,0 @@ |
|||
{ |
|||
"version": "1.0.0", |
|||
"lockfileVersion": 1 |
|||
} |
|||
@ -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" |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
cargo build --target x86_64-unknown-linux-musl --release |
|||
@ -1,11 +0,0 @@ |
|||
{ |
|||
"extends": "athom", |
|||
"ignorePatterns": [ |
|||
"**/vendor/*.js" |
|||
], |
|||
"rules": { |
|||
"consistent-return": "off", |
|||
"no-shadow": "off", |
|||
"max-len": "off" |
|||
} |
|||
} |
|||
@ -1 +0,0 @@ |
|||
/node_modules |
|||
@ -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 || ''; |
|||
@ -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}`); |
|||
}); |
|||
} |
|||
|
|||
}; |
|||
@ -1,10 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
module.exports = class ServerError extends Error { |
|||
|
|||
constructor(message, statusCode = 500) { |
|||
super(message); |
|||
this.statusCode = statusCode; |
|||
} |
|||
|
|||
}; |
|||
@ -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()); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
}; |
|||
@ -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(); |
|||
} |
|||
|
|||
}; |
|||
@ -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) |
|||
} |
|||
} |
|||
} |
|||
@ -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" |
|||
} |
|||
} |
|||
@ -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); |
|||
}); |
|||
@ -1,5 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
const Server = require('../lib/Server'); |
|||
|
|||
module.exports = new Server(); |
|||
@ -1,5 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
const WireGuard = require('../lib/WireGuard'); |
|||
|
|||
module.exports = new WireGuard(); |
|||
@ -0,0 +1,2 @@ |
|||
pub mod os; |
|||
pub mod misc; |
|||
@ -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 |
|||
} |
|||
|
|||
@ -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), |
|||
}, |
|||
} |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
pub mod commands; |
|||
pub mod config; |
|||
@ -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
|
|||
// // }
|
|||
@ -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 |
|||
} |
|||
} |
|||