mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Enforce fragment block size and validate args
Allow configuring fragmentation.Fragmentation with a fragment
block size which will be enforced when processing fragments. Also
validate arguments when processing fragments.
Test:
- fragmentation.TestErrors
- ipv6_test.TestReceiveIPv6Fragments
- ipv4_test.TestReceiveIPv6Fragments
PiperOrigin-RevId: 324081521
This commit is contained in:
committed by
gVisor bot
parent
3c70b4c986
commit
9960a816a9
@@ -17,6 +17,7 @@
|
||||
package fragmentation
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
@@ -25,20 +26,31 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/tcpip/buffer"
|
||||
)
|
||||
|
||||
// DefaultReassembleTimeout is based on the linux stack: net.ipv4.ipfrag_time.
|
||||
const DefaultReassembleTimeout = 30 * time.Second
|
||||
const (
|
||||
// DefaultReassembleTimeout is based on the linux stack: net.ipv4.ipfrag_time.
|
||||
DefaultReassembleTimeout = 30 * time.Second
|
||||
|
||||
// HighFragThreshold is the threshold at which we start trimming old
|
||||
// fragmented packets. Linux uses a default value of 4 MB. See
|
||||
// net.ipv4.ipfrag_high_thresh for more information.
|
||||
const HighFragThreshold = 4 << 20 // 4MB
|
||||
// HighFragThreshold is the threshold at which we start trimming old
|
||||
// fragmented packets. Linux uses a default value of 4 MB. See
|
||||
// net.ipv4.ipfrag_high_thresh for more information.
|
||||
HighFragThreshold = 4 << 20 // 4MB
|
||||
|
||||
// LowFragThreshold is the threshold we reach to when we start dropping
|
||||
// older fragmented packets. It's important that we keep enough room for newer
|
||||
// packets to be re-assembled. Hence, this needs to be lower than
|
||||
// HighFragThreshold enough. Linux uses a default value of 3 MB. See
|
||||
// net.ipv4.ipfrag_low_thresh for more information.
|
||||
const LowFragThreshold = 3 << 20 // 3MB
|
||||
// LowFragThreshold is the threshold we reach to when we start dropping
|
||||
// older fragmented packets. It's important that we keep enough room for newer
|
||||
// packets to be re-assembled. Hence, this needs to be lower than
|
||||
// HighFragThreshold enough. Linux uses a default value of 3 MB. See
|
||||
// net.ipv4.ipfrag_low_thresh for more information.
|
||||
LowFragThreshold = 3 << 20 // 3MB
|
||||
|
||||
// minBlockSize is the minimum block size for fragments.
|
||||
minBlockSize = 1
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidArgs indicates to the caller that that an invalid argument was
|
||||
// provided.
|
||||
ErrInvalidArgs = errors.New("invalid args")
|
||||
)
|
||||
|
||||
// Fragmentation is the main structure that other modules
|
||||
// of the stack should use to implement IP Fragmentation.
|
||||
@@ -50,10 +62,13 @@ type Fragmentation struct {
|
||||
rList reassemblerList
|
||||
size int
|
||||
timeout time.Duration
|
||||
blockSize uint16
|
||||
}
|
||||
|
||||
// NewFragmentation creates a new Fragmentation.
|
||||
//
|
||||
// blockSize specifies the fragment block size, in bytes.
|
||||
//
|
||||
// highMemoryLimit specifies the limit on the memory consumed
|
||||
// by the fragments stored by Fragmentation (overhead of internal data-structures
|
||||
// is not accounted). Fragments are dropped when the limit is reached.
|
||||
@@ -64,7 +79,7 @@ type Fragmentation struct {
|
||||
// reassemblingTimeout specifies the maximum time allowed to reassemble a packet.
|
||||
// Fragments are lazily evicted only when a new a packet with an
|
||||
// already existing fragmentation-id arrives after the timeout.
|
||||
func NewFragmentation(highMemoryLimit, lowMemoryLimit int, reassemblingTimeout time.Duration) *Fragmentation {
|
||||
func NewFragmentation(blockSize uint16, highMemoryLimit, lowMemoryLimit int, reassemblingTimeout time.Duration) *Fragmentation {
|
||||
if lowMemoryLimit >= highMemoryLimit {
|
||||
lowMemoryLimit = highMemoryLimit
|
||||
}
|
||||
@@ -73,17 +88,46 @@ func NewFragmentation(highMemoryLimit, lowMemoryLimit int, reassemblingTimeout t
|
||||
lowMemoryLimit = 0
|
||||
}
|
||||
|
||||
if blockSize < minBlockSize {
|
||||
blockSize = minBlockSize
|
||||
}
|
||||
|
||||
return &Fragmentation{
|
||||
reassemblers: make(map[uint32]*reassembler),
|
||||
highLimit: highMemoryLimit,
|
||||
lowLimit: lowMemoryLimit,
|
||||
timeout: reassemblingTimeout,
|
||||
blockSize: blockSize,
|
||||
}
|
||||
}
|
||||
|
||||
// Process processes an incoming fragment belonging to an ID and returns a
|
||||
// complete packet when all the packets belonging to that ID have been received.
|
||||
//
|
||||
// [first, last] is the range of the fragment bytes.
|
||||
//
|
||||
// first must be a multiple of the block size f is configured with. The size
|
||||
// of the fragment data must be a multiple of the block size, unless there are
|
||||
// no fragments following this fragment (more set to false).
|
||||
func (f *Fragmentation) Process(id uint32, first, last uint16, more bool, vv buffer.VectorisedView) (buffer.VectorisedView, bool, error) {
|
||||
if first > last {
|
||||
return buffer.VectorisedView{}, false, fmt.Errorf("first=%d is greater than last=%d: %w", first, last, ErrInvalidArgs)
|
||||
}
|
||||
|
||||
if first%f.blockSize != 0 {
|
||||
return buffer.VectorisedView{}, false, fmt.Errorf("first=%d is not a multiple of block size=%d: %w", first, f.blockSize, ErrInvalidArgs)
|
||||
}
|
||||
|
||||
fragmentSize := last - first + 1
|
||||
if more && fragmentSize%f.blockSize != 0 {
|
||||
return buffer.VectorisedView{}, false, fmt.Errorf("fragment size=%d bytes is not a multiple of block size=%d on non-final fragment: %w", fragmentSize, f.blockSize, ErrInvalidArgs)
|
||||
}
|
||||
|
||||
if l := vv.Size(); l < int(fragmentSize) {
|
||||
return buffer.VectorisedView{}, false, fmt.Errorf("got fragment size=%d bytes less than the expected fragment size=%d bytes (first=%d last=%d): %w", l, fragmentSize, first, last, ErrInvalidArgs)
|
||||
}
|
||||
vv.CapLength(int(fragmentSize))
|
||||
|
||||
f.mu.Lock()
|
||||
r, ok := f.reassemblers[id]
|
||||
if ok && r.tooOld(f.timeout) {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package fragmentation
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -81,7 +82,7 @@ var processTestCases = []struct {
|
||||
func TestFragmentationProcess(t *testing.T) {
|
||||
for _, c := range processTestCases {
|
||||
t.Run(c.comment, func(t *testing.T) {
|
||||
f := NewFragmentation(1024, 512, DefaultReassembleTimeout)
|
||||
f := NewFragmentation(minBlockSize, 1024, 512, DefaultReassembleTimeout)
|
||||
for i, in := range c.in {
|
||||
vv, done, err := f.Process(in.id, in.first, in.last, in.more, in.vv)
|
||||
if err != nil {
|
||||
@@ -110,7 +111,7 @@ func TestFragmentationProcess(t *testing.T) {
|
||||
|
||||
func TestReassemblingTimeout(t *testing.T) {
|
||||
timeout := time.Millisecond
|
||||
f := NewFragmentation(1024, 512, timeout)
|
||||
f := NewFragmentation(minBlockSize, 1024, 512, timeout)
|
||||
// Send first fragment with id = 0, first = 0, last = 0, and more = true.
|
||||
f.Process(0, 0, 0, true, vv(1, "0"))
|
||||
// Sleep more than the timeout.
|
||||
@@ -127,7 +128,7 @@ func TestReassemblingTimeout(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMemoryLimits(t *testing.T) {
|
||||
f := NewFragmentation(3, 1, DefaultReassembleTimeout)
|
||||
f := NewFragmentation(minBlockSize, 3, 1, DefaultReassembleTimeout)
|
||||
// Send first fragment with id = 0.
|
||||
f.Process(0, 0, 0, true, vv(1, "0"))
|
||||
// Send first fragment with id = 1.
|
||||
@@ -151,7 +152,7 @@ func TestMemoryLimits(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMemoryLimitsIgnoresDuplicates(t *testing.T) {
|
||||
f := NewFragmentation(1, 0, DefaultReassembleTimeout)
|
||||
f := NewFragmentation(minBlockSize, 1, 0, DefaultReassembleTimeout)
|
||||
// Send first fragment with id = 0.
|
||||
f.Process(0, 0, 0, true, vv(1, "0"))
|
||||
// Send the same packet again.
|
||||
@@ -163,3 +164,99 @@ func TestMemoryLimitsIgnoresDuplicates(t *testing.T) {
|
||||
t.Errorf("Wrong size, duplicates are not handled correctly: got=%d, want=%d.", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
const fragID = 5
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
blockSize uint16
|
||||
first uint16
|
||||
last uint16
|
||||
more bool
|
||||
data string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "exact block size without more",
|
||||
blockSize: 2,
|
||||
first: 2,
|
||||
last: 3,
|
||||
more: false,
|
||||
data: "01",
|
||||
},
|
||||
{
|
||||
name: "exact block size with more",
|
||||
blockSize: 2,
|
||||
first: 2,
|
||||
last: 3,
|
||||
more: true,
|
||||
data: "01",
|
||||
},
|
||||
{
|
||||
name: "exact block size with more and extra data",
|
||||
blockSize: 2,
|
||||
first: 2,
|
||||
last: 3,
|
||||
more: true,
|
||||
data: "012",
|
||||
},
|
||||
{
|
||||
name: "exact block size with more and too little data",
|
||||
blockSize: 2,
|
||||
first: 2,
|
||||
last: 3,
|
||||
more: true,
|
||||
data: "0",
|
||||
err: ErrInvalidArgs,
|
||||
},
|
||||
{
|
||||
name: "not exact block size with more",
|
||||
blockSize: 2,
|
||||
first: 2,
|
||||
last: 2,
|
||||
more: true,
|
||||
data: "0",
|
||||
err: ErrInvalidArgs,
|
||||
},
|
||||
{
|
||||
name: "not exact block size without more",
|
||||
blockSize: 2,
|
||||
first: 2,
|
||||
last: 2,
|
||||
more: false,
|
||||
data: "0",
|
||||
},
|
||||
{
|
||||
name: "first not a multiple of block size",
|
||||
blockSize: 2,
|
||||
first: 3,
|
||||
last: 4,
|
||||
more: true,
|
||||
data: "01",
|
||||
err: ErrInvalidArgs,
|
||||
},
|
||||
{
|
||||
name: "first more than last",
|
||||
blockSize: 2,
|
||||
first: 4,
|
||||
last: 3,
|
||||
more: true,
|
||||
data: "01",
|
||||
err: ErrInvalidArgs,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
f := NewFragmentation(test.blockSize, HighFragThreshold, LowFragThreshold, DefaultReassembleTimeout)
|
||||
_, done, err := f.Process(fragID, test.first, test.last, test.more, vv(len(test.data), test.data))
|
||||
if !errors.Is(err, test.err) {
|
||||
t.Errorf("got Proceess(%d, %d, %d, %t, %q) = (_, _, %v), want = (_, _, %v)", fragID, test.first, test.last, test.more, test.data, err, test.err)
|
||||
}
|
||||
if done {
|
||||
t.Errorf("got Proceess(%d, %d, %d, %t, %q) = (_, true, _), want = (_, false, _)", fragID, test.first, test.last, test.more, test.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,10 @@ const (
|
||||
|
||||
// buckets is the number of identifier buckets.
|
||||
buckets = 2048
|
||||
|
||||
// The size of a fragment block, in bytes, as per RFC 791 section 3.1,
|
||||
// page 14.
|
||||
fragmentblockSize = 8
|
||||
)
|
||||
|
||||
type endpoint struct {
|
||||
@@ -66,7 +70,7 @@ func (p *protocol) NewEndpoint(nicID tcpip.NICID, addrWithPrefix tcpip.AddressWi
|
||||
prefixLen: addrWithPrefix.PrefixLen,
|
||||
linkEP: linkEP,
|
||||
dispatcher: dispatcher,
|
||||
fragmentation: fragmentation.NewFragmentation(fragmentation.HighFragThreshold, fragmentation.LowFragThreshold, fragmentation.DefaultReassembleTimeout),
|
||||
fragmentation: fragmentation.NewFragmentation(fragmentblockSize, fragmentation.HighFragThreshold, fragmentation.LowFragThreshold, fragmentation.DefaultReassembleTimeout),
|
||||
protocol: p,
|
||||
stack: st,
|
||||
}
|
||||
|
||||
@@ -519,6 +519,11 @@ func TestReceiveFragments(t *testing.T) {
|
||||
// UDP header plus a payload of 0..256 in increments of 2.
|
||||
ipv4Payload2 := udpGen(128, 2)
|
||||
udpPayload2 := ipv4Payload2[header.UDPMinimumSize:]
|
||||
// UDP header plus a payload of 0..256 in increments of 3.
|
||||
// Used to test cases where the fragment blocks are not a multiple of
|
||||
// the fragment block size of 8 (RFC 791 section 3.1 page 14).
|
||||
ipv4Payload3 := udpGen(127, 3)
|
||||
udpPayload3 := ipv4Payload3[header.UDPMinimumSize:]
|
||||
|
||||
type fragmentData struct {
|
||||
id uint16
|
||||
@@ -544,6 +549,18 @@ func TestReceiveFragments(t *testing.T) {
|
||||
},
|
||||
expectedPayloads: [][]byte{udpPayload1},
|
||||
},
|
||||
{
|
||||
name: "No fragmentation with size not a multiple of fragment block size",
|
||||
fragments: []fragmentData{
|
||||
{
|
||||
id: 1,
|
||||
flags: 0,
|
||||
fragmentOffset: 0,
|
||||
payload: ipv4Payload3,
|
||||
},
|
||||
},
|
||||
expectedPayloads: [][]byte{udpPayload3},
|
||||
},
|
||||
{
|
||||
name: "More fragments without payload",
|
||||
fragments: []fragmentData{
|
||||
@@ -586,6 +603,42 @@ func TestReceiveFragments(t *testing.T) {
|
||||
},
|
||||
expectedPayloads: [][]byte{udpPayload1},
|
||||
},
|
||||
{
|
||||
name: "Two fragments with last fragment size not a multiple of fragment block size",
|
||||
fragments: []fragmentData{
|
||||
{
|
||||
id: 1,
|
||||
flags: header.IPv4FlagMoreFragments,
|
||||
fragmentOffset: 0,
|
||||
payload: ipv4Payload3[:64],
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
flags: 0,
|
||||
fragmentOffset: 64,
|
||||
payload: ipv4Payload3[64:],
|
||||
},
|
||||
},
|
||||
expectedPayloads: [][]byte{udpPayload3},
|
||||
},
|
||||
{
|
||||
name: "Two fragments with first fragment size not a multiple of fragment block size",
|
||||
fragments: []fragmentData{
|
||||
{
|
||||
id: 1,
|
||||
flags: header.IPv4FlagMoreFragments,
|
||||
fragmentOffset: 0,
|
||||
payload: ipv4Payload3[:63],
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
flags: 0,
|
||||
fragmentOffset: 63,
|
||||
payload: ipv4Payload3[63:],
|
||||
},
|
||||
},
|
||||
expectedPayloads: nil,
|
||||
},
|
||||
{
|
||||
name: "Second fragment has MoreFlags set",
|
||||
fragments: []fragmentData{
|
||||
|
||||
@@ -467,7 +467,7 @@ func (p *protocol) NewEndpoint(nicID tcpip.NICID, addrWithPrefix tcpip.AddressWi
|
||||
linkEP: linkEP,
|
||||
linkAddrCache: linkAddrCache,
|
||||
dispatcher: dispatcher,
|
||||
fragmentation: fragmentation.NewFragmentation(fragmentation.HighFragThreshold, fragmentation.LowFragThreshold, fragmentation.DefaultReassembleTimeout),
|
||||
fragmentation: fragmentation.NewFragmentation(header.IPv6FragmentExtHdrFragmentOffsetBytesPerUnit, fragmentation.HighFragThreshold, fragmentation.LowFragThreshold, fragmentation.DefaultReassembleTimeout),
|
||||
protocol: p,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -678,13 +678,18 @@ type fragmentData struct {
|
||||
}
|
||||
|
||||
func TestReceiveIPv6Fragments(t *testing.T) {
|
||||
const nicID = 1
|
||||
const udpPayload1Length = 256
|
||||
const udpPayload2Length = 128
|
||||
const fragmentExtHdrLen = 8
|
||||
// Note, not all routing extension headers will be 8 bytes but this test
|
||||
// uses 8 byte routing extension headers for most sub tests.
|
||||
const routingExtHdrLen = 8
|
||||
const (
|
||||
nicID = 1
|
||||
udpPayload1Length = 256
|
||||
udpPayload2Length = 128
|
||||
// Used to test cases where the fragment blocks are not a multiple of
|
||||
// the fragment block size of 8 (RFC 8200 section 4.5).
|
||||
udpPayload3Length = 127
|
||||
fragmentExtHdrLen = 8
|
||||
// Note, not all routing extension headers will be 8 bytes but this test
|
||||
// uses 8 byte routing extension headers for most sub tests.
|
||||
routingExtHdrLen = 8
|
||||
)
|
||||
|
||||
udpGen := func(payload []byte, multiplier uint8) buffer.View {
|
||||
payloadLen := len(payload)
|
||||
@@ -716,6 +721,10 @@ func TestReceiveIPv6Fragments(t *testing.T) {
|
||||
udpPayload2 := udpPayload2Buf[:]
|
||||
ipv6Payload2 := udpGen(udpPayload2, 2)
|
||||
|
||||
var udpPayload3Buf [udpPayload3Length]byte
|
||||
udpPayload3 := udpPayload3Buf[:]
|
||||
ipv6Payload3 := udpGen(udpPayload3, 3)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
expectedPayload []byte
|
||||
@@ -750,6 +759,24 @@ func TestReceiveIPv6Fragments(t *testing.T) {
|
||||
},
|
||||
expectedPayloads: [][]byte{udpPayload1},
|
||||
},
|
||||
{
|
||||
name: "Atomic fragment with size not a multiple of fragment block size",
|
||||
fragments: []fragmentData{
|
||||
{
|
||||
nextHdr: fragmentExtHdrID,
|
||||
data: buffer.NewVectorisedView(
|
||||
fragmentExtHdrLen+len(ipv6Payload3),
|
||||
[]buffer.View{
|
||||
// Fragment extension header.
|
||||
buffer.View([]byte{uint8(header.UDPProtocolNumber), 0, 0, 0, 0, 0, 0, 0}),
|
||||
|
||||
ipv6Payload3,
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
expectedPayloads: [][]byte{udpPayload3},
|
||||
},
|
||||
{
|
||||
name: "Two fragments",
|
||||
fragments: []fragmentData{
|
||||
@@ -784,6 +811,74 @@ func TestReceiveIPv6Fragments(t *testing.T) {
|
||||
},
|
||||
expectedPayloads: [][]byte{udpPayload1},
|
||||
},
|
||||
{
|
||||
name: "Two fragments with last fragment size not a multiple of fragment block size",
|
||||
fragments: []fragmentData{
|
||||
{
|
||||
nextHdr: fragmentExtHdrID,
|
||||
data: buffer.NewVectorisedView(
|
||||
fragmentExtHdrLen+64,
|
||||
[]buffer.View{
|
||||
// Fragment extension header.
|
||||
//
|
||||
// Fragment offset = 0, More = true, ID = 1
|
||||
buffer.View([]byte{uint8(header.UDPProtocolNumber), 0, 0, 1, 0, 0, 0, 1}),
|
||||
|
||||
ipv6Payload3[:64],
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
nextHdr: fragmentExtHdrID,
|
||||
data: buffer.NewVectorisedView(
|
||||
fragmentExtHdrLen+len(ipv6Payload3)-64,
|
||||
[]buffer.View{
|
||||
// Fragment extension header.
|
||||
//
|
||||
// Fragment offset = 8, More = false, ID = 1
|
||||
buffer.View([]byte{uint8(header.UDPProtocolNumber), 0, 0, 64, 0, 0, 0, 1}),
|
||||
|
||||
ipv6Payload3[64:],
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
expectedPayloads: [][]byte{udpPayload3},
|
||||
},
|
||||
{
|
||||
name: "Two fragments with first fragment size not a multiple of fragment block size",
|
||||
fragments: []fragmentData{
|
||||
{
|
||||
nextHdr: fragmentExtHdrID,
|
||||
data: buffer.NewVectorisedView(
|
||||
fragmentExtHdrLen+63,
|
||||
[]buffer.View{
|
||||
// Fragment extension header.
|
||||
//
|
||||
// Fragment offset = 0, More = true, ID = 1
|
||||
buffer.View([]byte{uint8(header.UDPProtocolNumber), 0, 0, 1, 0, 0, 0, 1}),
|
||||
|
||||
ipv6Payload3[:63],
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
nextHdr: fragmentExtHdrID,
|
||||
data: buffer.NewVectorisedView(
|
||||
fragmentExtHdrLen+len(ipv6Payload3)-63,
|
||||
[]buffer.View{
|
||||
// Fragment extension header.
|
||||
//
|
||||
// Fragment offset = 8, More = false, ID = 1
|
||||
buffer.View([]byte{uint8(header.UDPProtocolNumber), 0, 0, 64, 0, 0, 0, 1}),
|
||||
|
||||
ipv6Payload3[63:],
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
expectedPayloads: nil,
|
||||
},
|
||||
{
|
||||
name: "Two fragments with different IDs",
|
||||
fragments: []fragmentData{
|
||||
|
||||
Reference in New Issue
Block a user