Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ IS_UBUNTU_24_OR_NEWER := $(shell \

RUN_NETLAUNCH_TRIDENT_BIN ?= $(if $(filter yes,$(IS_UBUNTU_24_OR_NEWER)),bin/trident-azl3,bin/trident)

.PHONY: run-netlaunch run-netlaunch-stream
.PHONY: run-netlaunch run-netlaunch-stream run-netlaunch-proxy
run-netlaunch: $(NETLAUNCH_CONFIG) $(TRIDENT_CONFIG) $(NETLAUNCH_ISO) bin/netlaunch validate artifacts/osmodifier $(RUN_NETLAUNCH_TRIDENT_BIN)
@echo "Using trident binary: $(RUN_NETLAUNCH_TRIDENT_BIN)"
@mkdir -p artifacts/test-image
Expand All @@ -579,20 +579,35 @@ run-netlaunch-stream: $(NETLAUNCH_CONFIG) $(TRIDENT_CONFIG) $(NETLAUNCH_ISO) bin
@cp $(RUN_NETLAUNCH_TRIDENT_BIN) artifacts/test-image/trident
@cp artifacts/osmodifier artifacts/test-image/
@bin/netlaunch \
--stream-image \
--trident-binary $(RUN_NETLAUNCH_TRIDENT_BIN) \
--osmodifier-binary artifacts/osmodifier \
--rcp-agent-mode cli \
--rcp-agent-mode grpc-stream \
--iso $(NETLAUNCH_ISO) \
$(if $(NETLAUNCH_PORT),--port $(NETLAUNCH_PORT)) \
--config $(NETLAUNCH_CONFIG) \
--trident $(TRIDENT_CONFIG) \
--logstream \

Comment thread
frhuelsz marked this conversation as resolved.
Outdated
--remoteaddress remote-addr \
--servefolder artifacts/test-image \
--trace-file trident-metrics.jsonl \
$(if $(LOG_TRACE),--log-trace)

run-netlaunch-proxy: $(NETLAUNCH_CONFIG) $(NETLAUNCH_ISO) bin/netlaunch artifacts/osmodifier $(RUN_NETLAUNCH_TRIDENT_BIN)
@echo "Using trident binary: $(RUN_NETLAUNCH_TRIDENT_BIN)"
@mkdir -p artifacts/test-image
@cp $(RUN_NETLAUNCH_TRIDENT_BIN) artifacts/test-image/trident
@cp artifacts/osmodifier artifacts/test-image/
@bin/netlaunch \
--trident-binary $(RUN_NETLAUNCH_TRIDENT_BIN) \
--osmodifier-binary artifacts/osmodifier \
--rcp-agent-mode grpc-local-proxy \
--iso $(NETLAUNCH_ISO) \
$(if $(NETLAUNCH_PORT),--port $(NETLAUNCH_PORT)) \
--config $(NETLAUNCH_CONFIG) \
--servefolder artifacts/test-image \
--trace-file trident-metrics.jsonl \
$(if $(LOG_TRACE),--log-trace)

# To run this, VM requires at least 11 GiB of memory (virt-deploy create --mem 11).
.PHONY: run-netlaunch-container-images
run-netlaunch-container-images: \
Expand Down
28 changes: 22 additions & 6 deletions tools/cmd/netlaunch/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,14 @@ var (
tridentBinaryPath string
osmodifierBinaryPath string
streamImage bool
localProxyPath string
)

const (
rcpModeLegacy = "cli"
rcpModeGrpc = "grpc"
rcpModeLegacy = "cli"
rcpModeGrpcLocalProxy = "grpc-local-proxy"
rcpModeGrpcInstall = "grpc-install"
rcpModeGrpcStream = "grpc-stream"
)

var backgroundLogstreamFull string
Expand Down Expand Up @@ -74,8 +77,8 @@ var rootCmd = &cobra.Command{

if rcpMode != "" {
log.Infof("Using RCP mode: %s", rcpMode)
if rcpMode != rcpModeGrpc && rcpMode != rcpModeLegacy {
log.Fatalf("Invalid RCP mode, must be: %s or %s, got: %s", rcpModeLegacy, rcpModeGrpc, rcpMode)
if rcpMode != rcpModeGrpcLocalProxy && rcpMode != rcpModeGrpcInstall && rcpMode != rcpModeGrpcStream && rcpMode != rcpModeLegacy {
log.Fatalf("Invalid RCP mode, must be: %s, %s, %s or %s, got: %s", rcpModeLegacy, rcpModeGrpcLocalProxy, rcpModeGrpcInstall, rcpModeGrpcStream, rcpMode)
}
} else {
if tridentBinaryPath != "" {
Expand Down Expand Up @@ -118,8 +121,20 @@ var rootCmd = &cobra.Command{
config.MaxPhonehomeFailures = maxFailures

if rcpMode != "" {
config.Rcp = &netlaunch.RcpConfiguration{
GrpcMode: rcpMode == rcpModeGrpc,
config.Rcp = &netlaunch.RcpConfiguration{}

// Map the CLI RCP mode to the config GRPC mode.
switch rcpMode {
case rcpModeGrpcLocalProxy:
config.Rcp.GrpcMode = netlaunch.GrpcModeLocalProxy
config.Rcp.LocalProxySocket = localProxyPath
log.Infof("Using local proxy socket path: %s", localProxyPath)
case rcpModeGrpcInstall:
config.Rcp.GrpcMode = netlaunch.GrpcModeInstall
case rcpModeGrpcStream:
config.Rcp.GrpcMode = netlaunch.GrpcModeStream
case rcpModeLegacy:
config.Rcp.GrpcMode = netlaunch.GrpcModeDisabled
}

if tridentBinaryPath != "" {
Expand Down Expand Up @@ -170,6 +185,7 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&tridentBinaryPath, "trident-binary", "", "", "Optional path to Trident binary to be copied into the VM, requires RCP mode.")
Comment on lines 184 to 185

Copilot AI Mar 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The --rcp-agent-mode flag help text still says "(grpc|cli)", but the CLI now accepts multiple gRPC modes (grpc-local-proxy / grpc-install / grpc-stream). Update the help string to list the supported values so users discover the new modes via --help.

Copilot uses AI. Check for mistakes.
rootCmd.PersistentFlags().StringVarP(&osmodifierBinaryPath, "osmodifier-binary", "", "", "Optional path to Osmodifier binary to be copied into the VM, requires RCP mode.")
rootCmd.PersistentFlags().BoolVarP(&streamImage, "stream-image", "", false, "Use stream image for installation instead of the default method, requires RCP mode.")
rootCmd.PersistentFlags().StringVarP(&localProxyPath, "local-proxy-socket", "", "/tmp/rcp_local_proxy.sock", "Path to the local proxy socket to use when RCP mode is grpc-local-proxy")
rootCmd.Flags().StringVarP(&iso, "iso", "i", "", "ISO for Netlaunch testing")
rootCmd.MarkFlagRequired("iso-template")
}
Expand Down
52 changes: 41 additions & 11 deletions tools/pkg/netlaunch/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,9 @@ type NetLaunchConfig struct {

// Configuration for netlaunch reverse-connect proxy.
type RcpConfiguration struct {
// Run netlaunch in gRPC mode. When true, netlaunch will use the
// reverse-connect proxy to communicate with Trident using gRPC. When false,
// netlaunch will use the reverse-connect proxy to download the Host
// Configuration file and start the legacy installation service.
//
// If omitted, defaults to false.
GrpcMode bool `yaml:"grpcMode,omitempty"`
// gRPC mode to use in netlaunch. If not specified, gRPC mode will be
// disabled and netlaunch will use the legacy CLI installation method.
GrpcMode GrpcMode `yaml:"grpcMode,omitempty"`

// Port number to listen on for incoming connections from the
// reverse-connect proxy.
Expand All @@ -84,15 +80,49 @@ type RcpConfiguration struct {
// If not specified, no Trident binary will be copied.
LocalTridentPath *string `yaml:"localTridentPath,omitempty"`

// An optional path to a local osmodifier binary to copy into the remote host.
// If not specified, no Osmodifier binary will be copied.
// An optional path to a local osmodifier binary to copy into the remote
// host. If not specified, no Osmodifier binary will be copied.
LocalOsmodifierPath *string `yaml:"localOsmodifierPath,omitempty"`

// Replace the execution for trident-install to use stream image instead of
// the default installation method.
UseStreamImage bool `yaml:"useStreamImage,omitempty"`

// The local Unix socket netlaunch will listen on when gRPC mode is
// `local-proxy`.
LocalProxySocket string `yaml:"localProxySocket,omitempty"`
}

func (c *RcpConfiguration) GetGrpcMode() GrpcMode {
if c.GrpcMode == "" {
return GrpcModeDisabled
}

return c.GrpcMode
}

func (c *RcpConfiguration) IsGrpcModeEnabled() bool {
return c.GetGrpcMode() != GrpcModeDisabled
}

type GrpcMode string

const (
// GrpcModeDisabled: gRPC mode is disabled and netlaunch will use the legacy
// CLI installation method.
GrpcModeDisabled GrpcMode = "disabled"
// GrpcModeLocalProxy: gRPC mode is enabled and netlaunch will open a local
// listener and forward connections to Trident via the reverse-connect
// proxy.
GrpcModeLocalProxy GrpcMode = "local-proxy"
// GrpcModeDirect: gRPC mode is enabled and netlaunch will directly connect
Comment thread
frhuelsz marked this conversation as resolved.
Outdated
// to Trident via the reverse-connect proxy to perform an install.
GrpcModeInstall GrpcMode = "install"
// GrpcModeStream: gRPC mode is enabled and netlaunch will directly connect
// to Trident via the reverse-connect proxy to stream a disk image.
GrpcModeStream GrpcMode = "stream"
)

type HostConnectionConfiguration struct {
// Configuration for physical/emulated BMCs.

Expand Down Expand Up @@ -142,6 +172,6 @@ type NetListenConfig struct {
}
}

func (c *NetLaunchConfig) IsGrpcMode() bool {
return c.Rcp != nil && c.Rcp.GrpcMode
func (c *NetLaunchConfig) IsGrpcModeEnabled() bool {
return c.Rcp != nil && c.Rcp.IsGrpcModeEnabled()
}
61 changes: 61 additions & 0 deletions tools/pkg/netlaunch/grpc.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,78 @@
package netlaunch

import (
"context"
"errors"
"fmt"
"io"
"net"
"tridenttools/pkg/tridentgrpc"
"tridenttools/pkg/tridentgrpc/tridentpbv1"
"tridenttools/pkg/tridentgrpc/tridentpbv1preview"

"github.com/fatih/color"
"github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
Comment thread
frhuelsz marked this conversation as resolved.
)

func doGrpcInstall(ctx context.Context, conn net.Conn, hostConfiguration string) error {
tridentClient, err := tridentgrpc.NewTridentClientFromNetworkConnection(conn)
if err != nil {
return fmt.Errorf("failed to create Trident gRPC client from RCP connection: %w", err)
}
defer tridentClient.Close()

stream, err := tridentClient.Install(ctx, &tridentpbv1preview.InstallRequest{
Stage: &tridentpbv1preview.StageInstallRequest{
Config: &tridentpbv1preview.HostConfiguration{
Config: hostConfiguration,
},
},
Finalize: &tridentpbv1preview.FinalizeInstallRequest{
Reboot: &tridentpbv1.RebootManagement{
Handling: tridentpbv1.RebootHandling_TRIDENT_HANDLES_REBOOT,
},
},
})
if err != nil {
return fmt.Errorf("failed to start installation via gRPC: %w", err)
}

err = handleServicingResponseStream(stream)
if err != nil {
return fmt.Errorf("error during installation via gRPC: %w", err)
}

return nil
}

func doGrpcStream(ctx context.Context, conn net.Conn, imageUrl string, imageHash string) error {
tridentClient, err := tridentgrpc.NewTridentClientFromNetworkConnection(conn)
if err != nil {
return fmt.Errorf("failed to create Trident gRPC client from RCP connection: %w", err)
}
defer tridentClient.Close()

stream, err := tridentClient.StreamingServiceClient.StreamDisk(ctx, &tridentpbv1.StreamDiskRequest{
ImageUrl: imageUrl,
ImageHash: &imageHash,
Reboot: &tridentpbv1.RebootManagement{
Handling: tridentpbv1.RebootHandling_TRIDENT_HANDLES_REBOOT,
},
})
if err != nil {
return fmt.Errorf("failed to start streaming via gRPC: %w", err)
}

err = handleServicingResponseStream(stream)
if err != nil {
return fmt.Errorf("error during streaming via gRPC: %w", err)
}

return nil
}

func handleServicingResponseStream(stream grpc.ServerStreamingClient[tridentpbv1.ServicingResponse]) error {
for {
resp, err := stream.Recv()
Expand Down
127 changes: 127 additions & 0 deletions tools/pkg/netlaunch/localproxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package netlaunch

import (
"context"
"errors"
"fmt"
"io"
"net"
"os"
"syscall"
"time"
rcpclient "tridenttools/pkg/rcp/client"

"github.com/sirupsen/logrus"
Comment thread
frhuelsz marked this conversation as resolved.
Outdated
log "github.com/sirupsen/logrus"
)

// openLocalProxy sets up a local proxy that listens on the specified socket
// path and forwards connections to the netlaunch server. Only one connection
// will be accepted and forwarded at a time. When a connection closes, the proxy
// will accept a new connection.
func openLocalProxy(ctx context.Context, socketPath string, rcpListener *rcpclient.RcpListener) error {
for {
log.Info("Waiting for RCP connection...")
select {
case <-ctx.Done():
return ctx.Err()
case conn := <-rcpListener.ConnChan:
log.Infof("Accepted RCP connection from %s", conn.RemoteAddr())
err := runLocalProxy(ctx, socketPath, conn)
if err != nil {
log.Errorf("Error running local proxy: %v", err)
}
}
}
}

func runLocalProxy(ctx context.Context, socketPath string, remoteConn net.Conn) error {
defer remoteConn.Close()
// Remove any existing socket file to avoid "address already in use" errors.
if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove existing socket %s: %w", socketPath, err)
}

listener, err := net.Listen("unix", socketPath)
if err != nil {
return fmt.Errorf("failed to listen on unix socket %s: %w", socketPath, err)
}

Copilot AI Mar 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The local proxy listens on a predictable socket path (default under /tmp) but does not set restrictive permissions on the created Unix socket. On multi-user systems this can allow other local users to connect and issue gRPC calls through to Trident in the VM. Consider creating the socket in a private runtime dir (e.g., under $XDG_RUNTIME_DIR) and/or explicitly chmod'ing the socket to 0600 after net.Listen.

Suggested change
}
}
// Restrict permissions on the Unix socket so that only the owner can access it.
if err := os.Chmod(socketPath, 0o600); err != nil {
listener.Close()
_ = os.Remove(socketPath)
return fmt.Errorf("failed to set permissions on unix socket %s: %w", socketPath, err)
}

Copilot uses AI. Check for mistakes.
defer listener.Close()
defer os.Remove(socketPath)
Comment thread
frhuelsz marked this conversation as resolved.

log.WithField("socket", socketPath).Info("Local proxy listening")

// Close the listener when the context is cancelled so Accept unblocks.
go func() {
<-ctx.Done()
listener.Close()
}()

localConn, err := listener.Accept()
if err != nil {
// Check if the context was cancelled.
if ctx.Err() != nil {
return ctx.Err()
}
return fmt.Errorf("failed to accept connection: %w", err)
}

log.WithField("addr", localConn.RemoteAddr()).Info("Accepted local proxy connection")

// Forward data bidirectionally between the local and remote connections.
// Block until the forwarding completes (one connection at a time).
forwardConnections(ctx, localConn, remoteConn)

return nil
}

// forwardConnections copies data bidirectionally between two connections.
// It blocks until both directions are finished (i.e. one side closes or errors).

Copilot AI Mar 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on forwardConnections says it "blocks until both directions are finished", but the implementation returns after the first copy completes (it waits for a single message from doneChan). Please update the comment to match the actual behavior (or wait for both directions if that was the intent).

Suggested change
// It blocks until both directions are finished (i.e. one side closes or errors).
// It blocks until one direction is finished (i.e. one side closes or errors) or the context is cancelled.

Copilot uses AI. Check for mistakes.
func forwardConnections(ctx context.Context, local, remote net.Conn) {
defer local.Close()

local.SetReadDeadline(time.Time{})
remote.SetReadDeadline(time.Time{})
Comment on lines +94 to +100

Copilot AI Mar 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forwardConnections' docstring says it blocks until both directions are finished, but the implementation waits for only one doneChan message (or context cancel) and then returns. Either update the comment to match the behavior, or wait for both copy goroutines to finish (e.g., read twice from doneChan) to match the stated contract.

Copilot uses AI. Check for mistakes.

// Channel to signal when copying is done. Buffered to allow both goroutines
// to send without blocking.
doneChan := make(chan string, 2)

// Start the proxying
go func() {
_, err := io.Copy(remote, local)
if err != nil {
switch {
case errors.Is(err, io.EOF),
errors.Is(err, net.ErrClosed),
errors.Is(err, syscall.EPIPE):
logrus.Debugf("Connection closed while copying from client to server: %v", err)
default:
logrus.Errorf("Error copying from client to server: %v", err)
}
}
doneChan <- "local->remote"
}()
go func() {
_, err := io.Copy(local, remote)
if err != nil {
switch {
case errors.Is(err, io.EOF),
errors.Is(err, net.ErrClosed),
errors.Is(err, syscall.EPIPE):
logrus.Debugf("Connection closed while copying from server to client: %v", err)
default:
logrus.Errorf("Error copying from server to client: %v", err)
}
}
doneChan <- "remote->local"
}()

// Wait for either copy to finish or context cancellation
select {
case direction := <-doneChan:
logrus.Infof("Connection closed by '%s'", direction)
case <-ctx.Done():
logrus.Info("Context cancelled")
}
}
Loading
Loading