Incrementally update checksum when NAT-ing

...instead of calculating a fresh checksum to avoid re-calcalculating
a checksum on unchanged bytes.

Fixes #5340.

PiperOrigin-RevId: 381403888
This commit is contained in:
Ghanan Gowripalan
2021-06-24 22:45:17 -07:00
committed by gVisor bot
parent 49986674aa
commit 1f113b96e6
8 changed files with 465 additions and 56 deletions
+62
View File
@@ -18,6 +18,7 @@ package header
import (
"encoding/binary"
"fmt"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/buffer"
@@ -234,3 +235,64 @@ func PseudoHeaderChecksum(protocol tcpip.TransportProtocolNumber, srcAddr tcpip.
return Checksum([]byte{0, uint8(protocol)}, xsum)
}
// checksumUpdate2ByteAlignedUint16 updates a uint16 value in a calculated
// checksum.
//
// The value MUST begin at a 2-byte boundary in the original buffer.
func checksumUpdate2ByteAlignedUint16(xsum, old, new uint16) uint16 {
// As per RFC 1071 page 4,
// (4) Incremental Update
//
// ...
//
// To update the checksum, simply add the differences of the
// sixteen bit integers that have been changed. To see why this
// works, observe that every 16-bit integer has an additive inverse
// and that addition is associative. From this it follows that
// given the original value m, the new value m', and the old
// checksum C, the new checksum C' is:
//
// C' = C + (-m) + m' = C + (m' - m)
return ChecksumCombine(xsum, ChecksumCombine(new, ^old))
}
// checksumUpdate2ByteAlignedAddress updates an address in a calculated
// checksum.
//
// The addresses must have the same length and must contain an even number
// of bytes. The address MUST begin at a 2-byte boundary in the original buffer.
func checksumUpdate2ByteAlignedAddress(xsum uint16, old, new tcpip.Address) uint16 {
const uint16Bytes = 2
if len(old) != len(new) {
panic(fmt.Sprintf("buffer lengths are different; old = %d, new = %d", len(old), len(new)))
}
if len(old)%uint16Bytes != 0 {
panic(fmt.Sprintf("buffer has an odd number of bytes; got = %d", len(old)))
}
// As per RFC 1071 page 4,
// (4) Incremental Update
//
// ...
//
// To update the checksum, simply add the differences of the
// sixteen bit integers that have been changed. To see why this
// works, observe that every 16-bit integer has an additive inverse
// and that addition is associative. From this it follows that
// given the original value m, the new value m', and the old
// checksum C, the new checksum C' is:
//
// C' = C + (-m) + m' = C + (m' - m)
for len(old) != 0 {
// Convert the 2 byte sequences to uint16 values then apply the increment
// update.
xsum = checksumUpdate2ByteAlignedUint16(xsum, (uint16(old[0])<<8)+uint16(old[1]), (uint16(new[0])<<8)+uint16(new[1]))
old = old[uint16Bytes:]
new = new[uint16Bytes:]
}
return xsum
}
+203
View File
@@ -23,6 +23,7 @@ import (
"sync"
"testing"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/buffer"
"gvisor.dev/gvisor/pkg/tcpip/header"
)
@@ -256,3 +257,205 @@ func TestICMPv6Checksum(t *testing.T) {
})
}, want, fmt.Sprintf("header: {% x} data {% x}", h, vv.ToView()))
}
func randomAddress(size int) tcpip.Address {
s := make([]byte, size)
for i := 0; i < size; i++ {
s[i] = byte(rand.Uint32())
}
return tcpip.Address(s)
}
func TestChecksummableNetworkUpdateAddress(t *testing.T) {
tests := []struct {
name string
update func(header.IPv4, tcpip.Address)
}{
{
name: "SetSourceAddressWithChecksumUpdate",
update: header.IPv4.SetSourceAddressWithChecksumUpdate,
},
{
name: "SetDestinationAddressWithChecksumUpdate",
update: header.IPv4.SetDestinationAddressWithChecksumUpdate,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
for i := 0; i < 1000; i++ {
var origBytes [header.IPv4MinimumSize]byte
header.IPv4(origBytes[:]).Encode(&header.IPv4Fields{
TOS: 1,
TotalLength: header.IPv4MinimumSize,
ID: 2,
Flags: 3,
FragmentOffset: 4,
TTL: 5,
Protocol: 6,
Checksum: 0,
SrcAddr: randomAddress(header.IPv4AddressSize),
DstAddr: randomAddress(header.IPv4AddressSize),
})
addr := randomAddress(header.IPv4AddressSize)
bytesCopy := origBytes
h := header.IPv4(bytesCopy[:])
origXSum := h.CalculateChecksum()
h.SetChecksum(^origXSum)
test.update(h, addr)
got := ^h.Checksum()
h.SetChecksum(0)
want := h.CalculateChecksum()
if got != want {
t.Errorf("got h.Checksum() = 0x%x, want = 0x%x; originalBytes = 0x%x, new addr = %s", got, want, origBytes, addr)
}
}
})
}
}
func TestChecksummableTransportUpdatePort(t *testing.T) {
// The fields in the pseudo header is not tested here so we just use 0.
const pseudoHeaderXSum = 0
tests := []struct {
name string
transportHdr func(_, _ uint16) (header.ChecksummableTransport, func(uint16) uint16)
proto tcpip.TransportProtocolNumber
}{
{
name: "TCP",
transportHdr: func(src, dst uint16) (header.ChecksummableTransport, func(uint16) uint16) {
h := header.TCP(make([]byte, header.TCPMinimumSize))
h.Encode(&header.TCPFields{
SrcPort: src,
DstPort: dst,
SeqNum: 1,
AckNum: 2,
DataOffset: header.TCPMinimumSize,
Flags: 3,
WindowSize: 4,
Checksum: 0,
UrgentPointer: 5,
})
h.SetChecksum(^h.CalculateChecksum(pseudoHeaderXSum))
return h, h.CalculateChecksum
},
proto: header.TCPProtocolNumber,
},
{
name: "UDP",
transportHdr: func(src, dst uint16) (header.ChecksummableTransport, func(uint16) uint16) {
h := header.UDP(make([]byte, header.UDPMinimumSize))
h.Encode(&header.UDPFields{
SrcPort: src,
DstPort: dst,
Length: 0,
Checksum: 0,
})
h.SetChecksum(^h.CalculateChecksum(pseudoHeaderXSum))
return h, h.CalculateChecksum
},
proto: header.UDPProtocolNumber,
},
}
for i := 0; i < 1000; i++ {
origSrcPort := uint16(rand.Uint32())
origDstPort := uint16(rand.Uint32())
newPort := uint16(rand.Uint32())
t.Run(fmt.Sprintf("OrigSrcPort=%d,OrigDstPort=%d,NewPort=%d", origSrcPort, origDstPort, newPort), func(*testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
for _, subTest := range []struct {
name string
update func(header.ChecksummableTransport)
}{
{
name: "Source port",
update: func(h header.ChecksummableTransport) { h.SetSourcePortWithChecksumUpdate(newPort) },
},
{
name: "Destination port",
update: func(h header.ChecksummableTransport) { h.SetDestinationPortWithChecksumUpdate(newPort) },
},
} {
t.Run(subTest.name, func(t *testing.T) {
h, calcXSum := test.transportHdr(origSrcPort, origDstPort)
subTest.update(h)
// TCP and UDP hold the 1s complement of the fully calculated
// checksum.
got := ^h.Checksum()
h.SetChecksum(0)
if want := calcXSum(pseudoHeaderXSum); got != want {
h, _ := test.transportHdr(origSrcPort, origDstPort)
t.Errorf("got Checksum() = 0x%x, want = 0x%x; originalBytes = %#v, new port = %d", got, want, h, newPort)
}
})
}
})
}
})
}
}
func TestChecksummableTransportUpdatePseudoHeaderAddress(t *testing.T) {
const addressSize = 6
tests := []struct {
name string
transportHdr func() header.ChecksummableTransport
proto tcpip.TransportProtocolNumber
}{
{
name: "TCP",
transportHdr: func() header.ChecksummableTransport { return header.TCP(make([]byte, header.TCPMinimumSize)) },
proto: header.TCPProtocolNumber,
},
{
name: "UDP",
transportHdr: func() header.ChecksummableTransport { return header.UDP(make([]byte, header.UDPMinimumSize)) },
proto: header.UDPProtocolNumber,
},
}
for i := 0; i < 1000; i++ {
permanent := randomAddress(addressSize)
old := randomAddress(addressSize)
new := randomAddress(addressSize)
t.Run(fmt.Sprintf("Permanent=%q,Old=%q,New=%q", permanent, old, new), func(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
for _, fullChecksum := range []bool{true, false} {
t.Run(fmt.Sprintf("FullChecksum=%t", fullChecksum), func(t *testing.T) {
initialXSum := header.PseudoHeaderChecksum(test.proto, permanent, old, 0)
if fullChecksum {
// TCP and UDP hold the 1s complement of the fully calculated
// checksum.
initialXSum = ^initialXSum
}
h := test.transportHdr()
h.SetChecksum(initialXSum)
h.UpdateChecksumPseudoHeaderAddress(old, new, fullChecksum)
got := h.Checksum()
if fullChecksum {
got = ^got
}
if want := header.PseudoHeaderChecksum(test.proto, permanent, new, 0); got != want {
t.Errorf("got Checksum() = 0x%x, want = 0x%x; h = %#v", got, want, h)
}
})
}
})
}
})
}
}
+38
View File
@@ -53,6 +53,31 @@ type Transport interface {
Payload() []byte
}
// ChecksummableTransport is a Transport that supports checksumming.
type ChecksummableTransport interface {
Transport
// SetSourcePortWithChecksumUpdate sets the source port and updates
// the checksum.
//
// The receiver's checksum must be a fully calculated checksum.
SetSourcePortWithChecksumUpdate(port uint16)
// SetDestinationPortWithChecksumUpdate sets the destination port and updates
// the checksum.
//
// The receiver's checksum must be a fully calculated checksum.
SetDestinationPortWithChecksumUpdate(port uint16)
// UpdateChecksumPseudoHeaderAddress updates the checksum to reflect an
// updated address in the pseudo header.
//
// If fullChecksum is true, the receiver's checksum field is assumed to hold a
// fully calculated checksum. Otherwise, it is assumed to hold a partially
// calculated checksum which only reflects the pseudo header.
UpdateChecksumPseudoHeaderAddress(old, new tcpip.Address, fullChecksum bool)
}
// Network offers generic methods to query and/or update the fields of the
// header of a network protocol buffer.
type Network interface {
@@ -90,3 +115,16 @@ type Network interface {
// SetTOS sets the values of the "type of service" and "flow label" fields.
SetTOS(t uint8, l uint32)
}
// ChecksummableNetwork is a Network that supports checksumming.
type ChecksummableNetwork interface {
Network
// SetSourceAddressAndChecksum sets the source address and updates the
// checksum to reflect the new address.
SetSourceAddressWithChecksumUpdate(tcpip.Address)
// SetDestinationAddressAndChecksum sets the destination address and
// updates the checksum to reflect the new address.
SetDestinationAddressWithChecksumUpdate(tcpip.Address)
}
+12
View File
@@ -305,6 +305,18 @@ func (b IPv4) DestinationAddress() tcpip.Address {
return tcpip.Address(b[dstAddr : dstAddr+IPv4AddressSize])
}
// SetSourceAddressWithChecksumUpdate implements ChecksummableNetwork.
func (b IPv4) SetSourceAddressWithChecksumUpdate(new tcpip.Address) {
b.SetChecksum(^checksumUpdate2ByteAlignedAddress(^b.Checksum(), b.SourceAddress(), new))
b.SetSourceAddress(new)
}
// SetDestinationAddressWithChecksumUpdate implements ChecksummableNetwork.
func (b IPv4) SetDestinationAddressWithChecksumUpdate(new tcpip.Address) {
b.SetChecksum(^checksumUpdate2ByteAlignedAddress(^b.Checksum(), b.DestinationAddress(), new))
b.SetDestinationAddress(new)
}
// padIPv4OptionsLength returns the total length for IPv4 options of length l
// after applying padding according to RFC 791:
// The internet header padding is used to ensure that the internet
+29
View File
@@ -390,6 +390,35 @@ func (b TCP) EncodePartial(partialChecksum, length uint16, seqnum, acknum uint32
b.SetChecksum(^checksum)
}
// SetSourcePortWithChecksumUpdate implements ChecksummableTransport.
func (b TCP) SetSourcePortWithChecksumUpdate(new uint16) {
old := b.SourcePort()
b.SetSourcePort(new)
b.SetChecksum(^checksumUpdate2ByteAlignedUint16(^b.Checksum(), old, new))
}
// SetDestinationPortWithChecksumUpdate implements ChecksummableTransport.
func (b TCP) SetDestinationPortWithChecksumUpdate(new uint16) {
old := b.DestinationPort()
b.SetDestinationPort(new)
b.SetChecksum(^checksumUpdate2ByteAlignedUint16(^b.Checksum(), old, new))
}
// UpdateChecksumPseudoHeaderAddress implements ChecksummableTransport.
func (b TCP) UpdateChecksumPseudoHeaderAddress(old, new tcpip.Address, fullChecksum bool) {
xsum := b.Checksum()
if fullChecksum {
xsum = ^xsum
}
xsum = checksumUpdate2ByteAlignedAddress(xsum, old, new)
if fullChecksum {
xsum = ^xsum
}
b.SetChecksum(xsum)
}
// ParseSynOptions parses the options received in a SYN segment and returns the
// relevant ones. opts should point to the option part of the TCP header.
func ParseSynOptions(opts []byte, isAck bool) TCPSynOptions {
+29
View File
@@ -130,3 +130,32 @@ func (b UDP) Encode(u *UDPFields) {
binary.BigEndian.PutUint16(b[udpLength:], u.Length)
binary.BigEndian.PutUint16(b[udpChecksum:], u.Checksum)
}
// SetSourcePortWithChecksumUpdate implements ChecksummableTransport.
func (b UDP) SetSourcePortWithChecksumUpdate(new uint16) {
old := b.SourcePort()
b.SetSourcePort(new)
b.SetChecksum(^checksumUpdate2ByteAlignedUint16(^b.Checksum(), old, new))
}
// SetDestinationPortWithChecksumUpdate implements ChecksummableTransport.
func (b UDP) SetDestinationPortWithChecksumUpdate(new uint16) {
old := b.DestinationPort()
b.SetDestinationPort(new)
b.SetChecksum(^checksumUpdate2ByteAlignedUint16(^b.Checksum(), old, new))
}
// UpdateChecksumPseudoHeaderAddress implements ChecksummableTransport.
func (b UDP) UpdateChecksumPseudoHeaderAddress(old, new tcpip.Address, fullChecksum bool) {
xsum := b.Checksum()
if fullChecksum {
xsum = ^xsum
}
xsum = checksumUpdate2ByteAlignedAddress(xsum, old, new)
if fullChecksum {
xsum = ^xsum
}
b.SetChecksum(xsum)
}
+31 -20
View File
@@ -405,16 +405,23 @@ func (ct *ConnTrack) handlePacket(pkt *PacketBuffer, hook Hook, r *Route) bool {
// validated if checksum offloading is off. It may require IP defrag if the
// packets are fragmented.
var newAddr tcpip.Address
var newPort uint16
updateSRCFields := false
switch hook {
case Prerouting, Output:
if conn.manip == manipDestination {
switch dir {
case dirOriginal:
tcpHeader.SetDestinationPort(conn.reply.srcPort)
netHeader.SetDestinationAddress(conn.reply.srcAddr)
newPort = conn.reply.srcPort
newAddr = conn.reply.srcAddr
case dirReply:
tcpHeader.SetSourcePort(conn.original.dstPort)
netHeader.SetSourceAddress(conn.original.dstAddr)
newPort = conn.original.dstPort
newAddr = conn.original.dstAddr
updateSRCFields = true
}
pkt.NatDone = true
}
@@ -422,11 +429,13 @@ func (ct *ConnTrack) handlePacket(pkt *PacketBuffer, hook Hook, r *Route) bool {
if conn.manip == manipSource {
switch dir {
case dirOriginal:
tcpHeader.SetSourcePort(conn.reply.dstPort)
netHeader.SetSourceAddress(conn.reply.dstAddr)
newPort = conn.reply.dstPort
newAddr = conn.reply.dstAddr
updateSRCFields = true
case dirReply:
tcpHeader.SetDestinationPort(conn.original.srcPort)
netHeader.SetDestinationAddress(conn.original.srcAddr)
newPort = conn.original.srcPort
newAddr = conn.original.srcAddr
}
pkt.NatDone = true
}
@@ -437,29 +446,31 @@ func (ct *ConnTrack) handlePacket(pkt *PacketBuffer, hook Hook, r *Route) bool {
return false
}
fullChecksum := false
updatePseudoHeader := false
switch hook {
case Prerouting, Input:
case Output, Postrouting:
// Calculate the TCP checksum and set it.
tcpHeader.SetChecksum(0)
length := uint16(len(tcpHeader) + pkt.Data().Size())
xsum := header.PseudoHeaderChecksum(header.TCPProtocolNumber, netHeader.SourceAddress(), netHeader.DestinationAddress(), length)
if pkt.GSOOptions.Type != GSONone && pkt.GSOOptions.NeedsCsum {
tcpHeader.SetChecksum(xsum)
updatePseudoHeader = true
} else if r.RequiresTXTransportChecksum() {
xsum = header.ChecksumCombine(xsum, pkt.Data().AsRange().Checksum())
tcpHeader.SetChecksum(^tcpHeader.CalculateChecksum(xsum))
fullChecksum = true
updatePseudoHeader = true
}
default:
panic(fmt.Sprintf("unrecognized hook = %s", hook))
}
// After modification, IPv4 packets need a valid checksum.
if pkt.NetworkProtocolNumber == header.IPv4ProtocolNumber {
netHeader := header.IPv4(pkt.NetworkHeader().View())
netHeader.SetChecksum(0)
netHeader.SetChecksum(^netHeader.CalculateChecksum())
}
rewritePacket(
netHeader,
tcpHeader,
updateSRCFields,
fullChecksum,
updatePseudoHeader,
newPort,
newAddr,
)
// Update the state of tcb.
conn.mu.Lock()
+61 -36
View File
@@ -133,29 +133,23 @@ func (rt *RedirectTarget) Action(pkt *PacketBuffer, ct *ConnTrack, hook Hook, r
switch protocol := pkt.TransportProtocolNumber; protocol {
case header.UDPProtocolNumber:
udpHeader := header.UDP(pkt.TransportHeader().View())
udpHeader.SetDestinationPort(rt.Port)
// Calculate UDP checksum and set it.
if hook == Output {
udpHeader.SetChecksum(0)
netHeader := pkt.Network()
netHeader.SetDestinationAddress(address)
// Only calculate the checksum if offloading isn't supported.
if r.RequiresTXTransportChecksum() {
length := uint16(pkt.Size()) - uint16(len(pkt.NetworkHeader().View()))
xsum := header.PseudoHeaderChecksum(protocol, netHeader.SourceAddress(), netHeader.DestinationAddress(), length)
xsum = header.ChecksumCombine(xsum, pkt.Data().AsRange().Checksum())
udpHeader.SetChecksum(^udpHeader.CalculateChecksum(xsum))
}
requiresChecksum := r.RequiresTXTransportChecksum()
rewritePacket(
pkt.Network(),
udpHeader,
false, /* updateSRCFields */
requiresChecksum,
requiresChecksum,
rt.Port,
address,
)
} else {
udpHeader.SetDestinationPort(rt.Port)
}
// After modification, IPv4 packets need a valid checksum.
if pkt.NetworkProtocolNumber == header.IPv4ProtocolNumber {
netHeader := header.IPv4(pkt.NetworkHeader().View())
netHeader.SetChecksum(0)
netHeader.SetChecksum(^netHeader.CalculateChecksum())
}
pkt.NatDone = true
case header.TCPProtocolNumber:
if ct == nil {
@@ -214,26 +208,18 @@ func (st *SNATTarget) Action(pkt *PacketBuffer, ct *ConnTrack, hook Hook, r *Rou
switch protocol := pkt.TransportProtocolNumber; protocol {
case header.UDPProtocolNumber:
udpHeader := header.UDP(pkt.TransportHeader().View())
udpHeader.SetChecksum(0)
udpHeader.SetSourcePort(st.Port)
netHeader := pkt.Network()
netHeader.SetSourceAddress(st.Addr)
// Only calculate the checksum if offloading isn't supported.
if r.RequiresTXTransportChecksum() {
length := uint16(pkt.Size()) - uint16(len(pkt.NetworkHeader().View()))
xsum := header.PseudoHeaderChecksum(protocol, netHeader.SourceAddress(), netHeader.DestinationAddress(), length)
xsum = header.ChecksumCombine(xsum, pkt.Data().AsRange().Checksum())
udpHeader.SetChecksum(^udpHeader.CalculateChecksum(xsum))
}
requiresChecksum := r.RequiresTXTransportChecksum()
rewritePacket(
pkt.Network(),
header.UDP(pkt.TransportHeader().View()),
true, /* updateSRCFields */
requiresChecksum,
requiresChecksum,
st.Port,
st.Addr,
)
// After modification, IPv4 packets need a valid checksum.
if pkt.NetworkProtocolNumber == header.IPv4ProtocolNumber {
netHeader := header.IPv4(pkt.NetworkHeader().View())
netHeader.SetChecksum(0)
netHeader.SetChecksum(^netHeader.CalculateChecksum())
}
pkt.NatDone = true
case header.TCPProtocolNumber:
if ct == nil {
@@ -252,3 +238,42 @@ func (st *SNATTarget) Action(pkt *PacketBuffer, ct *ConnTrack, hook Hook, r *Rou
return RuleAccept, 0
}
func rewritePacket(n header.Network, t header.ChecksummableTransport, updateSRCFields, fullChecksum, updatePseudoHeader bool, newPort uint16, newAddr tcpip.Address) {
if updateSRCFields {
if fullChecksum {
t.SetSourcePortWithChecksumUpdate(newPort)
} else {
t.SetSourcePort(newPort)
}
} else {
if fullChecksum {
t.SetDestinationPortWithChecksumUpdate(newPort)
} else {
t.SetDestinationPort(newPort)
}
}
if updatePseudoHeader {
var oldAddr tcpip.Address
if updateSRCFields {
oldAddr = n.SourceAddress()
} else {
oldAddr = n.DestinationAddress()
}
t.UpdateChecksumPseudoHeaderAddress(oldAddr, newAddr, fullChecksum)
}
if checksummableNetHeader, ok := n.(header.ChecksummableNetwork); ok {
if updateSRCFields {
checksummableNetHeader.SetSourceAddressWithChecksumUpdate(newAddr)
} else {
checksummableNetHeader.SetDestinationAddressWithChecksumUpdate(newAddr)
}
} else if updateSRCFields {
n.SetSourceAddress(newAddr)
} else {
n.SetDestinationAddress(newAddr)
}
}