Send ICMP error message if IP fragment reassembly fails

Fixes #4427, #4428

PiperOrigin-RevId: 338805047
This commit is contained in:
Toshi Kikuchi
2020-10-24 00:19:15 -07:00
committed by gVisor bot
parent d1e4813e01
commit 0a035a1011
14 changed files with 920 additions and 66 deletions
+32
View File
@@ -953,6 +953,38 @@ func ICMPv6Code(want header.ICMPv6Code) TransportChecker {
}
}
// ICMPv6TypeSpecific creates a checker that checks the ICMPv6 TypeSpecific
// field.
func ICMPv6TypeSpecific(want uint32) TransportChecker {
return func(t *testing.T, h header.Transport) {
t.Helper()
icmpv6, ok := h.(header.ICMPv6)
if !ok {
t.Fatalf("unexpected transport header passed to checker, got = %T, want = header.ICMPv6", h)
}
if got := icmpv6.TypeSpecific(); got != want {
t.Fatalf("unexpected ICMP TypeSpecific, got = %d, want = %d", got, want)
}
}
}
// ICMPv6Payload creates a checker that checks the payload in an ICMPv6 packet.
func ICMPv6Payload(want []byte) TransportChecker {
return func(t *testing.T, h header.Transport) {
t.Helper()
icmpv6, ok := h.(header.ICMPv6)
if !ok {
t.Fatalf("unexpected transport header passed to checker, got = %T, want = header.ICMPv6", h)
}
payload := icmpv6.Payload()
if diff := cmp.Diff(want, payload); diff != "" {
t.Errorf("ICMP payload mismatch (-want +got):\n%s", diff)
}
}
}
// NDP creates a checker that checks that the packet contains a valid NDP
// message for type of ty, with potentially additional checks specified by
// checkers.
+2 -1
View File
@@ -99,7 +99,8 @@ const (
// ICMP codes for ICMPv4 Time Exceeded messages as defined in RFC 792.
const (
ICMPv4TTLExceeded ICMPv4Code = 0
ICMPv4TTLExceeded ICMPv4Code = 0
ICMPv4ReassemblyTimeout ICMPv4Code = 1
)
// ICMP codes for ICMPv4 Destination Unreachable messages as defined in RFC 792.
@@ -136,8 +136,16 @@ func NewFragmentation(blockSize uint16, highMemoryLimit, lowMemoryLimit int, rea
// 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.
//
// releaseCB is a callback that will run when the fragment reassembly of a
// packet is complete or cancelled. releaseCB take a a boolean argument which is
// true iff the reassembly is cancelled due to timeout. releaseCB should be
// passed only with the first fragment of a packet. If more than one releaseCB
// are passed for the same packet, only the first releaseCB will be saved for
// the packet and the succeeding ones will be dropped by running them
// immediately with a false argument.
func (f *Fragmentation) Process(
id FragmentID, first, last uint16, more bool, proto uint8, vv buffer.VectorisedView) (
id FragmentID, first, last uint16, more bool, proto uint8, vv buffer.VectorisedView, releaseCB func(bool)) (
buffer.VectorisedView, uint8, bool, error) {
if first > last {
return buffer.VectorisedView{}, 0, false, fmt.Errorf("first=%d is greater than last=%d: %w", first, last, ErrInvalidArgs)
@@ -171,6 +179,12 @@ func (f *Fragmentation) Process(
f.releaseReassemblersLocked()
}
}
if releaseCB != nil {
if !r.setCallback(releaseCB) {
// We got a duplicate callback. Release it immediately.
releaseCB(false /* timedOut */)
}
}
f.mu.Unlock()
res, firstFragmentProto, done, consumed, err := r.process(first, last, more, proto, vv)
@@ -178,14 +192,14 @@ func (f *Fragmentation) Process(
// We probably got an invalid sequence of fragments. Just
// discard the reassembler and move on.
f.mu.Lock()
f.release(r)
f.release(r, false /* timedOut */)
f.mu.Unlock()
return buffer.VectorisedView{}, 0, false, fmt.Errorf("fragmentation processing error: %w", err)
}
f.mu.Lock()
f.size += consumed
if done {
f.release(r)
f.release(r, false /* timedOut */)
}
// Evict reassemblers if we are consuming more memory than highLimit until
// we reach lowLimit.
@@ -195,14 +209,14 @@ func (f *Fragmentation) Process(
if tail == nil {
break
}
f.release(tail)
f.release(tail, false /* timedOut */)
}
}
f.mu.Unlock()
return res, firstFragmentProto, done, nil
}
func (f *Fragmentation) release(r *reassembler) {
func (f *Fragmentation) release(r *reassembler, timedOut bool) {
// Before releasing a fragment we need to check if r is already marked as done.
// Otherwise, we would delete it twice.
if r.checkDoneOrMark() {
@@ -216,6 +230,8 @@ func (f *Fragmentation) release(r *reassembler) {
log.Printf("memory counter < 0 (%d), this is an accounting bug that requires investigation", f.size)
f.size = 0
}
r.release(timedOut) // releaseCB may run.
}
// releaseReassemblersLocked releases already-expired reassemblers, then
@@ -238,7 +254,7 @@ func (f *Fragmentation) releaseReassemblersLocked() {
break
}
// If the oldest reassembler has already expired, release it.
f.release(r)
f.release(r, true /* timedOut*/)
}
}
@@ -105,7 +105,7 @@ func TestFragmentationProcess(t *testing.T) {
f := NewFragmentation(minBlockSize, 1024, 512, reassembleTimeout, &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)
vv, proto, done, err := f.Process(in.id, in.first, in.last, in.more, in.proto, in.vv, nil)
if err != nil {
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)
@@ -240,7 +240,7 @@ func TestReassemblingTimeout(t *testing.T) {
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))
_, _, done, err := f.Process(FragmentID{}, frag.first, frag.last, frag.more, protocol, vv(len(frag.data), frag.data), nil)
if err != nil {
t.Fatalf("%s: f.Process failed: %s", event.name, err)
}
@@ -259,15 +259,15 @@ func TestReassemblingTimeout(t *testing.T) {
func TestMemoryLimits(t *testing.T) {
f := NewFragmentation(minBlockSize, 3, 1, reassembleTimeout, &faketime.NullClock{})
// Send first fragment with id = 0.
f.Process(FragmentID{ID: 0}, 0, 0, true, 0xFF, vv(1, "0"))
f.Process(FragmentID{ID: 0}, 0, 0, true, 0xFF, vv(1, "0"), nil)
// Send first fragment with id = 1.
f.Process(FragmentID{ID: 1}, 0, 0, true, 0xFF, vv(1, "1"))
f.Process(FragmentID{ID: 1}, 0, 0, true, 0xFF, vv(1, "1"), nil)
// Send first fragment with id = 2.
f.Process(FragmentID{ID: 2}, 0, 0, true, 0xFF, vv(1, "2"))
f.Process(FragmentID{ID: 2}, 0, 0, true, 0xFF, vv(1, "2"), nil)
// 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, 0xFF, vv(1, "3"))
f.Process(FragmentID{ID: 3}, 0, 0, true, 0xFF, vv(1, "3"), nil)
if _, ok := f.reassemblers[FragmentID{ID: 0}]; ok {
t.Errorf("Memory limits are not respected: id=0 has not been evicted.")
@@ -283,9 +283,9 @@ func TestMemoryLimits(t *testing.T) {
func TestMemoryLimitsIgnoresDuplicates(t *testing.T) {
f := NewFragmentation(minBlockSize, 1, 0, reassembleTimeout, &faketime.NullClock{})
// Send first fragment with id = 0.
f.Process(FragmentID{}, 0, 0, true, 0xFF, vv(1, "0"))
f.Process(FragmentID{}, 0, 0, true, 0xFF, vv(1, "0"), nil)
// Send the same packet again.
f.Process(FragmentID{}, 0, 0, true, 0xFF, vv(1, "0"))
f.Process(FragmentID{}, 0, 0, true, 0xFF, vv(1, "0"), nil)
got := f.size
want := 1
@@ -377,7 +377,7 @@ func TestErrors(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
f := NewFragmentation(test.blockSize, HighFragThreshold, LowFragThreshold, reassembleTimeout, &faketime.NullClock{})
_, _, done, err := f.Process(FragmentID{}, test.first, test.last, test.more, 0, vv(len(test.data), test.data))
_, _, done, err := f.Process(FragmentID{}, test.first, test.last, test.more, 0, vv(len(test.data), test.data), nil)
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)
}
@@ -497,3 +497,89 @@ func TestPacketFragmenter(t *testing.T) {
})
}
}
func TestReleaseCallback(t *testing.T) {
const (
proto = 99
)
var result int
var callbackReasonIsTimeout bool
cb1 := func(timedOut bool) { result = 1; callbackReasonIsTimeout = timedOut }
cb2 := func(timedOut bool) { result = 2; callbackReasonIsTimeout = timedOut }
tests := []struct {
name string
callbacks []func(bool)
timeout bool
wantResult int
wantCallbackReasonIsTimeout bool
}{
{
name: "callback runs on release",
callbacks: []func(bool){cb1},
timeout: false,
wantResult: 1,
wantCallbackReasonIsTimeout: false,
},
{
name: "first callback is nil",
callbacks: []func(bool){nil, cb2},
timeout: false,
wantResult: 2,
wantCallbackReasonIsTimeout: false,
},
{
name: "two callbacks - first one is set",
callbacks: []func(bool){cb1, cb2},
timeout: false,
wantResult: 1,
wantCallbackReasonIsTimeout: false,
},
{
name: "callback runs on timeout",
callbacks: []func(bool){cb1},
timeout: true,
wantResult: 1,
wantCallbackReasonIsTimeout: true,
},
{
name: "no callbacks",
callbacks: []func(bool){nil},
timeout: false,
wantResult: 0,
wantCallbackReasonIsTimeout: false,
},
}
id := FragmentID{ID: 0}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result = 0
callbackReasonIsTimeout = false
f := NewFragmentation(minBlockSize, HighFragThreshold, LowFragThreshold, reassembleTimeout, &faketime.NullClock{})
for i, cb := range test.callbacks {
_, _, _, err := f.Process(id, uint16(i), uint16(i), true, proto, vv(1, "0"), cb)
if err != nil {
t.Errorf("f.Process error = %s", err)
}
}
r, ok := f.reassemblers[id]
if !ok {
t.Fatalf("Reassemberr not found")
}
f.release(r, test.timeout)
if result != test.wantResult {
t.Errorf("got result = %d, want = %d", result, test.wantResult)
}
if callbackReasonIsTimeout != test.wantCallbackReasonIsTimeout {
t.Errorf("got callbackReasonIsTimeout = %t, want = %t", callbackReasonIsTimeout, test.wantCallbackReasonIsTimeout)
}
})
}
}
@@ -41,6 +41,7 @@ type reassembler struct {
heap fragHeap
done bool
creationTime int64
callback func(bool)
}
func newReassembler(id FragmentID, clock tcpip.Clock) *reassembler {
@@ -123,3 +124,24 @@ func (r *reassembler) checkDoneOrMark() bool {
r.mu.Unlock()
return prev
}
func (r *reassembler) setCallback(c func(bool)) bool {
r.mu.Lock()
defer r.mu.Unlock()
if r.callback != nil {
return false
}
r.callback = c
return true
}
func (r *reassembler) release(timedOut bool) {
r.mu.Lock()
callback := r.callback
r.callback = nil
r.mu.Unlock()
if callback != nil {
callback(timedOut)
}
}
@@ -105,3 +105,26 @@ func TestUpdateHoles(t *testing.T) {
}
}
}
func TestSetCallback(t *testing.T) {
result := 0
reasonTimeout := false
cb1 := func(timedOut bool) { result = 1; reasonTimeout = timedOut }
cb2 := func(timedOut bool) { result = 2; reasonTimeout = timedOut }
r := newReassembler(FragmentID{}, &faketime.NullClock{})
if !r.setCallback(cb1) {
t.Errorf("setCallback failed")
}
if r.setCallback(cb2) {
t.Errorf("setCallback should fail if one is already set")
}
r.release(true)
if result != 1 {
t.Errorf("got result = %d, want = 1", result)
}
if !reasonTimeout {
t.Errorf("got reasonTimeout = %t, want = true", reasonTimeout)
}
}
+1
View File
@@ -29,6 +29,7 @@ go_test(
"//pkg/tcpip",
"//pkg/tcpip/buffer",
"//pkg/tcpip/checker",
"//pkg/tcpip/faketime",
"//pkg/tcpip/header",
"//pkg/tcpip/link/channel",
"//pkg/tcpip/link/sniffer",
+16 -2
View File
@@ -237,6 +237,13 @@ type icmpReasonProtoUnreachable struct{}
func (*icmpReasonProtoUnreachable) isICMPReason() {}
// icmpReasonReassemblyTimeout is an error where insufficient fragments are
// received to complete reassembly of a packet within a configured time after
// the reception of the first-arriving fragment of that packet.
type icmpReasonReassemblyTimeout struct{}
func (*icmpReasonReassemblyTimeout) isICMPReason() {}
// returnError takes an error descriptor and generates the appropriate ICMP
// error packet for IPv4 and sends it back to the remote device that sent
// the problematic packet. It incorporates as much of that packet as
@@ -377,17 +384,24 @@ func (p *protocol) returnError(r *stack.Route, reason icmpReason, pkt *stack.Pac
icmpPkt.TransportProtocolNumber = header.ICMPv4ProtocolNumber
icmpHdr := header.ICMPv4(icmpPkt.TransportHeader().Push(header.ICMPv4MinimumSize))
var counter *tcpip.StatCounter
switch reason.(type) {
case *icmpReasonPortUnreachable:
icmpHdr.SetType(header.ICMPv4DstUnreachable)
icmpHdr.SetCode(header.ICMPv4PortUnreachable)
counter = sent.DstUnreachable
case *icmpReasonProtoUnreachable:
icmpHdr.SetType(header.ICMPv4DstUnreachable)
icmpHdr.SetCode(header.ICMPv4ProtoUnreachable)
counter = sent.DstUnreachable
case *icmpReasonReassemblyTimeout:
icmpHdr.SetType(header.ICMPv4TimeExceeded)
icmpHdr.SetCode(header.ICMPv4ReassemblyTimeout)
counter = sent.TimeExceeded
default:
panic(fmt.Sprintf("unsupported ICMP type %T", reason))
}
icmpHdr.SetType(header.ICMPv4DstUnreachable)
icmpHdr.SetChecksum(header.ICMPv4Checksum(icmpHdr, icmpPkt.Data))
counter := sent.DstUnreachable
if err := route.WritePacket(
nil, /* gso */
+25 -2
View File
@@ -38,7 +38,7 @@ const (
// Considering that it is an old recommendation, we use the same reassembly
// timeout that linux defines, which is 30 seconds:
// https://github.com/torvalds/linux/blob/47ec5303d73ea344e84f46660fff693c57641386/include/net/ip.h#L138
reassembleTimeout = 30 * time.Second
ReassembleTimeout = 30 * time.Second
// ProtocolNumber is the ipv4 protocol number.
ProtocolNumber = header.IPv4ProtocolNumber
@@ -520,6 +520,28 @@ func (e *endpoint) HandlePacket(r *stack.Route, pkt *stack.PacketBuffer) {
r.Stats().IP.MalformedFragmentsReceived.Increment()
return
}
// Set up a callback in case we need to send a Time Exceeded Message, as per
// RFC 792:
//
// If a host reassembling a fragmented datagram cannot complete the
// reassembly due to missing fragments within its time limit it discards
// the datagram, and it may send a time exceeded message.
//
// If fragment zero is not available then no time exceeded need be sent at
// all.
var releaseCB func(bool)
if start == 0 {
pkt := pkt.Clone()
r := r.Clone()
releaseCB = func(timedOut bool) {
if timedOut {
_ = e.protocol.returnError(&r, &icmpReasonReassemblyTimeout{}, pkt)
}
r.Release()
}
}
var ready bool
var err error
proto := h.Protocol()
@@ -537,6 +559,7 @@ func (e *endpoint) HandlePacket(r *stack.Route, pkt *stack.PacketBuffer) {
h.More(),
proto,
pkt.Data,
releaseCB,
)
if err != nil {
r.Stats().IP.MalformedPacketsReceived.Increment()
@@ -856,7 +879,7 @@ func NewProtocol(s *stack.Stack) stack.NetworkProtocol {
ids: ids,
hashIV: hashIV,
defaultTTL: DefaultTTL,
fragmentation: fragmentation.NewFragmentation(fragmentblockSize, fragmentation.HighFragThreshold, fragmentation.LowFragThreshold, reassembleTimeout, s.Clock()),
fragmentation: fragmentation.NewFragmentation(fragmentblockSize, fragmentation.HighFragThreshold, fragmentation.LowFragThreshold, ReassembleTimeout, s.Clock()),
}
}
+254 -2
View File
@@ -26,6 +26,7 @@ import (
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/buffer"
"gvisor.dev/gvisor/pkg/tcpip/checker"
"gvisor.dev/gvisor/pkg/tcpip/faketime"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/link/channel"
"gvisor.dev/gvisor/pkg/tcpip/link/sniffer"
@@ -788,7 +789,6 @@ func TestInvalidFragments(t *testing.T) {
autoChecksum bool // if true, the Checksum field will be overwritten.
}
// These packets have both IHL and TotalLength set to 0.
tests := []struct {
name string
fragments []fragmentData
@@ -1028,7 +1028,6 @@ func TestInvalidFragments(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
s := stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{
ipv4.NewProtocol,
@@ -1071,6 +1070,259 @@ func TestInvalidFragments(t *testing.T) {
}
}
func TestFragmentReassemblyTimeout(t *testing.T) {
const (
nicID = 1
linkAddr = tcpip.LinkAddress("\x0a\x0b\x0c\x0d\x0e\x0e")
addr1 = "\x0a\x00\x00\x01"
addr2 = "\x0a\x00\x00\x02"
tos = 0
ident = 1
ttl = 48
protocol = 99
data = "TEST_FRAGMENT_REASSEMBLY_TIMEOUT"
)
type fragmentData struct {
ipv4fields header.IPv4Fields
payload []byte
}
tests := []struct {
name string
fragments []fragmentData
expectICMP bool
}{
{
name: "first fragment only",
fragments: []fragmentData{
{
ipv4fields: header.IPv4Fields{
IHL: header.IPv4MinimumSize,
TOS: tos,
TotalLength: header.IPv4MinimumSize + 16,
ID: ident,
Flags: header.IPv4FlagMoreFragments,
FragmentOffset: 0,
TTL: ttl,
Protocol: protocol,
SrcAddr: addr1,
DstAddr: addr2,
},
payload: []byte(data)[:16],
},
},
expectICMP: true,
},
{
name: "two first fragments",
fragments: []fragmentData{
{
ipv4fields: header.IPv4Fields{
IHL: header.IPv4MinimumSize,
TOS: tos,
TotalLength: header.IPv4MinimumSize + 16,
ID: ident,
Flags: header.IPv4FlagMoreFragments,
FragmentOffset: 0,
TTL: ttl,
Protocol: protocol,
SrcAddr: addr1,
DstAddr: addr2,
},
payload: []byte(data)[:16],
},
{
ipv4fields: header.IPv4Fields{
IHL: header.IPv4MinimumSize,
TOS: tos,
TotalLength: header.IPv4MinimumSize + 16,
ID: ident,
Flags: header.IPv4FlagMoreFragments,
FragmentOffset: 0,
TTL: ttl,
Protocol: protocol,
SrcAddr: addr1,
DstAddr: addr2,
},
payload: []byte(data)[:16],
},
},
expectICMP: true,
},
{
name: "second fragment only",
fragments: []fragmentData{
{
ipv4fields: header.IPv4Fields{
IHL: header.IPv4MinimumSize,
TOS: tos,
TotalLength: uint16(header.IPv4MinimumSize + len(data) - 16),
ID: ident,
Flags: 0,
FragmentOffset: 8,
TTL: ttl,
Protocol: protocol,
SrcAddr: addr1,
DstAddr: addr2,
},
payload: []byte(data)[16:],
},
},
expectICMP: false,
},
{
name: "two fragments with a gap",
fragments: []fragmentData{
{
ipv4fields: header.IPv4Fields{
IHL: header.IPv4MinimumSize,
TOS: tos,
TotalLength: header.IPv4MinimumSize + 8,
ID: ident,
Flags: header.IPv4FlagMoreFragments,
FragmentOffset: 0,
TTL: ttl,
Protocol: protocol,
SrcAddr: addr1,
DstAddr: addr2,
},
payload: []byte(data)[:8],
},
{
ipv4fields: header.IPv4Fields{
IHL: header.IPv4MinimumSize,
TOS: tos,
TotalLength: uint16(header.IPv4MinimumSize + len(data) - 16),
ID: ident,
Flags: 0,
FragmentOffset: 16,
TTL: ttl,
Protocol: protocol,
SrcAddr: addr1,
DstAddr: addr2,
},
payload: []byte(data)[16:],
},
},
expectICMP: true,
},
{
name: "two fragments with a gap in reverse order",
fragments: []fragmentData{
{
ipv4fields: header.IPv4Fields{
IHL: header.IPv4MinimumSize,
TOS: tos,
TotalLength: uint16(header.IPv4MinimumSize + len(data) - 16),
ID: ident,
Flags: 0,
FragmentOffset: 16,
TTL: ttl,
Protocol: protocol,
SrcAddr: addr1,
DstAddr: addr2,
},
payload: []byte(data)[16:],
},
{
ipv4fields: header.IPv4Fields{
IHL: header.IPv4MinimumSize,
TOS: tos,
TotalLength: header.IPv4MinimumSize + 8,
ID: ident,
Flags: header.IPv4FlagMoreFragments,
FragmentOffset: 0,
TTL: ttl,
Protocol: protocol,
SrcAddr: addr1,
DstAddr: addr2,
},
payload: []byte(data)[:8],
},
},
expectICMP: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
clock := faketime.NewManualClock()
s := stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{
ipv4.NewProtocol,
},
Clock: clock,
})
e := channel.New(1, 1500, linkAddr)
if err := s.CreateNIC(nicID, e); err != nil {
t.Fatalf("CreateNIC(%d, _) = %s", nicID, err)
}
if err := s.AddAddress(nicID, ipv4.ProtocolNumber, addr2); err != nil {
t.Fatalf("AddAddress(%d, %d, %s) = %s", nicID, header.IPv4ProtocolNumber, addr2, err)
}
s.SetRouteTable([]tcpip.Route{{
Destination: header.IPv4EmptySubnet,
NIC: nicID,
}})
var firstFragmentSent buffer.View
for _, f := range test.fragments {
pktSize := header.IPv4MinimumSize
hdr := buffer.NewPrependable(pktSize)
ip := header.IPv4(hdr.Prepend(pktSize))
ip.Encode(&f.ipv4fields)
ip.SetChecksum(0)
ip.SetChecksum(^ip.CalculateChecksum())
vv := hdr.View().ToVectorisedView()
vv.AppendView(f.payload)
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
Data: vv,
})
if firstFragmentSent == nil && ip.FragmentOffset() == 0 {
firstFragmentSent = stack.PayloadSince(pkt.NetworkHeader())
}
e.InjectInbound(header.IPv4ProtocolNumber, pkt)
}
clock.Advance(ipv4.ReassembleTimeout)
reply, ok := e.Read()
if !test.expectICMP {
if ok {
t.Fatalf("unexpected ICMP error message received: %#v", reply)
}
return
}
if !ok {
t.Fatal("expected ICMP error message missing")
}
if firstFragmentSent == nil {
t.Fatalf("unexpected ICMP error message received: %#v", reply)
}
checker.IPv4(t, stack.PayloadSince(reply.Pkt.NetworkHeader()),
checker.SrcAddr(addr2),
checker.DstAddr(addr1),
checker.IPFullLength(uint16(header.IPv4MinimumSize+header.ICMPv4MinimumSize+firstFragmentSent.Size())),
checker.IPv4HeaderLength(header.IPv4MinimumSize),
checker.ICMPv4(
checker.ICMPv4Type(header.ICMPv4TimeExceeded),
checker.ICMPv4Code(header.ICMPv4ReassemblyTimeout),
checker.ICMPv4Checksum(),
checker.ICMPv4Payload([]byte(firstFragmentSent)),
),
)
})
}
}
// TestReceiveFragments feeds fragments in through the incoming packet path to
// test reassembly
func TestReceiveFragments(t *testing.T) {
+1
View File
@@ -36,6 +36,7 @@ go_test(
"//pkg/tcpip",
"//pkg/tcpip/buffer",
"//pkg/tcpip/checker",
"//pkg/tcpip/faketime",
"//pkg/tcpip/header",
"//pkg/tcpip/link/channel",
"//pkg/tcpip/link/sniffer",
+14 -1
View File
@@ -744,6 +744,13 @@ type icmpReasonPortUnreachable struct{}
func (*icmpReasonPortUnreachable) isICMPReason() {}
// icmpReasonReassemblyTimeout is an error where insufficient fragments are
// received to complete reassembly of a packet within a configured time after
// the reception of the first-arriving fragment of that packet.
type icmpReasonReassemblyTimeout struct{}
func (*icmpReasonReassemblyTimeout) isICMPReason() {}
// returnError takes an error descriptor and generates the appropriate ICMP
// error packet for IPv6 and sends it.
func (p *protocol) returnError(r *stack.Route, reason icmpReason, pkt *stack.PacketBuffer) *tcpip.Error {
@@ -836,7 +843,9 @@ func (p *protocol) returnError(r *stack.Route, reason icmpReason, pkt *stack.Pac
if payloadLen > available {
payloadLen = available
}
payload := buffer.NewVectorisedView(pkt.Size(), pkt.Views())
payload := network.ToVectorisedView()
payload.AppendView(transport)
payload.Append(pkt.Data)
payload.CapLength(payloadLen)
newPkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
@@ -857,6 +866,10 @@ func (p *protocol) returnError(r *stack.Route, reason icmpReason, pkt *stack.Pac
icmpHdr.SetType(header.ICMPv6DstUnreachable)
icmpHdr.SetCode(header.ICMPv6PortUnreachable)
counter = sent.DstUnreachable
case *icmpReasonReassemblyTimeout:
icmpHdr.SetType(header.ICMPv6TimeExceeded)
icmpHdr.SetCode(header.ICMPv6ReassemblyTimeout)
counter = sent.TimeExceeded
default:
panic(fmt.Sprintf("unsupported ICMP type %T", reason))
}
+49 -4
View File
@@ -41,7 +41,7 @@ const (
//
// Linux also uses 60 seconds for reassembly timeout:
// https://github.com/torvalds/linux/blob/47ec5303d73ea344e84f46660fff693c57641386/include/net/ipv6.h#L456
reassembleTimeout = 60 * time.Second
ReassembleTimeout = 60 * time.Second
// ProtocolNumber is the ipv6 protocol number.
ProtocolNumber = header.IPv6ProtocolNumber
@@ -777,6 +777,8 @@ func (e *endpoint) HandlePacket(r *stack.Route, pkt *stack.PacketBuffer) {
continue
}
fragmentFieldOffset := it.ParseOffset()
// Don't consume the iterator if we have the first fragment because we
// will use it to validate that the first fragment holds the upper layer
// header.
@@ -834,17 +836,59 @@ func (e *endpoint) HandlePacket(r *stack.Route, pkt *stack.PacketBuffer) {
return
}
// As per RFC 2460 Section 4.5:
//
// If the length of a fragment, as derived from the fragment packet's
// Payload Length field, is not a multiple of 8 octets and the M flag
// of that fragment is 1, then that fragment must be discarded and an
// ICMP Parameter Problem, Code 0, message should be sent to the source
// of the fragment, pointing to the Payload Length field of the
// fragment packet.
if extHdr.More() && fragmentPayloadLen%header.IPv6FragmentExtHdrFragmentOffsetBytesPerUnit != 0 {
r.Stats().IP.MalformedPacketsReceived.Increment()
r.Stats().IP.MalformedFragmentsReceived.Increment()
_ = e.protocol.returnError(r, &icmpReasonParameterProblem{
code: header.ICMPv6ErroneousHeader,
pointer: header.IPv6PayloadLenOffset,
}, pkt)
return
}
// The packet is a fragment, let's try to reassemble it.
start := extHdr.FragmentOffset() * header.IPv6FragmentExtHdrFragmentOffsetBytesPerUnit
// Drop the fragment if the size of the reassembled payload would exceed
// the maximum payload size.
// As per RFC 2460 Section 4.5:
//
// If the length and offset of a fragment are such that the Payload
// Length of the packet reassembled from that fragment would exceed
// 65,535 octets, then that fragment must be discarded and an ICMP
// Parameter Problem, Code 0, message should be sent to the source of
// the fragment, pointing to the Fragment Offset field of the fragment
// packet.
if int(start)+fragmentPayloadLen > header.IPv6MaximumPayloadSize {
r.Stats().IP.MalformedPacketsReceived.Increment()
r.Stats().IP.MalformedFragmentsReceived.Increment()
_ = e.protocol.returnError(r, &icmpReasonParameterProblem{
code: header.ICMPv6ErroneousHeader,
pointer: fragmentFieldOffset,
}, pkt)
return
}
// Set up a callback in case we need to send a Time Exceeded Message as
// per RFC 2460 Section 4.5.
var releaseCB func(bool)
if start == 0 {
pkt := pkt.Clone()
r := r.Clone()
releaseCB = func(timedOut bool) {
if timedOut {
_ = e.protocol.returnError(&r, &icmpReasonReassemblyTimeout{}, pkt)
}
r.Release()
}
}
// Note that pkt doesn't have its transport header set after reassembly,
// and won't until DeliverNetworkPacket sets it.
data, proto, ready, err := e.protocol.fragmentation.Process(
@@ -860,6 +904,7 @@ func (e *endpoint) HandlePacket(r *stack.Route, pkt *stack.PacketBuffer) {
extHdr.More(),
uint8(rawPayload.Identifier),
rawPayload.Buf,
releaseCB,
)
if err != nil {
r.Stats().IP.MalformedPacketsReceived.Increment()
@@ -1535,7 +1580,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, reassembleTimeout, s.Clock()),
fragmentation: fragmentation.NewFragmentation(header.IPv6FragmentExtHdrFragmentOffsetBytesPerUnit, fragmentation.HighFragThreshold, fragmentation.LowFragThreshold, ReassembleTimeout, s.Clock()),
ids: ids,
hashIV: hashIV,
+364 -39
View File
@@ -24,6 +24,7 @@ import (
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/buffer"
"gvisor.dev/gvisor/pkg/tcpip/checker"
"gvisor.dev/gvisor/pkg/tcpip/faketime"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/link/channel"
"gvisor.dev/gvisor/pkg/tcpip/network/testutil"
@@ -1912,16 +1913,19 @@ func TestReceiveIPv6Fragments(t *testing.T) {
func TestInvalidIPv6Fragments(t *testing.T) {
const (
nicID = 1
fragmentExtHdrLen = 8
addr1 = "\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"
addr2 = "\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02"
linkAddr1 = tcpip.LinkAddress("\x0a\x0b\x0c\x0d\x0e\x0e")
nicID = 1
hoplimit = 255
ident = 1
data = "TEST_INVALID_IPV6_FRAGMENTS"
)
payloadGen := func(payloadLen int) []byte {
payload := make([]byte, payloadLen)
for i := 0; i < len(payload); i++ {
payload[i] = 0x30
}
return payload
type fragmentData struct {
ipv6Fields header.IPv6Fields
ipv6FragmentFields header.IPv6FragmentFields
payload []byte
}
tests := []struct {
@@ -1929,31 +1933,64 @@ func TestInvalidIPv6Fragments(t *testing.T) {
fragments []fragmentData
wantMalformedIPPackets uint64
wantMalformedFragments uint64
expectICMP bool
expectICMPType header.ICMPv6Type
expectICMPCode header.ICMPv6Code
expectICMPTypeSpecific uint32
}{
{
name: "fragments reassembled into a payload exceeding the max IPv6 payload size",
name: "fragment size is not a multiple of 8 and the M flag is true",
fragments: []fragmentData{
{
srcAddr: addr1,
dstAddr: addr2,
nextHdr: fragmentExtHdrID,
data: buffer.NewVectorisedView(
fragmentExtHdrLen+(header.IPv6MaximumPayloadSize+1)-16,
[]buffer.View{
// Fragment extension header.
// Fragment offset = 8190, More = false, ID = 1
buffer.View([]byte{uint8(header.UDPProtocolNumber), 0,
((header.IPv6MaximumPayloadSize + 1) - 16) >> 8,
((header.IPv6MaximumPayloadSize + 1) - 16) & math.MaxUint8,
0, 0, 0, 1}),
// Payload length = 16
payloadGen(16),
},
),
ipv6Fields: header.IPv6Fields{
PayloadLength: header.IPv6FragmentHeaderSize + 9,
NextHeader: header.IPv6FragmentHeader,
HopLimit: hoplimit,
SrcAddr: addr1,
DstAddr: addr2,
},
ipv6FragmentFields: header.IPv6FragmentFields{
NextHeader: uint8(header.UDPProtocolNumber),
FragmentOffset: 0 >> 3,
M: true,
Identification: ident,
},
payload: []byte(data)[:9],
},
},
wantMalformedIPPackets: 1,
wantMalformedFragments: 1,
expectICMP: true,
expectICMPType: header.ICMPv6ParamProblem,
expectICMPCode: header.ICMPv6ErroneousHeader,
expectICMPTypeSpecific: header.IPv6PayloadLenOffset,
},
{
name: "fragments reassembled into a payload exceeding the max IPv6 payload size",
fragments: []fragmentData{
{
ipv6Fields: header.IPv6Fields{
PayloadLength: header.IPv6FragmentHeaderSize + 16,
NextHeader: header.IPv6FragmentHeader,
HopLimit: hoplimit,
SrcAddr: addr1,
DstAddr: addr2,
},
ipv6FragmentFields: header.IPv6FragmentFields{
NextHeader: uint8(header.UDPProtocolNumber),
FragmentOffset: ((header.IPv6MaximumPayloadSize + 1) - 16) >> 3,
M: false,
Identification: ident,
},
payload: []byte(data)[:16],
},
},
wantMalformedIPPackets: 1,
wantMalformedFragments: 1,
expectICMP: true,
expectICMPType: header.ICMPv6ParamProblem,
expectICMPCode: header.ICMPv6ErroneousHeader,
expectICMPTypeSpecific: header.IPv6MinimumSize + 2, /* offset for 'Fragment Offset' in the fragment header */
},
}
@@ -1964,33 +2001,40 @@ func TestInvalidIPv6Fragments(t *testing.T) {
NewProtocol,
},
})
e := channel.New(0, 1500, linkAddr1)
e := channel.New(1, 1500, linkAddr1)
if err := s.CreateNIC(nicID, e); err != nil {
t.Fatalf("CreateNIC(%d, _) = %s", nicID, err)
}
if err := s.AddAddress(nicID, ProtocolNumber, addr2); err != nil {
t.Fatalf("AddAddress(%d, %d, %s) = %s", nicID, ProtocolNumber, addr2, err)
}
s.SetRouteTable([]tcpip.Route{{
Destination: header.IPv6EmptySubnet,
NIC: nicID,
}})
var expectICMPPayload buffer.View
for _, f := range test.fragments {
hdr := buffer.NewPrependable(header.IPv6MinimumSize)
hdr := buffer.NewPrependable(header.IPv6MinimumSize + header.IPv6FragmentHeaderSize)
// Serialize IPv6 fixed header.
ip := header.IPv6(hdr.Prepend(header.IPv6MinimumSize))
ip.Encode(&header.IPv6Fields{
PayloadLength: uint16(f.data.Size()),
NextHeader: f.nextHdr,
HopLimit: 255,
SrcAddr: f.srcAddr,
DstAddr: f.dstAddr,
})
ip := header.IPv6(hdr.Prepend(header.IPv6MinimumSize + header.IPv6FragmentHeaderSize))
ip.Encode(&f.ipv6Fields)
fragHDR := header.IPv6Fragment(hdr.View()[header.IPv6MinimumSize:])
fragHDR.Encode(&f.ipv6FragmentFields)
vv := hdr.View().ToVectorisedView()
vv.Append(f.data)
vv.AppendView(f.payload)
e.InjectInbound(ProtocolNumber, stack.NewPacketBuffer(stack.PacketBufferOptions{
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
Data: vv,
}))
})
if test.expectICMP {
expectICMPPayload = stack.PayloadSince(pkt.NetworkHeader())
}
e.InjectInbound(ProtocolNumber, pkt)
}
if got, want := s.Stats().IP.MalformedPacketsReceived.Value(), test.wantMalformedIPPackets; got != want {
@@ -1999,6 +2043,287 @@ func TestInvalidIPv6Fragments(t *testing.T) {
if got, want := s.Stats().IP.MalformedFragmentsReceived.Value(), test.wantMalformedFragments; got != want {
t.Errorf("got Stats.IP.MalformedFragmentsReceived = %d, want = %d", got, want)
}
reply, ok := e.Read()
if !test.expectICMP {
if ok {
t.Fatalf("unexpected ICMP error message received: %#v", reply)
}
return
}
if !ok {
t.Fatal("expected ICMP error message missing")
}
checker.IPv6(t, stack.PayloadSince(reply.Pkt.NetworkHeader()),
checker.SrcAddr(addr2),
checker.DstAddr(addr1),
checker.IPFullLength(uint16(header.IPv6MinimumSize+header.ICMPv6MinimumSize+expectICMPPayload.Size())),
checker.ICMPv6(
checker.ICMPv6Type(test.expectICMPType),
checker.ICMPv6Code(test.expectICMPCode),
checker.ICMPv6TypeSpecific(test.expectICMPTypeSpecific),
checker.ICMPv6Payload([]byte(expectICMPPayload)),
),
)
})
}
}
func TestFragmentReassemblyTimeout(t *testing.T) {
const (
addr1 = "\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"
addr2 = "\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02"
linkAddr1 = tcpip.LinkAddress("\x0a\x0b\x0c\x0d\x0e\x0e")
nicID = 1
hoplimit = 255
ident = 1
data = "TEST_FRAGMENT_REASSEMBLY_TIMEOUT"
)
type fragmentData struct {
ipv6Fields header.IPv6Fields
ipv6FragmentFields header.IPv6FragmentFields
payload []byte
}
tests := []struct {
name string
fragments []fragmentData
expectICMP bool
}{
{
name: "first fragment only",
fragments: []fragmentData{
{
ipv6Fields: header.IPv6Fields{
PayloadLength: header.IPv6FragmentHeaderSize + 16,
NextHeader: header.IPv6FragmentHeader,
HopLimit: hoplimit,
SrcAddr: addr1,
DstAddr: addr2,
},
ipv6FragmentFields: header.IPv6FragmentFields{
NextHeader: uint8(header.UDPProtocolNumber),
FragmentOffset: 0,
M: true,
Identification: ident,
},
payload: []byte(data)[:16],
},
},
expectICMP: true,
},
{
name: "two first fragments",
fragments: []fragmentData{
{
ipv6Fields: header.IPv6Fields{
PayloadLength: header.IPv6FragmentHeaderSize + 16,
NextHeader: header.IPv6FragmentHeader,
HopLimit: hoplimit,
SrcAddr: addr1,
DstAddr: addr2,
},
ipv6FragmentFields: header.IPv6FragmentFields{
NextHeader: uint8(header.UDPProtocolNumber),
FragmentOffset: 0,
M: true,
Identification: ident,
},
payload: []byte(data)[:16],
},
{
ipv6Fields: header.IPv6Fields{
PayloadLength: header.IPv6FragmentHeaderSize + 16,
NextHeader: header.IPv6FragmentHeader,
HopLimit: hoplimit,
SrcAddr: addr1,
DstAddr: addr2,
},
ipv6FragmentFields: header.IPv6FragmentFields{
NextHeader: uint8(header.UDPProtocolNumber),
FragmentOffset: 0,
M: true,
Identification: ident,
},
payload: []byte(data)[:16],
},
},
expectICMP: true,
},
{
name: "second fragment only",
fragments: []fragmentData{
{
ipv6Fields: header.IPv6Fields{
PayloadLength: uint16(header.IPv6FragmentHeaderSize + len(data) - 16),
NextHeader: header.IPv6FragmentHeader,
HopLimit: hoplimit,
SrcAddr: addr1,
DstAddr: addr2,
},
ipv6FragmentFields: header.IPv6FragmentFields{
NextHeader: uint8(header.UDPProtocolNumber),
FragmentOffset: 8,
M: false,
Identification: ident,
},
payload: []byte(data)[16:],
},
},
expectICMP: false,
},
{
name: "two fragments with a gap",
fragments: []fragmentData{
{
ipv6Fields: header.IPv6Fields{
PayloadLength: header.IPv6FragmentHeaderSize + 16,
NextHeader: header.IPv6FragmentHeader,
HopLimit: hoplimit,
SrcAddr: addr1,
DstAddr: addr2,
},
ipv6FragmentFields: header.IPv6FragmentFields{
NextHeader: uint8(header.UDPProtocolNumber),
FragmentOffset: 0,
M: true,
Identification: ident,
},
payload: []byte(data)[:16],
},
{
ipv6Fields: header.IPv6Fields{
PayloadLength: uint16(header.IPv6FragmentHeaderSize + len(data) - 16),
NextHeader: header.IPv6FragmentHeader,
HopLimit: hoplimit,
SrcAddr: addr1,
DstAddr: addr2,
},
ipv6FragmentFields: header.IPv6FragmentFields{
NextHeader: uint8(header.UDPProtocolNumber),
FragmentOffset: 8,
M: false,
Identification: ident,
},
payload: []byte(data)[16:],
},
},
expectICMP: true,
},
{
name: "two fragments with a gap in reverse order",
fragments: []fragmentData{
{
ipv6Fields: header.IPv6Fields{
PayloadLength: uint16(header.IPv6FragmentHeaderSize + len(data) - 16),
NextHeader: header.IPv6FragmentHeader,
HopLimit: hoplimit,
SrcAddr: addr1,
DstAddr: addr2,
},
ipv6FragmentFields: header.IPv6FragmentFields{
NextHeader: uint8(header.UDPProtocolNumber),
FragmentOffset: 8,
M: false,
Identification: ident,
},
payload: []byte(data)[16:],
},
{
ipv6Fields: header.IPv6Fields{
PayloadLength: header.IPv6FragmentHeaderSize + 16,
NextHeader: header.IPv6FragmentHeader,
HopLimit: hoplimit,
SrcAddr: addr1,
DstAddr: addr2,
},
ipv6FragmentFields: header.IPv6FragmentFields{
NextHeader: uint8(header.UDPProtocolNumber),
FragmentOffset: 0,
M: true,
Identification: ident,
},
payload: []byte(data)[:16],
},
},
expectICMP: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
clock := faketime.NewManualClock()
s := stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{
NewProtocol,
},
Clock: clock,
})
e := channel.New(1, 1500, linkAddr1)
if err := s.CreateNIC(nicID, e); err != nil {
t.Fatalf("CreateNIC(%d, _) = %s", nicID, err)
}
if err := s.AddAddress(nicID, ProtocolNumber, addr2); err != nil {
t.Fatalf("AddAddress(%d, %d, %s) = %s", nicID, header.IPv6ProtocolNumber, addr2, err)
}
s.SetRouteTable([]tcpip.Route{{
Destination: header.IPv6EmptySubnet,
NIC: nicID,
}})
var firstFragmentSent buffer.View
for _, f := range test.fragments {
hdr := buffer.NewPrependable(header.IPv6MinimumSize + header.IPv6FragmentHeaderSize)
ip := header.IPv6(hdr.Prepend(header.IPv6MinimumSize + header.IPv6FragmentHeaderSize))
ip.Encode(&f.ipv6Fields)
fragHDR := header.IPv6Fragment(hdr.View()[header.IPv6MinimumSize:])
fragHDR.Encode(&f.ipv6FragmentFields)
vv := hdr.View().ToVectorisedView()
vv.AppendView(f.payload)
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
Data: vv,
})
if firstFragmentSent == nil && fragHDR.FragmentOffset() == 0 {
firstFragmentSent = stack.PayloadSince(pkt.NetworkHeader())
}
e.InjectInbound(ProtocolNumber, pkt)
}
clock.Advance(ReassembleTimeout)
reply, ok := e.Read()
if !test.expectICMP {
if ok {
t.Fatalf("unexpected ICMP error message received: %#v", reply)
}
return
}
if !ok {
t.Fatal("expected ICMP error message missing")
}
if firstFragmentSent == nil {
t.Fatalf("unexpected ICMP error message received: %#v", reply)
}
checker.IPv6(t, stack.PayloadSince(reply.Pkt.NetworkHeader()),
checker.SrcAddr(addr2),
checker.DstAddr(addr1),
checker.IPFullLength(uint16(header.IPv6MinimumSize+header.ICMPv6MinimumSize+firstFragmentSent.Size())),
checker.ICMPv6(
checker.ICMPv6Type(header.ICMPv6TimeExceeded),
checker.ICMPv6Code(header.ICMPv6ReassemblyTimeout),
checker.ICMPv6Payload([]byte(firstFragmentSent)),
),
)
})
}
}