Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/workflows/nightly-linux.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -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

Expand Down
57 changes: 52 additions & 5 deletions capture-sidecar/src/netmon.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<Raw> {
/// 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<Raw> {
let out = Command::new("lsof")
.args(["-nP", "-iTCP", "-sTCP:ESTABLISHED", "-Fpcn"])
.output()
Expand DownExpand Up@@ -67,6 +68,52 @@ async fn poll_lsof() -> Vec<Raw> {
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<Raw> {
// PID -> image name, from one tasklist call.
let mut names: std::collections::HashMap<String, String> = 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();
Expand DownExpand Up@@ -114,7 +161,7 @@ pub fn spawn(
let dns_cache: Mutex<HashMap<String, String>> = 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<String> = HashSet::new();
for r in rows {
Expand Down
41 changes: 8 additions & 33 deletions scripts/bearbrowser-package-source-build.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -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..."
Expand Down
26 changes: 25 additions & 1 deletion scripts/build-windows-cross.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
54 changes: 54 additions & 0 deletions scripts/stage-bearnet.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# stage-bearnet.sh <GRE_DIR> [SIDECAR_BIN]
#
# Stage BearNet into a packaged app so it renders + goes LIVE. Cross-platform:
# <GRE_DIR> is the app's "GreD" (resource base) — the dir that contains the
# `browser/` subdir and the binary:
# macOS : <App>.app/Contents/Resources
# Linux : <dist>/bearbrowser
# Windows : <dist>/firefox (top dir inside the win64 zip)
#
# It stages, all where the autoconfig's resource://bearstart/ substitution + its
# Subprocess launcher expect them:
# <GRE>/browser/bearstart/ the start page + BearNet panel + fonts + world map
# <GRE>/sidecars/ the capture sidecar binary the app launches
# <GRE>/scripts/ agent-control-bridge.py (the governance engine)
# <GRE>/policy/ the enforcing contract
# <GRE>/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 <GRE_DIR> [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
13 changes: 9 additions & 4 deletions settings/start/bearstart-autoconfig.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -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"],
Expand Down
Loading