diff --git a/Dockerfile b/Dockerfile index 7f7994d..85a320c 100755 --- a/Dockerfile +++ b/Dockerfile @@ -38,6 +38,7 @@ RUN g++ binsrc/proxy.cpp -o modules/proxy -O3 -pthread -lboost_system -lboost_th COPY ./backend/ /execute/ COPY --from=frontend /app/build/ ./frontend/ -ENTRYPOINT ["/bin/sh", "/execute/docker-entrypoint.sh"] + +CMD ["/bin/sh", "/execute/docker-entrypoint.sh"] diff --git a/README.md b/README.md index d1cdc4c..5bfda25 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,6 @@ Initiially the project was based only on regex filters, and also now the main fu ## Next points -- Create hijacking port to proxy - Explanation about tools in the dedicated pages making them more user-friendly - buffering the TCP and(/or) the UDP stream to avoid to bypass the proxy dividing the information in more packets - Adding new section with "general firewall rules" to manage "simple" TCP traffic rules graphically and through nftables diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh index 6d7520a..a44a0b0 100644 --- a/backend/docker-entrypoint.sh +++ b/backend/docker-entrypoint.sh @@ -2,8 +2,7 @@ chown nobody:nobody -R /execute/ -capsh --caps="cap_net_admin+eip cap_setpcap,cap_setuid,cap_setgid+ep" \ - --keep=1 --user=nobody --addamb=cap_net_admin -- \ - -c "python3 /execute/app.py DOCKER" +exec capsh --caps="cap_net_admin+eip cap_setpcap,cap_setuid,cap_setgid+ep" \ + --keep=1 --user=nobody --addamb=cap_net_admin -- -c "python3 /execute/app.py DOCKER" diff --git a/backend/modules/nfregex/firegex.py b/backend/modules/nfregex/firegex.py index 15d5d40..0753e0f 100644 --- a/backend/modules/nfregex/firegex.py +++ b/backend/modules/nfregex/firegex.py @@ -1,10 +1,12 @@ from typing import Dict, List, Set -from utils.firegextables import FiregexFilter, FiregexTables -from utils import ip_parse, ip_family, run_func +from modules.nfregex.nftables import FiregexTables +from utils import ip_parse, run_func from modules.nfregex.models import Service, Regex import re, os, asyncio import traceback +nft = FiregexTables() + class RegexFilter: def __init__( self, regex, @@ -52,7 +54,7 @@ class RegexFilter: class FiregexInterceptor: def __init__(self): - self.filter:FiregexFilter + self.srv:Service self.filter_map_lock:asyncio.Lock self.filter_map: Dict[str, RegexFilter] self.regex_filters: Set[RegexFilter] @@ -61,16 +63,14 @@ class FiregexInterceptor: self.update_task: asyncio.Task @classmethod - async def start(cls, filter: FiregexFilter): + async def start(cls, srv: Service): self = cls() - self.filter = filter + self.srv = srv self.filter_map_lock = asyncio.Lock() self.update_config_lock = asyncio.Lock() input_range, output_range = await self._start_binary() self.update_task = asyncio.create_task(self.update_blocked()) - if not filter in FiregexTables().get(): - FiregexTables().add_input(queue_range=input_range, proto=self.filter.proto, port=self.filter.port, ip_int=self.filter.ip_int) - FiregexTables().add_output(queue_range=output_range, proto=self.filter.proto, port=self.filter.port, ip_int=self.filter.ip_int) + nft.add(self.srv, input_range, output_range) return self async def _start_binary(self): @@ -139,8 +139,3 @@ class FiregexInterceptor: except Exception: pass return res -def delete_by_srv(srv:Service): - nft = FiregexTables() - for filter in nft.get(): - if filter.port == srv.port and filter.proto == srv.proto and ip_parse(filter.ip_int) == ip_parse(srv.ip_int): - nft.cmd({"delete":{"rule": {"handle": filter.id, "table": nft.table_name, "chain": filter.target, "family": "inet"}}}) \ No newline at end of file diff --git a/backend/modules/nfregex/firewall.py b/backend/modules/nfregex/firewall.py index 03ba62b..18544f6 100644 --- a/backend/modules/nfregex/firewall.py +++ b/backend/modules/nfregex/firewall.py @@ -1,6 +1,7 @@ import asyncio from typing import Dict -from modules.nfregex.firegex import FiregexFilter, FiregexInterceptor, FiregexTables, RegexFilter, delete_by_srv +from modules.nfregex.firegex import FiregexInterceptor, RegexFilter +from modules.nfregex.nftables import FiregexTables, FiregexFilter from modules.nfregex.models import Regex, Service from utils.sqlite import SQLite @@ -8,38 +9,40 @@ class STATUS: STOP = "stop" ACTIVE = "active" +nft = FiregexTables() + class FirewallManager: def __init__(self, db:SQLite): self.db = db - self.proxy_table: Dict[str, ServiceManager] = {} + self.service_table: Dict[str, ServiceManager] = {} self.lock = asyncio.Lock() async def close(self): - for key in list(self.proxy_table.keys()): + for key in list(self.service_table.keys()): await self.remove(key) async def remove(self,srv_id): async with self.lock: - if srv_id in self.proxy_table: - await self.proxy_table[srv_id].next(STATUS.STOP) - del self.proxy_table[srv_id] + if srv_id in self.service_table: + await self.service_table[srv_id].next(STATUS.STOP) + del self.service_table[srv_id] async def init(self): - FiregexTables().init() + nft.init() await self.reload() async def reload(self): async with self.lock: for srv in self.db.query('SELECT * FROM services;'): srv = Service.from_dict(srv) - if srv.id in self.proxy_table: + if srv.id in self.service_table: continue - self.proxy_table[srv.id] = ServiceManager(srv, self.db) - await self.proxy_table[srv.id].next(srv.status) + self.service_table[srv.id] = ServiceManager(srv, self.db) + await self.service_table[srv.id].next(srv.status) def get(self,srv_id): - if srv_id in self.proxy_table: - return self.proxy_table[srv_id] + if srv_id in self.service_table: + return self.service_table[srv_id] else: raise ServiceNotFoundException() @@ -94,13 +97,13 @@ class ServiceManager: async def start(self): if not self.interceptor: - delete_by_srv(self.srv) - self.interceptor = await FiregexInterceptor.start(FiregexFilter(self.srv.proto,self.srv.port, self.srv.ip_int)) + nft.delete(self.srv) + self.interceptor = await FiregexInterceptor.start(self.srv) await self._update_filters_from_db() self._set_status(STATUS.ACTIVE) async def stop(self): - delete_by_srv(self.srv) + nft.delete(self.srv) if self.interceptor: await self.interceptor.stop() self.interceptor = None diff --git a/backend/modules/nfregex/models.py b/backend/modules/nfregex/models.py index d365412..e020d87 100644 --- a/backend/modules/nfregex/models.py +++ b/backend/modules/nfregex/models.py @@ -11,7 +11,14 @@ class Service: @classmethod def from_dict(cls, var: dict): - return cls(id=var["service_id"], status=var["status"], port=var["port"], name=var["name"], proto=var["proto"], ip_int=var["ip_int"]) + return cls( + id=var["service_id"], + status=var["status"], + port=var["port"], + name=var["name"], + proto=var["proto"], + ip_int=var["ip_int"] + ) class Regex: @@ -27,4 +34,13 @@ class Regex: @classmethod def from_dict(cls, var: dict): - return cls(id=var["regex_id"], regex=base64.b64decode(var["regex"]), mode=var["mode"], service_id=var["service_id"], is_blacklist=var["is_blacklist"], blocked_packets=var["blocked_packets"], is_case_sensitive=var["is_case_sensitive"], active=var["active"]) \ No newline at end of file + return cls( + id=var["regex_id"], + regex=base64.b64decode(var["regex"]), + mode=var["mode"], + service_id=var["service_id"], + is_blacklist=var["is_blacklist"], + blocked_packets=var["blocked_packets"], + is_case_sensitive=var["is_case_sensitive"], + active=var["active"] + ) \ No newline at end of file diff --git a/backend/modules/nfregex/nftables.py b/backend/modules/nfregex/nftables.py new file mode 100644 index 0000000..47b77c2 --- /dev/null +++ b/backend/modules/nfregex/nftables.py @@ -0,0 +1,109 @@ +from typing import List +from modules.nfregex.models import Service +from utils import ip_parse, ip_family, NFTableManager, nftables_int_to_json + +class FiregexFilter: + def __init__(self, proto:str, port:int, ip_int:str, target:str, id:int): + self.id = id + self.target = target + self.proto = proto + self.port = int(port) + self.ip_int = str(ip_int) + + def __eq__(self, o: object) -> bool: + if isinstance(o, FiregexFilter): + return self.port == o.port and self.proto == o.proto and ip_parse(self.ip_int) == ip_parse(o.ip_int) + elif isinstance(o, Service): + return self.port == o.port and self.proto == o.proto and ip_parse(self.ip_int) == ip_parse(o.ip_int) + return False + +class FiregexTables(NFTableManager): + input_chain = "nfregex_input" + output_chain = "nfregex_output" + + def __init__(self): + super().__init__([ + {"add":{"chain":{ + "family":"inet", + "table":self.table_name, + "name":self.input_chain, + "type":"filter", + "hook":"prerouting", + "prio":-150, + "policy":"accept" + }}}, + {"add":{"chain":{ + "family":"inet", + "table":self.table_name, + "name":self.output_chain, + "type":"filter", + "hook":"postrouting", + "prio":-150, + "policy":"accept" + }}} + ],[ + {"flush":{"chain":{"table":self.table_name,"family":"inet", "name":self.input_chain}}}, + {"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.input_chain}}}, + {"flush":{"chain":{"table":self.table_name,"family":"inet", "name":self.output_chain}}}, + {"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.output_chain}}}, + ]) + + def add(self, srv:Service, queue_range_input, queue_range_output): + + for ele in self.get(): + if ele.__eq__(srv): return + + init, end = queue_range_output + if init > end: init, end = end, init + self.cmd({ "insert":{ "rule": { + "family": "inet", + "table": self.table_name, + "chain": self.output_chain, + "expr": [ + {'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}}, + {'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "sport"}}, "op": "==", "right": int(srv.port)}}, + {"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}} + ] + }}}) + + init, end = queue_range_input + if init > end: init, end = end, init + self.cmd({"insert":{"rule":{ + "family": "inet", + "table": self.table_name, + "chain": self.input_chain, + "expr": [ + {'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}}, + {'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "dport"}}, "op": "==", "right": int(srv.port)}}, + {"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}} + ] + }}}) + + + def get(self) -> List[FiregexFilter]: + res = [] + for filter in self.list_rules(tables=[self.table_name], chains=[self.input_chain,self.output_chain]): + ip_int = None + if isinstance(filter["expr"][0]["match"]["right"],str): + ip_int = str(ip_parse(filter["expr"][0]["match"]["right"])) + else: + ip_int = f'{filter["expr"][0]["match"]["right"]["prefix"]["addr"]}/{filter["expr"][0]["match"]["right"]["prefix"]["len"]}' + res.append(FiregexFilter( + target=filter["chain"], + id=int(filter["handle"]), + proto=filter["expr"][1]["match"]["left"]["payload"]["protocol"], + port=filter["expr"][1]["match"]["right"], + ip_int=ip_int + )) + return res + + def delete(self, srv:Service): + for filter in self.get(): + if filter.__eq__(srv): + self.cmd({ "delete":{ "rule": { + "family": "inet", + "table": self.table_name, + "chain": filter.target, + "handle": filter.id + }}}) + \ No newline at end of file diff --git a/backend/modules/porthijack/__init__.py b/backend/modules/porthijack/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/modules/porthijack/firewall.py b/backend/modules/porthijack/firewall.py new file mode 100644 index 0000000..b1d21a4 --- /dev/null +++ b/backend/modules/porthijack/firewall.py @@ -0,0 +1,77 @@ +import asyncio +from typing import Dict +from modules.porthijack.nftables import FiregexTables +from modules.porthijack.models import Service +from utils.sqlite import SQLite + +nft = FiregexTables() + +class FirewallManager: + def __init__(self, db:SQLite): + self.db = db + self.service_table: Dict[str, ServiceManager] = {} + self.lock = asyncio.Lock() + + async def close(self): + for key in list(self.service_table.keys()): + await self.remove(key) + + async def remove(self,srv_id): + async with self.lock: + if srv_id in self.service_table: + await self.service_table[srv_id].disable() + del self.service_table[srv_id] + + async def init(self): + FiregexTables().init() + await self.reload() + + async def reload(self): + async with self.lock: + for srv in self.db.query('SELECT * FROM services;'): + srv = Service.from_dict(srv) + if srv.service_id in self.service_table: + continue + self.service_table[srv.service_id] = ServiceManager(srv, self.db) + if srv.active: + await self.service_table[srv.service_id].enable() + + def get(self,srv_id): + if srv_id in self.service_table: + return self.service_table[srv_id] + else: + raise ServiceNotFoundException() + +class ServiceNotFoundException(Exception): pass + +class ServiceManager: + def __init__(self, srv: Service, db): + self.srv = srv + self.db = db + self.active = False + self.lock = asyncio.Lock() + + async def enable(self): + if not self.active: + async with self.lock: + nft.delete(self.srv) + nft.add(self.srv) + self._set_status(True) + + async def disable(self): + if self.active: + async with self.lock: + nft.delete(self.srv) + self._set_status(False) + + async def refresh(self, srv:Service): + self.srv = srv + if self.active: await self.restart() + + def _set_status(self,active): + self.active = active + self.db.query("UPDATE services SET active = ? WHERE service_id = ?;", active, self.srv.service_id) + + async def restart(self): + await self.disable() + await self.enable() \ No newline at end of file diff --git a/backend/modules/porthijack/models.py b/backend/modules/porthijack/models.py new file mode 100644 index 0000000..a89e6d6 --- /dev/null +++ b/backend/modules/porthijack/models.py @@ -0,0 +1,23 @@ +class Service: + def __init__(self, service_id: str, active: bool, public_port: int, proxy_port: int, name: str, proto: str, ip_src: str, ip_dst:str): + self.service_id = service_id + self.active = active + self.public_port = public_port + self.proxy_port = proxy_port + self.name = name + self.proto = proto + self.ip_src = ip_src + self.ip_dst = ip_dst + + @classmethod + def from_dict(cls, var: dict): + return cls( + service_id=var["service_id"], + active=var["active"], + public_port=var["public_port"], + proxy_port=var["proxy_port"], + name=var["name"], + proto=var["proto"], + ip_src=var["ip_src"], + ip_dst=var["ip_dst"] + ) diff --git a/backend/modules/porthijack/nftables.py b/backend/modules/porthijack/nftables.py new file mode 100644 index 0000000..92255f3 --- /dev/null +++ b/backend/modules/porthijack/nftables.py @@ -0,0 +1,105 @@ +from typing import List +from modules.porthijack.models import Service +from utils import addr_parse, ip_parse, ip_family, NFTableManager, nftables_json_to_int + +class FiregexHijackRule(): + def __init__(self, proto:str, public_port:int,proxy_port:int, ip_src:str, ip_dst:str, target:str, id:int): + self.id = id + self.target = target + self.proto = proto + self.public_port = public_port + self.proxy_port = proxy_port + self.ip_src = str(ip_src) + self.ip_dst = str(ip_dst) + + def __eq__(self, o: object) -> bool: + if isinstance(o, FiregexHijackRule): + return self.public_port == o.public_port and self.proto == o.proto and ip_parse(self.ip_src) == ip_parse(o.ip_src) + elif isinstance(o, Service): + return self.public_port == o.public_port and self.proto == o.proto and ip_parse(self.ip_src) == ip_parse(o.ip_src) + return False + +class FiregexTables(NFTableManager): + prerouting_porthijack = "prerouting_porthijack" + postrouting_porthijack = "postrouting_porthijack" + + def __init__(self): + super().__init__([ + {"add":{"chain":{ + "family":"inet", + "table":self.table_name, + "name":self.prerouting_porthijack, + "type":"filter", + "hook":"prerouting", + "prio":-300, + "policy":"accept" + }}}, + {"add":{"chain":{ + "family":"inet", + "table":self.table_name, + "name":self.postrouting_porthijack, + "type":"filter", + "hook":"postrouting", + "prio":-300, + "policy":"accept" + }}} + ],[ + {"flush":{"chain":{"table":self.table_name,"family":"inet", "name":self.prerouting_porthijack}}}, + {"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.prerouting_porthijack}}}, + {"flush":{"chain":{"table":self.table_name,"family":"inet", "name":self.postrouting_porthijack}}}, + {"delete":{"chain":{"table":self.table_name,"family":"inet", "name":self.postrouting_porthijack}}} + ]) + + def add(self, srv:Service): + + for ele in self.get(): + if ele.__eq__(srv): return + + self.cmd({ "insert":{ "rule": { + "family": "inet", + "table": self.table_name, + "chain": self.prerouting_porthijack, + "expr": [ + {'match': {'left': {'payload': {'protocol': ip_family(srv.ip_src), 'field': 'daddr'}}, 'op': '==', 'right': addr_parse(srv.ip_src)}}, + {'match': {'left': {'payload': {'protocol': str(srv.proto), 'field': 'dport'}}, 'op': '==', 'right': int(srv.public_port)}}, + {'mangle': {'key': {'payload': {'protocol': str(srv.proto), 'field': 'dport'}}, 'value': int(srv.proxy_port)}}, + {'mangle': {'key': {'payload': {'protocol': ip_family(srv.ip_src), 'field': 'daddr'}}, 'value': addr_parse(srv.ip_dst)}} + ] + }}}) + self.cmd({ "insert":{ "rule": { + "family": "inet", + "table": self.table_name, + "chain": self.postrouting_porthijack, + "expr": [ + {'match': {'left': {'payload': {'protocol': ip_family(srv.ip_dst), 'field': 'saddr'}}, 'op': '==', 'right': addr_parse(srv.ip_dst)}}, + {'match': {'left': { "payload": {"protocol": str(srv.proto), "field": "sport"}}, "op": "==", "right": int(srv.proxy_port)}}, + {'mangle': {'key': {'payload': {'protocol': str(srv.proto), 'field': 'sport'}}, 'value': int(srv.public_port)}}, + {'mangle': {'key': {'payload': {'protocol': ip_family(srv.ip_dst), 'field': 'saddr'}}, 'value': addr_parse(srv.ip_src)}} + ] + }}}) + + + def get(self) -> List[FiregexHijackRule]: + res = [] + for filter in self.list_rules(tables=[self.table_name], chains=[self.prerouting_porthijack,self.postrouting_porthijack]): + filter["expr"][0]["match"]["right"] + res.append(FiregexHijackRule( + target=filter["chain"], + id=int(filter["handle"]), + proto=filter["expr"][1]["match"]["left"]["payload"]["protocol"], + public_port=filter["expr"][1]["match"]["right"] if filter["chain"] == self.prerouting_porthijack else filter["expr"][2]["mangle"]["value"], + proxy_port=filter["expr"][1]["match"]["right"] if filter["chain"] == self.postrouting_porthijack else filter["expr"][2]["mangle"]["value"], + ip_src=nftables_json_to_int(filter["expr"][0]["match"]["right"]) if filter["chain"] == self.prerouting_porthijack else nftables_json_to_int(filter["expr"][3]["mangle"]["value"]), + ip_dst=nftables_json_to_int(filter["expr"][0]["match"]["right"]) if filter["chain"] == self.postrouting_porthijack else nftables_json_to_int(filter["expr"][3]["mangle"]["value"]), + )) + return res + + def delete(self, srv:Service): + for filter in self.get(): + if filter.__eq__(srv): + self.cmd({ "delete":{ "rule": { + "family": "inet", + "table": self.table_name, + "chain": filter.target, + "handle": filter.id + }}}) \ No newline at end of file diff --git a/backend/routers/nfregex.py b/backend/routers/nfregex.py index 48c6e5e..91131b1 100644 --- a/backend/routers/nfregex.py +++ b/backend/routers/nfregex.py @@ -5,7 +5,7 @@ import sqlite3 from typing import List, Union from fastapi import APIRouter, HTTPException from pydantic import BaseModel -from modules.nfregex.firegex import FiregexTables +from modules.nfregex.nftables import FiregexTables from modules.nfregex.firewall import STATUS, FirewallManager from utils.sqlite import SQLite from utils import ip_parse, refactor_name, refresh_frontend @@ -145,7 +145,7 @@ async def get_service_list(): """) @app.get('/service/{service_id}', response_model=ServiceModel) -async def get_service_by_id(service_id: str, ): +async def get_service_by_id(service_id: str): """Get info about a specific service using his id""" res = db.query(""" SELECT @@ -164,21 +164,21 @@ async def get_service_by_id(service_id: str, ): return res[0] @app.get('/service/{service_id}/stop', response_model=StatusMessageModel) -async def service_stop(service_id: str, ): +async def service_stop(service_id: str): """Request the stop of a specific service""" await firewall.get(service_id).next(STATUS.STOP) await refresh_frontend() return {'status': 'ok'} @app.get('/service/{service_id}/start', response_model=StatusMessageModel) -async def service_start(service_id: str, ): +async def service_start(service_id: str): """Request the start of a specific service""" await firewall.get(service_id).next(STATUS.ACTIVE) await refresh_frontend() return {'status': 'ok'} @app.get('/service/{service_id}/delete', response_model=StatusMessageModel) -async def service_delete(service_id: str, ): +async def service_delete(service_id: str): """Request the deletion of a specific service""" db.query('DELETE FROM services WHERE service_id = ?;', service_id) db.query('DELETE FROM regexes WHERE service_id = ?;', service_id) @@ -187,7 +187,7 @@ async def service_delete(service_id: str, ): return {'status': 'ok'} @app.post('/service/{service_id}/rename', response_model=StatusMessageModel) -async def service_rename(service_id: str, form: RenameForm, ): +async def service_rename(service_id: str, form: RenameForm): """Request to change the name of a specific service""" form.name = refactor_name(form.name) if not form.name: return {'status': 'The name cannot be empty!'} @@ -199,7 +199,7 @@ async def service_rename(service_id: str, form: RenameForm, ): return {'status': 'ok'} @app.get('/service/{service_id}/regexes', response_model=List[RegexModel]) -async def get_service_regexe_list(service_id: str, ): +async def get_service_regexe_list(service_id: str): """Get the list of the regexes of a service""" return db.query(""" SELECT @@ -209,7 +209,7 @@ async def get_service_regexe_list(service_id: str, ): """, service_id) @app.get('/regex/{regex_id}', response_model=RegexModel) -async def get_regex_by_id(regex_id: int, ): +async def get_regex_by_id(regex_id: int): """Get regex info using his id""" res = db.query(""" SELECT @@ -221,7 +221,7 @@ async def get_regex_by_id(regex_id: int, ): return res[0] @app.get('/regex/{regex_id}/delete', response_model=StatusMessageModel) -async def regex_delete(regex_id: int, ): +async def regex_delete(regex_id: int): """Delete a regex using his id""" res = db.query('SELECT * FROM regexes WHERE regex_id = ?;', regex_id) if len(res) != 0: @@ -232,7 +232,7 @@ async def regex_delete(regex_id: int, ): return {'status': 'ok'} @app.get('/regex/{regex_id}/enable', response_model=StatusMessageModel) -async def regex_enable(regex_id: int, ): +async def regex_enable(regex_id: int): """Request the enabling of a regex""" res = db.query('SELECT * FROM regexes WHERE regex_id = ?;', regex_id) if len(res) != 0: @@ -242,7 +242,7 @@ async def regex_enable(regex_id: int, ): return {'status': 'ok'} @app.get('/regex/{regex_id}/disable', response_model=StatusMessageModel) -async def regex_disable(regex_id: int, ): +async def regex_disable(regex_id: int): """Request the deactivation of a regex""" res = db.query('SELECT * FROM regexes WHERE regex_id = ?;', regex_id) if len(res) != 0: @@ -252,7 +252,7 @@ async def regex_disable(regex_id: int, ): return {'status': 'ok'} @app.post('/regexes/add', response_model=StatusMessageModel) -async def add_new_regex(form: RegexAddForm, ): +async def add_new_regex(form: RegexAddForm): """Add a new regex""" try: re.compile(b64decode(form.regex)) @@ -269,7 +269,7 @@ async def add_new_regex(form: RegexAddForm, ): return {'status': 'ok'} @app.post('/services/add', response_model=ServiceAddResponse) -async def add_new_service(form: ServiceAddForm, ): +async def add_new_service(form: ServiceAddForm): """Add a new service""" try: form.ip_int = ip_parse(form.ip_int) diff --git a/backend/routers/porthijack.py b/backend/routers/porthijack.py new file mode 100644 index 0000000..16b92ba --- /dev/null +++ b/backend/routers/porthijack.py @@ -0,0 +1,196 @@ +import secrets +import sqlite3 +from typing import List, Union +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from modules.porthijack.models import Service +from utils.sqlite import SQLite +from utils import addr_parse, ip_family, refactor_name, refresh_frontend +from utils.models import ResetRequest, StatusMessageModel +from modules.porthijack.nftables import FiregexTables +from modules.porthijack.firewall import FirewallManager + +class ServiceModel(BaseModel): + service_id: str + active: bool + public_port: int + proxy_port: int + name: str + proto: str + ip_src: str + ip_dst: str + +class RenameForm(BaseModel): + name:str + +class ServiceAddForm(BaseModel): + name: str + public_port: int + proxy_port: int + proto: str + ip_src: str + ip_dst: str + +class ServiceAddResponse(BaseModel): + status:str + service_id: Union[None,str] + +class GeneralStatModel(BaseModel): + services: int + +app = APIRouter() + +db = SQLite('db/port-hijacking.db', { + 'services': { + 'service_id': 'VARCHAR(100) PRIMARY KEY', + 'active' : 'BOOLEAN NOT NULL CHECK (active IN (0, 1))', + 'public_port': 'INT NOT NULL CHECK(public_port > 0 and public_port < 65536)', + 'proxy_port': 'INT NOT NULL CHECK(proxy_port > 0 and proxy_port < 65536 and proxy_port != public_port)', + 'name': 'VARCHAR(100) NOT NULL UNIQUE', + 'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "udp"))', + 'ip_src': 'VARCHAR(100) NOT NULL', + 'ip_dst': 'VARCHAR(100) NOT NULL', + }, + 'QUERY':[ + "CREATE UNIQUE INDEX IF NOT EXISTS unique_services ON services (public_port, ip_src, proto);" + ] +}) + +async def reset(params: ResetRequest): + if not params.delete: + db.backup() + await firewall.close() + FiregexTables().reset() + if params.delete: + db.delete() + db.init() + else: + db.restore() + await firewall.init() + + +async def startup(): + db.init() + await firewall.init() + +async def shutdown(): + db.backup() + await firewall.close() + db.disconnect() + db.restore() + +def gen_service_id(): + while True: + res = secrets.token_hex(8) + if len(db.query('SELECT 1 FROM services WHERE service_id = ?;', res)) == 0: + break + return res + +firewall = FirewallManager(db) + +@app.get('/stats', response_model=GeneralStatModel) +async def get_general_stats(): + """Get firegex general status about services""" + return db.query(""" + SELECT + (SELECT COUNT(*) FROM services) services + """)[0] + +@app.get('/services', response_model=List[ServiceModel]) +async def get_service_list(): + """Get the list of existent firegex services""" + return db.query("SELECT service_id, active, public_port, proxy_port, name, proto, ip_src, ip_dst FROM services;") + +@app.get('/service/{service_id}', response_model=ServiceModel) +async def get_service_by_id(service_id: str): + """Get info about a specific service using his id""" + res = db.query("SELECT service_id, active, public_port, proxy_port, name, proto, ip_src, ip_dst FROM services WHERE service_id = ?;", service_id) + if len(res) == 0: raise HTTPException(status_code=400, detail="This service does not exists!") + return res[0] + +@app.get('/service/{service_id}/stop', response_model=StatusMessageModel) +async def service_stop(service_id: str): + """Request the stop of a specific service""" + await firewall.get(service_id).disable() + await refresh_frontend() + return {'status': 'ok'} + +@app.get('/service/{service_id}/start', response_model=StatusMessageModel) +async def service_start(service_id: str): + """Request the start of a specific service""" + await firewall.get(service_id).enable() + await refresh_frontend() + return {'status': 'ok'} + +@app.get('/service/{service_id}/delete', response_model=StatusMessageModel) +async def service_delete(service_id: str): + """Request the deletion of a specific service""" + db.query('DELETE FROM services WHERE service_id = ?;', service_id) + await firewall.remove(service_id) + await refresh_frontend() + return {'status': 'ok'} + +@app.post('/service/{service_id}/rename', response_model=StatusMessageModel) +async def service_rename(service_id: str, form: RenameForm): + """Request to change the name of a specific service""" + form.name = refactor_name(form.name) + if not form.name: return {'status': 'The name cannot be empty!'} + try: + db.query('UPDATE services SET name=? WHERE service_id = ?;', form.name, service_id) + except sqlite3.IntegrityError: + return {'status': 'This name is already used'} + await refresh_frontend() + return {'status': 'ok'} + +class ChangeDestination(BaseModel): + ip_dst: str + proxy_port: int + +@app.post('/service/{service_id}/change-destination', response_model=StatusMessageModel) +async def service_change_destination(service_id: str, form: ChangeDestination): + """Request to change the proxy destination of the service""" + + try: + form.ip_dst = addr_parse(form.ip_dst) + except ValueError: + return {"status":"Invalid address"} + srv = Service.from_dict(db.query('SELECT * FROM services WHERE service_id = ?;', service_id)[0]) + if ip_family(form.ip_dst) != ip_family(srv.ip_src): + return {'status': 'The destination ip is not of the same family as the source ip'} + try: + db.query('UPDATE services SET proxy_port=?, ip_dst=? WHERE service_id = ?;', form.proxy_port, form.ip_dst, service_id) + except sqlite3.IntegrityError: + return {'status': 'Invalid proxy port or service'} + + srv.ip_dst = form.ip_dst + srv.proxy_port = form.proxy_port + await firewall.get(service_id).refresh(srv) + + await refresh_frontend() + return {'status': 'ok'} + +@app.post('/services/add', response_model=ServiceAddResponse) +async def add_new_service(form: ServiceAddForm): + """Add a new service""" + try: + form.ip_src = addr_parse(form.ip_src) + form.ip_dst = addr_parse(form.ip_dst) + except ValueError: + return {"status":"Invalid address"} + + if ip_family(form.ip_dst) != ip_family(form.ip_src): + return {"status":"Destination and source addresses must be of the same family"} + if form.proto not in ["tcp", "udp"]: + return {"status":"Invalid protocol"} + + srv_id = None + try: + srv_id = gen_service_id() + db.query("INSERT INTO services (service_id, active, public_port, proxy_port, name, proto, ip_src, ip_dst) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + srv_id, False, form.public_port, form.proxy_port , form.name, form.proto, form.ip_src, form.ip_dst) + except sqlite3.IntegrityError: + return {'status': 'This type of service already exists'} + + await firewall.reload() + await refresh_frontend() + return {'status': 'ok', 'service_id': srv_id} diff --git a/backend/routers/regexproxy.py b/backend/routers/regexproxy.py index dd49ec5..c7d565e 100644 --- a/backend/routers/regexproxy.py +++ b/backend/routers/regexproxy.py @@ -100,7 +100,7 @@ async def get_service_list(): """) @app.get('/service/{service_id}', response_model=ServiceModel) -async def get_service_by_id(service_id: str, ): +async def get_service_by_id(service_id: str): """Get info about a specific service using his id""" res = db.query(""" SELECT @@ -118,28 +118,28 @@ async def get_service_by_id(service_id: str, ): return res[0] @app.get('/service/{service_id}/stop', response_model=StatusMessageModel) -async def service_stop(service_id: str, ): +async def service_stop(service_id: str): """Request the stop of a specific service""" await firewall.get(service_id).next(STATUS.STOP) await refresh_frontend() return {'status': 'ok'} @app.get('/service/{service_id}/pause', response_model=StatusMessageModel) -async def service_pause(service_id: str, ): +async def service_pause(service_id: str): """Request the pause of a specific service""" await firewall.get(service_id).next(STATUS.PAUSE) await refresh_frontend() return {'status': 'ok'} @app.get('/service/{service_id}/start', response_model=StatusMessageModel) -async def service_start(service_id: str, ): +async def service_start(service_id: str): """Request the start of a specific service""" await firewall.get(service_id).next(STATUS.ACTIVE) await refresh_frontend() return {'status': 'ok'} @app.get('/service/{service_id}/delete', response_model=StatusMessageModel) -async def service_delete(service_id: str, ): +async def service_delete(service_id: str): """Request the deletion of a specific service""" db.query('DELETE FROM services WHERE service_id = ?;', service_id) db.query('DELETE FROM regexes WHERE service_id = ?;', service_id) @@ -149,7 +149,7 @@ async def service_delete(service_id: str, ): @app.get('/service/{service_id}/regen-port', response_model=StatusMessageModel) -async def regen_service_port(service_id: str, ): +async def regen_service_port(service_id: str): """Request the regeneration of a the internal proxy port of a specific service""" db.query('UPDATE services SET internal_port = ? WHERE service_id = ?;', gen_internal_port(db), service_id) await firewall.get(service_id).update_port() @@ -161,7 +161,7 @@ class ChangePortForm(BaseModel): internalPort: Union[int, None] @app.post('/service/{service_id}/change-ports', response_model=StatusMessageModel) -async def change_service_ports(service_id: str, change_port:ChangePortForm ): +async def change_service_ports(service_id: str, change_port:ChangePortForm): """Choose and change the ports of the service""" if change_port.port is None and change_port.internalPort is None: return {'status': 'Invalid Request!'} @@ -195,7 +195,7 @@ class RegexModel(BaseModel): active:bool @app.get('/service/{service_id}/regexes', response_model=List[RegexModel]) -async def get_service_regexe_list(service_id: str, ): +async def get_service_regexe_list(service_id: str): """Get the list of the regexes of a service""" return db.query(""" SELECT @@ -205,7 +205,7 @@ async def get_service_regexe_list(service_id: str, ): """, service_id) @app.get('/regex/{regex_id}', response_model=RegexModel) -async def get_regex_by_id(regex_id: int, ): +async def get_regex_by_id(regex_id: int): """Get regex info using his id""" res = db.query(""" SELECT @@ -217,7 +217,7 @@ async def get_regex_by_id(regex_id: int, ): return res[0] @app.get('/regex/{regex_id}/delete', response_model=StatusMessageModel) -async def regex_delete(regex_id: int, ): +async def regex_delete(regex_id: int): """Delete a regex using his id""" res = db.query('SELECT * FROM regexes WHERE regex_id = ?;', regex_id) if len(res) != 0: @@ -227,7 +227,7 @@ async def regex_delete(regex_id: int, ): return {'status': 'ok'} @app.get('/regex/{regex_id}/enable', response_model=StatusMessageModel) -async def regex_enable(regex_id: int, ): +async def regex_enable(regex_id: int): """Request the enabling of a regex""" res = db.query('SELECT * FROM regexes WHERE regex_id = ?;', regex_id) if len(res) != 0: @@ -237,7 +237,7 @@ async def regex_enable(regex_id: int, ): return {'status': 'ok'} @app.get('/regex/{regex_id}/disable', response_model=StatusMessageModel) -async def regex_disable(regex_id: int, ): +async def regex_disable(regex_id: int): """Request the deactivation of a regex""" res = db.query('SELECT * FROM regexes WHERE regex_id = ?;', regex_id) if len(res) != 0: @@ -255,7 +255,7 @@ class RegexAddForm(BaseModel): is_case_sensitive: bool @app.post('/regexes/add', response_model=StatusMessageModel) -async def add_new_regex(form: RegexAddForm, ): +async def add_new_regex(form: RegexAddForm): """Add a new regex""" try: re.compile(b64decode(form.regex)) @@ -283,7 +283,7 @@ class RenameForm(BaseModel): name:str @app.post('/service/{service_id}/rename', response_model=StatusMessageModel) -async def service_rename(service_id: str, form: RenameForm, ): +async def service_rename(service_id: str, form: RenameForm): """Request to change the name of a specific service""" form.name = refactor_name(form.name) if not form.name: return {'status': 'The name cannot be empty!'} @@ -295,7 +295,7 @@ async def service_rename(service_id: str, form: RenameForm, ): return {'status': 'ok'} @app.post('/services/add', response_model=ServiceAddStatus) -async def add_new_service(form: ServiceAddForm, ): +async def add_new_service(form: ServiceAddForm): """Add a new service""" serv_id = gen_service_id(db) form.name = refactor_name(form.name) diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py index ed23156..052be3f 100755 --- a/backend/utils/__init__.py +++ b/backend/utils/__init__.py @@ -1,7 +1,6 @@ import asyncio -from ipaddress import ip_interface -import os, socket, psutil -import sys +from ipaddress import ip_address, ip_interface +import os, socket, psutil, sys, nftables from fastapi_socketio import SocketManager LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1")) @@ -38,6 +37,9 @@ def list_files(mypath): def ip_parse(ip:str): return str(ip_interface(ip).network) +def addr_parse(ip:str): + return str(ip_address(ip)) + def ip_family(ip:str): return "ip6" if ip_interface(ip).version == 6 else "ip" @@ -47,4 +49,60 @@ def get_interfaces(): for interf in interfs: if interf.family in [socket.AF_INET, socket.AF_INET6]: yield {"name": int_name, "addr":interf.address} - return list(_get_interfaces()) \ No newline at end of file + return list(_get_interfaces()) + +def nftables_int_to_json(ip_int): + ip_int = ip_parse(ip_int) + ip_addr = str(ip_int).split("/")[0] + ip_addr_cidr = int(str(ip_int).split("/")[1]) + return {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}} + +def nftables_json_to_int(ip_json_int): + if isinstance(ip_json_int,str): + return str(ip_parse(ip_json_int)) + else: + return f'{ip_json_int["prefix"]["addr"]}/{ip_json_int["prefix"]["len"]}' + +class Singleton(object): + __instance = None + def __new__(class_, *args, **kwargs): + if not isinstance(class_.__instance, class_): + class_.__instance = object.__new__(class_, *args, **kwargs) + return class_.__instance + +class NFTableManager(Singleton): + + table_name = "firegex" + + def __init__(self, init_cmd, reset_cmd): + self.__init_cmds = init_cmd + self.__reset_cmds = reset_cmd + self.nft = nftables.Nftables() + + def raw_cmd(self, *cmds): + return self.nft.json_cmd({"nftables": list(cmds)}) + + def cmd(self, *cmds): + code, out, err = self.raw_cmd(*cmds) + + if code == 0: return out + else: raise Exception(err) + + def init(self): + self.reset() + self.raw_cmd({"add":{"table":{"name":self.table_name,"family":"inet"}}}) + self.cmd(*self.__init_cmds) + + def reset(self): + self.raw_cmd(*self.__reset_cmds) + + def list_rules(self, tables = None, chains = None): + for filter in [ele["rule"] for ele in self.raw_list() if "rule" in ele ]: + if tables and filter["table"] not in tables: continue + if chains and filter["chain"] not in chains: continue + yield filter + + def raw_list(self): + return self.cmd({"list": {"ruleset": None}})["nftables"] + + \ No newline at end of file diff --git a/backend/utils/firegextables.py b/backend/utils/firegextables.py deleted file mode 100644 index 8919625..0000000 --- a/backend/utils/firegextables.py +++ /dev/null @@ -1,125 +0,0 @@ -from typing import List -import nftables -from utils import ip_parse, ip_family - -class FiregexFilter(): - def __init__(self, proto:str, port:int, ip_int:str, queue=None, target:str=None, id=None): - self.nftables = nftables.Nftables() - self.id = int(id) if id else None - self.queue = queue - self.target = target - self.proto = proto - self.port = int(port) - self.ip_int = str(ip_int) - - def __eq__(self, o: object) -> bool: - if isinstance(o, FiregexFilter): - return self.port == o.port and self.proto == o.proto and ip_parse(self.ip_int) == ip_parse(o.ip_int) - return False - -class FiregexTables: - - def __init__(self): - self.table_name = "firegex" - self.nft = nftables.Nftables() - - def raw_cmd(self, *cmds): - return self.nft.json_cmd({"nftables": list(cmds)}) - - def cmd(self, *cmds): - code, out, err = self.raw_cmd(*cmds) - - if code == 0: return out - else: raise Exception(err) - - def init(self): - self.reset() - code, out, err = self.raw_cmd({"create":{"table":{"name":self.table_name,"family":"inet"}}}) - if code == 0: - self.cmd( - {"create":{"chain":{ - "family":"inet", - "table":self.table_name, - "name":"input", - "type":"filter", - "hook":"prerouting", - "prio":-150, - "policy":"accept" - }}}, - {"create":{"chain":{ - "family":"inet", - "table":self.table_name, - "name":"output", - "type":"filter", - "hook":"postrouting", - "prio":-150, - "policy":"accept" - }}} - ) - - - def reset(self): - self.raw_cmd( - {"flush":{"table":{"name":"firegex","family":"inet"}}}, - {"delete":{"table":{"name":"firegex","family":"inet"}}}, - ) - - def list(self): - return self.cmd({"list": {"ruleset": None}})["nftables"] - - def add_output(self, queue_range, proto, port, ip_int): - init, end = queue_range - if init > end: init, end = end, init - ip_int = ip_parse(ip_int) - ip_addr = str(ip_int).split("/")[0] - ip_addr_cidr = int(str(ip_int).split("/")[1]) - self.cmd({ "insert":{ "rule": { - "family": "inet", - "table": self.table_name, - "chain": "output", - "expr": [ - {'match': {'left': {'payload': {'protocol': ip_family(ip_int), 'field': 'saddr'}}, 'op': '==', 'right': {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}}}}, - {'match': {"left": { "payload": {"protocol": str(proto), "field": "sport"}}, "op": "==", "right": int(port)}}, - {"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}} - ] - }}}) - - def add_input(self, queue_range, proto = None, port = None, ip_int = None): - init, end = queue_range - if init > end: init, end = end, init - ip_int = ip_parse(ip_int) - ip_addr = str(ip_int).split("/")[0] - ip_addr_cidr = int(str(ip_int).split("/")[1]) - self.cmd({"insert":{"rule":{ - "family": "inet", - "table": self.table_name, - "chain": "input", - "expr": [ - {'match': {'left': {'payload': {'protocol': ip_family(ip_int), 'field': 'daddr'}}, 'op': '==', 'right': {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}}}}, - {'match': {"left": { "payload": {"protocol": str(proto), "field": "dport"}}, "op": "==", "right": int(port)}}, - {"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}} - ] - }}}) - - def get(self) -> List[FiregexFilter]: - res = [] - for filter in [ele["rule"] for ele in self.list() if "rule" in ele and ele["rule"]["table"] == self.table_name]: - queue_str = filter["expr"][2]["queue"]["num"] - queue = None - if isinstance(queue_str,dict): queue = int(queue_str["range"][0]), int(queue_str["range"][1]) - else: queue = int(queue_str), int(queue_str) - ip_int = None - if isinstance(filter["expr"][0]["match"]["right"],str): - ip_int = str(ip_parse(filter["expr"][0]["match"]["right"])) - else: - ip_int = f'{filter["expr"][0]["match"]["right"]["prefix"]["addr"]}/{filter["expr"][0]["match"]["right"]["prefix"]["len"]}' - res.append(FiregexFilter( - target=filter["chain"], - id=int(filter["handle"]), - queue=queue, - proto=filter["expr"][1]["match"]["left"]["payload"]["protocol"], - port=filter["expr"][1]["match"]["right"], - ip_int=ip_int - )) - return res - \ No newline at end of file diff --git a/backend/utils/loader.py b/backend/utils/loader.py index f10e3b3..d60c217 100644 --- a/backend/utils/loader.py +++ b/backend/utils/loader.py @@ -90,7 +90,7 @@ def load_routers(app): resets, startups, shutdowns = [], [], [] for router in get_router_modules(): if router.router: - app.include_router(router.router, prefix=f"/{router.name}") + app.include_router(router.router, prefix=f"/{router.name}", tags=[router.name]) if router.reset: resets.append(router.reset) if router.startup: diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5863c83..64ed9e8 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,11 +6,12 @@ import { Outlet, Route, Routes } from 'react-router-dom'; import MainLayout from './components/MainLayout'; import { PasswordSend, ServerStatusResponse } from './js/models'; import { errorNotify, fireUpdateRequest, getstatus, HomeRedirector, login, setpassword } from './js/utils'; -import NFRegex from './pages/NFRegex.tsx'; +import NFRegex from './pages/NFRegex'; import io from 'socket.io-client'; import RegexProxy from './pages/RegexProxy'; -import ServiceDetailsNFRegex from './pages/NFRegex.tsx/ServiceDetails'; +import ServiceDetailsNFRegex from './pages/NFRegex/ServiceDetails'; import ServiceDetailsProxyRegex from './pages/RegexProxy/ServiceDetails'; +import PortHijack from './pages/PortHijack'; const socket = io({transports: ["websocket", "polling"], path:"/sock" }); @@ -153,6 +154,7 @@ function App() { } > } /> + } /> } /> diff --git a/frontend/src/_vars.scss b/frontend/src/_vars.scss index e65237e..6df635d 100755 --- a/frontend/src/_vars.scss +++ b/frontend/src/_vars.scss @@ -1,4 +1,4 @@ $primary_color: #242a33; -$second_color: #1A1B1E; +$secondary_color: #1A1B1E; $third_color:#25262b; diff --git a/frontend/src/components/Footer/index.tsx b/frontend/src/components/Footer/index.tsx index 7f61c54..bcc4588 100755 --- a/frontend/src/components/Footer/index.tsx +++ b/frontend/src/components/Footer/index.tsx @@ -3,7 +3,6 @@ import React from 'react'; import style from "./index.module.scss"; - function FooterPage() { return