Discard IP fragments as soon as it expires

Currently expired IP fragments are discarded only if another fragment for the
same IP datagram is received after timeout or the total size of the fragment
queue exceeded a predefined value.

Test: fragmentation.TestReassemblingTimeout

Fixes #3960

PiperOrigin-RevId: 334423710
This commit is contained in:
Toshi Kikuchi
2020-09-29 11:29:50 -07:00
committed by gVisor bot
parent b6fb11a290
commit f15182243e
10 changed files with 203 additions and 41 deletions
+20
View File
@@ -24,6 +24,26 @@ import (
"gvisor.dev/gvisor/pkg/tcpip"
)
// NullClock implements a clock that never advances.
type NullClock struct{}
var _ tcpip.Clock = (*NullClock)(nil)
// NowNanoseconds implements tcpip.Clock.NowNanoseconds.
func (*NullClock) NowNanoseconds() int64 {
return 0
}
// NowMonotonic implements tcpip.Clock.NowMonotonic.
func (*NullClock) NowMonotonic() int64 {
return 0
}
// AfterFunc implements tcpip.Clock.AfterFunc.
func (*NullClock) AfterFunc(time.Duration, func()) tcpip.Timer {
return nil
}
// ManualClock implements tcpip.Clock and only advances manually with Advance
// method.
type ManualClock struct {
+1
View File
@@ -43,5 +43,6 @@ go_test(
library = ":fragmentation",
deps = [
"//pkg/tcpip/buffer",
"//pkg/tcpip/faketime",
],
)
@@ -81,6 +81,8 @@ type Fragmentation struct {
size int
timeout time.Duration
blockSize uint16
clock tcpip.Clock
releaseJob *tcpip.Job
}
// NewFragmentation creates a new Fragmentation.
@@ -97,7 +99,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(blockSize uint16, highMemoryLimit, lowMemoryLimit int, reassemblingTimeout time.Duration) *Fragmentation {
func NewFragmentation(blockSize uint16, highMemoryLimit, lowMemoryLimit int, reassemblingTimeout time.Duration, clock tcpip.Clock) *Fragmentation {
if lowMemoryLimit >= highMemoryLimit {
lowMemoryLimit = highMemoryLimit
}
@@ -110,13 +112,17 @@ func NewFragmentation(blockSize uint16, highMemoryLimit, lowMemoryLimit int, rea
blockSize = minBlockSize
}
return &Fragmentation{
f := &Fragmentation{
reassemblers: make(map[FragmentID]*reassembler),
highLimit: highMemoryLimit,
lowLimit: lowMemoryLimit,
timeout: reassemblingTimeout,
blockSize: blockSize,
clock: clock,
}
f.releaseJob = tcpip.NewJob(f.clock, &f.mu, f.releaseReassemblersLocked)
return f
}
// Process processes an incoming fragment belonging to an ID and returns a
@@ -155,15 +161,17 @@ func (f *Fragmentation) Process(
f.mu.Lock()
r, ok := f.reassemblers[id]
if ok && r.tooOld(f.timeout) {
// This is very likely to be an id-collision or someone performing a slow-rate attack.
f.release(r)
ok = false
}
if !ok {
r = newReassembler(id)
r = newReassembler(id, f.clock)
f.reassemblers[id] = r
wasEmpty := f.rList.Empty()
f.rList.PushFront(r)
if wasEmpty {
// If we have just pushed a first reassembler into an empty list, we
// should kickstart the release job. The release job will keep
// rescheduling itself until the list becomes empty.
f.releaseReassemblersLocked()
}
}
f.mu.Unlock()
@@ -211,3 +219,27 @@ func (f *Fragmentation) release(r *reassembler) {
f.size = 0
}
}
// releaseReassemblersLocked releases already-expired reassemblers, then
// schedules the job to call back itself for the remaining reassemblers if
// any. This function must be called with f.mu locked.
func (f *Fragmentation) releaseReassemblersLocked() {
now := f.clock.NowMonotonic()
for {
// The reassembler at the end of the list is the oldest.
r := f.rList.Back()
if r == nil {
// The list is empty.
break
}
elapsed := time.Duration(now-r.creationTime) * time.Nanosecond
if f.timeout > elapsed {
// If the oldest reassembler has not expired, schedule the release
// job so that this function is called back when it has expired.
f.releaseJob.Schedule(f.timeout - elapsed)
break
}
// If the oldest reassembler has already expired, release it.
f.release(r)
}
}
@@ -21,6 +21,7 @@ import (
"time"
"gvisor.dev/gvisor/pkg/tcpip/buffer"
"gvisor.dev/gvisor/pkg/tcpip/faketime"
)
// vv is a helper to build VectorisedView from different strings.
@@ -95,7 +96,7 @@ var processTestCases = []struct {
func TestFragmentationProcess(t *testing.T) {
for _, c := range processTestCases {
t.Run(c.comment, func(t *testing.T) {
f := NewFragmentation(minBlockSize, 1024, 512, DefaultReassembleTimeout)
f := NewFragmentation(minBlockSize, 1024, 512, DefaultReassembleTimeout, &faketime.NullClock{})
firstFragmentProto := c.in[0].proto
for i, in := range c.in {
vv, proto, done, err := f.Process(in.id, in.first, in.last, in.more, in.proto, in.vv)
@@ -131,25 +132,126 @@ func TestFragmentationProcess(t *testing.T) {
}
func TestReassemblingTimeout(t *testing.T) {
timeout := time.Millisecond
f := NewFragmentation(minBlockSize, 1024, 512, timeout)
// Send first fragment with id = 0, first = 0, last = 0, and more = true.
f.Process(FragmentID{}, 0, 0, true, 0xFF, vv(1, "0"))
// Sleep more than the timeout.
time.Sleep(2 * timeout)
// Send another fragment that completes a packet.
// However, no packet should be reassembled because the fragment arrived after the timeout.
_, _, done, err := f.Process(FragmentID{}, 1, 1, false, 0xFF, vv(1, "1"))
if err != nil {
t.Fatalf("f.Process(0, 1, 1, false, 0xFF, vv(1, \"1\")) failed: %v", err)
const (
reassemblyTimeout = time.Millisecond
protocol = 0xff
)
type fragment struct {
first uint16
last uint16
more bool
data string
}
if done {
t.Errorf("Fragmentation does not respect the reassembling timeout.")
type event struct {
// name is a nickname of this event.
name string
// clockAdvance is a duration to advance the clock. The clock advances
// before a fragment specified in the fragment field is processed.
clockAdvance time.Duration
// fragment is a fragment to process. This can be nil if there is no
// fragment to process.
fragment *fragment
// expectDone is true if the fragmentation instance should report the
// reassembly is done after the fragment is processd.
expectDone bool
// sizeAfterEvent is the expected size of the fragmentation instance after
// the event.
sizeAfterEvent int
}
half1 := &fragment{first: 0, last: 0, more: true, data: "0"}
half2 := &fragment{first: 1, last: 1, more: false, data: "1"}
tests := []struct {
name string
events []event
}{
{
name: "half1 and half2 are reassembled successfully",
events: []event{
{
name: "half1",
fragment: half1,
expectDone: false,
sizeAfterEvent: 1,
},
{
name: "half2",
fragment: half2,
expectDone: true,
sizeAfterEvent: 0,
},
},
},
{
name: "half1 timeout, half2 timeout",
events: []event{
{
name: "half1",
fragment: half1,
expectDone: false,
sizeAfterEvent: 1,
},
{
name: "half1 just before reassembly timeout",
clockAdvance: reassemblyTimeout - 1,
sizeAfterEvent: 1,
},
{
name: "half1 reassembly timeout",
clockAdvance: 1,
sizeAfterEvent: 0,
},
{
name: "half2",
fragment: half2,
expectDone: false,
sizeAfterEvent: 1,
},
{
name: "half2 just before reassembly timeout",
clockAdvance: reassemblyTimeout - 1,
sizeAfterEvent: 1,
},
{
name: "half2 reassembly timeout",
clockAdvance: 1,
sizeAfterEvent: 0,
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
clock := faketime.NewManualClock()
f := NewFragmentation(minBlockSize, HighFragThreshold, LowFragThreshold, reassemblyTimeout, clock)
for _, event := range test.events {
clock.Advance(event.clockAdvance)
if frag := event.fragment; frag != nil {
_, _, done, err := f.Process(FragmentID{}, frag.first, frag.last, frag.more, protocol, vv(len(frag.data), frag.data))
if err != nil {
t.Fatalf("%s: f.Process failed: %s", event.name, err)
}
if done != event.expectDone {
t.Fatalf("%s: got done = %t, want = %t", event.name, done, event.expectDone)
}
}
if got, want := f.size, event.sizeAfterEvent; got != want {
t.Errorf("%s: got f.size = %d, want = %d", event.name, got, want)
}
}
})
}
}
func TestMemoryLimits(t *testing.T) {
f := NewFragmentation(minBlockSize, 3, 1, DefaultReassembleTimeout)
f := NewFragmentation(minBlockSize, 3, 1, DefaultReassembleTimeout, &faketime.NullClock{})
// Send first fragment with id = 0.
f.Process(FragmentID{ID: 0}, 0, 0, true, 0xFF, vv(1, "0"))
// Send first fragment with id = 1.
@@ -173,7 +275,7 @@ func TestMemoryLimits(t *testing.T) {
}
func TestMemoryLimitsIgnoresDuplicates(t *testing.T) {
f := NewFragmentation(minBlockSize, 1, 0, DefaultReassembleTimeout)
f := NewFragmentation(minBlockSize, 1, 0, DefaultReassembleTimeout, &faketime.NullClock{})
// Send first fragment with id = 0.
f.Process(FragmentID{}, 0, 0, true, 0xFF, vv(1, "0"))
// Send the same packet again.
@@ -268,7 +370,7 @@ func TestErrors(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
f := NewFragmentation(test.blockSize, HighFragThreshold, LowFragThreshold, DefaultReassembleTimeout)
f := NewFragmentation(test.blockSize, HighFragThreshold, LowFragThreshold, DefaultReassembleTimeout, &faketime.NullClock{})
_, _, done, err := f.Process(FragmentID{}, test.first, test.last, test.more, 0, vv(len(test.data), test.data))
if !errors.Is(err, test.err) {
t.Errorf("got Process(_, %d, %d, %t, _, %q) = (_, _, _, %v), want = (_, _, _, %v)", test.first, test.last, test.more, test.data, err, test.err)
@@ -18,9 +18,9 @@ import (
"container/heap"
"fmt"
"math"
"time"
"gvisor.dev/gvisor/pkg/sync"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/buffer"
)
@@ -40,15 +40,15 @@ type reassembler struct {
deleted int
heap fragHeap
done bool
creationTime time.Time
creationTime int64
}
func newReassembler(id FragmentID) *reassembler {
func newReassembler(id FragmentID, clock tcpip.Clock) *reassembler {
r := &reassembler{
id: id,
holes: make([]hole, 0, 16),
heap: make(fragHeap, 0, 8),
creationTime: time.Now(),
creationTime: clock.NowMonotonic(),
}
r.holes = append(r.holes, hole{
first: 0,
@@ -116,10 +116,6 @@ func (r *reassembler) process(first, last uint16, more bool, proto uint8, vv buf
return res, r.proto, true, consumed, nil
}
func (r *reassembler) tooOld(timeout time.Duration) bool {
return time.Now().Sub(r.creationTime) > timeout
}
func (r *reassembler) checkDoneOrMark() bool {
r.mu.Lock()
prev := r.done
@@ -18,6 +18,8 @@ import (
"math"
"reflect"
"testing"
"gvisor.dev/gvisor/pkg/tcpip/faketime"
)
type updateHolesInput struct {
@@ -94,7 +96,7 @@ var holesTestCases = []struct {
func TestUpdateHoles(t *testing.T) {
for _, c := range holesTestCases {
r := newReassembler(FragmentID{})
r := newReassembler(FragmentID{}, &faketime.NullClock{})
for _, i := range c.in {
r.updateHoles(i.first, i.last, i.more)
}
+1 -1
View File
@@ -804,6 +804,6 @@ func NewProtocol(s *stack.Stack) stack.NetworkProtocol {
ids: ids,
hashIV: hashIV,
defaultTTL: DefaultTTL,
fragmentation: fragmentation.NewFragmentation(fragmentblockSize, fragmentation.HighFragThreshold, fragmentation.LowFragThreshold, fragmentation.DefaultReassembleTimeout),
fragmentation: fragmentation.NewFragmentation(fragmentblockSize, fragmentation.HighFragThreshold, fragmentation.LowFragThreshold, fragmentation.DefaultReassembleTimeout, s.Clock()),
}
}
+4 -1
View File
@@ -1236,7 +1236,10 @@ func TestLinkAddressRequest(t *testing.T) {
}
for _, test := range tests {
p := NewProtocol(nil)
s := stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{NewProtocol},
})
p := s.NetworkProtocolInstance(ProtocolNumber)
linkRes, ok := p.(stack.LinkAddressResolver)
if !ok {
t.Fatalf("expected IPv6 protocol to implement stack.LinkAddressResolver")
+1 -1
View File
@@ -1306,7 +1306,7 @@ func NewProtocolWithOptions(opts Options) stack.NetworkProtocolFactory {
return func(s *stack.Stack) stack.NetworkProtocol {
p := &protocol{
stack: s,
fragmentation: fragmentation.NewFragmentation(header.IPv6FragmentExtHdrFragmentOffsetBytesPerUnit, fragmentation.HighFragThreshold, fragmentation.LowFragThreshold, fragmentation.DefaultReassembleTimeout),
fragmentation: fragmentation.NewFragmentation(header.IPv6FragmentExtHdrFragmentOffsetBytesPerUnit, fragmentation.HighFragThreshold, fragmentation.LowFragThreshold, fragmentation.DefaultReassembleTimeout, s.Clock()),
ndpDisp: opts.NDPDisp,
ndpConfigs: opts.NDPConfigs,
+10 -4
View File
@@ -1507,13 +1507,19 @@ func TestTTL(t *testing.T) {
if flow.isMulticast() {
wantTTL = multicastTTL
} else {
var p stack.NetworkProtocol
var p stack.NetworkProtocolFactory
var n tcpip.NetworkProtocolNumber
if flow.isV4() {
p = ipv4.NewProtocol(nil)
p = ipv4.NewProtocol
n = ipv4.ProtocolNumber
} else {
p = ipv6.NewProtocol(nil)
p = ipv6.NewProtocol
n = ipv6.ProtocolNumber
}
ep := p.NewEndpoint(&testInterface{}, nil, nil, nil)
s := stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{p},
})
ep := s.NetworkProtocolInstance(n).NewEndpoint(&testInterface{}, nil, nil, nil)
wantTTL = ep.DefaultTTL()
ep.Close()
}