From 5d9cd3c207349f667bffc0a8345788446699db80 Mon Sep 17 00:00:00 2001 From: Artyom Titov Date: Wed, 26 Aug 2026 21:44:58 +0300 Subject: [PATCH] Created daemon python script. --- .gitignore | 1 + Cargo.lock | 293 +++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 7 ++ install.zsh | 110 +++++++++++++++++++ py/daemon.py | 171 ++++++++++++++++++++++++++++++ src/main.rs | 48 +++++++++ test.json | 21 ++++ 7 files changed, 651 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100755 install.zsh create mode 100755 py/daemon.py create mode 100644 src/main.rs create mode 100644 test.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e6aaad6 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,293 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "find-msvc-tools" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "ipban" +version = "0.1.0" +dependencies = [ + "rusqlite", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror", +] + +[[package]] +name = "rusqlite" +version = "0.40.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1d53942 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "ipban" +version = "0.1.0" +edition = "2024" + +[dependencies] +rusqlite = "0.40.0" diff --git a/install.zsh b/install.zsh new file mode 100755 index 0000000..e6b9c5d --- /dev/null +++ b/install.zsh @@ -0,0 +1,110 @@ +#!/bin/zsh + +# synapsys: +# cout "output_type" "message" +# supported output types: +# inf war err +cout() { + local msg="$2" + local lead="[=]" + if [[ "$1" == "war" ]]; then + lead="[?]" + fi + if [[ "$1" == "inf" ]]; then + lead="[=]" + fi + if [[ "$1" == "err" ]]; then + lead="[!]" + fi + print -P "$lead" "$msg" +} + + +# synapsys: +# ask "default_ans" "message" +# +# if user answers YES ask returns 0 +# if NO, returns 1 +# +# ex: ask "y" "overwrite?" +# out: overwrite? [Y/n]: +ask() { + local def_ans="$1" + local promt + if [[ "$def_ans" == "y" ]]; then + promt="[Y/n]" + elif [[ "$def_ans" == "n" ]]; then + promt="[y/N]" + else + cout "err" "ask() got \$default_ans as $1" + exit 10 + fi + + local msg="$2" + while [[ true ]]; do + local ans='' + read -r "ans?$msg $promt: " + local ans="${ans:l}" + + if [[ "$def_ans" == "y" ]]; then + if [[ -z "$ans" || "$ans" == "y" || "$ans" == "yes" ]]; then + return 0 + elif [[ "$ans" == "n" || "$ans" == "no" ]]; then + return 1 + fi + + elif [[ "$def_ans" == "n" ]]; then + if [[ -z "$ans" || "$ans" == "n" || "$ans" == "no" ]]; then + return 1 + elif [[ "$ans" == "y" || "$ans" == "YES" ]]; then + return 0 + fi + fi + done + +} + +# synapsys: +# abort_install "reason" +abort_install() { + cout "err" "%F{red}Install aborted:%f\t$1" + exit 1 +} + +if [[ ! $(id -g ipban) ]]; then + sudo groupadd --system ipban +else + abort_install "Group %Bipban%b already exists" +fi + +if [[ ! $(id -u ipban) ]]; then + sudo useradd \ + --system \ + --gid ipban \ + --no-create-home \ + --comment "ipban service" \ + --home-dir / \ + --shell /sbin/nologin \ + ipban +else + abort_install "User %Bipban%b already exists" +fi + +db_dir="/var/lib/ipban" +if [[ ! -d $db_dir ]]; then + sudo mkdir $db_dir + sudo chmod 775 $db_dir + sudo chown ipban:ipban $db_dir +else + if ask "n" "directory $db_dir already exists. rewrite?"; then + sudo rm -rf $db_dir + + sudo mkdir $db_dir + sudo chmod 775 $db_dir + sudo chown ipban:ipban $db_dir + else + abort_install "User decided not to overwrite \'$db_dir\'. Which is requred to store banned ip\'s" + fi +fi + +#TODO: create systemd service diff --git a/py/daemon.py b/py/daemon.py new file mode 100755 index 0000000..ec398fa --- /dev/null +++ b/py/daemon.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python + +import sqlite3 +import ipaddress +from datetime import date, datetime, timezone +import os +import signal +import socket +from pathlib import Path +import json +from nftables import Nftables +from collections.abc import Iterable + +#create table and its internal content +def create_nft_table(): + nft = Nftables() + + #rc, output, error = nft.cmd("add table inet ipban { flags owner; }") + rc, output, error = nft.cmd("add table inet ipban") + rc, output, error = nft.cmd(f"add set inet ipban banned_ipv4 {{ type ipv4_addr; }}") + rc, output, error = nft.cmd(f"add set inet ipban banned_ipv6 {{ type ipv6_addr; }}") + + rc, output, error = nft.cmd("add chain inet ipban in_ban { type filter hook input priority filter; }") + + rc, output, error = nft.cmd("add rule inet ipban in_ban ip saddr @banned_ipv4 drop") + rc, output, error = nft.cmd("add rule inet ipban in_ban ip6 saddr @banned_ipv6 drop") + +def destroy_nft_table(): + nft = Nftables() + rc, output, error = nft.cmd("destroy table inet ipban") + +#delete old table, create new, and fill with ip's +def flush_nft(ips : Iterable[ipaddress.IPv4Address]): + nft = Nftables() + ips_str = "" + for ip in ips: + ips_str += f"{str(ip)}, " + ips_str = ips_str[:-2] + if ips_str == "": + return + rc, output, error = nft.cmd("destroy set inet ipban banned_ipv4") + rc, output, error = nft.cmd(f"""add set inet ipban banned_ipv4 {{type ipv4_addr; + elements = {{ {ips_str} }}; }}""") + +#add list of ips to set in nft +def add_to_end_nft(ips: Iterable[ipaddress.IPv4Address]): + nft = Nftables() + ips_str = "" + for ip in ips: + ips_str += f"{str(ip)}, " + ips_str = ips_str[:-2] + if ips_str == "": + return + rc, output, error = nft.cmd(f"add element inet ipban banned_ipv4 {{ {ips_str} }}") + +def init_nft(sql_conn: sqlite3.Connection): + rows = sql_conn.execute("""SELECT ip FROM banned + """).fetchall() + flush_nft([row[0] for row in rows]) + +#add selected ip to db table banned +def make_ban(sql_conn: sqlite3.Connection, ip: ipaddress.IPv4Address, remark = "", reason = "") -> bool: + #checking if ip is already banned + row = sql_conn.execute(f"""SELECT id FROM {ban_t_name} + WHERE ip = ? LIMIT 1""", (str(ip),)).fetchone() + print(row) + + if row is not None: + return False #already banned + + ban_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + sql_conn.execute(f"""INSERT INTO {ban_t_name} (ban_time,ip,remark,reason) VALUES (?, ?, ?, ?)""", + (str(ban_time), str(ip), remark, reason)) + sql_conn.commit() + return True + +#what to do in case of req_type=="action" +#TODO: protect from situation: ban "1.1.1.1", uban "1.1.1.1" +def action(sql_conn: sqlite3.Connection, conn: socket.socket, data: str): + data = data[data.find('\n')+1:] #strip req_type + data_json = json.loads(data) + + ips_to_ban = list() + for ip,creds in data_json["ban"].items(): + ip_addr = ipaddress.IPv4Address(ip) + remark = creds["remark"] + reason = creds["reason"] + make_ban(sql_conn, ip_addr, remark, reason) + ips_to_ban.append(ip) + + for ip, creds in data_json["uban"].items(): + print("Not implemented yet") + break + add_to_end_nft(ips_to_ban) + +def list_ips(sql_conn: sqlite3.Connection, conn: socket.socket, data: str): + data = data[data.find('\n')+1:] #strip req_type + + +def recv_all(conn: socket.socket) -> bytes: + chunks = [] + + while True: + data = conn.recv(4096) + + if not data: + break + chunks.append(data) + return b"".join(chunks) + + +#initilise UNIX socket and do someting like http handler +def run_server(socket_path: Path, sql_conn: sqlite3.Connection): + if os.path.exists(socket_path): + os.unlink(socket_path) + server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + + server.bind(socket_path.as_posix()) + server.listen() + + try: + while True: + conn,_ = server.accept() + + try: + data=recv_all(conn) + if not data: + continue + + data = data.decode() + req_type = data.split('\n')[0] + + match req_type: + case "action": + action(sql_conn, conn, data) + case "list": + list_ips(sql_conn, conn, data) + + + finally: + conn.close() + finally: + server.close() + if os.path.exists(socket_path): + os.unlink(socket_path) + + +def stop_handler(sugnum, frame): + destroy_nft_table() + os._exit(0) + +if __name__ == "__main__": + signal.signal(signal.SIGINT, stop_handler) + signal.signal(signal.SIGTERM, stop_handler) + sql_conn = sqlite3.connect("/var/lib/ipban/db.file") + ban_t_name="banned" + sql_conn.execute(f"""CREATE TABLE IF NOT EXISTS {ban_t_name} + (id INTEGER PRIMARY KEY AUTOINCREMENT, + ban_time TEXT NOT NULL, + ip TEXT NOT NULL, + remark TEXT, + reason TEXT)""") + + create_nft_table() + init_nft(sql_conn) + #flush_nft([ ipaddress.IPv4Address("123.134.124.1"), ipaddress.IPv4Address("8.8.8.8")]) + + sock_path = Path("/run/ipban/ipban.sock") + sock_path.parent.mkdir(parents=True,exist_ok=True) + run_server(sock_path, sql_conn) + destroy_nft_table() diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..720f24a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,48 @@ +use rusqlite; +use std::{path::PathBuf, str::FromStr}; + +struct SqliteWrapper { + pub file: PathBuf, + connection: rusqlite::Connection, +} + +impl SqliteWrapper { + pub fn new(&mut self){ + self.file = PathBuf::new(); + } + + pub fn initialize(&mut self){ + self.connection = rusqlite::Connection::open(self.file.to_str().unwrap()); + } +} + + +fn main() { + let conn = rusqlite::Connection::open_in_memory().unwrap(); + conn.execute( + "CREATE TABLE person ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + data BLOB)", + (), + ) + + conn.execute( + "INSERT INTO person (name, data) VALUES (?1, ?2)", + (&me.name, &me.data), + ) + .unwrap(); + + let mut stmt = conn.prepare("SELECT id, name, data FROM person").unwrap(); + let person_iterator = stmt.query_map([], |row| { + Ok(Person { + id: row.get(0).unwrap(), + name: row.get(1).unwrap(), + data: row.get(2).unwrap(), + }) + }); + + for person in person_iterator.unwrap() { + println!("Found person {}", person.unwrap().to_str()); + } +} diff --git a/test.json b/test.json new file mode 100644 index 0000000..53abb4c --- /dev/null +++ b/test.json @@ -0,0 +1,21 @@ +action +{ + "ban": { + "1.2.3.4": { + "remark": "Cloudflare's web", + "reason": "bot" + }, + "8.8.8.8": { + "remark": "", + "reason": "" + } + }, + "uban": { + "1.3.3.7": { + "reason": "Misstake" + }, + "1.0.0.1": { + "reason": "DNS" + } + } +}