feat: initial import
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "sqlite3-inet"]
|
||||||
|
path = sqlite3-inet
|
||||||
|
url = https://github.com/AlexeyPechnikov/sqlite3-inet.git
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Postfix policy server that bans clients by their upstream Autonomous System
|
||||||
|
Wants=postfix.service
|
||||||
|
After=postfix.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/opt/as_ban_policy_server
|
||||||
|
ExecStart=/usr/bin/node /opt/as_ban_policy_server/index.mjs
|
||||||
|
User=postfix
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { DatabaseSync } from "node:sqlite";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
const MAX_TTL = 1 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
const dbPath = resolve(import.meta.dirname, "data", "service.db");
|
||||||
|
const extPath = resolve(import.meta.dirname, "data", "libsqliteipv4.so");
|
||||||
|
console.log(`Database at ${dbPath}`);
|
||||||
|
const db = new DatabaseSync(dbPath, { readonly: false, allowExtension: true });
|
||||||
|
|
||||||
|
db.loadExtension(extPath);
|
||||||
|
|
||||||
|
const schema = `
|
||||||
|
CREATE TABLE IF NOT EXISTS "banned_as" (
|
||||||
|
"as" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"is_active" BOOLEAN NOT NULL,
|
||||||
|
"comment" TEXT
|
||||||
|
"reason" TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "as_stats" (
|
||||||
|
"as" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"accepted" INT,
|
||||||
|
"rejected" INT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "cidr_cache" (
|
||||||
|
"range" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"as" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"date_created" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.exec(schema);
|
||||||
|
|
||||||
|
const getASFromCacheQuery = db.prepare(`SELECT range, "as", date_created FROM cidr_cache WHERE ISINNET(?, range) LIMIT 1`);
|
||||||
|
const deleteRangeFromCacheQuery = db.prepare(`DELETE FROM cidr_cache WHERE range = ?`);
|
||||||
|
|
||||||
|
export function getASFromCache(ip) {
|
||||||
|
const data = getASFromCacheQuery.get(ip);
|
||||||
|
|
||||||
|
if(!data) { return null };
|
||||||
|
|
||||||
|
const { range, as, date_created } = data;
|
||||||
|
|
||||||
|
if(new Date(date_created).getTime() - new Date().getTime() > MAX_TTL) {
|
||||||
|
console.log(`AS Cache for ${range} is stale. Removing`);
|
||||||
|
deleteRangeFromCacheQuery.run(range);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return as;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isASBannedQuery = db.prepare(`SELECT reason FROM banned_as WHERE "as" = ? AND is_active = true;`);
|
||||||
|
export function isASBanned(as) {
|
||||||
|
return isASBannedQuery.get(as);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateASQuery = db.prepare(`INSERT INTO cidr_cache VALUES (?, ?, ?, datetime('now', 'localtime'));`);
|
||||||
|
export function updateAS(as, range, description) {
|
||||||
|
updateASQuery.run(range, as, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
const incrementAcceptedQuery = db.prepare(`INSERT INTO as_stats ("as", accepted) VALUES (?, 1) ON CONFLICT("as") DO UPDATE SET accepted = accepted + 1;`)
|
||||||
|
const incrementRejectedQuery = db.prepare(`INSERT INTO as_stats ("as", rejected) VALUES (?, 1) ON CONFLICT("as") DO UPDATE SET rejected = rejected + 1;`)
|
||||||
|
|
||||||
|
export function incrementAccepted(as) {
|
||||||
|
incrementAcceptedQuery.run(as);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function incrementRejected(as) {
|
||||||
|
incrementRejectedQuery.run(as);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import net from "node:net";
|
||||||
|
import IpToAsn from "ip-to-asn";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getASFromCache,
|
||||||
|
updateAS,
|
||||||
|
isASBanned,
|
||||||
|
incrementRejected,
|
||||||
|
incrementAccepted,
|
||||||
|
} from "./db.mjs";
|
||||||
|
|
||||||
|
const asClient = new IpToAsn();
|
||||||
|
|
||||||
|
// Create a server
|
||||||
|
const server = net.createServer((socket) => {
|
||||||
|
// console.log(`Policy server connection established`);
|
||||||
|
|
||||||
|
socket.on("data", async (data) => {
|
||||||
|
const lines = data.toString().trim();
|
||||||
|
|
||||||
|
let response = "dunno"; // Default response
|
||||||
|
|
||||||
|
try {
|
||||||
|
const command = lines.split(/[\r\n]/).reduce((acc, i) => {
|
||||||
|
const [key, value] = i.split("=");
|
||||||
|
return { ...acc, [key]: value };
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const { client_address, client_name, sender, recipient } = command;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Incoming email from "${client_name}[${client_address}]. ${sender} -> ${recipient}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// check if IP belongs to previously cached AS records
|
||||||
|
let as = getASFromCache(client_address);
|
||||||
|
|
||||||
|
if (!as) {
|
||||||
|
// console.log(`Fetching AS for ${client_address}`);
|
||||||
|
const result = await query(client_address);
|
||||||
|
if (result) {
|
||||||
|
const { range, ASN, description } = result;
|
||||||
|
as = ASN;
|
||||||
|
updateAS(as, range, description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reason = isASBanned(as);
|
||||||
|
if (reason) {
|
||||||
|
// console.log(`AS ${as} is banned`);
|
||||||
|
response = `reject ${reason.reason}`;
|
||||||
|
incrementRejected(as);
|
||||||
|
} else {
|
||||||
|
incrementAccepted(as);
|
||||||
|
}
|
||||||
|
// Check if AS is in the ban list
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error processing command:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Verdict: ${response}`);
|
||||||
|
socket.write(`${response}\r\n`);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("end", () => {
|
||||||
|
console.log("Policy server connection ended");
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("error", (err) => {
|
||||||
|
console.error("Policy server socket error:", err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
server.listen(12346, "127.0.0.1", () => {
|
||||||
|
console.log(`Policy server running`);
|
||||||
|
});
|
||||||
|
|
||||||
|
function query(...ip) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
asClient.query(ip, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(result[ip[0]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Generated
+22
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "as_ban_policy_server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "as_ban_policy_server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-to-asn": "github:vsviridov/ip-to-asn#0779e4c247bcf736f7adc56b730bea3dbe69500a"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ip-to-asn": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "git+ssh://git@github.com/vsviridov/ip-to-asn.git#0779e4c247bcf736f7adc56b730bea3dbe69500a",
|
||||||
|
"integrity": "sha512-K986ggPV8Akw58l13tDmYoa9uB01t0ojq60qBY8q6wugsyyw3Sa9UVNN0YX7eq5z6H8CR74FvomDE4zXIQXgWQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "as_ban_policy_server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-to-asn": "github:vsviridov/ip-to-asn#0779e4c247bcf736f7adc56b730bea3dbe69500a"
|
||||||
|
}
|
||||||
|
}
|
||||||
Submodule
+1
Submodule sqlite3-inet added at 3aac8aff9a
Reference in New Issue
Block a user