commit 0bbe91bec3ee8fe4fa0d8dfa60c4ae7d80402de5 Author: Kai Waggeling Date: Sat Dec 6 20:04:11 2025 +0100 improved Database & Models diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2a6d9f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +data/* +*test*.mjs +package-lock.json \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..347707b --- /dev/null +++ b/docker/Dockerfile @@ -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"] \ No newline at end of file diff --git a/docker/nftables.conf b/docker/nftables.conf new file mode 100644 index 0000000..4cd76ff --- /dev/null +++ b/docker/nftables.conf @@ -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; + } +} \ No newline at end of file diff --git a/docker/start.sh b/docker/start.sh new file mode 100644 index 0000000..5ad2f7b --- /dev/null +++ b/docker/start.sh @@ -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 diff --git a/docker/supervisor.conf b/docker/supervisor.conf new file mode 100644 index 0000000..33078d7 --- /dev/null +++ b/docker/supervisor.conf @@ -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 \ No newline at end of file diff --git a/docs/database.png b/docs/database.png new file mode 100644 index 0000000..f48a78c Binary files /dev/null and b/docs/database.png differ diff --git a/lib/generator/wg_config.mjs b/lib/generator/wg_config.mjs new file mode 100644 index 0000000..7e19e68 --- /dev/null +++ b/lib/generator/wg_config.mjs @@ -0,0 +1,15 @@ +import { + instance, + peer, + address +} from "../models.mjs"; + + +export async function generateWireguardServerConfig(interfaceName) { + +} + + +export async function generateWireguardClientConfig(interfaceName) { + +} \ No newline at end of file diff --git a/lib/models.mjs b/lib/models.mjs new file mode 100644 index 0000000..98e4d92 --- /dev/null +++ b/lib/models.mjs @@ -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 +} \ No newline at end of file diff --git a/lib/nftables/config.mjs b/lib/nftables/config.mjs new file mode 100644 index 0000000..9ee0b33 --- /dev/null +++ b/lib/nftables/config.mjs @@ -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 + ) +} \ No newline at end of file diff --git a/lib/wireguard/config.mjs b/lib/wireguard/config.mjs new file mode 100644 index 0000000..c2ab708 --- /dev/null +++ b/lib/wireguard/config.mjs @@ -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 + ) +} \ No newline at end of file diff --git a/lib/wireguard/process.mjs b/lib/wireguard/process.mjs new file mode 100644 index 0000000..8a44e25 --- /dev/null +++ b/lib/wireguard/process.mjs @@ -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; + } +} \ No newline at end of file diff --git a/master.mjs b/master.mjs new file mode 100644 index 0000000..6c0fab0 --- /dev/null +++ b/master.mjs @@ -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 }); +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..f2d5f0d --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/routes/api/v1/routes.mjs b/routes/api/v1/routes.mjs new file mode 100644 index 0000000..9af25de --- /dev/null +++ b/routes/api/v1/routes.mjs @@ -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) { }); + }); +} \ No newline at end of file diff --git a/templates/nftables.ejs b/templates/nftables.ejs new file mode 100644 index 0000000..fdb1968 --- /dev/null +++ b/templates/nftables.ejs @@ -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 %> +<% }) %> diff --git a/templates/wg_client.ejs b/templates/wg_client.ejs new file mode 100644 index 0000000..04e575e --- /dev/null +++ b/templates/wg_client.ejs @@ -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 %> \ No newline at end of file diff --git a/templates/wg_server.ejs b/templates/wg_server.ejs new file mode 100644 index 0000000..db59bd0 --- /dev/null +++ b/templates/wg_server.ejs @@ -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 %> + +<% }) %> diff --git a/webserver.mjs b/webserver.mjs new file mode 100644 index 0000000..4ce4afa --- /dev/null +++ b/webserver.mjs @@ -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); + } +});