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
36 changes: 26 additions & 10 deletions vmnetsock/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@ const (
// With TSO enabled, the adapter tracks TCP handshakes and splits complete-checksum
// TCP frames using the peer's MSS and the interface MTU. It also converts standard
// Virtio TCPv4 and TCPv6 GSO, including ECN, using gso_size as a payload limit.
// GSO conversion requires TSO on iface. UDP GSO is not supported.
// Virtio UDP_L4 GSO is split into complete UDP datagrams at gso_size boundaries,
// with a shorter final datagram when needed. It requires no TCP handshake.
// GSO conversion requires TSO on iface for the aggregate input and output buffers.
// Aggregate input is bounded by iface.MaxPacketSize(); each output must fit MTU.
// UDP gso_size plus IP and UDP headers must fit MTU.
// UDP supports fixed IPv4/IPv6 headers without fragments, extensions, or VLANs.
// Legacy UFO (IP fragmentation), UDP RSC, and UDP ECN GSO modifiers are unsupported.
// TCP RSC normalization also requires TSO. It splits checksum-validated packets
// and recalculates their checksums, including when vmnet supplies no GSO size.
// RSC metadata that reports duplicate ACKs is not supported.
Expand All @@ -72,7 +78,7 @@ func WithVirtioHeaderConversion() Option {
}
}

// WithPeerVirtioHeaderNormalization completes partial checksums and splits TCP
// WithPeerVirtioHeaderNormalization completes partial checksums and splits TCP/UDP
// GSO received from the socket peer before sending packets to iface. Both ends
// exchange 12-byte little-endian Virtio network headers. Start iface with
// vmnet.WithInterfaceVirtioHeader(true). Normalized packets have zeroed headers.
Expand All @@ -82,9 +88,13 @@ func WithVirtioHeaderConversion() Option {
// from the socket peer require TSO and a supported TCP packet. Close returns
// the forwarding error for unsupported RSC metadata, including duplicate ACKs.
//
// Checksum completion works without TSO. TCPv4 and TCPv6 GSO, including ECN,
// require TSO on iface. Output is limited by gso_size, the interface MTU, and
// the iface-side receiver's advertised MSS. UDP GSO is not supported.
// Checksum completion works without TSO. GSO requires TSO on iface for the
// aggregate input and output buffers. TCPv4 and TCPv6 GSO support ECN, with output
// limited by gso_size, the interface MTU, and the iface-side receiver's advertised
// MSS. UDP_L4 GSO preserves gso_size datagram boundaries without a handshake.
// UDP gso_size plus IP and UDP headers must fit MTU. Aggregate input is bounded by
// iface.MaxPacketSize(). UDP has the packet layout and metadata restrictions of
// [WithVirtioHeaderConversion], including no legacy UFO support.
// TCP RSC uses the same MTU and MSS limits and recalculates checksums from the
// packet instead of interpreting its coalescing counters as checksum offsets.
//
Expand Down Expand Up @@ -353,7 +363,7 @@ func (s *adapterState) forwardToPeer(ctx context.Context, iface *vmnet.Interface
continue
}
if config.mode == packetEthernet && s.tcp != nil {
if err := s.forwardTCPPackets(packets, peerFrames, false, writePackets); err != nil {
if err := s.forwardVirtioPackets(packets, peerFrames, false, writePackets); err != nil {
return err
}
continue
Expand Down Expand Up @@ -392,7 +402,7 @@ func (s *adapterState) normalizePeerPackets(packets, output [][]byte, writePacke
}
}
if s.tcp != nil {
return s.forwardTCPPackets(packets, output, true, writePackets)
return s.forwardVirtioPackets(packets, output, true, writePackets)
}
for _, packet := range packets {
if _, err := virtioEthernetPacket(packet); err != nil {
Expand All @@ -415,9 +425,9 @@ func (s *adapterState) forwardObservedPackets(packets [][]byte, writePackets fun
return writePackets(packets)
}

// forwardTCPPackets copies each borrowed segment before its scratch buffer is reused.
// Flush full batches so a large frame with a small MSS does not grow the batch.
func (s *adapterState) forwardTCPPackets(packets, peerFrames [][]byte, virtioOutput bool, writePackets func([][]byte) error) error {
// forwardVirtioPackets copies each borrowed segment before its scratch buffer is reused.
// Flush full batches so a large GSO frame does not grow the batch.
func (s *adapterState) forwardVirtioPackets(packets, peerFrames [][]byte, virtioOutput bool, writePackets func([][]byte) error) error {
prefix := 0
if virtioOutput {
prefix = virtioHeaderSize
Expand All @@ -442,6 +452,12 @@ func (s *adapterState) forwardTCPPackets(packets, peerFrames [][]byte, virtioOut
return nil
}
for _, frame := range packets {
if len(frame) >= 2 && frame[1]&^virtioGSOECN == virtioGSOUDPL4 {
if err := forwardVirtioUDPGSO(frame, s.tcp.mtu, emit); err != nil {
return err
}
continue
}
if err := s.tcp.forwardVirtio(frame, time.Now(), emit); err != nil {
return err
}
Expand Down
22 changes: 22 additions & 0 deletions vmnetsock/checksum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package vmnetsock

import "encoding/binary"

func packetSum(data []byte, sum uint64) uint64 {
for len(data) >= 2 {
sum += uint64(binary.BigEndian.Uint16(data))
data = data[2:]
}
if len(data) > 0 {
sum += uint64(data[0]) << 8
}
return sum
}

func packetChecksum(data []byte, sum uint64) uint16 {
sum = packetSum(data, sum)
for sum>>16 != 0 {
sum = sum&0xffff + sum>>16
}
return ^uint16(sum)
}
16 changes: 8 additions & 8 deletions vmnetsock/gso_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func TestVirtioTCPGSO(t *testing.T) {
var sizes []int
var joined []byte
batches := 0
err := state.forwardTCPPackets([][]byte{packet}, buffers, false, func(frames [][]byte) error {
err := state.forwardVirtioPackets([][]byte{packet}, buffers, false, func(frames [][]byte) error {
batches++
if len(frames) > 2 {
t.Fatal("output exceeded the available batch buffers")
Expand Down Expand Up @@ -168,7 +168,7 @@ func TestVirtioTCPGSOInvalidMetadata(t *testing.T) {
{"wrong checksum start", func(p []byte) { p[6]++ }},
{"wrong checksum offset", func(p []byte) { p[8]++ }},
{"UFO", func(p []byte) { p[1] = 3 }},
{"UDP L4", func(p []byte) { p[1] = 5 }},
{"UDP L4 with TCP payload", func(p []byte) { p[1] = virtioGSOUDPL4 }},
{"ECN alone", func(p []byte) { p[1] = 0x80 }},
{"unknown GSO", func(p []byte) { p[1] = 0x41 }},
} {
Expand All @@ -177,7 +177,7 @@ func TestVirtioTCPGSOInvalidMetadata(t *testing.T) {
packet := gsoTestPacket(false, false, 701, 250)
test.edit(packet)
written := 0
err := state.forwardTCPPackets([][]byte{packet}, [][]byte{make([]byte, 1514)}, false, func(frames [][]byte) error {
err := state.forwardVirtioPackets([][]byte{packet}, [][]byte{make([]byte, 1514)}, false, func(frames [][]byte) error {
written += len(frames)
return nil
})
Expand All @@ -189,7 +189,7 @@ func TestVirtioTCPGSOInvalidMetadata(t *testing.T) {
state := gsoTestState(t, false, 1460)
valid := gsoTestPacket(false, false, 701, 250)
for _, packet := range [][]byte{nil, valid[:1], valid[:25], valid[:26], gsoTestPacket(false, false, 0, 250)} {
if err := state.forwardTCPPackets([][]byte{packet}, [][]byte{make([]byte, 1514)}, false, func([][]byte) error {
if err := state.forwardVirtioPackets([][]byte{packet}, [][]byte{make([]byte, 1514)}, false, func([][]byte) error {
t.Fatal("short or empty GSO emitted data")
return nil
}); err == nil {
Expand All @@ -199,7 +199,7 @@ func TestVirtioTCPGSOInvalidMetadata(t *testing.T) {
state = &adapterState{
tcp: newTCPSegmentation(1500),
}
if err := state.forwardTCPPackets([][]byte{gsoTestPacket(false, false, 701, 250)}, [][]byte{make([]byte, 1514)}, false, func([][]byte) error {
if err := state.forwardVirtioPackets([][]byte{gsoTestPacket(false, false, 701, 250)}, [][]byte{make([]byte, 1514)}, false, func([][]byte) error {
t.Fatal("unobserved connection emitted data")
return nil
}); err == nil {
Expand All @@ -211,7 +211,7 @@ func TestVirtioTCPGSOWriteError(t *testing.T) {
state := gsoTestState(t, false, 1460)
want := errors.New("peer closed")
calls := 0
err := state.forwardTCPPackets([][]byte{gsoTestPacket(false, false, 2101, 60)}, [][]byte{make([]byte, 1514), make([]byte, 1514)}, false, func([][]byte) error {
err := state.forwardVirtioPackets([][]byte{gsoTestPacket(false, false, 2101, 60)}, [][]byte{make([]byte, 1514), make([]byte, 1514)}, false, func([][]byte) error {
calls++
return want
})
Expand All @@ -238,7 +238,7 @@ func TestVirtioTCPGSOFrameLength(t *testing.T) {
length := binary.BigEndian.Uint16(packet[lengthOffset:])
binary.BigEndian.PutUint16(packet[lengthOffset:], length-64)
written := 0
err := state.forwardTCPPackets([][]byte{packet}, [][]byte{make([]byte, 1514)}, false, func(frames [][]byte) error {
err := state.forwardVirtioPackets([][]byte{packet}, [][]byte{make([]byte, 1514)}, false, func(frames [][]byte) error {
written += len(frames)
return nil
})
Expand All @@ -258,7 +258,7 @@ func TestVirtioTCPGSOFrameLength(t *testing.T) {
binary.LittleEndian.PutUint16(packet[6:], 34)
binary.LittleEndian.PutUint16(packet[8:], 16)
written := 0
err := state.forwardTCPPackets([][]byte{packet}, [][]byte{make([]byte, 1514)}, false, func(frames [][]byte) error {
err := state.forwardVirtioPackets([][]byte{packet}, [][]byte{make([]byte, 1514)}, false, func(frames [][]byte) error {
for _, frame := range frames {
written++
if len(frame) != 55 || frame[54] != 20 || wireSum(frame[14:34]) != 65535 {
Expand Down
10 changes: 5 additions & 5 deletions vmnetsock/rsc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func TestVirtioTCPRSCRejectsUnsupportedMetadata(t *testing.T) {
state := gsoTestState(t, false, 1460)
packet := rscTestPacket(false, 0, 37, 0)
test.edit(packet)
err := state.forwardTCPPackets([][]byte{packet}, [][]byte{make([]byte, 1514)}, false, func([][]byte) error {
err := state.forwardVirtioPackets([][]byte{packet}, [][]byte{make([]byte, 1514)}, false, func([][]byte) error {
t.Fatal("unsupported RSC metadata emitted a packet")
return nil
})
Expand All @@ -123,7 +123,7 @@ func TestVirtioTCPRSCAcknowledgmentAndErrors(t *testing.T) {
packet := rscTestPacket(false, 0, 0, 0)
packet[virtioHeaderSize+ethernetHeaderSize+ipv4HeaderSize+tcpFlagsOffset] = 0x10
calls := 0
if err := state.forwardTCPPackets([][]byte{packet}, [][]byte{make([]byte, 1514)}, false, func(frames [][]byte) error {
if err := state.forwardVirtioPackets([][]byte{packet}, [][]byte{make([]byte, 1514)}, false, func(frames [][]byte) error {
calls++
if len(frames) != 1 || len(frames[0]) != 66 {
t.Fatalf("ACK-only RSC output = %d frames", len(frames))
Expand All @@ -140,7 +140,7 @@ func TestVirtioTCPRSCAcknowledgmentAndErrors(t *testing.T) {
}
packet = rscTestPacket(false, 0, 3001, 0)
state = &adapterState{tcp: newTCPSegmentation(1500)}
if err := state.forwardTCPPackets([][]byte{packet}, [][]byte{make([]byte, 1514)}, false, func([][]byte) error {
if err := state.forwardVirtioPackets([][]byte{packet}, [][]byte{make([]byte, 1514)}, false, func([][]byte) error {
t.Fatal("unknown connection emitted RSC data")
return nil
}); err == nil {
Expand All @@ -149,7 +149,7 @@ func TestVirtioTCPRSCAcknowledgmentAndErrors(t *testing.T) {
state = gsoTestState(t, false, 1460)
want := errors.New("receiver closed")
calls = 0
err := state.forwardTCPPackets([][]byte{packet}, [][]byte{make([]byte, 1514)}, false, func([][]byte) error {
err := state.forwardVirtioPackets([][]byte{packet}, [][]byte{make([]byte, 1514)}, false, func([][]byte) error {
calls++
return want
})
Expand All @@ -165,7 +165,7 @@ func TestVirtioRSCNonTCPIsUnchanged(t *testing.T) {
binary.LittleEndian.PutUint16(packet[virtioRSCSegmentsOffset:], 65535)
want := bytes.Clone(packet[virtioHeaderSize:])
calls := 0
if err := state.forwardTCPPackets([][]byte{packet}, [][]byte{make([]byte, 1514)}, false, func(frames [][]byte) error {
if err := state.forwardVirtioPackets([][]byte{packet}, [][]byte{make([]byte, 1514)}, false, func(frames [][]byte) error {
calls++
if len(frames) != 1 || !bytes.Equal(frames[0], want) {
t.Fatal("non-TCP RSC Ethernet bytes changed")
Expand Down
27 changes: 4 additions & 23 deletions vmnetsock/tcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ func (p *tcpPacket) updateSegmentHeaders(ip []byte, segmentIndex int) {
id := binary.BigEndian.Uint16(p.ip[ipv4IdentificationOffset:]) + uint16(segmentIndex)
binary.BigEndian.PutUint16(ip[ipv4IdentificationOffset:], id)
binary.BigEndian.PutUint16(ip[ipv4ChecksumOffset:], 0)
binary.BigEndian.PutUint16(ip[ipv4ChecksumOffset:], tcpChecksum(ip[:ipv4HeaderSize], 0))
binary.BigEndian.PutUint16(ip[ipv4ChecksumOffset:], packetChecksum(ip[:ipv4HeaderSize], 0))
} else {
binary.BigEndian.PutUint16(ip[ipv6PayloadLengthOffset:], uint16(len(tcp)))
}
Expand All @@ -538,28 +538,9 @@ func (p *tcpPacket) updateSegmentHeaders(ip []byte, segmentIndex int) {
binary.BigEndian.PutUint16(tcp[tcpChecksumOffset:], 0)
sum := uint64(ipProtocolTCP + len(tcp))
if p.version == ipv4Version {
sum = tcpSum(ip[ipv4SourceOffset:ipv4HeaderSize], sum)
sum = packetSum(ip[ipv4SourceOffset:ipv4HeaderSize], sum)
} else {
sum = tcpSum(ip[ipv6SourceOffset:ipv6HeaderSize], sum)
sum = packetSum(ip[ipv6SourceOffset:ipv6HeaderSize], sum)
}
binary.BigEndian.PutUint16(tcp[tcpChecksumOffset:], tcpChecksum(tcp, sum))
}

func tcpSum(data []byte, sum uint64) uint64 {
for len(data) >= 2 {
sum += uint64(binary.BigEndian.Uint16(data))
data = data[2:]
}
if len(data) > 0 {
sum += uint64(data[0]) << 8
}
return sum
}

func tcpChecksum(data []byte, sum uint64) uint16 {
sum = tcpSum(data, sum)
for sum>>16 != 0 {
sum = sum&0xffff + sum>>16
}
return ^uint16(sum)
binary.BigEndian.PutUint16(tcp[tcpChecksumOffset:], packetChecksum(tcp, sum))
}
2 changes: 1 addition & 1 deletion vmnetsock/tcp_batch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestTCPBatchCopiesSegments(t *testing.T) {
peerFrames := [][]byte{make([]byte, 1514), make([]byte, 1514)}
var batchSizes []int
var received []byte
err := state.forwardTCPPackets([][]byte{packet}, peerFrames, false, func(frames [][]byte) error {
err := state.forwardVirtioPackets([][]byte{packet}, peerFrames, false, func(frames [][]byte) error {
batchSizes = append(batchSizes, len(frames))
for _, frame := range frames {
if len(frame) < 54 {
Expand Down
Loading
Loading