Skip to content

Instantly share code, notes, and snippets.

@smoser
Last active October 16, 2025 17:54
Show Gist options
  • Save smoser/cadc1a0c78484fc4d822b8ccab73c0f5 to your computer and use it in GitHub Desktop.
Save smoser/cadc1a0c78484fc4d822b8ccab73c0f5 to your computer and use it in GitHub Desktop.
vsock shell - provide an ssh like experience over vsock

vsock-shell

A lightweight tool for executing commands over VM sockets (AF_VSOCK), providing SSH-like functionality for virtual machine communication. It supports both interactive shell sessions and non-interactive command execution, similar to SSH.

It may be useful for host->guest use cases where ssh is not needed.

Note, there is exactly zero security here.

  • No authentication - any connection will get a prompt.
  • No encryption - data is plaintext

If that didn't scare you off, read below for more.

Features

  • Interactive shell sessions with PTY allocation
  • Non-interactive command execution for scripting
  • Exit code propagation from remote commands
  • Multiple concurrent connections (server mode)
  • Single-connection mode for one-off commands
  • No Inherent Shell - ssh passes commands through the user's shell. Commands invoked with vsock-shell client are executed directly. If you want a shell, then sh -c "your command".

Building

go build -o vsock-shell vsock-shell.go

Usage

Server Mode

Start the server (typically inside a VM):

# Listen on default port 9999
./vsock-shell serve

# Listen on a specific port
./vsock-shell serve -p 1042

# Accept one connection then exit
./vsock-shell serve -p 9998 --single
./vsock-shell serve -p 9998 -1          # shorthand

The server will display its CID and listening port:

2025/10/16 14:05:44 Local CID: 3464389509
2025/10/16 14:05:44 Listening on vsock(3464389509:9999)

In order to start a guest with vsock support in qemu, you need to invoke qemu with arguments like the following. CID is a unique 32 bit unsigned number.

-device vhost-vsock-pci,guest-cid=<CID>

Client Mode

Interactive Shell

Connect to a remote shell (PTY mode):

# Connect to CID on default port
./vsock-shell exec 3464389509
./vsock-shell x 3464389509        # shorthand

# Connect to specific port
./vsock-shell exec -p 9998 3464389509
./vsock-shell x -p 9998 3464389509

This provides a full interactive shell with PTY support.

Non-Interactive Commands

Execute commands without a PTY:

# Run a single command
./vsock-shell exec -p 9998 3464389509 cat /etc/issue
./vsock-shell x -p 9998 3464389509 cat /etc/issue

# Run command with arguments
./vsock-shell x -p 9998 3464389509 echo "Hello World"

# Pipe input to command
echo "test data" | ./vsock-shell x -p 9998 3464389509 cat

# Use in scripts (exit codes are preserved)
if ./vsock-shell x -p 9998 3464389509 test -f /etc/passwd; then
    echo "File exists"
fi

Force PTY Allocation

Use -t to force PTY allocation even when running a command:

# Run command with PTY
./vsock-shell x -t -p 9998 3464389509 /bin/bash

# Useful for commands that need a terminal
./vsock-shell x -t -p 9998 3464389509 top

Testing / loopback

If you have the vsock_loopback module loaded, then you can test this entirely inside a guest. Use the special CID 1 (VMADDR_CID_LOCAL). Just run the daemon as you would, and then:

./vsock-shell x -t -p 9998 1 top

Command-Line Options

Server Command

./vsock-shell serve [flags]

Flags:

  • -p, --port <port> - Port to listen on (default: 9999)
  • --single - Exit after handling one client connection
  • -1, --one - Exit after handling one client connection (shorthand)

Client Command

./vsock-shell exec CID [command...] [flags]
./vsock-shell x CID [command...] [flags]    # shorthand

Arguments:

  • CID - Context ID of the VM to connect to (required)
  • command... - Optional command to execute (if omitted, opens interactive shell)

Flags:

  • -p, --port <port> - Port to connect to (default: 9999)
  • -t, --tty - Force PTY allocation (for interactive programs)

Examples

Example 1: Basic Server/Client

On the VM (server):

./vsock-shell serve -p 9998

On the host (client):

# Get VM's /etc/issue
./vsock-shell x -p 9998 3464389509 cat /etc/issue

# Interactive shell
./vsock-shell x -p 9998 3464389509

Example 2: One-Shot Command Execution

On the VM:

./vsock-shell serve -p 9998 --single

On the host:

# This will execute and the server will exit
./vsock-shell x -p 9998 3464389509 uptime

Example 3: Exit Code Handling

# Success (exit code 0)
./vsock-shell x -p 9998 3464389509 /bin/true
echo $?  # prints: 0

# Failure (exit code 1)
./vsock-shell x -p 9998 3464389509 /bin/false
echo $?  # prints: 1

# Custom exit code
./vsock-shell x -p 9998 3464389509 sh -c 'exit 42'
echo $?  # prints: 42

Example 4: Script Usage

#!/bin/bash
CID=3464389509
PORT=9998

# Run multiple commands
./vsock-shell x -p $PORT $CID hostname
./vsock-shell x -p $PORT $CID uptime
./vsock-shell x -p $PORT $CID df -h

# Conditional execution based on exit code
if ./vsock-shell x -p $PORT $CID test -d /tmp; then
    echo "/tmp exists"
fi

How It Works

PTY Mode (Interactive)

  • Allocates a pseudo-terminal on the server
  • Supports interactive programs (shells, editors, etc.)
  • Raw terminal mode on client for proper character handling
  • Exit code sent via JSON after session ends

Non-PTY Mode (Commands)

  • Direct stdin/stdout/stderr piping
  • Efficient for scripting and automation
  • Exit code sent as final byte after command output

Exit Codes

  • 0 - Success
  • 1-254 - Command's exit code
  • 255 - Connection error or internal error

Logging

The server logs connection events:

2025/10/16 14:08:49 [conn-1] Client connected
2025/10/16 14:08:57 [conn-1] Client disconnected

Requirements

  • Linux with AF_VSOCK support
  • Go 1.16+ (for building)
  • /dev/vsock device

Dependencies

  • github.com/creack/pty - PTY support
  • github.com/spf13/cobra - Command-line interface
  • golang.org/x/sys/unix - Unix system calls
  • golang.org/x/term - Terminal handling

License

This tool is provided as-is for VM communication purposes.

Links

These links were helpful.

module vsock-shell
go 1.25.3
require (
github.com/creack/pty v1.1.24
golang.org/x/sys v0.37.0
golang.org/x/term v0.36.0
)
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/cobra v1.10.1 // indirect
github.com/spf13/pflag v1.0.9 // indirect
)
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"os"
"os/exec"
"sync"
"sync/atomic"
"time"
"unsafe"
"github.com/creack/pty"
"github.com/spf13/cobra"
"golang.org/x/sys/unix"
"golang.org/x/term"
)
const (
// IOCTL command to get local CID
IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x7b9
defaultPort = 9999
// Channel IDs for multiplexing
channelStdin = 0
channelStdout = 1
channelStderr = 2
channelControl = 3
// Frame header size: 1 byte channel + 4 bytes length
frameHeaderSize = 5
maxFrameSize = 256 * 1024 // Increased from 32KB to 256KB for better throughput
)
// ClientRequest is sent from client to server to specify execution mode
type ClientRequest struct {
UsePTY bool `json:"use_pty"`
Command []string `json:"command"` // empty means interactive shell
}
// ServerResponse is sent from server to client with the exit code
type ServerResponse struct {
ExitCode int `json:"exit_code"`
}
var (
port int
single bool
forcePTY bool
connID int32
)
// Frame represents a multiplexed data frame
type Frame struct {
Channel byte
Data []byte
}
// writeFrame writes a frame to the connection using vectored I/O for efficiency
func writeFrame(w io.Writer, channel byte, data []byte) error {
if len(data) > maxFrameSize {
return fmt.Errorf("frame data too large: %d > %d", len(data), maxFrameSize)
}
header := make([]byte, frameHeaderSize)
header[0] = channel
// Write length in big-endian (network byte order)
header[1] = byte(len(data) >> 24)
header[2] = byte(len(data) >> 16)
header[3] = byte(len(data) >> 8)
header[4] = byte(len(data))
// Try to use writev for efficient writing (single syscall)
if file, ok := w.(*os.File); ok && len(data) > 0 {
// Use writev to write header and data in one syscall
buffers := [][]byte{header, data}
_, err := unix.Writev(int(file.Fd()), buffers)
return err
}
// Fallback for non-file writers
if _, err := w.Write(header); err != nil {
return err
}
if len(data) > 0 {
if _, err := w.Write(data); err != nil {
return err
}
}
return nil
}
// readFrame reads a frame from the connection
func readFrame(r io.Reader) (*Frame, error) {
header := make([]byte, frameHeaderSize)
if _, err := io.ReadFull(r, header); err != nil {
return nil, err
}
channel := header[0]
length := int(header[1])<<24 | int(header[2])<<16 | int(header[3])<<8 | int(header[4])
if length > maxFrameSize {
return nil, fmt.Errorf("frame too large: %d > %d", length, maxFrameSize)
}
data := make([]byte, length)
if length > 0 {
if _, err := io.ReadFull(r, data); err != nil {
return nil, err
}
}
return &Frame{Channel: channel, Data: data}, nil
}
// getLocalCID retrieves the local CID by performing an ioctl on /dev/vsock
func getLocalCID() (uint32, error) {
f, err := os.Open("/dev/vsock")
if err != nil {
return 0, fmt.Errorf("failed to open /dev/vsock: %w", err)
}
defer f.Close()
var cid uint32
_, _, errno := unix.Syscall(
unix.SYS_IOCTL,
f.Fd(),
IOCTL_VM_SOCKETS_GET_LOCAL_CID,
uintptr(unsafe.Pointer(&cid)),
)
if errno != 0 {
return 0, fmt.Errorf("ioctl failed: %v", errno)
}
return cid, nil
}
// runServer starts the vsock server
func runServer() error {
cid, err := getLocalCID()
if err != nil {
return fmt.Errorf("failed to get local CID: %w", err)
}
log.Printf("Local CID: %d", cid)
// Create a vsock socket
fd, err := unix.Socket(unix.AF_VSOCK, unix.SOCK_STREAM, 0)
if err != nil {
return fmt.Errorf("failed to create socket: %w", err)
}
defer unix.Close(fd)
// Bind to the vsock address
sockaddr := &unix.SockaddrVM{
CID: unix.VMADDR_CID_ANY,
Port: uint32(port),
}
if err := unix.Bind(fd, sockaddr); err != nil {
return fmt.Errorf("failed to bind: %w", err)
}
// Listen for connections
if err := unix.Listen(fd, 128); err != nil {
return fmt.Errorf("failed to listen: %w", err)
}
log.Printf("Listening on vsock(%d:%d)", cid, port)
// Accept connections in a loop
for {
clientFd, _, err := unix.Accept(fd)
if err != nil {
log.Printf("Accept error: %v", err)
continue
}
if single {
// Handle client and exit after it's done
handleClient(clientFd)
return nil
} else {
// Handle each client in a goroutine
go handleClient(clientFd)
}
}
}
// handleClient handles an individual client connection
func handleClient(clientFd int) {
id := atomic.AddInt32(&connID, 1)
connName := fmt.Sprintf("conn-%d", id)
log.Printf("[%s] Client connected", connName)
defer log.Printf("[%s] Client disconnected", connName)
// Create a file from the client fd for easier I/O
// Note: closing clientFile will also close the FD
clientFile := os.NewFile(uintptr(clientFd), "vsock-client")
defer clientFile.Close()
// Read the client request
var req ClientRequest
decoder := json.NewDecoder(clientFile)
if err := decoder.Decode(&req); err != nil {
log.Printf("[%s] Failed to read client request: %v", connName, err)
return
}
// Determine what to execute
var cmdArgs []string
if len(req.Command) == 0 {
// No command specified, use default shell
shell := os.Getenv("SHELL")
if shell == "" {
shell = "/bin/sh"
}
cmdArgs = []string{shell}
} else {
cmdArgs = req.Command
}
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
cmd.Env = os.Environ()
var exitCode int
if req.UsePTY {
// PTY mode
ptmx, err := pty.Start(cmd)
if err != nil {
log.Printf("[%s] Failed to start command with PTY: %v", connName, err)
exitCode = 255
// Send error exit code
resp := ServerResponse{ExitCode: exitCode}
json.NewEncoder(clientFile).Encode(&resp)
return
}
defer ptmx.Close()
// Copy data bidirectionally
var wg sync.WaitGroup
wg.Add(2)
// Client -> PTY
go func() {
defer wg.Done()
io.Copy(ptmx, clientFile)
}()
// PTY -> Client
go func() {
defer wg.Done()
io.Copy(clientFile, ptmx)
}()
// Wait for the command to exit
if err := cmd.Wait(); err != nil {
log.Printf("[%s] Command error: %v", connName, err)
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode = exitErr.ExitCode()
} else {
exitCode = 255
}
} else {
exitCode = 0
}
// Close the PTY to stop writing to client
ptmx.Close()
// Shutdown read side to unblock the Client->PTY goroutine
unix.Shutdown(int(clientFile.Fd()), unix.SHUT_RD)
// Wait for goroutines
wg.Wait()
// Send exit code to client
resp := ServerResponse{ExitCode: exitCode}
json.NewEncoder(clientFile).Encode(&resp)
} else {
// Non-PTY mode: use pipes for stdin/stdout/stderr and multiplex them
// Create pipes for stdin, stdout, stderr
stdinPipe, err := cmd.StdinPipe()
if err != nil {
log.Printf("[%s] Failed to create stdin pipe: %v", connName, err)
exitCode = 255
respData, _ := json.Marshal(ServerResponse{ExitCode: exitCode})
writeFrame(clientFile, channelControl, respData)
return
}
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
log.Printf("[%s] Failed to create stdout pipe: %v", connName, err)
exitCode = 255
respData, _ := json.Marshal(ServerResponse{ExitCode: exitCode})
writeFrame(clientFile, channelControl, respData)
return
}
stderrPipe, err := cmd.StderrPipe()
if err != nil {
log.Printf("[%s] Failed to create stderr pipe: %v", connName, err)
exitCode = 255
respData, _ := json.Marshal(ServerResponse{ExitCode: exitCode})
writeFrame(clientFile, channelControl, respData)
return
}
if err := cmd.Start(); err != nil {
log.Printf("[%s] Failed to start command: %v", connName, err)
exitCode = 255
respData, _ := json.Marshal(ServerResponse{ExitCode: exitCode})
writeFrame(clientFile, channelControl, respData)
return
}
var wg sync.WaitGroup
var stdinWg sync.WaitGroup
// Track stdout/stderr completion separately
wg.Add(2)
stdinWg.Add(1)
// Client stdin -> Command stdin
go func() {
defer stdinWg.Done()
defer stdinPipe.Close()
for {
frame, err := readFrame(clientFile)
if err != nil {
return
}
if frame.Channel == channelStdin {
if len(frame.Data) > 0 {
stdinPipe.Write(frame.Data)
}
}
}
}()
// Command stdout -> Client
go func() {
defer wg.Done()
buf := make([]byte, maxFrameSize)
for {
n, err := stdoutPipe.Read(buf)
if n > 0 {
writeFrame(clientFile, channelStdout, buf[:n])
}
if err != nil {
return
}
}
}()
// Command stderr -> Client
go func() {
defer wg.Done()
buf := make([]byte, maxFrameSize)
for {
n, err := stderrPipe.Read(buf)
if n > 0 {
writeFrame(clientFile, channelStderr, buf[:n])
}
if err != nil {
return
}
}
}()
// Wait for stdout/stderr to be fully read (they'll get EOF when command exits)
wg.Wait()
// Now it's safe to call Wait - all pipe data has been read
if err := cmd.Wait(); err != nil {
log.Printf("[%s] Command error: %v", connName, err)
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode = exitErr.ExitCode()
} else {
exitCode = 255
}
} else {
exitCode = 0
}
// Shutdown read side to unblock stdin reader goroutine
unix.Shutdown(int(clientFile.Fd()), unix.SHUT_RD)
// Wait for stdin goroutine to finish
stdinWg.Wait()
// Send exit code on control channel
resp := ServerResponse{ExitCode: exitCode}
respData, _ := json.Marshal(resp)
writeFrame(clientFile, channelControl, respData)
}
}
// runClient connects to a vsock server
func runClient(cid uint32, command []string) error {
// Create a vsock socket
fd, err := unix.Socket(unix.AF_VSOCK, unix.SOCK_STREAM, 0)
if err != nil {
return fmt.Errorf("failed to create socket: %w", err)
}
defer unix.Close(fd)
// Connect to the server
sockaddr := &unix.SockaddrVM{
CID: cid,
Port: uint32(port),
}
if err := unix.Connect(fd, sockaddr); err != nil {
return fmt.Errorf("failed to connect: %w", err)
}
// Create a file from the socket for easier I/O
conn := os.NewFile(uintptr(fd), "vsock-conn")
// Determine if we need PTY
// PTY is needed if: no command provided OR -t flag is set
usePTY := len(command) == 0 || forcePTY
// Send the client request
req := ClientRequest{
UsePTY: usePTY,
Command: command,
}
encoder := json.NewEncoder(conn)
if err := encoder.Encode(&req); err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
if usePTY {
// PTY mode: set terminal to raw mode if stdin is a terminal
var oldState *term.State
if term.IsTerminal(int(os.Stdin.Fd())) {
oldState, err = term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
return fmt.Errorf("failed to set raw mode: %w", err)
}
defer term.Restore(int(os.Stdin.Fd()), oldState)
}
// Channel to receive exit code
exitCodeChan := make(chan int, 1)
// Stdin -> Server
go func() {
io.Copy(conn, os.Stdin)
}()
// Server -> Stdout, then read exit code JSON
go func() {
// Copy all PTY output to stdout until the JSON starts
// We need to detect the JSON exit code message
buf := make([]byte, 32*1024)
var jsonBuf []byte
for {
n, err := conn.Read(buf)
if n > 0 {
// Check if this contains the start of our JSON message
data := buf[:n]
// Look for {"exit_code": pattern
jsonStart := -1
for i := 0; i < len(data); i++ {
if i <= len(data)-13 && string(data[i:i+13]) == `{"exit_code":` {
jsonStart = i
break
}
}
if jsonStart >= 0 {
// Write everything before the JSON
if jsonStart > 0 {
os.Stdout.Write(data[:jsonStart])
}
// Start collecting JSON
jsonBuf = append(jsonBuf, data[jsonStart:]...)
break
} else {
// No JSON yet, write all output
os.Stdout.Write(data)
}
}
if err != nil {
exitCodeChan <- 255 // Error reading
return
}
}
// Continue reading to get complete JSON
for {
n, err := conn.Read(buf)
if n > 0 {
jsonBuf = append(jsonBuf, buf[:n]...)
}
if err != nil {
break
}
}
// Parse the JSON exit code
var resp ServerResponse
if err := json.Unmarshal(jsonBuf, &resp); err != nil {
exitCodeChan <- 255
} else {
exitCodeChan <- resp.ExitCode
}
}()
// Wait for exit code
exitCode := <-exitCodeChan
// Restore terminal before exiting
if oldState != nil {
term.Restore(int(os.Stdin.Fd()), oldState)
}
os.Exit(exitCode)
} else {
// Non-PTY mode: use framing protocol for stdin/stdout/stderr
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
exitCodeChan := make(chan int, 1)
var wg sync.WaitGroup
wg.Add(2)
// Stdin -> Server (on stdin channel)
go func() {
defer wg.Done()
buf := make([]byte, maxFrameSize)
for {
select {
case <-ctx.Done():
return
default:
}
n, err := os.Stdin.Read(buf)
if n > 0 {
if err := writeFrame(conn, channelStdin, buf[:n]); err != nil {
return
}
}
if err != nil {
return
}
}
}()
// Server -> Stdout/Stderr
go func() {
defer wg.Done()
for {
frame, err := readFrame(conn)
if err != nil {
exitCodeChan <- 255
return
}
switch frame.Channel {
case channelStdout:
os.Stdout.Write(frame.Data)
case channelStderr:
os.Stderr.Write(frame.Data)
case channelControl:
// Parse exit code
var resp ServerResponse
if err := json.Unmarshal(frame.Data, &resp); err != nil {
exitCodeChan <- 255
} else {
exitCodeChan <- resp.ExitCode
}
// Cancel context to stop stdin reader
cancel()
return
}
}
}()
// Wait for exit code
exitCode := <-exitCodeChan
// Give goroutines a moment to finish, but don't wait forever
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
case <-time.After(100 * time.Millisecond):
// Force exit if goroutines don't finish quickly
}
os.Exit(exitCode)
}
return nil // unreachable
}
func main() {
var rootCmd = &cobra.Command{
Use: "vsock-shell",
Short: "A tool for executing commands over VM sockets",
Long: `vsock-shell provides SSH-like functionality for virtual machine communication using VSOCK sockets.`,
}
var serverCmd = &cobra.Command{
Use: "serve",
Short: "Run in server mode",
Long: `Start a vsock-shell server that listens for client connections and executes commands.`,
RunE: func(cmd *cobra.Command, args []string) error {
return runServer()
},
}
serverCmd.Flags().IntVarP(&port, "port", "p", defaultPort, "Port to listen on")
serverCmd.Flags().BoolVar(&single, "single", false, "Exit after handling one client connection")
serverCmd.Flags().BoolVarP(&single, "one", "1", false, "Exit after handling one client connection (shorthand)")
var clientCmd = &cobra.Command{
Use: "exec CID [command...]",
Aliases: []string{"x"},
Short: "Run in client mode",
Long: `Connect to a vsock-shell server and execute commands or open an interactive shell.`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var cid uint32
if _, err := fmt.Sscanf(args[0], "%d", &cid); err != nil {
return fmt.Errorf("invalid CID: %v", err)
}
command := args[1:]
return runClient(cid, command)
},
}
clientCmd.Flags().IntVarP(&port, "port", "p", defaultPort, "Port to connect to")
clientCmd.Flags().BoolVarP(&forcePTY, "tty", "t", false, "Force PTY allocation")
rootCmd.AddCommand(serverCmd)
rootCmd.AddCommand(clientCmd)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment