This commit is contained in:
Ilya Starchak
2025-12-10 02:17:54 +03:00
parent 811773e009
commit c237112077
11 changed files with 327 additions and 49 deletions

View File

@@ -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 }) => {
<Route path="traffic" element={<TrafficViewerMain />} />
<Route path="firewall" element={<Firewall />} />
<Route path="porthijack" element={<PortHijack />} />
<Route path="setup" element={<SetupPage />} />
<Route path="*" element={<HomeRedirector />} />
</Route>
</Routes>

View File

@@ -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')}
/>}

View File

@@ -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 │

View File

@@ -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() {
<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="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">
<Title order={5}>Experimental Features 🧪</Title>
</Box>

View File

@@ -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<TrafficEvent>([]);
const [loading, setLoading] = useState(true);
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 [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() {
<Divider my="md" />
<Box px="md">
<TextInput
placeholder="Filter by IP, verdict, filter name, or protocol..."
value={filterText}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterText(e.currentTarget.value)}
leftSection={<FaFilter />}
mb="md"
/>
<Grid mb="md">
<Grid.Col span={{ base: 12, md: 6 }}>
<TextInput
placeholder="Search by IP, port, verdict, filter name, or protocol..."
value={filterText}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterText(e.currentTarget.value)}
leftSection={<FaFilter />}
/>
</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)' }}>
<Table striped highlightOnHover>

View File

@@ -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 { nfproxyServiceQuery } from '../../components/NFProxy/utils';
import { nfregexServiceQuery } from '../../components/NFRegex/utils';
import { isMediumScreen } from '../../js/utils';
import { MdDoubleArrow, MdVisibility } from 'react-icons/md';
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() {
const services = nfproxyServiceQuery();
const nfproxyServices = nfproxyServiceQuery();
const nfregexServices = nfregexServiceQuery();
const navigate = useNavigate();
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') || [];
const stoppedServices = services.data?.filter(s => s.status !== 'active') || [];
// Combine services from both modules
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 <>
<Box px="md" mt="lg">
@@ -26,20 +113,72 @@ export default function TrafficViewer() {
Traffic Viewer
</Title>
<Text c="dimmed" mt="sm">
Monitor live network traffic for all NFProxy services
Monitor live network traffic for NFProxy and NFRegex services
</Text>
</Box>
<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">
<Title order={3} className='center-flex' style={{ textAlign: "center" }}>
No NFProxy services found
No services found
</Title>
<Space h="xs" />
<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>
</Box>
) : (
@@ -51,15 +190,32 @@ export default function TrafficViewer() {
Running Services
</Title>
{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">
<Box>
<Group>
<ThemeIcon color="lime" variant="light" size="lg">
<TbPlugConnected size={20} />
<ThemeIcon
color={service.type === 'nfproxy' ? 'lime' : 'grape'}
variant="light"
size="lg"
>
{service.type === 'nfproxy' ? (
<TbPlugConnected size={20} />
) : (
<BsRegex size={20} />
)}
</ThemeIcon>
<div>
<Text fw={700} size="lg">{service.name}</Text>
<Group gap="xs">
<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}>
<Badge color="cyan" size="sm">:{service.port}</Badge>
<Badge color="violet" size="sm">{service.proto}</Badge>
@@ -71,13 +227,21 @@ export default function TrafficViewer() {
<Box>
<Group>
<Box style={{ textAlign: 'right' }}>
<Badge color="orange" size="sm" mb={4}>
{service.edited_packets} edited
</Badge>
<br />
<Badge color="yellow" size="sm">
{service.blocked_packets} blocked
</Badge>
{service.type === 'nfproxy' ? (
<>
<Badge color="orange" size="sm" mb={4}>
{service.stats.edited_packets || 0} edited
</Badge>
<br />
<Badge color="yellow" size="sm">
{service.stats.blocked_packets || 0} blocked
</Badge>
</>
) : (
<Badge color="yellow" size="sm">
{service.stats.n_packets || 0} blocked
</Badge>
)}
</Box>
<Tooltip label="View traffic">
<ActionIcon
@@ -85,11 +249,12 @@ export default function TrafficViewer() {
size="xl"
radius="md"
variant="filled"
onClick={() => navigate(`/nfproxy/${service.service_id}/traffic`)}
onClick={() => navigate(`/${service.type}/${service.service_id}/traffic`)}
>
<MdDoubleArrow size="24px" />
</ActionIcon>
</Tooltip>
</Tooltip>
</Group>
</Box>
</Group>
@@ -110,11 +275,16 @@ export default function TrafficViewer() {
<Group justify="space-between">
<Box>
<Group>
<ThemeIcon color="gray" variant="light" size="lg">
<FaServer size={18} />
<ThemeIcon color={service.type === 'nfproxy' ? 'lime' : 'grape'} variant="light" size="lg">
{service.type === 'nfproxy' ? <TbPlugConnected size={18} /> : <BsRegex size={18} />}
</ThemeIcon>
<div>
<Text fw={500} size="lg" c="dimmed">{service.name}</Text>
<Group gap="xs">
<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}>
<Badge color="gray" size="sm">:{service.port}</Badge>
<Badge color="gray" size="sm">{service.proto}</Badge>