From c23711207791fe9365bae3703170f693c7e77267 Mon Sep 17 00:00:00 2001 From: Ilya Starchak <66072408+ilyastar9999@users.noreply.github.com> Date: Wed, 10 Dec 2025 02:17:54 +0300 Subject: [PATCH] sd --- README.md | 31 +++ backend/modules/nfproxy/firegex.py | 6 +- backend/modules/nfproxy/nftables.py | 2 + backend/routers/nfproxy.py | 4 +- docs/TRAFFIC_VIEWER.md | 2 +- frontend/src/App.tsx | 2 + .../src/components/NFProxy/AddEditService.tsx | 3 +- .../src/components/NFProxy/NFProxyDocs.tsx | 2 +- frontend/src/components/NavBar/index.tsx | 3 +- frontend/src/pages/NFProxy/TrafficViewer.tsx | 101 ++++++-- frontend/src/pages/TrafficViewer/index.tsx | 220 ++++++++++++++++-- 11 files changed, 327 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 467d440..38bb0c2 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,37 @@ All the configuration at the startup is customizable in [firegex.py](./run.py) o - Create basic firewall rules to allow and deny specific traffic, like ufw or iptables but using firegex graphic interface (by using [nftable](https://netfilter.org/projects/nftables/)) - Port Hijacking allows you to redirect the traffic on a specific port to another port. Thanks to this you can start your own proxy, connecting to the real service using the loopback interface. Firegex will be resposable about the routing of the packets using internally [nftables](https://netfilter.org/projects/nftables/) - EXPERIMENTAL: Netfilter Proxy uses [nfqueue](https://netfilter.org/projects/libnetfilter_queue/) to simulate a python proxy, you can write your own filter in python and use it to filter the traffic. There are built-in some data handler to parse protocols like HTTP, and before apply the filter you can test it with fgex command (you need to install firegex lib from pypi). +- Traffic Viewer allows you to monitor live network traffic for all services in real-time +- Setup Import/Export allows you to backup and restore your entire Firegex configuration as a JSON file, making it easy to deploy identical configurations across multiple servers + +## Configuration Management + +Firegex supports importing and exporting configurations via JSON files. This is useful for: +- Backing up your configuration +- Deploying the same setup across multiple servers +- Version controlling your firewall rules +- Quick disaster recovery + +### Using the Web Interface +Navigate to "Setup Import/Export" in the sidebar to: +- **Export**: Download your current configuration as JSON +- **Import from File**: Upload a setup.json file +- **Import from JSON**: Paste JSON directly into the interface + +### Using the API +```bash +# Export configuration +curl -H "Authorization: Bearer YOUR_TOKEN" \ + http://localhost:4444/api/setup/export > setup.json + +# Import configuration +curl -X POST -H "Authorization: Bearer YOUR_TOKEN" \ + -H "Content-Type: application/json" \ + -d @setup.json \ + http://localhost:4444/api/setup/import +``` + +See [setup.example.json](setup.example.json) for the configuration file format. ## Documentation diff --git a/backend/modules/nfproxy/firegex.py b/backend/modules/nfproxy/firegex.py index 54cd7cf..4723389 100644 --- a/backend/modules/nfproxy/firegex.py +++ b/backend/modules/nfproxy/firegex.py @@ -99,6 +99,8 @@ class FiregexInterceptor: async def _start_binary(self): proxy_binary_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../cpproxy")) + # Determine match mode based on protocol + match_mode = "stream" if self.srv.proto in ["tcp", "http"] else "block" self.process = await asyncio.create_subprocess_exec( proxy_binary_path, stdin=asyncio.subprocess.DEVNULL, stdout=asyncio.subprocess.PIPE, @@ -106,7 +108,9 @@ class FiregexInterceptor: env={ "NTHREADS": os.getenv("NTHREADS","1"), "FIREGEX_NFQUEUE_FAIL_OPEN": "1" if self.srv.fail_open else "0", - "FIREGEX_NFPROXY_SOCK": self.sock_path + "FIREGEX_NFPROXY_SOCK": self.sock_path, + "MATCH_MODE": match_mode, + "PROTOCOL": self.srv.proto }, ) nicenessify(-10, self.process.pid) diff --git a/backend/modules/nfproxy/nftables.py b/backend/modules/nfproxy/nftables.py index 6aa7c71..3a4c359 100644 --- a/backend/modules/nfproxy/nftables.py +++ b/backend/modules/nfproxy/nftables.py @@ -6,6 +6,8 @@ def convert_protocol_to_l4(proto:str): return "tcp" elif proto == "http": return "tcp" + elif proto == "udp": + return "udp" else: raise Exception("Invalid protocol") diff --git a/backend/routers/nfproxy.py b/backend/routers/nfproxy.py index e9a40b3..534efbe 100644 --- a/backend/routers/nfproxy.py +++ b/backend/routers/nfproxy.py @@ -65,7 +65,7 @@ db = SQLite('db/nft-pyfilters.db', { 'status': 'VARCHAR(100) NOT NULL', 'port': 'INT NOT NULL CHECK(port > 0 and port < 65536)', 'name': 'VARCHAR(100) NOT NULL UNIQUE', - 'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "http"))', + 'proto': 'VARCHAR(4) NOT NULL CHECK (proto IN ("tcp", "http", "udp"))', 'l4_proto': 'VARCHAR(3) NOT NULL CHECK (l4_proto IN ("tcp", "udp"))', 'ip_int': 'VARCHAR(100) NOT NULL', 'fail_open': 'BOOLEAN NOT NULL CHECK (fail_open IN (0, 1)) DEFAULT 1', @@ -305,7 +305,7 @@ async def add_new_service(form: ServiceAddForm): form.ip_int = ip_parse(form.ip_int) except ValueError: raise HTTPException(status_code=400, detail="Invalid address") - if form.proto not in ["tcp", "http"]: + if form.proto not in ["tcp", "http", "udp"]: raise HTTPException(status_code=400, detail="Invalid protocol") srv_id = None try: diff --git a/docs/TRAFFIC_VIEWER.md b/docs/TRAFFIC_VIEWER.md index 70fbbe4..eab58bf 100644 --- a/docs/TRAFFIC_VIEWER.md +++ b/docs/TRAFFIC_VIEWER.md @@ -1,6 +1,6 @@ # Traffic Viewer - JSON Event Format -The traffic viewer is now fully integrated. To enable structured event display, the NFProxy C++ binary (`backend/binsrc/nfproxy.cpp`) should emit JSON lines to stdout with the following format: +The traffic viewer is now fully integrated and supports **TCP, HTTP, and UDP** protocols. To enable structured event display, the NFProxy C++ binary (`backend/binsrc/nfproxy.cpp`) should emit JSON lines to stdout with the following format: ## JSON Event Schema diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2242bc4..c9a48ea 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -15,6 +15,7 @@ import NFProxy from './pages/NFProxy'; import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails'; import TrafficViewer from './pages/NFProxy/TrafficViewer'; import TrafficViewerMain from './pages/TrafficViewer'; +import SetupPage from './pages/Setup'; import { useAuthStore } from './js/store'; function App() { @@ -179,6 +180,7 @@ const PageRouting = ({ getStatus }:{ getStatus:()=>void }) => { } /> } /> } /> + } /> } /> diff --git a/frontend/src/components/NFProxy/AddEditService.tsx b/frontend/src/components/NFProxy/AddEditService.tsx index c827259..decab63 100644 --- a/frontend/src/components/NFProxy/AddEditService.tsx +++ b/frontend/src/components/NFProxy/AddEditService.tsx @@ -26,7 +26,7 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=> validate:{ name: (value) => edit? null : value !== "" ? null : "Service name is required", port: (value) => (value>0 && value<65536) ? null : "Invalid port", - proto: (value) => ["tcp","http"].includes(value) ? null : "Invalid protocol", + proto: (value) => ["tcp","http","udp"].includes(value) ? null : "Invalid protocol", ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address", } }) @@ -115,6 +115,7 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=> data={[ { label: 'TCP', value: 'tcp' }, { label: 'HTTP', value: 'http' }, + { label: 'UDP', value: 'udp' }, ]} {...form.getInputProps('proto')} />} diff --git a/frontend/src/components/NFProxy/NFProxyDocs.tsx b/frontend/src/components/NFProxy/NFProxyDocs.tsx index a12ee86..f7e2490 100644 --- a/frontend/src/components/NFProxy/NFProxyDocs.tsx +++ b/frontend/src/components/NFProxy/NFProxyDocs.tsx @@ -72,7 +72,7 @@ export const HELP_NFPROXY_SIM = `➤ fgex nfproxy -h │ * port INTEGER The port of the target to proxy [default: None] [required] │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Options ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ --proto [tcp|http] The protocol to proxy [default: tcp] │ +│ --proto [tcp|http|udp] The protocol to proxy [default: tcp] │ │ --from-address TEXT The address of the local server [default: None] │ │ --from-port INTEGER The port of the local server [default: 7474] │ │ -6 Use IPv6 for the connection │ diff --git a/frontend/src/components/NavBar/index.tsx b/frontend/src/components/NavBar/index.tsx index 06a7544..1edc5d0 100644 --- a/frontend/src/components/NavBar/index.tsx +++ b/frontend/src/components/NavBar/index.tsx @@ -7,7 +7,7 @@ import { PiWallLight } from "react-icons/pi"; import { useNavbarStore } from "../../js/store"; import { getMainPath } from "../../js/utils"; import { BsRegex } from "react-icons/bs"; -import { MdVisibility } from "react-icons/md"; +import { MdVisibility, MdSettings } from "react-icons/md"; function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }: { navigate?: string, closeNav: () => void, name: string, icon: any, color: MantineColor, disabled?: boolean, onClick?: CallableFunction }) { @@ -42,6 +42,7 @@ export default function NavBar() { } /> } /> } /> + } /> {/* Experimental Features 🧪 diff --git a/frontend/src/pages/NFProxy/TrafficViewer.tsx b/frontend/src/pages/NFProxy/TrafficViewer.tsx index 1562d0c..ee32507 100644 --- a/frontend/src/pages/NFProxy/TrafficViewer.tsx +++ b/frontend/src/pages/NFProxy/TrafficViewer.tsx @@ -1,4 +1,4 @@ -import { ActionIcon, Badge, Box, Code, Divider, Grid, LoadingOverlay, Modal, ScrollArea, Space, Table, Text, TextInput, Title, Tooltip } from '@mantine/core'; +import { ActionIcon, Badge, Box, Code, Divider, Grid, Group, LoadingOverlay, Modal, ScrollArea, Select, Space, Table, Text, TextInput, Title, Tooltip } from '@mantine/core'; import { Navigate, useNavigate, useParams } from 'react-router'; import { useEffect, useState } from 'react'; import { nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils'; @@ -30,6 +30,9 @@ export default function TrafficViewer() { const [events, eventsHandlers] = useListState([]); const [loading, setLoading] = useState(true); const [filterText, setFilterText] = useState(''); + const [filterDirection, setFilterDirection] = useState(null); + const [filterProto, setFilterProto] = useState(null); + const [filterVerdict, setFilterVerdict] = useState(null); const [selectedEvent, setSelectedEvent] = useState(null); const [modalOpened, setModalOpened] = useState(false); @@ -87,15 +90,37 @@ export default function TrafficViewer() { }; const filteredEvents = events.filter((e: TrafficEvent) => { - if (!filterText) return true; - const search = filterText.toLowerCase(); - return ( - e.src_ip?.toLowerCase().includes(search) || - e.dst_ip?.toLowerCase().includes(search) || - e.verdict?.toLowerCase().includes(search) || - e.filter?.toLowerCase().includes(search) || - e.proto?.toLowerCase().includes(search) - ); + // Text filter + if (filterText) { + const search = filterText.toLowerCase(); + const matchesText = ( + e.src_ip?.toLowerCase().includes(search) || + e.dst_ip?.toLowerCase().includes(search) || + e.verdict?.toLowerCase().includes(search) || + e.filter?.toLowerCase().includes(search) || + e.proto?.toLowerCase().includes(search) || + e.src_port?.toString().includes(search) || + e.dst_port?.toString().includes(search) + ); + if (!matchesText) return false; + } + + // Direction filter + if (filterDirection && e.direction !== filterDirection) { + return false; + } + + // Protocol filter + if (filterProto && e.proto !== filterProto) { + return false; + } + + // Verdict filter + if (filterVerdict && e.verdict !== filterVerdict) { + return false; + } + + return true; }); const formatTimestamp = (ts: number) => { @@ -147,13 +172,55 @@ export default function TrafficViewer() { - ) => setFilterText(e.currentTarget.value)} - leftSection={} - mb="md" - /> + + + ) => setFilterText(e.currentTarget.value)} + leftSection={} + /> + + + + + + + + + + + {allServices.length === 0 ? ( - No NFProxy services found + No services found - Create a service in the Netfilter Proxy section to start monitoring traffic + Create services in Netfilter Proxy or Netfilter Regex to start monitoring traffic + + + ) : filteredServices.length === 0 ? ( + + + No services match your filters + + + + Try adjusting your filter criteria ) : ( @@ -51,15 +190,32 @@ export default function TrafficViewer() { Running Services {activeServices.map(service => ( - + - - + + {service.type === 'nfproxy' ? ( + + ) : ( + + )}
- {service.name} + + {service.name} + + {service.type === 'nfproxy' ? 'Proxy' : 'Regex'} + + :{service.port} {service.proto} @@ -71,13 +227,21 @@ export default function TrafficViewer() { - - {service.edited_packets} edited - -
- - {service.blocked_packets} blocked - + {service.type === 'nfproxy' ? ( + <> + + {service.stats.edited_packets || 0} edited + +
+ + {service.stats.blocked_packets || 0} blocked + + + ) : ( + + {service.stats.n_packets || 0} blocked + + )}
navigate(`/nfproxy/${service.service_id}/traffic`)} + onClick={() => navigate(`/${service.type}/${service.service_id}/traffic`)} > +
@@ -110,11 +275,16 @@ export default function TrafficViewer() { - - + + {service.type === 'nfproxy' ? : }
- {service.name} + + {service.name} + + {service.type === 'nfproxy' ? 'Proxy' : 'Regex'} + + :{service.port} {service.proto}