reset button interfaces
All checks were successful
Build and Deploy MITM Webserver / traffic_target (push) Successful in 1s
Build and Deploy MITM Webserver / build (push) Successful in 10s

This commit is contained in:
2026-03-09 17:23:00 +01:00
parent 650b92d2c5
commit b836bf3f02
4 changed files with 141 additions and 1 deletions

View File

@@ -2,6 +2,8 @@
import asyncio
import logging
import os
import subprocess
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect
@@ -16,6 +18,8 @@ from src.utilities.interface_bridge_helpers import get_bridge_ports_once, read_i
router = APIRouter()
logger = logging.getLogger("network_router")
_ETHTOOL_BIN = "/usr/sbin/ethtool" if os.path.exists("/usr/sbin/ethtool") else "ethtool"
DEFAULT_INTERFACE_MTU = 1500
ip: IPRoute | None = None
ndb: NDB | None = None
@@ -106,6 +110,30 @@ class BridgeLinkStateEnableRequest(BaseModel):
)
class InterfaceResetDefaultsRequest(BaseModel):
"""Payload for resetting one or more interfaces to baseline settings."""
interfaces: List[str] = Field(..., min_length=1, description="Interface names to reset.")
class InterfaceResetDefaultsResult(BaseModel):
"""Outcome for one interface reset attempt."""
interface: str = Field(..., description="Interface name.")
mtu: Optional[int] = Field(None, description="Resulting MTU after reset.")
ethernet_profile: Optional[Dict[str, Any]] = Field(
None,
description="Resulting speed/duplex/autoneg profile when available.",
)
message: str = Field(..., description="Human-readable reset result.")
class InterfaceResetDefaultsResponse(BaseModel):
"""Batch reset response for one or more interfaces."""
results: List[InterfaceResetDefaultsResult] = Field(default_factory=list)
class BridgeMemberLinkStateInfo(BaseModel):
"""Current link-state snapshot for one bridge member."""
@@ -282,6 +310,38 @@ def publish_network_state_update(reason: str) -> None:
logger.exception("Failed to publish network state update")
def _reset_interface_defaults(ifname: str, ip_route: IPRoute) -> InterfaceResetDefaultsResult:
"""Reset one interface to a conservative baseline configuration."""
idx = iface_index(ifname, ip_route)
messages: list[str] = []
ip_route.link("set", index=idx, state="down")
ip_route.link("set", index=idx, mtu=DEFAULT_INTERFACE_MTU)
try:
subprocess.run(
[_ETHTOOL_BIN, "-s", ifname, "autoneg", "on"],
capture_output=True,
text=True,
check=True,
)
messages.append("autoneg on")
except (FileNotFoundError, subprocess.CalledProcessError) as exc:
logger.debug("Failed to reset ethtool defaults on %s: %s", ifname, exc)
messages.append("autoneg unchanged")
ip_route.link("set", index=idx, state="up")
messages.append(f"mtu {DEFAULT_INTERFACE_MTU}")
messages.append("admin up")
return InterfaceResetDefaultsResult(
interface=ifname,
mtu=DEFAULT_INTERFACE_MTU,
ethernet_profile=read_interface_ethernet_profile(ifname),
message=", ".join(messages),
)
@router.get("/interfaces", response_model=List[InterfaceInfo])
def get_interfaces(ip: IPRoute = Depends(get_iproute)) -> List[InterfaceInfo]:
"""List host interfaces with addresses and decoded flags."""
@@ -422,6 +482,18 @@ def full_state(ip: IPRoute = Depends(get_iproute)) -> FullStateResponse:
return _build_full_state_response(ip)
@router.post("/interfaces/reset-defaults", response_model=InterfaceResetDefaultsResponse)
def reset_interfaces_to_defaults(
req: InterfaceResetDefaultsRequest,
ip: IPRoute = Depends(get_iproute),
) -> InterfaceResetDefaultsResponse:
"""Reset listed interfaces to baseline MTU/autoneg/up settings."""
unique_ifaces = list(dict.fromkeys(req.interfaces))
results = [_reset_interface_defaults(ifname, ip) for ifname in unique_ifaces]
publish_network_state_update("interfaces_reset_defaults")
return InterfaceResetDefaultsResponse(results=results)
@router.post("/bridge/create")
def create_bridge(req: BridgeCreateRequest, ip: IPRoute = Depends(get_iproute)) -> dict:
"""Create a bridge and attach listed interfaces."""

View File

@@ -8,6 +8,8 @@ import {
BridgeInfo,
BridgeRemoveRequest,
FullState,
InterfaceResetDefaultsRequest,
InterfaceResetDefaultsResponse,
InterfaceInfo,
RouteInfo,
} from '../types/network';
@@ -82,6 +84,13 @@ export const fetchFullState = async (): Promise<FullState> => {
return res.data;
};
export const resetInterfaceDefaults = async (
req: InterfaceResetDefaultsRequest,
): Promise<InterfaceResetDefaultsResponse> => {
const res = await api.post<InterfaceResetDefaultsResponse>('/network/interfaces/reset-defaults', req);
return res.data;
};
export const getNetworkStateWebSocketUrl = (): string => {
const url = new URL(BASE);
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';

View File

@@ -27,6 +27,7 @@ import {
fetchFullState,
getNetworkStateWebSocketUrl,
removeBridge,
resetInterfaceDefaults,
} from '../api/apiClient';
import type {
EthernetProfile,
@@ -34,6 +35,7 @@ import type {
BridgeLinkStateWatcherStatus,
FullState,
InterfaceInfo,
InterfaceResetDefaultsResponse,
} from '../types/network';
const { Title, Paragraph } = Typography;
@@ -47,6 +49,7 @@ export default function Network() {
const [watcherStatuses, setWatcherStatuses] = useState<Record<string, BridgeLinkStateWatcherStatus>>({});
const [watcherBridgeName, setWatcherBridgeName] = useState<string>();
const [watcherBusyBridge, setWatcherBusyBridge] = useState<string>();
const [resetBusyInterface, setResetBusyInterface] = useState<string>();
const getNetworkPageState = (silent = false) => {
fetchFullState()
@@ -227,8 +230,24 @@ export default function Network() {
<span>—</span>
),
},
{
title: 'Actions',
key: 'actions',
width: 160,
render: (_: unknown, record: InterfaceInfo) => (
<Popconfirm
title={`Reset ${record.name} to defaults?`}
description="Sets MTU to 1500, enables autoneg, and brings the interface up."
onConfirm={() => handleResetInterface(record.name)}
okText="Reset"
cancelText="Cancel"
>
<Button loading={resetBusyInterface === record.name}>Reset defaults</Button>
</Popconfirm>
),
},
],
[],
[resetBusyInterface],
);
const bridgeColumns: ColumnsType<BridgeInfo> = useMemo(
@@ -392,6 +411,31 @@ export default function Network() {
});
}
function handleResetInterface(ifname: string) {
setResetBusyInterface(ifname);
resetInterfaceDefaults({ interfaces: [ifname] })
.then((response: InterfaceResetDefaultsResponse) => {
const result = response.results[0];
notification.success({
message: 'Success',
description: result?.message
? `${ifname}: ${result.message}`
: `${ifname} reset to defaults`,
});
getNetworkPageState(true);
})
.catch((error) => {
console.error(error);
notification.error({
message: 'Error',
description: (error as Error).message ?? `Failed to reset ${ifname}`,
});
})
.finally(() => {
setResetBusyInterface(undefined);
});
}
return (
<div style={{ padding: 16 }}>
<Row justify="space-between" align="middle" style={{ marginBottom: 12 }}>

View File

@@ -63,6 +63,21 @@ export interface BridgeLinkStateEnableRequest {
recovery_holdoff_seconds: number;
}
export interface InterfaceResetDefaultsRequest {
interfaces: string[];
}
export interface InterfaceResetDefaultsResult {
interface: string;
mtu?: number | null;
ethernet_profile?: EthernetProfile | null;
message: string;
}
export interface InterfaceResetDefaultsResponse {
results: InterfaceResetDefaultsResult[];
}
export interface BridgeMemberLinkStateInfo {
ifname: string;
admin_up?: boolean | null;