Fixes interface auto-detection failures on Pi Zero 2W where wlan0 doesn't exist
at boot. Implementation:
- Added detect_interface_with_retry() function with 3x retry fallback chain
- Config-supplied interface preferred, then /proc/net/route, then UP interface
- Exponential backoff: 5s, 10s, 20s delays between retries
- Filters excluded prefixes: lo, tailscale*, wg*, tun*, docker*, veth*, br-*
- Used in Engine.__init__(), PacketCapture.start(), MacManager.start()
- Full test coverage: 10 tests for detection, backoff, filtering
Addresses issue where network stack initializes after boot and interfaces
become available after 5-10 second delay.
Parse nmap vendor name from MAC Address line (filtering out 'Unknown').
Fall back to socket.gethostbyaddr for IPs nmap didn't resolve a hostname
for. Alert label is now: 'hostname (vendor)' | 'hostname' | 'vendor' |
'unknown', in that priority order.
urllib's default Python-urllib user agent is flagged by Cloudflare Bot
Fight Mode (error 1010) when m.example.org homeserver sits behind
Cloudflare. Add a browser-like User-Agent to Matrix PUT/POST requests so
Cloudflare passes the traffic through.
- Remove 20-wifi.yaml netplan block (Bug 1): causes netplan to spawn its own wpa_supplicant instance, which claims ctrl_iface and blocks wpa_supplicant@wlan0.service. The per-interface systemd service is the correct approach.
- Add cleanup line to remove any pre-existing 20-wifi.yaml from the Armbian image.
- Patch 10-dhcp-all-interfaces.yaml to suppress default route on ethernet (Bug 2): without this, ethernet DHCP default route metric 100 beats WiFi metric 1024, routing internet traffic to ethernet which has no upstream. Causes Tailscale and firstboot to fail.
- Force IPv4 resolution in Tailscale curl (Bug 3): some homelab/ISP networks lack IPv6 internet routes. IPv6 DNS resolution for tailscale.com causes immediate curl failure on first boot.
- Remove wpa_supplicant@wlan0.service enablement: Netplan spawns its own
wpa_supplicant instance, enabling the @wlan0 unit creates a second one
that fights for the socket, causing WiFi instability and SSH drops
- Write /etc/nftables.conf to card at flash time: Armbian bookworm default
nftables policy drops all input; SSH was unreachable despite sshd running.
Ruleset allows established, loopback, ICMP, and TCP/22 only
Fixes DevTrack #317 and #295
- Set HOME/GOPATH/GOMODCACHE before go install kerbrute on arm64 to fix
'module cache not found' abort that prevented --enable-service from running
- Enable bigbrother-capture, bigbrother-watchdog, and bettercap.service
in --enable-service block (previously only bigbrother-core was enabled)
- Add services/bettercap.service: runs passive_recon.cap with net.recon +
net.sniff + events.stream instead of idle api.rest only
- bettercap.service depends on bigbrother-core to ensure engine is up first
Fixes DevTrack #311 and #312
- Remove generic wpa_supplicant.service symlink when wpa_supplicant@wlan0 is enabled — both services fight over wlan0 causing WiFi instability and SSH drops
- Add udev rule to disable WiFi power saving (aw859a drops connections under load with power save on)
- Drop set -e from firstboot script so a failing step does not kill the whole run
- Guard setup.sh call — skip gracefully if not present (dev/test scenario)
Fixes#295 and #296
Armbian minimal uses systemd-networkd+wpa_supplicant via Netplan, not NM.
Write Netplan YAML, wpa_supplicant-wlan0.conf, and networkd .network file.
Enable wpa_supplicant@wlan0.service via symlink if template exists.
WiFi, SSH keys, root password, firstboot service were all being written to
the FAT32 boot partition (p1) instead of the ext4 rootfs (p2). Detect the
ext4 partition by filesystem type and mount that as MOUNT_POINT.
mktemp creates the file before ssh-keygen runs — keygen sees it exists
and prompts. Switch to mktemp -d so keygen writes a fresh file.
When run via sudo SUDO_USER is set but HOME=/root. Resolve the real
home via getent so the key lands in the operator's ~/.ssh, not root's.
echo calls in summary/final sections were missing -e flag so ANSI
escape codes printed literally instead of rendering.
Infisical SSH key storage was targeting a non-existent 'bigbrother'
folder, silently failing to store the private key. Removed folder
argument so keys store at root where creds CLI can retrieve them.
Matrix doesn't use a simple webhook URL — needs homeserver, room ID,
and bot token separately. Uses PUT /_matrix/client/v3/rooms/.../send
with Bearer token auth. Room ID is bash-encoded (! → %21, : → %3A)
to avoid Python dependency pre-setup. Also adds ntfy and plain
webhook as options under a typed alerter menu.
Device ID is now a MAC-format hex string (12 chars) using the same
OUI as the randomly selected device profile. If the device gets an
Amazon OUI for its MAC, the device ID starts with fc65de. Hostname,
MAC, and device ID all point to the same vendor — no inconsistency
for anyone watching the network.
bb- prefix is a tool fingerprint — visible in Infisical key names,
alerter payloads, and config files on the device. Default is now
a plain 10-char hex string with no identifying prefix.
Tailscale handles NAT traversal automatically via DERP relay — more
reliable than WireGuard for devices landing on unknown networks.
Add reminder to use reusable non-expiring keys for long-term drops.
Blind drops need some way to locate the device when no VPN is selected.
Alerter now reports public IP (via ipify) with fallback to local IP,
plus hostname — gives enough info to SSH directly on same-network drops.
Generates a dedicated tunnel keypair during setup, prints public key
for operator to pre-stage on server, writes private key to SD card.
On first boot: installs autossh, writes bb-tunnel.service with
correct RemoteForward and reconnect settings. Zero network traffic
until operator initiates connection through the tunnel port.
- Add prompt_default() helper: shows defaults, accepts Enter to use them
- WiFi: default=n (skip), keeps passphrase prompt loop
- Root password: auto-generate with openssl, show in summary if used, operator can override
- SSH key: default=1 (generate new)
- Device ID: auto-generate bb-<8-char-uuid>
- Boot mode: default=1 (passive)
- VPN: default=1 (none)
- Hostname: NEW section with auto-generated node-<6-char-uuid> default
- Alerter: default=n (disabled)
- Download: use curl -L (no -f), verify file exists and >50MB
- Config summary: show hostname, mask password (show auto-generated clearly)
- Apply config: write BB_HOSTNAME to /etc/hostname and /etc/hosts
- No eval with user input: use printf -v for variable assignment
ls glob on empty cache dir exits non-zero and kills script under
set -euo pipefail. Switched to find which exits 0 on no matches.
Same fix for grep|sed pipeline in device selection; added empty
DEVICE guard.
Single script covers full drop implant prep: image download and flash,
WiFi (with UUID fix), per-device SSH keygen to Infisical, mode selection,
VPN (Tailscale/WireGuard), network alerter webhook, and first-boot service.
Insert card, power on, walk away.
WiFi keyfiles require UUID or NM silently ignores them — that was why
WiFi never connected. Rewrote preconfig_sd.sh to:
- Generate UUID for NM keyfile
- Copy BB source to /root/bb-src/ on the card
- Install bb-firstboot.service that runs setup.sh --enable-service
after network-online.target, then disables itself
- Per-device keygen, private key to Infisical
Single command now produces a card that boots and self-installs.
Each device gets a unique ed25519 keypair generated at preconfig time.
Private key goes to Infisical (bigbrother/SSH_PRIVKEY_<ID>), public key
to the SD card only. No operator keys or identifying comments on device.
Copying authorized_keys verbatim onto a drop implant leaks internal hostnames,
usernames, and tool names in key comments. preconfig_sd.sh now strips all
comments (awk '{print $1,$2}') when writing keys to SD card. deploy.sh adds
the same strip step on the live device as a safety net.
build_data.py created 'profiles' but mac_manager queries 'mac_profiles'.
Columns category/model renamed to device_type/device_name. device_type
values aligned with _CATEGORY_MAP in mac_manager (streaming, smart_tv,
phone, tablet, smart_speaker, iot, printer, gaming).
ja3_fingerprints.db table renamed from 'fingerprints' to 'ja3_profiles'
and expanded with all columns ja3_spoofer queries (name, description,
cipher_suites, extensions, elliptic_curves, ec_point_formats as JSON).
StateManager.reset_module_status() clears stale 'running' entries on
startup so watchdog doesn't flag old-session PIDs as dead modules.
Engine.start_all() calls reset before launch and does a 3s post-startup
liveness check, logging any modules that crashed immediately after fork.
When wlan0 is the primary data interface (WiFi-only deployment), changing
its MAC via SIOCSIFHWADDR drops the AP association and puts all AF_PACKET
sockets into ENETDOWN permanently until wpa_supplicant re-associates.
- _apply_profile: skip set_mac if _eth_iface is a WiFi interface
- _apply_profile: skip WiFi set_mac if _wifi_iface == _eth_iface (same card)
- stop: mirror the same skip logic for MAC restoration
DHCP hostname + TCP stack tuning still applied for blending on WiFi-only nodes.
with conn: context manager in Python sqlite3 doesn't respect busy_timeout
for the implicit BEGIN; under concurrent subprocess writes the transaction
would fail immediately. Replace with explicit BEGIN IMMEDIATE + retry loop
(6 attempts, 0.5s back-off) to handle WAL write contention.
credential_sniffer and credential_db both wrote to the same credentials.db
with different schemas; credential_db's executescript failed when creating
the crack_status index on an existing table that lacked that column.
Fix: split credential_db's schema init into table DDL + migration + indexes
so missing columns are added before index creation runs. Migration also
handles any future schema drift.
state.py: add 30s timeout + busy_timeout PRAGMA to write connection so
concurrent subprocess writes stop failing with 'database is locked'.
capture_bus.py: reconnect on ENETDOWN instead of aborting the capture
loop — mac_manager temporarily brings the interface down during MAC
rotation which was silently killing all packet capture.
With multiprocessing fork, threads are not inherited. The parent's
CaptureBus reader thread runs in the parent but module subprocesses
subscribe AFTER fork, so their SubscriberQueue entries are added to
a child-local copy of _subscribers that the parent reader never sees.
Fix: reset and restart CaptureBus in each module subprocess so it
has its own reader thread on the inherited AF_PACKET socket. Each
module gets independent packet delivery. Linux AF_PACKET allows
multiple readers on the same interface.
Three bugs causing all modules to exit within seconds of startup:
1. _module_runner had no keep-alive loop after calling module.start().
All modules use a thread-spawning pattern where start() returns
immediately, so the subprocess exited and killed all daemon threads.
Added while module._running: sleep(1) loop to block until stop().
2. Engine injected capture_bus under key 'capture_bus' but 9 modules
(auth_flow_tracker, cloud_token_harvester, ldap_harvester, network_mapper,
rdp_monitor, quic_analyzer, smb_monitor, db_interceptor, vlan_discovery)
look for '_capture_bus'. Inject both keys so all modules get it.
3. mac_manager crashes on startup when innocuous_macs.db exists but has
no mac_profiles table (DB not yet seeded). Added OperationalError
handler to fall back to random OUI instead of crashing.
- Add secure_wipe_file() to utils/crypto.py for file overwrites + deletion
- Add secure_wipe parameter to StateManager.stop() method
- Wipes all SQLite WAL files (.db, .db-wal, .db-shm) with 3 passes
- Normal exit now calls state.stop(secure_wipe=True) to clear state database
- Falls back to regular delete if secure wipe fails
- Prevents state.db from persisting on disk after shutdown
- Add encrypt_credential_dict() and decrypt_credential_field() to utils/crypto.py
- Create utils/credential_encryption.py with encryption/decryption helpers
- Add get_credential_encryption_key() to retrieve key from Infisical at runtime
- Add emit_credential_found() wrapper for modules to use instead of direct bus.emit()
- Update all 15 modules emitting CREDENTIAL_FOUND to use encrypted wrapper
- Update 4 modules consuming CREDENTIAL_FOUND to decrypt payload before processing
- Sensitive fields (username, password, hash, token, etc.) encrypted with AES-256-GCM
- Falls back to plaintext if encryption key unavailable or encryption fails
- Move crash_callback execution outside of self._lock
- Callback is called after releasing the monitor lock
- Re-acquire lock only for restart decision logic
- Prevents deadlock if callback tries to acquire same lock
- Maintains thread safety for restart and process state updates