nfproxy module writing: written part of the firegex lib, frontend refactored and improved, c++ improves

This commit is contained in:
Domingo Dirutigliano
2025-02-20 19:51:28 +01:00
parent d6e7cab353
commit 8652f40235
51 changed files with 1864 additions and 343 deletions

1
fgex-lib/MANIFEST.in Normal file
View File

@@ -0,0 +1 @@
include requirements.txt

3
fgex-lib/README.md Normal file
View File

@@ -0,0 +1,3 @@
# Firegex Python Library and CLI
It's a work in progress!

6
fgex-lib/fgex Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env python3
from firegex.cli import run
if __name__ == "__main__":
run()

View File

@@ -0,0 +1,5 @@
# Firegex python library
Alias of 'firegex' libaray
It's a work in progress!

View File

@@ -0,0 +1 @@
from firegex import *

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env python3
from firegex.cli import run
if __name__ == "__main__":
run()

View File

@@ -0,0 +1,25 @@
import setuptools
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
setuptools.setup(
name="fgex",
version="0.0.0",
author="Pwnzer0tt1",
author_email="pwnzer0tt1@poliba.it",
py_modules=["fgex"],
install_requires=["fgex"],
include_package_data=True,
description="Firegex client",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/pwnzer0tt1/firegex",
packages=setuptools.find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Operating System :: OS Independent",
],
python_requires='>=3.10',
)

View File

@@ -0,0 +1,7 @@
__version__ = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0"
#Exported functions
__all__ = [
]

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env python3
from firegex.cli import run
if __name__ == "__main__":
run()

5
fgex-lib/firegex/cli.py Normal file
View File

@@ -0,0 +1,5 @@
def run():
pass # TODO implement me

View File

@@ -0,0 +1,38 @@
import functools
ACCEPT = 0
DROP = 1
REJECT = 2
MANGLE = 3
EXCEPTION = 4
INVALID = 5
def pyfilter(func):
"""
Decorator to mark functions that will be used in the proxy.
Stores the function reference in a global registry.
"""
if not hasattr(pyfilter, "registry"):
pyfilter.registry = set()
pyfilter.registry.add(func.__name__)
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
def get_pyfilters():
"""Returns the list of functions marked with @pyfilter."""
return list(pyfilter.registry)

View File

@@ -0,0 +1,161 @@
from inspect import signature
from firegex.nfproxy.params import RawPacket, NotReadyToRun
from firegex.nfproxy import ACCEPT, DROP, REJECT, MANGLE, EXCEPTION, INVALID
RESULTS = [
ACCEPT,
DROP,
REJECT,
MANGLE,
EXCEPTION,
INVALID
]
FULL_STREAM_ACTIONS = [
"flush"
"accept",
"reject",
"drop"
]
type_annotations_associations = {
"tcp": {
RawPacket: RawPacket.fetch_from_global
},
"http": {
RawPacket: RawPacket.fetch_from_global
}
}
def _generate_filter_structure(filters: list[str], proto:str, glob:dict, local:dict):
if proto not in type_annotations_associations.keys():
raise Exception("Invalid protocol")
res = []
valid_annotation_type = type_annotations_associations[proto]
def add_func_to_list(func):
if not callable(func):
raise Exception(f"{func} is not a function")
sig = signature(func)
params_function = []
for k, v in sig.parameters.items():
if v.annotation in valid_annotation_type.keys():
params_function.append((v.annotation, valid_annotation_type[v.annotation]))
else:
raise Exception(f"Invalid type annotation {v.annotation} for function {func.__name__}")
res.append((func, params_function))
for filter in filters:
if not isinstance(filter, str):
raise Exception("Invalid filter list: must be a list of strings")
if filter in glob.keys():
add_func_to_list(glob[filter])
elif filter in local.keys():
add_func_to_list(local[filter])
else:
raise Exception(f"Filter {filter} not found")
return res
def get_filters_info(code:str, proto:str):
glob = {}
local = {}
exec(code, glob, local)
exec("import firegex.nfproxy", glob, local)
filters = eval("firegex.nfproxy.get_pyfilters()", glob, local)
return _generate_filter_structure(filters, proto, glob, local)
def get_filter_names(code:str, proto:str):
return [ele[0].__name__ for ele in get_filters_info(code, proto)]
def compile():
glob = globals()
local = locals()
filters = glob["__firegex_pyfilter_enabled"]
proto = glob["__firegex_proto"]
glob["__firegex_func_list"] = _generate_filter_structure(filters, proto, glob, local)
glob["__firegex_stream"] = []
glob["__firegex_stream_size"] = 0
if "FGEX_STREAM_MAX_SIZE" in local and int(local["FGEX_STREAM_MAX_SIZE"]) > 0:
glob["__firegex_stream_max_size"] = int(local["FGEX_STREAM_MAX_SIZE"])
elif "FGEX_STREAM_MAX_SIZE" in glob and int(glob["FGEX_STREAM_MAX_SIZE"]) > 0:
glob["__firegex_stream_max_size"] = int(glob["FGEX_STREAM_MAX_SIZE"])
else:
glob["__firegex_stream_max_size"] = 1*8e20 # 1MB default value
if "FGEX_FULL_STREAM_ACTION" in local and local["FGEX_FULL_STREAM_ACTION"] in FULL_STREAM_ACTIONS:
glob["__firegex_full_stream_action"] = local["FGEX_FULL_STREAM_ACTION"]
else:
glob["__firegex_full_stream_action"] = "flush"
glob["__firegex_pyfilter_result"] = None
def handle_packet():
glob = globals()
func_list = glob["__firegex_func_list"]
final_result = ACCEPT
cache_call = {}
cache_call[RawPacket] = RawPacket.fetch_from_global()
data_size = len(cache_call[RawPacket].data)
if glob["__firegex_stream_size"]+data_size > glob["__firegex_stream_max_size"]:
match glob["__firegex_full_stream_action"]:
case "flush":
glob["__firegex_stream"] = []
glob["__firegex_stream_size"] = 0
case "accept":
glob["__firegex_pyfilter_result"] = {
"action": ACCEPT,
"matched_by": None,
"mangled_packet": None
}
return
case "reject":
glob["__firegex_pyfilter_result"] = {
"action": REJECT,
"matched_by": "@MAX_STREAM_SIZE_REACHED",
"mangled_packet": None
}
return
case "drop":
glob["__firegex_pyfilter_result"] = {
"action": DROP,
"matched_by": "@MAX_STREAM_SIZE_REACHED",
"mangled_packet": None
}
return
glob["__firegex_stream"].append(cache_call[RawPacket])
glob["__firegex_stream_size"] += data_size
func_name = None
mangled_packet = None
for filter in func_list:
final_params = []
for ele in filter[1]:
if ele[0] not in cache_call.keys():
try:
cache_call[ele[0]] = ele[1]()
except NotReadyToRun:
cache_call[ele[0]] = None
if cache_call[ele[0]] is None:
continue # Parsing raised NotReadyToRun, skip filter
final_params.append(cache_call[ele[0]])
res = filter[0](*final_params)
if res is None:
continue #ACCEPTED
if res == MANGLE:
if RawPacket not in cache_call.keys():
continue #Packet not modified
pkt:RawPacket = cache_call[RawPacket]
mangled_packet = pkt.raw_packet
break
elif res != ACCEPT:
final_result = res
func_name = filter[0].__name__
break
glob["__firegex_pyfilter_result"] = {
"action": final_result,
"matched_by": func_name,
"mangled_packet": mangled_packet
}

View File

@@ -0,0 +1,71 @@
class NotReadyToRun(Exception): # raise this exception if the stream state is not ready to parse this object, the call will be skipped
pass
class RawPacket:
def __init__(self,
data: bytes,
raw_packet: bytes,
is_input: bool,
is_ipv6: bool,
is_tcp: bool,
):
self.__data = bytes(data)
self.__raw_packet = bytes(raw_packet)
self.__is_input = bool(is_input)
self.__is_ipv6 = bool(is_ipv6)
self.__is_tcp = bool(is_tcp)
@property
def is_input(self) -> bool:
return self.__is_input
@property
def is_ipv6(self) -> bool:
return self.__is_ipv6
@property
def is_tcp(self) -> bool:
return self.__is_tcp
@property
def data(self) -> bytes:
return self.__data
@property
def proto_header(self) -> bytes:
return self.__raw_packet[:self.proto_header_len]
@property
def proto_header_len(self) -> int:
return len(self.__raw_packet) - len(self.__data)
@data.setter
def data(self, v:bytes):
if not isinstance(v, bytes):
raise Exception("Invalid data type, data MUST be of type bytes")
self.__raw_packet = self.proto_header + v
self.__data = v
@property
def raw_packet(self) -> bytes:
return self.__raw_packet
@raw_packet.setter
def raw_packet(self, v:bytes):
if not isinstance(v, bytes):
raise Exception("Invalid data type, data MUST be of type bytes")
if len(v) < self.proto_header_len:
raise Exception("Invalid packet length")
header_len = self.proto_header_len
self.__data = v[header_len:]
self.__raw_packet = v
@staticmethod
def fetch_from_global():
glob = globals()
if "__firegex_packet_info" not in glob.keys():
raise Exception("Packet info not found")
return RawPacket(**glob["__firegex_packet_info"])

10
fgex-lib/requirements.txt Normal file
View File

@@ -0,0 +1,10 @@
typer==0.15.1
requests>=2.32.3
pydantic>=2
typing-extensions>=4.7.1
fasteners==0.19
textual==2.1.0
python-socketio[client]==5.12.1
fgex
orjson

31
fgex-lib/setup.py Normal file
View File

@@ -0,0 +1,31 @@
import setuptools
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
with open('requirements.txt', 'r', encoding='utf-8') as f:
required = [ele.strip() for ele in f.read().splitlines() if not ele.strip().startswith("#") and ele.strip() != ""]
VERSION = "{{VERSION_PLACEHOLDER}}"
setuptools.setup(
name="firegex",
version= VERSION if "{" not in VERSION else "0.0.0", #uv pip install -U . --no-cache-dir for testing
author="Pwnzer0tt1",
author_email="pwnzer0tt1@poliba.it",
scripts=["fgex"],
py_modules=["fgex"],
install_requires=required,
include_package_data=True,
description="Firegex client",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/pwnzer0tt1/firegex",
packages=setuptools.find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Operating System :: OS Independent",
],
python_requires='>=3.10',
)