From 4ff1ce52adf1da78b8399febdba6f57747e5231b Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:12:29 -0400 Subject: [PATCH] bearnet: make it LIVE on Linux + Windows too (was macOS-only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the paper-tiger fix to all three platforms via a shared stager: - scripts/stage-bearnet.sh: one cross-platform stager — pages loose at /browser/bearstart/, sidecar/bridge/policy/geoip at /*, so the autoconfig's resource://bearstart/ substitution + Subprocess launcher find everything. macOS packager refactored onto it (DRY). - netmon.rs: Windows connection enumerator (netstat -no + tasklist for PID→name) behind #[cfg(windows)]; unix keeps lsof. No lsof on Windows. - autoconfig: launcher tries both bearbrowser-capture-sidecar-bin and .exe. - nightly-linux: build the sidecar natively → stage → RE-TAR dist/bearbrowser (mach's tarball predates staging). - build-windows-cross: cross-compile the sidecar to x86_64-pc-windows-gnu (mingw — far simpler than MSVC cross-linking; deps are all clean cross-platform, no MSVC-only C deps) → stage into dist/firefox → re-zip the portable archive. Host sidecar still builds; windows #[cfg] path compiles for the target on Linux CI (macOS host can't cross-link mingw — verified deps are windows-clean instead). macOS remains screenshot-verified; Linux/Windows confirmed by the nightlies. --- .github/workflows/nightly-linux.yml | 13 ++++- capture-sidecar/src/netmon.rs | 57 +++++++++++++++++++-- scripts/bearbrowser-package-source-build.sh | 41 +++------------ scripts/build-windows-cross.sh | 26 +++++++++- scripts/stage-bearnet.sh | 54 +++++++++++++++++++ settings/start/bearstart-autoconfig.js | 13 +++-- 6 files changed, 159 insertions(+), 45 deletions(-) create mode 100755 scripts/stage-bearnet.sh diff --git a/.github/workflows/nightly-linux.yml b/.github/workflows/nightly-linux.yml index f1950a2..fe07cf4 100644 --- a/.github/workflows/nightly-linux.yml +++ b/.github/workflows/nightly-linux.yml @@ -185,10 +185,19 @@ jobs: # `mach package` creates, so it failed the moment the build finally # got far enough to package. Proven on the sovereign Linux builder. ( cd "$SRC" && MOZBUILD_STATE_PATH=${{ github.workspace }}/.mozbuild ./mach package ) - SRC_TAR="$(ls "$OBJ"/dist/bearbrowser-*.linux-x86_64.tar.* 2>/dev/null | head -1)" + # Make BearNet LIVE: build the sidecar natively, then stage it + the + # pages + geo into the unpacked app dir (GreD = dist/bearbrowser) and + # RE-TAR that (mach's own tarball predates the staging). Best-effort — + # BearNet degrades to "offline" if the sidecar build fails. + CAP="" + if command -v cargo >/dev/null 2>&1; then + cargo build --release --manifest-path capture-sidecar/Cargo.toml \ + && CAP="$PWD/capture-sidecar/target/release/capture-sidecar" || true + fi + bash scripts/stage-bearnet.sh "$OBJ/dist/bearbrowser" "$CAP" || true TAR="BearBrowser-${{ env.VERSION }}-${DATE}-${{ matrix.artifact_suffix }}.tar.xz" mkdir -p build/nightly - cp "$SRC_TAR" "build/nightly/${TAR}" + tar -C "$OBJ/dist" -cJf "build/nightly/${TAR}" bearbrowser echo "name=${TAR}" >> $GITHUB_OUTPUT echo "path=build/nightly/${TAR}" >> $GITHUB_OUTPUT diff --git a/capture-sidecar/src/netmon.rs b/capture-sidecar/src/netmon.rs index 0c57bae..f14bcab 100644 --- a/capture-sidecar/src/netmon.rs +++ b/capture-sidecar/src/netmon.rs @@ -24,10 +24,11 @@ struct Raw { remote_port: String, } -/// Run `lsof` once and parse established outbound TCP connections. Uses field -/// output (-F) so it's robust to spaces in process names. Best-effort: any -/// error yields an empty list (the monitor just shows nothing that tick). -async fn poll_lsof() -> Vec { +/// Unix: run `lsof` once and parse established outbound TCP connections. Uses +/// field output (-F) so it's robust to spaces in process names. Best-effort: +/// any error yields an empty list (the monitor just shows nothing that tick). +#[cfg(unix)] +async fn poll_connections() -> Vec { let out = Command::new("lsof") .args(["-nP", "-iTCP", "-sTCP:ESTABLISHED", "-Fpcn"]) .output() @@ -67,6 +68,52 @@ async fn poll_lsof() -> Vec { rows } +/// Windows: `lsof` doesn't exist. Enumerate established outbound TCP via +/// `netstat -no` (connections + owning PID), then map PID → process name with a +/// single `tasklist /fo csv` pass. Same Raw rows as the unix path. +#[cfg(windows)] +async fn poll_connections() -> Vec { + // PID -> image name, from one tasklist call. + let mut names: std::collections::HashMap = std::collections::HashMap::new(); + if let Ok(out) = Command::new("tasklist").args(["/fo", "csv", "/nh"]).output().await { + let text = String::from_utf8_lossy(&out.stdout); + for line in text.lines() { + // "image.exe","1234","Console","1","12,345 K" + let cols: Vec<&str> = line.split("\",\"").collect(); + if cols.len() >= 2 { + let name = cols[0].trim_matches(['"', ' ']).to_string(); + let pid = cols[1].trim_matches(['"', ' ']).to_string(); + if !pid.is_empty() { + names.insert(pid, name); + } + } + } + } + let mut rows = Vec::new(); + let Ok(out) = Command::new("netstat").args(["-no", "-p", "TCP"]).output().await else { + return rows; + }; + let text = String::from_utf8_lossy(&out.stdout); + for line in text.lines() { + let f: Vec<&str> = line.split_whitespace().collect(); + // Proto Local Foreign State PID — established outbound only. + if f.len() >= 5 && f[0].eq_ignore_ascii_case("TCP") && f[3].eq_ignore_ascii_case("ESTABLISHED") { + let foreign = f[2]; + if let Some(idx) = foreign.rfind(':') { + let (host, port) = foreign.split_at(idx); + let host = host.trim_matches(['[', ']']); + let pid = f[4].to_string(); + rows.push(Raw { + process: names.get(&pid).cloned().unwrap_or_else(|| format!("pid {pid}")), + remote_ip: host.to_string(), + remote_port: port[1..].to_string(), + }); + } + } + } + rows +} + /// Does this process belong to the browser? (scope=Browser filter) fn is_browser(proc: &str) -> bool { let p = proc.to_ascii_lowercase(); @@ -114,7 +161,7 @@ pub fn spawn( let dns_cache: Mutex> = Mutex::new(HashMap::new()); loop { let want_browser = matches!(*scope.read().unwrap(), Scope::Browser); - let rows = poll_lsof().await; + let rows = poll_connections().await; let mut live_keys: HashSet = HashSet::new(); for r in rows { diff --git a/scripts/bearbrowser-package-source-build.sh b/scripts/bearbrowser-package-source-build.sh index 8143569..fa03a8e 100755 --- a/scripts/bearbrowser-package-source-build.sh +++ b/scripts/bearbrowser-package-source-build.sh @@ -232,39 +232,14 @@ EOF_BEARSTART_WIRING echo " bearstart-autoconfig.js → Contents/Resources/bearbrowser.cfg (new-tab wiring)" fi -# ── Stage the bearstart pages LOOSE + the BearNet sidecar/bridge/geo ────────── -# resource://bearstart/ (registered by the autoconfig) maps to -# Contents/Resources/browser/bearstart/. FINAL_TARGET_FILES doesn't survive -# mach package (verified on a real DMG — the branded new-tab shipped BROKEN), so -# stage these loose here, guaranteed. This is what makes the start page render -# and BearNet go LIVE instead of a paper tiger. -bs_dest="$out_app/Contents/Resources/browser/bearstart" -mkdir -p "$bs_dest" -for f in bearbrowser-start.html bearnet.html world.json dm-sans-latin.woff2 dm-sans-latin-ext.woff2; do - [ -f "$repo_root/settings/start/$f" ] && cp "$repo_root/settings/start/$f" "$bs_dest/" -done -echo " start page + BearNet panel → $bs_dest ($(ls "$bs_dest" | wc -l | tr -d ' ') files)" - -# The capture sidecar + its governance bridge + policy + geo DBs, so the -# autoconfig can launch a LIVE BearNet. Best-effort: if the sidecar binary -# wasn't built (no cargo at build time), BearNet degrades honestly to "offline". -res="$out_app/Contents/Resources" -_capbin="$repo_root/capture-sidecar/target/release/capture-sidecar" -if [ -x "$_capbin" ]; then - mkdir -p "$res/sidecars" "$res/scripts" "$res/policy" "$res/geoip" - cp "$_capbin" "$res/sidecars/bearbrowser-capture-sidecar-bin" - cp "$repo_root/scripts/agent-control-bridge.py" "$res/scripts/" - cp "$repo_root/scripts/strip-json-comments.py" "$res/scripts/" 2>/dev/null || true - cp "$repo_root/policy/bearbrowser-contract.yaml" "$res/policy/" - # GeoIP: fetch if absent, then stage both DBs (city + ASN). - bash "$repo_root/scripts/fetch-geoip.sh" "$repo_root/capture-sidecar/geoip" >/dev/null 2>&1 || true - for db in dbip-city-lite.mmdb dbip-asn-lite.mmdb; do - [ -f "$repo_root/capture-sidecar/geoip/$db" ] && cp "$repo_root/capture-sidecar/geoip/$db" "$res/geoip/" - done - echo " capture sidecar + bridge + policy + $(ls "$res/geoip" 2>/dev/null | wc -l | tr -d ' ') geoip DBs → app (BearNet live)" -else - echo " NOTE: capture-sidecar binary not built — BearNet ships but shows 'offline' (build it: scripts/build-capture-sidecar.sh)" -fi +# ── Stage BearNet (pages + sidecar + governance + geo) into the app ────────── +# Via the shared cross-platform stager. GreD on macOS = Contents/Resources. +# This is what makes the start page render and BearNet go LIVE (verified on a +# real DMG that resource:///bearstart/ was broken without it). +bash "$repo_root/scripts/stage-bearnet.sh" \ + "$out_app/Contents/Resources" \ + "$repo_root/capture-sidecar/target/release/capture-sidecar" \ + | sed 's/^/ /' # ── Step 5: Ad-hoc sign ─────────────────────────────────────────────────────── echo "[5/6] Code signing..." diff --git a/scripts/build-windows-cross.sh b/scripts/build-windows-cross.sh index f23b283..8a4b05e 100755 --- a/scripts/build-windows-cross.sh +++ b/scripts/build-windows-cross.sh @@ -164,5 +164,29 @@ ulimit -n "$NOFILE" ./mach build -j"${MOZ_BUILD_JOBS:-$(nproc)}" ./mach package +# ── Make BearNet LIVE on Windows ───────────────────────────────────────────── +# Cross-compile the sidecar to Windows (mingw target — far simpler than MSVC +# cross-linking), stage it + the pages + geo into the unpacked app (GreD = +# dist/firefox), and re-zip the portable archive so it carries BearNet. The +# sidecar's connection monitor uses netstat/tasklist on Windows (not lsof). +# Best-effort — BearNet degrades to "offline" if the sidecar cross-build fails. +log "BearNet: cross-compile sidecar (windows-gnu) + stage into the app" +CAPWIN="" +if command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1 || sudo apt-get install -y -qq gcc-mingw-w64-x86-64 2>/dev/null; then + ( cd "$REPO_ROOT" \ + && rustup target add x86_64-pc-windows-gnu \ + && cargo build --release --target x86_64-pc-windows-gnu --manifest-path capture-sidecar/Cargo.toml ) \ + && CAPWIN="$REPO_ROOT/capture-sidecar/target/x86_64-pc-windows-gnu/release/capture-sidecar.exe" || true +fi +if [ -d "$SRCDIR/obj-win64/dist/firefox" ]; then + bash "$REPO_ROOT/scripts/stage-bearnet.sh" "$SRCDIR/obj-win64/dist/firefox" "$CAPWIN" || true + # Re-zip the portable archive with BearNet included. + ZIP="$(ls "$SRCDIR"/obj-win64/dist/firefox-*.win64.zip 2>/dev/null | head -1)" + if [ -n "$ZIP" ]; then + ( cd "$SRCDIR/obj-win64/dist" && rm -f "$ZIP" && zip -qr "$(basename "$ZIP")" firefox ) + log "re-zipped $(basename "$ZIP") with BearNet staged in" + fi +fi + log "artifacts" -ls -la "$SRCDIR"/obj-win64/dist/*.win64.zip "$SRCDIR"/obj-win64/dist/*.win64.installer.exe +ls -la "$SRCDIR"/obj-win64/dist/*.win64.zip "$SRCDIR"/obj-win64/dist/*.win64.installer.exe 2>/dev/null diff --git a/scripts/stage-bearnet.sh b/scripts/stage-bearnet.sh new file mode 100755 index 0000000..b7083d6 --- /dev/null +++ b/scripts/stage-bearnet.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# stage-bearnet.sh [SIDECAR_BIN] +# +# Stage BearNet into a packaged app so it renders + goes LIVE. Cross-platform: +# is the app's "GreD" (resource base) — the dir that contains the +# `browser/` subdir and the binary: +# macOS : .app/Contents/Resources +# Linux : /bearbrowser +# Windows : /firefox (top dir inside the win64 zip) +# +# It stages, all where the autoconfig's resource://bearstart/ substitution + its +# Subprocess launcher expect them: +# /browser/bearstart/ the start page + BearNet panel + fonts + world map +# /sidecars/ the capture sidecar binary the app launches +# /scripts/ agent-control-bridge.py (the governance engine) +# /policy/ the enforcing contract +# /geoip/ DB-IP City + ASN (the map + who-owns-it data) +# +# Why loose (not omni.ja): FINAL_TARGET_FILES doesn't survive `mach package` — +# verified on a real DMG that the branded new-tab was shipping BROKEN. +set -euo pipefail + +GRE="${1:?usage: stage-bearnet.sh [SIDECAR_BIN]}" +SIDECAR="${2:-}" +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +[ -d "$GRE" ] || { echo "stage-bearnet: GRE dir not found: $GRE" >&2; exit 1; } + +# 1. The pages, loose where resource://bearstart/ resolves. +bs="$GRE/browser/bearstart" +mkdir -p "$bs" +for f in bearbrowser-start.html bearnet.html world.json dm-sans-latin.woff2 dm-sans-latin-ext.woff2; do + [ -f "$REPO/settings/start/$f" ] && cp "$REPO/settings/start/$f" "$bs/" +done +echo "stage-bearnet: pages → $bs ($(ls "$bs" | wc -l | tr -d ' ') files)" + +# 2. The sidecar + governance + geo, so BearNet is LIVE (not "offline"). +if [ -n "$SIDECAR" ] && [ -f "$SIDECAR" ]; then + mkdir -p "$GRE/sidecars" "$GRE/scripts" "$GRE/policy" "$GRE/geoip" + # The autoconfig launcher looks for bearbrowser-capture-sidecar-bin[.exe]. + case "$SIDECAR" in + *.exe) cp "$SIDECAR" "$GRE/sidecars/bearbrowser-capture-sidecar-bin.exe" ;; + *) cp "$SIDECAR" "$GRE/sidecars/bearbrowser-capture-sidecar-bin"; chmod +x "$GRE/sidecars/bearbrowser-capture-sidecar-bin" ;; + esac + cp "$REPO/scripts/agent-control-bridge.py" "$GRE/scripts/" + cp "$REPO/scripts/strip-json-comments.py" "$GRE/scripts/" 2>/dev/null || true + cp "$REPO/policy/bearbrowser-contract.yaml" "$GRE/policy/" + bash "$REPO/scripts/fetch-geoip.sh" "$REPO/capture-sidecar/geoip" >/dev/null 2>&1 || true + for db in dbip-city-lite.mmdb dbip-asn-lite.mmdb; do + [ -f "$REPO/capture-sidecar/geoip/$db" ] && cp "$REPO/capture-sidecar/geoip/$db" "$GRE/geoip/" + done + echo "stage-bearnet: sidecar + bridge + policy + $(ls "$GRE/geoip" 2>/dev/null | wc -l | tr -d ' ') geoip DBs → LIVE" +else + echo "stage-bearnet: no sidecar binary given — BearNet ships but shows 'offline'" +fi diff --git a/settings/start/bearstart-autoconfig.js b/settings/start/bearstart-autoconfig.js index 5fb2e82..fa0705b 100644 --- a/settings/start/bearstart-autoconfig.js +++ b/settings/start/bearstart-autoconfig.js @@ -40,12 +40,17 @@ try { "resource://gre/modules/Subprocess.sys.mjs" ); const greD = Services.dirsvc.get("GreD", Ci.nsIFile); - const bin = greD.clone(); - bin.append("sidecars"); - bin.append("bearbrowser-capture-sidecar-bin"); const geo = greD.clone(); geo.append("geoip"); - if (bin.exists()) { + // Unix binary or the Windows .exe, whichever was staged. + let bin = null; + for (const name of ["bearbrowser-capture-sidecar-bin", "bearbrowser-capture-sidecar-bin.exe"]) { + const f = greD.clone(); + f.append("sidecars"); + f.append(name); + if (f.exists()) { bin = f; break; } + } + if (bin) { Subprocess.call({ command: bin.path, arguments: ["--repo-root", greD.path, "--port", "8093"],