Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
11 changes: 1 addition & 10 deletions pkg/machine/apple/apple.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,7 @@ import (

const applehvMACAddress = "5a:94:ef:e4:0c:ee"

var (
gvProxyWaitBackoff = 500 * time.Millisecond
gvProxyMaxBackoffAttempts = 6
ignitionSocketName = "ignition.sock"
)
var ignitionSocketName = "ignition.sock"

// ResizeDisk uses os truncate to resize (only larger) a raw disk. the input size
// is assumed GiB
Expand Down Expand Up @@ -78,11 +74,6 @@ func StartGenericAppleVM(mc *vmconfigs.MachineConfig, cmdBinary string, bootload
return nil, nil, err
}

// Wait on gvproxy to be running and aware
if err := sockets.WaitForSocketWithBackoffs(gvProxyMaxBackoffAttempts, gvProxyWaitBackoff, gvproxySocket.GetPath(), "gvproxy"); err != nil {
return nil, nil, err
}

netDevice.SetUnixSocketPath(gvproxySocket.GetPath())

// create a one-time virtual machine for starting because we dont want all this information in the
Expand Down
5 changes: 4 additions & 1 deletion pkg/machine/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import (
"go.podman.io/podman/v6/pkg/machine/vmconfigs"
)

const apiUpTimeout = 20 * time.Second
const (
apiUpTimeout = 20 * time.Second
GvProxyReadyTimeout = 30 * time.Second
)

var ForwarderBinaryName = "gvproxy"

Expand Down
14 changes: 12 additions & 2 deletions pkg/machine/gvproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"io/fs"
"path/filepath"
"strconv"
"time"

Expand Down Expand Up @@ -33,7 +34,8 @@ func readPIDFileWithRetry(f define.VMFile) ([]byte, error) {
return f.Read()
}

// CleanupGVProxy reads the --pid-file for gvproxy attempts to stop it
// CleanupGVProxy reads the --pid-file for gvproxy, stops the process,
// removes the PID file, and cleans up the notification socket.
func CleanupGVProxy(f define.VMFile) error {
gvPid, err := readPIDFileWithRetry(f)
if err != nil {
Expand All @@ -51,5 +53,13 @@ func CleanupGVProxy(f define.VMFile) error {
if err := waitOnProcess(proxyPid); err != nil {
return err
}
return removeGVProxyPIDFile(f)
if err := removeGVProxyPIDFile(f); err != nil {
return err
}

// Remove notification socket after gvproxy has exited
runtimeDir := filepath.Dir(f.GetPath())
CleanupGvProxyNotifySocket(runtimeDir)

return nil
}
165 changes: 165 additions & 0 deletions pkg/machine/gvproxy_notify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package machine

import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"os"
"path/filepath"

gvProxyTypes "github.com/containers/gvisor-tap-vsock/pkg/types"
"github.com/sirupsen/logrus"
)

const notifySocketName = "gvproxy-notify.sock"

// GvProxyNotifier listens on a unix socket for JSON notification messages
// from gvproxy.
type GvProxyNotifier struct {
listener net.Listener
socketPath string
readyCh chan struct{}
connectedCh chan string
errorCh chan error
}

// SocketPath returns the path to the notification unix socket.
func (n *GvProxyNotifier) SocketPath() string {
return n.socketPath
}

// NewGvProxyNotifier creates a notification listener socket in the given runtime directory.
//
// The socket begins accepting connections at the kernel level immediately upon creation
// (via net.Listen), so gvproxy can be started and connect before Start() is called without
// losing messages. The kernel backlog buffers both the connection and any data sent on it
// until Accept()/Read() consume them.
//
// The notifier is only used during machine start. Close() stops the listener but
// intentionally leaves the socket file on disk because gvproxy remains running after
// start completes and may still dial the socket for later notifications. The socket
// file is removed by CleanupGvProxyNotifySocket, which is called from CleanupGVProxy
// during machine stop after gvproxy has exited. Any stale socket from a previous run
// is removed at the top of this constructor before creating a new listener.
func NewGvProxyNotifier(runtimeDir string) (*GvProxyNotifier, error) {
socketPath := filepath.Join(runtimeDir, notifySocketName)

if err := os.Remove(socketPath); err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("removing old notification socket: %w", err)
}

listener, err := net.Listen("unix", socketPath)
if err != nil {
return nil, fmt.Errorf("creating notification listener: %w", err)
}

// Prevent Close() from removing the socket file. gvproxy may still
// dial it for notifications after the listener is closed. The file is
// removed by CleanupGvProxyNotifySocket during machine stop.
listener.(*net.UnixListener).SetUnlinkOnClose(false)

return &GvProxyNotifier{
listener: listener,
socketPath: socketPath,
readyCh: make(chan struct{}, 1),
connectedCh: make(chan string, 1),
errorCh: make(chan error, 1),
}, nil
}

// CleanupGvProxyNotifySocket removes the notification socket file from the given runtime directory.
//
// This is called from CleanupGVProxy during machine stop, at which point the notifier
// (which only runs during machine start) has long since closed its listener.
func CleanupGvProxyNotifySocket(runtimeDir string) {
socketPath := filepath.Join(runtimeDir, notifySocketName)
if err := os.Remove(socketPath); err != nil && !errors.Is(err, os.ErrNotExist) {
logrus.Debugf("failed to remove gvproxy notification socket: %v", err)
}
}

// Close stops the notifier listener.
//
// The socket file is intentionally left on disk because gvproxy may still dial it for later notifications
// (connection_established, connection_closed).
// The file is cleaned up by CleanupGvProxyNotifySocket after gvproxy exits.
func (n *GvProxyNotifier) Close() {
n.listener.Close()
}

// Start begins accepting connections and processing notifications.
//
// It blocks until the context is canceled or the listener is closed,
// intended to be run as a goroutine.
func (n *GvProxyNotifier) Start(ctx context.Context) {
for {
conn, err := n.listener.Accept()
if err != nil {
if ctx.Err() != nil {
return
}
if errors.Is(err, net.ErrClosed) {
return
}
logrus.Debugf("notification listener accept error: %v", err)
return
}
go n.handleConnection(ctx, conn)
}
}

func (n *GvProxyNotifier) handleConnection(ctx context.Context, conn net.Conn) {
defer conn.Close()
decoder := json.NewDecoder(conn)

for {
if ctx.Err() != nil {
return
}

var msg gvProxyTypes.NotificationMessage
if err := decoder.Decode(&msg); err != nil {
if ctx.Err() != nil {
return
}
logrus.Debugf("notification decode error: %v", err)
return
}

logrus.Debugf("gvproxy notification received: type=%s mac=%s", msg.NotificationType, msg.MacAddress)

switch msg.NotificationType {
case gvProxyTypes.Ready:
select {
case n.readyCh <- struct{}{}:
default:
}
case gvProxyTypes.ConnectionEstablished:
select {
case n.connectedCh <- msg.MacAddress:
default:
}
case gvProxyTypes.HypervisorError:
select {
case n.errorCh <- fmt.Errorf("gvproxy reported hypervisor error"):
default:
}
case gvProxyTypes.ConnectionClosed:
logrus.Debugf("gvproxy: VM disconnected (mac=%s)", msg.MacAddress)
}
}
}

// WaitReady blocks until the "ready" notification is received or the context expires.
func (n *GvProxyNotifier) WaitReady(ctx context.Context) error {
select {
case <-n.readyCh:
return nil
case err := <-n.errorCh:
return err
case <-ctx.Done():
return fmt.Errorf("timeout waiting for gvproxy ready notification: %w", ctx.Err())
}
}
95 changes: 95 additions & 0 deletions pkg/machine/gvproxy_notify_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//go:build amd64 || arm64

package machine

import (
"context"
"encoding/json"
"errors"
"net"
"os"
"testing"
"time"

gvproxyTypes "github.com/containers/gvisor-tap-vsock/pkg/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGvProxyNotifier_Ready(t *testing.T) {
dir := t.TempDir()
notifier, err := NewGvProxyNotifier(dir)
require.NoError(t, err)
defer notifier.Close()

go notifier.Start(t.Context())

conn, err := net.Dial("unix", notifier.SocketPath())
require.NoError(t, err)
defer conn.Close()

err = json.NewEncoder(conn).Encode(gvproxyTypes.NotificationMessage{
NotificationType: gvproxyTypes.Ready,
})
require.NoError(t, err)

waitCtx, waitCancel := context.WithTimeout(t.Context(), 2*time.Second)
defer waitCancel()
err = notifier.WaitReady(waitCtx)
assert.NoError(t, err)
}

func TestGvProxyNotifier_HypervisorError(t *testing.T) {
dir := t.TempDir()
notifier, err := NewGvProxyNotifier(dir)
require.NoError(t, err)
defer notifier.Close()

go notifier.Start(t.Context())

conn, err := net.Dial("unix", notifier.SocketPath())
require.NoError(t, err)
defer conn.Close()

err = json.NewEncoder(conn).Encode(gvproxyTypes.NotificationMessage{
NotificationType: gvproxyTypes.HypervisorError,
})
require.NoError(t, err)

waitCtx, waitCancel := context.WithTimeout(t.Context(), 2*time.Second)
defer waitCancel()
err = notifier.WaitReady(waitCtx)
assert.Error(t, err)
assert.Contains(t, err.Error(), "hypervisor error")
}

func TestGvProxyNotifier_Timeout(t *testing.T) {
dir := t.TempDir()
notifier, err := NewGvProxyNotifier(dir)
require.NoError(t, err)
defer notifier.Close()

go notifier.Start(t.Context())

waitCtx, waitCancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
defer waitCancel()
err = notifier.WaitReady(waitCtx)
assert.Error(t, err)
assert.ErrorIs(t, err, context.DeadlineExceeded)
}

func TestCleanupGvProxyNotifySocket(t *testing.T) {
dir := t.TempDir()

notifier, err := NewGvProxyNotifier(dir)
require.NoError(t, err)
socketPath := notifier.SocketPath()
notifier.Close()

_, err = os.Stat(socketPath)
assert.NoError(t, err, "socket file should still exist after Close()")

CleanupGvProxyNotifySocket(dir)
_, err = os.Stat(socketPath)
assert.True(t, errors.Is(err, os.ErrNotExist), "socket file should be gone after cleanup")
}
15 changes: 0 additions & 15 deletions pkg/machine/qemu/stubber.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,6 @@ type QEMUStubber struct {
virtiofsHelpers []virtiofsdHelperCmd
}

var (
gvProxyWaitBackoff = 500 * time.Millisecond
gvProxyMaxBackoffAttempts = 6
)

func (q *QEMUStubber) UserModeNetworkEnabled(*vmconfigs.MachineConfig) bool {
return true
}
Expand Down Expand Up @@ -155,16 +150,6 @@ func (q *QEMUStubber) StartVM(mc *vmconfigs.MachineConfig) (func() error, func()
return nil, nil, err
}

gvProxySock, err := mc.GVProxySocket()
if err != nil {
return nil, nil, err
}

// Wait on gvproxy to be running and aware
if err := sockets.WaitForSocketWithBackoffs(gvProxyMaxBackoffAttempts, gvProxyWaitBackoff, gvProxySock.GetPath(), "gvproxy"); err != nil {
return nil, nil, err
}

dnr, dnw, err := machine.GetDevNullFiles()
if err != nil {
return nil, nil, err
Expand Down
Loading
Loading