mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Only use the NextHeader value of the first IPv6 fragment extension header.
As per RFC 8200 Section 4.5: The Next Header field of the last header of the Per-Fragment headers is obtained from the Next Header field of the first fragment's Fragment header. Test: - pkg/tcpip/network/ipv6:ipv6_test - pkg/tcpip/network/ipv4:ipv4_test - pkg/tcpip/network/fragmentation:fragmentation_test Updates #2197 PiperOrigin-RevId: 327671635
This commit is contained in:
@@ -41,5 +41,7 @@ go_test(
|
||||
"reassembler_test.go",
|
||||
],
|
||||
library = ":fragmentation",
|
||||
deps = ["//pkg/tcpip/buffer"],
|
||||
deps = [
|
||||
"//pkg/tcpip/buffer",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -120,29 +120,36 @@ func NewFragmentation(blockSize uint16, highMemoryLimit, lowMemoryLimit int, rea
|
||||
}
|
||||
|
||||
// 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.
|
||||
// complete packet and its protocol number 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 FragmentID, first, last uint16, more bool, vv buffer.VectorisedView) (buffer.VectorisedView, bool, error) {
|
||||
//
|
||||
// proto is the protocol number marked in the fragment being processed. It has
|
||||
// to be given here outside of the FragmentID struct because IPv6 should not use
|
||||
// the protocol to identify a fragment.
|
||||
func (f *Fragmentation) Process(
|
||||
id FragmentID, first, last uint16, more bool, proto uint8, vv buffer.VectorisedView) (
|
||||
buffer.VectorisedView, uint8, bool, error) {
|
||||
if first > last {
|
||||
return buffer.VectorisedView{}, false, fmt.Errorf("first=%d is greater than last=%d: %w", first, last, ErrInvalidArgs)
|
||||
return buffer.VectorisedView{}, 0, 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)
|
||||
return buffer.VectorisedView{}, 0, 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)
|
||||
return buffer.VectorisedView{}, 0, 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)
|
||||
return buffer.VectorisedView{}, 0, 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))
|
||||
|
||||
@@ -160,14 +167,14 @@ func (f *Fragmentation) Process(id FragmentID, first, last uint16, more bool, vv
|
||||
}
|
||||
f.mu.Unlock()
|
||||
|
||||
res, done, consumed, err := r.process(first, last, more, vv)
|
||||
res, firstFragmentProto, done, consumed, err := r.process(first, last, more, proto, vv)
|
||||
if err != nil {
|
||||
// We probably got an invalid sequence of fragments. Just
|
||||
// discard the reassembler and move on.
|
||||
f.mu.Lock()
|
||||
f.release(r)
|
||||
f.mu.Unlock()
|
||||
return buffer.VectorisedView{}, false, fmt.Errorf("fragmentation processing error: %v", err)
|
||||
return buffer.VectorisedView{}, 0, false, fmt.Errorf("fragmentation processing error: %w", err)
|
||||
}
|
||||
f.mu.Lock()
|
||||
f.size += consumed
|
||||
@@ -186,7 +193,7 @@ func (f *Fragmentation) Process(id FragmentID, first, last uint16, more bool, vv
|
||||
}
|
||||
}
|
||||
f.mu.Unlock()
|
||||
return res, done, nil
|
||||
return res, firstFragmentProto, done, nil
|
||||
}
|
||||
|
||||
func (f *Fragmentation) release(r *reassembler) {
|
||||
|
||||
@@ -38,12 +38,14 @@ type processInput struct {
|
||||
first uint16
|
||||
last uint16
|
||||
more bool
|
||||
proto uint8
|
||||
vv buffer.VectorisedView
|
||||
}
|
||||
|
||||
type processOutput struct {
|
||||
vv buffer.VectorisedView
|
||||
done bool
|
||||
vv buffer.VectorisedView
|
||||
proto uint8
|
||||
done bool
|
||||
}
|
||||
|
||||
var processTestCases = []struct {
|
||||
@@ -62,6 +64,17 @@ var processTestCases = []struct {
|
||||
{vv: vv(4, "01", "23"), done: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
comment: "Next Header protocol mismatch",
|
||||
in: []processInput{
|
||||
{id: FragmentID{ID: 0}, first: 0, last: 1, more: true, proto: 6, vv: vv(2, "01")},
|
||||
{id: FragmentID{ID: 0}, first: 2, last: 3, more: false, proto: 17, vv: vv(2, "23")},
|
||||
},
|
||||
out: []processOutput{
|
||||
{vv: buffer.VectorisedView{}, done: false},
|
||||
{vv: vv(4, "01", "23"), proto: 6, done: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
comment: "Two IDs",
|
||||
in: []processInput{
|
||||
@@ -83,18 +96,26 @@ func TestFragmentationProcess(t *testing.T) {
|
||||
for _, c := range processTestCases {
|
||||
t.Run(c.comment, func(t *testing.T) {
|
||||
f := NewFragmentation(minBlockSize, 1024, 512, DefaultReassembleTimeout)
|
||||
firstFragmentProto := c.in[0].proto
|
||||
for i, in := range c.in {
|
||||
vv, done, err := f.Process(in.id, in.first, in.last, in.more, in.vv)
|
||||
vv, proto, done, err := f.Process(in.id, in.first, in.last, in.more, in.proto, in.vv)
|
||||
if err != nil {
|
||||
t.Fatalf("f.Process(%+v, %+d, %+d, %t, %+v) failed: %v", in.id, in.first, in.last, in.more, in.vv, err)
|
||||
t.Fatalf("f.Process(%+v, %d, %d, %t, %d, %X) failed: %s",
|
||||
in.id, in.first, in.last, in.more, in.proto, in.vv.ToView(), err)
|
||||
}
|
||||
if !reflect.DeepEqual(vv, c.out[i].vv) {
|
||||
t.Errorf("got Process(%d) = %+v, want = %+v", i, vv, c.out[i].vv)
|
||||
t.Errorf("got Process(%+v, %d, %d, %t, %d, %X) = (%X, _, _, _), want = (%X, _, _, _)",
|
||||
in.id, in.first, in.last, in.more, in.proto, in.vv.ToView(), vv.ToView(), c.out[i].vv.ToView())
|
||||
}
|
||||
if done != c.out[i].done {
|
||||
t.Errorf("got Process(%d) = %+v, want = %+v", i, done, c.out[i].done)
|
||||
t.Errorf("got Process(%+v, %d, %d, %t, %d, _) = (_, _, %t, _), want = (_, _, %t, _)",
|
||||
in.id, in.first, in.last, in.more, in.proto, done, c.out[i].done)
|
||||
}
|
||||
if c.out[i].done {
|
||||
if firstFragmentProto != proto {
|
||||
t.Errorf("got Process(%+v, %d, %d, %t, %d, _) = (_, %d, _, _), want = (_, %d, _, _)",
|
||||
in.id, in.first, in.last, in.more, in.proto, proto, firstFragmentProto)
|
||||
}
|
||||
if _, ok := f.reassemblers[in.id]; ok {
|
||||
t.Errorf("Process(%d) did not remove buffer from reassemblers", i)
|
||||
}
|
||||
@@ -113,14 +134,14 @@ 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, vv(1, "0"))
|
||||
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, vv(1, "1"))
|
||||
_, _, done, err := f.Process(FragmentID{}, 1, 1, false, 0xFF, vv(1, "1"))
|
||||
if err != nil {
|
||||
t.Fatalf("f.Process(0, 1, 1, false, vv(1, \"1\")) failed: %v", err)
|
||||
t.Fatalf("f.Process(0, 1, 1, false, 0xFF, vv(1, \"1\")) failed: %v", err)
|
||||
}
|
||||
if done {
|
||||
t.Errorf("Fragmentation does not respect the reassembling timeout.")
|
||||
@@ -130,15 +151,15 @@ func TestReassemblingTimeout(t *testing.T) {
|
||||
func TestMemoryLimits(t *testing.T) {
|
||||
f := NewFragmentation(minBlockSize, 3, 1, DefaultReassembleTimeout)
|
||||
// Send first fragment with id = 0.
|
||||
f.Process(FragmentID{ID: 0}, 0, 0, true, vv(1, "0"))
|
||||
f.Process(FragmentID{ID: 0}, 0, 0, true, 0xFF, vv(1, "0"))
|
||||
// Send first fragment with id = 1.
|
||||
f.Process(FragmentID{ID: 1}, 0, 0, true, vv(1, "1"))
|
||||
f.Process(FragmentID{ID: 1}, 0, 0, true, 0xFF, vv(1, "1"))
|
||||
// Send first fragment with id = 2.
|
||||
f.Process(FragmentID{ID: 2}, 0, 0, true, vv(1, "2"))
|
||||
f.Process(FragmentID{ID: 2}, 0, 0, true, 0xFF, vv(1, "2"))
|
||||
|
||||
// Send first fragment with id = 3. This should caused id = 0 and id = 1 to be
|
||||
// evicted.
|
||||
f.Process(FragmentID{ID: 3}, 0, 0, true, vv(1, "3"))
|
||||
f.Process(FragmentID{ID: 3}, 0, 0, true, 0xFF, vv(1, "3"))
|
||||
|
||||
if _, ok := f.reassemblers[FragmentID{ID: 0}]; ok {
|
||||
t.Errorf("Memory limits are not respected: id=0 has not been evicted.")
|
||||
@@ -154,9 +175,9 @@ func TestMemoryLimits(t *testing.T) {
|
||||
func TestMemoryLimitsIgnoresDuplicates(t *testing.T) {
|
||||
f := NewFragmentation(minBlockSize, 1, 0, DefaultReassembleTimeout)
|
||||
// Send first fragment with id = 0.
|
||||
f.Process(FragmentID{}, 0, 0, true, vv(1, "0"))
|
||||
f.Process(FragmentID{}, 0, 0, true, 0xFF, vv(1, "0"))
|
||||
// Send the same packet again.
|
||||
f.Process(FragmentID{}, 0, 0, true, vv(1, "0"))
|
||||
f.Process(FragmentID{}, 0, 0, true, 0xFF, vv(1, "0"))
|
||||
|
||||
got := f.size
|
||||
want := 1
|
||||
@@ -248,12 +269,12 @@ func TestErrors(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
f := NewFragmentation(test.blockSize, HighFragThreshold, LowFragThreshold, DefaultReassembleTimeout)
|
||||
_, done, err := f.Process(FragmentID{}, test.first, test.last, test.more, vv(len(test.data), test.data))
|
||||
_, _, 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 Proceess(_, %d, %d, %t, %q) = (_, _, %v), want = (_, _, %v)", test.first, test.last, test.more, test.data, 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)
|
||||
}
|
||||
if done {
|
||||
t.Errorf("got Proceess(_, %d, %d, %t, %q) = (_, true, _), want = (_, false, _)", test.first, test.last, test.more, test.data)
|
||||
t.Errorf("got Process(_, %d, %d, %t, _, %q) = (_, _, true, _), want = (_, _, false, _)", test.first, test.last, test.more, test.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ type reassembler struct {
|
||||
reassemblerEntry
|
||||
id FragmentID
|
||||
size int
|
||||
proto uint8
|
||||
mu sync.Mutex
|
||||
holes []hole
|
||||
deleted int
|
||||
@@ -46,7 +47,6 @@ func newReassembler(id FragmentID) *reassembler {
|
||||
r := &reassembler{
|
||||
id: id,
|
||||
holes: make([]hole, 0, 16),
|
||||
deleted: 0,
|
||||
heap: make(fragHeap, 0, 8),
|
||||
creationTime: time.Now(),
|
||||
}
|
||||
@@ -78,7 +78,7 @@ func (r *reassembler) updateHoles(first, last uint16, more bool) bool {
|
||||
return used
|
||||
}
|
||||
|
||||
func (r *reassembler) process(first, last uint16, more bool, vv buffer.VectorisedView) (buffer.VectorisedView, bool, int, error) {
|
||||
func (r *reassembler) process(first, last uint16, more bool, proto uint8, vv buffer.VectorisedView) (buffer.VectorisedView, uint8, bool, int, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
consumed := 0
|
||||
@@ -86,7 +86,18 @@ func (r *reassembler) process(first, last uint16, more bool, vv buffer.Vectorise
|
||||
// A concurrent goroutine might have already reassembled
|
||||
// the packet and emptied the heap while this goroutine
|
||||
// was waiting on the mutex. We don't have to do anything in this case.
|
||||
return buffer.VectorisedView{}, false, consumed, nil
|
||||
return buffer.VectorisedView{}, 0, false, consumed, nil
|
||||
}
|
||||
// For IPv6, it is possible to have different Protocol values between
|
||||
// fragments of a packet (because, unlike IPv4, the Protocol is not used to
|
||||
// identify a fragment). In this case, only the Protocol of the first
|
||||
// fragment must be used as per RFC 8200 Section 4.5.
|
||||
//
|
||||
// TODO(gvisor.dev/issue/3648): The entire first IP header should be recorded
|
||||
// here (instead of just the protocol) because most IP options should be
|
||||
// derived from the first fragment.
|
||||
if first == 0 {
|
||||
r.proto = proto
|
||||
}
|
||||
if r.updateHoles(first, last, more) {
|
||||
// We store the incoming packet only if it filled some holes.
|
||||
@@ -96,13 +107,13 @@ func (r *reassembler) process(first, last uint16, more bool, vv buffer.Vectorise
|
||||
}
|
||||
// Check if all the holes have been deleted and we are ready to reassamble.
|
||||
if r.deleted < len(r.holes) {
|
||||
return buffer.VectorisedView{}, false, consumed, nil
|
||||
return buffer.VectorisedView{}, 0, false, consumed, nil
|
||||
}
|
||||
res, err := r.heap.reassemble()
|
||||
if err != nil {
|
||||
return buffer.VectorisedView{}, false, consumed, fmt.Errorf("fragment reassembly failed: %v", err)
|
||||
return buffer.VectorisedView{}, 0, false, consumed, fmt.Errorf("fragment reassembly failed: %w", err)
|
||||
}
|
||||
return res, true, consumed, nil
|
||||
return res, r.proto, true, consumed, nil
|
||||
}
|
||||
|
||||
func (r *reassembler) tooOld(timeout time.Duration) bool {
|
||||
|
||||
@@ -415,18 +415,20 @@ func (e *endpoint) HandlePacket(r *stack.Route, pkt *stack.PacketBuffer) {
|
||||
}
|
||||
var ready bool
|
||||
var err error
|
||||
pkt.Data, ready, err = e.protocol.fragmentation.Process(
|
||||
proto := h.Protocol()
|
||||
pkt.Data, _, ready, err = e.protocol.fragmentation.Process(
|
||||
// As per RFC 791 section 2.3, the identification value is unique
|
||||
// for a source-destination pair and protocol.
|
||||
fragmentation.FragmentID{
|
||||
Source: h.SourceAddress(),
|
||||
Destination: h.DestinationAddress(),
|
||||
ID: uint32(h.ID()),
|
||||
Protocol: h.Protocol(),
|
||||
Protocol: proto,
|
||||
},
|
||||
h.FragmentOffset(),
|
||||
last,
|
||||
h.More(),
|
||||
proto,
|
||||
pkt.Data,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -321,10 +321,9 @@ func (e *endpoint) HandlePacket(r *stack.Route, pkt *stack.PacketBuffer) {
|
||||
return
|
||||
}
|
||||
|
||||
var ready bool
|
||||
// Note that pkt doesn't have its transport header set after reassembly,
|
||||
// and won't until DeliverNetworkPacket sets it.
|
||||
pkt.Data, ready, err = e.protocol.fragmentation.Process(
|
||||
data, proto, ready, err := e.protocol.fragmentation.Process(
|
||||
// IPv6 ignores the Protocol field since the ID only needs to be unique
|
||||
// across source-destination pairs, as per RFC 8200 section 4.5.
|
||||
fragmentation.FragmentID{
|
||||
@@ -335,6 +334,7 @@ func (e *endpoint) HandlePacket(r *stack.Route, pkt *stack.PacketBuffer) {
|
||||
start,
|
||||
last,
|
||||
extHdr.More(),
|
||||
uint8(rawPayload.Identifier),
|
||||
rawPayload.Buf,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -342,12 +342,14 @@ func (e *endpoint) HandlePacket(r *stack.Route, pkt *stack.PacketBuffer) {
|
||||
r.Stats().IP.MalformedFragmentsReceived.Increment()
|
||||
return
|
||||
}
|
||||
pkt.Data = data
|
||||
|
||||
if ready {
|
||||
// We create a new iterator with the reassembled packet because we could
|
||||
// have more extension headers in the reassembled payload, as per RFC
|
||||
// 8200 section 4.5.
|
||||
it = header.MakeIPv6PayloadIterator(rawPayload.Identifier, pkt.Data)
|
||||
// 8200 section 4.5. We also use the NextHeader value from the first
|
||||
// fragment.
|
||||
it = header.MakeIPv6PayloadIterator(header.IPv6ExtensionHeaderIdentifier(proto), pkt.Data)
|
||||
}
|
||||
|
||||
case header.IPv6DestinationOptionsExtHdr:
|
||||
|
||||
@@ -865,6 +865,46 @@ func TestReceiveIPv6Fragments(t *testing.T) {
|
||||
},
|
||||
expectedPayloads: [][]byte{udpPayload1Addr1ToAddr2},
|
||||
},
|
||||
{
|
||||
name: "Two fragments with different Next Header values",
|
||||
fragments: []fragmentData{
|
||||
{
|
||||
srcAddr: addr1,
|
||||
dstAddr: addr2,
|
||||
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}),
|
||||
|
||||
ipv6Payload1Addr1ToAddr2[:64],
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
srcAddr: addr1,
|
||||
dstAddr: addr2,
|
||||
nextHdr: fragmentExtHdrID,
|
||||
data: buffer.NewVectorisedView(
|
||||
fragmentExtHdrLen+len(ipv6Payload1Addr1ToAddr2)-64,
|
||||
[]buffer.View{
|
||||
// Fragment extension header.
|
||||
//
|
||||
// Fragment offset = 8, More = false, ID = 1
|
||||
// NextHeader value is different than the one in the first fragment, so
|
||||
// this NextHeader should be ignored.
|
||||
buffer.View([]byte{uint8(header.IPv6NoNextHeaderIdentifier), 0, 0, 64, 0, 0, 0, 1}),
|
||||
|
||||
ipv6Payload1Addr1ToAddr2[64:],
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
expectedPayloads: [][]byte{udpPayload1Addr1ToAddr2},
|
||||
},
|
||||
{
|
||||
name: "Two fragments with last fragment size not a multiple of fragment block size",
|
||||
fragments: []fragmentData{
|
||||
|
||||
Reference in New Issue
Block a user