From 59652fc697417328f37edc43ceda7e5390ee91a4 Mon Sep 17 00:00:00 2001 From: Domingo Dirutigliano Date: Tue, 18 Feb 2025 17:36:15 +0100 Subject: [PATCH] optional nfqueue fail-open option --- backend/binsrc/classes/nfqueue.cpp | 18 +-- backend/binsrc/nfregex.cpp | 9 +- backend/modules/nfregex/firegex.py | 6 +- backend/modules/nfregex/models.py | 3 +- backend/routers/nfregex.py | 54 ++++++- .../src/components/NFRegex/AddEditService.tsx | 139 ++++++++++++++++++ .../src/components/NFRegex/AddNewService.tsx | 105 ------------- .../components/NFRegex/ServiceRow/index.tsx | 13 +- frontend/src/components/NFRegex/utils.ts | 15 +- frontend/src/pages/NFRegex/ServiceDetails.tsx | 13 +- frontend/src/pages/NFRegex/index.tsx | 5 +- 11 files changed, 247 insertions(+), 133 deletions(-) create mode 100644 frontend/src/components/NFRegex/AddEditService.tsx delete mode 100644 frontend/src/components/NFRegex/AddNewService.tsx diff --git a/backend/binsrc/classes/nfqueue.cpp b/backend/binsrc/classes/nfqueue.cpp index f794ed7..513db4a 100644 --- a/backend/binsrc/classes/nfqueue.cpp +++ b/backend/binsrc/classes/nfqueue.cpp @@ -237,17 +237,15 @@ class NfQueue { nlh = nfq_nlmsg_put(queue_msg_buffer, NFQNL_MSG_CONFIG, queue_num); nfq_nlmsg_cfg_put_params(nlh, NFQNL_COPY_PACKET, 0xffff); - #ifdef NFQUEUE_FAIL_OPEN + char * enable_fail_open = getenv("FIREGEX_NFQUEUE_FAIL_OPEN"); - mnl_attr_put_u32(nlh, NFQA_CFG_FLAGS, htonl(NFQA_CFG_F_GSO|NFQA_CFG_F_FAIL_OPEN)); - mnl_attr_put_u32(nlh, NFQA_CFG_MASK, htonl(NFQA_CFG_F_GSO|NFQA_CFG_F_FAIL_OPEN)); - - #else - - mnl_attr_put_u32(nlh, NFQA_CFG_FLAGS, htonl(NFQA_CFG_F_GSO)); - mnl_attr_put_u32(nlh, NFQA_CFG_MASK, htonl(NFQA_CFG_F_GSO)); - - #endif + if (strcmp(enable_fail_open, "1") == 0){ + mnl_attr_put_u32(nlh, NFQA_CFG_FLAGS, htonl(NFQA_CFG_F_GSO|NFQA_CFG_F_FAIL_OPEN)); + mnl_attr_put_u32(nlh, NFQA_CFG_MASK, htonl(NFQA_CFG_F_GSO|NFQA_CFG_F_FAIL_OPEN)); + }else{ + mnl_attr_put_u32(nlh, NFQA_CFG_FLAGS, htonl(NFQA_CFG_F_GSO)); + mnl_attr_put_u32(nlh, NFQA_CFG_MASK, htonl(NFQA_CFG_F_GSO)); + } if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) { _clear(); diff --git a/backend/binsrc/nfregex.cpp b/backend/binsrc/nfregex.cpp index 1b95ef8..dacd6c1 100644 --- a/backend/binsrc/nfregex.cpp +++ b/backend/binsrc/nfregex.cpp @@ -9,10 +9,7 @@ using namespace Firegex::Regex; using Firegex::NfQueue::MultiThreadQueue; /* - Compile options: -NFQUEUE_FAIL_OPEN - enable fail-open option of nfqueueß ---- USE_PIPES_FOR_BLOKING_QUEUE - use pipes instead of conditional variable, queue and mutex for blocking queue */ @@ -63,14 +60,14 @@ int main(int argc, char *argv[]){ if (matchmode != nullptr && strcmp(matchmode, "block") == 0){ stream_mode = false; } + + bool fail_open = strcmp(getenv("FIREGEX_NFQUEUE_FAIL_OPEN"), "1") == 0; regex_config.reset(new RegexRules(stream_mode)); - MultiThreadQueue queue_manager(n_of_threads); - osyncstream(cout) << "QUEUE " << queue_manager.queue_num() << endl; - cerr << "[info] [main] Queue: " << queue_manager.queue_num() << " threads assigned: " << n_of_threads << " stream mode: " << stream_mode << endl; + cerr << "[info] [main] Queue: " << queue_manager.queue_num() << " threads assigned: " << n_of_threads << " stream mode: " << stream_mode << " fail open: " << fail_open << endl; thread qthr([&](){ queue_manager.start(); diff --git a/backend/modules/nfregex/firegex.py b/backend/modules/nfregex/firegex.py index f74dca7..026b832 100644 --- a/backend/modules/nfregex/firegex.py +++ b/backend/modules/nfregex/firegex.py @@ -88,7 +88,11 @@ class FiregexInterceptor: self.process = await asyncio.create_subprocess_exec( proxy_binary_path, stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE, - env={"MATCH_MODE": "stream" if self.srv.proto == "tcp" else "block", "NTHREADS": os.getenv("NTHREADS","1")}, + env={ + "MATCH_MODE": "stream" if self.srv.proto == "tcp" else "block", + "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/nfregex/models.py b/backend/modules/nfregex/models.py index 0c36890..c06daa6 100644 --- a/backend/modules/nfregex/models.py +++ b/backend/modules/nfregex/models.py @@ -1,13 +1,14 @@ import base64 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/nfregex.py b/backend/routers/nfregex.py index 1a41d41..744b6f2 100644 --- a/backend/routers/nfregex.py +++ b/backend/routers/nfregex.py @@ -19,10 +19,17 @@ class ServiceModel(BaseModel): ip_int: str n_regex: int n_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 RegexModel(BaseModel): regex:str mode:str @@ -44,6 +51,7 @@ class ServiceAddForm(BaseModel): port: PortType proto: str ip_int: str + fail_open: bool = False class ServiceAddResponse(BaseModel): status:str @@ -59,6 +67,7 @@ db = SQLite('db/nft-regex.db', { 'name': 'VARCHAR(100) NOT NULL UNIQUE', 'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "udp"))', 'ip_int': 'VARCHAR(100) NOT NULL', + 'fail_open': 'BOOLEAN NOT NULL CHECK (fail_open IN (0, 1)) DEFAULT 1' }, 'regexes': { 'regex': 'TEXT NOT NULL', @@ -128,6 +137,7 @@ async def get_service_list(): s.name name, s.proto proto, s.ip_int ip_int, + s.fail_open fail_open, COUNT(r.regex_id) n_regex, COALESCE(SUM(r.blocked_packets),0) n_packets FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id @@ -145,6 +155,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(r.regex_id) n_regex, COALESCE(SUM(r.blocked_packets),0) n_packets FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id @@ -190,6 +201,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}/regexes', response_model=list[RegexModel]) async def get_service_regexe_list(service_id: str): """Get the list of the regexes of a service""" @@ -275,8 +325,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/frontend/src/components/NFRegex/AddEditService.tsx b/frontend/src/components/NFRegex/AddEditService.tsx new file mode 100644 index 0000000..1a696f5 --- /dev/null +++ b/frontend/src/components/NFRegex/AddEditService.tsx @@ -0,0 +1,139 @@ +import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { useEffect, useState } from 'react'; +import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils'; +import { ImCross } from "react-icons/im" +import { nfregex, Service } from './utils'; +import PortAndInterface from '../PortAndInterface'; +import { IoMdInformationCircleOutline } from "react-icons/io"; +import { ServiceAddForm as ServiceAddFormOriginal } from './utils'; + +type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean} + +function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) { + + const initialValues = { + name: "", + port:edit?.port??8080, + ip_int:edit?.ip_int??"", + proto:edit?.proto??"tcp", + fail_open: edit?.fail_open??false, + autostart: true + } + + const form = useForm({ + initialValues: initialValues, + validate:{ + name: (value) => edit? null : value !== "" ? null : "Service name is required", + port: (value) => (value>0 && value<65536) ? null : "Invalid port", + proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol", + ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address", + } + }) + + useEffect(() => { + if (opened){ + form.setInitialValues(initialValues) + form.reset() + } + }, [opened]) + + const close = () =>{ + onClose() + form.reset() + setError(null) + } + + const [submitLoading, setSubmitLoading] = useState(false) + const [error, setError] = useState(null) + + const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{ + setSubmitLoading(true) + if (edit){ + nfregex.settings(edit.service_id, { port, proto, ip_int, fail_open }).then( res => { + if (!res){ + setSubmitLoading(false) + close(); + okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`) + } + }).catch( err => { + setSubmitLoading(false) + setError("Request Failed! [ "+err+" ]") + }) + }else{ + nfregex.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => { + if (res.status === "ok" && res.service_id){ + setSubmitLoading(false) + close(); + if (autostart) nfregex.servicestart(res.service_id) + okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`) + }else{ + setSubmitLoading(false) + setError("Invalid request! [ "+res.status+" ]") + } + }).catch( err => { + setSubmitLoading(false) + setError("Request Failed! [ "+err+" ]") + }) + } + } + + + return +
+ {!edit?:null} + + + + + + + {!edit?:null} + + + Enable fail-open nfqueue + + + Firegex use internally nfqueue to handle packets
enabling this option will allow packets to pass through the firewall
in case the filtering is too slow or too many traffic is coming
+ }> + +
+
} + {...form.getInputProps('fail_open', { type: 'checkbox' })} + /> +
+ + + + + + + + + {error?<> + + } color="red" onClose={()=>{setError(null)}}> + Error: {error} + + :null} + + +
+ +} + +export default AddEditService; diff --git a/frontend/src/components/NFRegex/AddNewService.tsx b/frontend/src/components/NFRegex/AddNewService.tsx deleted file mode 100644 index f88f7a9..0000000 --- a/frontend/src/components/NFRegex/AddNewService.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box } from '@mantine/core'; -import { useForm } from '@mantine/form'; -import { useState } from 'react'; -import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils'; -import { ImCross } from "react-icons/im" -import { nfregex } from './utils'; -import PortAndInterface from '../PortAndInterface'; - -type ServiceAddForm = { - name:string, - port:number, - proto:string, - ip_int:string, - autostart: boolean, -} - -function AddNewService({ opened, onClose }:{ opened:boolean, onClose:()=>void }) { - - const form = useForm({ - initialValues: { - name:"", - port:8080, - ip_int:"", - proto:"tcp", - autostart: true - }, - validate:{ - name: (value) => value !== "" ? null : "Service name is required", - port: (value) => (value>0 && value<65536) ? null : "Invalid port", - proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol", - ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address", - } - }) - - const close = () =>{ - onClose() - form.reset() - setError(null) - } - - const [submitLoading, setSubmitLoading] = useState(false) - const [error, setError] = useState(null) - - const submitRequest = ({ name, port, autostart, proto, ip_int }:ServiceAddForm) =>{ - setSubmitLoading(true) - nfregex.servicesadd({name, port, proto, ip_int }).then( res => { - if (res.status === "ok" && res.service_id){ - setSubmitLoading(false) - close(); - if (autostart) nfregex.servicestart(res.service_id) - okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`) - }else{ - setSubmitLoading(false) - setError("Invalid request! [ "+res.status+" ]") - } - }).catch( err => { - setSubmitLoading(false) - setError("Request Failed! [ "+err+" ]") - }) - } - - - return -
- - - - - - - - - - - - - - - - {error?<> - - } color="red" onClose={()=>{setError(null)}}> - Error: {error} - - :null} - - -
- -} - -export default AddNewService; diff --git a/frontend/src/components/NFRegex/ServiceRow/index.tsx b/frontend/src/components/NFRegex/ServiceRow/index.tsx index d8167bc..53a5c1b 100644 --- a/frontend/src/components/NFRegex/ServiceRow/index.tsx +++ b/frontend/src/components/NFRegex/ServiceRow/index.tsx @@ -12,6 +12,8 @@ import { MenuDropDownWithButton } from '../../MainLayout'; import { useQueryClient } from '@tanstack/react-query'; import { FaFilter } from "react-icons/fa"; import { VscRegex } from "react-icons/vsc"; +import { IoSettingsSharp } from 'react-icons/io5'; +import AddEditService from '../AddEditService'; export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) { @@ -26,6 +28,7 @@ export default function ServiceRow({ service, onClick }:{ service:Service, onCli const [tooltipStopOpened, setTooltipStopOpened] = useState(false); const [deleteModal, setDeleteModal] = useState(false) const [renameModal, setRenameModal] = useState(false) + const [editModal, setEditModal] = useState(false) const isMedium = isMediumScreen() const stopService = async () => { @@ -104,12 +107,13 @@ export default function ServiceRow({ service, onClick }:{ service:Service, onCli {isMedium?:} - Rename service + Edit service + } onClick={()=>setEditModal(true)}>Service Settings } onClick={()=>setRenameModal(true)}>Change service name Danger zone } onClick={()=>setDeleteModal(true)}>Delete Service - + + setEditModal(false)} + edit={service} + /> } diff --git a/frontend/src/components/NFRegex/utils.ts b/frontend/src/components/NFRegex/utils.ts index 1c34bc3..faa67c4 100644 --- a/frontend/src/components/NFRegex/utils.ts +++ b/frontend/src/components/NFRegex/utils.ts @@ -12,6 +12,7 @@ export type Service = { ip_int: string, n_packets:number, n_regex:number, + fail_open:boolean, } export type ServiceAddForm = { @@ -19,6 +20,14 @@ export type ServiceAddForm = { port:number, proto:string, ip_int:string, + fail_open: boolean, +} + +export type ServiceSettings = { + port?:number, + proto?:string, + ip_int?:string, + fail_open?: boolean, } export type ServiceAddResponse = { @@ -79,5 +88,9 @@ export const nfregex = { }, serviceregexes: async (service_id:string) => { return await getapi(`nfregex/services/${service_id}/regexes`) as RegexFilter[]; - } + }, + settings: async (service_id:string, data:ServiceSettings) => { + const { status } = await putapi(`nfregex/services/${service_id}/settings`,data) as ServerResponse; + return status === "ok"?undefined:status + }, } \ No newline at end of file diff --git a/frontend/src/pages/NFRegex/ServiceDetails.tsx b/frontend/src/pages/NFRegex/ServiceDetails.tsx index 176a83d..bb6255f 100644 --- a/frontend/src/pages/NFRegex/ServiceDetails.tsx +++ b/frontend/src/pages/NFRegex/ServiceDetails.tsx @@ -3,7 +3,7 @@ import { Navigate, useNavigate, useParams } from 'react-router-dom'; import RegexView from '../../components/RegexView'; import AddNewRegex from '../../components/AddNewRegex'; import { BsPlusLg } from "react-icons/bs"; -import { nfregexServiceQuery, nfregexServiceRegexesQuery } from '../../components/NFRegex/utils'; +import { nfregexServiceQuery, nfregexServiceRegexesQuery, Service } from '../../components/NFRegex/utils'; import { Badge, Divider, Menu } from '@mantine/core'; import { useState } from 'react'; import { FaFilter, FaPlay, FaStop } from 'react-icons/fa'; @@ -18,6 +18,8 @@ import { MenuDropDownWithButton } from '../../components/MainLayout'; import { useQueryClient } from '@tanstack/react-query'; import { FaArrowLeft } from "react-icons/fa"; import { VscRegex } from 'react-icons/vsc'; +import { IoSettingsSharp } from 'react-icons/io5'; +import AddEditService from '../../components/NFRegex/AddEditService'; export default function ServiceDetailsNFRegex() { @@ -29,6 +31,7 @@ export default function ServiceDetailsNFRegex() { const regexesList = nfregexServiceRegexesQuery(srv??"") const [deleteModal, setDeleteModal] = useState(false) const [renameModal, setRenameModal] = useState(false) + const [editModal, setEditModal] = useState(false) const [buttonLoading, setButtonLoading] = useState(false) const queryClient = useQueryClient() const [tooltipStopOpened, setTooltipStopOpened] = useState(false); @@ -108,7 +111,8 @@ export default function ServiceDetailsNFRegex() { - Rename service + Edit service + } onClick={()=>setEditModal(true)}>Service Settings } onClick={()=>setRenameModal(true)}>Change service name Danger zone @@ -190,5 +194,10 @@ export default function ServiceDetailsNFRegex() { opened={renameModal} service={serviceInfo} /> + setEditModal(false)} + edit={serviceInfo} + /> } diff --git a/frontend/src/pages/NFRegex/index.tsx b/frontend/src/pages/NFRegex/index.tsx index 8ad5445..6153458 100644 --- a/frontend/src/pages/NFRegex/index.tsx +++ b/frontend/src/pages/NFRegex/index.tsx @@ -5,7 +5,7 @@ import { useNavigate, useParams } from 'react-router-dom'; import ServiceRow from '../../components/NFRegex/ServiceRow'; import { nfregexServiceQuery } from '../../components/NFRegex/utils'; import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils'; -import AddNewService from '../../components/NFRegex/AddNewService'; +import AddEditService from '../../components/NFRegex/AddEditService'; import AddNewRegex from '../../components/AddNewRegex'; import { useQueryClient } from '@tanstack/react-query'; import { TbReload } from 'react-icons/tb'; @@ -81,13 +81,12 @@ function NFRegex({ children }: { children: any }) { } - } {srv?children:null} {srv? : - + } }