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

@@ -19,6 +19,7 @@ from src.utilities.interface_bridge_helpers import (
get_bridge_ports_once,
read_interface_admin_up,
read_interface_carrier,
read_interface_ethernet_profile,
read_interface_mtu,
read_interface_operstate,
)
@@ -255,9 +256,7 @@ class BridgeLinkStateWatcher:
matured_failing_members = self._track_failing_members(failing_members, now)
if not matured_failing_members:
self._set_last_action(
f"waiting {settings.bridge_link_state_failure_holdoff_seconds:.2f}s before suppressing due to failures={failing_members}"
)
self._set_last_action(f"waiting before suppressing transient failures on {failing_members}")
return
with self._lock:
@@ -270,14 +269,17 @@ class BridgeLinkStateWatcher:
with self._lock:
self._failing_since = {}
should_restore = False
waiting_for_restore = False
with self._lock:
self._degraded = True
if self._all_clear_since is None:
self._all_clear_since = now
should_restore = now - self._all_clear_since >= self.recovery_holdoff_seconds
if not should_restore:
remaining = max(0.0, self.recovery_holdoff_seconds - (now - self._all_clear_since))
self._last_action = f"waiting {remaining:.2f}s before restoring suppressed members"
waiting_for_restore = True
if waiting_for_restore:
self._set_last_action("waiting before restoring suppressed members")
if should_restore:
self._restore_suppressed_members(reason=reason)
@@ -413,69 +415,36 @@ class BridgeLinkStateWatcher:
def _read_ethernet_profile(self, ifname: str) -> Optional[EthernetProfile]:
"""Read ethtool speed/duplex/autoneg for one interface if supported."""
try:
result = subprocess.run(
[_ETHTOOL_BIN, ifname],
capture_output=True,
text=True,
check=True,
)
except (FileNotFoundError, subprocess.CalledProcessError):
return None
values: dict[str, str] = {}
for line in result.stdout.splitlines():
if ":" not in line:
continue
key, value = line.split(":", 1)
values[key.strip()] = value.strip()
speed_value = values.get("Speed")
duplex_value = values.get("Duplex")
autoneg_value = values.get("Auto-negotiation")
speed_mbps: Optional[int] = None
if speed_value and speed_value.endswith("Mb/s"):
try:
speed_mbps = int(speed_value[:-4], 10)
except ValueError:
speed_mbps = None
duplex: Optional[str] = None
if duplex_value and duplex_value.lower() in {"full", "half"}:
duplex = duplex_value.lower()
autoneg: Optional[bool] = None
if autoneg_value:
lowered = autoneg_value.lower()
if lowered in {"on", "off"}:
autoneg = lowered == "on"
if speed_mbps is None and duplex is None and autoneg is None:
profile = read_interface_ethernet_profile(ifname)
if profile is None:
return None
return EthernetProfile(
speed_mbps=speed_mbps,
duplex=duplex,
autoneg=autoneg,
speed_mbps=profile.get("speed_mbps"),
duplex=profile.get("duplex"),
autoneg=profile.get("autoneg"),
)
def _sync_member_configuration(self, source_ifname: str, states: dict[str, MemberLinkState]) -> None:
"""Mirror MTU and ethtool link settings from one healthy member to its siblings."""
source = states[source_ifname]
changed_members: list[str] = []
changes: list[str] = []
for target_ifname, target in sorted(states.items()):
if target_ifname == source_ifname:
continue
mtu_changed = self._sync_member_mtu(source_ifname, source, target_ifname, target)
profile_changed = self._sync_member_ethernet_profile(source_ifname, source, target_ifname, target)
if mtu_changed or profile_changed:
changed_members.append(target_ifname)
mtu_change = self._sync_member_mtu(source_ifname, source, target_ifname, target)
profile_change = self._sync_member_ethernet_profile(source_ifname, source, target_ifname, target)
if mtu_change:
changes.append(mtu_change)
if profile_change:
changes.append(profile_change)
if changed_members:
self._set_last_action(f"synchronized configuration from {source_ifname} to {changed_members}")
if changes:
self._set_last_action(f"synchronized from {source_ifname}: {'; '.join(changes)}")
else:
self._set_last_action(f"no configuration mismatch detected after event on {source_ifname}")
def _sync_member_mtu(
self,
@@ -483,10 +452,10 @@ class BridgeLinkStateWatcher:
source: MemberLinkState,
target_ifname: str,
target: MemberLinkState,
) -> bool:
) -> Optional[str]:
"""Mirror MTU when the source member differs from the target."""
if source.mtu is None or target.mtu is None or source.mtu == target.mtu:
return False
return None
with IPRoute() as ipr:
indices = ipr.link_lookup(ifname=target_ifname)
@@ -502,7 +471,7 @@ class BridgeLinkStateWatcher:
self.bridge_name,
source.mtu,
)
return True
return f"{target_ifname} mtu={source.mtu}"
def _sync_member_ethernet_profile(
self,
@@ -510,12 +479,12 @@ class BridgeLinkStateWatcher:
source: MemberLinkState,
target_ifname: str,
target: MemberLinkState,
) -> bool:
) -> Optional[str]:
"""Mirror ethtool speed/duplex/autoneg from the source member to the target."""
source_profile = source.ethernet_profile
target_profile = target.ethernet_profile
if source_profile is None or target_profile is None or source_profile == target_profile:
return False
return None
cmd = [_ETHTOOL_BIN, "-s", target_ifname]
if source_profile.autoneg is True:
@@ -532,7 +501,7 @@ class BridgeLinkStateWatcher:
]
)
else:
return False
return None
try:
subprocess.run(cmd, capture_output=True, text=True, check=True)
@@ -544,7 +513,7 @@ class BridgeLinkStateWatcher:
self.bridge_name,
exc,
)
return False
return None
self._mark_managed_change(target_ifname)
logger.info(
@@ -554,7 +523,11 @@ class BridgeLinkStateWatcher:
self.bridge_name,
source_profile,
)
return True
return (
f"{target_ifname} link="
f"{source_profile.speed_mbps or 'unknown'}Mb/{source_profile.duplex or 'unknown'}/"
f"{'autoneg-on' if source_profile.autoneg else 'autoneg-off'}"
)
def _suppress_other_members(self, states: dict[str, MemberLinkState], failing_members: list[str]) -> None:
desired_suppressed = set(states) - set(failing_members)
@@ -579,7 +552,7 @@ class BridgeLinkStateWatcher:
if changed_members
else f"holding suppressed members because failing members={failing_members}"
)
self._last_action = action
self._set_last_action(action)
def _restore_suppressed_members(self, reason: str) -> None:
with self._lock:
@@ -600,7 +573,7 @@ class BridgeLinkStateWatcher:
with self._lock:
self._suppressed_members = {}
self._all_clear_since = None
self._last_action = f"restored {restored_members} ({reason})"
self._set_last_action(f"restored {restored_members} ({reason})")
def _set_interface_admin_state(self, ifname: str, target_up: bool) -> None:
if not check_interface_exists(ifname):
@@ -625,9 +598,23 @@ class BridgeLinkStateWatcher:
def _set_last_action(self, action: str) -> None:
"""Update the watcher action text."""
changed = False
with self._lock:
changed = action != self._last_action
self._last_action = action
if changed:
self._publish_network_state_update()
def _publish_network_state_update(self) -> None:
"""Publish a network snapshot after a meaningful watcher state change."""
try:
from src.api.network_api import publish_network_state_update
publish_network_state_update(f"bridge_watcher:{self.bridge_name}")
except Exception:
logger.exception("Failed to publish network state for bridge=%s", self.bridge_name)
class BridgeLinkStateManager:
"""Track bridge link-state watchers keyed by bridge name."""