From e5973947e6b9f9fff98fe60566a2d92516e3013b Mon Sep 17 00:00:00 2001 From: Domingo Dirutigliano Date: Tue, 18 Feb 2025 23:49:53 +0100 Subject: [PATCH] test on settings API added + improves on nfproxy code including fail-open --- Dockerfile | 8 ++--- backend/modules/nfproxy/firegex.py | 5 ++- backend/modules/nfproxy/models.py | 3 +- backend/routers/nfproxy.py | 56 ++++++++++++++++++++++++++++-- tests/nf_test.py | 33 ++++++++++++++---- tests/ph_test.py | 5 +++ tests/utils/firegexapi.py | 8 +++-- 7 files changed, 100 insertions(+), 18 deletions(-) diff --git a/Dockerfile b/Dockerfile index d8270d2..a09e4e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,19 +15,19 @@ RUN bun run build #Building main conteiner FROM --platform=$TARGETARCH registry.fedoraproject.org/fedora:latest -RUN dnf -y update && dnf install -y python3.13-devel python3-pip @development-tools gcc-c++ \ +RUN dnf -y update && dnf install -y python3.13-devel @development-tools gcc-c++ \ libnetfilter_queue-devel libnfnetlink-devel libmnl-devel libcap-ng-utils nftables \ - vectorscan-devel libtins-devel python3-nftables libpcap-devel boost-devel + vectorscan-devel libtins-devel python3-nftables libpcap-devel boost-devel uv RUN mkdir -p /execute/modules WORKDIR /execute ADD ./backend/requirements.txt /execute/requirements.txt -RUN pip3 install --no-cache-dir --break-system-packages -r /execute/requirements.txt --no-warn-script-location +RUN uv pip install --no-cache --system -r /execute/requirements.txt COPY ./backend/binsrc /execute/binsrc RUN g++ binsrc/nfregex.cpp -o modules/cppregex -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl) -#RUN g++ binsrc/nfproxy.cpp -o modules/cpproxy -std=c++23 -O3 -lnetfilter_queue -lpython3.13 -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl python3) +RUN g++ binsrc/nfproxy.cpp -o modules/cpproxy -std=c++23 -O3 -lnetfilter_queue -lpython3.13 -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl python3) COPY ./backend/ /execute/ COPY --from=frontend /app/dist/ ./frontend/ diff --git a/backend/modules/nfproxy/firegex.py b/backend/modules/nfproxy/firegex.py index 37055c3..70fb5ca 100644 --- a/backend/modules/nfproxy/firegex.py +++ b/backend/modules/nfproxy/firegex.py @@ -48,7 +48,10 @@ class FiregexInterceptor: self.process = await asyncio.create_subprocess_exec( proxy_binary_path, stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE, - env={"NTHREADS": os.getenv("NTHREADS","1")}, + env={ + "NTHREADS": os.getenv("NTHREADS","1"), + "FIREGEX_NFQUEUE_FAIL_OPEN": "1" if self.srv.fail_open else "0", + }, ) line_fut = self.process.stdout.readuntil() try: diff --git a/backend/modules/nfproxy/models.py b/backend/modules/nfproxy/models.py index ba048c4..4417db0 100644 --- a/backend/modules/nfproxy/models.py +++ b/backend/modules/nfproxy/models.py @@ -1,12 +1,13 @@ class Service: - def __init__(self, service_id: str, status: str, port: int, name: str, proto: str, ip_int: str, **other): + def __init__(self, service_id: str, status: str, port: int, name: str, proto: str, ip_int: str, fail_open: bool, **other): self.id = service_id self.status = status self.port = port self.name = name self.proto = proto self.ip_int = ip_int + self.fail_open = fail_open @classmethod def from_dict(cls, var: dict): diff --git a/backend/routers/nfproxy.py b/backend/routers/nfproxy.py index 4cbb825..703fff7 100644 --- a/backend/routers/nfproxy.py +++ b/backend/routers/nfproxy.py @@ -18,10 +18,17 @@ class ServiceModel(BaseModel): n_filters: int edited_packets: int blocked_packets: int + fail_open: bool class RenameForm(BaseModel): name:str +class SettingsForm(BaseModel): + port: PortType|None = None + proto: str|None = None + ip_int: str|None = None + fail_open: bool|None = None + class PyFilterModel(BaseModel): filter_id: int name: str @@ -34,12 +41,13 @@ class ServiceAddForm(BaseModel): port: PortType proto: str ip_int: str + fail_open: bool = True class ServiceAddResponse(BaseModel): status:str service_id: str|None = None -#app = APIRouter() Not released in this version +app = APIRouter() db = SQLite('db/nft-pyfilters.db', { 'services': { @@ -49,6 +57,7 @@ db = SQLite('db/nft-pyfilters.db', { 'name': 'VARCHAR(100) NOT NULL UNIQUE', 'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "http"))', 'ip_int': 'VARCHAR(100) NOT NULL', + 'fail_open': 'BOOLEAN NOT NULL CHECK (fail_open IN (0, 1)) DEFAULT 1', }, 'pyfilter': { 'filter_id': 'INTEGER PRIMARY KEY', @@ -116,6 +125,7 @@ async def get_service_list(): s.name name, s.proto proto, s.ip_int ip_int, + s.fail_open fail_open, COUNT(f.filter_id) n_filters, COALESCE(SUM(f.blocked_packets),0) blocked_packets, COALESCE(SUM(f.edited_packets),0) edited_packets @@ -134,6 +144,7 @@ async def get_service_by_id(service_id: str): s.name name, s.proto proto, s.ip_int ip_int, + s.fail_open fail_open, COUNT(f.filter_id) n_filters, COALESCE(SUM(f.blocked_packets),0) blocked_packets, COALESCE(SUM(f.edited_packets),0) edited_packets @@ -180,6 +191,45 @@ async def service_rename(service_id: str, form: RenameForm): await refresh_frontend() return {'status': 'ok'} +@app.put('/services/{service_id}/settings', response_model=StatusMessageModel) +async def service_settings(service_id: str, form: SettingsForm): + """Request to change the settings of a specific service (will cause a restart)""" + + if form.proto is not None and form.proto not in ["tcp", "udp"]: + raise HTTPException(status_code=400, detail="Invalid protocol") + + if form.port is not None and (form.port < 1 or form.port > 65535): + raise HTTPException(status_code=400, detail="Invalid port") + + if form.ip_int is not None: + try: + form.ip_int = ip_parse(form.ip_int) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid address") + + keys = [] + values = [] + + for key, value in form.model_dump(exclude_none=True).items(): + keys.append(key) + values.append(value) + + if len(keys) == 0: + raise HTTPException(status_code=400, detail="No settings to change provided") + + try: + db.query(f'UPDATE services SET {", ".join([f"{key}=?" for key in keys])} WHERE service_id = ?;', *values, service_id) + except sqlite3.IntegrityError: + raise HTTPException(status_code=400, detail="A service with these settings already exists") + + old_status = firewall.get(service_id).status + await firewall.remove(service_id) + await firewall.reload() + await firewall.get(service_id).next(old_status) + + await refresh_frontend() + return {'status': 'ok'} + @app.get('/services/{service_id}/pyfilters', response_model=list[PyFilterModel]) async def get_service_pyfilter_list(service_id: str): """Get the list of the pyfilters of a service""" @@ -246,8 +296,8 @@ async def add_new_service(form: ServiceAddForm): srv_id = None try: srv_id = gen_service_id() - db.query("INSERT INTO services (service_id ,name, port, status, proto, ip_int) VALUES (?, ?, ?, ?, ?, ?)", - srv_id, refactor_name(form.name), form.port, STATUS.STOP, form.proto, form.ip_int) + db.query("INSERT INTO services (service_id ,name, port, status, proto, ip_int, fail_open) VALUES (?, ?, ?, ?, ?, ?, ?)", + srv_id, refactor_name(form.name), form.port, STATUS.STOP, form.proto, form.ip_int, form.fail_open) except sqlite3.IntegrityError: raise HTTPException(status_code=400, detail="This type of service already exists") await firewall.reload() diff --git a/tests/nf_test.py b/tests/nf_test.py index cc780bd..c8c7bd9 100644 --- a/tests/nf_test.py +++ b/tests/nf_test.py @@ -43,6 +43,11 @@ def exit_test(code): exit_test(1) exit(code) +srvs = firegex.nf_get_services() +for ele in srvs: + if ele['name'] == args.service_name: + firegex.nf_delete_service(ele['service_id']) + service_id = firegex.nf_add_service(args.service_name, args.port, args.proto , "::1" if args.ipv6 else "127.0.0.1" ) if service_id: puts(f"Sucessfully created service {service_id} ✔", color=colors.green) @@ -64,7 +69,7 @@ try: else: puts("Test Failed: Data was corrupted ", color=colors.red) exit_test(1) -except Exception as e: +except Exception: puts("Test Failed: Couldn't send data to the server ", color=colors.red) exit_test(1) #Add new regex @@ -194,10 +199,24 @@ else: exit_test(1) #Check if service was renamed correctly -for services in firegex.nf_get_services(): - if services["name"] == f"{args.service_name}2": - puts("Checked that service was renamed correctly ✔", color=colors.green) - exit_test(0) +service = firegex.nf_get_service(service_id) +if service["name"] == f"{args.service_name}2": + puts("Checked that service was renamed correctly ✔", color=colors.green) +else: + puts("Test Failed: Service wasn't renamed correctly ✗", color=colors.red) + exit_test(1) -puts("Test Failed: Service wasn't renamed correctly ✗", color=colors.red) -exit_test(1) +#Change settings +opposite_proto = "udp" if args.proto == "tcp" else "tcp" +if(firegex.nf_settings_service(service_id, 1338, opposite_proto, "::dead:beef" if args.ipv6 else "123.123.123.123", True)): + srv_updated = firegex.nf_get_service(service_id) + if srv_updated["port"] == 1338 and srv_updated["proto"] == opposite_proto and ("::dead:beef" if args.ipv6 else "123.123.123.123") in srv_updated["ip_int"] and srv_updated["fail_open"]: + puts("Sucessfully changed service settings ✔", color=colors.green) + else: + puts("Test Failed: Service settings weren't updated correctly ✗", color=colors.red) + exit_test(1) +else: + puts("Test Failed: Coulnd't change service settings ✗", color=colors.red) + exit_test(1) + +exit_test(0) diff --git a/tests/ph_test.py b/tests/ph_test.py index 7e1df9c..c54dae2 100644 --- a/tests/ph_test.py +++ b/tests/ph_test.py @@ -42,6 +42,11 @@ def exit_test(code): exit_test(1) exit(code) +srvs = firegex.ph_get_services() +for ele in srvs: + if ele['name'] == args.service_name: + firegex.ph_delete_service(ele['service_id']) + #Create and start serivce service_id = firegex.ph_add_service(args.service_name, args.port, args.port+1, args.proto , "::1" if args.ipv6 else "127.0.0.1", "::1" if args.ipv6 else "127.0.0.1") if service_id: diff --git a/tests/utils/firegexapi.py b/tests/utils/firegexapi.py index 13f7a0c..9d40304 100644 --- a/tests/utils/firegexapi.py +++ b/tests/utils/firegexapi.py @@ -101,6 +101,10 @@ class FiregexAPI: def nf_rename_service(self,service_id: str, newname: str): req = self.s.put(f"{self.address}api/nfregex/services/{service_id}/rename" , json={"name":newname}) return verify(req) + + def nf_settings_service(self,service_id: str, port: int, proto: str, ip_int: str, fail_open: bool): + req = self.s.put(f"{self.address}api/nfregex/services/{service_id}/settings" , json={"port":port, "proto":proto, "ip_int":ip_int, "fail_open":fail_open}) + return verify(req) def nf_get_service_regexes(self,service_id: str): req = self.s.get(f"{self.address}api/nfregex/services/{service_id}/regexes") @@ -127,9 +131,9 @@ class FiregexAPI: json={"service_id": service_id, "regex": regex, "mode": mode, "active": active, "is_case_sensitive": is_case_sensitive}) return verify(req) - def nf_add_service(self, name: str, port: int, proto: str, ip_int: str): + def nf_add_service(self, name: str, port: int, proto: str, ip_int: str, fail_open: bool = False): req = self.s.post(f"{self.address}api/nfregex/services" , - json={"name":name,"port":port, "proto": proto, "ip_int": ip_int}) + json={"name":name,"port":port, "proto": proto, "ip_int": ip_int, "fail_open": fail_open}) return req.json()["service_id"] if verify(req) else False #PortHijack