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.
+
+ - Right-click (or Control-click) the file.
+ - Choose Open from the menu.
+ - macOS will show a warning: “${PKG_NAME} cannot be opened because Apple cannot check it for malicious software.” Click Open.
+ - 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.
+
+
+
+
+
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
+
+ - Grey: not enrolled yet — repeat step 4.
+ - Yellow with “syncing”: wait 15-30 seconds, the first sync is in flight.
+ - Yellow with an error: click the icon, copy the error line, and DM the operator.
+
+
+
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