From 3e675aaabf2066dd4dafa6f51a77c1f7c9f03111 Mon Sep 17 00:00:00 2001 From: cabbage Date: Sun, 6 Sep 2026 06:12:41 +0000 Subject: [PATCH] feat: discover active Windows sessions --- docs/implementation-plan.v1.md | 8 ++ .../supervisor/windows/discovery_windows.go | 100 ++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 internal/client/supervisor/windows/discovery_windows.go diff --git a/docs/implementation-plan.v1.md b/docs/implementation-plan.v1.md index 206f290..6a5fd85 100644 --- a/docs/implementation-plan.v1.md +++ b/docs/implementation-plan.v1.md @@ -805,6 +805,14 @@ Guest Control only with `--passwordfile`. The current VirtualBox `7.2.16` Guest Control build supports `--wait-stdout` and `--wait-stderr`, but not `--wait-exit`; adapters must not pass that unsupported flag. +For a guest `cmd.exe` command payload, this Guest Control build requires +`--unquoted-args` so the controller preserves Windows backslashes and the +single `/c` command payload. Treat each Guest Control process as a bounded, +owned resource: record its guest session/PID, close it after completion, and +use `closeprocess` before reset if an output wait does not complete. Do not use +this compatibility rule to assemble untrusted command text; it is solely the +adapter transport for prevalidated test commands and exact executable paths. + Use a local, non-secret environment description when operating the lane. The host alias must resolve through the operator's SSH config; another controller may substitute an equivalent target, but must record the resulting host/VM diff --git a/internal/client/supervisor/windows/discovery_windows.go b/internal/client/supervisor/windows/discovery_windows.go new file mode 100644 index 0000000..663b55c --- /dev/null +++ b/internal/client/supervisor/windows/discovery_windows.go @@ -0,0 +1,100 @@ +//go:build windows + +package windows + +import ( + "fmt" + "unsafe" + + "golang.org/x/sys/windows" +) + +const ( + wtsCurrentServerHandle windows.Handle = 0 + invalidSessionID = ^uint32(0) +) + +// DiscoverActiveSessions translates only verified active WTS user tokens into +// selector DTOs. It is intended for the LocalSystem service: WTSQueryUserToken +// requires LocalSystem and SeTcbPrivilege. A token-query failure leaves that +// WTS session unusable rather than guessing an identity. +func DiscoverActiveSessions() ([]SessionCandidate, error) { + var sessions *windows.WTS_SESSION_INFO + var count uint32 + if err := windows.WTSEnumerateSessions(wtsCurrentServerHandle, 0, 1, &sessions, &count); err != nil { + return nil, fmt.Errorf("enumerate WTS sessions: %w", err) + } + if sessions == nil { + return nil, nil + } + defer windows.WTSFreeMemory(uintptr(unsafe.Pointer(sessions))) + + console := windows.WTSGetActiveConsoleSessionId() + result := make([]SessionCandidate, 0, count) + for _, session := range unsafe.Slice(sessions, count) { + if session.State != windows.WTSActive { + continue + } + candidate, err := candidateFromSession(session.SessionID, console) + if err == nil { + result = append(result, candidate) + } + } + return result, nil +} + +func candidateFromSession(sessionID, console uint32) (SessionCandidate, error) { + var token windows.Token + if err := windows.WTSQueryUserToken(sessionID, &token); err != nil { + return SessionCandidate{}, fmt.Errorf("query user token for session %d: %w", sessionID, err) + } + defer token.Close() + + user, err := token.GetTokenUser() + if err != nil || user.User.Sid == nil { + return SessionCandidate{}, fmt.Errorf("query token user for session %d: %w", sessionID, err) + } + logonSID, err := tokenLogonSID(token) + if err != nil { + return SessionCandidate{}, fmt.Errorf("query token logon SID for session %d: %w", sessionID, err) + } + facts := tokenFacts(token) + if !facts.Usable { + return SessionCandidate{}, fmt.Errorf("token for session %d is not usable", sessionID) + } + return SessionCandidate{ + SessionID: sessionID, Console: console != invalidSessionID && sessionID == console, + UserSID: user.User.Sid.String(), LogonSID: logonSID, Token: facts, + }, nil +} + +func tokenFacts(token windows.Token) TokenFacts { + facts := TokenFacts{Usable: true} + if token.IsElevated() { + facts.FullAdministrator = true + return facts + } + // WTSQueryUserToken normally returns the user's standard/filtered primary + // token. The launch adapter re-verifies integrity and can create a + // restricted medium token if a legacy full-only token is encountered. + facts.StandardOrFiltered = true + linked, err := token.GetLinkedToken() + if err == nil { + facts.LinkedFullAvailable = linked.IsElevated() + _ = linked.Close() + } + return facts +} + +func tokenLogonSID(token windows.Token) (string, error) { + groups, err := token.GetTokenGroups() + if err != nil { + return "", err + } + for _, group := range groups.AllGroups() { + if group.Sid != nil && group.Attributes&windows.SE_GROUP_LOGON_ID == windows.SE_GROUP_LOGON_ID { + return group.Sid.String(), nil + } + } + return "", fmt.Errorf("token has no logon SID") +}