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