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