Skip to content
Merged
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
44 changes: 34 additions & 10 deletions vmnet/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,17 @@ func WithInterfaceIsolation(enabled bool) InterfaceOption {
type Interface struct{ owner *interfaceOwner }
type interfaceOwner struct{ resource *interfaceResource }
type interfaceResource struct {
mu sync.Mutex
session unsafe.Pointer
handle cgo.Handle
wake, done chan struct{}
stopping bool
stopErr error
maxSize int
mac [6]byte
readers chan struct{}
mu sync.Mutex
session unsafe.Pointer
handle cgo.Handle
wake, done chan struct{}
stopping bool
stopErr error
maxSize int
virtioHeader bool
tso bool
mac [6]byte
readers chan struct{}
}

var interfaceWake = purego.NewCallback(func(handle uint64) {
Expand Down Expand Up @@ -163,7 +165,13 @@ func startInterface(n *Network, name string, opts ...InterfaceOption) (*Interfac
if c.enableVirtioHeader && c.enableChecksumOffload {
return nil, fmt.Errorf("vmnet virtio header and checksum offload cannot both be enabled")
}
r := &interfaceResource{wake: make(chan struct{}, 1), done: make(chan struct{}), readers: make(chan struct{}, 1)}
r := &interfaceResource{
wake: make(chan struct{}, 1),
done: make(chan struct{}),
readers: make(chan struct{}, 1),
virtioHeader: c.enableVirtioHeader,
tso: c.enableTSO,
}
r.handle = cgo.NewHandle(r.wake)
var size uint64
var errorPtr unsafe.Pointer
Expand Down Expand Up @@ -237,6 +245,22 @@ func (i *Interface) MaxPacketSize() int {
return i.owner.resource.maxSize
}

// VirtioHeaderEnabled reports whether the interface was started with raw Virtio
// headers. It returns false for a nil or uninitialized interface.
//
// See https://developer.apple.com/documentation/vmnet/vmnet_enable_virtio_header_key
func (i *Interface) VirtioHeaderEnabled() bool {
return i != nil && i.owner != nil && i.owner.resource.virtioHeader
}

// TSOEnabled reports whether the interface was started with TCP segmentation
// offload. It returns false for a nil or uninitialized interface.
//
// See https://developer.apple.com/documentation/vmnet/vmnet_enable_tso_key
func (i *Interface) TSOEnabled() bool {
return i != nil && i.owner != nil && i.owner.resource.tso
}

// ReadPacket waits for one frame. The buffer must hold MaxPacketSize bytes.
// [WithInterfaceVirtioHeader] includes the Virtio header in the buffer and byte count.
// A canceled context or Stop interrupts the wait. Concurrent readers share packets.
Expand Down
2 changes: 1 addition & 1 deletion vmnet/interface_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func TestVMNetInterfaceBoundaries(t *testing.T) {
if err := i.Stop(); err == nil {
t.Fatal("invalid interface stopped")
}
if i.MaxPacketSize() != 0 || i.MACAddress() != nil {
if i.MaxPacketSize() != 0 || i.MACAddress() != nil || i.VirtioHeaderEnabled() || i.TSOEnabled() {
t.Fatal("invalid interface metadata")
}
}
Expand Down
91 changes: 80 additions & 11 deletions vmnetsock/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,26 @@ const (
Datagram
)

// Option configures a socket adapter.
type Option func(*adapterConfiguration)

type adapterConfiguration struct {
convertVirtioHeader bool
}

// WithVirtioHeaderConversion connects a raw Virtio vmnet interface to a peer
// that exchanges plain Ethernet frames, such as QEMU stream/socket networking.
// Start iface with vmnet.WithInterfaceVirtioHeader(true) and leave TSO disabled.
// The adapter completes partial checksums and removes incoming Virtio headers.
// The peer must send fully checksummed Ethernet frames; the adapter adds
// zeroed headers to them. Unsupported segmentation or RSC checksum metadata
// stops forwarding. Close returns the error.
func WithVirtioHeaderConversion() Option {
return func(config *adapterConfiguration) {
config.convertVirtioHeader = true
}
}

// Adapter forwards packets until Close, context cancellation, or a forwarding error.
// Copies share a single lifetime. Adapter borrows the Interface and never calls Stop.
type Adapter struct {
Expand All @@ -44,9 +64,10 @@ type adapterState struct {
// New creates a socket pair and starts forwarding immediately.
// The Adapter owns both sockets and borrows iface without stopping it.
// Dup returns a caller-owned socket to pass through exec.Cmd.ExtraFiles.
// Packets pass through unchanged. If iface uses Virtio headers, the peer must
// support that format and the offload features enabled on iface.
func New(ctx context.Context, iface *vmnet.Interface, transport Transport) (*Adapter, error) {
// Packets pass through unchanged unless WithVirtioHeaderConversion is set.
// Without conversion, a peer of an interface using Virtio headers must support that
// format and the offload features enabled on iface.
func New(ctx context.Context, iface *vmnet.Interface, transport Transport, opts ...Option) (*Adapter, error) {
if iface == nil || iface.MaxPacketSize() == 0 {
return nil, fmt.Errorf("vmnet interface is nil or uninitialized")
}
Expand All @@ -57,6 +78,22 @@ func New(ctx context.Context, iface *vmnet.Interface, transport Transport) (*Ada
return nil, err
}

var config adapterConfiguration
for _, opt := range opts {
opt(&config)
}
if config.convertVirtioHeader {
if !iface.VirtioHeaderEnabled() {
return nil, fmt.Errorf("Virtio header conversion requires interface Virtio headers")
}
if iface.TSOEnabled() {
return nil, fmt.Errorf("Virtio header conversion requires interface TSO to be disabled")
}
if iface.MaxPacketSize() <= virtioHeaderSize {
return nil, fmt.Errorf("vmnet packet size cannot hold a Virtio header")
}
}

conn, file, err := socketPair(transport, iface.MaxPacketSize())
if err != nil {
return nil, err
Expand All @@ -73,10 +110,10 @@ func New(ctx context.Context, iface *vmnet.Interface, transport Transport) (*Ada

var pumps sync.WaitGroup
pumps.Go(func() {
s.finish(s.forwardToInterface(run, iface, transport))
s.finish(s.forwardToInterface(run, iface, transport, config))
})
pumps.Go(func() {
s.finish(s.forwardToPeer(run, iface, transport))
s.finish(s.forwardToPeer(run, iface, transport, config))
})
go func() {
<-run.Done()
Expand Down Expand Up @@ -112,13 +149,16 @@ func (a *Adapter) Dup() (*os.File, error) {
return os.NewFile(uintptr(fd), "vmnet-peer"), nil
}

func (s *adapterState) forwardToInterface(ctx context.Context, iface *vmnet.Interface, transport Transport) error {
func (s *adapterState) forwardToInterface(ctx context.Context, iface *vmnet.Interface, transport Transport, config adapterConfiguration) error {
size := min(iface.MaxPacketSize(), packetBatchBytes)
if config.convertVirtioHeader {
size -= virtioHeaderSize
}
var readPackets func() ([][]byte, error)
if transport == Stream {
reader := newStreamPacketReader(s.conn, iface.MaxPacketSize())
reader := newStreamPacketReader(s.conn, size)
readPackets = reader.readPackets
} else {
size := min(iface.MaxPacketSize(), packetBatchBytes)
count := packetBatchSize(size)
raw, err := s.conn.SyscallConn()
if err != nil {
Expand All @@ -134,11 +174,26 @@ func (s *adapterState) forwardToInterface(ctx context.Context, iface *vmnet.Inte
}
}

var nativeFrames [][]byte
if config.convertVirtioHeader {
nativeFrames = make([][]byte, packetBatchSize(size))
for i := range nativeFrames {
nativeFrames[i] = make([]byte, size+virtioHeaderSize)
}
}

for {
if err := ctx.Err(); err != nil {
return err
}
frames, err := readPackets()
if config.convertVirtioHeader {
for i, frame := range frames {
nativeFrames[i] = nativeFrames[i][:virtioHeaderSize+len(frame)]
copy(nativeFrames[i][virtioHeaderSize:], frame)
}
frames = nativeFrames[:len(frames)]
}
if len(frames) > 0 {
if _, writeErr := iface.WritePackets(frames); writeErr != nil && !vmnetBufferExhausted(writeErr) {
return writeErr
Expand All @@ -153,13 +208,17 @@ func (s *adapterState) forwardToInterface(ctx context.Context, iface *vmnet.Inte
}
}

func (s *adapterState) forwardToPeer(ctx context.Context, iface *vmnet.Interface, transport Transport) error {
func (s *adapterState) forwardToPeer(ctx context.Context, iface *vmnet.Interface, transport Transport, config adapterConfiguration) error {
size := iface.MaxPacketSize()
count := packetBatchSize(size)
frames := make([][]byte, count)
for j := range frames {
frames[j] = make([]byte, size)
}
var peerFrames [][]byte
if config.convertVirtioHeader {
peerFrames = make([][]byte, count)
}
var headers [][4]byte
var vectors net.Buffers
var raw syscall.RawConn
Expand All @@ -178,14 +237,24 @@ func (s *adapterState) forwardToPeer(ctx context.Context, iface *vmnet.Interface
if err != nil {
return err
}
packets := frames[:n]
if config.convertVirtioHeader {
for i, frame := range packets {
peerFrames[i], err = virtioEthernetPacket(frame)
if err != nil {
return err
}
}
packets = peerFrames[:n]
}
if transport == Stream {
if err := writeStreamFrames(s.conn, frames[:n], headers, vectors); err != nil {
if err := writeStreamFrames(s.conn, packets, headers, vectors); err != nil {
return err
}
} else {
// Drop the unsent suffix on ENOBUFS. Socket writability does not
// guarantee space in the peer's datagram receive buffer.
if err := writeDatagramPackets(raw, frames[:n]); err != nil && !errors.Is(err, unix.ENOBUFS) {
if err := writeDatagramPackets(raw, packets); err != nil && !errors.Is(err, unix.ENOBUFS) {
return err
}
}
Expand Down
61 changes: 55 additions & 6 deletions vmnetsock/adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import (
"github.com/Code-Hex/vz/v4/vmnetsock"
)

func newInterface(t *testing.T, n *vmnet.Network) *vmnet.Interface {
func newInterface(t *testing.T, n *vmnet.Network, opts ...vmnet.InterfaceOption) *vmnet.Interface {
t.Helper()
i, err := vmnet.StartInterfaceWithNetwork(n)
i, err := vmnet.StartInterfaceWithNetwork(n, opts...)
if err != nil {
t.Fatal(err)
}
Expand All @@ -37,8 +37,18 @@ func TestAdapterFramesAndLifetime(t *testing.T) {
if macos.Available(26, vzbridge.SDKVersion) != nil {
t.Skip("requires macOS 26 SDK")
}
for _, transport := range []vmnetsock.Transport{vmnetsock.Stream, vmnetsock.Datagram} {
t.Run(string(rune('0'+transport)), func(t *testing.T) {
for _, test := range []struct {
name string
transport vmnetsock.Transport
convert bool
}{
{name: "stream", transport: vmnetsock.Stream},
{name: "datagram", transport: vmnetsock.Datagram},
{name: "stream Virtio conversion", transport: vmnetsock.Stream, convert: true},
{name: "datagram Virtio conversion", transport: vmnetsock.Datagram, convert: true},
} {
t.Run(test.name, func(t *testing.T) {
transport := test.transport
c, err := vmnet.NewNetworkConfiguration(vmnet.ModeHost)
if err != nil {
t.Fatal(err)
Expand All @@ -49,10 +59,21 @@ func TestAdapterFramesAndLifetime(t *testing.T) {
t.Fatal(err)
}
t.Cleanup(func() { objc.Release((*objc.Pointer)(n)) })
a, b := newInterface(t, n), newInterface(t, n)
a := newInterface(t, n, vmnet.WithInterfaceVirtioHeader(test.convert))
b := newInterface(t, n)
if a.VirtioHeaderEnabled() != test.convert || a.TSOEnabled() {
t.Fatal("unexpected interface offload configuration")
}
var opts []vmnetsock.Option
if test.convert {
opts = append(opts, vmnetsock.WithVirtioHeaderConversion())
} else if adapter, err := vmnetsock.New(context.Background(), a, transport, vmnetsock.WithVirtioHeaderConversion()); err == nil {
adapter.Close()
t.Fatal("conversion accepted an interface without Virtio headers")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
adapter, err := vmnetsock.New(ctx, a, transport)
adapter, err := vmnetsock.New(ctx, a, transport, opts...)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -126,6 +147,9 @@ func TestAdapterFramesAndLifetime(t *testing.T) {
if _, err := peer.Write(frame); err == nil {
t.Fatal("peer write succeeded after adapter closed")
}
if test.convert {
frame = append(make([]byte, 12), frame...)
}
if err := a.WritePacket(frame); err != nil {
t.Fatalf("borrowed interface stopped: %v", err)
}
Expand Down Expand Up @@ -336,3 +360,28 @@ func TestAdapterQueuedBurst(t *testing.T) {
})
}
}

func TestAdapterVirtioConversionRejectsTSO(t *testing.T) {
if macos.Available(26, vzbridge.SDKVersion) != nil {
t.Skip("requires macOS 26 SDK")
}
c, err := vmnet.NewNetworkConfiguration(vmnet.ModeHost, vmnet.WithMTU(1500))
if err != nil {
t.Fatal(err)
}
defer objc.Release(c)
n, err := vmnet.NewNetwork(c)
if err != nil {
t.Fatal(err)
}
defer objc.Release((*objc.Pointer)(n))
iface := newInterface(t, n, vmnet.WithInterfaceVirtioHeader(true), vmnet.WithInterfaceTSO(true))
if !iface.VirtioHeaderEnabled() || !iface.TSOEnabled() {
t.Fatal("interface did not retain offload configuration")
}
adapter, err := vmnetsock.New(context.Background(), iface, vmnetsock.Stream, vmnetsock.WithVirtioHeaderConversion())
if err == nil {
adapter.Close()
t.Fatal("conversion accepted an interface with TSO")
}
}
Loading
Loading