feat: add native Windows hierarchy test harness

This commit is contained in:
2026-09-09 16:48:54 +00:00
parent 8985457d37
commit 409b64a2fb
30 changed files with 1033 additions and 44 deletions
+45 -1
View File
@@ -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}
+41
View File
@@ -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 {
+53 -6
View File
@@ -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 {
+5
View File
@@ -39,6 +39,11 @@ use the defaults documented by the all-knob client reference.
- The daemon prints its effective configuration after validation, with no
command data or TLS material. Since v1 stores command/environment payloads in
plaintext, configuration output is hygiene rather than a secrecy guarantee.
- With an empty `tls.ca_file`, the client accepts a matching-host self-signed
server leaf. This provides TLS encryption and hostname routing only; v1 makes
no server-authentication or endpoint-ownership guarantee. Supplying a PEM CA
bundle restores normal CA-chain verification and is the preferred future
deployment model.
## Limits and cross-field validation
+2 -1
View File
@@ -17,7 +17,8 @@ max_queued_commands = 100
shutdown_grace = "30s"
[tls]
# Optional PEM CA bundle; empty uses the operating-system trust store.
# Optional PEM CA bundle. Empty permits a matching-host self-signed leaf in v1;
# this encrypts transport but does not authenticate server ownership.
ca_file = ""
# Optional certificate name override; empty derives it from server_url.
server_name = ""
+2 -1
View File
@@ -17,7 +17,8 @@ max_queued_commands = 100
shutdown_grace = "30s"
[tls]
# Optional PEM CA bundle; empty uses the Windows trust store.
# Optional PEM CA bundle. Empty permits a matching-host self-signed leaf in v1;
# this encrypts transport but does not authenticate server ownership.
ca_file = ""
# Optional certificate-name override; empty derives it from server_url.
server_name = ""
+39
View File
@@ -740,6 +740,45 @@ side effect must add a hook and coverage row before merging.
### 2.6 Native Windows test-host requirements
#### 2.6.0 Implemented current-controller native hierarchy lane
Implement scripts/windows/native-test as the first-class production-shaped
native gate. One run must create an immutable run directory, compile a
separately tagged fixture-only rvbox.exe in the pinned Docker toolchain, and
run the Linux server and nginx fixture from test/linux-server in a labeled
Docker Compose project on the current controller. Helium hosts only the
Windows VM and the VirtualBox Guest Control bridge: no RVBox server, proxy,
or Compose resource may be staged or run there. The local stack must create a
two-day matching-host self-signed leaf for the explicit current-controller
endpoint (x1.xcel.me by default, overridable by
RVBOX_NATIVE_ENDPOINT_HOST). It binds the configured test port on the current
controller and copies only rvbox.exe and client configuration to the Windows
guest test root.
The Windows service must connect through that WSS endpoint; controller-side
success is not enough. With no tls.ca_file configured, the v1 client must
accept the matching-host self-signed leaf while retaining hostname validation;
that is encrypted routing only and makes no claim that the endpoint is
authenticated. Wait for the client to become connected through the local
server's actual Unix rvc socket, issue real CMD commands, wait for durable
terminal success, and retain the resulting command status records. Verify,
in order: normal ACTIVE_USER; elevated ACTIVE_USER_ELEVATED; forced
ACTIVE_USER_ELEVATED preparation failure falling back to ACTIVE_SYSTEM; forced
ACTIVE_USER_ELEVATED plus ACTIVE_SYSTEM failure falling back to LOCAL_SYSTEM;
then log off the sole fixture console session and verify normal LOCAL_SERVICE
and elevated LOCAL_SYSTEM. The forced faults must be available only in the
separately tagged fixture binary, only before child-process creation, and only
for these two active elevated contexts. They are not a protocol field, TOML
knob, release-build behavior, or product broker.
On ordinary success collect bounded guest and local server/proxy artifacts,
remove only that labeled Compose project and its volumes, and restore the exact
snapshot. On failure retain the exact local stack and VM lease for recover;
clean must reset the fixture and retain artifacts, while an explicit purge with
a confirmation removes only the matching local run root. Add static
tests for the runner's file layout and tagged-build boundary, and promote the
native coverage row only after this lane passes on the documented VM.
Native Windows is mandatory for the Phase 4/5 gates. Development can begin with
unit tests and cross-compilation before a host is connected, but the Windows
supervisor/service/tray implementation cannot be called complete without it.
+59
View File
@@ -174,6 +174,65 @@ connectivity and CA-trust probe. The resulting service and artifact paths are
recorded in the run report and reclaimed by the snapshot reset rather than broad
guest deletion.
The first-class end-to-end controller is scripts/windows/native-test run
--run-id ID. It runs the per-run Linux server and nginx stack in Docker Compose
under test/linux-server on the current controller. Helium hosts only the
Windows VM and VirtualBox Guest Control bridge. The client connects to the
explicit current-controller WSS endpoint (x1.xcel.me by default), never the
DHCP guest address, and intentionally accepts that endpoint's matching-host
self-signed certificate in v1. The controller drives requests with the local
stack's rvc, collects bounded artifacts, and deletes just its labeled Compose
project during cleanup.
The test bundle never sends a Windows executable over the controller-to-Helium
SSH hop. The tagged fixture build strips symbol and DWARF tables; `test-host
stage` creates a temporary `xz -3` payload, publishes it to the authenticated
controller HTTP endpoint, and directs Helium to download it through its SOCKS5
acceleration proxy. Helium resumes the HTTP payload, verifies its compressed
SHA-256, atomically decompresses it into the exact host stage, then validates
the ordinary uncompressed bundle manifest before Guest Control copies files
into Windows. The baseline Helium host must provide `curl` and `xz`. Small
non-secret `client.toml` and optional CA files still use bounded-retry SSH
copies; executable staging fails closed if the accelerated route is unavailable.
Each successful stage prints `verified transfer_sha256` and the verified
manifest. No manual remote checksum check is needed for a normal or resumed
test run. The controller uses bounded SSH retries (four attempts, short
backoff) for idempotent inspection, staging, installation, and service-run
operations. Reset, stop, and logoff intentionally remain single-attempt;
after an interrupted lifecycle transition, use `recover` and decide whether to
resume or reset instead of replaying an ambiguous action.
For this controller/Helium fixture, the required executable route publishes
only the stripped, compressed test executable under the current controller's
Downloads HTTP endpoint
(`http://x1.xcel.me:9124`, authentication `x1:x1`) and asks Helium to fetch it
through its metered `socks5h://127.0.0.1:1085` proxy. The endpoint currently
advertises Basic authentication, so the harness deliberately uses curl's
`--anyauth` negotiation rather than assuming Digest. It never publishes
`client.toml`, CA material, passwords, or other configuration. Helium resumes
the HTTP download, verifies the compressed SHA-256, atomically decompresses it
into its stage, and the ordinary manifest check still runs before Guest
Control copies files into Windows. The uniquely named published artifact is
removed after successful guest staging (and on a subsequently failed stage).
`RVBOX_TEST_ACCEL_HTTP_URL`, `RVBOX_TEST_ACCEL_HTTP_AUTH`,
`RVBOX_TEST_ACCEL_HTTP_PUBLISH_DIR`, and `RVBOX_TEST_ACCEL_SOCKS5` override the
documented fixture defaults.
Early Windows-service failures are appended to
`C:\ProgramData\RVBox\service-startup.log`, before client config, durable
state, or normal observability logging begins. This is intentionally outside a
per-run bundle directory so the LocalSystem service can report an ACL/path
failure affecting that directory.
To prove the complete elevated fallback chain in one single-user fixture, the
controller builds a separately tagged disposable rvbox.exe. Its only extra
behavior is the internal --test-fail-contexts switch, which can force
ACTIVE_USER_ELEVATED and then ACTIVE_SYSTEM token preparation to fail before
launch. Release binaries reject the switch. The normal active-user,
active-user-elevated, active-system, local-system, local-service, and
logged-out local-system rows are therefore observed through the real SCM
service without adding a product broker or protocol field.
### Clean baseline and non-interactive installation
`rvboxtest` deliberately remains a split-token administrator. Guest Control
+39
View File
@@ -56,6 +56,37 @@ The bundle manifest records the source commit and SHA-256 of every bundled
file. It is a test artifact, not a signed release package; release signing,
version resources, and publication are Phase 8 gates.
The preferred complete native lane is now one command:
~~~sh
scripts/windows/native-test run --run-id windows-hierarchy
~~~
It builds a disposable fixture-tagged Windows binary in the pinned toolchain,
starts the real Linux server and nginx TLS proxy in Docker Compose under
test/linux-server on the current controller, and connects the Helium-hosted
NAT guest to the current controller's explicit endpoint (x1.xcel.me by
default). Helium hosts the VM only; it does not host any RVBox server
containers. The self-signed server certificate is intentionally accepted by
the v1 client without a test CA. It then drives the installed SCM service
through the server's real Unix control socket and verifies every Windows
execution context. The tagged binary's controlled pre-launch failures are
limited to the test fixture; a release binary rejects that switch.
Successful runs collect bounded artifacts, remove only their labeled Compose
project, and restore the exact clean snapshot. A failed or --keep run stays
recoverable:
~~~sh
scripts/windows/native-test recover --run-id windows-hierarchy
scripts/windows/native-test clean --run-id windows-hierarchy
scripts/windows/native-test clean --run-id windows-hierarchy --purge --yes
~~~
clean retains local artifacts by default. The explicit purge form removes only
the exact local run root after the local stack is down and the VM snapshot has
been restored.
The integration harness provides the Phase 0 `sample` suite, the incremental
Phase 2 `store` suite, and the incremental Phase 3 `server-session` suite.
The resumable E2E harness adds `smoke`, `script`, `recovery`, and `all`
@@ -180,6 +211,14 @@ also performs a bounded guest-to-nginx HTTPS/TCP readiness probe before it
starts the service. Wine and protocol stubs are not equivalent Windows
coverage.
For the hierarchy fixture only, run --fail-contexts
ACTIVE_USER_ELEVATED[,ACTIVE_SYSTEM] restarts the separately tagged test
service and forces the named token preparation step to fail before process
creation. This proves the real daemon's fallback order without changing the
wire protocol, normal client TOML, or release binary. The logoff action ends
the sole active fixture session so the LocalService and no-user LocalSystem
rows can be tested with the same running service.
The clean baseline intentionally contains no RVBox service, tray registration,
or RVBox state. Guest Control supplies `rvboxtest` with a filtered medium UAC
token, so it cannot safely perform the first machine-wide install. The fixture
+4 -1
View File
@@ -37,7 +37,10 @@ func syncDirectory(path string) error {
if err != nil {
return err
}
handle, err := windows.CreateFile(value, windows.GENERIC_READ, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_BACKUP_SEMANTICS, 0)
// FlushFileBuffers requires GENERIC_WRITE even when the handle refers to a
// directory. Opening it read-only succeeds, then deterministically fails
// the durability barrier with ERROR_ACCESS_DENIED on Windows.
handle, err := windows.CreateFile(value, windows.GENERIC_READ|windows.GENERIC_WRITE, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_BACKUP_SEMANTICS, 0)
if err != nil {
return err
}
+2 -4
View File
@@ -18,11 +18,9 @@ const privateStateSDDL = "D:P(A;;FA;;;SY)(A;;FA;;;BA)"
func ensurePrivateFile(path string) error {
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0o600)
if err == nil {
err = file.Close()
} else if !errors.Is(err, os.ErrExist) {
return err
return file.Close()
}
if err != nil {
if !errors.Is(err, os.ErrExist) {
return err
}
info, err := os.Lstat(path)
@@ -0,0 +1,18 @@
//go:build windows
package spool
import (
"path/filepath"
"testing"
)
func TestEnsurePrivateFileAcceptsExistingPrivateFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "spool.lock")
if err := ensurePrivateFile(path); err != nil {
t.Fatalf("create private file: %v", err)
}
if err := ensurePrivateFile(path); err != nil {
t.Fatalf("recheck existing private file: %v", err)
}
}
+13 -2
View File
@@ -11,6 +11,7 @@ import (
"net/url"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
@@ -116,8 +117,7 @@ func Open(ctx context.Context, options Options) (*Store, error) {
query.Add("_pragma", "foreign_keys(ON)")
query.Add("_pragma", "synchronous(FULL)")
query.Add("_pragma", "busy_timeout("+strconv.FormatInt(options.BusyTimeout.Milliseconds(), 10)+")")
databaseURL := &url.URL{Scheme: "file", Path: filepath.ToSlash(databasePath), RawQuery: query.Encode()}
db, err := sql.Open("sqlite", databaseURL.String())
db, err := sql.Open("sqlite", sqliteFileURI(databasePath, query))
if err != nil {
_ = unlock()
return nil, err
@@ -136,6 +136,17 @@ func Open(ctx context.Context, options Options) (*Store, error) {
return store, nil
}
// sqliteFileURI creates a file: URI with no authority. SQLite requires a
// Windows drive path to be /C:/... in the URI path; without the leading slash
// net/url renders C: as an authority (file://C:/...), which SQLite rejects.
func sqliteFileURI(path string, query url.Values) string {
path = strings.ReplaceAll(path, `\`, "/")
if len(path) >= 2 && path[1] == ':' {
path = "/" + path
}
return (&url.URL{Scheme: "file", Path: path, RawQuery: query.Encode()}).String()
}
func (store *Store) ClientInstanceID() domain.UUID { return store.identity }
func (store *Store) Close() error {
+8
View File
@@ -5,6 +5,7 @@ import (
"context"
"crypto/sha256"
"errors"
"net/url"
"os"
"path/filepath"
"testing"
@@ -16,6 +17,13 @@ import (
"google.golang.org/protobuf/proto"
)
func TestSQLiteFileURIWindowsDrivePath(t *testing.T) {
query := url.Values{"_pragma": {"journal_mode(WAL)"}}
if got, want := sqliteFileURI(`C:\ProgramData\RVBox\test-state\spool.db`, query), "file:///C:/ProgramData/RVBox/test-state/spool.db?_pragma=journal_mode%28WAL%29"; got != want {
t.Fatalf("sqliteFileURI() = %q, want %q", got, want)
}
}
func TestExecutionSpecIsDurableAndQuotaCounted_HP_DISPATCH_07(t *testing.T) {
t.Parallel()
ctx := context.Background()
@@ -43,7 +43,13 @@ type NativeOptions struct {
MaxWrapperBytes uint64
MaxOutputChunk uint64
WindowsTermGrace time.Duration
Now func() time.Time
// TestContextFailures is populated only by the separately tagged native
// fixture binary. It is deliberately not protocol or TOML policy: it lets
// the fixture fail token preparation before launch so the real daemon can
// prove its fallback order without creating a second broker or weakening a
// release binary.
TestContextFailures map[ExecutionContext]bool
Now func() time.Time
}
// JobProfile is the validated administrator policy for one protocol profile.
@@ -388,6 +388,10 @@ func (manager *execSupervisor) selectToken(elevated bool) (winapi.Token, supervi
}
for _, attempt := range selection.Attempts {
token, identity, err := openTokenForAttempt(attempt.Context, selected)
if err == nil && manager.options.TestContextFailures[attempt.Context] {
_ = token.Close()
err = errors.New("native test fixture forced pre-launch context failure")
}
if err == nil {
return token, withEvidence(identity), nil
}
@@ -396,6 +400,33 @@ func (manager *execSupervisor) selectToken(elevated bool) (winapi.Token, supervi
return 0, supervisor.EffectiveIdentity{}, rejection(err)
}
}
// Select stops at the first policy-available elevated context. Native
// preparation can still fail after that point (a linked token can vanish or
// SeTcbPrivilege can be unavailable), so preserve the documented order by
// trying ACTIVE_SYSTEM before the final LOCAL_SYSTEM fallback. This is a
// preparation fallback only: no child has been created yet.
if elevated && selected != nil {
seenActiveSystem := false
for _, contextName := range attempted {
if contextName == string(ContextActiveSystem) {
seenActiveSystem = true
break
}
}
if !seenActiveSystem {
addAttempt(ContextActiveSystem, "fallback after active-user-elevated preparation failure")
if token, identity, err := openTokenForAttempt(ContextActiveSystem, selected); err == nil {
if manager.options.TestContextFailures[ContextActiveSystem] {
_ = token.Close()
addAttempt(ContextActiveSystem, "native test fixture forced pre-launch context failure")
} else {
return token, withEvidence(identity), nil
}
} else {
addAttempt(ContextActiveSystem, "native preparation failed: "+err.Error())
}
}
}
// The pure selector stops as soon as ACTIVE_SYSTEM is available. A native
// privilege/session operation can still fail (for example, SeTcb was
// removed), so the final LOCAL_SYSTEM fallback is attempted here before
@@ -0,0 +1,15 @@
//go:build !rvbox_native_test
package windows
import "fmt"
// NativeTestContextFailures is intentionally unavailable in shipped binaries.
// The native fixture compiles a separately tagged test executable when it
// needs to prove a pre-launch fallback row.
func NativeTestContextFailures(raw string) (map[ExecutionContext]bool, error) {
if raw != "" {
return nil, fmt.Errorf("--test-fail-contexts requires the rvbox_native_test fixture build")
}
return nil, nil
}
@@ -0,0 +1,28 @@
//go:build rvbox_native_test
package windows
import (
"fmt"
"strings"
)
// NativeTestContextFailures accepts only the two active elevated preparation
// stages. It is compiled into the disposable native-fixture binary, never a
// release binary, and is applied before a process is launched.
func NativeTestContextFailures(raw string) (map[ExecutionContext]bool, error) {
if raw == "" {
return nil, nil
}
result := make(map[ExecutionContext]bool)
for _, item := range strings.Split(raw, ",") {
contextName := ExecutionContext(strings.TrimSpace(item))
switch contextName {
case ContextActiveUserElevated, ContextActiveSystem:
result[contextName] = true
default:
return nil, fmt.Errorf("unsupported native test context %q", item)
}
}
return result, nil
}
@@ -74,8 +74,13 @@ func newTrayPipe() (*os.File, error) {
Length: uint32(unsafe.Sizeof(winapi.SecurityAttributes{})),
SecurityDescriptor: descriptor,
}
mode := uint32(winapi.PIPE_ACCESS_DUPLEX | winapi.PIPE_TYPE_MESSAGE | winapi.PIPE_READMODE_MESSAGE | winapi.PIPE_WAIT | winapi.PIPE_REJECT_REMOTE_CLIENTS | winapi.SECURITY_IDENTIFICATION)
handle, err := winapi.CreateNamedPipe(name, mode, pipeInstances, pipeBufferBytes, pipeBufferBytes, 0, 0, attributes)
// CreateNamedPipe has separate open-mode and pipe-mode arguments. Remote
// client rejection belongs to the latter; placing it in open mode produces
// ERROR_INVALID_PARAMETER on Windows. We inspect the client token directly
// and never impersonate it, so no SQOS/impersonation flag is needed here.
openMode := uint32(winapi.PIPE_ACCESS_DUPLEX)
pipeMode := uint32(winapi.PIPE_TYPE_MESSAGE | winapi.PIPE_READMODE_MESSAGE | winapi.PIPE_WAIT | winapi.PIPE_REJECT_REMOTE_CLIENTS)
handle, err := winapi.CreateNamedPipe(name, openMode, pipeMode, pipeInstances, pipeBufferBytes, pipeBufferBytes, 0, attributes)
if err != nil {
return nil, err
}
+11
View File
@@ -49,6 +49,17 @@ func TestAnnotatedExamples_HP_CFG_01(t *testing.T) {
}
}
func TestClientDefaultsDecodeForWindowsWithoutNativeFilesystem(t *testing.T) {
t.Parallel()
client, err := DecodeClient([]byte("[client]\nstate_dir = \"C:\\\\ProgramData\\\\RVBox\\\\state\"\ndaemon_cwd = \"C:\\\\ProgramData\\\\RVBox\\\\work\"\n"), ClientOptions{Platform: PlatformWindows, CheckFilesystem: false})
if err != nil {
t.Fatalf("DecodeClient Windows defaults: %v", err)
}
if client.Shells.SH != "/bin/sh" || client.Shells.CMD == "" {
t.Fatalf("Windows defaults have unexpected shell paths: %+v", client.Shells)
}
}
func TestStrictTOMLRejections_HP_CFG_01(t *testing.T) {
t.Parallel()
+6 -2
View File
@@ -3,6 +3,7 @@ package config
import (
"fmt"
"os"
"path"
"path/filepath"
"runtime"
"strings"
@@ -21,10 +22,13 @@ func validateAbsolutePath(name, value string, platform Platform, allowEmpty bool
}
return cleanWindowsPath(value), nil
}
if !filepath.IsAbs(value) {
// Config parsing can validate an inactive Unix shell path while running on
// Windows. filepath follows the host OS, whereas the config field's stated
// platform is Unix, so use slash-path semantics here.
if !path.IsAbs(value) {
return "", fmt.Errorf("%s must be an absolute Unix path", name)
}
return filepath.Clean(value), nil
return path.Clean(value), nil
}
func isWindowsAbsolute(value string) bool {
+17 -1
View File
@@ -7,7 +7,7 @@ repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
usage() {
cat <<'EOF'
usage: scripts/windows/build-test-bundle --run-id ID --config FILE [--ca FILE] [--no-build]
usage: scripts/windows/build-test-bundle --run-id ID --config FILE [--ca FILE] [--no-build] [--native-fixture]
Builds bin/rvbox.exe in the pinned Docker toolchain, then creates exactly:
.test-runs/ID/windows-bundle/{rvbox.exe,client.toml[,ca.pem],manifest.sha256}
@@ -15,6 +15,10 @@ Builds bin/rvbox.exe in the pinned Docker toolchain, then creates exactly:
The bundle is for the resettable native-test fixture only. The config must use
the target guest paths and the declared test nginx endpoint. Existing bundles
are refused rather than overwritten.
--native-fixture compiles the Windows executable with the rvbox_native_test
tag. That non-release binary alone accepts --test-fail-contexts, used only to
prove pre-launch elevation fallback order in the disposable VM.
EOF
}
@@ -24,12 +28,14 @@ run_id=
config=
ca=
build=yes
native_fixture=no
while [ "$#" -gt 0 ]; do
case $1 in
--run-id) [ "$#" -ge 2 ] || fail "--run-id needs a value"; run_id=$2; shift 2 ;;
--config) [ "$#" -ge 2 ] || fail "--config needs a file"; config=$2; shift 2 ;;
--ca) [ "$#" -ge 2 ] || fail "--ca needs a PEM file"; ca=$2; shift 2 ;;
--no-build) build=no; shift ;;
--native-fixture) native_fixture=yes; shift ;;
--help|-h) usage; exit 0 ;;
*) fail "unknown argument $1" ;;
esac
@@ -48,6 +54,15 @@ esac
if [ -n "$ca" ]; then [ -f "$ca" ] && [ ! -L "$ca" ] || fail "--ca must be a regular non-symlink file"; fi
if [ "$build" = yes ]; then "$repo_root/scripts/build" build; fi
if [ "$native_fixture" = yes ]; then
# Keep the server/rvc artifacts produced by scripts/build, but replace only
# the disposable test client with its explicitly tagged fixture build.
# No host Go toolchain is used. Strip symbol/DWARF tables because this
# disposable binary is transferred to the remote VM fixture; this does not
# change executable behavior or the tagged test boundary.
docker compose -f "$repo_root/deploy/compose.yaml" run --rm toolchain \
env GOOS=windows GOARCH=amd64 go build -trimpath -tags rvbox_native_test -ldflags '-H=windowsgui -s -w' -o bin/rvbox.exe ./cmd/rvbox
fi
binary=$repo_root/bin/rvbox.exe
[ -f "$binary" ] && [ ! -L "$binary" ] || fail "expected regular Windows binary at bin/rvbox.exe"
@@ -66,6 +81,7 @@ commit=$(git -C "$repo_root" rev-parse HEAD)
{
printf 'run_id=%s\n' "$run_id"
printf 'git_commit=%s\n' "$commit"
printf 'native_fixture=%s\n' "$native_fixture"
(cd "$bundle" && sha256sum rvbox.exe client.toml ${ca:+ca.pem})
} >"$bundle/manifest.sha256"
chmod 600 "$bundle/manifest.sha256"
+221
View File
@@ -0,0 +1,221 @@
#!/bin/sh
# Production-shaped Linux server/nginx -> native Windows client test lane.
set -eu
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
usage() {
cat <<'EOF'
usage: scripts/windows/native-test run|recover|clean --run-id ID [options]
run options:
--port PORT Current-controller TLS port (default RVBOX_NATIVE_PORT or 16899)
--keep retain the stack/VM lease for inspection
recover reports the exact VM lease and Compose resources.
clean stops only the matching stack and resets the matching VM run.
--purge --yes also delete only the matching local and Helium run files
run uses a separately tagged disposable fixture binary to prove every Windows
execution context through SCM, nginx WSS, the Linux server, and rvc. Release
binaries reject the fixture-only pre-launch failure switch.
EOF
}
fail() { printf '%s\n' "native-test: $*" >&2; exit 2; }
safe_id() {
case $1 in [a-z0-9]* ) ;; *) fail "run ID must start with lowercase alphanumeric" ;; esac
case $1 in ''|*[!a-z0-9-]*|????????????????????????????????????????????????????????????????*) fail "run ID must match [a-z0-9][a-z0-9-]{0,63}" ;; esac
}
action=${1-}
[ -n "$action" ] || { usage >&2; exit 2; }
shift
case $action in run|recover|clean|--help|-h) ;; *) usage >&2; fail "unknown action $action" ;; esac
[ "$action" != --help ] && [ "$action" != -h ] || { usage; exit 0; }
run_id=
port=${RVBOX_NATIVE_PORT:-16899}
keep=no
purge=no
yes=no
while [ "$#" -gt 0 ]; do
case $1 in
--run-id) [ "$#" -ge 2 ] || fail "--run-id needs a value"; run_id=$2; shift 2 ;;
--port) [ "$#" -ge 2 ] || fail "--port needs a value"; port=$2; shift 2 ;;
--keep) keep=yes; shift ;;
--purge) purge=yes; shift ;;
--yes) yes=yes; shift ;;
--help|-h) usage; exit 0 ;;
*) fail "unknown argument $1" ;;
esac
done
[ -n "$run_id" ] || fail "$action requires --run-id"
safe_id "$run_id"
case $port in *[!0-9]*|'') fail "--port must be an integer" ;; esac
[ "$port" -ge 1024 ] && [ "$port" -le 65535 ] || fail "--port must be 1024..65535"
[ "$purge" = no ] || [ "$action" = clean ] || fail "--purge is only valid with clean"
[ "$yes" = no ] || [ "$action" = clean ] || fail "--yes is only valid with clean"
[ "$purge" = no ] || [ "$yes" = yes ] || fail "--purge requires --yes"
: "${RVBOX_TEST_VBOX_HOST:=helium-remote}"
case $RVBOX_TEST_VBOX_HOST in ''|*[!A-Za-z0-9._:@-]*) fail "RVBOX_TEST_VBOX_HOST contains unsupported characters" ;; esac
endpoint_host=${RVBOX_NATIVE_ENDPOINT_HOST:-x1.xcel.me}
case $endpoint_host in ''|*[!A-Za-z0-9.-]*) fail "RVBOX_NATIVE_ENDPOINT_HOST contains unsupported characters" ;; esac
project=rvbox-native-$run_id
run_root=$repo_root/.test-runs/$run_id
fixture_dir=$run_root/native-windows
client_id=native-$run_id
client_config=$fixture_dir/client.toml
server_config=$fixture_dir/server.toml
vm_prepared=no
compose() {
RVBOX_NATIVE_RUN_ID="$run_id" RVBOX_NATIVE_PORT="$port" \
RVBOX_NATIVE_BIND=0.0.0.0 RVBOX_NATIVE_UID="$(id -u)" RVBOX_NATIVE_GID="$(id -g)" \
RVBOX_NATIVE_RUNTIME_DIR="$fixture_dir" RVBOX_NATIVE_ENDPOINT_HOST="$endpoint_host" \
docker compose -p "$project" -f "$repo_root/test/linux-server/compose.yaml" "$@"
}
rvc() {
compose exec -T server /opt/rvbox/rvc --socket /run/rvbox/server.sock "$@"
}
prepare_files() {
umask 077
mkdir -p "$fixture_dir"
[ ! -e "$server_config" ] || fail "refusing to overwrite existing $server_config"
[ ! -e "$client_config" ] || fail "refusing to overwrite existing $client_config"
printf '%s\n' '[server]' 'data_dir = "/state/data"' 'agent_listen = "0.0.0.0:6899"' 'control_socket = "/run/rvbox/server.sock"' '' '[observability]' 'listen = "0.0.0.0:6901"' >"$server_config"
printf '%s\n' \
'[client]' \
"server_url = \"wss://$endpoint_host:$port/v1/agent\"" \
'state_dir = "C:\\ProgramData\\RVBox\\test-state"' \
"client_id = \"$client_id\"" \
'daemon_cwd = "C:\\ProgramData\\RVBox\\test-work"' \
'' '[tls]' \
'# Empty intentionally exercises v1 matching-host self-signed TLS.' \
'ca_file = ""' \
"server_name = \"$endpoint_host\"" \
'' '[observability]' \
'listen = "127.0.0.1:6902"' \
'log_file = "C:\\ProgramData\\RVBox\\test-logs\\rvbox.log"' >"$client_config"
chmod 600 "$server_config" "$client_config"
}
stage_stack() {
# scripts/build intentionally execs its Docker command. Keep that process
# replacement inside a subshell so this lifecycle controller continues.
("$repo_root/scripts/build" build)
install -d -m 700 "$fixture_dir/pki" "$fixture_dir/state/data" "$fixture_dir/state/control"
compose --profile tools run --rm certgen
compose up -d server nginx
attempt=0
while [ "$attempt" -lt 30 ]; do
if rvc stat >/dev/null 2>&1; then return 0; fi
attempt=$((attempt + 1)); sleep 1
done
compose logs --tail=200
fail "server control socket did not become ready"
}
wait_client() {
attempt=0
while [ "$attempt" -lt 45 ]; do
state=$(rvc stat "$client_id" 2>/dev/null || true)
if printf '%s\n' "$state" | grep -q "client $client_id connected=true"; then return 0; fi
attempt=$((attempt + 1)); sleep 1
done
compose logs --tail=200
fail "native Windows client did not connect through nginx WSS"
}
assert_context() {
label=$1
elevated=$2
want=$3
if [ "$elevated" = yes ]; then
issued=$(rvc run --background --shell cmd --elevated "$client_id" "echo RVBOX_NATIVE_$label" 2>&1) || fail "$label admission failed: $issued"
else
issued=$(rvc run --background --shell cmd "$client_id" "echo RVBOX_NATIVE_$label" 2>&1) || fail "$label admission failed: $issued"
fi
issue=$(printf '%s\n' "$issued" | awk 'NR == 1 { print $1 }')
case $issue in ????????-????-7???-????-????????????) ;; *) fail "$label returned invalid issue UUID: $issued" ;; esac
attempt=0
while [ "$attempt" -lt 45 ]; do
result=$(rvc stat "$client_id" "$issue" 2>/dev/null || true)
if printf '%s\n' "$result" | grep -q 'lifecycle=COMMAND_SUCCEEDED'; then
printf '%s\n' "$result" | grep -q "windows_effective_context=$want" || fail "$label effective context mismatch: $result"
printf '%s\n' "$result" >"$fixture_dir/$label.stat"
printf 'passed %s issue=%s context=%s\n' "$label" "$issue" "$want"
return 0
fi
case $result in *'lifecycle=COMMAND_FAILED'*|*'lifecycle=COMMAND_REJECTED'*|*'lifecycle=COMMAND_TERMINATED'*) fail "$label did not succeed: $result" ;; esac
attempt=$((attempt + 1)); sleep 1
done
fail "$label did not reach terminal success"
}
collect() {
if [ "$vm_prepared" = yes ]; then
"$repo_root/scripts/windows/test-host" collect --run-id "$run_id" || true
fi
if [ -d "$fixture_dir" ]; then
compose logs --no-color --tail=500 >"$fixture_dir/server-proxy.log" 2>&1 || true
fi
}
clean() {
compose down --volumes --remove-orphans || true
# A reset is the isolation boundary for the next run. Do not conceal a
# failed shutdown/snapshot restore behind a successful-looking `clean`:
# callers must repair or explicitly inspect the retained VM lease first.
"$repo_root/scripts/windows/test-host" reset --run-id "$run_id"
if [ "$purge" = yes ]; then
[ -L "$run_root" ] && fail "refusing symlink run root $run_root"
rm -rf "$run_root"
fi
}
case $action in
recover)
"$repo_root/scripts/windows/test-host" recover --run-id "$run_id"
compose ps
printf 'run_root=%s\n' "$run_root"
;;
clean)
collect
clean
printf 'cleaned run_id=%s\n' "$run_id"
;;
run)
trap 'status=$?; if [ "$status" -ne 0 ]; then collect; fi' EXIT
[ ! -e "$run_root" ] || fail "refusing to reuse existing run root $run_root; inspect with recover or remove with clean --purge --yes"
prepare_files
stage_stack
"$repo_root/scripts/windows/build-test-bundle" --native-fixture --run-id "$run_id" --config "$client_config"
"$repo_root/scripts/windows/test-host" prepare --run-id "$run_id"
vm_prepared=yes
"$repo_root/scripts/windows/test-host" stage --run-id "$run_id" --bundle "$run_root/windows-bundle"
"$repo_root/scripts/windows/test-host" install --run-id "$run_id"
"$repo_root/scripts/windows/test-host" run --run-id "$run_id" --endpoint "$endpoint_host:$port"
wait_client
assert_context active-user no active-user
assert_context active-user-elevated yes active-user-elevated
"$repo_root/scripts/windows/test-host" run --run-id "$run_id" --fail-contexts ACTIVE_USER_ELEVATED
wait_client
assert_context active-system yes active-system
"$repo_root/scripts/windows/test-host" run --run-id "$run_id" --fail-contexts ACTIVE_USER_ELEVATED,ACTIVE_SYSTEM
wait_client
assert_context local-system-active-fallback yes local-system
"$repo_root/scripts/windows/test-host" run --run-id "$run_id"
wait_client
"$repo_root/scripts/windows/test-host" logoff --run-id "$run_id"
assert_context local-service no local-service
assert_context local-system-no-user yes local-system
collect
if [ "$keep" = no ]; then clean; fi
printf 'native Windows hierarchy run passed: %s\n' "$run_id"
;;
esac
+214 -20
View File
@@ -12,7 +12,7 @@ repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
usage() {
cat <<'EOF'
usage: scripts/windows/test-host ACTION [--run-id ID] [--bundle DIRECTORY] [--endpoint HOST:PORT]
usage: scripts/windows/test-host ACTION [--run-id ID] [--bundle DIRECTORY] [--endpoint HOST:PORT] [--fail-contexts LIST]
Actions:
status read-only VM/snapshot identity and state check
@@ -20,7 +20,9 @@ Actions:
stage copy a bundle containing rvbox.exe and client.toml into the guest test root
install install and start RVBox from the staged bundle through a fixture-only full-admin principal
run start the already-installed RVBox SCM service from the staged bundle
logoff log off the sole active fixture user; use only after service installation
collect copy bounded guest artifacts to the local test-run directory
inspect read-only RVBox SCM state and bounded client log from a prepared run
stop stop RVBox through SCM and request a graceful guest shutdown
reset stop the guest if necessary, restore the declared baseline, and leave it off
recover read-only fixture/run-state check for a stopped-resumable run
@@ -34,6 +36,9 @@ Optional environment:
(default Administrator and the documented fixture password file)
RVBOX_TEST_HOST_STAGE_ROOT (default /home/cabbage/.local/state/rvbox-test-runs)
RVBOX_TEST_RUN_ROOT (default .test-runs/windows-vm)
RVBOX_TEST_ACCEL_HTTP_URL, RVBOX_TEST_ACCEL_HTTP_AUTH,
RVBOX_TEST_ACCEL_HTTP_PUBLISH_DIR, RVBOX_TEST_ACCEL_SOCKS5
(documented accelerated HTTP stage route; empty URL disables it)
EOF
}
@@ -70,17 +75,19 @@ shift
run_id=
bundle=
endpoint=
fail_contexts=
while [ "$#" -gt 0 ]; do
case $1 in
--run-id) [ "$#" -ge 2 ] || fail "--run-id needs a value"; run_id=$2; shift 2 ;;
--bundle) [ "$#" -ge 2 ] || fail "--bundle needs a value"; bundle=$2; shift 2 ;;
--endpoint) [ "$#" -ge 2 ] || fail "--endpoint needs a value"; endpoint=$2; shift 2 ;;
--fail-contexts) [ "$#" -ge 2 ] || fail "--fail-contexts needs a value"; fail_contexts=$2; shift 2 ;;
--help|-h) usage; exit 0 ;;
*) fail "unknown argument $1" ;;
esac
done
case $action in status|prepare|stage|install|run|collect|stop|reset|recover) ;; *) usage >&2; fail "unknown action $action" ;; esac
case $action in status|prepare|stage|install|run|logoff|collect|inspect|stop|reset|recover) ;; *) usage >&2; fail "unknown action $action" ;; esac
if [ "$action" != status ]; then
[ -n "$run_id" ] || fail "$action requires --run-id"
safe_id "$run_id"
@@ -91,6 +98,7 @@ if [ "$action" = stage ]; then
[ -f "$bundle/client.toml" ] || fail "bundle must contain client.toml"
fi
if [ -n "$endpoint" ]; then safe_word endpoint "$endpoint"; fi
if [ -n "$fail_contexts" ]; then safe_word fail_contexts "$fail_contexts"; fi
# The Helium smoke fixture is the only supported native lane today. Keep its
# non-secret identity and host-local password-file *path* here so a developer
@@ -106,8 +114,13 @@ if [ -n "$endpoint" ]; then safe_word endpoint "$endpoint"; fi
: "${RVBOX_TEST_GUEST_PASSWORD_FILE:=/home/cabbage/.local/share/rvbox-secrets/rvbox-win10-test.password}"
: "${RVBOX_TEST_PROVISIONER_USER:=Administrator}"
: "${RVBOX_TEST_PROVISIONER_PASSWORD_FILE:=$RVBOX_TEST_GUEST_PASSWORD_FILE}"
: "${RVBOX_TEST_ACCEL_HTTP_URL:=http://x1.xcel.me:9124}"
: "${RVBOX_TEST_ACCEL_HTTP_AUTH:=x1:x1}"
: "${RVBOX_TEST_ACCEL_HTTP_PUBLISH_DIR:=/home/ubuntu/Downloads}"
: "${RVBOX_TEST_ACCEL_SOCKS5:=socks5h://127.0.0.1:1085}"
provisioner_user=$RVBOX_TEST_PROVISIONER_USER
provisioner_password_file=$RVBOX_TEST_PROVISIONER_PASSWORD_FILE
accelerated_artifact=
for name in RVBOX_TEST_VBOX_HOST RVBOX_TEST_VBOX_VM RVBOX_TEST_VBOX_VM_UUID \
RVBOX_TEST_VBOX_SNAPSHOT RVBOX_TEST_VBOX_SNAPSHOT_UUID \
@@ -124,24 +137,49 @@ safe_word RVBOX_TEST_GUEST_USER "$RVBOX_TEST_GUEST_USER"
safe_word RVBOX_TEST_GUEST_PASSWORD_FILE "$RVBOX_TEST_GUEST_PASSWORD_FILE"
if [ -n "$provisioner_user" ]; then safe_word RVBOX_TEST_PROVISIONER_USER "$provisioner_user"; fi
if [ -n "$provisioner_password_file" ]; then safe_word RVBOX_TEST_PROVISIONER_PASSWORD_FILE "$provisioner_password_file"; fi
if [ -n "$RVBOX_TEST_ACCEL_HTTP_URL" ]; then
safe_word RVBOX_TEST_ACCEL_HTTP_URL "$RVBOX_TEST_ACCEL_HTTP_URL"
safe_word RVBOX_TEST_ACCEL_HTTP_AUTH "$RVBOX_TEST_ACCEL_HTTP_AUTH"
safe_word RVBOX_TEST_ACCEL_HTTP_PUBLISH_DIR "$RVBOX_TEST_ACCEL_HTTP_PUBLISH_DIR"
safe_word RVBOX_TEST_ACCEL_SOCKS5 "$RVBOX_TEST_ACCEL_SOCKS5"
case $RVBOX_TEST_ACCEL_HTTP_URL in http://*|https://*) ;; *) fail "RVBOX_TEST_ACCEL_HTTP_URL must use http(s)" ;; esac
case $RVBOX_TEST_ACCEL_SOCKS5 in socks5://*|socks5h://*) ;; *) fail "RVBOX_TEST_ACCEL_SOCKS5 must use socks5" ;; esac
fi
host_stage_root=${RVBOX_TEST_HOST_STAGE_ROOT:-/home/cabbage/.local/state/rvbox-test-runs}
run_root=${RVBOX_TEST_RUN_ROOT:-$repo_root/.test-runs/windows-vm}
safe_word RVBOX_TEST_HOST_STAGE_ROOT "$host_stage_root"
remote_run_id=${run_id:-fixture-status}
host_stage=$host_stage_root/$remote_run_id
guest_root="C:\\ProgramData\\RVBox\\test-runs\\$remote_run_id"
# This value crosses a remote POSIX shell before Guest Control. Windows accepts
# forward slashes, which avoids backslash loss while SSH reconstructs argv.
guest_root="C:/ProgramData/RVBox/test-runs/$remote_run_id"
remote() {
# All values below are constrained words before becoming remote shell
# arguments. Password contents are never transmitted or printed; only the
# approved host-local password-file path is passed to VBoxManage.
# arguments. Password contents are never transmitted or printed; only the
# approved host-local password-file path is passed to VBoxManage. Execute
# the helper through this one SSH connection: the former upload-then-run
# scheme could race with a stale controller that removed the shared helper
# filename between those two connections.
remote_action=$1
retry_limit=1
# These operations are either read-only or converge on the same staged
# artifact/service configuration. A lost SSH response is therefore safe to
# retry. Lifecycle transitions remain single-attempt: their caller must
# inspect/recover rather than risk a duplicate reset, shutdown, or logoff.
case $remote_action in
status|recover|prepare-stage|stage|stage-create-root|stage-copy-exe|stage-copy-config|stage-copy-ca|collect|inspect|install|run)
retry_limit=4
;;
esac
remote_endpoint=${endpoint:--}
remote_script=/home/cabbage/.local/state/rvbox-test-controller/$remote_run_id.sh
ssh -o BatchMode=yes "$RVBOX_TEST_VBOX_HOST" \
"install -d -m 700 /home/cabbage/.local/state/rvbox-test-controller && cat > '$remote_script' && chmod 700 '$remote_script'" <<'REMOTE'
remote_fail_contexts=${fail_contexts:--}
retry_attempt=1
while [ "$retry_attempt" -le "$retry_limit" ]; do
if ssh -o BatchMode=yes -o ConnectTimeout=10 -o ServerAliveInterval=10 -o ServerAliveCountMax=2 "$RVBOX_TEST_VBOX_HOST" \
"sh -s -- '$1' '$RVBOX_TEST_VBOX_VM' '$RVBOX_TEST_VBOX_VM_UUID' '$RVBOX_TEST_VBOX_SNAPSHOT' '$RVBOX_TEST_VBOX_SNAPSHOT_UUID' '$RVBOX_TEST_GUEST_USER' '$RVBOX_TEST_GUEST_PASSWORD_FILE' '$host_stage' '$guest_root' '$remote_endpoint' '$provisioner_user' '$provisioner_password_file' '$remote_fail_contexts'" <<'REMOTE'
set -eu
trap 'rm -f "$0"' EXIT
action=$1
vm=$2
@@ -156,7 +194,9 @@ shift 9
endpoint=$1
provisioner_user=$2
provisioner_password_file=$3
fail_contexts=$4
[ "$endpoint" = - ] && endpoint=
[ "$fail_contexts" = - ] && fail_contexts=
fail() { printf '%s\n' "remote test-host: $*" >&2; exit 2; }
@@ -289,6 +329,19 @@ wait_service() {
fail "RVBoxClient did not reach $wanted"
}
wait_service_stopped() {
attempt=0
while [ "$attempt" -lt 30 ]; do
if guest_run --exe 'C:\\Windows\\System32\\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
/d /s /c 'sc.exe query RVBoxClient | findstr /c:"STOPPED" >NUL && echo RVBOX_GUEST_OK' >/dev/null 2>&1; then
return 0
fi
attempt=$((attempt + 1))
sleep 1
done
fail "RVBoxClient did not reach STOPPED"
}
case "$action" in
prepare-stage)
assert_identity
@@ -332,8 +385,8 @@ case "$action" in
assert_identity
require_lease
[ "$(state)" = running ] || fail "stage requires a running prepared VM"
guest_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
/d /s /c "if not exist \"$guest_root\" mkdir \"$guest_root\" & echo RVBOX_GUEST_OK" >/dev/null
guest_run --exe 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' --wait-stdout --wait-stderr --unquoted-args -- \
-NoProfile -NonInteractive -Command "New-Item -ItemType Directory -Force -Path '$guest_root','C:/ProgramData/RVBox/test-work','C:/ProgramData/RVBox/test-logs' | Out-Null; Write-Output RVBOX_GUEST_OK" >/dev/null
;;
stage-copy-exe)
assert_identity
@@ -384,7 +437,14 @@ case "$action" in
guest_run --exe 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' --wait-stdout --wait-stderr --unquoted-args -- \
-NoProfile -NonInteractive -Command "if (-not (Test-NetConnection -ComputerName '$endpoint_host' -Port $endpoint_port -InformationLevel Quiet)) { exit 1 }; Write-Output RVBOX_GUEST_OK" >/dev/null
fi
image="\\\"$guest_root\\rvbox.exe\\\" --service --config \\\"$guest_root\\client.toml\\\""
# Reconfigure from a stopped service so a controlled fixture fault cannot
# leak across hierarchy rows. The release build rejects this argument;
# only the separately tagged disposable test binary accepts it.
provisioner_run --exe 'C:\\Windows\\System32\\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
/d /s /c '(sc.exe stop RVBoxClient >NUL 2>&1 || sc.exe query RVBoxClient | findstr /c:"STOPPED" >NUL) && echo RVBOX_GUEST_OK' >/dev/null || true
wait_service_stopped
image="\\\"$guest_root\\rvbox.exe\\\" --service --config \\\"$guest_root\\client.toml\\\""
if [ -n "$fail_contexts" ]; then image="$image --test-fail-contexts $fail_contexts"; fi
provisioner_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
/d /s /c "sc.exe query RVBoxClient >NUL 2>&1 && echo RVBOX_GUEST_OK" >/dev/null || \
fail "RVBoxClient is not installed; run install from the clean baseline first"
@@ -397,6 +457,32 @@ case "$action" in
wait_service RUNNING
printf 'service=RVBoxClient state=RUNNING\n'
;;
logoff)
assert_identity
require_lease
[ "$(state)" = running ] || fail "logoff requires a running prepared VM"
# The clean fixture has exactly one active console account. Logging it off
# leaves the LocalSystem service alive and makes the no-user hierarchy
# rows observable without introducing a second session candidate. Do not
# run `logoff` through that account's Guest Control channel: Windows ends
# the channel before VBoxManage can return its completion sentinel. The
# fixture-only full-token provisioner is non-interactive (and separately
# asserted absent from WTS candidates), so it can prove the transition.
provisioner_run --exe 'C:\\Windows\\System32\\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
/d /s /c "logoff 1 & echo RVBOX_GUEST_OK" >/dev/null
attempt=0
while [ "$attempt" -lt 30 ]; do
if provisioner_run --exe 'C:\\Windows\\System32\\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
/d /s /c "query user | findstr /i /c:\"$guest_user\" >NUL & if errorlevel 1 echo RVBOX_GUEST_OK" >/dev/null 2>&1; then
step logoff-no-active-user
printf 'session=none\n'
break
fi
attempt=$((attempt + 1))
sleep 1
done
[ "$attempt" -lt 30 ] || fail "fixture user did not log off"
;;
collect)
assert_identity
require_lease
@@ -409,6 +495,13 @@ case "$action" in
fi
printf 'collected host_stage=%s/artifacts\n' "$host_stage"
;;
inspect)
assert_identity
require_lease
[ "$(state)" = running ] || fail "inspect requires a running prepared VM"
guest_run --exe 'C:\Windows\System32\cmd.exe' --wait-stdout --wait-stderr --unquoted-args -- \
/d /s /c "sc.exe queryex RVBoxClient & sc.exe qc RVBoxClient & reg.exe query \"HKLM\\SYSTEM\\CurrentControlSet\\Services\\RVBoxClient\" /v ImagePath & reg.exe query \"HKLM\\SYSTEM\\CurrentControlSet\\Services\\RVBoxClient\" /v ObjectName & dir \"C:/ProgramData/RVBox\" & icacls \"C:/ProgramData/RVBox\" & certutil -hashfile \"$guest_root\\rvbox.exe\" SHA256 & \"$guest_root\\rvbox.exe\" --check-config --config \"$guest_root\\client.toml\" > \"$guest_root\\check-config.txt\" 2>&1 & type \"$guest_root\\check-config.txt\" & \"$guest_root\\rvbox.exe\" --service --config \"$guest_root\\client.toml\" > \"$guest_root\\direct-service-probe.txt\" 2>&1 & type \"$guest_root\\direct-service-probe.txt\" & if exist \"C:/ProgramData/RVBox/service-startup.log\" type \"C:/ProgramData/RVBox/service-startup.log\" & wevtutil qe System /q:\"*[System[(EventID=7000 or EventID=7009 or EventID=7031 or EventID=7034)]]\" /c:3 /rd:true /f:text & if exist \"C:/ProgramData/RVBox/test-logs/rvbox.log\" type \"C:/ProgramData/RVBox/test-logs/rvbox.log\" & echo RVBOX_GUEST_OK"
;;
stop)
assert_identity
require_lease
@@ -428,6 +521,16 @@ case "$action" in
;;
reset)
assert_identity
# A controller may be interrupted after it has brought down its
# Compose stack but before it removes local run files. When the VM is
# already powered off at the declared baseline and no lease exists,
# reset is therefore a safe no-op. It lets `native-test clean` repair
# that abandoned local run without pretending it owns a live VM.
if [ ! -f "$lease_owner" ]; then
[ "$(state)" = poweroff ] || fail "reset requires the run lease while the VM is not powered off"
printf 'reset vm=%s snapshot=%s already-clean\n' "$vm" "$snapshot"
exit 0
fi
require_lease
if [ "$(state)" = running ]; then
VBoxManage controlvm "$vm" acpipowerbutton >/dev/null
@@ -454,12 +557,74 @@ case "$action" in
;;
esac
REMOTE
ssh -o BatchMode=yes "$RVBOX_TEST_VBOX_HOST" sh "$remote_script" \
"$1" "$RVBOX_TEST_VBOX_VM" "$RVBOX_TEST_VBOX_VM_UUID" \
"$RVBOX_TEST_VBOX_SNAPSHOT" "$RVBOX_TEST_VBOX_SNAPSHOT_UUID" \
"$RVBOX_TEST_GUEST_USER" "$RVBOX_TEST_GUEST_PASSWORD_FILE" \
"$host_stage" "$guest_root" "$remote_endpoint" \
"$provisioner_user" "$provisioner_password_file" </dev/null
then
return 0
fi
retry_attempt=$((retry_attempt + 1))
if [ "$retry_attempt" -le "$retry_limit" ]; then
printf 'remote action=%s interrupted; retry %s/%s\n' "$remote_action" "$retry_attempt" "$retry_limit" >&2
sleep 2
fi
done
fail "remote action $remote_action exhausted $retry_limit SSH attempts"
}
# accelerated_stage uses the documented controller HTTP endpoint only for a
# compressed disposable test executable. The endpoint remains authenticated;
# the artifact name contains the run ID and payload digest and is deleted after
# successful guest staging. A failed HTTP attempt leaves no host executable
# change and fails the stage; the executable is never sent over the fragile
# SSH route.
accelerated_stage() {
[ -d "$RVBOX_TEST_ACCEL_HTTP_PUBLISH_DIR" ] || {
printf 'stage: accelerated publish directory unavailable\n' >&2
return 1
}
stage_http_dir=$(mktemp -d "${TMPDIR:-/tmp}/rvbox-http-stage.XXXXXX")
stage_http_xz=$stage_http_dir/rvbox.exe.xz
xz -T0 -3 -c "$bundle/rvbox.exe" >"$stage_http_xz"
stage_http_hash=$(sha256sum "$stage_http_xz" | awk '{print $1}')
stage_http_name="rvbox-$run_id-$stage_http_hash.xz"
stage_http_published=$RVBOX_TEST_ACCEL_HTTP_PUBLISH_DIR/$stage_http_name
if [ -e "$stage_http_published" ]; then
[ ! -L "$stage_http_published" ] || fail "refusing symlink accelerated artifact $stage_http_published"
existing_hash=$(sha256sum "$stage_http_published" | awk '{print $1}')
[ "$existing_hash" = "$stage_http_hash" ] || fail "accelerated artifact name collision: $stage_http_published"
else
# The payload contains no configuration or credential. It is readable
# only to the authenticated HTTP service so nginx can serve it.
install -m 644 "$stage_http_xz" "$stage_http_published"
fi
rm -rf "$stage_http_dir"
stage_http_url=$RVBOX_TEST_ACCEL_HTTP_URL/$stage_http_name
printf 'stage: accelerated HTTP transfer\n'
if ! ssh -o BatchMode=yes -o ConnectTimeout=10 -o ServerAliveInterval=10 -o ServerAliveCountMax=2 "$RVBOX_TEST_VBOX_HOST" \
"set -eu; stage='$host_stage'; target=\$stage/rvbox.exe.xz.http; curl --proxy '$RVBOX_TEST_ACCEL_SOCKS5' --anyauth -u '$RVBOX_TEST_ACCEL_HTTP_AUTH' --continue-at - --retry 4 --retry-all-errors --retry-delay 2 --connect-timeout 15 --max-time 120 --fail --silent --show-error --output \$target '$stage_http_url'; expected='$stage_http_hash'; actual=\$(sha256sum \$target | awk '{print \$1}'); test \"\$actual\" = \"\$expected\"; xz -dc \$target >\$stage/rvbox.exe.new; chmod 700 \$stage/rvbox.exe.new; mv \$stage/rvbox.exe.new \$stage/rvbox.exe; rm -f \$target"; then
printf 'stage: accelerated HTTP transfer failed\n' >&2
rm -f "$stage_http_published"
return 1
fi
accelerated_artifact=$stage_http_published
return 0
}
# Only small non-secret configuration files use the SSH control route. The
# executable itself always uses accelerated_stage above.
copy_stage_file() {
source_file=$1
target_name=$2
copy_attempt=1
while [ "$copy_attempt" -le 4 ]; do
if scp -q "$source_file" "$RVBOX_TEST_VBOX_HOST:$host_stage/$target_name"; then
return 0
fi
copy_attempt=$((copy_attempt + 1))
if [ "$copy_attempt" -le 4 ]; then
printf 'stage: small-file SSH copy retry %s/4 (%s)\n' "$copy_attempt" "$target_name" >&2
sleep 2
fi
done
fail "could not copy staged $target_name after 4 SSH attempts"
}
case $action in
@@ -468,14 +633,43 @@ case $action in
printf 'prepared vm=%s stage=%s\n' "$RVBOX_TEST_VBOX_VM" "$host_stage"
;;
stage)
printf 'stage: verify prepared VM and lease\n'
remote prepare-stage
scp -q "$bundle/rvbox.exe" "$bundle/client.toml" "$RVBOX_TEST_VBOX_HOST:$host_stage/"
if [ -f "$bundle/ca.pem" ]; then scp -q "$bundle/ca.pem" "$RVBOX_TEST_VBOX_HOST:$host_stage/"; fi
[ -f "$bundle/rvbox.exe" ] && [ ! -L "$bundle/rvbox.exe" ] || fail "rvbox.exe must be a regular non-symlink file"
[ -f "$bundle/client.toml" ] && [ ! -L "$bundle/client.toml" ] || fail "client.toml must be a regular non-symlink file"
if [ -f "$bundle/ca.pem" ]; then
[ ! -L "$bundle/ca.pem" ] || fail "ca.pem must not be a symlink"
local_hashes=$(cd "$bundle" && sha256sum rvbox.exe client.toml ca.pem)
else
local_hashes=$(cd "$bundle" && sha256sum rvbox.exe client.toml)
fi
copy_stage_file "$bundle/client.toml" client.toml
if [ -f "$bundle/ca.pem" ]; then copy_stage_file "$bundle/ca.pem" ca.pem; fi
local_exe_hash=$(sha256sum "$bundle/rvbox.exe" | awk '{print $1}')
remote_exe_hash=$(ssh -o BatchMode=yes "$RVBOX_TEST_VBOX_HOST" "sha256sum '$host_stage/rvbox.exe' 2>/dev/null | awk '{print \$1}'" 2>/dev/null || true)
if [ "$local_exe_hash" = "$remote_exe_hash" ]; then
printf 'stage: matching accelerated executable already present\n'
else
accelerated_stage || fail "accelerated executable transfer failed"
trap 'if [ -n "$accelerated_artifact" ]; then rm -f "$accelerated_artifact"; fi' EXIT HUP INT TERM
fi
if [ -f "$bundle/ca.pem" ]; then
remote_hashes=$(ssh -o BatchMode=yes "$RVBOX_TEST_VBOX_HOST" "cd '$host_stage' && sha256sum rvbox.exe client.toml ca.pem" 2>/dev/null || true)
else
remote_hashes=$(ssh -o BatchMode=yes "$RVBOX_TEST_VBOX_HOST" "cd '$host_stage' && sha256sum rvbox.exe client.toml" 2>/dev/null || true)
fi
[ "$local_hashes" = "$remote_hashes" ] || fail "bundle transfer did not reach the expected SHA-256 manifest"
printf 'verified transfer_sha256 run_id=%s\n%s\n' "$run_id" "$local_hashes"
remote stage
remote stage-create-root
remote stage-copy-exe
remote stage-copy-config
if [ -f "$bundle/ca.pem" ]; then remote stage-copy-ca; fi
if [ -n "$accelerated_artifact" ]; then
rm -f "$accelerated_artifact"
accelerated_artifact=
trap - EXIT HUP INT TERM
fi
printf 'staged guest_root=%s\n' "$guest_root"
;;
collect)
+8 -2
View File
@@ -590,8 +590,14 @@ tests = ["internal/client/spool/spool_test.go:TestSendWindowPinsUnacknowledgedBy
[[requirements]]
id = "HP-WINCTX-02"
layer = "integration"
status = "blocked_native_windows"
tests = []
status = "planned"
tests = ["test/windowsnative/native_fixture_test.go:TestNativeFixtureAssets_HP_HARNESS_20"]
[[requirements]]
id = "HP-HARNESS-20"
layer = "unit"
status = "implemented"
tests = ["test/windowsnative/native_fixture_test.go:TestNativeFixtureAssets_HP_HARNESS_20"]
[[requirements]]
id = "HP-STORE-01"
+11
View File
@@ -0,0 +1,11 @@
# Linux server native-test stack
This directory owns the Docker Compose stack for the Windows native E2E lane.
It runs on the current Linux controller and owns the Linux RVBox server, nginx
TLS proxy with a per-run matching-host self-signed leaf, SQLite/segment state,
control socket, and logs. The v1 client deliberately accepts this self-signed
leaf when no CA file is configured; it is encrypted transport, not server
authentication.
The Helium VM never hosts this stack. It receives only rvbox.exe and client
TOML through the Windows fixture controller.
+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
set -eu
test -f /pki/server.pem && test -f /pki/server-key.pem && exit 0
apk add --no-cache openssl
umask 077
openssl genrsa -out /pki/server-key.pem 2048
openssl req -x509 -new -key /pki/server-key.pem -sha256 -days 2 \
-subj "/CN=${RVBOX_NATIVE_ENDPOINT_HOST:?}" \
-addext "subjectAltName=DNS:${RVBOX_NATIVE_ENDPOINT_HOST}" \
-out /pki/server.pem
chmod 600 /pki/*key.pem
chmod 644 /pki/server.pem
+37
View File
@@ -0,0 +1,37 @@
# Per-run production-shaped Linux server/proxy fixture. It runs on the current
# controller; the Windows VM receives only its client bundle.
services:
server:
image: alpine:3.22
user: "${RVBOX_NATIVE_UID:-65532}:${RVBOX_NATIVE_GID:-65532}"
command: ["/opt/rvbox/rvbox-server", "--config", "/etc/rvbox/server.toml"]
volumes:
- ../../bin/rvbox-server:/opt/rvbox/rvbox-server:ro
- ../../bin/rvc:/opt/rvbox/rvc:ro
- "${RVBOX_NATIVE_RUNTIME_DIR}/server.toml:/etc/rvbox/server.toml:ro"
- "${RVBOX_NATIVE_RUNTIME_DIR}/state:/state"
- "${RVBOX_NATIVE_RUNTIME_DIR}/state/control:/run/rvbox"
networks: [native]
labels: { rvbox.native.run_id: "${RVBOX_NATIVE_RUN_ID}" }
nginx:
image: nginx:1.27-alpine
depends_on: [server]
ports: [ "${RVBOX_NATIVE_BIND:-0.0.0.0}:${RVBOX_NATIVE_PORT}:443" ]
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- "${RVBOX_NATIVE_RUNTIME_DIR}/pki/server.pem:/etc/nginx/tls/server.pem:ro"
- "${RVBOX_NATIVE_RUNTIME_DIR}/pki/server-key.pem:/etc/nginx/tls/server-key.pem:ro"
networks: [native]
labels: { rvbox.native.run_id: "${RVBOX_NATIVE_RUN_ID}" }
certgen:
image: alpine:3.22
profiles: [tools]
environment:
RVBOX_NATIVE_ENDPOINT_HOST: "${RVBOX_NATIVE_ENDPOINT_HOST}"
volumes:
- "${RVBOX_NATIVE_RUNTIME_DIR}/pki:/pki"
- ./certgen.sh:/fixture/certgen.sh:ro
entrypoint: ["/bin/sh", "/fixture/certgen.sh"]
networks:
native:
labels: { rvbox.native.run_id: "${RVBOX_NATIVE_RUN_ID}" }
+21
View File
@@ -0,0 +1,21 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
server_name _;
ssl_certificate /etc/nginx/tls/server.pem;
ssl_certificate_key /etc/nginx/tls/server-key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location = /v1/agent {
proxy_pass http://server:6899;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 75s;
proxy_send_timeout 15s;
}
location / { return 404; }
}
+56
View File
@@ -0,0 +1,56 @@
package windowsnative
import (
"os"
"path/filepath"
"strings"
"testing"
)
// TestNativeFixtureAssets_HP_HARNESS_20 prevents the host-side controller from
// drifting back to a PowerShell-only or protocol-stub lane. It intentionally
// checks only static contracts; the real hierarchy proof remains the documented
// native VM run.
func TestNativeFixtureAssets_HP_HARNESS_20(t *testing.T) {
t.Parallel()
root := filepath.Clean(filepath.Join("..", ".."))
read := func(relative string) string {
t.Helper()
data, err := os.ReadFile(filepath.Join(root, relative))
if err != nil {
t.Fatalf("read %s: %v", relative, err)
}
return string(data)
}
runner := read("scripts/windows/native-test")
for _, required := range []string{
"scripts/windows/build-test-bundle\" --native-fixture",
"scripts/windows/test-host\" prepare",
"ACTIVE_USER_ELEVATED,ACTIVE_SYSTEM",
"assert_context local-service no local-service",
"clean --purge --yes",
"RVBOX_NATIVE_ENDPOINT_HOST",
"test/linux-server/compose.yaml",
} {
if !strings.Contains(runner, required) {
t.Fatalf("native runner is missing %q", required)
}
}
testHost := read("scripts/windows/test-host")
for _, required := range []string{"xz -T0 -3", "accelerated_stage", "copy_stage_file", "--proxy", "--anyauth", "--continue-at", "rvbox.exe.xz", "retry_limit=4", "ConnectTimeout=10", "bundle transfer did not reach the expected SHA-256 manifest", "verified transfer_sha256"} {
if !strings.Contains(testHost, required) {
t.Fatalf("native test-host is missing compressed transfer contract %q", required)
}
}
compose := read("test/linux-server/compose.yaml")
for _, required := range []string{"../../bin/rvbox-server", "nginx:1.27-alpine", "rvbox.native.run_id", "RVBOX_NATIVE_RUNTIME_DIR"} {
if !strings.Contains(compose, required) {
t.Fatalf("native Compose fixture is missing %q", required)
}
}
defaults := read("internal/client/supervisor/windows/testfaults_default.go")
fixture := read("internal/client/supervisor/windows/testfaults_fixture.go")
if !strings.Contains(defaults, "//go:build !rvbox_native_test") || !strings.Contains(fixture, "//go:build rvbox_native_test") {
t.Fatal("fixture-only context faults are not separated from release builds")
}
}