| // Copyright 2018 The gVisor Authors. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package fragmentation |
| |
| import ( |
| "bytes" |
| "math" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| "gvisor.dev/gvisor/pkg/tcpip/buffer" |
| "gvisor.dev/gvisor/pkg/tcpip/faketime" |
| "gvisor.dev/gvisor/pkg/tcpip/stack" |
| ) |
| |
| type processParams struct { |
| first uint16 |
| last uint16 |
| more bool |
| pkt *stack.PacketBuffer |
| wantDone bool |
| wantError error |
| } |
| |
| func TestReassemblerProcess(t *testing.T) { |
| const proto = 99 |
| |
| v := func(size int) buffer.View { |
| payload := buffer.NewView(size) |
| for i := 1; i < size; i++ { |
| payload[i] = uint8(i) * 3 |
| } |
| return payload |
| } |
| |
| pkt := func(sizes ...int) *stack.PacketBuffer { |
| var vv buffer.VectorisedView |
| for _, size := range sizes { |
| vv.AppendView(v(size)) |
| } |
| return stack.NewPacketBuffer(stack.PacketBufferOptions{ |
| Data: vv, |
| }) |
| } |
| |
| var tests = []struct { |
| name string |
| params []processParams |
| want []hole |
| wantPkt *stack.PacketBuffer |
| }{ |
| { |
| name: "No fragments", |
| params: nil, |
| want: []hole{{first: 0, last: math.MaxUint16, filled: false, final: true}}, |
| }, |
| { |
| name: "One fragment at beginning", |
| params: []processParams{{first: 0, last: 1, more: true, pkt: pkt(2), wantDone: false, wantError: nil}}, |
| want: []hole{ |
| {first: 0, last: 1, filled: true, final: false, pkt: pkt(2)}, |
| {first: 2, last: math.MaxUint16, filled: false, final: true}, |
| }, |
| }, |
| { |
| name: "One fragment in the middle", |
| params: []processParams{{first: 1, last: 2, more: true, pkt: pkt(2), wantDone: false, wantError: nil}}, |
| want: []hole{ |
| {first: 1, last: 2, filled: true, final: false, pkt: pkt(2)}, |
| {first: 0, last: 0, filled: false, final: false}, |
| {first: 3, last: math.MaxUint16, filled: false, final: true}, |
| }, |
| }, |
| { |
| name: "One fragment at the end", |
| params: []processParams{{first: 1, last: 2, more: false, pkt: pkt(2), wantDone: false, wantError: nil}}, |
| want: []hole{ |
| {first: 1, last: 2, filled: true, final: true, pkt: pkt(2)}, |
| {first: 0, last: 0, filled: false}, |
| }, |
| }, |
| { |
| name: "One fragment completing a packet", |
| params: []processParams{{first: 0, last: 1, more: false, pkt: pkt(2), wantDone: true, wantError: nil}}, |
| want: []hole{ |
| {first: 0, last: 1, filled: true, final: true}, |
| }, |
| wantPkt: pkt(2), |
| }, |
| { |
| name: "Two fragments completing a packet", |
| params: []processParams{ |
| {first: 0, last: 1, more: true, pkt: pkt(2), wantDone: false, wantError: nil}, |
| {first: 2, last: 3, more: false, pkt: pkt(2), wantDone: true, wantError: nil}, |
| }, |
| want: []hole{ |
| {first: 0, last: 1, filled: true, final: false}, |
| {first: 2, last: 3, filled: true, final: true}, |
| }, |
| wantPkt: pkt(2, 2), |
| }, |
| { |
| name: "Two fragments completing a packet with a duplicate", |
| params: []processParams{ |
| {first: 0, last: 1, more: true, pkt: pkt(2), wantDone: false, wantError: nil}, |
| {first: 0, last: 1, more: true, pkt: pkt(2), wantDone: false, wantError: nil}, |
| {first: 2, last: 3, more: false, pkt: pkt(2), wantDone: true, wantError: nil}, |
| }, |
| want: []hole{ |
| {first: 0, last: 1, filled: true, final: false}, |
| {first: 2, last: 3, filled: true, final: true}, |
| }, |
| wantPkt: pkt(2, 2), |
| }, |
| { |
| name: "Two fragments completing a packet with a partial duplicate", |
| params: []processParams{ |
| {first: 0, last: 3, more: true, pkt: pkt(4), wantDone: false, wantError: nil}, |
| {first: 1, last: 2, more: true, pkt: pkt(2), wantDone: false, wantError: nil}, |
| {first: 4, last: 5, more: false, pkt: pkt(2), wantDone: true, wantError: nil}, |
| }, |
| want: []hole{ |
| {first: 0, last: 3, filled: true, final: false}, |
| {first: 4, last: 5, filled: true, final: true}, |
| }, |
| wantPkt: pkt(4, 2), |
| }, |
| { |
| name: "Two overlapping fragments", |
| params: []processParams{ |
| {first: 0, last: 10, more: true, pkt: pkt(11), wantDone: false, wantError: nil}, |
| {first: 5, last: 15, more: false, pkt: pkt(11), wantDone: false, wantError: ErrFragmentOverlap}, |
| }, |
| want: []hole{ |
| {first: 0, last: 10, filled: true, final: false, pkt: pkt(11)}, |
| {first: 11, last: math.MaxUint16, filled: false, final: true}, |
| }, |
| }, |
| { |
| name: "Two final fragments with different ends", |
| params: []processParams{ |
| {first: 10, last: 14, more: false, pkt: pkt(5), wantDone: false, wantError: nil}, |
| {first: 0, last: 9, more: false, pkt: pkt(10), wantDone: false, wantError: ErrFragmentConflict}, |
| }, |
| want: []hole{ |
| {first: 10, last: 14, filled: true, final: true, pkt: pkt(5)}, |
| {first: 0, last: 9, filled: false, final: false}, |
| }, |
| }, |
| { |
| name: "Two final fragments - duplicate", |
| params: []processParams{ |
| {first: 5, last: 14, more: false, pkt: pkt(10), wantDone: false, wantError: nil}, |
| {first: 10, last: 14, more: false, pkt: pkt(5), wantDone: false, wantError: nil}, |
| }, |
| want: []hole{ |
| {first: 5, last: 14, filled: true, final: true, pkt: pkt(10)}, |
| {first: 0, last: 4, filled: false, final: false}, |
| }, |
| }, |
| { |
| name: "Two final fragments - duplicate, with different ends", |
| params: []processParams{ |
| {first: 5, last: 14, more: false, pkt: pkt(10), wantDone: false, wantError: nil}, |
| {first: 10, last: 13, more: false, pkt: pkt(4), wantDone: false, wantError: ErrFragmentConflict}, |
| }, |
| want: []hole{ |
| {first: 5, last: 14, filled: true, final: true, pkt: pkt(10)}, |
| {first: 0, last: 4, filled: false, final: false}, |
| }, |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(test.name, func(t *testing.T) { |
| r := newReassembler(FragmentID{}, &faketime.NullClock{}) |
| var resPkt *stack.PacketBuffer |
| var isDone bool |
| for _, param := range test.params { |
| pkt, _, done, _, err := r.process(param.first, param.last, param.more, proto, param.pkt) |
| if done != param.wantDone || err != param.wantError { |
| t.Errorf("got r.process(%d, %d, %t, %d, _) = (_, _, %t, _, %v), want = (%t, %v)", param.first, param.last, param.more, proto, done, err, param.wantDone, param.wantError) |
| } |
| if done { |
| resPkt = pkt |
| isDone = true |
| } |
| } |
| |
| ignorePkt := func(a, b *stack.PacketBuffer) bool { return true } |
| cmpPktData := func(a, b *stack.PacketBuffer) bool { |
| if a == nil || b == nil { |
| return a == b |
| } |
| return bytes.Equal(a.Data().AsRange().ToOwnedView(), b.Data().AsRange().ToOwnedView()) |
| } |
| |
| if isDone { |
| if diff := cmp.Diff( |
| test.want, r.holes, |
| cmp.AllowUnexported(hole{}), |
| // Do not compare pkt in hole. Data will be altered. |
| cmp.Comparer(ignorePkt), |
| ); diff != "" { |
| t.Errorf("r.holes mismatch (-want +got):\n%s", diff) |
| } |
| if diff := cmp.Diff(test.wantPkt, resPkt, cmp.Comparer(cmpPktData)); diff != "" { |
| t.Errorf("Reassembled pkt mismatch (-want +got):\n%s", diff) |
| } |
| } else { |
| if diff := cmp.Diff( |
| test.want, r.holes, |
| cmp.AllowUnexported(hole{}), |
| cmp.Comparer(cmpPktData), |
| ); diff != "" { |
| t.Errorf("r.holes mismatch (-want +got):\n%s", diff) |
| } |
| } |
| }) |
| } |
| } |