feat: add native Windows hierarchy test harness
This commit is contained in:
+45
-1
@@ -33,6 +33,8 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
var nativeTestContextFailures map[clientwindows.ExecutionContext]bool
|
||||
|
||||
// run is deliberately a small mode dispatcher. The SCM service invokes only
|
||||
// --service with an explicit config path; tray/helper modes cannot silently
|
||||
// turn an ordinary process invocation into a privileged service.
|
||||
@@ -60,6 +62,7 @@ func run(args []string, output, diagnostics io.Writer) error {
|
||||
start := flags.Bool("start-service", false, "start the machine-wide service")
|
||||
stop := flags.Bool("stop-service", false, "stop the machine-wide service")
|
||||
restart := flags.Bool("restart-service", false, "restart the machine-wide service")
|
||||
testFailContexts := flags.String("test-fail-contexts", "", "fixture-only pre-launch Windows context failures")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -120,6 +123,11 @@ func run(args []string, output, diagnostics io.Writer) error {
|
||||
}
|
||||
return runSignalHelper(*channel, diagnostics)
|
||||
}
|
||||
var testFaultErr error
|
||||
nativeTestContextFailures, testFaultErr = clientwindows.NativeTestContextFailures(*testFailContexts)
|
||||
if testFaultErr != nil {
|
||||
return testFaultErr
|
||||
}
|
||||
return runService(*configPath, diagnostics)
|
||||
}
|
||||
|
||||
@@ -167,7 +175,7 @@ func runClientDaemon(ctx context.Context, configPath string, diagnostics io.Writ
|
||||
limits = agentproto.DefaultLimits()
|
||||
}
|
||||
eventReady := make(chan domain.UUID, 256)
|
||||
supervised, err := clientwindows.NewSupervisor(clientwindows.NativeOptions{Shells: clientwindows.ShellPaths{CMD: configured.Shells.CMD, PowerShell: configured.Shells.PowerShell}, WorkRoot: configured.Client.DaemonCWD, JobProfiles: clientJobProfiles(configured.Profiles), MaxWrapperBytes: configured.Execution.MaxScriptBytes, MaxOutputChunk: configured.Execution.MaxRawChunkBytes, WindowsTermGrace: configured.Execution.WindowsTermGrace})
|
||||
supervised, err := clientwindows.NewSupervisor(clientwindows.NativeOptions{Shells: clientwindows.ShellPaths{CMD: configured.Shells.CMD, PowerShell: configured.Shells.PowerShell}, WorkRoot: configured.Client.DaemonCWD, JobProfiles: clientJobProfiles(configured.Profiles), MaxWrapperBytes: configured.Execution.MaxScriptBytes, MaxOutputChunk: configured.Execution.MaxRawChunkBytes, WindowsTermGrace: configured.Execution.WindowsTermGrace, TestContextFailures: nativeTestContextFailures})
|
||||
if err != nil {
|
||||
return fmt.Errorf("configure command supervisor: %w", err)
|
||||
}
|
||||
@@ -249,10 +257,46 @@ func clientHTTPClient(settings config.TLS) (*http.Client, error) {
|
||||
return nil, errors.New("TLS CA file contains no certificates")
|
||||
}
|
||||
tlsConfig.RootCAs = pool
|
||||
} else {
|
||||
// v1 deliberately permits a self-signed endpoint certificate without a
|
||||
// separately distributed CA. Keep hostname checking: this retains the
|
||||
// useful routing guard while making no claim that the peer is trusted or
|
||||
// authenticated. A configured CA file opts back into normal PKI-only
|
||||
// verification above.
|
||||
tlsConfig.InsecureSkipVerify = true // #nosec G402 -- verified below to admit matching self-signed leaves for v1.
|
||||
tlsConfig.VerifyConnection = verifySystemOrSelfSignedServer
|
||||
}
|
||||
return &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}, nil
|
||||
}
|
||||
|
||||
func verifySystemOrSelfSignedServer(state tls.ConnectionState) error {
|
||||
if len(state.PeerCertificates) == 0 {
|
||||
return errors.New("TLS peer sent no certificates")
|
||||
}
|
||||
leaf := state.PeerCertificates[0]
|
||||
roots, err := x509.SystemCertPool()
|
||||
if err != nil || roots == nil {
|
||||
roots = x509.NewCertPool()
|
||||
}
|
||||
intermediates := x509.NewCertPool()
|
||||
for _, certificate := range state.PeerCertificates[1:] {
|
||||
intermediates.AddCert(certificate)
|
||||
}
|
||||
_, verifyErr := leaf.Verify(x509.VerifyOptions{
|
||||
DNSName: state.ServerName,
|
||||
Roots: roots,
|
||||
Intermediates: intermediates,
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
})
|
||||
if verifyErr == nil {
|
||||
return nil
|
||||
}
|
||||
if err := leaf.CheckSignatureFrom(leaf); err != nil {
|
||||
return verifyErr
|
||||
}
|
||||
return leaf.VerifyHostname(state.ServerName)
|
||||
}
|
||||
|
||||
func clientHello(configured *config.Client, instance domain.UUID) *rvboxv1.ClientHello {
|
||||
platform := rvboxv1.Platform_PLATFORM_LINUX
|
||||
shells := []rvboxv1.ShellType{rvboxv1.ShellType_SHELL_CMD, rvboxv1.ShellType_SHELL_POWERSHELL}
|
||||
|
||||
@@ -2,7 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/rvbox/rvbox/internal/config"
|
||||
)
|
||||
|
||||
func TestClientModeSelectionRequiresExactlyOneMode_HP_WINCLI_01(t *testing.T) {
|
||||
@@ -19,6 +23,43 @@ func TestClientModeSelectionRequiresExactlyOneMode_HP_WINCLI_01(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientHTTPClientAcceptsMatchingSelfSignedLeaf(t *testing.T) {
|
||||
t.Parallel()
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client, err := clientHTTPClient(config.TLS{ServerName: "example.com"})
|
||||
if err != nil {
|
||||
t.Fatalf("clientHTTPClient: %v", err)
|
||||
}
|
||||
response, err := client.Get(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("self-signed request: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusNoContent {
|
||||
t.Fatalf("status = %d, want %d", response.StatusCode, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientHTTPClientRejectsWrongNameSelfSignedLeaf(t *testing.T) {
|
||||
t.Parallel()
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client, err := clientHTTPClient(config.TLS{ServerName: "wrong-name.invalid"})
|
||||
if err != nil {
|
||||
t.Fatalf("clientHTTPClient: %v", err)
|
||||
}
|
||||
if _, err := client.Get(server.URL); err == nil {
|
||||
t.Fatal("self-signed request with wrong name unexpectedly succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonWindowsServiceModesRemainExplicitlyUnsupported_BH_WINCLI_01(t *testing.T) {
|
||||
t.Parallel()
|
||||
if err := runService("", nil); err == nil {
|
||||
|
||||
@@ -26,30 +26,77 @@ func defaultClientConfigPath() string {
|
||||
}
|
||||
|
||||
func runService(configPath string, diagnostics io.Writer) error {
|
||||
earlyDiagnostics, closeDiagnostics := openServiceDiagnostics(configPath, diagnostics)
|
||||
defer closeDiagnostics()
|
||||
_, _ = fmt.Fprintf(earlyDiagnostics, "rvbox service preflight config=%s\n", configPath)
|
||||
inService, err := svc.IsWindowsService()
|
||||
if err != nil {
|
||||
return fmt.Errorf("detect service control manager context: %w", err)
|
||||
err = fmt.Errorf("detect service control manager context: %w", err)
|
||||
_, _ = fmt.Fprintf(earlyDiagnostics, "rvbox service preflight failed: %v\n", err)
|
||||
return err
|
||||
}
|
||||
if !inService {
|
||||
return errors.New("--service is reserved for the installed Windows service")
|
||||
err = errors.New("--service is reserved for the installed Windows service")
|
||||
_, _ = fmt.Fprintf(earlyDiagnostics, "rvbox service preflight failed: %v\n", err)
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintln(earlyDiagnostics, "rvbox service preflight confirmed SCM context")
|
||||
return runWindowsService(configPath, diagnostics)
|
||||
}
|
||||
|
||||
func runWindowsService(configPath string, diagnostics io.Writer) error {
|
||||
return windowsservice.Run(func(ctx context.Context) error {
|
||||
serviceDiagnostics, closeDiagnostics := openServiceDiagnostics(configPath, diagnostics)
|
||||
defer closeDiagnostics()
|
||||
_, _ = fmt.Fprintf(serviceDiagnostics, "rvbox Windows service starting config=%s\n", configPath)
|
||||
err := windowsservice.Run(func(ctx context.Context) error {
|
||||
// The tray endpoint lives in the same LocalSystem service process. It
|
||||
// has no store access; the handler below returns only bounded status/path
|
||||
// data and rechecks SCM authorization in the native pipe adapter.
|
||||
go func() {
|
||||
if err := windowstray.Serve(ctx, func(requestContext context.Context, _ windowstray.Peer, request windowstray.Frame) (windowstray.Frame, error) {
|
||||
return handleTrayRequest(requestContext, configPath, request)
|
||||
}); err != nil && ctx.Err() == nil && diagnostics != nil {
|
||||
_, _ = fmt.Fprintf(diagnostics, "rvbox tray endpoint stopped: %v\n", err)
|
||||
}); err != nil && ctx.Err() == nil {
|
||||
_, _ = fmt.Fprintf(serviceDiagnostics, "rvbox tray endpoint stopped: %v\n", err)
|
||||
}
|
||||
}()
|
||||
return runClientDaemon(ctx, configPath, diagnostics)
|
||||
err := runClientDaemon(ctx, configPath, serviceDiagnostics)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(serviceDiagnostics, "rvbox Windows service startup failed: %v\n", err)
|
||||
}
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(serviceDiagnostics, "rvbox Windows service stopped with error: %v\n", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// openServiceDiagnostics preserves the reason for an early service failure:
|
||||
// a GUI-subsystem executable has no reliable inherited stderr under SCM. The
|
||||
// file is deliberately machine-wide instead of beside the selected config.
|
||||
// LocalSystem can therefore report an access failure to a per-run bundle,
|
||||
// config, or state directory before the normal client logger exists.
|
||||
func openServiceDiagnostics(configPath string, diagnostics io.Writer) (io.Writer, func()) {
|
||||
_ = configPath // Kept in the signature so callers document the selected config.
|
||||
root := os.Getenv("ProgramData")
|
||||
if root == "" {
|
||||
root = `C:\ProgramData`
|
||||
}
|
||||
path := filepath.Join(root, "RVBox", "service-startup.log")
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
if diagnostics == nil {
|
||||
return io.Discard, func() {}
|
||||
}
|
||||
return diagnostics, func() {}
|
||||
}
|
||||
if diagnostics == nil {
|
||||
return file, func() { _ = file.Close() }
|
||||
}
|
||||
// A GUI-subsystem service can inherit an invalid stderr handle from SCM.
|
||||
// MultiWriter stops on its first failed destination, so keep the durable
|
||||
// machine log first and make the inherited diagnostic stream best effort.
|
||||
return io.MultiWriter(file, diagnostics), func() { _ = file.Close() }
|
||||
}
|
||||
|
||||
func runTray(configPath string, diagnostics io.Writer) error {
|
||||
|
||||
Reference in New Issue
Block a user