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:
Ghanan Gowripalan
2020-07-30 14:25:53 -07:00
committed by gVisor bot
parent 3c70b4c986
commit 9960a816a9
6 changed files with 319 additions and 26 deletions
@@ -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)
}
})
}
}
+5 -1
View File
@@ -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,
}
+53
View File
@@ -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{
+1 -1
View File
@@ -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
}
+102 -7
View File
@@ -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{