From 16c56735181ae9feb2cc783bc904413cf0276331 Mon Sep 17 00:00:00 2001 From: fitz123 <10243861+fitz123@users.noreply.github.com> Date: Thu, 21 May 2026 15:45:21 +0400 Subject: [PATCH 1/3] =?UTF-8?q?feat(client):=20Phase=206=20=E2=80=94=20ad-?= =?UTF-8?q?hoc=20codesign=20+=20release=20runbook=20+=20install=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - client/pkg-build/build.sh: codesign -s - --force the staged bb-vpn, sing-box, xray binaries and BBVPN.app; verify --strict before pkgbuild. Strips xattrs AFTER signing (signature lives in LC_CODE_SIGNATURE / Contents/_CodeSignature, not xattrs). - docs/release.md: operator runbook covering build, ad-hoc signing semantics, hosting via long-random nginx path, per-user install-page generation via envsubst, token rotation procedure (9-step table with time estimates), upgrade flow + verification steps on a clean Mac. - client/pkg-build/install-page-template.html: user-facing install page template with PKG_URL/PKG_NAME/ENROLL_URI/USER_NAME envsubst slots. Self-contained CSS, dark-mode aware, robots noindex/nofollow. --- client/pkg-build/build.sh | 46 +++ client/pkg-build/install-page-template.html | 147 ++++++++++ docs/release.md | 300 ++++++++++++++++++++ 3 files changed, 493 insertions(+) create mode 100644 client/pkg-build/install-page-template.html create mode 100644 docs/release.md diff --git a/client/pkg-build/build.sh b/client/pkg-build/build.sh index 4c714c1..99e64f1 100755 --- a/client/pkg-build/build.sh +++ b/client/pkg-build/build.sh @@ -51,6 +51,7 @@ extract_version() { command -v jq >/dev/null 2>&1 || die "jq is required" command -v pkgbuild >/dev/null 2>&1 || die "pkgbuild is required (Xcode CLT)" command -v productbuild >/dev/null 2>&1 || die "productbuild is required (Xcode CLT)" +command -v codesign >/dev/null 2>&1 || die "codesign is required (Xcode CLT)" EXPECT_BB=$(jq -r '.bb_vpn' "$MANIFEST") EXPECT_SB=$(jq -r '.sing_box' "$MANIFEST") @@ -148,9 +149,54 @@ jq --arg tok "$TOKEN_TRIMMED" \ > "$STAGING_DIR/Library/Application Support/bb-dpi/control-plane.json" chmod 0600 "$STAGING_DIR/Library/Application Support/bb-dpi/control-plane.json" +# Phase 6: ad-hoc codesign every executable + the .app bundle in the +# staging tree. `-s -` is ad-hoc (no Apple identity); the signature +# only gives each binary a stable code-signing identifier so the +# kernel's library-validation and TCC paths don't trip on completely +# unsigned binaries. Gatekeeper still treats the .pkg + .app as +# "unidentified developer" — the user-facing right-click → Open dance +# is documented in docs/release.md. +# +# `--force` overwrites any existing signature (upstream sing-box and +# xray release builds ship with their own ad-hoc sigs we don't want to +# rely on). `--deep` recurses into BBVPN.app's bundle. +# +# Resign order is leaves-first for the .app (Mach-O binary inside +# Contents/MacOS first, then the bundle), so that the bundle's +# CodeResources record sees the final signature of the embedded binary. +# For standalone binaries the order doesn't matter. +blue "ad-hoc codesigning payload..." +SIGN_BINS=( + "$STAGING_DIR/Library/Application Support/bb-dpi/bin/bb-vpn" + "$STAGING_DIR/Library/Application Support/bb-dpi/bin/sing-box" + "$STAGING_DIR/Library/Application Support/bb-dpi/bin/xray" +) +for bin in "${SIGN_BINS[@]}"; do + [[ -f "$bin" ]] || die "missing payload binary for codesign: $bin" + codesign --sign - --force --timestamp=none "$bin" +done +# BBVPN.app's embedded executable, then the bundle. +codesign --sign - --force --timestamp=none \ + "$STAGING_DIR/Applications/BBVPN.app/Contents/MacOS/BBVPN" +codesign --sign - --force --deep --timestamp=none \ + "$STAGING_DIR/Applications/BBVPN.app" + +# Verify everything we just signed actually validates. If codesign +# silently no-op'd anything (e.g., quarantine on the staging dir, FS +# without xattr support), this catches it before the .pkg ships. +for bin in "${SIGN_BINS[@]}"; do + codesign --verify --strict "$bin" || die "codesign verify failed: $bin" +done +codesign --verify --deep --strict \ + "$STAGING_DIR/Applications/BBVPN.app" || die "codesign verify failed: BBVPN.app" +green "ad-hoc signatures verified." + # Strip xattrs so pkgbuild doesn't emit AppleDouble ._* sidecars for # every payload file (com.apple.provenance and friends on brew-installed # binaries). Keeps the .pkg smaller and the payload listing clean. +# Runs AFTER codesign — the signature is stored inside the Mach-O +# LC_CODE_SIGNATURE load command (and inside the .app's +# Contents/_CodeSignature/), not as an xattr, so xattr -cr is safe. xattr -cr "$STAGING_DIR" # postinstall lives in pkgbuild's --scripts dir, NOT the payload tree. diff --git a/client/pkg-build/install-page-template.html b/client/pkg-build/install-page-template.html new file mode 100644 index 0000000..ba83ea1 --- /dev/null +++ b/client/pkg-build/install-page-template.html @@ -0,0 +1,147 @@ + + + + + + + +BB-VPN install — ${USER_NAME} + + + +
+ +

BB-VPN install

+

Personal link for ${USER_NAME}. Don't share this page — both the installer and your enrollment link are on it.

+ +
+

1Download the installer

+

Download ${PKG_NAME}

+

~20 MB. Save it somewhere you can find in Finder (Downloads is fine).

+
+ +
+

2Right-click → Open the installer

+

Find ${PKG_NAME} in Finder. Don't double-click it.

+
    +
  1. Right-click (or Control-click) the file.
  2. +
  3. Choose Open from the menu.
  4. +
  5. macOS will show a warning: “${PKG_NAME} cannot be opened because Apple cannot check it for malicious software.” Click Open.
  6. +
  7. The standard installer opens. Click through. Enter your Mac password when asked.
  8. +
+
+ Why the warning? The installer isn't signed with an Apple Developer ID (we don't have a paid Apple Developer account). Right-click → Open is the official one-time approval flow. macOS remembers your approval; you won't see this dialog again for this file. +
+
+ +
+

3Open BBVPN once

+

After install, open BBVPN from your Applications folder the same way: right-click → Open → click Open on the warning dialog. A small icon (grey or yellow circle) appears in your menu bar.

+
+ +
+

4Enroll

+

Click the button below. Your browser will ask if you want to open the link in BBVPN — click Allow or Open BBVPN.

+

Enroll this Mac

+
+ The button doesn't open BBVPN — what now? +

Copy this URL and paste it into your address bar (Safari, Chrome) and press Enter. Browser will route it to BBVPN.

+

${ENROLL_URI}

+

Still nothing? Open Terminal (Spotlight → “Terminal”) and run:

+

sudo bb-vpn enroll '${ENROLL_URI}'

+
+
+ +
+ +

How do I know it's working?

+

The menu bar icon turns green within a minute of enrollment. Click the icon to see your exit country and which services are running. Visit ifconfig.co — the IP shown should NOT be your real one.

+ +

It's stuck on yellow / grey

+
    +
  1. Grey: not enrolled yet — repeat step 4.
  2. +
  3. Yellow with “syncing”: wait 15-30 seconds, the first sync is in flight.
  4. +
  5. Yellow with an error: click the icon, copy the error line, and DM the operator.
  6. +
+ +

Stop / start

+

In Terminal:

+

sudo bb-vpn stop — turn the VPN off (survives reboots).
+ sudo bb-vpn start — turn it back on.

+ +

Uninstall

+

If you ever need to remove BB-VPN, in Terminal:

+

sudo /Library/Application\ Support/bb-dpi/bin/bb-vpn-uninstall

+ +
+

Trouble? DM the operator. Don't forward this page.

+ +
+ + diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 0000000..cbc8eb4 --- /dev/null +++ b/docs/release.md @@ -0,0 +1,300 @@ +# Release runbook + +Operator runbook for cutting a `BB-VPN-.pkg`, signing it ad-hoc, +hosting it, and rolling it out. Phase 6 of the pkg-and-pull-control-plane +plan. + +The operator does **not** have an Apple Developer license. Everything is +unsigned (or ad-hoc signed). No notarization. Users see Gatekeeper +warnings on first install and first launch; the user-facing install page +walks them through the right-click → Open dance. + +--- + +## 1. Prerequisites + +One-time on the dev machine: + +- Xcode Command Line Tools (`xcrun`, `pkgbuild`, `productbuild`, `lipo`, + `codesign`, `swiftc`) — `xcode-select --install`. +- Go ≥ 1.22 — `brew install go`. `go.mod` enforces the floor. +- `jq` — `brew install jq`. +- Control plane bootstrap completed: `config/control-plane/endpoints.json` + + `config/control-plane/token` exist (see + [control-plane-bootstrap.md](control-plane-bootstrap.md)). +- `sing-box` and `xray` binaries dropped into + `client/pkg-build/payload-binaries/` along with `geoip.dat` + + `geosite.dat` (see `client/pkg-build/README.md`). +- `config/control-plane/package-manifest.json` versions match the + payload binaries (`make build-pkg` aborts otherwise). + +--- + +## 2. Build + +From the project root: + +``` +make build-pkg +``` + +This runs in sequence: + +1. `make build-bb-vpn-pkg` — Darwin universal `bb-vpn` (arm64+amd64 via + `lipo`) at `build/pkg/bb-vpn`. Version from + `package-manifest.json.bb_vpn`, baked in via `-ldflags`. +2. `client/menubar/build.sh` — Darwin universal `BBVPN.app` at + `build/menubar/BBVPN.app`. +3. `client/pkg-build/build.sh`: + - Version-couples bb-vpn, sing-box, xray against the manifest. Abort + on mismatch. + - Stages payload under `build/pkg-staging/`. + - **Ad-hoc codesigns** `bb-vpn`, `sing-box`, `xray`, and `BBVPN.app` + in place inside the staging tree (`codesign -s - --force --deep`). + No Apple identity, no notarization. The signatures only give each + binary a stable code-signing identifier so the kernel's + library-validation and TCC paths don't trip on "completely + unsigned" binaries. Gatekeeper still treats the .pkg and .app as + "unidentified developer" — the right-click → Open dance is still + required on first install + first launch. + - Runs `pkgbuild` + `productbuild`. Output: + `client/pkg-build/dist/BB-VPN-.pkg`. + +The .pkg itself is unsigned. `productbuild --sign` requires a Developer +ID Installer cert that the operator doesn't have; skipping it is the +intentional cost of zero-license distribution. + +Smoke-check the bundled signatures: + +``` +pkgutil --expand client/pkg-build/dist/BB-VPN-.pkg /tmp/bb-pkg-expand +cd /tmp/bb-pkg-expand && cat Payload | gunzip | cpio -i -d +codesign -dv ./Library/Application\ Support/bb-dpi/bin/bb-vpn +codesign -dv ./Applications/BBVPN.app +``` + +Expected: `Signature=adhoc` for each. `codesign --verify --deep +--strict` should pass. + +--- + +## 3. Host the .pkg + install page + +The .pkg and the user-facing install page get served from a separate +nginx `location` on the cover-site backend (the same nginx that already +fronts xray's REALITY fallback on `127.0.0.1:8081`). + +**Why a separate location, not under `/control/bundle.json`'s +`auth_request` umbrella**: browsers can't add `Authorization` headers on +plain navigation, so a token-gated download URL would just hand every +user a 401. The protection here is path-obscurity: + +``` +https:///d/<32-hex>/BB-VPN-.pkg +https:///d/<32-hex>/install-.html +``` + +The 32-hex segment is per-cohort random. Anyone with the URL has the +.pkg (which itself contains the bearer token in +`control-plane.json`); rotation cost is documented in +[§5](#5-token-rotation). + +### 3a. Mint a random path + +``` +PATH_HEX=$(openssl rand -hex 16) +echo "$PATH_HEX" +``` + +Record this somewhere out-of-band (1Password, operator notes). Treat it +as a secret of the same blast-radius as the token: leaked path = leaked +.pkg = leaked token. + +### 3b. nginx location + +On each cover-site host that should serve downloads, drop this snippet +into the same server block where `/control/bundle.json` lives (NOT +gated by `auth_request`): + +``` +# /etc/nginx/snippets/bb-dpi-downloads.conf +location ^~ /d// { + alias /var/www/bb-dpi-downloads/; + autoindex off; + add_header Cache-Control "no-store"; + # Same cover fallthrough as /control/bundle.json — any miss/error + # leaks to the cover site's natural 404 so the directory itself + # isn't fingerprintable. + error_page 403 404 = @cover_404; +} +``` + +`^~` prevents the location from also matching adjacent regex +`location ~` blocks (e.g., a `.html$` PHP handler on the cover site). +`@cover_404` is already wired during control-plane bootstrap. + +Then in the parent server block: + +``` +include /etc/nginx/snippets/bb-dpi-downloads.conf; +``` + +Reload: + +``` +nginx -t +systemctl reload nginx +``` + +### 3c. Drop the files + +``` +sudo mkdir -p /var/www/bb-dpi-downloads +sudo chown -R root: /var/www/bb-dpi-downloads +sudo chmod 0750 /var/www/bb-dpi-downloads +sudo cp BB-VPN-.pkg /var/www/bb-dpi-downloads/ +sudo chmod 0640 /var/www/bb-dpi-downloads/BB-VPN-.pkg +``` + +(Repeat the install-page step below per user.) + +### 3d. Per-user install page + +`client/pkg-build/install-page-template.html` is the template. Fill the +placeholders with `envsubst`: + +``` +export PKG_URL="https:///d//BB-VPN-.pkg" +export PKG_NAME="BB-VPN-.pkg" +export ENROLL_URI=$(./scripts/xray-users enroll-url "") +export USER_NAME="" +envsubst < client/pkg-build/install-page-template.html \ + > /tmp/install-.html +scp /tmp/install-.html \ + cover-host:/var/www/bb-dpi-downloads/install-.html +ssh cover-host 'sudo chmod 0640 /var/www/bb-dpi-downloads/install-*.html && \ + sudo chown root: /var/www/bb-dpi-downloads/install-*.html' +``` + +(Use a non-guessable `` — first name + 4 random hex is +plenty.) The DM the user receives is just the URL of this HTML page. + +--- + +## 4. Upgrade flow + +`.pkg` is fully self-contained. The user downloads `BB-VPN-1.0.1.pkg`, +right-clicks → Open, runs through the install dialog. No uninstall +required. + +What postinstall does on top of an existing install: + +- Overwrites every payload file (bb-vpn binary, sing-box, xray, + BBVPN.app, plists, geoip.dat/geosite.dat). +- Re-runs `lsregister` for BBVPN.app so `bb-vpn://` URL handler resolves + to the new binary. +- `launchctl bootout` + `launchctl bootstrap` for the sync daemon and + the menubar LaunchAgent (the bb-vpn binary they exec resolves to the + new copy at next spawn). +- Calls `bb-vpn start` (guarded by `manually_stopped.flag` absence + + `identity.json` presence). For an already-installed user with an + active identity, this kicks the daemons fresh. +- **Does NOT clobber** `manually_stopped.flag` on reinstall. A user who + stopped the VPN stays stopped through an upgrade. +- **Does NOT clobber** `identity.json` (the user's UUID + enrollment). + Enrollment survives upgrades. The same .pkg can be installed onto a + pristine Mac (where postinstall skips `bb-vpn start` because + `identity.json` is absent — user clicks the enroll link in the + install page to enroll). + +sing-box and xray daemons are re-bootstrapped by the new bb-vpn on its +next sync tick (within 15 min, or immediately on a config change), after +its `pre-restart validation` check passes (sing-box `check`, xray +`-test`). Until then the previous version's daemons keep running. + +`control-plane.json` (endpoint URLs + bearer token) is **baked into the +.pkg payload** at build time. On reinstall, the new copy replaces the +old — which means token rotation is a "reinstall everyone" event. + +--- + +## 5. Token rotation + +Token rotation is high-cost and not routine. Trigger only on suspected +compromise of the .pkg (URL leak with enrollment data, lost laptop with +active install, etc.). Roll out plan: + +| step | action | est. time | +|------|------------------------------------------------------------------|-----------| +| 1 | Mint a new token: `openssl rand -base64 48 \| tr -d '+/=\n' \| cut -c1-64 > config/control-plane/token` | <1 min | +| 2 | Redeploy nginx snippet on every cover-site host: re-substitute `@@TOKEN@@`, reload nginx (see [control-plane-bootstrap.md §2](control-plane-bootstrap.md)) | ~5 min × n hosts | +| 3 | `make publish-bundle` — push new bundle.json (still uses the old token at this point; the swap is nginx-side) | <1 min | +| 4 | `make publish-status` — confirm every endpoint serves the new bundle | <1 min | +| 5 | Bump `package-manifest.json.bb_vpn` patch version (forces the cached `bundle.min_versions` floor to advance) and `make build-pkg` | ~3 min | +| 6 | Mint a fresh download path (`openssl rand -hex 16`), update nginx `/d/` snippet on every cover-site host, reload nginx, drop the new .pkg in the new path | ~5 min × n hosts | +| 7 | Regenerate per-user install pages with the new `PKG_URL`, host them | ~1 min × n users | +| 8 | Slack DM every user: new install URL, deadline (24-48h), "your current install will stop working after this date" | ~15 min × n users (interactive) | +| 9 | After deadline: any user who hasn't pulled the new .pkg → their installed bb-vpn returns 401 from `/control/bundle.json` → bb-vpn's `runtime_blackhole` circuit breaker eventually kicks → daemons stop. Old token + old path are dead. Sweep stragglers manually. | open-ended | + +Total dev-machine time for a 7-user fleet on 1 cover-site host: +~30-45 min active, plus the user-driven adoption tail (1-3 days). + +--- + +## 6. Verification + +After a fresh build + host: + +1. From a **clean Mac** (or wipe `/Library/Application Support/bb-dpi/`, + `/Applications/BBVPN.app`, the launchd plists, and the manually-stopped + flag on a test machine): + ``` + rm -rf "/Library/Application Support/bb-dpi" /Applications/BBVPN.app + launchctl bootout system/com.bb-dpi.bb-vpn-sync 2>&1 || true + launchctl bootout system/com.sing-box-vpn 2>&1 || true + launchctl bootout system/com.xray-xhttp 2>&1 || true + rm -f /Library/LaunchDaemons/com.bb-dpi.bb-vpn-sync.plist \ + /Library/LaunchDaemons/com.sing-box-vpn.plist \ + /Library/LaunchDaemons/com.xray-xhttp.plist \ + /Library/LaunchAgents/com.bb-dpi.bb-vpn-menubar.plist + ``` +2. In a browser, open the per-user install page URL. Click the download + button. +3. In Finder, right-click `BB-VPN-.pkg` → Open. Gatekeeper warning + dialog → "Open" → password prompt → install completes. +4. Open `/Applications/BBVPN.app` (right-click → Open the first time). + The menubar icon appears (grey on a pristine install — not yet + enrolled). +5. In the install page, click the `bb-vpn://enroll?uuid=...` link. + BBVPN.app receives the URL via `LSGetApplicationForURL`, shells out + to `bb-vpn enroll`. Menubar icon turns yellow (first sync in flight) + then green (synced, daemons up). +6. Verify exit: + ``` + curl -fsS https://ifconfig.co/json + ``` + The exit country in the menubar should match the country of the VPN + server. + +Subsequent launches don't need right-click → Open. Gatekeeper remembers +the user's first-time approval. + +--- + +## Don'ts + +- **Don't** post the install page URL in any public channel. The URL is + the only barrier between a stranger and the bearer token baked into + the .pkg. +- **Don't** `productbuild --sign` with an arbitrary identity. Without a + Developer ID Installer cert, the result is worse than unsigned (it + positively asserts an unknown signer, which Gatekeeper rejects more + aggressively than no signature at all). +- **Don't** put the .pkg behind Cloudflare or another TLS-terminating + CDN — the cover-site SNI camouflage relies on the cover host serving + its own LE cert; a CDN would serve its own cert and break the + REALITY camouflage at the same time. +- **Don't** reuse a download path across rotations. After token rotation + (§5), retire the old `/d//` location entirely — leaving it + alive lets an attacker who scraped the old URL keep downloading old + .pkgs (which contain the old, still-cached bundle). From 614c70e3e3234e0bde89b123d02f1a3a9984fe3d Mon Sep 17 00:00:00 2001 From: fitz123 <10243861+fitz123@users.noreply.github.com> Date: Thu, 21 May 2026 16:03:08 +0400 Subject: [PATCH 2/3] fix: address review phase 1 findings --- AGENTS.md | 13 ++- README.md | 19 ++++ client/pkg-build/build.sh | 17 +--- client/pkg-build/install-page-template.html | 14 +-- docs/release.md | 103 ++++++++++++++------ 5 files changed, 115 insertions(+), 51 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 14a9284..a1f15e1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -91,7 +91,7 @@ bb-vpn enroll # Submit an enrollment URI (the menuba bb-vpn --version # Print version (ldflags-stamped at build time) ``` -### .pkg installer (Phase 4 + 5) +### .pkg installer (Phase 4 + 5 + 6) ```bash make build-bb-vpn-host # Host-arch bb-vpn binary -> build/bb-vpn (dev/test) make build-bb-vpn-pkg # Darwin universal bb-vpn -> build/pkg/bb-vpn (for .pkg) @@ -100,6 +100,17 @@ make test-bb-vpn # Run client/bb-vpn Go tests make build-pkg # Assemble BB-VPN-.pkg (incl. BBVPN.app) in client/pkg-build/dist/ ``` +Phase 6 adds ad-hoc codesigning to `build.sh` (`codesign -s - --force +--deep`; no Apple Developer license, no notarization — Gatekeeper still +shows "unidentified developer" on first install + first launch) and a +user-facing install page template at +`client/pkg-build/install-page-template.html`. The operator-facing +host/distribute runbook (build, sign, host on a long-random nginx +location, per-user install page via envsubst, token rotation, +verification) lives in [`docs/release.md`](docs/release.md). Future +operators/agents touching the .pkg flow should read it before +modifying `build.sh` or the install page. + `vpn-start` no longer parses its own flags. Any args after the program name are forwarded verbatim to `render-config`; xray-need is auto-detected from the rendered sing-box config (presence of any `xhttp-*` SOCKS outbound). diff --git a/README.md b/README.md index 94cfa92..dd19d9f 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,18 @@ openssl s_client -connect :443 -servername -alpn h2 -tls1_ # NOT xray's synthetic cert. ``` +## .pkg distribution (Phase 4–6) + +The macOS `.pkg` installer flow under `client/pkg-build/` (with the +`bb-vpn` control-plane binary in `client/bb-vpn/` and the `BBVPN.app` +menu-bar app in `client/menubar/`) is the supported way to ship to +end-users. Build with `make build-pkg`, host the resulting +`BB-VPN-.pkg` + per-user install page on a long-random URL, and +share that URL out-of-band. + +Full operator runbook (build, ad-hoc codesign, host, per-user install +page, token rotation, verification): [docs/release.md](docs/release.md). + ## Files ``` @@ -163,6 +175,13 @@ config/ sing-box.template.json - Legacy single-server sing-box template com.xray-xhttp.plist - launchd plist for xray-core com.sing-box-vpn.plist - launchd plist for sing-box +client/ + bb-vpn/ - Go control-plane CLI (shipped in the .pkg) + menubar/ - SwiftUI BBVPN.app sources + pkg-build/ - .pkg installer assembly (build.sh, postinstall.sh, install-page-template.html) +docs/ + release.md - Phase 6 operator runbook for the .pkg flow + control-plane-bootstrap.md - Phase 1 control-plane setup scripts/ deploy.sh - First-time server deployment xray-users - User management CLI diff --git a/client/pkg-build/build.sh b/client/pkg-build/build.sh index 99e64f1..13d4e9c 100755 --- a/client/pkg-build/build.sh +++ b/client/pkg-build/build.sh @@ -159,12 +159,9 @@ chmod 0600 "$STAGING_DIR/Library/Application Support/bb-dpi/control-plane.json" # # `--force` overwrites any existing signature (upstream sing-box and # xray release builds ship with their own ad-hoc sigs we don't want to -# rely on). `--deep` recurses into BBVPN.app's bundle. -# -# Resign order is leaves-first for the .app (Mach-O binary inside -# Contents/MacOS first, then the bundle), so that the bundle's -# CodeResources record sees the final signature of the embedded binary. -# For standalone binaries the order doesn't matter. +# rely on). `--deep` recurses into BBVPN.app's bundle and signs the +# embedded Mach-O at Contents/MacOS/BBVPN in the same invocation, so +# no separate inner pre-sign is needed. blue "ad-hoc codesigning payload..." SIGN_BINS=( "$STAGING_DIR/Library/Application Support/bb-dpi/bin/bb-vpn" @@ -172,18 +169,12 @@ SIGN_BINS=( "$STAGING_DIR/Library/Application Support/bb-dpi/bin/xray" ) for bin in "${SIGN_BINS[@]}"; do - [[ -f "$bin" ]] || die "missing payload binary for codesign: $bin" codesign --sign - --force --timestamp=none "$bin" done -# BBVPN.app's embedded executable, then the bundle. -codesign --sign - --force --timestamp=none \ - "$STAGING_DIR/Applications/BBVPN.app/Contents/MacOS/BBVPN" codesign --sign - --force --deep --timestamp=none \ "$STAGING_DIR/Applications/BBVPN.app" -# Verify everything we just signed actually validates. If codesign -# silently no-op'd anything (e.g., quarantine on the staging dir, FS -# without xattr support), this catches it before the .pkg ships. +# Verify everything we just signed actually validates. for bin in "${SIGN_BINS[@]}"; do codesign --verify --strict "$bin" || die "codesign verify failed: $bin" done diff --git a/client/pkg-build/install-page-template.html b/client/pkg-build/install-page-template.html index ba83ea1..b078de8 100644 --- a/client/pkg-build/install-page-template.html +++ b/client/pkg-build/install-page-template.html @@ -57,8 +57,6 @@ background: var(--accent); color: var(--accent-fg); text-decoration: none; font-weight: 600; } .btn:hover { filter: brightness(1.05); } - .btn.secondary { background: transparent; color: var(--accent); - border: 1px solid var(--accent); } ol { padding-left: 22px; } ol li { margin: 6px 0; } code { background: var(--bg); padding: 1px 6px; border-radius: 4px; @@ -96,13 +94,13 @@

BB-VPN install

  • The standard installer opens. Click through. Enter your Mac password when asked.
  • - Why the warning? The installer isn't signed with an Apple Developer ID (we don't have a paid Apple Developer account). Right-click → Open is the official one-time approval flow. macOS remembers your approval; you won't see this dialog again for this file. + Why the warning? The installer isn't signed with an Apple Developer ID (we don't have a paid Apple Developer account). Right-click → Open is the official one-time approval flow. macOS remembers your approval; you won't see this dialog again for this file. (Text-only instructions are intentional — no screenshots are shipped with the page.)
    -

    3Open BBVPN once

    -

    After install, open BBVPN from your Applications folder the same way: right-click → Open → click Open on the warning dialog. A small icon (grey or yellow circle) appears in your menu bar.

    +

    3BBVPN starts automatically

    +

    After the installer finishes, BBVPN launches on its own — a small icon (grey or yellow circle) appears in your menu bar within a few seconds. If the icon doesn't show up, open /Applications/BBVPN.app once via right-click → Open → click Open on the warning dialog.

    @@ -111,10 +109,8 @@

    BB-VPN install

    Enroll this Mac

    The button doesn't open BBVPN — what now? -

    Copy this URL and paste it into your address bar (Safari, Chrome) and press Enter. Browser will route it to BBVPN.

    -

    ${ENROLL_URI}

    -

    Still nothing? Open Terminal (Spotlight → “Terminal”) and run:

    -

    sudo bb-vpn enroll '${ENROLL_URI}'

    +

    Open Terminal (Spotlight → “Terminal”) and run:

    +

    sudo "/Library/Application Support/bb-dpi/bin/bb-vpn" enroll '${ENROLL_URI}'

    diff --git a/docs/release.md b/docs/release.md index cbc8eb4..e84c5b3 100644 --- a/docs/release.md +++ b/docs/release.md @@ -19,6 +19,8 @@ One-time on the dev machine: `codesign`, `swiftc`) — `xcode-select --install`. - Go ≥ 1.22 — `brew install go`. `go.mod` enforces the floor. - `jq` — `brew install jq`. +- `envsubst` (ships in `gettext`) — `brew install gettext` (needed by + the per-user install-page recipe in [§3d](#3d-per-user-install-page)). - Control plane bootstrap completed: `config/control-plane/endpoints.json` + `config/control-plane/token` exist (see [control-plane-bootstrap.md](control-plane-bootstrap.md)). @@ -64,17 +66,22 @@ The .pkg itself is unsigned. `productbuild --sign` requires a Developer ID Installer cert that the operator doesn't have; skipping it is the intentional cost of zero-license distribution. -Smoke-check the bundled signatures: +Smoke-check the bundled signatures. `build.sh` already runs +`codesign --verify --deep --strict` on the staging tree (output: +`ad-hoc signatures verified.`), so the in-pkg payload is signed. +To inspect the in-pkg signatures directly: ``` +rm -rf /tmp/bb-pkg-expand pkgutil --expand client/pkg-build/dist/BB-VPN-.pkg /tmp/bb-pkg-expand -cd /tmp/bb-pkg-expand && cat Payload | gunzip | cpio -i -d +# productbuild distribution .pkgs expand to a component subdir; +# the cpio Payload lives inside it. +cd /tmp/bb-pkg-expand/BB-VPN-component.pkg && cat Payload | gunzip | cpio -i -d codesign -dv ./Library/Application\ Support/bb-dpi/bin/bb-vpn codesign -dv ./Applications/BBVPN.app ``` -Expected: `Signature=adhoc` for each. `codesign --verify --deep ---strict` should pass. +Expected: `Signature=adhoc` for each. --- @@ -114,7 +121,17 @@ as a secret of the same blast-radius as the token: leaked path = leaked On each cover-site host that should serve downloads, drop this snippet into the same server block where `/control/bundle.json` lives (NOT -gated by `auth_request`): +gated by `auth_request`). Replace `` with the hex string +from §3a before reload — same pattern as the control-plane bootstrap +`@@TOKEN@@` substitution: + +``` +PATH_HEX=<32-hex from §3a> +awk -v ph="$PATH_HEX" '{gsub("", ph); print}' \ + snippet-template.conf > /etc/nginx/snippets/bb-dpi-downloads.conf +``` + +Template (`snippet-template.conf`): ``` # /etc/nginx/snippets/bb-dpi-downloads.conf @@ -124,8 +141,10 @@ location ^~ /d// { add_header Cache-Control "no-store"; # Same cover fallthrough as /control/bundle.json — any miss/error # leaks to the cover site's natural 404 so the directory itself - # isn't fingerprintable. - error_page 403 404 = @cover_404; + # isn't fingerprintable. Mirror the wide status list from + # nginx-bundle.conf.template so malformed-Authorization edge cases + # (400/413/414) and internal 5xx also funnel through @cover_404. + error_page 400 401 403 404 405 408 413 414 444 500 502 503 504 = @cover_404; } ``` @@ -148,6 +167,16 @@ systemctl reload nginx ### 3c. Drop the files +`` is whatever the cover-site nginx worker runs as +(typically `www-data` on Debian/Ubuntu, `nginx` on RHEL/Alpine, +`http` on Arch). Discover the local value with: + +``` +ps -o group= -p "$(pgrep -f 'nginx: worker' | head -1)" +``` + +Then drop the .pkg into the download dir: + ``` sudo mkdir -p /var/www/bb-dpi-downloads sudo chown -R root: /var/www/bb-dpi-downloads @@ -207,10 +236,21 @@ What postinstall does on top of an existing install: `identity.json` is absent — user clicks the enroll link in the install page to enroll). -sing-box and xray daemons are re-bootstrapped by the new bb-vpn on its -next sync tick (within 15 min, or immediately on a config change), after -its `pre-restart validation` check passes (sing-box `check`, xray -`-test`). Until then the previous version's daemons keep running. +For an already-enrolled user (the upgrade case), postinstall calls +`bb-vpn start` immediately after bootstrapping the sync daemon, so +sing-box and xray are re-bootstrapped within seconds — not on the +next 15-min sync tick. The first sync tick still runs at install time +(`RunAtLoad=true` on the sync daemon) and the daemons go through +pre-restart validation (sing-box `check`, xray `-test`). On a pristine +install (no `identity.json`), postinstall skips `bb-vpn start`; the +user enrolls via the install-page link and the first sync triggers +the daemons. + +To fully remove an existing install before reinstalling (rarely +needed — the .pkg's postinstall is reinstall-safe), the uninstaller +ships in the same payload as `bb-vpn` and lives at +`/Library/Application Support/bb-dpi/bin/bb-vpn-uninstall` (see +[§6 verification](#6-verification) for the one-liner). `control-plane.json` (endpoint URLs + bearer token) is **baked into the .pkg payload** at build time. On reinstall, the new copy replaces the @@ -226,11 +266,11 @@ active install, etc.). Roll out plan: | step | action | est. time | |------|------------------------------------------------------------------|-----------| -| 1 | Mint a new token: `openssl rand -base64 48 \| tr -d '+/=\n' \| cut -c1-64 > config/control-plane/token` | <1 min | -| 2 | Redeploy nginx snippet on every cover-site host: re-substitute `@@TOKEN@@`, reload nginx (see [control-plane-bootstrap.md §2](control-plane-bootstrap.md)) | ~5 min × n hosts | +| 1 | Mint a new token: `openssl rand -base64 48 \| tr -d '+/=\n' \| cut -c1-64 > config/control-plane/token && chmod 600 config/control-plane/token` (the `chmod` re-asserts the 0600 mode from control-plane bootstrap §1b, in case the file was deleted between rotations and a fresh umask write left it world-readable) | <1 min | +| 2 | Redeploy nginx snippet on every cover-site host: re-substitute `@@TOKEN@@`, reload nginx (see [control-plane-bootstrap.md](control-plane-bootstrap.md) §2) | ~5 min × n hosts | | 3 | `make publish-bundle` — push new bundle.json (still uses the old token at this point; the swap is nginx-side) | <1 min | | 4 | `make publish-status` — confirm every endpoint serves the new bundle | <1 min | -| 5 | Bump `package-manifest.json.bb_vpn` patch version (forces the cached `bundle.min_versions` floor to advance) and `make build-pkg` | ~3 min | +| 5 | (Optional) bump `package-manifest.json.bb_vpn` patch version, then `make build-pkg`. The .pkg's job in a rotation is to carry the new control-plane.json token; the version bump is independent of token rotation and only matters if you also want the rotation to force a `bundle.min_versions` floor advance | ~3 min | | 6 | Mint a fresh download path (`openssl rand -hex 16`), update nginx `/d/` snippet on every cover-site host, reload nginx, drop the new .pkg in the new path | ~5 min × n hosts | | 7 | Regenerate per-user install pages with the new `PKG_URL`, host them | ~1 min × n users | | 8 | Slack DM every user: new install URL, deadline (24-48h), "your current install will stop working after this date" | ~15 min × n users (interactive) | @@ -245,26 +285,24 @@ Total dev-machine time for a 7-user fleet on 1 cover-site host: After a fresh build + host: -1. From a **clean Mac** (or wipe `/Library/Application Support/bb-dpi/`, - `/Applications/BBVPN.app`, the launchd plists, and the manually-stopped - flag on a test machine): +1. From a **clean Mac** (or wipe an existing install on a test machine + by running the shipped uninstaller): ``` - rm -rf "/Library/Application Support/bb-dpi" /Applications/BBVPN.app - launchctl bootout system/com.bb-dpi.bb-vpn-sync 2>&1 || true - launchctl bootout system/com.sing-box-vpn 2>&1 || true - launchctl bootout system/com.xray-xhttp 2>&1 || true - rm -f /Library/LaunchDaemons/com.bb-dpi.bb-vpn-sync.plist \ - /Library/LaunchDaemons/com.sing-box-vpn.plist \ - /Library/LaunchDaemons/com.xray-xhttp.plist \ - /Library/LaunchAgents/com.bb-dpi.bb-vpn-menubar.plist + sudo "/Library/Application Support/bb-dpi/bin/bb-vpn-uninstall" ``` + The uninstaller boots out every daemon + LaunchAgent, removes both + `/Library/Application Support/bb-dpi/` and `/Applications/BBVPN.app`, + and clears the LaunchDaemon plists. On a pristine Mac there's + nothing to wipe — skip to step 2. 2. In a browser, open the per-user install page URL. Click the download button. 3. In Finder, right-click `BB-VPN-.pkg` → Open. Gatekeeper warning dialog → "Open" → password prompt → install completes. -4. Open `/Applications/BBVPN.app` (right-click → Open the first time). - The menubar icon appears (grey on a pristine install — not yet - enrolled). +4. Watch for the menubar icon. Postinstall bootstraps the BBVPN + LaunchAgent so the app auto-launches; the icon (grey on a pristine + install — not yet enrolled) should appear within a few seconds. + If it doesn't, open `/Applications/BBVPN.app` once via right-click + → Open to clear Gatekeeper's first-launch prompt. 5. In the install page, click the `bb-vpn://enroll?uuid=...` link. BBVPN.app receives the URL via `LSGetApplicationForURL`, shells out to `bb-vpn enroll`. Menubar icon turns yellow (first sync in flight) @@ -276,6 +314,15 @@ After a fresh build + host: The exit country in the menubar should match the country of the VPN server. +If you need to inspect launchd state directly during verification, +remember system-domain bootout requires root: + +``` +sudo launchctl bootout system/com.bb-dpi.bb-vpn-sync +sudo launchctl bootout system/com.sing-box-vpn +sudo launchctl bootout system/com.xray-xhttp +``` + Subsequent launches don't need right-click → Open. Gatekeeper remembers the user's first-time approval. From 34b262f8aaa6d83c16312af44fd04802456030c7 Mon Sep 17 00:00:00 2001 From: fitz123 <10243861+fitz123@users.noreply.github.com> Date: Thu, 21 May 2026 16:33:15 +0400 Subject: [PATCH 3/3] fix: address Copilot review feedback on PR #28 - install-page Stop/start: use absolute bb-vpn path (sudo strips PATH so ~/.local/bin/bb-vpn shortcut isn't on the search path; same fix already applied to the enroll Terminal fallback). - docs/release.md + AGENTS.md: clarify that --deep only applies to BBVPN.app; standalone bb-vpn/sing-box/xray Mach-Os are signed without --deep (matches what build.sh actually does). Dismissed (not in Phase 6 must-fix scope): HTML-escape of envsubst placeholders (operator-controlled inputs, single-user threat model); token-gen tr -d shortening (pre-existing pattern from control-plane-bootstrap.md, mirrored in release.md for consistency). --- AGENTS.md | 8 +++++--- client/pkg-build/install-page-template.html | 5 +++-- docs/release.md | 7 +++++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a1f15e1..a52a225 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -100,9 +100,11 @@ make test-bb-vpn # Run client/bb-vpn Go tests make build-pkg # Assemble BB-VPN-.pkg (incl. BBVPN.app) in client/pkg-build/dist/ ``` -Phase 6 adds ad-hoc codesigning to `build.sh` (`codesign -s - --force ---deep`; no Apple Developer license, no notarization — Gatekeeper still -shows "unidentified developer" on first install + first launch) and a +Phase 6 adds ad-hoc codesigning to `build.sh` (`codesign -s - --force` +on the standalone bb-vpn/sing-box/xray Mach-Os, `codesign -s - --force +--deep` on `BBVPN.app`; no Apple Developer license, no notarization — +Gatekeeper still shows "unidentified developer" on first install + first +launch) and a user-facing install page template at `client/pkg-build/install-page-template.html`. The operator-facing host/distribute runbook (build, sign, host on a long-random nginx diff --git a/client/pkg-build/install-page-template.html b/client/pkg-build/install-page-template.html index b078de8..c1cf863 100644 --- a/client/pkg-build/install-page-template.html +++ b/client/pkg-build/install-page-template.html @@ -128,8 +128,9 @@

    It's stuck on yellow / grey

    Stop / start

    In Terminal:

    -

    sudo bb-vpn stop — turn the VPN off (survives reboots).
    - sudo bb-vpn start — turn it back on.

    +

    sudo "/Library/Application Support/bb-dpi/bin/bb-vpn" stop — turn the VPN off (survives reboots).
    + sudo "/Library/Application Support/bb-dpi/bin/bb-vpn" start — turn it back on.

    +

    The absolute path is needed because sudo resets $PATH and the ~/.local/bin/bb-vpn shortcut isn't on its search path.

    Uninstall

    If you ever need to remove BB-VPN, in Terminal:

    diff --git a/docs/release.md b/docs/release.md index e84c5b3..d1f316c 100644 --- a/docs/release.md +++ b/docs/release.md @@ -52,8 +52,11 @@ This runs in sequence: on mismatch. - Stages payload under `build/pkg-staging/`. - **Ad-hoc codesigns** `bb-vpn`, `sing-box`, `xray`, and `BBVPN.app` - in place inside the staging tree (`codesign -s - --force --deep`). - No Apple identity, no notarization. The signatures only give each + in place inside the staging tree (`codesign -s - --force` on the + standalone Mach-O binaries; `codesign -s - --force --deep` on + `BBVPN.app` so the bundle's `Contents/MacOS/BBVPN` is signed in + the same invocation). No Apple identity, no notarization. The + signatures only give each binary a stable code-signing identifier so the kernel's library-validation and TCC paths don't trip on "completely unsigned" binaries. Gatekeeper still treats the .pkg and .app as