From 1b023ea802ef231d1dce4ada0e571c9b0e0e9ad9 Mon Sep 17 00:00:00 2001 From: May P Date: Sat, 21 Mar 2026 14:54:25 +0200 Subject: [PATCH] removed unused stuff, changed natQ to use opnsense api --- .env.example | 10 ++--- index.js | 2 - package.json | 4 +- routes/exec.js | 63 -------------------------- routes/webhook.js | 5 +-- services/natQueue.js | 104 +++++++++++++++++++++++++++++++++---------- services/sshExec.js | 58 ------------------------ 7 files changed, 88 insertions(+), 158 deletions(-) delete mode 100644 routes/exec.js delete mode 100644 services/sshExec.js diff --git a/.env.example b/.env.example index 58dd0a2..ae13a0c 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,10 @@ -ROUTER_HOST=192.168.0.1 -ROUTER_USER=username -ROUTER_PASS=password +OPNSENSE_HOST=192.168.1.1 +OPNSENSE_KEY=keeeeey +OPNSENSE_SECRET=seeeecret WEBHOOK_SECRET=secret -ROUTER_WAN_IF=gi0 - -BIND_ADDRESS=192.168.0.2 +BIND_ADDRESS=192.168.1.2 ADMIN_PASS=password diff --git a/index.js b/index.js index 6c01cf5..1e808d7 100644 --- a/index.js +++ b/index.js @@ -8,7 +8,6 @@ import { fileURLToPath } from 'url'; dotenv.config(); import webhookRouter from './routes/webhook.js'; -import execRouter from './routes/exec.js'; import { info } from './util/Console.js'; @@ -19,7 +18,6 @@ app.use(cookieParser()); app.use(express.static('public')); app.use('/webhook', webhookRouter); -app.use('/exec', execRouter); const PORT = process.env.PORT || 3000; const HOST = process.env.BIND_ADDRESS || '0.0.0.0'; diff --git a/package.json b/package.json index 6e1decc..a8bde2a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "smithy-pelican-router-webhook-api", - "version": "0.0.1", - "description": "api to manage the router for pelican servers", + "version": "0.0.2", + "description": "api to manage opnsense rules for pelican servers", "license": "ISC", "author": "may p", "type": "module", diff --git a/routes/exec.js b/routes/exec.js deleted file mode 100644 index 86c6923..0000000 --- a/routes/exec.js +++ /dev/null @@ -1,63 +0,0 @@ -import express from 'express'; -import { enqueueCmd } from '../services/natQueue.js'; -import { runRouterCmds } from '../services/sshExec.js'; -import { error, info } from '../util/Console.js'; - -const router = express.Router(); - -const SAFE_CMD_REGX = /^\s*(show|ping|traceroute|terminal length|show ip|show interfaces|show version|show running-config|show ip nat)\b/i; - -function checkAdminToken(req) { - const token = req.headers['x-admin-token'] || req.body.token || req.query.token; - const saved = req.app.get('adminToken'); - const expiry = req.app.get('adminTokenExpory') || 0; - if (!token || !saved) return false; - if (Date.now() > expiry) return false; - return token === saved; -} - -router.post('/', async (req, res) => { - const cmd = (req.body && req.body.cmd) || ''; - if (!cmd) return res.status(400).json({ error: 'no command' }); - const isAdmin = checkAdminToken(req); - if (!isAdmin && !SAFE_CMD_REGX.test(cmd)) return res.status(403).json({ error: 'not allowed' }); - - info(`Exec requested${isAdmin ? ' [admin]' : ''}: ${cmd}`); - - try { - enqueueCmd(() => runRouterCmds([cmd])); - res.json({ success: true }); - } catch (err) { - error(err); - res.status(500).json({ error: 'exec failed' }); - } -}); - -router.get('/stream', (req, res) => { - const cmd = req.query.cmd || ''; - if (!cmd) return res.status(400).send('no command'); - const isAdmin = checkAdminToken(req); - if (!isAdmin && !SAFE_CMD_REGX.test(cmd)) return res.status(400).send('not allowed'); - - res.setHeader('Content-Type', 'text/event-stream'); - res.setHeader('Cache-Control', 'no-cache'); - res.setHeader('Connection', 'keep-alive'); - res.flushHeaders && res.flushHeaders(); - - enqueueCmd(() => new Promise(async (resolve) => { - try { - await runRouterCmds([cmd], chunk => { - res.write(`data: ${chunk.replace(/\n/g, '\\n')}\n\n`); - }); - res.write('data: [[EOF]]\n\n'); - res.end(); - resolve() - } catch (err) { - res.write(`data: [ERROR] ${String(err)}\n\n`); - res.end(); - resolve(); - } - })); -}); - -export default router; \ No newline at end of file diff --git a/routes/webhook.js b/routes/webhook.js index 60f0ec6..a402bb3 100644 --- a/routes/webhook.js +++ b/routes/webhook.js @@ -13,12 +13,11 @@ router.post('/', (req, res) => { const event = body.event || ''; const data = body || {}; info(`Webhook: ${event}\n${JSON.stringify(body)}`); - // return res.status(418).json({ error: 'im a teapot' }); // temporary try { if (event === 'created: Allocation') { - enqueueNatAdd(data.ip, data.port, 'tcp'); + enqueueNatAdd(data.ip, data.port); } else if (event === 'deleted: Allocation') { - enqueueNatRemove(data.ip, data.port, 'tcp'); + enqueueNatRemove(data.ip, data.port); } else { warn(`Unhandled webhook event: ${event}`); } diff --git a/services/natQueue.js b/services/natQueue.js index c25a79e..3770682 100644 --- a/services/natQueue.js +++ b/services/natQueue.js @@ -1,5 +1,4 @@ import { error, info, success } from "../util/Console.js"; -import { runRouterCmds } from "./sshExec.js"; const queue = []; let running = false; @@ -13,38 +12,95 @@ async function processQueue() { if (queue.length === 0) { running = false; return; } running = true; const t = queue.shift(); - try { await t(); } catch (err) { error('Queue error: ' + String(err)); } + try { await t(); } catch (err) { error('Q error ' + err); } setImmediate(processQueue); } -export function enqueueCmd(fn) { - enqueue(() => fn()); +async function opnsenseRequest(method, path, body = null) { + const auth = Buffer.from(`${process.env.OPNSENSE_KEY}:${process.env.OPNSENSE_SECRET}`).toString('base64'); + const opts = { + method, + headers: { + 'Authorization': `Basic ${auth}`, + 'Content-Type': 'application/json', + }, + }; + if (body) opts.body = JSON.stringify(body); + const res = await fetch(`${process.env.OPNSENSE_HOST}${path}`, opts); + const text = await res.text(); + try { + return JSON.parse(text); + } catch { + error(`got non-json response: ${text}`); + return null; + } } -export function enqueueNatAdd(ip, port, proto='tcp') { +async function applyRules() { + await opnsenseRequest('POST', '/api/firewall/d_nat/apply'); +} + +export function enqueueNatAdd(ip, port, proto = 'tcp') { enqueue(async () => { - info(`NAT ADD ${proto} ${ip}:${port}`); - await runRouterCmds([ - 'conf t', - `ip nat inside source static tcp ${ip} ${port} interface ${process.env.ROUTER_WAN_IF || 'gi0'} ${port}`, - `ip nat inside source static udp ${ip} ${port} interface ${process.env.ROUTER_WAN_IF || 'gi0'} ${port}`, - 'end', - 'wr mem' - ]); - success(`NAT ADDED ${proto} ${ip}:${port}`); + info(`adding rule udp+tcp at ${ip}:${port}`); + + await opnsenseRequest('POST', '/api/firewall/d_nat/addRule', { + rule: { + interface: 'wan', + protocol: 'tcp', + destination: { + network: 'wanip', + port: `${port}`, + }, + target: ip, + 'local-port': `${port}`, + descr: `pelican-${ip}-${port}-tcp`, + disabled: '0', + } + }); + + await opnsenseRequest('POST', '/api/firewall/d_nat/addRule', { + rule: { + interface: 'wan', + protocol: 'udp', + destination: { + network: 'wanip', + port: `${port}`, + }, + target: ip, + 'local-port': `${port}`, + descr: `pelican-${ip}-${port}-udp`, + disabled: '0', + } + }); + + await applyRules(); + success(`added rule udp+tcp at ${ip}:${port}`); }); } -export function enqueueNatRemove(ip, port, proto='tcp') { +export function enqueueNatRemove(ip, port, proto = 'tcp') { enqueue(async () => { - info(`NAT DEL ${proto} ${ip}:${port}`); - await runRouterCmds([ - 'conf t', - `no ip nat inside source static tcp ${ip} ${port} interface ${process.env.ROUTER_WAN_IF || 'gi0'} ${port}`, - `no ip nat inside source static udp ${ip} ${port} interface ${process.env.ROUTER_WAN_IF || 'gi0'} ${port}`, - 'end', - 'wr mem' - ]); - success(`NAT DELETED ${proto} ${ip}:${port}`); + info(`deleting rule udp+tcp at ${ip}:${port}`); + + const result = await opnsenseRequest('POST', '/api/firewall/d_nat/searchRule', { + current: 1, + rowCount: 1000, + searchPhrase: `pelican-${ip}-${port}`, + }) + + const rows = result?.rows || []; + + for (const rule of rows) { + if ( + rule.descr === `pelican-${ip}-${port}-udp` || + rule.descr === `pelican-${ip}-${port}-tcp` + ) { + await opnsenseRequest('POST', `/api/firewall/d_nat/delRule/${rule.uuid}`); + } + } + + await applyRules(); + success(`deleted rule udp+tcp at ${ip}:${port}`); }); } \ No newline at end of file diff --git a/services/sshExec.js b/services/sshExec.js deleted file mode 100644 index dc26d22..0000000 --- a/services/sshExec.js +++ /dev/null @@ -1,58 +0,0 @@ -import { Client } from "ssh2"; -import { info } from "../util/Console.js"; -import fs from 'fs'; - - -export function runRouterCmds(cmds, onData) { - const ROUTER = { - host: process.env.ROUTER_HOST || '192.168.0.1', - username: process.env.ROUTER_USER || 'cisco', - password: process.env.ROUTER_PASS || '', - wanInterface: process.env.ROUTER_WAN_IF || 'gi0' - }; - - return new Promise((resolve, reject) => { - const conn = new Client(); - let out = ''; - conn.on('ready', () => { - conn.shell((err, stream) => { - if (err) return reject(err); - stream.on('data', d => { - const s = d.toString(); - out += s; - if (onData) onData(s); - }); - stream.on('close', () => { - conn.end(); - resolve(out); - }); - cmds.forEach(c => stream.write(c + '\n')); - stream.end('exit\n'); - }); - }).on('error', reject).connect({ - host: ROUTER.host, - username: ROUTER.username, - password: ROUTER.password, - readyTimeout: 20000, - algorithms: { - kex: [ - 'diffie-hellman-group14-sha1', - 'diffie-hellman-group-exchange-sha1', - ], - cipher: [ - 'aes128-ctr', - 'aes192-ctr', - 'aes256-ctr', - ], - serverHostKey: [ - 'ssh-rsa', - ], - hmac: [ - 'hmac-sha1', - 'hmac-sha2-256', - ] - }, - hostVerifier: () => true - }); - }); -} \ No newline at end of file