Files
bigbrother/modules/connectivity/bridge.py
T
Cobra ffd384f64b Strip OPSEC tool identity fingerprints
Replace all sensor.* logger namespaces with __name__ (generic module
identifiers instead of discoverable 'sensor.*' prefixes).

Change hardcoded 'bb' API user to 'admin' in config and code defaults.

Change hardcoded relay_user 'bb' to 'operator' — prevents network
profiling from exposing tool identity via SSH config.

Fixes #457, #458, #459
2026-04-08 22:18:35 -04:00

360 lines
14 KiB
Python

#!/usr/bin/env python3
"""Bridge module — transparent inline network bridge.
Creates a transparent L2 bridge between the onboard ethernet adapter and a
USB ethernet adapter. The bridge has NO IP address — it is invisible on the
network and can only be detected by physical inspection.
Provides 802.1X bypass via silent bridge (EAP passthrough) and suppresses
STP/BPDU/CDP/LLDP frames via ebtables to avoid triggering network monitoring.
A C watchdog thread monitors bridge health and restores direct connectivity
within 15 seconds if the bridge fails.
"""
import logging
import os
import subprocess
import threading
import time
from pathlib import Path
from typing import Optional
from modules.base import BaseModule
logger = logging.getLogger(__name__)
BRIDGE_NAME = "br0"
WATCHDOG_INTERVAL = 5.0 # seconds between health checks
WATCHDOG_MAX_FAILURES = 3 # consecutive failures before teardown
FAILSAFE_TIMEOUT = 15 # seconds — must restore connectivity within this
class Bridge(BaseModule):
"""Transparent inline bridge — invisible on the network."""
name = "bridge"
module_type = "connectivity"
priority = -300
requires_root = True
def __init__(self, bus, state, config, engine=None):
super().__init__(bus, state, config, engine)
self._if1: Optional[str] = None
self._if2: Optional[str] = None
self._bridge_up = False
self._watchdog_thread: Optional[threading.Thread] = None
self._consecutive_failures = 0
self._scripts_dir: Optional[str] = None
# ------------------------------------------------------------------
# BaseModule interface
# ------------------------------------------------------------------
def start(self) -> None:
if self._running:
return
br_cfg = self.config.get("connectivity", {}).get("bridge", {})
if not br_cfg:
logger.error("No bridge config in bigbrother.yaml")
self.state.set_module_status(self.name, "error")
return
self._if1 = br_cfg.get("interface1")
self._if2 = br_cfg.get("interface2")
if not self._if1 or not self._if2:
logger.error("Bridge requires interface1 and interface2 in config")
self.state.set_module_status(self.name, "error")
return
# Locate scripts directory
install_path = self.config.get("device", {}).get("install_path", "/opt/.cache/bb")
self._scripts_dir = os.path.join(install_path, "scripts")
if not self.setup_bridge(self._if1, self._if2):
self.state.set_module_status(self.name, "error")
return
# Start watchdog thread
self._running = True
self._pid = os.getpid()
self._start_time = time.time()
self._bridge_up = True
self._watchdog_thread = threading.Thread(
target=self._watchdog_loop, daemon=True, name="sensor-bridge-watchdog",
)
self._watchdog_thread.start()
self.state.set_module_status(self.name, "running", pid=self._pid)
self.bus.emit("CONNECTIVITY_CHANGED",
{"module": self.name, "state": "up",
"bridge": BRIDGE_NAME, "if1": self._if1, "if2": self._if2},
source_module=self.name)
logger.info("Bridge %s active: %s <-> %s", BRIDGE_NAME, self._if1, self._if2)
def stop(self) -> None:
if not self._running:
return
self._running = False
if self._watchdog_thread and self._watchdog_thread.is_alive():
self._watchdog_thread.join(timeout=5.0)
self.teardown_bridge()
self._bridge_up = False
self.state.set_module_status(self.name, "stopped")
self.bus.emit("CONNECTIVITY_CHANGED",
{"module": self.name, "state": "down"},
source_module=self.name)
logger.info("Bridge torn down")
def status(self) -> dict:
return {
"running": self._running,
"pid": self._pid,
"uptime": time.time() - self._start_time if self._start_time else 0,
"bridge_up": self._bridge_up,
"bridge_name": BRIDGE_NAME,
"interface1": self._if1,
"interface2": self._if2,
"healthy": self.is_healthy(),
"consecutive_failures": self._consecutive_failures,
}
def configure(self, config: dict) -> None:
self.config.update(config)
def health_check(self) -> bool:
if not self._running:
return False
return self.is_healthy()
# ------------------------------------------------------------------
# Bridge operations
# ------------------------------------------------------------------
def setup_bridge(self, if1: str, if2: str) -> bool:
"""Create transparent bridge between two interfaces.
Tries the shell script first; falls back to inline commands if
the script is missing.
"""
script = os.path.join(self._scripts_dir, "setup_bridge.sh") if self._scripts_dir else None
if script and os.path.isfile(script):
try:
result = subprocess.run(
["bash", script, if1, if2],
capture_output=True, text=True, timeout=30,
)
if result.returncode == 0:
logger.info("Bridge setup via script succeeded")
return True
logger.warning("Bridge script failed: %s", result.stderr.strip())
except (subprocess.TimeoutExpired, OSError) as exc:
logger.warning("Bridge script error: %s", exc)
# Fallback: inline bridge setup
return self._setup_bridge_inline(if1, if2)
def teardown_bridge(self) -> bool:
"""Remove bridge and restore interfaces."""
script = os.path.join(self._scripts_dir, "teardown_bridge.sh") if self._scripts_dir else None
if script and os.path.isfile(script):
try:
result = subprocess.run(
["bash", script],
capture_output=True, text=True, timeout=30,
)
if result.returncode == 0:
logger.info("Bridge teardown via script succeeded")
return True
logger.warning("Teardown script failed: %s", result.stderr.strip())
except (subprocess.TimeoutExpired, OSError) as exc:
logger.warning("Teardown script error: %s", exc)
# Fallback: inline teardown
return self._teardown_bridge_inline()
def is_healthy(self) -> bool:
"""Check if the bridge interface exists and both ports are attached."""
try:
# Check bridge exists
result = subprocess.run(
["ip", "link", "show", BRIDGE_NAME],
capture_output=True, timeout=5,
)
if result.returncode != 0:
return False
# Check both interfaces are in the bridge
result = subprocess.run(
["bridge", "link", "show", "dev", self._if1],
capture_output=True, text=True, timeout=5,
)
if BRIDGE_NAME not in result.stdout:
return False
result = subprocess.run(
["bridge", "link", "show", "dev", self._if2],
capture_output=True, text=True, timeout=5,
)
if BRIDGE_NAME not in result.stdout:
return False
return True
except (subprocess.TimeoutExpired, OSError):
return False
# ------------------------------------------------------------------
# Inline bridge commands (fallback if scripts missing)
# ------------------------------------------------------------------
def _setup_bridge_inline(self, if1: str, if2: str) -> bool:
"""Create bridge using ip/bridge/ebtables commands directly."""
commands = [
# Create bridge, no IP
["ip", "link", "add", "name", BRIDGE_NAME, "type", "bridge"],
# Disable STP
["ip", "link", "set", BRIDGE_NAME, "type", "bridge", "stp_state", "0"],
# Disable forwarding delay
["ip", "link", "set", BRIDGE_NAME, "type", "bridge", "forward_delay", "0"],
# Set promiscuous mode on interfaces
["ip", "link", "set", if1, "promisc", "on"],
["ip", "link", "set", if2, "promisc", "on"],
# Flush any existing IP addresses
["ip", "addr", "flush", "dev", if1],
["ip", "addr", "flush", "dev", if2],
# Add interfaces to bridge
["ip", "link", "set", if1, "master", BRIDGE_NAME],
["ip", "link", "set", if2, "master", BRIDGE_NAME],
# Bring everything up
["ip", "link", "set", "up", "dev", if1],
["ip", "link", "set", "up", "dev", if2],
["ip", "link", "set", "up", "dev", BRIDGE_NAME],
]
for cmd in commands:
try:
subprocess.run(cmd, check=True, capture_output=True, timeout=10)
except subprocess.CalledProcessError as exc:
logger.error("Bridge setup command failed: %s%s",
" ".join(cmd), exc.stderr.decode(errors='replace'))
self._teardown_bridge_inline()
return False
except subprocess.TimeoutExpired:
logger.error("Bridge setup command timed out: %s", " ".join(cmd))
self._teardown_bridge_inline()
return False
# Suppress STP/BPDU/CDP/LLDP via ebtables
self._apply_ebtables_rules(if1, if2)
logger.info("Inline bridge setup complete: %s <-> %s via %s", if1, if2, BRIDGE_NAME)
return True
def _teardown_bridge_inline(self) -> bool:
"""Remove bridge and restore interfaces."""
# Remove ebtables rules (best-effort)
self._remove_ebtables_rules()
commands = [
["ip", "link", "set", "down", "dev", BRIDGE_NAME],
]
# Remove interfaces from bridge
if self._if1:
commands.append(["ip", "link", "set", self._if1, "nomaster"])
commands.append(["ip", "link", "set", self._if1, "promisc", "off"])
if self._if2:
commands.append(["ip", "link", "set", self._if2, "nomaster"])
commands.append(["ip", "link", "set", self._if2, "promisc", "off"])
commands.append(["ip", "link", "del", BRIDGE_NAME])
for cmd in commands:
try:
subprocess.run(cmd, capture_output=True, timeout=10)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
pass # Best-effort teardown
logger.info("Inline bridge teardown complete")
return True
def _apply_ebtables_rules(self, if1: str, if2: str) -> None:
"""Apply ebtables rules to suppress discovery protocols."""
# STP/BPDU destination: 01:80:C2:00:00:00
# CDP/VTP: 01:00:0C:CC:CC:CC
# LLDP: 01:80:C2:00:00:0E
suppress_macs = [
"01:80:C2:00:00:00", # STP/BPDU
"01:00:0C:CC:CC:CC", # CDP/VTP
"01:80:C2:00:00:0E", # LLDP
"01:00:0C:CC:CC:CD", # Cisco PVST+
]
for mac in suppress_macs:
for direction in ["FORWARD", "OUTPUT"]:
cmd = ["ebtables", "-A", direction, "-d", mac, "-j", "DROP"]
try:
subprocess.run(cmd, capture_output=True, timeout=5)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
logger.debug("ebtables rule failed (may not be installed): %s", " ".join(cmd))
def _remove_ebtables_rules(self) -> None:
"""Flush ebtables rules."""
try:
subprocess.run(["ebtables", "-F"], capture_output=True, timeout=5)
subprocess.run(["ebtables", "-X"], capture_output=True, timeout=5)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
pass
# ------------------------------------------------------------------
# Watchdog thread
# ------------------------------------------------------------------
def _watchdog_loop(self) -> None:
"""Monitor bridge health; restore direct connectivity on failure."""
while self._running:
time.sleep(WATCHDOG_INTERVAL)
if not self._running:
break
if self.is_healthy():
self._consecutive_failures = 0
continue
self._consecutive_failures += 1
logger.warning("Bridge health check failed (%d/%d)",
self._consecutive_failures, WATCHDOG_MAX_FAILURES)
if self._consecutive_failures >= WATCHDOG_MAX_FAILURES:
logger.error("Bridge failsafe triggered — restoring direct connectivity")
self.bus.emit("MODULE_ERROR",
{"module": self.name,
"error": "bridge_failsafe_triggered",
"consecutive_failures": self._consecutive_failures},
source_module=self.name)
# Teardown broken bridge and restore direct connection
self._teardown_bridge_inline()
self._bridge_up = False
# Try to re-establish bridge
if self._if1 and self._if2:
time.sleep(2.0)
if self._setup_bridge_inline(self._if1, self._if2):
self._bridge_up = True
self._consecutive_failures = 0
logger.info("Bridge recovered after failsafe")
else:
logger.error("Bridge recovery failed — operating without bridge")
self._running = False