removed unused stuff, changed natQ to use opnsense api
This commit is contained in:
10
.env.example
10
.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
|
||||
|
||||
|
||||
2
index.js
2
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';
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user