mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
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:
committed by
gVisor bot
parent
b6fb11a290
commit
f15182243e
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user