diff --git a/backend/src/api/sniffer_api.py b/backend/src/api/sniffer_api.py
index 6aa5f71..4a2add6 100644
--- a/backend/src/api/sniffer_api.py
+++ b/backend/src/api/sniffer_api.py
@@ -58,7 +58,7 @@ class SnifferStatusResponse(BaseModel):
# Endpoints
# ------------------------------
-@router.post("/sniffer/start", response_model=SnifferStartResponse)
+@router.post("/start", response_model=SnifferStartResponse)
def sniffer_start(req: SnifferStartRequest):
"""
Start the AF_PACKET sniffer for the given bridge.
@@ -70,7 +70,7 @@ def sniffer_start(req: SnifferStartRequest):
raise HTTPException(status_code=500, detail=f"Failed to start sniffer: {exc}")
-@router.post("/sniffer/stop", response_model=SnifferStopResponse)
+@router.post("/stop", response_model=SnifferStopResponse)
def sniffer_stop():
"""
Stop the AF_PACKET sniffer (if running).
@@ -82,7 +82,7 @@ def sniffer_stop():
raise HTTPException(status_code=500, detail=f"Failed to stop sniffer: {exc}")
-@router.get("/sniffer/status", response_model=SnifferStatusResponse)
+@router.get("/status", response_model=SnifferStatusResponse)
def sniffer_status():
"""
Return the sniffer status information.
diff --git a/frontend/src/Layout/Sidebar.tsx b/frontend/src/Layout/Sidebar.tsx
index f1ed6bd..6127c0c 100644
--- a/frontend/src/Layout/Sidebar.tsx
+++ b/frontend/src/Layout/Sidebar.tsx
@@ -1,5 +1,5 @@
// src/components/Sidebar.tsx
-import { ApartmentOutlined, HomeOutlined, InfoCircleOutlined, SettingOutlined } from '@ant-design/icons';
+import { ApartmentOutlined, ApiOutlined, HomeOutlined, InfoCircleOutlined, SettingOutlined } from '@ant-design/icons';
import { Layout, Menu } from 'antd';
import React from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
@@ -11,6 +11,7 @@ const { Sider } = Layout;
const menuItems = [
{ key: PATHS.HOME, icon: , label: 'Home' },
{ key: PATHS.NETWORK, icon: , label: 'Network' },
+ { key: PATHS.SNIFFING, icon: , label: 'Sniffing' },
{ key: '/about', icon: , label: 'About' },
{ key: '/settings', icon: , label: 'Settings' },
];
diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts
index f5c9078..304ba77 100644
--- a/frontend/src/api/apiClient.ts
+++ b/frontend/src/api/apiClient.ts
@@ -1,13 +1,14 @@
// src/apiClient.ts
import axios from "axios";
import {
- BridgeCreateRequest,
- BridgeInfo,
- BridgeRemoveRequest,
- FullState,
- InterfaceInfo,
- RouteInfo,
+ BridgeCreateRequest,
+ BridgeInfo,
+ BridgeRemoveRequest,
+ FullState,
+ InterfaceInfo,
+ RouteInfo,
} from "../types/network";
+import { SnifferStatusResponse } from "../types/sniffer";
const BASE = "http://mitm.lan/api";
@@ -17,7 +18,7 @@ export const api = axios.create({
timeout: 10000,
});
-// 🔹 Normalize FastAPI errors here
+// Normalize FastAPI errors here
api.interceptors.response.use(
(response) => response,
(error) => {
@@ -59,6 +60,12 @@ export const fetchFullState = async (): Promise => {
return res.data;
};
+export const fetchSnifferStatus = async (): Promise => {
+ const res = await api.get("/sniffer/status");
+ return res.data;
+};
+
+
// Mutations
export const createBridge = async (req: BridgeCreateRequest) => {
const res = await api.post("network/bridge/create", req);
diff --git a/frontend/src/appRouter.tsx b/frontend/src/appRouter.tsx
index c411d2f..57c46d8 100644
--- a/frontend/src/appRouter.tsx
+++ b/frontend/src/appRouter.tsx
@@ -3,6 +3,7 @@ import { Navigate, Route, Routes } from 'react-router-dom';
import App from './App'; // your layout component (has )
import Home from './pages/Home';
import Network from './pages/Network';
+import Sniffing from './pages/Sniffing';
import { PATHS } from './routes';
export default function AppRouter() {
@@ -16,6 +17,7 @@ export default function AppRouter() {
{/* Child routes - these render inside App's */}
} />
} />
+ } />
{/* Fallback (renders inside layout too) */}
} />
diff --git a/frontend/src/hooks/useNetwork.ts b/frontend/src/hooks/useBackendAPI.ts
similarity index 86%
rename from frontend/src/hooks/useNetwork.ts
rename to frontend/src/hooks/useBackendAPI.ts
index cf8ab8d..2e305d6 100644
--- a/frontend/src/hooks/useNetwork.ts
+++ b/frontend/src/hooks/useBackendAPI.ts
@@ -3,13 +3,14 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback, useState } from "react";
import * as api from "../api/apiClient";
import type {
- BridgeCreateRequest,
- BridgeInfo,
- BridgeRemoveRequest,
- FullState,
- InterfaceInfo,
- RouteInfo,
+ BridgeCreateRequest,
+ BridgeInfo,
+ BridgeRemoveRequest,
+ FullState,
+ InterfaceInfo,
+ RouteInfo,
} from "../types/network";
+import { SnifferStatusResponse } from "../types/sniffer";
/**
* useNetwork
@@ -22,7 +23,7 @@ import type {
*
* This gives "no initial auto-fetch" but "once fetched, auto-updates".
*/
-export function useNetwork() {
+export function useBackendAPI() {
const qc = useQueryClient();
// per-query enabled flags (start false => no automatic fetch)
@@ -74,6 +75,13 @@ export function useNetwork() {
...commonOptions,
});
+ const snifferStatusQuery = useQuery({
+ queryKey: ["sniffer-status"],
+ queryFn: api.fetchSnifferStatus,
+ enabled: fullStateEnabled,
+ ...commonOptions,
+ });
+
// Imperative fetch helpers that also enable auto-refetch behavior
const fetchInterfaces = useCallback(async () => {
const res = await qc.fetchQuery({
@@ -125,6 +133,16 @@ export function useNetwork() {
return res;
}, [qc]);
+ const fetchSnifferStatus = useCallback(async () => {
+ const res = await qc.fetchQuery({
+ queryKey: ["sniffer-status"],
+ queryFn: api.fetchSnifferStatus,
+ staleTime: 1000 * 5
+ });
+ setFullStateEnabled(true);
+ return res;
+ }, [qc]);
+
// Local loading state for simple UI feedback
const [isCreating, setIsCreating] = useState(false);
const [isRemoving, setIsRemoving] = useState(false);
@@ -154,6 +172,7 @@ export function useNetwork() {
await qc.invalidateQueries({ queryKey: ["bridges"] });
await qc.invalidateQueries({ queryKey: ["full-state"] });
await qc.invalidateQueries({ queryKey: ["interfaces"] });
+ await qc.invalidateQueries({ queryKey: ["sniffer-status"] });
} catch (err) {
const message =
err instanceof Error ? err.message : typeof err === "string" ? err : "Remove bridge failed";
@@ -184,6 +203,10 @@ export function useNetwork() {
return qc.invalidateQueries({ queryKey: ["full-state"] });
}
+ function refreshSnifferStatus() {
+ return qc.invalidateQueries({ queryKey: ["sniffer-status"] });
+ }
+
// Convenience: refresh all queries
function refreshAll() {
refreshInterfaces();
@@ -191,6 +214,7 @@ export function useNetwork() {
refreshRoutes();
refreshBridges();
refreshFullState();
+ refreshSnifferStatus();
}
return {
@@ -200,6 +224,7 @@ export function useNetwork() {
routesQuery,
bridgesQuery,
fullStateQuery,
+ snifferStatusQuery,
// manual fetchers (fetch+enable auto-updates)
fetchInterfaces,
@@ -207,6 +232,7 @@ export function useNetwork() {
fetchRoutes,
fetchBridges,
fetchFullState,
+ fetchSnifferStatus,
// simple mutation functions (imperative)
createBridge,
@@ -222,6 +248,9 @@ export function useNetwork() {
refreshRoutes,
refreshBridges,
refreshFullState,
+ refreshSnifferStatus,
+
+ // refresh all
refreshAll,
};
}
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx
index b91f6fc..2232bb9 100644
--- a/frontend/src/pages/Home.tsx
+++ b/frontend/src/pages/Home.tsx
@@ -1,9 +1,9 @@
import { useEffect } from 'react';
import { fetchFullState } from '../api/apiClient';
-import { useNetwork } from '../hooks/useNetwork';
+import { useBackendAPI } from '../hooks/useBackendAPI';
export default function Home() {
- const { fullStateQuery } = useNetwork();
+ const { fullStateQuery } = useBackendAPI();
const { data, isLoading, isError, error } = fullStateQuery;
if (isLoading) return Loading full state…
;
diff --git a/frontend/src/pages/Network.tsx b/frontend/src/pages/Network.tsx
index 80c9b0c..e01eeb0 100644
--- a/frontend/src/pages/Network.tsx
+++ b/frontend/src/pages/Network.tsx
@@ -3,7 +3,7 @@ import { DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'
import { Button, Col, Form, Input, message, Modal, Popconfirm, Row, Select, Space, Table, Tag, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { useEffect, useMemo, useState } from 'react';
-import { useNetwork } from '../hooks/useNetwork';
+import { useBackendAPI } from '../hooks/useBackendAPI';
import type { BridgeInfo, InterfaceInfo } from '../types/network';
const { Title, Paragraph } = Typography;
@@ -20,7 +20,7 @@ export default function Network() {
refreshInterfaces,
isCreating,
isRemoving,
- } = useNetwork();
+ } = useBackendAPI();
const [bridgeModalVisible, setBridgeModalVisible] = useState(false);
const [bridgeForm] = Form.useForm();
diff --git a/frontend/src/pages/Sniffing.tsx b/frontend/src/pages/Sniffing.tsx
new file mode 100644
index 0000000..03e639c
--- /dev/null
+++ b/frontend/src/pages/Sniffing.tsx
@@ -0,0 +1,32 @@
+import { useEffect } from 'react';
+import { useBackendAPI } from '../hooks/useBackendAPI';
+import { fetchSnifferStatus } from '../api/apiClient';
+
+export default function Sniffing() {
+ const {
+ interfacesQuery,
+ bridgesQuery,
+ createBridge,
+ removeBridge,
+ fetchBridges,
+ fetchInterfaces,
+ refreshBridges,
+ refreshInterfaces,
+ isCreating,
+ isRemoving,
+ } = useBackendAPI();
+
+ // fetch on mount (explicit, since queries are disabled by default)
+ useEffect(() => {
+ fetchInterfaces().catch(() => {});
+ fetchSnifferStatus().catch(() => {});
+ }, []);
+
+ const bridges = bridgesQuery.data ?? [];
+ const snifferStatus =
+ return (
+
+
Sniffing
+
+ );
+}
diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts
index 034772d..1215334 100644
--- a/frontend/src/routes.ts
+++ b/frontend/src/routes.ts
@@ -2,4 +2,5 @@ export const PATHS = {
ROOT: '/',
HOME: '/home',
NETWORK: '/network',
+ SNIFFING: '/sniffing',
};
\ No newline at end of file
diff --git a/frontend/src/types/sniffer.ts b/frontend/src/types/sniffer.ts
new file mode 100644
index 0000000..9675df3
--- /dev/null
+++ b/frontend/src/types/sniffer.ts
@@ -0,0 +1,21 @@
+/**
+ * Status of an individual interface monitored by the AF_PACKET sniffer.
+ */
+export interface InterfaceSnifferStatus {
+ /** Whether the sniffer thread is active */
+ running: boolean;
+
+ /** Whether the interface exists in /sys/class/net */
+ exists: boolean;
+
+ /** Whether the interface is operationally UP */
+ up: boolean;
+}
+
+/**
+ * Response model for the sniffer status endpoint.
+ * Maps interface names to their sniffer status.
+ */
+export interface SnifferStatusResponse {
+ interfaces: Record;
+}