Files
run/runtime/source_rcon_test.go
2026-08-26 09:56:43 +08:00

350 lines
14 KiB
Go

package runtime
import (
"context"
"encoding/binary"
"encoding/json"
"fmt"
"net"
"os"
"strings"
"testing"
"time"
"browser.local/run/protocol"
)
func TestExecuteSourceRCONAuthenticatesRunsAndRedacts(t *testing.T) {
listener, port := newSourceRCONListener(t)
defer listener.Close()
password := strings.Repeat("a", 64)
assignment := sourceRCONAssignment(port)
root := t.TempDir()
writeSourceRCONConfig(t, root, assignment, "127.0.0.1", password)
commands := make(chan string, 1)
serverDone := serveSourceRCONSession(listener, password, func(connection net.Conn, command sourceRCONPacket) error {
commands <- command.body
if err := writeSourceRCONPacket(context.Background(), connection, sourceRCONPacket{id: command.id, typeCode: sourceRCONResponseValue, body: "queued"}); err != nil {
return err
}
return writeSourceRCONPacket(context.Background(), connection, sourceRCONPacket{id: command.id, typeCode: sourceRCONResponseValue})
})
command := "SetTime 12"
result := NewLifecycleExecutor(WithLifecycleWorkspaceRoot(root), WithDLLExtensionRuntimeTarget("windows", "amd64")).ExecuteSourceRCON(context.Background(), assignment, command)
if result.State != lifecycleResultStateSucceeded || result.ErrorCode != "" || result.ExecutionResult.Kind != "source-rcon" {
t.Fatalf("expected successful Source RCON delivery, got %+v", result)
}
if got := <-commands; got != command {
t.Fatalf("expected transient command delivery, got %q", got)
}
awaitSourceRCONServer(t, serverDone)
serialized, err := json.Marshal(result)
if err != nil {
t.Fatalf("marshal result: %v", err)
}
for _, private := range []string{command, password, "queued"} {
if strings.Contains(string(serialized), private) {
t.Fatalf("Source RCON result exposed private wire data %q: %s", private, serialized)
}
}
}
func TestExecuteSourceRCONReadsConfigFromManagedNestedDeployment(t *testing.T) {
listener, port := newSourceRCONListener(t)
defer listener.Close()
password := strings.Repeat("e", 64)
assignment := sourceRCONAssignment(port)
root := t.TempDir()
writeSourceRCONConfigAt(t, root, assignment, "bin/"+assignment.ExecutionInput.SourceRCON.ConfigRef, "127.0.0.1", password)
serverDone := serveSourceRCONSession(listener, password, func(connection net.Conn, command sourceRCONPacket) error {
if command.body != "rcon.status" {
return fmt.Errorf("unexpected nested deployment command %q", command.body)
}
return writeSourceRCONPacket(context.Background(), connection, sourceRCONPacket{id: command.id, typeCode: sourceRCONResponseValue})
})
result := NewLifecycleExecutor(WithLifecycleWorkspaceRoot(root), WithDLLExtensionRuntimeTarget("windows", "amd64")).ExecuteSourceRCON(context.Background(), assignment, "rcon.status")
if result.State != lifecycleResultStateSucceeded {
t.Fatalf("expected nested managed config delivery, got %+v", result)
}
awaitSourceRCONServer(t, serverDone)
}
func TestExecuteSourceRCONRedactsSourceErrorsAndMalformedPackets(t *testing.T) {
password := strings.Repeat("b", 64)
command := "SpawnItem secret-item"
for _, testCase := range []struct {
name string
respond func(net.Conn, sourceRCONPacket) error
wantCode string
private string
}{
{
name: "source error response",
respond: func(connection net.Conn, packet sourceRCONPacket) error {
if err := writeSourceRCONPacket(context.Background(), connection, sourceRCONPacket{id: packet.id, typeCode: sourceRCONResponseValue, body: "err"}); err != nil {
return err
}
if err := writeSourceRCONPacket(context.Background(), connection, sourceRCONPacket{id: packet.id, typeCode: sourceRCONResponseValue, body: "or: denied secret-item"}); err != nil {
return err
}
return writeSourceRCONPacket(context.Background(), connection, sourceRCONPacket{id: packet.id, typeCode: sourceRCONResponseValue})
},
wantCode: "source_rcon_command_failed",
private: "denied secret-item",
},
{
name: "malformed response packet",
respond: func(connection net.Conn, _ sourceRCONPacket) error {
var size [4]byte
binary.LittleEndian.PutUint32(size[:], sourceRCONMaxPacketSize+1)
_, err := connection.Write(size[:])
return err
},
wantCode: "source_rcon_protocol_failed",
private: "source response body",
},
} {
t.Run(testCase.name, func(t *testing.T) {
listener, port := newSourceRCONListener(t)
defer listener.Close()
assignment := sourceRCONAssignment(port)
root := t.TempDir()
writeSourceRCONConfig(t, root, assignment, "127.0.0.1", password)
serverDone := serveSourceRCONSession(listener, password, testCase.respond)
result := NewLifecycleExecutor(WithLifecycleWorkspaceRoot(root), WithDLLExtensionRuntimeTarget("windows", "amd64")).ExecuteSourceRCON(context.Background(), assignment, command)
if result.State != lifecycleResultStateFailed || result.ErrorCode != testCase.wantCode || result.Retryable {
t.Fatalf("expected safe non-retryable %s failure, got %+v", testCase.wantCode, result)
}
awaitSourceRCONServer(t, serverDone)
serialized, err := json.Marshal(result)
if err != nil {
t.Fatalf("marshal result: %v", err)
}
for _, private := range []string{command, password, testCase.private} {
if strings.Contains(string(serialized), private) {
t.Fatalf("Source RCON failure exposed private wire data %q: %s", private, serialized)
}
}
})
}
}
func TestExecuteSourceRCONRejectsUnsafeConfigAndNonWindowsBeforeDial(t *testing.T) {
listener, port := newSourceRCONListener(t)
acceptResult := make(chan error, 1)
go func() {
connection, err := listener.Accept()
if err == nil {
_ = connection.Close()
}
acceptResult <- err
}()
password := strings.Repeat("c", 64)
assignment := sourceRCONAssignment(port)
root := t.TempDir()
writeSourceRCONConfig(t, root, assignment, "0.0.0.0", password)
result := NewLifecycleExecutor(WithLifecycleWorkspaceRoot(root), WithDLLExtensionRuntimeTarget("windows", "amd64")).ExecuteSourceRCON(context.Background(), assignment, "rcon.status")
if result.ErrorCode != "source_rcon_config_invalid" {
t.Fatalf("expected unsafe local config rejection, got %+v", result)
}
acceptReturned := false
acceptedConnection := false
select {
case err := <-acceptResult:
acceptReturned = true
if err == nil {
acceptedConnection = true
}
case <-time.After(150 * time.Millisecond):
// No connection is expected before the listener is closed below.
}
if acceptedConnection {
t.Fatal("unsafe config opened a socket")
}
if err := listener.Close(); err != nil {
t.Fatalf("close listener: %v", err)
}
if !acceptReturned {
if err := <-acceptResult; err == nil {
t.Fatal("unsafe config opened a socket")
}
}
linuxResult := NewLifecycleExecutor(WithLifecycleWorkspaceRoot(t.TempDir()), WithDLLExtensionRuntimeTarget("linux", "amd64")).ExecuteSourceRCON(context.Background(), assignment, "rcon.status")
if linuxResult.ErrorCode != "unsupported_extension_platform" {
t.Fatalf("expected non-Windows rejection, got %+v", linuxResult)
}
}
func TestWorkerSourceRCONConsumesInputOnceWithoutJournalOrResultLeakage(t *testing.T) {
listener, port := newSourceRCONListener(t)
defer listener.Close()
password := strings.Repeat("d", 64)
command := "SendChat 4 \"maintenance complete\""
client := newFakeWorkerClient()
assignment := workerJobAssignment(protocol.RunCapabilityRemoteRunRCONCommand)
assignment.TargetKey = "rcon.password"
assignment.InputRef = "input://source-rcon/job-worker"
assignment.MaxAttempts = 1
assignment.ExecutionInput = protocol.RunJobExecutionInput{
WorkspaceScope: "run-local",
RemoteAdapterKey: "rcon",
RemoteAdapterKind: "rcon",
TimeoutSeconds: 5,
SourceRCON: &protocol.RuntimeSourceRCONPlan{Protocol: "source-rcon", ExtensionKey: "scum-simple-rcon", ModKey: "scum_simple_rcon", ConfigRef: "ue4ss/Mods/scum_simple_rcon/config.ini", DeploymentStateRef: "runtime/ue4ss-dll/ue4ss/scum-simple-rcon/release.json", Port: port},
}
client.claimJob = assignment
client.sourceRCONInput = protocol.SourceRCONExecutionInputResponse{JobID: assignment.JobID, ServerInstanceID: assignment.ServerInstanceID, RunEndpointID: assignment.RunEndpointID, Command: command}
serverDone := serveSourceRCONSession(listener, password, func(connection net.Conn, packet sourceRCONPacket) error {
if err := writeSourceRCONPacket(context.Background(), connection, sourceRCONPacket{id: packet.id, typeCode: sourceRCONResponseValue, body: "accepted"}); err != nil {
return err
}
return writeSourceRCONPacket(context.Background(), connection, sourceRCONPacket{id: packet.id, typeCode: sourceRCONResponseValue})
})
config := workerTestConfig(t)
writeSourceRCONConfig(t, config.WorkspaceRoot, assignment, "127.0.0.1", password)
worker, err := NewWorker(config, client, WithDLLExtensionRuntimeTarget("windows", "amd64"))
if err != nil {
t.Fatalf("new worker: %v", err)
}
if err := worker.Register(context.Background()); err != nil {
t.Fatalf("register worker: %v", err)
}
if handled, err := worker.ClaimAndRunOnce(context.Background()); err != nil || !handled {
t.Fatalf("claim Source RCON job handled=%v err=%v", handled, err)
}
awaitSourceRCONServer(t, serverDone)
if len(client.sourceRCONRequests) != 1 || client.sourceRCONRequests[0].JobID != assignment.JobID {
t.Fatalf("expected one active-lease Source RCON input read, got %+v", client.sourceRCONRequests)
}
if len(client.resultRequests) != 1 || client.resultRequests[0].Retryable || client.resultRequests[0].State != lifecycleResultStateSucceeded {
t.Fatalf("expected one non-retryable safe result, got %+v", client.resultRequests)
}
for _, projection := range []any{worker.journal.ActiveJobs(), client.resultRequests, client.sourceRCONRequests} {
body, marshalErr := json.Marshal(projection)
if marshalErr != nil {
t.Fatalf("marshal safe projection: %v", marshalErr)
}
for _, private := range []string{command, password, "accepted"} {
if strings.Contains(string(body), private) {
t.Fatalf("journal or result projection exposed %q: %s", private, body)
}
}
}
}
func newSourceRCONListener(t *testing.T) (net.Listener, int) {
t.Helper()
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen Source RCON fixture: %v", err)
}
address, ok := listener.Addr().(*net.TCPAddr)
if !ok || address.Port < 1024 {
_ = listener.Close()
t.Fatal("invalid Source RCON fixture address")
}
return listener, address.Port
}
func sourceRCONAssignment(port int) protocol.RunJobAssignment {
assignment := lifecycleAssignment(protocol.RunCapabilityRemoteRunRCONCommand)
assignment.TargetKey = "rcon.password"
assignment.InputRef = "input://source-rcon/job-1"
assignment.MaxAttempts = 1
assignment.ExecutionInput = protocol.RunJobExecutionInput{
WorkspaceScope: "run-local",
RemoteAdapterKey: "rcon",
RemoteAdapterKind: "rcon",
TimeoutSeconds: 5,
SourceRCON: &protocol.RuntimeSourceRCONPlan{Protocol: "source-rcon", ExtensionKey: "scum-simple-rcon", ModKey: "scum_simple_rcon", ConfigRef: "ue4ss/Mods/scum_simple_rcon/config.ini", DeploymentStateRef: "runtime/ue4ss-dll/ue4ss/scum-simple-rcon/release.json", Port: port},
}
return assignment
}
func writeSourceRCONConfig(t *testing.T, root string, assignment protocol.RunJobAssignment, bindAddress string, password string) {
writeSourceRCONConfigAt(t, root, assignment, assignment.ExecutionInput.SourceRCON.ConfigRef, bindAddress, password)
}
func writeSourceRCONConfigAt(t *testing.T, root string, assignment protocol.RunJobAssignment, configRef string, bindAddress string, password string) {
t.Helper()
resolver := NewWorkspaceResolver(root)
scope, err := resolver.Scope(assignment.ServerInstanceID, assignment.ExecutionInput.WorkspaceScope)
if err != nil {
t.Fatalf("create Source RCON scope: %v", err)
}
path, _, err := resolver.WritableTarget(scope, configRef)
if err != nil {
t.Fatalf("resolve Source RCON config: %v", err)
}
body := fmt.Sprintf("%s\n[rcon]\nbind_address=%s\nport=%d\npassword=%s\n", managedRCONConfigMarker, bindAddress, assignment.ExecutionInput.SourceRCON.Port, password)
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatalf("write Source RCON config: %v", err)
}
markerPath, _, err := resolver.WritableTarget(scope, assignment.ExecutionInput.SourceRCON.DeploymentStateRef)
if err != nil {
t.Fatalf("resolve Source RCON deployment marker: %v", err)
}
markerBody, err := json.Marshal(managedDLLExtensionMarker{Version: ue4ssExtensionMarkerVersion, ReleaseVersion: "1.0.0", Checksum: "sha256:" + strings.Repeat("a", 64), SizeBytes: 1, ExtensionKey: assignment.ExecutionInput.SourceRCON.ExtensionKey, ModKey: assignment.ExecutionInput.SourceRCON.ModKey, ConfigRef: configRef, RCONPort: assignment.ExecutionInput.SourceRCON.Port})
if err != nil {
t.Fatalf("marshal Source RCON deployment marker: %v", err)
}
if err := os.WriteFile(markerPath, markerBody, 0o600); err != nil {
t.Fatalf("write Source RCON deployment marker: %v", err)
}
}
func serveSourceRCONSession(listener net.Listener, password string, respond func(net.Conn, sourceRCONPacket) error) <-chan error {
done := make(chan error, 1)
go func() {
connection, err := listener.Accept()
if err != nil {
done <- err
return
}
defer connection.Close()
context, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
auth, err := readSourceRCONPacket(context, connection)
if err != nil {
done <- err
return
}
if auth.id != sourceRCONAuthRequestID || auth.typeCode != sourceRCONAuthenticate || auth.body != password {
done <- fmt.Errorf("unexpected auth packet")
return
}
if err := writeSourceRCONPacket(context, connection, sourceRCONPacket{id: auth.id, typeCode: sourceRCONAuthResponse}); err != nil {
done <- err
return
}
command, err := readSourceRCONPacket(context, connection)
if err != nil {
done <- err
return
}
if command.id != sourceRCONCommandRequestID || command.typeCode != sourceRCONExecuteCommand {
done <- fmt.Errorf("unexpected command packet")
return
}
done <- respond(connection, command)
}()
return done
}
func awaitSourceRCONServer(t *testing.T, done <-chan error) {
t.Helper()
select {
case err := <-done:
if err != nil {
t.Fatalf("Source RCON fixture: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("Source RCON fixture did not finish")
}
}