Skip to content
Open
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
29 changes: 29 additions & 0 deletions .github/workflows/build-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ on:
installer_base_name:
required: true
type: string
obfuscate_go:
description: "Build Android Go/native libraries through garble"
required: false
type: boolean
default: false
secrets:
GRADLE_PROPERTIES:
required: true
APP_ENV:
required: true
KEYSTORE:
required: true
STEALTH_GARBLE_SEED:
required: false
Comment thread
reflog marked this conversation as resolved.

jobs:
build-android:
Expand Down Expand Up @@ -148,7 +162,22 @@ jobs:
env:
GOMOBILECACHE: ${{ env.GOMOBILECACHE }}

- name: Install garble
if: ${{ inputs.obfuscate_go }}
run: make install-garble
Comment thread
reflog marked this conversation as resolved.

- name: Build Android obfuscated (APK + AAB)
if: ${{ inputs.obfuscate_go }}
run: make android-release-ci-obfuscated
env:
BUILD_TYPE: ${{ inputs.build_type }}
VERSION: ${{ inputs.version }}
INSTALLER_NAME: ${{ inputs.installer_base_name }}
GOMOBILECACHE: ${{ env.GOMOBILECACHE }}
STEALTH_GARBLE_SEED: ${{ secrets.STEALTH_GARBLE_SEED }}
Comment thread
reflog marked this conversation as resolved.

- name: Build Android (APK + AAB)
if: ${{ !inputs.obfuscate_go }}
run: make android-release-ci
env:
BUILD_TYPE: ${{ inputs.build_type }}
Expand Down
122 changes: 121 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,22 @@ GOMOBILE_REPOS = \
./lantern-core/mobile \
./lantern-core/utils

GARBLE ?= garble
GARBLE_VERSION ?= v0.16.0
GARBLE_SEED ?= $(STEALTH_GARBLE_SEED)
GARBLE_FLAGS ?= -literals
GARBLE_GOGARBLE ?= github.com/getlantern/lantern
GARBLE_LDFLAGS ?= -w -s -buildid=

ifeq ($(OS),Windows_NT)
GARBLE_REAL_GO ?= $(shell powershell -NoProfile -ExecutionPolicy Bypass -Command '(Get-Command go -ErrorAction SilentlyContinue).Source')
GARBLE_ENV = $(if $(GARBLE_GOGARBLE),set GOGARBLE=$(GARBLE_GOGARBLE)&& ,set GOGARBLE=&& )
else
GARBLE_REAL_GO ?= $(shell command -v go 2>/dev/null)
GARBLE_ENV = $(if $(GARBLE_GOGARBLE),GOGARBLE="$(GARBLE_GOGARBLE)",env -u GOGARBLE)
endif
GARBLE_BUILD = $(GARBLE) $(GARBLE_FLAGS) -seed="$(GARBLE_SEED)" build

SIGN_ID="Developer ID Application: Brave New Software Project, Inc (ACZRKC3LQ9)"

define osxcodesign
Expand All @@ -170,6 +186,33 @@ guard-%:
check-gomobile:
@command -v gomobile >/dev/null || (echo "gomobile not found. Run 'make install-android-deps'" && exit 1)

.PHONY: check-garble require-garble-seed check-garble-seed check-garble-go
check-garble:
@command -v $(GARBLE) >/dev/null 2>&1 || \
{ echo "garble not found. Run 'make install-garble' or set GARBLE=/path/to/garble."; exit 1; }
@command -v git >/dev/null 2>&1 || \
{ echo "git not found. garble requires git to patch the Go linker."; exit 1; }
@$(GARBLE) -h >/dev/null 2>&1 || \
{ echo "garble was found but could not run. Check GARBLE=$(GARBLE) and your Go toolchain."; exit 1; }

require-garble-seed:
@if [ -z "$(GARBLE_SEED)" ]; then \
echo "GARBLE_SEED is required for obfuscated builds."; \
echo "Set GARBLE_SEED=<base64 profile seed> or STEALTH_GARBLE_SEED=<base64 profile seed>."; \
echo "Use GARBLE_SEED=random only for unreproducible local smoke builds."; \
exit 1; \
fi

check-garble-seed: check-garble require-garble-seed
@$(GARBLE) -seed="$(GARBLE_SEED)" version >/dev/null 2>&1 || \
{ echo "GARBLE_SEED must be 'random' or a base64-encoded seed accepted by garble."; exit 1; }

check-garble-go:
@if [ -z "$(GARBLE_REAL_GO)" ]; then \
echo "go not found. Install Go or set GARBLE_REAL_GO=/path/to/go for gomobile garble builds."; \
exit 1; \
fi


.PHONY: require-appdmg
require-appdmg:
Expand All @@ -196,6 +239,15 @@ desktop-lib:
-o $(LIB_NAME) ./$(FFI_DIR)
@echo "Built desktop library: $(LIB_NAME)"

.PHONY: desktop-lib-obfuscated
desktop-lib-obfuscated: check-garble-seed
$(call MKDIR_P,$(dir $(LIB_NAME)))
@$(SETENV) $(GARBLE_ENV) $(GARBLE_BUILD) -v -trimpath -buildmode=c-shared \
-tags="$(TAGS)" \
-ldflags="$(GARBLE_LDFLAGS) $(EXTRA_LDFLAGS)" \
-o $(LIB_NAME) ./$(FFI_DIR)
Comment thread
reflog marked this conversation as resolved.
@echo "Built obfuscated desktop library: $(LIB_NAME)"

# macOS build tools need to be installed when generating release builds,
# but are not necessarily required for debug builds
.PHONY: install-macos-deps
Expand Down Expand Up @@ -301,18 +353,32 @@ linux-arm64: $(LINUX_LIB_ARM64)
$(LINUX_LIB_ARM64): $(GO_SOURCES)
CC=$(LINUX_CC_ARM64) GOOS=linux GOARCH=arm64 LIB_NAME=$@ $(MAKE) desktop-lib

.PHONY: linux-arm64-obfuscated
linux-arm64-obfuscated: $(GO_SOURCES)
CC=$(LINUX_CC_ARM64) GOOS=linux GOARCH=arm64 LIB_NAME=$(LINUX_LIB_ARM64) $(MAKE) desktop-lib-obfuscated

.PHONY: linux-amd64
linux-amd64: $(LINUX_LIB_AMD64)

$(LINUX_LIB_AMD64): $(GO_SOURCES)
CC=$(LINUX_CC_AMD64) GOOS=linux GOARCH=amd64 LIB_NAME=$@ $(MAKE) desktop-lib

.PHONY: linux-amd64-obfuscated
linux-amd64-obfuscated: $(GO_SOURCES)
CC=$(LINUX_CC_AMD64) GOOS=linux GOARCH=amd64 LIB_NAME=$(LINUX_LIB_AMD64) $(MAKE) desktop-lib-obfuscated

.PHONY: linux
linux: linux-$(LINUX_TARGET_ARCH)
mkdir -p $(BIN_DIR)/linux
cp $(BIN_DIR)/linux-$(LINUX_TARGET_ARCH)/$(LINUX_LIB) $(LINUX_LIB_BUILD)

.PHONY: lanternd-linux-amd64 lanternd-linux-arm64
.PHONY: linux-obfuscated
linux-obfuscated: linux-$(LINUX_TARGET_ARCH)-obfuscated
mkdir -p $(BIN_DIR)/linux
cp $(BIN_DIR)/linux-$(LINUX_TARGET_ARCH)/$(LINUX_LIB) $(LINUX_LIB_BUILD)

.PHONY: lanternd-linux-amd64 lanternd-linux-arm64 \
lanternd-linux-amd64-obfuscated lanternd-linux-arm64-obfuscated

lanternd-linux-amd64: $(GO_SOURCES)
$(call MKDIR_P,$(dir $(LANTERND_LINUX_AMD64)))
Expand All @@ -330,6 +396,22 @@ lanternd-linux-arm64: $(GO_SOURCES)
-o $(LANTERND_LINUX_ARM64) $(LANTERND_SRC)
@echo "Built lanternd (linux-arm64): $(LANTERND_LINUX_ARM64)"

lanternd-linux-amd64-obfuscated: check-garble-seed $(GO_SOURCES)
$(call MKDIR_P,$(dir $(LANTERND_LINUX_AMD64)))
@$(GARBLE_ENV) GOOS=linux GOARCH=amd64 CGO_ENABLED=1 \
$(GARBLE_BUILD) -mod=mod -v -trimpath -tags "$(TAGS)" \
-ldflags "$(GARBLE_LDFLAGS) $(EXTRA_LDFLAGS)" \
-o $(LANTERND_LINUX_AMD64) $(LANTERND_SRC)
@echo "Built obfuscated lanternd (linux-amd64): $(LANTERND_LINUX_AMD64)"

lanternd-linux-arm64-obfuscated: check-garble-seed $(GO_SOURCES)
$(call MKDIR_P,$(dir $(LANTERND_LINUX_ARM64)))
@$(GARBLE_ENV) GOOS=linux GOARCH=arm64 CGO_ENABLED=1 \
$(GARBLE_BUILD) -mod=mod -v -trimpath -tags "$(TAGS)" \
-ldflags "$(GARBLE_LDFLAGS) $(EXTRA_LDFLAGS)" \
-o $(LANTERND_LINUX_ARM64) $(LANTERND_SRC)
@echo "Built obfuscated lanternd (linux-arm64): $(LANTERND_LINUX_ARM64)"

.PHONY: linux-debug
linux-debug:
@echo "Building Flutter app (debug) for Linux..."
Expand Down Expand Up @@ -453,6 +535,10 @@ install-gomobile:
echo "Skipping gomobile init (cached for $(GO_VERSION))"; \
fi

.PHONY: install-garble
install-garble:
GOTOOLCHAIN=$(GO_VERSION) go install -v mvdan.cc/garble@$(GARBLE_VERSION)

# Android Build
.PHONY: check-android-sdk
check-android-sdk:
Expand Down Expand Up @@ -497,6 +583,34 @@ build-android: check-android-sdk check-gomobile
cp $(ANDROID_LIB_BUILD) $(ANDROID_LIBS_DIR)
@echo "Built Android library: $(ANDROID_LIBS_DIR)/$(ANDROID_LIB)"

.PHONY: android-obfuscated
android-obfuscated: build-android-obfuscated

.PHONY: build-android-obfuscated
build-android-obfuscated: check-android-sdk check-gomobile check-garble-seed check-garble-go
@echo "Building obfuscated Android libraries..."
Comment thread
reflog marked this conversation as resolved.
rm -rf $(ANDROID_LIB_BUILD) $(ANDROID_LIBS_DIR)/$(ANDROID_LIB)
mkdir -p $(dir $(ANDROID_LIB_BUILD)) $(ANDROID_LIBS_DIR)

@PATH="$(CURDIR)/scripts/garble-go:$$PATH" \
GARBLE_REAL_GO="$(GARBLE_REAL_GO)" \
GARBLE_BIN="$(GARBLE)" \
GARBLE_SEED="$(GARBLE_SEED)" \
GARBLE_FLAGS="$(GARBLE_FLAGS)" \
GARBLE_GOGARBLE="$(GARBLE_GOGARBLE)" \
GOMOBILECACHE="$(GOMOBILECACHE)" \
GOTOOLCHAIN=$(GO_VERSION) GOOS=android gomobile bind -v \
-androidapi=23 \
-target="$(GOMOBILE_ANDROID_TARGET)" \
-javapkg=lantern.io \
-tags=$(TAGS) -trimpath \
-o=$(ANDROID_LIB_BUILD) \
-ldflags="$(ANDROID_GOMOBILE_LDFLAGS) $(GARBLE_LDFLAGS) $(EXTRA_LDFLAGS)" \
$(GOMOBILE_REPOS)

cp $(ANDROID_LIB_BUILD) $(ANDROID_LIBS_DIR)
@echo "Built obfuscated Android library: $(ANDROID_LIBS_DIR)/$(ANDROID_LIB)"

.PHONY: android-debug
android-debug: $(ANDROID_DEBUG_BUILD)

Expand Down Expand Up @@ -530,6 +644,12 @@ android-release: clean android pubget gen android-apk-release
.PHONY: android-release-ci
android-release-ci: android pubget gen android-apk-release android-aab-release

.PHONY: android-release-obfuscated
android-release-obfuscated: clean android-obfuscated pubget gen android-apk-release

.PHONY: android-release-ci-obfuscated
android-release-ci-obfuscated: android-obfuscated pubget gen android-apk-release android-aab-release

# iOS Build
.PHONY: install-ios-deps

Expand Down
110 changes: 110 additions & 0 deletions docs/stealth-go-garble.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Stealth Go/native obfuscation

Stealth Go/native builds are opt-in. Normal Makefile targets continue to use
`go build` and `gomobile bind` directly.

## Inputs

- `GARBLE_SEED` or `STEALTH_GARBLE_SEED` is required for every obfuscated
target. Use a base64-encoded seed from the stealth profile so support can
reproduce the build and use `garble reverse` when needed.
`GARBLE_SEED=random` is acceptable only for local, unreproducible smoke
builds.
- `GARBLE_FLAGS` defaults to `-literals`. Add `-tiny` only after accepting the
loss of useful panic and stack trace output.
- `GARBLE_LDFLAGS` defaults to `-w -s -buildid=` to strip symbol/debug tables
and the Go build ID from obfuscated artifacts.
- `GARBLE_GOGARBLE` defaults to `github.com/getlantern/lantern` so release
builds obfuscate local Go packages without forcing garble through every
dependency. Full dependency obfuscation can be attempted with
`GARBLE_GOGARBLE=*`, but dependency compatibility should be validated before
using that scope for a shipped artifact.

Treat release seeds as private support material. The Makefile suppresses command
echo for seed-bearing garble invocations, but release automation should still
keep the profile seed in a secret store alongside any private release metadata
needed for `garble reverse`. Record the exact `GARBLE_VERSION` and Go toolchain
version with each release. The Makefile defaults `GARBLE_VERSION` to `v0.16.0`
so CI and support builds use a reproducible tool version unless explicitly
overridden.

Install garble:

```sh
make install-garble
```

## Android

Build the Android AAR with garble and then produce the APK/AAB:

```sh
STEALTH_GARBLE_SEED="$PROFILE_GARBLE_SEED" make android-release-ci-obfuscated
```

`gomobile bind` invokes `go build` internally, so the obfuscated target prepends
`scripts/garble-go` to `PATH`. That wrapper delegates non-build commands to the
real Go binary and runs only gomobile's internal `go build` through `garble`.

Reusable workflow callers can opt in without changing normal Android releases:

```yaml
jobs:
build-android-stealth:
uses: ./.github/workflows/build-android.yml
secrets: inherit
with:
version: ${{ needs.set-metadata.outputs.version }}
build_type: stealth
installer_base_name: lantern-installer
obfuscate_go: true
```

The workflow consumes `secrets.STEALTH_GARBLE_SEED` when `obfuscate_go` is true.
This workflow does not generate Android app identities or stealth profiles.

## Other native targets

Linux shared library:

```sh
STEALTH_GARBLE_SEED="$PROFILE_GARBLE_SEED" make linux-obfuscated
```

Linux daemon:

```sh
STEALTH_GARBLE_SEED="$PROFILE_GARBLE_SEED" make lanternd-linux-amd64-obfuscated
STEALTH_GARBLE_SEED="$PROFILE_GARBLE_SEED" make lanternd-linux-arm64-obfuscated
```

Desktop C shared library for a specific platform:

```sh
STEALTH_GARBLE_SEED="$PROFILE_GARBLE_SEED" \
GOOS=darwin GOARCH=arm64 LIB_NAME=bin/macos-arm64/liblantern.dylib \
make desktop-lib-obfuscated
```

## ABI and support constraints

These boundaries are externally visible and their public names cannot be
obfuscated without breaking consumers:

- Gomobile binding packages in `GOMOBILE_REPOS`:
`github.com/sagernet/sing-box/experimental/libbox`,
`./lantern-core/mobile`, and `./lantern-core/utils`. Java/Kotlin bindings
are generated from exported Go APIs, and garble currently keeps exported
methods/functions visible.
- Desktop FFI package `./lantern-core/ffi`. Every `//export` function in
`lantern-core/ffi/ffi.go` is a C ABI symbol used by Flutter FFI and generated
headers; examples include `setup`, `startVPN`, `stopVPN`, `login`, `logout`,
and `freeCString`.
- Crash and support tooling. `lantern-core/utils.RunOffCgoStack` records
`runtime/debug.Stack`; garble `-tiny` removes runtime panic and trace output,
so it should not be the default for supportable builds.

Validation before shipping a stealth artifact should include Android connect,
auth, config fetch, and proxy/no-VPN smoke tests, plus a check that protobuf
marshal/unmarshal paths and gomobile exported calls still work with the selected
`GARBLE_GOGARBLE` scope.
52 changes: 52 additions & 0 deletions scripts/garble-go/go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail

real_go="${GARBLE_REAL_GO:-}"
garble_bin="${GARBLE_BIN:-garble}"
seed="${GARBLE_SEED:-}"

if [[ -z "$real_go" ]]; then
echo "garble go wrapper: GARBLE_REAL_GO is not set" >&2
exit 2
fi

if [[ ! -x "$real_go" ]]; then
echo "garble go wrapper: GARBLE_REAL_GO is not executable: $real_go" >&2
exit 2
fi

if [[ $# -eq 0 ]]; then
exec "$real_go"
fi

case "$1" in
build)
if [[ -z "$seed" ]]; then
echo "garble go wrapper: GARBLE_SEED is required for obfuscated go build" >&2
exit 2
fi
if ! command -v "$garble_bin" >/dev/null 2>&1; then
echo "garble go wrapper: garble not found. Set GARBLE_BIN or run 'make install-garble'." >&2
exit 2
fi

if [[ -n "${GARBLE_GOGARBLE:-}" ]]; then
export GOGARBLE="$GARBLE_GOGARBLE"
else
unset GOGARBLE
fi

garble_flags=()
if [[ -n "${GARBLE_FLAGS:-}" ]]; then
# GARBLE_FLAGS is intentionally simple make/CI input such as "-literals -tiny".
# Split on shell whitespace without pathname expansion.
read -r -a garble_flags <<< "${GARBLE_FLAGS}"
fi

real_go_dir="$(dirname "$real_go")"
exec env PATH="$real_go_dir:$PATH" "$garble_bin" "${garble_flags[@]}" -seed="$seed" "$@"
;;
*)
exec "$real_go" "$@"
;;
esac
Loading