improved Database & Models
This commit is contained in:
commit
0bbe91bec3
18 changed files with 956 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
node_modules/
|
||||||
|
data/*
|
||||||
|
*test*.mjs
|
||||||
|
package-lock.json
|
||||||
46
docker/Dockerfile
Normal file
46
docker/Dockerfile
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# Install required packages
|
||||||
|
# ----------------------------------------
|
||||||
|
RUN apk update && apk add --no-cache \
|
||||||
|
wireguard-tools \
|
||||||
|
wireguard-virt \
|
||||||
|
nftables \
|
||||||
|
supervisor \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
curl \
|
||||||
|
bash
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# Setup nftables base config
|
||||||
|
# You will manage rules from Node.js or mounted config
|
||||||
|
# ----------------------------------------
|
||||||
|
RUN mkdir -p /etc/nftables
|
||||||
|
COPY nftables.conf /etc/nftables/nftables.conf
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# Application
|
||||||
|
# ----------------------------------------
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY ../package.json ./
|
||||||
|
RUN npm install --production
|
||||||
|
COPY .. .
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# Supervisor config
|
||||||
|
# ----------------------------------------
|
||||||
|
COPY supervisor.conf /etc/
|
||||||
|
COPY start.sh /usr/local/bin/start.sh
|
||||||
|
RUN chmod +x /usr/local/bin/start.sh
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# Volumes
|
||||||
|
# ----------------------------------------
|
||||||
|
VOLUME ["/etc/wireguard", "/etc/nftables", "/app/data"]
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["/usr/local/bin/start.sh"]
|
||||||
28
docker/nftables.conf
Normal file
28
docker/nftables.conf
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/usr/sbin/nft -f
|
||||||
|
|
||||||
|
flush ruleset
|
||||||
|
|
||||||
|
table inet filter {
|
||||||
|
chain input {
|
||||||
|
type filter hook input priority 0;
|
||||||
|
|
||||||
|
# Accept localhost
|
||||||
|
iif lo accept
|
||||||
|
|
||||||
|
# Accept WireGuard traffic
|
||||||
|
udp dport 51820 accept
|
||||||
|
|
||||||
|
# Allow traffic from wg0 only if defined later (allowlist approach)
|
||||||
|
iif wg0 drop
|
||||||
|
}
|
||||||
|
|
||||||
|
chain forward {
|
||||||
|
type filter hook forward priority 0;
|
||||||
|
# Default deny
|
||||||
|
drop
|
||||||
|
}
|
||||||
|
|
||||||
|
chain output {
|
||||||
|
type filter hook output priority 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
docker/start.sh
Normal file
35
docker/start.sh
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# --------------------------------------------
|
||||||
|
# Ensure /etc/wireguard exists
|
||||||
|
# --------------------------------------------
|
||||||
|
if [ ! -d /etc/wireguard ]; then
|
||||||
|
echo "WARN: /etc/wireguard does not exist. Creating it..."
|
||||||
|
mkdir -p /etc/wireguard
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Default config für WireGuard
|
||||||
|
if [ ! -f /etc/wireguard/wg0.conf ]; then
|
||||||
|
echo "INFO: Installing default WireGuard config..."
|
||||||
|
cp /defaults/wg0.conf /etc/wireguard/wg0.conf
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------
|
||||||
|
# Ensure /etc/nftables exists
|
||||||
|
# --------------------------------------------
|
||||||
|
if [ ! -d /etc/nftables ]; then
|
||||||
|
echo "WARN: /etc/nftables does not exist. Creating it..."
|
||||||
|
mkdir -p /etc/nftables
|
||||||
|
fi
|
||||||
|
|
||||||
|
# default nftables.conf
|
||||||
|
if [ ! -f /etc/nftables/nftables.conf ]; then
|
||||||
|
echo "INFO: Installing default nftables.conf..."
|
||||||
|
cp /defaults/nftables.conf /etc/nftables/nftables.conf
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------
|
||||||
|
# Start Supervisor
|
||||||
|
# --------------------------------------------
|
||||||
|
exec /usr/bin/supervisord -c /etc/supervisor.conf
|
||||||
14
docker/supervisor.conf
Normal file
14
docker/supervisor.conf
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[program:nftables]
|
||||||
|
command=nft -f /etc/nftables/nftables.conf
|
||||||
|
priority=5
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/dev/fd/1
|
||||||
|
stderr_logfile=/dev/fd/2
|
||||||
|
|
||||||
|
[program:manager]
|
||||||
|
command=node /app/master.mjs
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/dev/fd/1
|
||||||
|
stderr_logfile=/dev/fd/2
|
||||||
BIN
docs/database.png
Normal file
BIN
docs/database.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
15
lib/generator/wg_config.mjs
Normal file
15
lib/generator/wg_config.mjs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import {
|
||||||
|
instance,
|
||||||
|
peer,
|
||||||
|
address
|
||||||
|
} from "../models.mjs";
|
||||||
|
|
||||||
|
|
||||||
|
export async function generateWireguardServerConfig(interfaceName) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function generateWireguardClientConfig(interfaceName) {
|
||||||
|
|
||||||
|
}
|
||||||
465
lib/models.mjs
Normal file
465
lib/models.mjs
Normal file
|
|
@ -0,0 +1,465 @@
|
||||||
|
import {
|
||||||
|
object,
|
||||||
|
array,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
enums,
|
||||||
|
boolean,
|
||||||
|
literal,
|
||||||
|
optional,
|
||||||
|
dynamic,
|
||||||
|
nullable,
|
||||||
|
defaulted,
|
||||||
|
union,
|
||||||
|
size,
|
||||||
|
create,
|
||||||
|
assert,
|
||||||
|
define,
|
||||||
|
} from "superstruct";
|
||||||
|
import { Database } from "all.db";
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
import {
|
||||||
|
ipv4,
|
||||||
|
ipv6
|
||||||
|
} from "cidr-block";
|
||||||
|
import path from "node:path";
|
||||||
|
import { EventEmitter } from "node:events";
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectModel {
|
||||||
|
#modelName;
|
||||||
|
#modelStruct;
|
||||||
|
#modelStore;
|
||||||
|
#dynRef = {};
|
||||||
|
#hooks = new EventEmitter();
|
||||||
|
|
||||||
|
constructor(settings) {
|
||||||
|
this.#modelName = settings.name;
|
||||||
|
this.#modelStruct = settings.model;
|
||||||
|
this.#modelStore = new Database({
|
||||||
|
dataPath: path.join(process.cwd(), 'data/database', `${settings.name}.json`)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (settings.hooks) {
|
||||||
|
Object.entries(settings.hooks).forEach(([eventName, eventFunction]) => {
|
||||||
|
this.on(eventName, eventFunction);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#dynRef.refOne = dynamic((objectId, context) => {
|
||||||
|
let permittedObjIds = Object.values(this.getAll()).map(item => item.id);
|
||||||
|
return enums(permittedObjIds);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#dynRef.refMany = dynamic((objectId, context) => {
|
||||||
|
let permittedObjIds = Object.values(this.getAll()).map(item => item.id);
|
||||||
|
return array(
|
||||||
|
enums(permittedObjIds)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (settings.dynRef instanceof Object) {
|
||||||
|
this.#dynRef = Object.assign(this.#dynRef, settings.dynRef)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
complete(data) {
|
||||||
|
try {
|
||||||
|
let result = create(data, this.#modelStruct);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`error completing ${this.#modelName}: ` + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
validate(data) {
|
||||||
|
try {
|
||||||
|
assert(data, this.#modelStruct);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`error validating ${this.#modelName}: ` + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll() {
|
||||||
|
return this.#modelStore.getAll();
|
||||||
|
}
|
||||||
|
getById(objectId) {
|
||||||
|
return this.#modelStore.get(objectId);
|
||||||
|
}
|
||||||
|
create(objectData) {
|
||||||
|
// try {
|
||||||
|
const newData = this.validate(
|
||||||
|
this.complete(objectData)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.#modelStore.exists(newData.id)) {
|
||||||
|
throw new Error(`failed create ${this.#modelName} with ID ${newData.id}. already existing in database.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#hooks.emit('beforeCreate', null, newData);
|
||||||
|
this.#modelStore.set(newData.id, newData);
|
||||||
|
this.#hooks.emit('afterCreate', newData);
|
||||||
|
return newData;
|
||||||
|
// } catch (error) {
|
||||||
|
// throw error;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
update(objectData) {
|
||||||
|
// try {
|
||||||
|
const newData = this.validate(
|
||||||
|
this.complete(objectData)
|
||||||
|
);
|
||||||
|
const oldData = this.getById(newData.id)
|
||||||
|
|
||||||
|
if (!oldData) {
|
||||||
|
throw new Error(`failed update ${this.#modelName} with ID ${newData.id}. not existing in database.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#hooks.emit('beforeUpdate', oldData, newData);
|
||||||
|
this.#modelStore.set(newData.id, newData);
|
||||||
|
this.#hooks.emit('afterUpdate', oldData, newData);
|
||||||
|
return newData;
|
||||||
|
// } catch (error) {
|
||||||
|
// throw error;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
delete(objectId) {
|
||||||
|
// try {
|
||||||
|
const oldData = this.getById(objectId);
|
||||||
|
|
||||||
|
if (!oldData) {
|
||||||
|
throw new Error(`failed delete ${this.#modelName} with ID ${newData.id}. not existing in database.`);
|
||||||
|
}
|
||||||
|
this.#hooks.emit('beforeDelete', oldData, null);
|
||||||
|
this.#modelStore.delete(oldData.id);
|
||||||
|
this.#hooks.emit('afterDelete', oldData, null);
|
||||||
|
return oldData;
|
||||||
|
// } catch (error) {
|
||||||
|
// throw error;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
has(objectId) {
|
||||||
|
return this.#modelStore.has(objectId);
|
||||||
|
}
|
||||||
|
get dynRef() {
|
||||||
|
return this.#dynRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
on(eventName, eventFunction) {
|
||||||
|
this.#hooks.on(eventName, eventFunction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||||
|
//
|
||||||
|
// Custom Checks
|
||||||
|
//
|
||||||
|
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||||
|
|
||||||
|
const ipv4Address = define('ipv4Address', (value) => {
|
||||||
|
return ipv4.isValidAddress(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ipv4Cidr = define('ipv4Cidr', (value) => {
|
||||||
|
console.log(value);
|
||||||
|
console.log(ipv4.isValidCIDR(value));
|
||||||
|
|
||||||
|
return ipv4.isValidCIDR(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||||
|
//
|
||||||
|
// Base Models
|
||||||
|
//
|
||||||
|
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||||
|
|
||||||
|
// Authorisation Provider Model
|
||||||
|
export const authProvider = new ObjectModel({
|
||||||
|
name: 'authProvider',
|
||||||
|
model: object({
|
||||||
|
id: defaulted(string(), () => {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}),
|
||||||
|
name: string(),
|
||||||
|
login_text: string(),
|
||||||
|
type: enums(["oauth2"]),
|
||||||
|
settings: dynamic((value, context) => {
|
||||||
|
switch (value.type) {
|
||||||
|
case "oauth2":
|
||||||
|
return object({
|
||||||
|
client_id: string(),
|
||||||
|
client_secret: string(),
|
||||||
|
authorize_url: string(),
|
||||||
|
token_url: string(),
|
||||||
|
user_info_url: string(),
|
||||||
|
redirect_url: string(),
|
||||||
|
scope: array(string())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return object({});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// User Model
|
||||||
|
export const user = new ObjectModel({
|
||||||
|
name: 'user',
|
||||||
|
model: object({
|
||||||
|
id: defaulted(string(), () => {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}),
|
||||||
|
external_id: string(),
|
||||||
|
provider: string(),
|
||||||
|
displayName: string(),
|
||||||
|
email: string()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// User Group Model
|
||||||
|
export const userGroup = new ObjectModel({
|
||||||
|
name: 'userGroup',
|
||||||
|
model: object({
|
||||||
|
id: defaulted(string(), () => {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}),
|
||||||
|
name: string(),
|
||||||
|
comment: defaulted(string(), ''),
|
||||||
|
members: user.dynRef.refMany
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||||
|
//
|
||||||
|
// Wireguard Models
|
||||||
|
//
|
||||||
|
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||||
|
|
||||||
|
// Wireguard Interface Model
|
||||||
|
export const wireguardInterface = new ObjectModel({
|
||||||
|
name: 'wireguardInterface',
|
||||||
|
model: object({
|
||||||
|
id: defaulted(string(), () => {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}),
|
||||||
|
name: size(string(), 1, 128),
|
||||||
|
comment: defaulted(size(string(), 0, 256), ''),
|
||||||
|
enabled: defaulted(boolean(), true),
|
||||||
|
privateKey: string(),
|
||||||
|
publicKey: string(),
|
||||||
|
ifName: string(),
|
||||||
|
ifAddress: ipv4Cidr,
|
||||||
|
listenPort: number(),
|
||||||
|
endpoint: string(),
|
||||||
|
dnsServer: nullable(ipv4Address)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wireguard Peer Model
|
||||||
|
export const wireguardPeer = new ObjectModel({
|
||||||
|
name: 'wireguardPeer',
|
||||||
|
model: object({
|
||||||
|
id: defaulted(string(), () => {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}),
|
||||||
|
name: size(string(), 1, 128),
|
||||||
|
comment: defaulted(size(string(), 0, 256), ''),
|
||||||
|
interface: wireguardInterface.dynRef.refOne,
|
||||||
|
enabled: defaulted(boolean(), true),
|
||||||
|
|
||||||
|
privateKey: string(),
|
||||||
|
publicKey: string(),
|
||||||
|
presharedKey: string(),
|
||||||
|
ifAddress: ipv4Cidr,
|
||||||
|
keepalive: size(defaulted(number(), 30), 1, 300),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||||
|
//
|
||||||
|
// CMDB Models
|
||||||
|
//
|
||||||
|
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||||
|
|
||||||
|
// Address Object Model
|
||||||
|
export const addressObject = new ObjectModel({
|
||||||
|
name: 'addressObject',
|
||||||
|
model: object({
|
||||||
|
id: defaulted(string(), () => {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}),
|
||||||
|
name: size(string(), 1, 128),
|
||||||
|
comment: defaulted(size(string(), 0, 256), ''),
|
||||||
|
elements: array(union([
|
||||||
|
object({
|
||||||
|
type: literal('ipmask'),
|
||||||
|
ipmask: ipv4Address
|
||||||
|
}),
|
||||||
|
object({
|
||||||
|
type: literal('fqdn'),
|
||||||
|
fqdn: string(),
|
||||||
|
result: nullable(defaulted(string(), null))
|
||||||
|
}),
|
||||||
|
object({
|
||||||
|
type: literal('peer'),
|
||||||
|
peer: wireguardPeer.dynRef.refOne
|
||||||
|
})
|
||||||
|
])),
|
||||||
|
members: dynamic((memberList, context) => {
|
||||||
|
// allow many values from list of Peer IDs
|
||||||
|
// - exclude objects that contain members (loop prevention)
|
||||||
|
// - exclude self (loop prevention)
|
||||||
|
let permittedAdressObjects = Object.values(addressObject.getAll())
|
||||||
|
.filter(object => object.members.length == 0)
|
||||||
|
.filter(object => object.id != context.id)
|
||||||
|
.map(item => item.id);
|
||||||
|
|
||||||
|
// return array from here to reduce Database reads
|
||||||
|
return array(enums(permittedAdressObjects));
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Service Object Model
|
||||||
|
export const serviceObject = new ObjectModel({
|
||||||
|
name: 'serviceObject',
|
||||||
|
model: object({
|
||||||
|
id: defaulted(string(), () => {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}),
|
||||||
|
name: size(string(), 1, 128),
|
||||||
|
comment: defaulted(size(string(), 0, 256), ''),
|
||||||
|
elements: array(union([
|
||||||
|
object({
|
||||||
|
type: literal('tcp'),
|
||||||
|
port: number()
|
||||||
|
}),
|
||||||
|
object({
|
||||||
|
type: literal('udp'),
|
||||||
|
port: number()
|
||||||
|
}),
|
||||||
|
object({
|
||||||
|
type: literal('icmp'),
|
||||||
|
icmpType: enums([
|
||||||
|
"echo-reply",
|
||||||
|
"destination-unreachable",
|
||||||
|
"source-quench",
|
||||||
|
"redirect",
|
||||||
|
"echo-request",
|
||||||
|
"time-exceeded",
|
||||||
|
"parameter-problem",
|
||||||
|
"timestamp-request",
|
||||||
|
"timestamp-reply",
|
||||||
|
"info-request",
|
||||||
|
"info-reply",
|
||||||
|
"address-mask-request",
|
||||||
|
"address-mask-reply",
|
||||||
|
"router-advertisement",
|
||||||
|
"router-solicitation"
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
object({
|
||||||
|
type: literal('esp'),
|
||||||
|
port: number()
|
||||||
|
})
|
||||||
|
]))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||||
|
//
|
||||||
|
// Firewall Policy Models
|
||||||
|
//
|
||||||
|
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||||
|
|
||||||
|
export const accessPolicy = new ObjectModel({
|
||||||
|
name: 'accessPolicy',
|
||||||
|
model: object({
|
||||||
|
id: defaulted(string(), () => {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}),
|
||||||
|
name: size(string(), 1, 128),
|
||||||
|
comment: defaulted(size(string(), 0, 256), ''),
|
||||||
|
enabled: defaulted(boolean(), true),
|
||||||
|
|
||||||
|
srcAddr: addressObject.dynRef.refMany,
|
||||||
|
dstAddr: addressObject.dynRef.refMany,
|
||||||
|
services: serviceObject.dynRef.refMany,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const natPolicy = new ObjectModel({
|
||||||
|
name: 'natPolicy',
|
||||||
|
model: object({
|
||||||
|
id: defaulted(string(), () => {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}),
|
||||||
|
name: size(string(), 1, 128),
|
||||||
|
comment: defaulted(size(string(), 0, 256), ''),
|
||||||
|
enabled: defaulted(boolean(), true),
|
||||||
|
|
||||||
|
natType: enums(['snat', 'dnat', ' masquerade']),
|
||||||
|
srcAddress: string(),
|
||||||
|
dstAddress: string(),
|
||||||
|
dnatPort: optional(number()), // Ziel Port für DNAT
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||||
|
//
|
||||||
|
// Access Control Models
|
||||||
|
//
|
||||||
|
// - --- --- --- --- --- --- --- --- --- --- --- ---
|
||||||
|
|
||||||
|
export const configToken = new ObjectModel({
|
||||||
|
name: 'configToken',
|
||||||
|
model: object({
|
||||||
|
id: defaulted(string(), () => {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}),
|
||||||
|
token: defaulted(string(), () => {
|
||||||
|
return crypto.randomBytes(32).toString('hex');
|
||||||
|
}),
|
||||||
|
peer: string(),
|
||||||
|
name: string()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiToken = new ObjectModel({
|
||||||
|
name: 'apiToken',
|
||||||
|
model: object({
|
||||||
|
id: defaulted(string(), () => {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}),
|
||||||
|
token: defaulted(string(), () => {
|
||||||
|
return crypto.randomBytes(32).toString('hex');
|
||||||
|
}),
|
||||||
|
name: string()
|
||||||
|
}),
|
||||||
|
hooks: {
|
||||||
|
beforeCreate: (oldData, newData) => {
|
||||||
|
console.log("before API Token created");
|
||||||
|
},
|
||||||
|
afterCreate: (oldData, newData) => {
|
||||||
|
console.log("after API Token created");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
wireguardInterface,
|
||||||
|
wireguardPeer,
|
||||||
|
addressObject,
|
||||||
|
authProvider,
|
||||||
|
user,
|
||||||
|
userGroup,
|
||||||
|
accessPolicy,
|
||||||
|
natPolicy,
|
||||||
|
configToken,
|
||||||
|
apiToken
|
||||||
|
}
|
||||||
40
lib/nftables/config.mjs
Normal file
40
lib/nftables/config.mjs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import {
|
||||||
|
wireguardInterface,
|
||||||
|
wireguardPeer,
|
||||||
|
addressObject,
|
||||||
|
accessPolicy,
|
||||||
|
natPolicy
|
||||||
|
} from "../models.mjs";
|
||||||
|
|
||||||
|
import path from "node:path";
|
||||||
|
import file from "node:fs";
|
||||||
|
import ejs from "ejs";
|
||||||
|
|
||||||
|
|
||||||
|
const configTemplatePath = path.join(process.cwd(), 'templates', 'nftables.ejs');
|
||||||
|
|
||||||
|
|
||||||
|
export async function generateNftablesConfig(interfaceId) {
|
||||||
|
const ifData = Object.values(wireguardInterface.getAll()).find(fi => fi.id == interfaceId);
|
||||||
|
const addressObjects = addressObject.getAll();
|
||||||
|
const accessPolicies = accessPolicy.getAll();
|
||||||
|
const natPolicies = natPolicy.getAll();
|
||||||
|
|
||||||
|
const configData = {
|
||||||
|
interface: ifData,
|
||||||
|
peerList
|
||||||
|
};
|
||||||
|
|
||||||
|
const configContent = await ejs.renderFile(
|
||||||
|
configTemplatePath,
|
||||||
|
configData,
|
||||||
|
{
|
||||||
|
async: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
file.writeFileSync(
|
||||||
|
path.join(process.cwd(), 'data', 'nftables', `${ifData.ifName}.conf`),
|
||||||
|
configContent
|
||||||
|
)
|
||||||
|
}
|
||||||
34
lib/wireguard/config.mjs
Normal file
34
lib/wireguard/config.mjs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import {
|
||||||
|
wireguardInterface,
|
||||||
|
wireguardPeer
|
||||||
|
} from "../models.mjs";
|
||||||
|
|
||||||
|
import path from "node:path";
|
||||||
|
import file from "node:fs";
|
||||||
|
import ejs from "ejs";
|
||||||
|
|
||||||
|
|
||||||
|
const serverTemplatePath = path.join(process.cwd(), 'templates', 'wg_server.ejs');
|
||||||
|
const clientTemplatePath = path.join(process.cwd(), 'templates', 'wg_client.ejs');
|
||||||
|
|
||||||
|
|
||||||
|
export async function generateInterfaceConfig(interfaceId) {
|
||||||
|
const ifData = Object.values(wireguardInterface.getAll()).find(fi => fi.id == interfaceId);
|
||||||
|
const peerList = Object.values(wireguardPeer.getAll()).filter(fi => fi.interface == interfaceId);
|
||||||
|
|
||||||
|
let configData = await ejs.renderFile(
|
||||||
|
serverTemplatePath,
|
||||||
|
{
|
||||||
|
interface: ifData,
|
||||||
|
peerList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
async: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
file.writeFileSync(
|
||||||
|
path.join(process.cwd(), 'data', 'wireguard', `${ifData.ifName}.conf`),
|
||||||
|
configData
|
||||||
|
)
|
||||||
|
}
|
||||||
45
lib/wireguard/process.mjs
Normal file
45
lib/wireguard/process.mjs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import {
|
||||||
|
execSync as execCommand
|
||||||
|
} from "node:child_process";
|
||||||
|
|
||||||
|
import {
|
||||||
|
wireguardInterface
|
||||||
|
} from "../models.mjs";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export async function startWireguardInterface(interfaceid) {
|
||||||
|
try {
|
||||||
|
const ifData = wireguardInterface.getById(interfaceid);
|
||||||
|
const ifLink = getInterfaceLink(ifData.ifName);
|
||||||
|
|
||||||
|
if (ifLink.operstate == 'UP') {
|
||||||
|
console.log(`Reloading interface ${ifData.ifName}`);
|
||||||
|
console.log(`» reloading interface ${ifData.ifName}`);
|
||||||
|
execCommand(`wg-quick up ${ifData.ifName}`, { stdio: 'inherit' });
|
||||||
|
console.log(`✓ reloaded interface ${ifData.ifName}`);
|
||||||
|
} else {
|
||||||
|
console.log(`» starting interface ${ifData.ifName}`);
|
||||||
|
execCommand(`wg-quick up ${ifData.ifName}`, { stdio: 'inherit' });
|
||||||
|
console.log(`✓ started interface ${ifData.ifName}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`✕ failed to start interface ${iface}:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function getInterfaceLink(ifName) {
|
||||||
|
try {
|
||||||
|
let result = JSON.parse(
|
||||||
|
execCommand(`ip -j link show`, { stdio: 'inherit' })
|
||||||
|
).find(linkResult => linkResult.ifname == ifName);
|
||||||
|
|
||||||
|
if (!result) { return undefined; }
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`failed to get interface link for ${ifName}:`, error.message);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
master.mjs
Normal file
26
master.mjs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
import "./webserver.mjs";
|
||||||
|
import {
|
||||||
|
apiToken,
|
||||||
|
wireguardInterface
|
||||||
|
} from "./lib/models.mjs";
|
||||||
|
|
||||||
|
|
||||||
|
// create database directory if not exists
|
||||||
|
const databaseDir = path.join(process.cwd(), 'data', 'database');
|
||||||
|
if (!fs.existsSync(databaseDir)) {
|
||||||
|
fs.mkdirSync(databaseDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// create database directory if not exists
|
||||||
|
const wgConfigDir = path.join(process.cwd(), 'data', 'wireguard');
|
||||||
|
if (!fs.existsSync(wgConfigDir)) {
|
||||||
|
fs.mkdirSync(wgConfigDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// create database directory if not exists
|
||||||
|
const nftConfigDir = path.join(process.cwd(), 'data', 'nftables');
|
||||||
|
if (!fs.existsSync(nftConfigDir)) {
|
||||||
|
fs.mkdirSync(nftConfigDir, { recursive: true });
|
||||||
|
}
|
||||||
21
package.json
Normal file
21
package.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"name": "wireguard-manager",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/autoload": "^6.3.1",
|
||||||
|
"@fastify/view": "^11.1.1",
|
||||||
|
"all.db": "^0.3.2",
|
||||||
|
"cidr-block": "^2.1.1",
|
||||||
|
"ejs": "^3.1.10",
|
||||||
|
"fastify": "^5.6.2",
|
||||||
|
"superstruct": "^2.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
54
routes/api/v1/routes.mjs
Normal file
54
routes/api/v1/routes.mjs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import models from "../../../lib/models.mjs";
|
||||||
|
|
||||||
|
|
||||||
|
export default async function (fastify, opts) {
|
||||||
|
fastify.register(async function secureApiContext(secureApiScope) {
|
||||||
|
// check API token
|
||||||
|
secureApiScope.addHook('onRequest', async (request, reply) => {
|
||||||
|
if (!request.headers.authorization) {
|
||||||
|
reply.code(401).send({ error: 'Unauthorized: missing Authorization header' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = Object.values(await models.apiToken.getAll()).find(t => t.token === request.headers.authorization.replace('Bearer ', ''));
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
reply.code(403).send({ error: 'Forbidden: invalid Token' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// check requested Model Type
|
||||||
|
secureApiScope.addHook('onRequest', async (request, reply) => {
|
||||||
|
if (!request.params.modelType) {
|
||||||
|
reply.code(400).send({ error: 'Bad Request: missing Model Type' });
|
||||||
|
}
|
||||||
|
if (!models[request.params.modelType]) {
|
||||||
|
reply.code(404).send({ error: 'Unavailable: requested Model is not available' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
secureApiScope.get("/:modelType", async function (request, reply) {
|
||||||
|
const objects = await models[request.params.modelType].getAll();
|
||||||
|
reply.send(objects);
|
||||||
|
});
|
||||||
|
secureApiScope.post("/:modelType", async function (request, reply) {
|
||||||
|
try {
|
||||||
|
const newObject = await models[request.params.modelType].create(request.body);
|
||||||
|
reply.send(newObject);
|
||||||
|
} catch (error) {
|
||||||
|
reply.code(500).send({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
secureApiScope.get("/:modelType/:resourceId", async function (request, reply) {
|
||||||
|
const object = await models[request.params.modelType].getById(request.params.resourceId);
|
||||||
|
if (object) {
|
||||||
|
reply.send(object);
|
||||||
|
} else {
|
||||||
|
reply.code(404).send({ error: 'Unavailable: requested Object is not available' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
secureApiScope.patch("/:modelType/:resourceId", async function (request, reply) { });
|
||||||
|
secureApiScope.delete("/:modelType/:resourceId", async function (request, reply) { });
|
||||||
|
});
|
||||||
|
}
|
||||||
60
templates/nftables.ejs
Normal file
60
templates/nftables.ejs
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
#!/usr/sbin/nft -f
|
||||||
|
|
||||||
|
# Lösche alte Tabelle
|
||||||
|
flush ruleset
|
||||||
|
|
||||||
|
table inet <%= interface.ifName %> {
|
||||||
|
|
||||||
|
<% addressGroupList.forEach((addressGroup) => { %>
|
||||||
|
set addressGroup_<%= addressGroup.name %> {
|
||||||
|
type ipv4_addr
|
||||||
|
flags interval
|
||||||
|
elements = { <%= addressGroup.addressList.join(", ") %> }
|
||||||
|
}
|
||||||
|
<% }) %>
|
||||||
|
|
||||||
|
<% addressGroupList.forEach((addressGroup) => { %>
|
||||||
|
set addressGroup_<%= addressGroup.name %> {
|
||||||
|
type ipv4_addr
|
||||||
|
flags interval
|
||||||
|
elements = { <%= addressGroup.addressList.join(", ") %> }
|
||||||
|
}
|
||||||
|
<% }) %>
|
||||||
|
|
||||||
|
chain input_<%= interface.ifName %> {
|
||||||
|
type filter hook input priority 0; policy drop;
|
||||||
|
|
||||||
|
# Traffic vom Interface akzeptieren
|
||||||
|
iif "<%= interface %>" tcp dport { 22, 53 } accept
|
||||||
|
iif "<%= interface %>" udp dport 53 accept
|
||||||
|
iif "<%= interface %>" icmp type echo-request accept
|
||||||
|
iif "<%= interface %>" ip saddr @allowed_sources_<%= instanceId %> counter accept
|
||||||
|
}
|
||||||
|
|
||||||
|
chain forward_<%= interface.ifName %> {
|
||||||
|
type filter hook forward priority 0; policy drop;
|
||||||
|
|
||||||
|
# Eingehende Pakete von erlaubten IPs weiterleiten
|
||||||
|
iif "<%= interface %>" ip saddr @allowed_sources_<%= instanceId %> ip daddr @allowed_destinations_<%= instanceId %> accept
|
||||||
|
|
||||||
|
# Rückläufige Antworten zulassen (established connections)
|
||||||
|
oif "<%= interface %>" ip saddr @allowed_destinations_<%= instanceId %> ip daddr @allowed_sources_<%= instanceId %> ct state established accept
|
||||||
|
}
|
||||||
|
|
||||||
|
chain output_<%= interface.ifName %> {
|
||||||
|
type filter hook output priority 0; policy accept;
|
||||||
|
|
||||||
|
# Host -> WG Interface
|
||||||
|
oif "<%= interface %>" ip daddr @allowed_destinations_<%= instanceId %> accept
|
||||||
|
}
|
||||||
|
|
||||||
|
chain postrouting_<%= interface.ifName %> {
|
||||||
|
type route hook output priority 100; policy accept;
|
||||||
|
ip saddr <%= localSubnet %> oif "<%= outboundInterface %>" masquerade
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
<% accessRuleList.forEach((accessRule) => { %>
|
||||||
|
<%= accessRule.proto %> dport <%= accessRule.dstport %> ip saddr
|
||||||
|
# Description: <%= accessRule.description %>
|
||||||
|
<% }) %>
|
||||||
11
templates/wg_client.ejs
Normal file
11
templates/wg_client.ejs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[Interface]
|
||||||
|
PrivateKey = <%= client.PrivateKey %>
|
||||||
|
Address = <%= client.allowedIps %>
|
||||||
|
DNS = 1.1.1.1
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = <%= server.publicKey %>
|
||||||
|
PresharedKey = <%= client.presharedKey %>
|
||||||
|
Endpoint = <%= server.endpoint %>:<%= server.listenPort %>
|
||||||
|
AllowedIPs = 0.0.0.0/0
|
||||||
|
PersistentKeepalive = <%= client.allowedIps %>
|
||||||
14
templates/wg_server.ejs
Normal file
14
templates/wg_server.ejs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[Interface]
|
||||||
|
Address = <%= interface.ifAddress %>
|
||||||
|
<% if (interface.dnsServer) { %>
|
||||||
|
DNS = <%= interface.dnsServer %>
|
||||||
|
<% } %>
|
||||||
|
PrivateKey = <%= interface.privateKey %>
|
||||||
|
ListenPort = <%= interface.listenPort %>
|
||||||
|
|
||||||
|
<% peerList.forEach((peer) => { %>
|
||||||
|
[Peer]
|
||||||
|
# <%= peer.name %>
|
||||||
|
PublicKey = <%= peer.publicKey %>
|
||||||
|
|
||||||
|
<% }) %>
|
||||||
44
webserver.mjs
Normal file
44
webserver.mjs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import fastify from "fastify";
|
||||||
|
import fastifyView from '@fastify/view';
|
||||||
|
import autoLoad from "@fastify/autoload";
|
||||||
|
import path from "node:path";
|
||||||
|
import ejs from "ejs";
|
||||||
|
|
||||||
|
|
||||||
|
const webServer = fastify({ logger: false });
|
||||||
|
|
||||||
|
// EJS-Renderer
|
||||||
|
webServer.register(fastifyView, {
|
||||||
|
root: path.join(process.cwd(), 'templates'),
|
||||||
|
engine: {
|
||||||
|
ejs
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// file based route loader
|
||||||
|
webServer.register(autoLoad, {
|
||||||
|
dir: path.join(process.cwd(), "routes"),
|
||||||
|
dirNameRoutePrefix: true,
|
||||||
|
routeParams: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// load plugins
|
||||||
|
// import registerSSOPlugin from './plugins/sso.mjs';
|
||||||
|
// await registerSSOPlugin(webServer)
|
||||||
|
|
||||||
|
// error handling
|
||||||
|
webServer.setErrorHandler((error, request, reply) => {
|
||||||
|
// gezielt Fehler anzeigen, aber nicht alles loggen
|
||||||
|
console.error("! Fehler in Route:", request.url);
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
reply.status(500).send({ error: "Internal Server Error" });
|
||||||
|
});
|
||||||
|
|
||||||
|
// start server
|
||||||
|
webServer.listen({ port: 3000 }, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue