websockets and network interface datat added
All checks were successful
Build and Deploy MITM Webserver / traffic_target (push) Successful in 0s
Build and Deploy MITM Webserver / build (push) Successful in 9s

This commit is contained in:
2026-03-09 16:45:01 +01:00
parent e919853d07
commit 40d8b15417
8 changed files with 352 additions and 107 deletions

View File

@@ -82,6 +82,13 @@ export const fetchFullState = async (): Promise<FullState> => {
return res.data;
};
export const getNetworkStateWebSocketUrl = (): string => {
const url = new URL(BASE);
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
url.pathname = `${url.pathname.replace(/\/$/, '')}/network/ws/state`;
return url.toString();
};
export const createBridge = async (req: BridgeCreateRequest) => {
const res = await api.post('/network/bridge/create', req);
return res.data;
@@ -92,11 +99,6 @@ export const removeBridge = async (req: BridgeRemoveRequest) => {
return res.data;
};
export const fetchBridgeLinkStateWatchers = async (): Promise<BridgeLinkStateWatcherStatus[]> => {
const res = await api.get<BridgeLinkStateWatcherStatus[]>('/network/bridge/link-state-watchers');
return res.data;
};
export const enableBridgeLinkStateWatcher = async (
bridgeName: string,
req: BridgeLinkStateEnableRequest,

View File

@@ -24,11 +24,12 @@ import {
createBridge,
disableBridgeLinkStateWatcher,
enableBridgeLinkStateWatcher,
fetchBridgeLinkStateWatchers,
fetchFullState,
getNetworkStateWebSocketUrl,
removeBridge,
} from '../api/apiClient';
import type {
EthernetProfile,
BridgeInfo,
BridgeLinkStateWatcherStatus,
FullState,
@@ -48,15 +49,10 @@ export default function Network() {
const [watcherBusyBridge, setWatcherBusyBridge] = useState<string>();
const getNetworkPageState = (silent = false) => {
Promise.all([fetchFullState(), fetchBridgeLinkStateWatchers()])
.then(([state, watchers]) => {
fetchFullState()
.then((state) => {
setNetworkState(state);
setWatcherStatuses(
watchers.reduce<Record<string, BridgeLinkStateWatcherStatus>>((acc, watcher) => {
acc[watcher.bridge] = watcher;
return acc;
}, {}),
);
setWatcherStatuses(indexWatchers(state.watchers ?? []));
if (!silent) {
notification.success({
message: 'Success',
@@ -73,26 +69,70 @@ export default function Network() {
});
};
function indexWatchers(watchers: BridgeLinkStateWatcherStatus[]) {
return watchers.reduce<Record<string, BridgeLinkStateWatcherStatus>>((acc, watcher) => {
acc[watcher.bridge] = watcher;
return acc;
}, {});
}
function applySnapshot(state: FullState) {
setNetworkState(state);
setWatcherStatuses(indexWatchers(state.watchers ?? []));
}
useEffect(() => {
getNetworkPageState(true);
}, []);
const hasActiveWatchers = useMemo(
() => Object.values(watcherStatuses).some((watcher) => watcher.active),
[watcherStatuses],
);
useEffect(() => {
if (!hasActiveWatchers) {
return undefined;
let reconnectTimer: number | undefined;
let socket: WebSocket | undefined;
let cancelled = false;
const connect = () => {
socket = new WebSocket(getNetworkStateWebSocketUrl());
socket.onmessage = (event) => {
try {
const payload = JSON.parse(event.data);
if (payload?.type === 'network_state' && payload.snapshot) {
applySnapshot(payload.snapshot as FullState);
}
} catch (error) {
console.error('Failed to parse network websocket payload:', error);
}
};
socket.onclose = () => {
if (cancelled) {
return;
}
reconnectTimer = window.setTimeout(connect, 2000);
};
};
connect();
return () => {
cancelled = true;
if (reconnectTimer) {
window.clearTimeout(reconnectTimer);
}
socket?.close();
};
}, []);
function renderEthernetProfile(profile?: EthernetProfile | null) {
if (!profile) {
return '—';
}
const intervalId = window.setInterval(() => {
getNetworkPageState(true);
}, 2000);
return () => window.clearInterval(intervalId);
}, [hasActiveWatchers]);
const speed = profile.speed_mbps ? `${profile.speed_mbps} Mb/s` : 'speed ?';
const duplex = profile.duplex ?? 'duplex ?';
const autoneg = typeof profile.autoneg === 'boolean' ? (profile.autoneg ? 'autoneg on' : 'autoneg off') : 'autoneg ?';
return `${speed}, ${duplex}, ${autoneg}`;
}
const interfaceOptions = useMemo(
() =>
@@ -129,6 +169,13 @@ export default function Network() {
width: 100,
render: (value: boolean | null | undefined) => <Tag color={value ? 'green' : 'default'}>{value ? 'up' : 'down'}</Tag>,
},
{ title: 'MTU', dataIndex: 'mtu', key: 'mtu', width: 90, render: (value: number | null | undefined) => value ?? '—' },
{
title: 'Link',
dataIndex: 'ethernet_profile',
key: 'ethernet_profile',
render: (value: EthernetProfile | null | undefined) => renderEthernetProfile(value),
},
{
title: 'Operstate',
dataIndex: 'operstate',
@@ -157,6 +204,12 @@ export default function Network() {
{ title: 'State', dataIndex: 'state', key: 'state', render: (state) => <Tag>{state}</Tag> },
{ title: 'MAC', dataIndex: 'mac', key: 'mac', render: (mac) => mac ?? '—' },
{ title: 'MTU', dataIndex: 'mtu', key: 'mtu', width: 90 },
{
title: 'Link',
dataIndex: 'ethernet_profile',
key: 'ethernet_profile',
render: (value: EthernetProfile | null | undefined) => renderEthernetProfile(value),
},
{
title: 'Addresses',
dataIndex: 'addresses',
@@ -435,6 +488,9 @@ export default function Network() {
<Descriptions size="small" column={4} style={{ marginBottom: members.length ? 12 : 0 }}>
<Descriptions.Item label="Members">{bridge.members.map((member) => member.ifname).join(', ') || '—'}</Descriptions.Item>
<Descriptions.Item label="Mode">{watcher?.event_driven ? 'netlink events' : '—'}</Descriptions.Item>
<Descriptions.Item label="Failure holdoff">
{watcher?.failure_holdoff_seconds ? `${watcher.failure_holdoff_seconds}s` : '—'}
</Descriptions.Item>
<Descriptions.Item label="Recovery holdoff">
{watcher?.recovery_holdoff_seconds ? `${watcher.recovery_holdoff_seconds}s` : '—'}
</Descriptions.Item>

View File

@@ -4,6 +4,12 @@ export interface InterfaceAddress {
prefixlen: number;
}
export interface EthernetProfile {
speed_mbps?: number | null;
duplex?: string | null;
autoneg?: boolean | null;
}
export interface InterfaceInfo {
ifindex: number;
name: string;
@@ -11,6 +17,7 @@ export interface InterfaceInfo {
mac?: string | null;
mtu: number;
flags: string[];
ethernet_profile?: EthernetProfile | null;
addresses: InterfaceAddress[];
}
@@ -31,6 +38,7 @@ export interface BridgeInterfaceInfo {
ifname: string;
state?: string | null;
mtu?: number | null;
ethernet_profile?: EthernetProfile | null;
}
export interface BridgeInfo {
@@ -61,11 +69,7 @@ export interface BridgeMemberLinkStateInfo {
carrier_up?: boolean | null;
operstate?: string | null;
mtu?: number | null;
ethernet_profile?: {
speed_mbps?: number | null;
duplex?: string | null;
autoneg?: boolean | null;
} | null;
ethernet_profile?: EthernetProfile | null;
link_ready: boolean;
suppressed: boolean;
}
@@ -78,6 +82,7 @@ export interface BridgeLinkStateWatcherStatus {
last_error?: string | null;
last_action?: string | null;
suppressed_members: string[];
failure_holdoff_seconds?: number | null;
recovery_holdoff_seconds?: number | null;
degraded_recheck_seconds?: number | null;
members: Record<string, BridgeMemberLinkStateInfo>;
@@ -89,4 +94,5 @@ export interface FullState {
interfaces: InterfaceInfo[];
routes: RouteInfo[];
bridges: BridgeInfo[];
watchers: BridgeLinkStateWatcherStatus[];
}