sd
This commit is contained in:
31
README.md
31
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/))
|
- 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/)
|
- 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).
|
- 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
|
## Documentation
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ class FiregexInterceptor:
|
|||||||
|
|
||||||
async def _start_binary(self):
|
async def _start_binary(self):
|
||||||
proxy_binary_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../cpproxy"))
|
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(
|
self.process = await asyncio.create_subprocess_exec(
|
||||||
proxy_binary_path, stdin=asyncio.subprocess.DEVNULL,
|
proxy_binary_path, stdin=asyncio.subprocess.DEVNULL,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
@@ -106,7 +108,9 @@ class FiregexInterceptor:
|
|||||||
env={
|
env={
|
||||||
"NTHREADS": os.getenv("NTHREADS","1"),
|
"NTHREADS": os.getenv("NTHREADS","1"),
|
||||||
"FIREGEX_NFQUEUE_FAIL_OPEN": "1" if self.srv.fail_open else "0",
|
"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)
|
nicenessify(-10, self.process.pid)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ def convert_protocol_to_l4(proto:str):
|
|||||||
return "tcp"
|
return "tcp"
|
||||||
elif proto == "http":
|
elif proto == "http":
|
||||||
return "tcp"
|
return "tcp"
|
||||||
|
elif proto == "udp":
|
||||||
|
return "udp"
|
||||||
else:
|
else:
|
||||||
raise Exception("Invalid protocol")
|
raise Exception("Invalid protocol")
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ db = SQLite('db/nft-pyfilters.db', {
|
|||||||
'status': 'VARCHAR(100) NOT NULL',
|
'status': 'VARCHAR(100) NOT NULL',
|
||||||
'port': 'INT NOT NULL CHECK(port > 0 and port < 65536)',
|
'port': 'INT NOT NULL CHECK(port > 0 and port < 65536)',
|
||||||
'name': 'VARCHAR(100) NOT NULL UNIQUE',
|
'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"))',
|
'l4_proto': 'VARCHAR(3) NOT NULL CHECK (l4_proto IN ("tcp", "udp"))',
|
||||||
'ip_int': 'VARCHAR(100) NOT NULL',
|
'ip_int': 'VARCHAR(100) NOT NULL',
|
||||||
'fail_open': 'BOOLEAN NOT NULL CHECK (fail_open IN (0, 1)) DEFAULT 1',
|
'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)
|
form.ip_int = ip_parse(form.ip_int)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=400, detail="Invalid address")
|
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")
|
raise HTTPException(status_code=400, detail="Invalid protocol")
|
||||||
srv_id = None
|
srv_id = None
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Traffic Viewer - JSON Event Format
|
# 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
|
## JSON Event Schema
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import NFProxy from './pages/NFProxy';
|
|||||||
import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails';
|
import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails';
|
||||||
import TrafficViewer from './pages/NFProxy/TrafficViewer';
|
import TrafficViewer from './pages/NFProxy/TrafficViewer';
|
||||||
import TrafficViewerMain from './pages/TrafficViewer';
|
import TrafficViewerMain from './pages/TrafficViewer';
|
||||||
|
import SetupPage from './pages/Setup';
|
||||||
import { useAuthStore } from './js/store';
|
import { useAuthStore } from './js/store';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@@ -179,6 +180,7 @@ const PageRouting = ({ getStatus }:{ getStatus:()=>void }) => {
|
|||||||
<Route path="traffic" element={<TrafficViewerMain />} />
|
<Route path="traffic" element={<TrafficViewerMain />} />
|
||||||
<Route path="firewall" element={<Firewall />} />
|
<Route path="firewall" element={<Firewall />} />
|
||||||
<Route path="porthijack" element={<PortHijack />} />
|
<Route path="porthijack" element={<PortHijack />} />
|
||||||
|
<Route path="setup" element={<SetupPage />} />
|
||||||
<Route path="*" element={<HomeRedirector />} />
|
<Route path="*" element={<HomeRedirector />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>
|
|||||||
validate:{
|
validate:{
|
||||||
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
||||||
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
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",
|
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={[
|
data={[
|
||||||
{ label: 'TCP', value: 'tcp' },
|
{ label: 'TCP', value: 'tcp' },
|
||||||
{ label: 'HTTP', value: 'http' },
|
{ label: 'HTTP', value: 'http' },
|
||||||
|
{ label: 'UDP', value: 'udp' },
|
||||||
]}
|
]}
|
||||||
{...form.getInputProps('proto')}
|
{...form.getInputProps('proto')}
|
||||||
/>}
|
/>}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export const HELP_NFPROXY_SIM = `➤ fgex nfproxy -h
|
|||||||
│ * port INTEGER The port of the target to proxy [default: None] [required] │
|
│ * port INTEGER The port of the target to proxy [default: None] [required] │
|
||||||
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
╭─ Options ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
╭─ 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-address TEXT The address of the local server [default: None] │
|
||||||
│ --from-port INTEGER The port of the local server [default: 7474] │
|
│ --from-port INTEGER The port of the local server [default: 7474] │
|
||||||
│ -6 Use IPv6 for the connection │
|
│ -6 Use IPv6 for the connection │
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { PiWallLight } from "react-icons/pi";
|
|||||||
import { useNavbarStore } from "../../js/store";
|
import { useNavbarStore } from "../../js/store";
|
||||||
import { getMainPath } from "../../js/utils";
|
import { getMainPath } from "../../js/utils";
|
||||||
import { BsRegex } from "react-icons/bs";
|
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 }:
|
function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }:
|
||||||
{ navigate?: string, closeNav: () => void, name: string, icon: any, color: MantineColor, disabled?: boolean, onClick?: CallableFunction }) {
|
{ navigate?: string, closeNav: () => void, name: string, icon: any, color: MantineColor, disabled?: boolean, onClick?: CallableFunction }) {
|
||||||
@@ -42,6 +42,7 @@ export default function NavBar() {
|
|||||||
<NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="blue" icon={<GrDirections size={19} />} />
|
<NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="blue" icon={<GrDirections size={19} />} />
|
||||||
<NavBarButton navigate="nfproxy" closeNav={closeNav} name="Netfilter Proxy" color="lime" icon={<TbPlugConnected size={19} />} />
|
<NavBarButton navigate="nfproxy" closeNav={closeNav} name="Netfilter Proxy" color="lime" icon={<TbPlugConnected size={19} />} />
|
||||||
<NavBarButton navigate="traffic" closeNav={closeNav} name="Traffic Viewer" color="cyan" icon={<MdVisibility size={19} />} />
|
<NavBarButton navigate="traffic" closeNav={closeNav} name="Traffic Viewer" color="cyan" icon={<MdVisibility size={19} />} />
|
||||||
|
<NavBarButton navigate="setup" closeNav={closeNav} name="Setup Import/Export" color="teal" icon={<MdSettings size={19} />} />
|
||||||
{/* <Box px="xs" mt="lg">
|
{/* <Box px="xs" mt="lg">
|
||||||
<Title order={5}>Experimental Features 🧪</Title>
|
<Title order={5}>Experimental Features 🧪</Title>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -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 { Navigate, useNavigate, useParams } from 'react-router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
|
import { nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
|
||||||
@@ -30,6 +30,9 @@ export default function TrafficViewer() {
|
|||||||
const [events, eventsHandlers] = useListState<TrafficEvent>([]);
|
const [events, eventsHandlers] = useListState<TrafficEvent>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [filterText, setFilterText] = useState('');
|
const [filterText, setFilterText] = useState('');
|
||||||
|
const [filterDirection, setFilterDirection] = useState<string | null>(null);
|
||||||
|
const [filterProto, setFilterProto] = useState<string | null>(null);
|
||||||
|
const [filterVerdict, setFilterVerdict] = useState<string | null>(null);
|
||||||
const [selectedEvent, setSelectedEvent] = useState<TrafficEvent | null>(null);
|
const [selectedEvent, setSelectedEvent] = useState<TrafficEvent | null>(null);
|
||||||
const [modalOpened, setModalOpened] = useState(false);
|
const [modalOpened, setModalOpened] = useState(false);
|
||||||
|
|
||||||
@@ -87,15 +90,37 @@ export default function TrafficViewer() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filteredEvents = events.filter((e: TrafficEvent) => {
|
const filteredEvents = events.filter((e: TrafficEvent) => {
|
||||||
if (!filterText) return true;
|
// Text filter
|
||||||
|
if (filterText) {
|
||||||
const search = filterText.toLowerCase();
|
const search = filterText.toLowerCase();
|
||||||
return (
|
const matchesText = (
|
||||||
e.src_ip?.toLowerCase().includes(search) ||
|
e.src_ip?.toLowerCase().includes(search) ||
|
||||||
e.dst_ip?.toLowerCase().includes(search) ||
|
e.dst_ip?.toLowerCase().includes(search) ||
|
||||||
e.verdict?.toLowerCase().includes(search) ||
|
e.verdict?.toLowerCase().includes(search) ||
|
||||||
e.filter?.toLowerCase().includes(search) ||
|
e.filter?.toLowerCase().includes(search) ||
|
||||||
e.proto?.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) => {
|
const formatTimestamp = (ts: number) => {
|
||||||
@@ -147,13 +172,55 @@ export default function TrafficViewer() {
|
|||||||
<Divider my="md" />
|
<Divider my="md" />
|
||||||
|
|
||||||
<Box px="md">
|
<Box px="md">
|
||||||
|
<Grid mb="md">
|
||||||
|
<Grid.Col span={{ base: 12, md: 6 }}>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Filter by IP, verdict, filter name, or protocol..."
|
placeholder="Search by IP, port, verdict, filter name, or protocol..."
|
||||||
value={filterText}
|
value={filterText}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterText(e.currentTarget.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterText(e.currentTarget.value)}
|
||||||
leftSection={<FaFilter />}
|
leftSection={<FaFilter />}
|
||||||
mb="md"
|
|
||||||
/>
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={{ base: 12, md: 2 }}>
|
||||||
|
<Select
|
||||||
|
placeholder="Direction"
|
||||||
|
clearable
|
||||||
|
value={filterDirection}
|
||||||
|
onChange={setFilterDirection}
|
||||||
|
data={[
|
||||||
|
{ value: 'in', label: 'Incoming' },
|
||||||
|
{ value: 'out', label: 'Outgoing' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={{ base: 12, md: 2 }}>
|
||||||
|
<Select
|
||||||
|
placeholder="Protocol"
|
||||||
|
clearable
|
||||||
|
value={filterProto}
|
||||||
|
onChange={setFilterProto}
|
||||||
|
data={[
|
||||||
|
{ value: 'tcp', label: 'TCP' },
|
||||||
|
{ value: 'udp', label: 'UDP' },
|
||||||
|
{ value: 'http', label: 'HTTP' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={{ base: 12, md: 2 }}>
|
||||||
|
<Select
|
||||||
|
placeholder="Verdict"
|
||||||
|
clearable
|
||||||
|
value={filterVerdict}
|
||||||
|
onChange={setFilterVerdict}
|
||||||
|
data={[
|
||||||
|
{ value: 'accept', label: 'Accept' },
|
||||||
|
{ value: 'drop', label: 'Drop' },
|
||||||
|
{ value: 'reject', label: 'Reject' },
|
||||||
|
{ value: 'edited', label: 'Edited' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<ScrollArea style={{ height: 'calc(100vh - 280px)' }}>
|
<ScrollArea style={{ height: 'calc(100vh - 280px)' }}>
|
||||||
<Table striped highlightOnHover>
|
<Table striped highlightOnHover>
|
||||||
|
|||||||
@@ -1,20 +1,107 @@
|
|||||||
import { ActionIcon, Badge, Box, Card, Divider, Group, LoadingOverlay, Space, Text, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
import { ActionIcon, Badge, Box, Card, Divider, Group, LoadingOverlay, Select, Space, Text, TextInput, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { nfproxyServiceQuery } from '../../components/NFProxy/utils';
|
import { nfproxyServiceQuery } from '../../components/NFProxy/utils';
|
||||||
|
import { nfregexServiceQuery } from '../../components/NFRegex/utils';
|
||||||
import { isMediumScreen } from '../../js/utils';
|
import { isMediumScreen } from '../../js/utils';
|
||||||
import { MdDoubleArrow, MdVisibility } from 'react-icons/md';
|
import { MdDoubleArrow, MdVisibility } from 'react-icons/md';
|
||||||
import { TbPlugConnected } from 'react-icons/tb';
|
import { TbPlugConnected } from 'react-icons/tb';
|
||||||
import { FaServer } from 'react-icons/fa';
|
import { FaFilter, FaServer } from 'react-icons/fa';
|
||||||
|
import { BsRegex } from 'react-icons/bs';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
type UnifiedService = {
|
||||||
|
service_id: string;
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
port: number;
|
||||||
|
proto: string;
|
||||||
|
ip_int: string;
|
||||||
|
type: 'nfproxy' | 'nfregex';
|
||||||
|
stats: {
|
||||||
|
edited_packets?: number;
|
||||||
|
blocked_packets?: number;
|
||||||
|
n_packets?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default function TrafficViewer() {
|
export default function TrafficViewer() {
|
||||||
const services = nfproxyServiceQuery();
|
const nfproxyServices = nfproxyServiceQuery();
|
||||||
|
const nfregexServices = nfregexServiceQuery();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const isMedium = isMediumScreen();
|
const isMedium = isMediumScreen();
|
||||||
|
const [filterText, setFilterText] = useState('');
|
||||||
|
const [filterType, setFilterType] = useState<string | null>(null);
|
||||||
|
const [filterProto, setFilterProto] = useState<string | null>(null);
|
||||||
|
const [filterStatus, setFilterStatus] = useState<string | null>(null);
|
||||||
|
|
||||||
if (services.isLoading) return <LoadingOverlay visible={true} />;
|
if (nfproxyServices.isLoading || nfregexServices.isLoading) {
|
||||||
|
return <LoadingOverlay visible={true} />;
|
||||||
|
}
|
||||||
|
|
||||||
const activeServices = services.data?.filter(s => s.status === 'active') || [];
|
// Combine services from both modules
|
||||||
const stoppedServices = services.data?.filter(s => s.status !== 'active') || [];
|
const allServices: UnifiedService[] = [
|
||||||
|
...(nfproxyServices.data?.map(s => ({
|
||||||
|
service_id: s.service_id,
|
||||||
|
name: s.name,
|
||||||
|
status: s.status,
|
||||||
|
port: s.port,
|
||||||
|
proto: s.proto,
|
||||||
|
ip_int: s.ip_int,
|
||||||
|
type: 'nfproxy' as const,
|
||||||
|
stats: {
|
||||||
|
edited_packets: s.edited_packets,
|
||||||
|
blocked_packets: s.blocked_packets
|
||||||
|
}
|
||||||
|
})) || []),
|
||||||
|
...(nfregexServices.data?.map(s => ({
|
||||||
|
service_id: s.service_id,
|
||||||
|
name: s.name,
|
||||||
|
status: s.status,
|
||||||
|
port: s.port,
|
||||||
|
proto: s.proto,
|
||||||
|
ip_int: s.ip_int,
|
||||||
|
type: 'nfregex' as const,
|
||||||
|
stats: {
|
||||||
|
n_packets: s.n_packets
|
||||||
|
}
|
||||||
|
})) || [])
|
||||||
|
];
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
const filteredServices = allServices.filter(service => {
|
||||||
|
// Text filter
|
||||||
|
if (filterText) {
|
||||||
|
const search = filterText.toLowerCase();
|
||||||
|
const matchesText = (
|
||||||
|
service.name.toLowerCase().includes(search) ||
|
||||||
|
service.service_id.toLowerCase().includes(search) ||
|
||||||
|
service.port.toString().includes(search) ||
|
||||||
|
service.ip_int.toLowerCase().includes(search)
|
||||||
|
);
|
||||||
|
if (!matchesText) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type filter
|
||||||
|
if (filterType && service.type !== filterType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protocol filter
|
||||||
|
if (filterProto && service.proto !== filterProto) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status filter
|
||||||
|
if (filterStatus) {
|
||||||
|
if (filterStatus === 'active' && service.status !== 'active') return false;
|
||||||
|
if (filterStatus === 'stopped' && service.status === 'active') return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeServices = filteredServices.filter(s => s.status === 'active');
|
||||||
|
const stoppedServices = filteredServices.filter(s => s.status !== 'active');
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Box px="md" mt="lg">
|
<Box px="md" mt="lg">
|
||||||
@@ -26,20 +113,72 @@ export default function TrafficViewer() {
|
|||||||
Traffic Viewer
|
Traffic Viewer
|
||||||
</Title>
|
</Title>
|
||||||
<Text c="dimmed" mt="sm">
|
<Text c="dimmed" mt="sm">
|
||||||
Monitor live network traffic for all NFProxy services
|
Monitor live network traffic for NFProxy and NFRegex services
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Divider my="lg" />
|
<Divider my="lg" />
|
||||||
|
|
||||||
{services.data?.length === 0 ? (
|
<Box px="md" mb="lg">
|
||||||
|
<Group grow>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Search by name, ID, port, or IP..."
|
||||||
|
value={filterText}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterText(e.currentTarget.value)}
|
||||||
|
leftSection={<FaFilter />}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
placeholder="Service Type"
|
||||||
|
clearable
|
||||||
|
value={filterType}
|
||||||
|
onChange={setFilterType}
|
||||||
|
data={[
|
||||||
|
{ value: 'nfproxy', label: 'Netfilter Proxy' },
|
||||||
|
{ value: 'nfregex', label: 'Netfilter Regex' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
placeholder="Protocol"
|
||||||
|
clearable
|
||||||
|
value={filterProto}
|
||||||
|
onChange={setFilterProto}
|
||||||
|
data={[
|
||||||
|
{ value: 'tcp', label: 'TCP' },
|
||||||
|
{ value: 'udp', label: 'UDP' },
|
||||||
|
{ value: 'http', label: 'HTTP' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
placeholder="Status"
|
||||||
|
clearable
|
||||||
|
value={filterStatus}
|
||||||
|
onChange={setFilterStatus}
|
||||||
|
data={[
|
||||||
|
{ value: 'active', label: 'Active' },
|
||||||
|
{ value: 'stopped', label: 'Stopped' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{allServices.length === 0 ? (
|
||||||
<Box px="md">
|
<Box px="md">
|
||||||
<Title order={3} className='center-flex' style={{ textAlign: "center" }}>
|
<Title order={3} className='center-flex' style={{ textAlign: "center" }}>
|
||||||
No NFProxy services found
|
No services found
|
||||||
</Title>
|
</Title>
|
||||||
<Space h="xs" />
|
<Space h="xs" />
|
||||||
<Text className='center-flex' style={{ textAlign: "center" }} c="dimmed">
|
<Text className='center-flex' style={{ textAlign: "center" }} c="dimmed">
|
||||||
Create a service in the Netfilter Proxy section to start monitoring traffic
|
Create services in Netfilter Proxy or Netfilter Regex to start monitoring traffic
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
) : filteredServices.length === 0 ? (
|
||||||
|
<Box px="md">
|
||||||
|
<Title order={3} className='center-flex' style={{ textAlign: "center" }}>
|
||||||
|
No services match your filters
|
||||||
|
</Title>
|
||||||
|
<Space h="xs" />
|
||||||
|
<Text className='center-flex' style={{ textAlign: "center" }} c="dimmed">
|
||||||
|
Try adjusting your filter criteria
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
@@ -51,15 +190,32 @@ export default function TrafficViewer() {
|
|||||||
Running Services
|
Running Services
|
||||||
</Title>
|
</Title>
|
||||||
{activeServices.map(service => (
|
{activeServices.map(service => (
|
||||||
<Card key={service.service_id} shadow="sm" padding="lg" radius="md" withBorder mb="md">
|
<Card key={`${service.type}-${service.service_id}`} shadow="sm" padding="lg" radius="md" withBorder mb="md">
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Box>
|
<Box>
|
||||||
<Group>
|
<Group>
|
||||||
<ThemeIcon color="lime" variant="light" size="lg">
|
<ThemeIcon
|
||||||
|
color={service.type === 'nfproxy' ? 'lime' : 'grape'}
|
||||||
|
variant="light"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
{service.type === 'nfproxy' ? (
|
||||||
<TbPlugConnected size={20} />
|
<TbPlugConnected size={20} />
|
||||||
|
) : (
|
||||||
|
<BsRegex size={20} />
|
||||||
|
)}
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
<div>
|
<div>
|
||||||
|
<Group gap="xs">
|
||||||
<Text fw={700} size="lg">{service.name}</Text>
|
<Text fw={700} size="lg">{service.name}</Text>
|
||||||
|
<Badge
|
||||||
|
size="xs"
|
||||||
|
color={service.type === 'nfproxy' ? 'lime' : 'grape'}
|
||||||
|
variant="dot"
|
||||||
|
>
|
||||||
|
{service.type === 'nfproxy' ? 'Proxy' : 'Regex'}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
<Group gap="xs" mt={4}>
|
<Group gap="xs" mt={4}>
|
||||||
<Badge color="cyan" size="sm">:{service.port}</Badge>
|
<Badge color="cyan" size="sm">:{service.port}</Badge>
|
||||||
<Badge color="violet" size="sm">{service.proto}</Badge>
|
<Badge color="violet" size="sm">{service.proto}</Badge>
|
||||||
@@ -71,13 +227,21 @@ export default function TrafficViewer() {
|
|||||||
<Box>
|
<Box>
|
||||||
<Group>
|
<Group>
|
||||||
<Box style={{ textAlign: 'right' }}>
|
<Box style={{ textAlign: 'right' }}>
|
||||||
|
{service.type === 'nfproxy' ? (
|
||||||
|
<>
|
||||||
<Badge color="orange" size="sm" mb={4}>
|
<Badge color="orange" size="sm" mb={4}>
|
||||||
{service.edited_packets} edited
|
{service.stats.edited_packets || 0} edited
|
||||||
</Badge>
|
</Badge>
|
||||||
<br />
|
<br />
|
||||||
<Badge color="yellow" size="sm">
|
<Badge color="yellow" size="sm">
|
||||||
{service.blocked_packets} blocked
|
{service.stats.blocked_packets || 0} blocked
|
||||||
</Badge>
|
</Badge>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Badge color="yellow" size="sm">
|
||||||
|
{service.stats.n_packets || 0} blocked
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Tooltip label="View traffic">
|
<Tooltip label="View traffic">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
@@ -85,11 +249,12 @@ export default function TrafficViewer() {
|
|||||||
size="xl"
|
size="xl"
|
||||||
radius="md"
|
radius="md"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
onClick={() => navigate(`/nfproxy/${service.service_id}/traffic`)}
|
onClick={() => navigate(`/${service.type}/${service.service_id}/traffic`)}
|
||||||
>
|
>
|
||||||
<MdDoubleArrow size="24px" />
|
<MdDoubleArrow size="24px" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</Tooltip>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -110,11 +275,16 @@ export default function TrafficViewer() {
|
|||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Box>
|
<Box>
|
||||||
<Group>
|
<Group>
|
||||||
<ThemeIcon color="gray" variant="light" size="lg">
|
<ThemeIcon color={service.type === 'nfproxy' ? 'lime' : 'grape'} variant="light" size="lg">
|
||||||
<FaServer size={18} />
|
{service.type === 'nfproxy' ? <TbPlugConnected size={18} /> : <BsRegex size={18} />}
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
<div>
|
<div>
|
||||||
|
<Group gap="xs">
|
||||||
<Text fw={500} size="lg" c="dimmed">{service.name}</Text>
|
<Text fw={500} size="lg" c="dimmed">{service.name}</Text>
|
||||||
|
<Badge color="gray" size="sm">
|
||||||
|
{service.type === 'nfproxy' ? 'Proxy' : 'Regex'}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
<Group gap="xs" mt={4}>
|
<Group gap="xs" mt={4}>
|
||||||
<Badge color="gray" size="sm">:{service.port}</Badge>
|
<Badge color="gray" size="sm">:{service.port}</Badge>
|
||||||
<Badge color="gray" size="sm">{service.proto}</Badge>
|
<Badge color="gray" size="sm">{service.proto}</Badge>
|
||||||
|
|||||||
Reference in New Issue
Block a user