feat(windows): platform ops, service wrapper, helper, and air-gap paths
First Windows parity milestone (DESIGN-WINDOWS.md §13 build order item 1).
- Add a PlatformOps abstraction so command dispatch is OS-neutral:
- linuxPlatformOps keeps today's systemctl/journalctl/bash behavior (deliberately
untagged so shared dispatch tests run on Windows CI)
- windowsPlatformOps maps reboot/shutdown to shutdown.exe, service control to
sc.exe (stop+start for restart), fetch_logs to Get-WinEvent, arbitrary_bash to
powershell -EncodedCommand (byte-exact under arbitrary quoting), and declines
configure_ldap (Windows logon goes through OpenCredential)
- Run theta-agent as a Windows service (x/sys/windows/svc): SYSTEM auto-start,
SCM stop/shutdown handling; CLI install-service/remove-service via svc/mgr
- Add theta-agent-helper (session-0 companion): lock/display_off/logout via
user32/wtsapi32, and staged self-update (wait for service stop, swap the
locked exe, sc start)
- Self-update becomes platform-aware: Linux renames over the running binary;
Windows stages .new and hands the swap to the helper (running exe is locked)
- Platform paths: agent.yml and tray.sock under %ProgramData%\Theta42 (the
service runs as SYSTEM while the tray runs as the user, so the per-user temp
dir no longer works for tray IPC); LDAP byte-pump falls back to TCP loopback
- config: service_name, desktop_helper, public_ip_detect (air-gap: skips
external public-IP lookups in telemetry + home monitor), wireguard block
- cli: platform-aware config path + self-update artifact name + service restart
- tests: dispatch tests pin linuxPlatformOps; 0600 mode assertions gated to
POSIX so the suite is green on Windows
Rebuilds all tracked dist binaries (v2.1.0).
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
package main
|
||||
|
||||
// linuxPlatformOps implements PlatformOps for Linux (and any POSIX host).
|
||||
// The commands mirror what websocket.go executed before the PlatformOps split;
|
||||
// behaviour is unchanged. Deliberately NOT build-tagged: it compiles on every
|
||||
// OS (it only shells out to systemctl/journalctl/bash), so the shared command
|
||||
// dispatch tests can pin it and run identically on Windows CI.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type linuxPlatformOps struct {
|
||||
exec Executor
|
||||
}
|
||||
|
||||
func (p *linuxPlatformOps) Reboot() ([]byte, error) {
|
||||
return p.exec.Execute("reboot")
|
||||
}
|
||||
|
||||
func (p *linuxPlatformOps) Shutdown() ([]byte, error) {
|
||||
out, err := p.exec.Execute("shutdown", "-h", "now")
|
||||
if err != nil {
|
||||
return p.exec.Execute("poweroff")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (p *linuxPlatformOps) FetchLogs(service string, lines int) ([]byte, error) {
|
||||
return p.exec.Execute("journalctl", "-u", service, "-n", fmt.Sprintf("%d", lines), "--no-pager")
|
||||
}
|
||||
|
||||
func (p *linuxPlatformOps) ServiceControl(service, action string) ([]byte, error) {
|
||||
return p.exec.Execute("systemctl", action, service)
|
||||
}
|
||||
|
||||
func (p *linuxPlatformOps) RunScript(script string) ([]byte, error) {
|
||||
return p.exec.Execute("bash", "-c", script)
|
||||
}
|
||||
|
||||
func (p *linuxPlatformOps) DesktopControl(subAction, targetUser string) ([]byte, error) {
|
||||
switch subAction {
|
||||
case "lock_session", "lock":
|
||||
out, err := p.exec.Execute("loginctl", "lock-sessions")
|
||||
if err != nil {
|
||||
return p.exec.Execute("sh", "-c", "DISPLAY=:0 xdg-screensaver lock || DISPLAY=:0 xset dpms force off")
|
||||
}
|
||||
return out, nil
|
||||
case "logout_user", "logout":
|
||||
if targetUser != "" {
|
||||
out, err := p.exec.Execute("loginctl", "terminate-user", targetUser)
|
||||
if err != nil {
|
||||
return p.exec.Execute("pkill", "-KILL", "-u", targetUser)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
out, err := p.exec.Execute("loginctl", "terminate-session")
|
||||
if err != nil {
|
||||
return p.exec.Execute("pkill", "-9", "-f", "session-child")
|
||||
}
|
||||
return out, nil
|
||||
case "display_off":
|
||||
return p.exec.Execute("sh", "-c", "DISPLAY=:0 xset dpms force off || loginctl lock-sessions")
|
||||
case "sleep_host", "sleep":
|
||||
return p.exec.Execute("systemctl", "suspend")
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown desktop action '%s'", subAction)
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigureLDAP reproduces the original SSSD/nsswitch/PAM configuration flow.
|
||||
func (p *linuxPlatformOps) ConfigureLDAP(configData string) error {
|
||||
log.Println("Pushing updated SSSD configuration...")
|
||||
_ = os.MkdirAll("/etc/sssd", 0755)
|
||||
if err := p.exec.WriteFile("/etc/sssd/sssd.conf", []byte(configData), 0600); err != nil {
|
||||
return fmt.Errorf("failed to write SSSD config: %w", err)
|
||||
}
|
||||
|
||||
// Ensure /etc/nsswitch.conf enables sss for passwd, group, shadow, sudoers
|
||||
if nssBytes, err := os.ReadFile("/etc/nsswitch.conf"); err == nil {
|
||||
nssContent := string(nssBytes)
|
||||
updatedNss := false
|
||||
lines := strings.Split(nssContent, "\n")
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if (strings.HasPrefix(trimmed, "passwd:") || strings.HasPrefix(trimmed, "group:") || strings.HasPrefix(trimmed, "shadow:") || strings.HasPrefix(trimmed, "sudoers:")) && !strings.Contains(trimmed, "sss") {
|
||||
lines[i] = line + " sss"
|
||||
updatedNss = true
|
||||
}
|
||||
}
|
||||
if updatedNss {
|
||||
_ = os.WriteFile("/etc/nsswitch.conf", []byte(strings.Join(lines, "\n")), 0644)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Restarting SSSD service...")
|
||||
if _, err := p.exec.Execute("systemctl", "restart", "sssd"); err != nil {
|
||||
log.Printf("SSSD restart failed (%v), attempting auto-install of missing packages...", err)
|
||||
if _, err2 := p.exec.Execute("sh", "-c", "DEBIAN_FRONTEND=noninteractive apt-get update -y -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq sssd sssd-ldap libnss-sss libpam-sss libsss-sudo libpam-runtime || dnf install -y sssd sssd-ldap sssd-tools || yum install -y sssd sssd-ldap sssd-tools"); err2 == nil {
|
||||
_, _ = p.exec.Execute("sh", "-c", "pam-auth-update --package --enable mkhomedir sss || true")
|
||||
if _, err3 := p.exec.Execute("systemctl", "restart", "sssd"); err3 == nil {
|
||||
writeSSSDSshConf(p.exec)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("failed to restart sssd")
|
||||
}
|
||||
|
||||
writeSSSDSshConf(p.exec)
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeSSSDSshConf installs the AuthorizedKeysCommand config sshd needs to look
|
||||
// up LDAP-backed SSH keys.
|
||||
func writeSSSDSshConf(exec Executor) {
|
||||
_ = os.MkdirAll("/etc/ssh/sshd_config.d", 0755)
|
||||
sshConfPath := "/etc/ssh/sshd_config.d/theta-sssd.conf"
|
||||
sshConfContent := "AuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeys %u\nAuthorizedKeysCommandUser nobody\n"
|
||||
if err := os.WriteFile(sshConfPath, []byte(sshConfContent), 0644); err == nil {
|
||||
_, _ = exec.Execute("systemctl", "reload", "sshd")
|
||||
}
|
||||
if sshdBytes, err2 := os.ReadFile("/etc/ssh/sshd_config"); err2 == nil {
|
||||
sshdStr := string(sshdBytes)
|
||||
if !strings.Contains(sshdStr, "sss_ssh_authorizedkeys") {
|
||||
sshdStr += "\nAuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeys %u\nAuthorizedKeysCommandUser nobody\n"
|
||||
_ = os.WriteFile("/etc/ssh/sshd_config", []byte(sshdStr), 0644)
|
||||
_, _ = exec.Execute("systemctl", "reload", "sshd")
|
||||
}
|
||||
}
|
||||
_, _ = exec.Execute("sh", "-c", "pam-auth-update --package --enable mkhomedir sss || true")
|
||||
}
|
||||
|
||||
func (p *linuxPlatformOps) ApplyUpdate(downloadURL, checksum string) error {
|
||||
tmpPath, err := downloadBinary(downloadURL, checksum)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
selfPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve current binary path: %w", err)
|
||||
}
|
||||
if resolved, err := filepath.EvalSymlinks(selfPath); err == nil {
|
||||
selfPath = resolved
|
||||
}
|
||||
if err := os.Rename(tmpPath, selfPath); err != nil {
|
||||
return fmt.Errorf("failed to replace binary: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *linuxPlatformOps) SelfRestart() {
|
||||
os.Exit(0)
|
||||
}
|
||||
Reference in New Issue
Block a user