From 41eac09b757d1b5c031f35fe2ff2559623cae04c Mon Sep 17 00:00:00 2001 From: CMGS Date: Thu, 24 Sep 2026 00:50:21 +0800 Subject: [PATCH 1/9] =?UTF-8?q?build:=20bump=20github.com/mdlayher/vsock?= =?UTF-8?q?=20v1.2.1=E2=86=92v1.3.0,=20github.com/projecteru2/core=20v0.1.?= =?UTF-8?q?3=E2=86=92v0.1.5,=20golang.org/x/sys=20v0.47.0=E2=86=92v0.48.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 14 +++++++------- go.sum | 28 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index 779c032..7f6eb6e 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/cocoonstack/cocoon-agent go 1.27.0 require ( - github.com/mdlayher/vsock v1.2.1 - github.com/projecteru2/core v0.1.3 + github.com/mdlayher/vsock v1.3.0 + github.com/projecteru2/core v0.1.5 github.com/spf13/cobra v1.10.2 - golang.org/x/sys v0.47.0 + golang.org/x/sys v0.48.0 ) require ( @@ -14,14 +14,14 @@ require ( github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect github.com/cockroachdb/redact v1.1.8 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/getsentry/sentry-go v0.48.0 // indirect + github.com/getsentry/sentry-go v0.49.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.24 // indirect - github.com/mdlayher/socket v0.5.1 // indirect + github.com/mdlayher/socket v0.6.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/rogpeppe/go-internal v1.16.0 // indirect github.com/rs/zerolog v1.35.1 // indirect @@ -29,8 +29,8 @@ require ( golang.org/x/net v0.58.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/text v0.41.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect - google.golang.org/grpc v1.83.1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260918162117-cecb64721679 // indirect + google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.12 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect ) diff --git a/go.sum b/go.sum index 4388a8b..89a4e99 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/getsentry/sentry-go v0.48.0 h1:FRZNr7Uk1C86ev1bSJmYlUkL9oyivQA6YOcdYfaaMmY= -github.com/getsentry/sentry-go v0.48.0/go.mod h1:E5UkA5wp1qR2+MDydNYlVeUiNN2xEdjYMidkgf0Qoss= +github.com/getsentry/sentry-go v0.49.0 h1:Ehejknu1l023Ub7QoRBVLAI7g3Jnhqku4oWx4B4Sh5s= +github.com/getsentry/sentry-go v0.49.0/go.mod h1:nuMJAoCfe1u0Bts2ocyNI+TW8HT84vRMqwA5Qq/SKUI= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -28,17 +28,17 @@ github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= -github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= -github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= -github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= -github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= +github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU= +github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18= +github.com/mdlayher/vsock v1.3.0 h1:bqQfZ1OznI03y6YiXp2sze05RVdzLn/zsfjnjd4+ivI= +github.com/mdlayher/vsock v1.3.0/go.mod h1:WsuksavOvwCnV5UqGHUkvAvCy+Dqy81y4goKQTzxxNY= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/projecteru2/core v0.1.3 h1:0odzNdtDKRvbMir3xKpqCPJaThHdzfzvou/vGFWquZY= -github.com/projecteru2/core v0.1.3/go.mod h1:4WGV1OlU8TY0hra+N8Ua2s8hbX1AeJmcjnj3PcztnI4= +github.com/projecteru2/core v0.1.5 h1:cS3xfqa6I1bMXtGdbu2A2UzhjiHaP2mogjQWFTkSEF0= +github.com/projecteru2/core v0.1.5/go.mod h1:MDMJmmlJ3ZVvFQl5BnRcKV1/AVCHyiWoI4BI+sCnuYM= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g= github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= @@ -82,8 +82,8 @@ golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= @@ -96,10 +96,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 h1:1VUiZAXyC+zmiFYi+WLtBzr68Cj8wOofHjjrA/kkizc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= -google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= -google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260918162117-cecb64721679 h1:KmqdJU4vrNcxy/6qdg3JduZtalEXrJLspVltnR1cE+8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260918162117-cecb64721679/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From b738a9360f37289c23cd2811e46d7c28ebb5f3ce Mon Sep 17 00:00:00 2001 From: CMGS Date: Thu, 24 Sep 2026 01:11:44 +0800 Subject: [PATCH 2/9] fix: restart the Windows service after an error exit, not only a crash SCM queues failure actions for a STOPPED status with a non-zero exit code only when the failure-actions flag is set; serve returns exit code 1 after the 2-minute listen retry or a permanent accept error. The failure actions and the flag now apply on both fresh installs and re-runs over an existing service. --- docs/install.md | 2 +- packaging/install-cocoon-agent.ps1 | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/install.md b/docs/install.md index ede236f..52d2b7d 100644 --- a/docs/install.md +++ b/docs/install.md @@ -37,7 +37,7 @@ Windows support requires the `viosock` driver shipped with **virtio-win >= 0.1.2 .\install-cocoon-agent.ps1 ``` -The script copies `cocoon-agent.exe` to `C:\Program Files\Cocoon\`, registers a Windows service (`cocoon-agent`, `LocalSystem`, auto-start, restart-on-crash), and starts it. See [`packaging/install-cocoon-agent.ps1`](../packaging/install-cocoon-agent.ps1). +The script copies `cocoon-agent.exe` to `C:\Program Files\Cocoon\`, registers a Windows service (`cocoon-agent`, `LocalSystem`, auto-start, restart on failure), and starts it. See [`packaging/install-cocoon-agent.ps1`](../packaging/install-cocoon-agent.ps1). Verify it's running: diff --git a/packaging/install-cocoon-agent.ps1 b/packaging/install-cocoon-agent.ps1 index dfed20b..594e4b7 100644 --- a/packaging/install-cocoon-agent.ps1 +++ b/packaging/install-cocoon-agent.ps1 @@ -83,11 +83,13 @@ if ($existing) { Write-Step "creating service $ServiceName" Invoke-Bat ('sc.exe create {0} binPath= "\"{1}\" serve --port {2}" start= auto DisplayName= "{3}"' -f $ServiceName, $BinaryDest, $Port, $DisplayName) Invoke-Bat ('sc.exe description {0} "{1}"' -f $ServiceName, $Description) - # 24h reset window mirrors Linux's Restart=always intent — a flapping - # bug self-heals instead of permanently bricking the agent. - Invoke-Bat ('sc.exe failure {0} reset= 86400 actions= restart/5000/restart/5000/restart/5000' -f $ServiceName) } +# 24h reset window mirrors Linux's Restart=always intent — a flapping +# bug self-heals instead of permanently bricking the agent. +Invoke-Bat ('sc.exe failure {0} reset= 86400 actions= restart/5000/restart/5000/restart/5000' -f $ServiceName) +Invoke-Bat ('sc.exe failureflag {0} 1' -f $ServiceName) + # Confirm registration before attempting Start-Service; otherwise an SCM hiccup # in step 3 would surface here as a misleading "service not found" error. $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue From 4030f6be62628803148ebd3c9cc117680fa24b22 Mon Sep 17 00:00:00 2001 From: CMGS Date: Thu, 24 Sep 2026 01:12:01 +0800 Subject: [PATCH 3/9] review: move the Linux vsock constructors below the listener type, utilities below the method set they build --- cmd/transport_linux.go | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/cmd/transport_linux.go b/cmd/transport_linux.go index 0909bb5..58cd2d5 100644 --- a/cmd/transport_linux.go +++ b/cmd/transport_linux.go @@ -12,26 +12,6 @@ import ( "github.com/projecteru2/core/log" ) -func listenVsock(ctx context.Context, port uint32) (net.Listener, error) { - l, err := vsock.Listen(port, nil) - if err != nil { - return nil, fmt.Errorf("vsock listen: %w", err) - } - return &hostOnlyListener{ - Listener: l, - ctx: ctx, - logger: log.WithFunc("cmd.hostOnlyListener.Accept"), - }, nil -} - -func dialVsock(cid, port uint32) (io.ReadWriteCloser, error) { - conn, err := vsock.Dial(cid, port, nil) - if err != nil { - return nil, fmt.Errorf("vsock dial: %w", err) - } - return conn, nil -} - var _ net.Listener = (*hostOnlyListener)(nil) // Peers other than VMADDR_CID_HOST are rejected: VMADDR_CID_LOCAL loopback would let an unprivileged guest process run commands as root. @@ -55,6 +35,26 @@ func (l *hostOnlyListener) Accept() (net.Conn, error) { } } +func listenVsock(ctx context.Context, port uint32) (net.Listener, error) { + l, err := vsock.Listen(port, nil) + if err != nil { + return nil, fmt.Errorf("vsock listen: %w", err) + } + return &hostOnlyListener{ + Listener: l, + ctx: ctx, + logger: log.WithFunc("cmd.hostOnlyListener.Accept"), + }, nil +} + +func dialVsock(cid, port uint32) (io.ReadWriteCloser, error) { + conn, err := vsock.Dial(cid, port, nil) + if err != nil { + return nil, fmt.Errorf("vsock dial: %w", err) + } + return conn, nil +} + func isHostPeer(conn net.Conn) bool { addr, ok := conn.RemoteAddr().(*vsock.Addr) if !ok { From 4eaafc909b4f06126c8b77af7bd078484c91da47 Mon Sep 17 00:00:00 2001 From: CMGS Date: Thu, 24 Sep 2026 01:12:26 +0800 Subject: [PATCH 4/9] review: errors.AsType for the exit code, cmd.serve log tag, host-peer test conn embeds net.Conn --- agent/exec.go | 5 ++--- cmd/serve.go | 2 +- cmd/transport_linux_test.go | 12 ++---------- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/agent/exec.go b/agent/exec.go index 0bd7608..8e84279 100644 --- a/agent/exec.go +++ b/agent/exec.go @@ -101,10 +101,9 @@ func runExec(parentCtx context.Context, argv []string, env map[string]string, st } exitCode := 0 - var exitErr *exec.ExitError - switch { + switch exitErr, isExit := errors.AsType[*exec.ExitError](waitErr); { case waitErr == nil: - case errors.As(waitErr, &exitErr): + case isExit: exitCode = exitErr.ExitCode() case errors.Is(waitErr, exec.ErrWaitDelay): // Pipes were abandoned to a background child; the command itself exited. diff --git a/cmd/serve.go b/cmd/serve.go index a6428ca..71af8dc 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -30,7 +30,7 @@ func newServeCmd() *cobra.Command { return fmt.Errorf("listen vsock port %d: %w", port, err) } srv := agent.NewServer(listener) - log.WithFunc("cmd.newServeCmd").Infof(ctx, "cocoon-agent serving vsock port %d", port) + log.WithFunc("cmd.serve").Infof(ctx, "cocoon-agent serving vsock port %d", port) return srv.Serve(ctx) }, } diff --git a/cmd/transport_linux_test.go b/cmd/transport_linux_test.go index e008edd..2721a3b 100644 --- a/cmd/transport_linux_test.go +++ b/cmd/transport_linux_test.go @@ -3,10 +3,8 @@ package cmd import ( - "io" "net" "testing" - "time" "github.com/mdlayher/vsock" ) @@ -45,14 +43,8 @@ func TestIsHostPeer(t *testing.T) { } type staticAddrConn struct { + net.Conn addr net.Addr } -func (c *staticAddrConn) Read(_ []byte) (int, error) { return 0, io.EOF } -func (c *staticAddrConn) Write(p []byte) (int, error) { return len(p), nil } -func (c *staticAddrConn) Close() error { return nil } -func (c *staticAddrConn) LocalAddr() net.Addr { return c.addr } -func (c *staticAddrConn) RemoteAddr() net.Addr { return c.addr } -func (c *staticAddrConn) SetDeadline(_ time.Time) error { return nil } -func (c *staticAddrConn) SetReadDeadline(_ time.Time) error { return nil } -func (c *staticAddrConn) SetWriteDeadline(_ time.Time) error { return nil } +func (c *staticAddrConn) RemoteAddr() net.Addr { return c.addr } From 1e615f7ec166cedd1f97ec57828a1a63adca1292 Mon Sep 17 00:00:00 2001 From: CMGS Date: Thu, 24 Sep 2026 01:12:35 +0800 Subject: [PATCH 5/9] cut: fold the Windows process controller into setupProcess closures --- agent/sysproc_windows.go | 47 +++++++++++++++------------------------- 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/agent/sysproc_windows.go b/agent/sysproc_windows.go index 8a82cf4..e5c2c47 100644 --- a/agent/sysproc_windows.go +++ b/agent/sysproc_windows.go @@ -14,31 +14,6 @@ import ( var ntResumeProcess = windows.NewLazySystemDLL("ntdll.dll").NewProc("NtResumeProcess") -// windowsProcessController owns the Job Object of one runExec session; cmd.Cancel and runExec's deferred close both release it. -type windowsProcessController struct { - job windows.Handle - close func() -} - -func (c *windowsProcessController) assign(cmd *exec.Cmd) error { - pid := uint32(cmd.Process.Pid) //nolint:gosec // the OS hands out PIDs as DWORDs; the int round-trip can't overflow - proc, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE|windows.PROCESS_SUSPEND_RESUME, false, pid) - if err != nil { - return fmt.Errorf("open process: %w", err) - } - defer windows.CloseHandle(proc) //nolint:errcheck - - if err := windows.AssignProcessToJobObject(c.job, proc); err != nil { - return fmt.Errorf("assign process to job object: %w", err) - } - return resumeProcess(proc) -} - -func (c *windowsProcessController) cancel() error { - c.close() - return nil -} - // setupProcess creates a kill-on-close Job Object so the child's whole process tree dies with the session. func setupProcess(cmd *exec.Cmd) (processController, error) { job, err := windows.CreateJobObject(nil, nil) @@ -58,16 +33,30 @@ func setupProcess(cmd *exec.Cmd) (processController, error) { return processController{}, fmt.Errorf("set job object limits: %w", err) } - ctl := &windowsProcessController{job: job, close: sync.OnceFunc(func() { _ = windows.CloseHandle(job) })} + closeJob := sync.OnceFunc(func() { _ = windows.CloseHandle(job) }) cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: windows.CREATE_SUSPENDED} - cmd.Cancel = ctl.cancel + cmd.Cancel = func() error { closeJob(); return nil } return processController{ - afterStart: ctl.assign, - close: ctl.close, + afterStart: func(cmd *exec.Cmd) error { return assignToJob(job, cmd) }, + close: closeJob, }, nil } +func assignToJob(job windows.Handle, cmd *exec.Cmd) error { + pid := uint32(cmd.Process.Pid) //nolint:gosec // the OS hands out PIDs as DWORDs; the int round-trip can't overflow + proc, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE|windows.PROCESS_SUSPEND_RESUME, false, pid) + if err != nil { + return fmt.Errorf("open process: %w", err) + } + defer windows.CloseHandle(proc) //nolint:errcheck + + if err := windows.AssignProcessToJobObject(job, proc); err != nil { + return fmt.Errorf("assign process to job object: %w", err) + } + return resumeProcess(proc) +} + func resumeProcess(proc windows.Handle) error { if err := ntResumeProcess.Find(); err != nil { return err From 212eaad03c7f5eb74614409b59fed7779c0683d4 Mon Sep 17 00:00:00 2001 From: CMGS Date: Thu, 24 Sep 2026 01:12:57 +0800 Subject: [PATCH 6/9] docs: the auth boundary is the host-side vsock socket; smoke-test cocoon VMs with cocoon vm exec --- README.md | 2 +- docs/architecture.md | 2 +- docs/usage.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d346c16..2728841 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ PTY mode is planned; see ## Quick start ```bash -cocoon-agent client --cid 3 --port 1024 -- echo "hello from guest" +cocoon vm exec -- echo "hello from guest" ``` Full steps in [Usage](docs/usage.md). diff --git a/docs/architecture.md b/docs/architecture.md index c298eb2..f404a27 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -9,7 +9,7 @@ Cocoon's host components (vk-cocoon, cocoon CLI) and the VMs they manage always - requires per-VM credentials to be provisioned and rotated - comes with the full SSH protocol surface (handshake, key exchange, host key trust) -vsock is host↔guest only, has no IP layer, and the kernel `vhost_vsock` module is the auth boundary — anything that can connect from the host is already privileged on that host. cocoon-agent leverages this for a small focused daemon that gives kubectl-exec semantics (stdin / stdout / stderr / exit code) with none of those dependencies. +vsock is host↔guest only, has no IP layer, and access to the hypervisor's host-side vsock socket (for cocoon, the per-VM hybrid-vsock Unix socket) is the auth boundary — anything that can connect from the host is already privileged on that host. cocoon-agent leverages this for a small focused daemon that gives kubectl-exec semantics (stdin / stdout / stderr / exit code) with none of those dependencies. ## Status diff --git a/docs/usage.md b/docs/usage.md index 1688412..53bdb67 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -2,7 +2,7 @@ ## Smoke test from the host -cocoon-agent ships a `client` subcommand for vsock smoke tests without needing to plumb through cocoon CLI / vk-cocoon. It dials AF_VSOCK by CID, so take the guest CID from the hypervisor's vsock config (cocoon itself reaches the agent over the hybrid-vsock Unix socket that `cocoon vm inspect` reports as `vsock_socket`, not by CID) and run: +cocoon-agent ships a `client` subcommand for vsock smoke tests without needing to plumb through cocoon CLI / vk-cocoon. It dials AF_VSOCK by CID, so it only reaches guests of a vhost-vsock hypervisor (cocoon's CH/FC VMs sit behind a per-VM hybrid-vsock Unix socket; reach them with `cocoon vm exec -- `). Take the guest CID from the hypervisor's vsock config and run: ```bash cocoon-agent client --cid 3 --port 1024 -- echo "hello from guest" From 5b03f5c7f744eefe3c9fa40708b96645e166fb2f Mon Sep 17 00:00:00 2001 From: CMGS Date: Thu, 24 Sep 2026 01:54:42 +0800 Subject: [PATCH 7/9] cut: drop the client subcommand; no cocoon VM is reachable over host AF_VSOCK The subcommand came in 06dee79 for the pre-integration kernel-vsock design. Six hours later cocoon 7174e76 reached the agent over Cloud Hypervisor hybrid vsock (per-VM Unix socket plus CONNECT, constant guest CID 3), so the subcommand never had a target. Nothing in the workspace calls it; the client Go package stays because cocoon imports it. --- README.md | 2 +- cmd/client.go | 49 ---------------------------------------- cmd/root.go | 7 +----- cmd/transport_linux.go | 9 -------- cmd/transport_other.go | 5 ---- cmd/transport_windows.go | 33 +++------------------------ docs/configuration.md | 2 -- docs/index.md | 4 ++-- docs/usage.md | 8 +++---- 9 files changed, 11 insertions(+), 108 deletions(-) delete mode 100644 cmd/client.go diff --git a/README.md b/README.md index 2728841..0de8d74 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ In-VM exec agent for [Cocoon](https://github.com/cocoonstack/cocoon)-managed VMs ## Highlights - Linux and Windows guest exec with stdin, stdout, stderr, and exit-code forwarding. -- Host access through `cocoon vm exec`; direct AF_VSOCK client for smoke tests. +- Host access through `cocoon vm exec`. - Linux clone/restore reseed with optional machine-ID regeneration. PTY mode is planned; see diff --git a/cmd/client.go b/cmd/client.go deleted file mode 100644 index f42feca..0000000 --- a/cmd/client.go +++ /dev/null @@ -1,49 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - - "github.com/cocoonstack/cocoon-agent/agent" - "github.com/cocoonstack/cocoon-agent/client" -) - -// exitCodeError carries the child exit code through cobra's RunE so defers run before os.Exit. -type exitCodeError struct{ code int } - -func (e *exitCodeError) Error() string { return fmt.Sprintf("exit code %d", e.code) } - -func newClientCmd() *cobra.Command { - var ( - cid uint32 - port uint32 - ) - - cmd := &cobra.Command{ - Use: "client [flags] -- ...", - Short: "Run a command on a remote cocoon-agent (debug / smoke test)", - Args: cobra.MinimumNArgs(1), - RunE: func(c *cobra.Command, args []string) error { - ctx := c.Context() - conn, err := dialVsock(cid, port) - if err != nil { - return fmt.Errorf("dial vsock cid=%d port=%d: %w", cid, port, err) - } - // client.Run closes conn via its runCancel goroutine, so no defer Close here - exitCode, err := client.Run(ctx, conn, args, nil, os.Stdin, os.Stdout, os.Stderr) - if err != nil { - return err - } - if exitCode != 0 { - return &exitCodeError{code: exitCode} - } - return nil - }, - } - cmd.Flags().Uint32Var(&cid, "cid", 0, "vsock CID of the target VM (host-side; from hypervisor)") - cmd.Flags().Uint32Var(&port, "port", agent.DefaultPort, "vsock port the agent is listening on") - _ = cmd.MarkFlagRequired("cid") - return cmd -} diff --git a/cmd/root.go b/cmd/root.go index 0e55b49..a898cc6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,7 +3,6 @@ package cmd import ( "context" - "errors" "fmt" "os" "os/signal" @@ -21,12 +20,11 @@ func NewRootCmd() *cobra.Command { Use: "cocoon-agent", Short: "vsock-based command exec agent for Cocoon-managed VMs", Version: fmt.Sprintf("%s (rev=%s built=%s)", version.VERSION, version.REVISION, version.BUILTAT), - // run logs and maps exit codes itself, so cobra's Error/Usage dump would double up + // run logs the error itself, so cobra's Error/Usage dump would double up SilenceErrors: true, SilenceUsage: true, } rootCmd.AddCommand(newServeCmd()) - rootCmd.AddCommand(newClientCmd()) return rootCmd } @@ -53,9 +51,6 @@ func run() int { } if err := NewRootCmd().ExecuteContext(ctx); err != nil { - if ec, ok := errors.AsType[*exitCodeError](err); ok { - return ec.code - } log.WithFunc("cmd.run").Error(ctx, err, "command failed") return 1 } diff --git a/cmd/transport_linux.go b/cmd/transport_linux.go index 58cd2d5..451f075 100644 --- a/cmd/transport_linux.go +++ b/cmd/transport_linux.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "io" "net" "github.com/mdlayher/vsock" @@ -47,14 +46,6 @@ func listenVsock(ctx context.Context, port uint32) (net.Listener, error) { }, nil } -func dialVsock(cid, port uint32) (io.ReadWriteCloser, error) { - conn, err := vsock.Dial(cid, port, nil) - if err != nil { - return nil, fmt.Errorf("vsock dial: %w", err) - } - return conn, nil -} - func isHostPeer(conn net.Conn) bool { addr, ok := conn.RemoteAddr().(*vsock.Addr) if !ok { diff --git a/cmd/transport_other.go b/cmd/transport_other.go index 12fb6a5..90fd106 100644 --- a/cmd/transport_other.go +++ b/cmd/transport_other.go @@ -5,7 +5,6 @@ package cmd import ( "context" "errors" - "io" "net" ) @@ -15,7 +14,3 @@ var errVsockUnsupported = errors.New("vsock is only supported on linux and windo func listenVsock(_ context.Context, _ uint32) (net.Listener, error) { return nil, errVsockUnsupported } - -func dialVsock(_, _ uint32) (io.ReadWriteCloser, error) { - return nil, errVsockUnsupported -} diff --git a/cmd/transport_windows.go b/cmd/transport_windows.go index d21d0c3..d4bc1af 100644 --- a/cmd/transport_windows.go +++ b/cmd/transport_windows.go @@ -37,7 +37,6 @@ var ( procBind = modws2_32.NewProc("bind") procListen = modws2_32.NewProc("listen") procAccept = modws2_32.NewProc("accept") - procConnect = modws2_32.NewProc("connect") procRecv = modws2_32.NewProc("recv") procSend = modws2_32.NewProc("send") procWSAStartup = modws2_32.NewProc("WSAStartup") @@ -192,21 +191,13 @@ type vsockAddr struct { func (a *vsockAddr) Network() string { return "vsock" } func (a *vsockAddr) String() string { return fmt.Sprintf("vsock://%d:%d", a.cid, a.port) } -func newVsockSocket() (windows.Handle, error) { +func listenVsock(ctx context.Context, port uint32) (net.Listener, error) { if err := wsaInit(); err != nil { - return 0, err + return nil, err } h, err := windows.Socket(afVsock, windows.SOCK_STREAM, 0) if err != nil { - return 0, fmt.Errorf("vsock socket: %w", err) - } - return h, nil -} - -func listenVsock(ctx context.Context, port uint32) (net.Listener, error) { - h, err := newVsockSocket() - if err != nil { - return nil, err + return nil, fmt.Errorf("vsock socket: %w", err) } sa := sockaddrVM{Family: afVsock, Port: port, CID: vmAddrCidAny} r, _, callErr := procBind.Call(uintptr(h), uintptr(unsafe.Pointer(&sa)), uintptr(sockaddrVMSize)) //nolint:gosec // winsock bind requires raw pointer @@ -226,21 +217,3 @@ func listenVsock(ctx context.Context, port uint32) (net.Listener, error) { logger: log.WithFunc("cmd.vsockListener.Accept"), }, nil } - -func dialVsock(cid, port uint32) (io.ReadWriteCloser, error) { - h, err := newVsockSocket() - if err != nil { - return nil, err - } - sa := sockaddrVM{Family: afVsock, Port: port, CID: cid} - r, _, callErr := procConnect.Call(uintptr(h), uintptr(unsafe.Pointer(&sa)), uintptr(sockaddrVMSize)) //nolint:gosec // winsock connect requires raw pointer - if r == socketError { - _ = windows.Closesocket(h) - return nil, fmt.Errorf("vsock connect %d:%d: %w", cid, port, callErr) - } - return &vsockConn{ - h: h, - peerCID: cid, - peerPort: port, - }, nil -} diff --git a/docs/configuration.md b/docs/configuration.md index 9fe4eba..d8da913 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -9,5 +9,3 @@ CLI flags: | Flag | Default | Notes | |---|---|---| | `serve --port` | `1024` | vsock port to bind on | -| `client --cid` | required | target VM CID (host-side) | -| `client --port` | `1024` | matches `serve --port` | diff --git a/docs/index.md b/docs/index.md index aa2bb40..71260a3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,8 +26,8 @@ host (cocoon node) guest VM status, the wire protocol, and the roadmap - [Installation](install.md) — baking the Linux binary into an image, the Windows service installer, and pipe-mode limitations -- [Usage](usage.md) — smoke-testing an agent from the host with the - `client` subcommand +- [Usage](usage.md) — smoke-testing an agent from the host with + `cocoon vm exec` - [Configuration](configuration.md) — environment variables and CLI flags - [Build](build.md) — cross-compiling for Linux and Windows guests diff --git a/docs/usage.md b/docs/usage.md index 53bdb67..2314d94 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -2,11 +2,11 @@ ## Smoke test from the host -cocoon-agent ships a `client` subcommand for vsock smoke tests without needing to plumb through cocoon CLI / vk-cocoon. It dials AF_VSOCK by CID, so it only reaches guests of a vhost-vsock hypervisor (cocoon's CH/FC VMs sit behind a per-VM hybrid-vsock Unix socket; reach them with `cocoon vm exec -- `). Take the guest CID from the hypervisor's vsock config and run: +The host reaches the agent through cocoon: `cocoon vm exec` dials the VM's hybrid-vsock Unix socket and speaks the agent protocol. ```bash -cocoon-agent client --cid 3 --port 1024 -- echo "hello from guest" -echo "world" | cocoon-agent client --cid 3 --port 1024 -- cat +cocoon vm exec -- echo "hello from guest" +echo "world" | cocoon vm exec -i -- cat ``` -Exit codes pass through. +Stdin is attached only with `-i`; exit codes pass through. From 00a02dee943a5263c791620340434776f58b2865 Mon Sep 17 00:00:00 2001 From: CMGS Date: Thu, 24 Sep 2026 10:32:21 +0800 Subject: [PATCH 8/9] build: keep projecteru2/core at v0.1.3 The agent borrows only core/log; the round's bump moved core and its sentry-go without a reason. vsock, x/sys and the grpc security fix stay. --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 7f6eb6e..cfeb337 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.27.0 require ( github.com/mdlayher/vsock v1.3.0 - github.com/projecteru2/core v0.1.5 + github.com/projecteru2/core v0.1.3 github.com/spf13/cobra v1.10.2 golang.org/x/sys v0.48.0 ) @@ -14,7 +14,7 @@ require ( github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect github.com/cockroachdb/redact v1.1.8 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/getsentry/sentry-go v0.49.0 // indirect + github.com/getsentry/sentry-go v0.48.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/pretty v0.3.1 // indirect diff --git a/go.sum b/go.sum index 89a4e99..328c924 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/getsentry/sentry-go v0.49.0 h1:Ehejknu1l023Ub7QoRBVLAI7g3Jnhqku4oWx4B4Sh5s= -github.com/getsentry/sentry-go v0.49.0/go.mod h1:nuMJAoCfe1u0Bts2ocyNI+TW8HT84vRMqwA5Qq/SKUI= +github.com/getsentry/sentry-go v0.48.0 h1:FRZNr7Uk1C86ev1bSJmYlUkL9oyivQA6YOcdYfaaMmY= +github.com/getsentry/sentry-go v0.48.0/go.mod h1:E5UkA5wp1qR2+MDydNYlVeUiNN2xEdjYMidkgf0Qoss= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -37,8 +37,8 @@ github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTw github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/projecteru2/core v0.1.5 h1:cS3xfqa6I1bMXtGdbu2A2UzhjiHaP2mogjQWFTkSEF0= -github.com/projecteru2/core v0.1.5/go.mod h1:MDMJmmlJ3ZVvFQl5BnRcKV1/AVCHyiWoI4BI+sCnuYM= +github.com/projecteru2/core v0.1.3 h1:0odzNdtDKRvbMir3xKpqCPJaThHdzfzvou/vGFWquZY= +github.com/projecteru2/core v0.1.3/go.mod h1:4WGV1OlU8TY0hra+N8Ua2s8hbX1AeJmcjnj3PcztnI4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g= github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= From 6183712a299f5e4d4550b832ed696f3a0f1d603b Mon Sep 17 00:00:00 2001 From: CMGS Date: Thu, 24 Sep 2026 10:35:44 +0800 Subject: [PATCH 9/9] build: keep googleapis/rpc at main's version It moved only with the reverted core bump and is not linked into the binary. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index cfeb337..6a4d343 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( golang.org/x/net v0.58.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/text v0.41.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260918162117-cecb64721679 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.12 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect diff --git a/go.sum b/go.sum index 328c924..7e8a935 100644 --- a/go.sum +++ b/go.sum @@ -96,8 +96,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260918162117-cecb64721679 h1:KmqdJU4vrNcxy/6qdg3JduZtalEXrJLspVltnR1cE+8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260918162117-cecb64721679/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 h1:1VUiZAXyC+zmiFYi+WLtBzr68Cj8wOofHjjrA/kkizc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=