gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/pkg/tcpip/stack/gro/gro.go (about) 1 // Copyright 2022 The gVisor Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package gro implements generic receive offload. 16 package gro 17 18 import ( 19 "bytes" 20 "fmt" 21 22 "gvisor.dev/gvisor/pkg/tcpip" 23 "gvisor.dev/gvisor/pkg/tcpip/header" 24 "gvisor.dev/gvisor/pkg/tcpip/stack" 25 ) 26 27 // TODO(b/256037250): Enable by default. 28 // TODO(b/256037250): We parse headers here. We should save those headers in 29 // PacketBuffers so they don't have to be re-parsed later. 30 // TODO(b/256037250): I still see the occasional SACK block in the zero-loss 31 // benchmark, which should not happen. 32 // TODO(b/256037250): Some dispatchers, e.g. XDP and RecvMmsg, can receive 33 // multiple packets at a time. Even if the GRO interval is 0, there is an 34 // opportunity for coalescing. 35 // TODO(b/256037250): We're doing some header parsing here, which presents the 36 // opportunity to skip it later. 37 // TODO(b/256037250): Can we pass a packet list up the stack too? 38 39 const ( 40 // groNBuckets is the number of GRO buckets. 41 groNBuckets = 8 42 43 groNBucketsMask = groNBuckets - 1 44 45 // groBucketSize is the size of each GRO bucket. 46 groBucketSize = 8 47 48 // groMaxPacketSize is the maximum size of a GRO'd packet. 49 groMaxPacketSize = 1 << 16 // 65KB. 50 ) 51 52 // A groBucket holds packets that are undergoing GRO. 53 type groBucket struct { 54 // count is the number of packets in the bucket. 55 count int 56 57 // packets is the linked list of packets. 58 packets groPacketList 59 60 // packetsPrealloc and allocIdxs are used to preallocate and reuse 61 // groPacket structs and avoid allocation. 62 packetsPrealloc [groBucketSize]groPacket 63 64 allocIdxs [groBucketSize]int 65 } 66 67 func (gb *groBucket) full() bool { 68 return gb.count == groBucketSize 69 } 70 71 // insert inserts pkt into the bucket. 72 func (gb *groBucket) insert(pkt *stack.PacketBuffer, ipHdr []byte, tcpHdr header.TCP) { 73 groPkt := &gb.packetsPrealloc[gb.allocIdxs[gb.count]] 74 *groPkt = groPacket{ 75 pkt: pkt, 76 ipHdr: ipHdr, 77 tcpHdr: tcpHdr, 78 initialLength: pkt.Data().Size(), // pkt.Data() contains network header. 79 idx: groPkt.idx, 80 } 81 gb.count++ 82 gb.packets.PushBack(groPkt) 83 } 84 85 // removeOldest removes the oldest packet from gb and returns the contained 86 // PacketBuffer. gb must not be empty. 87 func (gb *groBucket) removeOldest() *stack.PacketBuffer { 88 pkt := gb.packets.Front() 89 gb.packets.Remove(pkt) 90 gb.count-- 91 gb.allocIdxs[gb.count] = pkt.idx 92 ret := pkt.pkt 93 pkt.reset() 94 return ret 95 } 96 97 // removeOne removes a packet from gb. It also resets pkt to its zero value. 98 func (gb *groBucket) removeOne(pkt *groPacket) { 99 gb.packets.Remove(pkt) 100 gb.count-- 101 gb.allocIdxs[gb.count] = pkt.idx 102 pkt.reset() 103 } 104 105 // findGROPacket4 returns the groPkt that matches ipHdr and tcpHdr, or nil if 106 // none exists. It also returns whether the groPkt should be flushed based on 107 // differences between the two headers. 108 func (gb *groBucket) findGROPacket4(pkt *stack.PacketBuffer, ipHdr header.IPv4, tcpHdr header.TCP) (*groPacket, bool) { 109 for groPkt := gb.packets.Front(); groPkt != nil; groPkt = groPkt.Next() { 110 // Do the addresses match? 111 groIPHdr := header.IPv4(groPkt.ipHdr) 112 if ipHdr.SourceAddress() != groIPHdr.SourceAddress() || ipHdr.DestinationAddress() != groIPHdr.DestinationAddress() { 113 continue 114 } 115 116 // Do the ports match? 117 if tcpHdr.SourcePort() != groPkt.tcpHdr.SourcePort() || tcpHdr.DestinationPort() != groPkt.tcpHdr.DestinationPort() { 118 continue 119 } 120 121 // We've found a packet of the same flow. 122 123 // IP checks. 124 TOS, _ := ipHdr.TOS() 125 groTOS, _ := groIPHdr.TOS() 126 if ipHdr.TTL() != groIPHdr.TTL() || TOS != groTOS { 127 return groPkt, true 128 } 129 130 // TCP checks. 131 if shouldFlushTCP(groPkt, tcpHdr) { 132 return groPkt, true 133 } 134 135 // There's an upper limit on coalesced packet size. 136 if pkt.Data().Size()-header.IPv4MinimumSize-int(tcpHdr.DataOffset())+groPkt.pkt.Data().Size() >= groMaxPacketSize { 137 return groPkt, true 138 } 139 140 return groPkt, false 141 } 142 143 return nil, false 144 } 145 146 // findGROPacket6 returns the groPkt that matches ipHdr and tcpHdr, or nil if 147 // none exists. It also returns whether the groPkt should be flushed based on 148 // differences between the two headers. 149 func (gb *groBucket) findGROPacket6(pkt *stack.PacketBuffer, ipHdr header.IPv6, tcpHdr header.TCP) (*groPacket, bool) { 150 for groPkt := gb.packets.Front(); groPkt != nil; groPkt = groPkt.Next() { 151 // Do the addresses match? 152 groIPHdr := header.IPv6(groPkt.ipHdr) 153 if ipHdr.SourceAddress() != groIPHdr.SourceAddress() || ipHdr.DestinationAddress() != groIPHdr.DestinationAddress() { 154 continue 155 } 156 157 // Need to check that headers are the same except: 158 // - Traffic class, a difference of which causes a flush. 159 // - Hop limit, a difference of which causes a flush. 160 // - Length, which is checked later. 161 // - Version, which is checked by an earlier call to IsValid(). 162 trafficClass, flowLabel := ipHdr.TOS() 163 groTrafficClass, groFlowLabel := groIPHdr.TOS() 164 if flowLabel != groFlowLabel || ipHdr.NextHeader() != groIPHdr.NextHeader() { 165 continue 166 } 167 // Unlike IPv4, IPv6 packets with extension headers can be coalesced. 168 if !bytes.Equal(ipHdr[header.IPv6MinimumSize:], groIPHdr[header.IPv6MinimumSize:]) { 169 continue 170 } 171 172 // Do the ports match? 173 if tcpHdr.SourcePort() != groPkt.tcpHdr.SourcePort() || tcpHdr.DestinationPort() != groPkt.tcpHdr.DestinationPort() { 174 continue 175 } 176 177 // We've found a packet of the same flow. 178 179 // TCP checks. 180 if shouldFlushTCP(groPkt, tcpHdr) { 181 return groPkt, true 182 } 183 184 // Do the traffic class and hop limit match? 185 if trafficClass != groTrafficClass || ipHdr.HopLimit() != groIPHdr.HopLimit() { 186 return groPkt, true 187 } 188 189 // This limit is artificial for IPv6 -- we could allow even 190 // larger packets via jumbograms. 191 if pkt.Data().Size()-len(ipHdr)-int(tcpHdr.DataOffset())+groPkt.pkt.Data().Size() >= groMaxPacketSize { 192 return groPkt, true 193 } 194 195 return groPkt, false 196 } 197 198 return nil, false 199 } 200 201 func (gb *groBucket) found(gd *GRO, groPkt *groPacket, flushGROPkt bool, pkt *stack.PacketBuffer, ipHdr []byte, tcpHdr header.TCP, updateIPHdr func([]byte, int)) { 202 // Flush groPkt or merge the packets. 203 pktSize := pkt.Data().Size() 204 flags := tcpHdr.Flags() 205 dataOff := tcpHdr.DataOffset() 206 tcpPayloadSize := pkt.Data().Size() - len(ipHdr) - int(dataOff) 207 if flushGROPkt { 208 // Flush the existing GRO packet. 209 pkt := groPkt.pkt 210 gb.removeOne(groPkt) 211 gd.handlePacket(pkt) 212 pkt.DecRef() 213 groPkt = nil 214 } else if groPkt != nil { 215 // Merge pkt in to GRO packet. 216 pkt.Data().TrimFront(len(ipHdr) + int(dataOff)) 217 groPkt.pkt.Data().Merge(pkt.Data()) 218 // Update the IP total length. 219 updateIPHdr(groPkt.ipHdr, tcpPayloadSize) 220 // Add flags from the packet to the GRO packet. 221 groPkt.tcpHdr.SetFlags(uint8(groPkt.tcpHdr.Flags() | (flags & (header.TCPFlagFin | header.TCPFlagPsh)))) 222 223 pkt = nil 224 } 225 226 // Flush if the packet isn't the same size as the previous packets or 227 // if certain flags are set. The reason for checking size equality is: 228 // - If the packet is smaller than the others, this is likely the end 229 // of some message. Peers will send MSS-sized packets until they have 230 // insufficient data to do so. 231 // - If the packet is larger than the others, this packet is either 232 // malformed, a local GSO packet, or has already been handled by host 233 // GRO. 234 flush := header.TCPFlags(flags)&(header.TCPFlagUrg|header.TCPFlagPsh|header.TCPFlagRst|header.TCPFlagSyn|header.TCPFlagFin) != 0 235 flush = flush || tcpPayloadSize == 0 236 if groPkt != nil { 237 flush = flush || pktSize != groPkt.initialLength 238 } 239 240 switch { 241 case flush && groPkt != nil: 242 // A merge occurred and we need to flush groPkt. 243 pkt := groPkt.pkt 244 gb.removeOne(groPkt) 245 gd.handlePacket(pkt) 246 pkt.DecRef() 247 case flush && groPkt == nil: 248 // No merge occurred and the incoming packet needs to be flushed. 249 gd.handlePacket(pkt) 250 case !flush && groPkt == nil: 251 // New flow and we don't need to flush. Insert pkt into GRO. 252 if gb.full() { 253 // Head is always the oldest packet 254 toFlush := gb.removeOldest() 255 gb.insert(pkt.IncRef(), ipHdr, tcpHdr) 256 gd.handlePacket(toFlush) 257 toFlush.DecRef() 258 } else { 259 gb.insert(pkt.IncRef(), ipHdr, tcpHdr) 260 } 261 default: 262 // A merge occurred and we don't need to flush anything. 263 } 264 } 265 266 // A groPacket is packet undergoing GRO. It may be several packets coalesced 267 // together. 268 type groPacket struct { 269 // groPacketEntry is an intrusive list. 270 groPacketEntry 271 272 // pkt is the coalesced packet. 273 pkt *stack.PacketBuffer 274 275 // ipHdr is the IP (v4 or v6) header for the coalesced packet. 276 ipHdr []byte 277 278 // tcpHdr is the TCP header for the coalesced packet. 279 tcpHdr header.TCP 280 281 // initialLength is the length of the first packet in the flow. It is 282 // used as a best-effort guess at MSS: senders will send MSS-sized 283 // packets until they run out of data, so we coalesce as long as 284 // packets are the same size. 285 initialLength int 286 287 // idx is the groPacket's index in its bucket packetsPrealloc. It is 288 // immutable. 289 idx int 290 } 291 292 // reset resets all mutable fields of the groPacket. 293 func (pk *groPacket) reset() { 294 *pk = groPacket{ 295 idx: pk.idx, 296 } 297 } 298 299 // payloadSize is the payload size of the coalesced packet, which does not 300 // include the network or transport headers. 301 func (pk *groPacket) payloadSize() int { 302 return pk.pkt.Data().Size() - len(pk.ipHdr) - int(pk.tcpHdr.DataOffset()) 303 } 304 305 // GRO coalesces incoming packets to increase throughput. 306 type GRO struct { 307 enabled bool 308 buckets [groNBuckets]groBucket 309 310 Dispatcher stack.NetworkDispatcher 311 } 312 313 // Init initializes GRO. 314 func (gd *GRO) Init(enabled bool) { 315 gd.enabled = enabled 316 for i := range gd.buckets { 317 bucket := &gd.buckets[i] 318 for j := range bucket.packetsPrealloc { 319 bucket.allocIdxs[j] = j 320 bucket.packetsPrealloc[j].idx = j 321 } 322 } 323 } 324 325 // Enqueue the packet in GRO. This does not flush packets; Flush() must be 326 // called explicitly for that. 327 // 328 // pkt.NetworkProtocolNumber and pkt.RXChecksumValidated must be set. 329 func (gd *GRO) Enqueue(pkt *stack.PacketBuffer) { 330 if !gd.enabled { 331 gd.handlePacket(pkt) 332 return 333 } 334 335 switch pkt.NetworkProtocolNumber { 336 case header.IPv4ProtocolNumber: 337 gd.dispatch4(pkt) 338 case header.IPv6ProtocolNumber: 339 gd.dispatch6(pkt) 340 default: 341 gd.handlePacket(pkt) 342 } 343 } 344 345 func (gd *GRO) dispatch4(pkt *stack.PacketBuffer) { 346 // Immediately get the IPv4 and TCP headers. We need a way to hash the 347 // packet into its bucket, which requires addresses and ports. Linux 348 // simply gets a hash passed by hardware, but we're not so lucky. 349 350 // We only GRO TCP packets. The check for the transport protocol number 351 // is done below so that we can PullUp both the IP and TCP headers 352 // together. 353 hdrBytes, ok := pkt.Data().PullUp(header.IPv4MinimumSize + header.TCPMinimumSize) 354 if !ok { 355 gd.handlePacket(pkt) 356 return 357 } 358 ipHdr := header.IPv4(hdrBytes) 359 360 // We don't handle fragments. That should be the vast majority of 361 // traffic, and simplifies handling. 362 if ipHdr.FragmentOffset() != 0 || ipHdr.Flags()&header.IPv4FlagMoreFragments != 0 { 363 gd.handlePacket(pkt) 364 return 365 } 366 367 // We only handle TCP packets without IP options. 368 if ipHdr.HeaderLength() != header.IPv4MinimumSize || tcpip.TransportProtocolNumber(ipHdr.Protocol()) != header.TCPProtocolNumber { 369 gd.handlePacket(pkt) 370 return 371 } 372 tcpHdr := header.TCP(hdrBytes[header.IPv4MinimumSize:]) 373 ipHdr = ipHdr[:header.IPv4MinimumSize] 374 dataOff := tcpHdr.DataOffset() 375 if dataOff < header.TCPMinimumSize { 376 // Malformed packet: will be handled further up the stack. 377 gd.handlePacket(pkt) 378 return 379 } 380 hdrBytes, ok = pkt.Data().PullUp(header.IPv4MinimumSize + int(dataOff)) 381 if !ok { 382 // Malformed packet: will be handled further up the stack. 383 gd.handlePacket(pkt) 384 return 385 } 386 387 tcpHdr = header.TCP(hdrBytes[header.IPv4MinimumSize:]) 388 389 // If either checksum is bad, flush the packet. Since we don't know 390 // what bits were flipped, we can't identify this packet with a flow. 391 if !pkt.RXChecksumValidated { 392 if !ipHdr.IsValid(pkt.Data().Size()) || !ipHdr.IsChecksumValid() { 393 gd.handlePacket(pkt) 394 return 395 } 396 payloadChecksum := pkt.Data().ChecksumAtOffset(header.IPv4MinimumSize + int(dataOff)) 397 tcpPayloadSize := pkt.Data().Size() - header.IPv4MinimumSize - int(dataOff) 398 if !tcpHdr.IsChecksumValid(ipHdr.SourceAddress(), ipHdr.DestinationAddress(), payloadChecksum, uint16(tcpPayloadSize)) { 399 gd.handlePacket(pkt) 400 return 401 } 402 // We've validated the checksum, no reason for others to do it 403 // again. 404 pkt.RXChecksumValidated = true 405 } 406 407 // Now we can get the bucket for the packet. 408 bucket := &gd.buckets[gd.bucketForPacket4(ipHdr, tcpHdr)&groNBucketsMask] 409 groPkt, flushGROPkt := bucket.findGROPacket4(pkt, ipHdr, tcpHdr) 410 bucket.found(gd, groPkt, flushGROPkt, pkt, ipHdr, tcpHdr, updateIPv4Hdr) 411 } 412 413 func (gd *GRO) dispatch6(pkt *stack.PacketBuffer) { 414 // Immediately get the IPv6 and TCP headers. We need a way to hash the 415 // packet into its bucket, which requires addresses and ports. Linux 416 // simply gets a hash passed by hardware, but we're not so lucky. 417 418 hdrBytes, ok := pkt.Data().PullUp(header.IPv6MinimumSize) 419 if !ok { 420 gd.handlePacket(pkt) 421 return 422 } 423 ipHdr := header.IPv6(hdrBytes) 424 425 // Getting the IP header (+ extension headers) size is a bit of a pain 426 // on IPv6. 427 transProto := tcpip.TransportProtocolNumber(ipHdr.NextHeader()) 428 buf := pkt.Data().ToBuffer() 429 buf.TrimFront(header.IPv6MinimumSize) 430 it := header.MakeIPv6PayloadIterator(header.IPv6ExtensionHeaderIdentifier(transProto), buf) 431 ipHdrSize := int(header.IPv6MinimumSize) 432 for { 433 transProto = tcpip.TransportProtocolNumber(it.NextHeaderIdentifier()) 434 extHdr, done, err := it.Next() 435 if err != nil { 436 gd.handlePacket(pkt) 437 return 438 } 439 if done { 440 break 441 } 442 switch extHdr.(type) { 443 // We can GRO these, so just skip over them. 444 case header.IPv6HopByHopOptionsExtHdr: 445 case header.IPv6RoutingExtHdr: 446 case header.IPv6DestinationOptionsExtHdr: 447 default: 448 // This is either a TCP header or something we can't handle. 449 ipHdrSize = int(it.HeaderOffset()) 450 done = true 451 } 452 extHdr.Release() 453 if done { 454 break 455 } 456 } 457 458 hdrBytes, ok = pkt.Data().PullUp(ipHdrSize + header.TCPMinimumSize) 459 if !ok { 460 gd.handlePacket(pkt) 461 return 462 } 463 ipHdr = header.IPv6(hdrBytes[:ipHdrSize]) 464 465 // We only handle TCP packets. 466 if transProto != header.TCPProtocolNumber { 467 gd.handlePacket(pkt) 468 return 469 } 470 tcpHdr := header.TCP(hdrBytes[ipHdrSize:]) 471 dataOff := tcpHdr.DataOffset() 472 if dataOff < header.TCPMinimumSize { 473 // Malformed packet: will be handled further up the stack. 474 gd.handlePacket(pkt) 475 return 476 } 477 478 hdrBytes, ok = pkt.Data().PullUp(ipHdrSize + int(dataOff)) 479 if !ok { 480 // Malformed packet: will be handled further up the stack. 481 gd.handlePacket(pkt) 482 return 483 } 484 tcpHdr = header.TCP(hdrBytes[ipHdrSize:]) 485 486 // If either checksum is bad, flush the packet. Since we don't know 487 // what bits were flipped, we can't identify this packet with a flow. 488 if !pkt.RXChecksumValidated { 489 if !ipHdr.IsValid(pkt.Data().Size()) { 490 gd.handlePacket(pkt) 491 return 492 } 493 payloadChecksum := pkt.Data().ChecksumAtOffset(ipHdrSize + int(dataOff)) 494 tcpPayloadSize := pkt.Data().Size() - ipHdrSize - int(dataOff) 495 if !tcpHdr.IsChecksumValid(ipHdr.SourceAddress(), ipHdr.DestinationAddress(), payloadChecksum, uint16(tcpPayloadSize)) { 496 gd.handlePacket(pkt) 497 return 498 } 499 // We've validated the checksum, no reason for others to do it 500 // again. 501 pkt.RXChecksumValidated = true 502 } 503 504 // Now we can get the bucket for the packet. 505 bucket := &gd.buckets[gd.bucketForPacket6(ipHdr, tcpHdr)&groNBucketsMask] 506 groPkt, flushGROPkt := bucket.findGROPacket6(pkt, ipHdr, tcpHdr) 507 bucket.found(gd, groPkt, flushGROPkt, pkt, ipHdr, tcpHdr, updateIPv6Hdr) 508 } 509 510 func (gd *GRO) bucketForPacket4(ipHdr header.IPv4, tcpHdr header.TCP) int { 511 // TODO(b/256037250): Use jenkins or checksum. Write a test to print 512 // distribution. 513 var sum int 514 srcAddr := ipHdr.SourceAddress() 515 for _, val := range srcAddr.AsSlice() { 516 sum += int(val) 517 } 518 dstAddr := ipHdr.DestinationAddress() 519 for _, val := range dstAddr.AsSlice() { 520 sum += int(val) 521 } 522 sum += int(tcpHdr.SourcePort()) 523 sum += int(tcpHdr.DestinationPort()) 524 return sum 525 } 526 527 func (gd *GRO) bucketForPacket6(ipHdr header.IPv6, tcpHdr header.TCP) int { 528 // TODO(b/256037250): Use jenkins or checksum. Write a test to print 529 // distribution. 530 var sum int 531 srcAddr := ipHdr.SourceAddress() 532 for _, val := range srcAddr.AsSlice() { 533 sum += int(val) 534 } 535 dstAddr := ipHdr.DestinationAddress() 536 for _, val := range dstAddr.AsSlice() { 537 sum += int(val) 538 } 539 sum += int(tcpHdr.SourcePort()) 540 sum += int(tcpHdr.DestinationPort()) 541 return sum 542 } 543 544 // Flush sends all packets up the stack. 545 func (gd *GRO) Flush() { 546 for i := range gd.buckets { 547 for groPkt := gd.buckets[i].packets.Front(); groPkt != nil; groPkt = groPkt.Next() { 548 pkt := groPkt.pkt 549 gd.buckets[i].removeOne(groPkt) 550 gd.handlePacket(pkt) 551 pkt.DecRef() 552 } 553 } 554 } 555 556 func (gd *GRO) handlePacket(pkt *stack.PacketBuffer) { 557 gd.Dispatcher.DeliverNetworkPacket(pkt.NetworkProtocolNumber, pkt) 558 } 559 560 // String implements fmt.Stringer. 561 func (gd *GRO) String() string { 562 ret := "GRO state: \n" 563 for i := range gd.buckets { 564 bucket := &gd.buckets[i] 565 ret += fmt.Sprintf("bucket %d: %d packets: ", i, bucket.count) 566 for groPkt := bucket.packets.Front(); groPkt != nil; groPkt = groPkt.Next() { 567 ret += fmt.Sprintf("%d, ", groPkt.pkt.Data().Size()) 568 } 569 ret += "\n" 570 } 571 return ret 572 } 573 574 // shouldFlushTCP returns whether the TCP headers indicate that groPkt should 575 // be flushed 576 func shouldFlushTCP(groPkt *groPacket, tcpHdr header.TCP) bool { 577 flags := tcpHdr.Flags() 578 groPktFlags := groPkt.tcpHdr.Flags() 579 dataOff := tcpHdr.DataOffset() 580 if flags&header.TCPFlagCwr != 0 || // Is congestion control occurring? 581 (flags^groPktFlags)&^(header.TCPFlagCwr|header.TCPFlagFin|header.TCPFlagPsh) != 0 || // Do the flags differ besides CRW, FIN, and PSH? 582 tcpHdr.AckNumber() != groPkt.tcpHdr.AckNumber() || // Do the ACKs match? 583 dataOff != groPkt.tcpHdr.DataOffset() || // Are the TCP headers the same length? 584 groPkt.tcpHdr.SequenceNumber()+uint32(groPkt.payloadSize()) != tcpHdr.SequenceNumber() { // Does the incoming packet match the expected sequence number? 585 return true 586 } 587 // The options, including timestamps, must be identical. 588 return !bytes.Equal(tcpHdr[header.TCPMinimumSize:], groPkt.tcpHdr[header.TCPMinimumSize:]) 589 } 590 591 func updateIPv4Hdr(ipHdrBytes []byte, newBytes int) { 592 ipHdr := header.IPv4(ipHdrBytes) 593 ipHdr.SetTotalLength(ipHdr.TotalLength() + uint16(newBytes)) 594 } 595 596 func updateIPv6Hdr(ipHdrBytes []byte, newBytes int) { 597 ipHdr := header.IPv6(ipHdrBytes) 598 ipHdr.SetPayloadLength(ipHdr.PayloadLength() + uint16(newBytes)) 599 }