Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions dataplane/forwarding/protocol/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
package protocol

import (
"fmt"

"github.com/openconfig/lemming/dataplane/forwarding/infra/fwdpacket"
fwdpb "github.com/openconfig/lemming/proto/forwarding"
)
Expand Down Expand Up @@ -92,6 +94,19 @@ func (d *Desc) PayloadDesc() *Desc {
return d.next
}

// PayloadField returns the value of id from the first payload descriptor (d.next
// onwards) that implements the field. This lets an outer tunnel header (such as
// VXLAN/UDP) read fields from its inner headers without matching itself or the
// headers outside it.
func (d *Desc) PayloadField(id fwdpacket.FieldID) ([]byte, error) {
for desc := d.next; desc != nil; desc = desc.next {
if f, err := desc.handler.Field(id); err == nil {
return f, nil
}
}
return nil, fmt.Errorf("protocol: PayloadField %v failed, not in payload", id)
}

// Payload returns the payload. The payload of a desc is represented by the
// set of all subsequent desc in the packet. handlers are rebuilt and the
// header and trailer bytes of each protocol handler is combined to form the
Expand Down
16 changes: 16 additions & 0 deletions dataplane/forwarding/protocol/packet.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,22 @@ func NewPacket(start fwdpb.PacketHeaderId, frame *frame.Frame) (*Packet, error)
return p, nil
}

// FirstDesc returns the first descriptor in the packet chain (typically METADATA).
func (p *Packet) FirstDesc() *Desc {
if d := p.headers[Sequence[0]]; d != nil {
return d
}
for _, seq := range Sequence {
if d := p.headers[seq]; d != nil {
for d.prev != nil {
d = d.prev
}
return d
}
}
return nil
}

// Decap removes the header specified by id. The header is marked as dirty.
func (p *Packet) Decap(id fwdpb.PacketHeaderId) error {
attr, ok := HeaderAttr[id]
Expand Down
7 changes: 7 additions & 0 deletions dataplane/forwarding/protocol/udp/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ go_library(
deps = [
"//dataplane/forwarding/infra/fwdpacket",
"//dataplane/forwarding/protocol",
"//dataplane/forwarding/protocol/vxlan",
"//dataplane/forwarding/util/frame",
"//dataplane/forwarding/util/hash/csum16",
"//proto/forwarding",
Expand All @@ -21,6 +22,12 @@ go_test(
deps = [
"//dataplane/forwarding/infra/fwdpacket",
"//dataplane/forwarding/protocol",
"//dataplane/forwarding/protocol/ethernet",
"//dataplane/forwarding/protocol/ip",
"//dataplane/forwarding/protocol/metadata",
"//dataplane/forwarding/protocol/opaque",
"//dataplane/forwarding/protocol/tcp",
"//dataplane/forwarding/protocol/vxlan",
"//dataplane/forwarding/util/frame",
"//proto/forwarding",
"@com_github_google_go_cmp//cmp",
Expand Down
33 changes: 31 additions & 2 deletions dataplane/forwarding/protocol/udp/udp.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"github.com/openconfig/lemming/dataplane/forwarding/infra/fwdpacket"
"github.com/openconfig/lemming/dataplane/forwarding/protocol"
"github.com/openconfig/lemming/dataplane/forwarding/protocol/vxlan"
"github.com/openconfig/lemming/dataplane/forwarding/util/frame"
"github.com/openconfig/lemming/dataplane/forwarding/util/hash/csum16"
fwdpb "github.com/openconfig/lemming/proto/forwarding"
Expand All @@ -45,7 +46,7 @@ var portMapMu sync.RWMutex

// PortMap maps UDP destination ports to protocol header IDs.
var PortMap = map[uint16]fwdpb.PacketHeaderId{
4789: fwdpb.PacketHeaderId_PACKET_HEADER_ID_VXLAN,
vxlan.DefaultPort: fwdpb.PacketHeaderId_PACKET_HEADER_ID_VXLAN,
}

// RegisterPort registers a protocol header ID for a given UDP destination port.
Expand Down Expand Up @@ -94,6 +95,22 @@ func (udp *UDP) field(id fwdpacket.FieldID) frame.Field {
}
}

// updateVXLANSourcePort populates the UDP source port with an RFC 7348
// inner-flow entropy port when encapsulating VXLAN and no source port is set.
func (udp *UDP) updateVXLANSourcePort() {
if udp.desc == nil || len(udp.header) < udpBytes {
return
}
dstPort := uint16(udp.header.Field(dstOffset, portBytes).Value())
srcPort := udp.header.Field(srcOffset, portBytes)
if dstPort != vxlan.DefaultPort || srcPort.Value() != 0 {
return
}
if entropyPort := vxlan.ComputeEntropySourcePort(udp.desc); entropyPort != 0 {
srcPort.SetValue(uint(entropyPort))
}
}

// Field finds bytes within the UDP header.
func (udp *UDP) Field(id fwdpacket.FieldID) ([]byte, error) {
if field := udp.field(id); field != nil {
Expand All @@ -105,7 +122,13 @@ func (udp *UDP) Field(id fwdpacket.FieldID) ([]byte, error) {
// UpdateField sets bytes within the UDP header.
func (udp *UDP) UpdateField(id fwdpacket.FieldID, op int, arg []byte) (bool, error) {
if field := udp.field(id); field != nil && op == fwdpacket.OpSet {
return true, field.Set(arg)
if err := field.Set(arg); err != nil {
return false, err
}
if !id.IsUDF && id.Num == fwdpb.PacketFieldNum_PACKET_FIELD_NUM_L4_PORT_DST {
udp.updateVXLANSourcePort()
}
return true, nil
}
return false, fmt.Errorf("udp: UpdateField failed, unsupported op %v for field %v", op, id)
}
Expand All @@ -126,13 +149,19 @@ func (UDP) Modify(_ fwdpb.PacketHeaderId) error {

// Rebuild updates the UDP header length and csum (if over IPv6).
func (udp *UDP) Rebuild() error {
if len(udp.header) < udpBytes {
return nil
}
// If the envelope, payload and udp header are unmodified, skip updates.
e := udp.desc.EnvelopeDesc()
p := udp.desc.PayloadDesc()
if e != nil && !e.Dirty() && p != nil && !p.Dirty() && !udp.desc.Dirty() {
return nil
}

// Update source port for VXLAN.
udp.updateVXLANSourcePort()

// Update the length and reset the checksum.
length := len(udp.Header()) + udp.desc.PayloadLength()
udp.header.Field(lenOffset, lenBytes).SetValue(uint(length))
Expand Down
111 changes: 111 additions & 0 deletions dataplane/forwarding/protocol/udp/udp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,18 @@ package udp

import (
"encoding/binary"
"slices"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/openconfig/lemming/dataplane/forwarding/infra/fwdpacket"
"github.com/openconfig/lemming/dataplane/forwarding/protocol"
_ "github.com/openconfig/lemming/dataplane/forwarding/protocol/ethernet"
_ "github.com/openconfig/lemming/dataplane/forwarding/protocol/ip"
_ "github.com/openconfig/lemming/dataplane/forwarding/protocol/metadata"
_ "github.com/openconfig/lemming/dataplane/forwarding/protocol/opaque"
_ "github.com/openconfig/lemming/dataplane/forwarding/protocol/tcp"
"github.com/openconfig/lemming/dataplane/forwarding/protocol/vxlan"
"github.com/openconfig/lemming/dataplane/forwarding/util/frame"
fwdpb "github.com/openconfig/lemming/proto/forwarding"
)
Expand Down Expand Up @@ -164,3 +171,107 @@ func TestUDPMethods(t *testing.T) {
}
}
}

func createAndRebuildUDPPacket(t *testing.T, srcPort, dstPort uint16, payload []byte) (uint16, []byte) {
t.Helper()
udpHdr := make([]byte, udpBytes)
binary.BigEndian.PutUint16(udpHdr[srcOffset:], srcPort)
binary.BigEndian.PutUint16(udpHdr[dstOffset:], dstPort)
binary.BigEndian.PutUint16(udpHdr[lenOffset:], uint16(udpBytes+len(payload)))

raw := append(udpHdr, payload...)
pkt, err := fwdpacket.New(fwdpb.PacketHeaderId_PACKET_HEADER_ID_UDP, raw)
if err != nil {
t.Fatalf("fwdpacket.New failed: %v", err)
}

srcFieldID := fwdpacket.NewFieldIDFromNum(fwdpb.PacketFieldNum_PACKET_FIELD_NUM_L4_PORT_SRC, 0)
// Dirty the UDP header so Rebuild is executed during Frame().
if err := pkt.Update(srcFieldID, fwdpacket.OpSet, udpHdr[srcOffset:srcOffset+portBytes]); err != nil {
t.Fatalf("pkt.Update srcPort failed: %v", err)
}

rebuiltFrame := pkt.Frame()
rebuiltSrcBytes, err := pkt.Field(srcFieldID)
if err != nil {
t.Fatalf("pkt.Field srcPort failed: %v", err)
}
rebuiltSrcPort := binary.BigEndian.Uint16(rebuiltSrcBytes)
return rebuiltSrcPort, rebuiltFrame
}

func TestUDPHeaderRebuild(t *testing.T) {
vxlanHdr := []byte{0x08, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x00}
innerEth := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x08, 0x00}
innerIPAndTCP := []byte{
0x45, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x40, 0x06, 0x00, 0x00,
10, 0, 0, 1, // src ip
10, 0, 0, 2, // dst ip
0x1f, 0x90, 0x00, 0x50, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x50, 0x02, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00,
}
data := []byte("UDP payload data")
flowPkt := slices.Concat(vxlanHdr, innerEth, innerIPAndTCP, data)

t.Run("VXLAN entropy source port calculation", func(t *testing.T) {
gotPort, frameBytes := createAndRebuildUDPPacket(t, 0, vxlan.DefaultPort, flowPkt)

// RFC 7348 §5: Must be within dynamic/private ephemeral range [49152, 65535]
if gotPort < 49152 {
t.Errorf("got source port %d, want within [49152, 65535]", gotPort)
}

// Verify frame bytes match the queried source port
framePort := binary.BigEndian.Uint16(frameBytes[srcOffset : srcOffset+portBytes])
if framePort != gotPort {
t.Errorf("frame source port %d does not match field source port %d", framePort, gotPort)
}
})

t.Run("Non-VXLAN destination port retains srcPort 0", func(t *testing.T) {
// Standard UDP packet targeting DNS port 53 with srcPort 0
portDNS, frameDNS := createAndRebuildUDPPacket(t, 0, 53, data)
if portDNS != 0 {
t.Errorf("standard UDP dstPort=53: got srcPort %d, want 0", portDNS)
}
framePort := binary.BigEndian.Uint16(frameDNS[srcOffset : srcOffset+portBytes])
if framePort != 0 {
t.Errorf("standard UDP frame dstPort=53: got srcPort %d, want 0", framePort)
}
})

t.Run("Pre-set source port retains explicit value", func(t *testing.T) {
// VXLAN packet with explicit pre-set srcPort 12345
portVXLAN, frameVXLAN := createAndRebuildUDPPacket(t, 12345, vxlan.DefaultPort, flowPkt)
if portVXLAN != 12345 {
t.Errorf("VXLAN with pre-set srcPort=12345: got %d, want 12345", portVXLAN)
}
framePortVXLAN := binary.BigEndian.Uint16(frameVXLAN[srcOffset : srcOffset+portBytes])
if framePortVXLAN != 12345 {
t.Errorf("VXLAN frame with pre-set srcPort=12345: got %d, want 12345", framePortVXLAN)
}

// Standard UDP packet with pre-set srcPort 12345
portDNS, _ := createAndRebuildUDPPacket(t, 12345, 53, data)
if portDNS != 12345 {
t.Errorf("standard UDP with pre-set srcPort=12345: got %d, want 12345", portDNS)
}
})

t.Run("Direct Rebuild with empty payload retains srcPort 0", func(t *testing.T) {
h, err := add(fwdpb.PacketHeaderId_PACKET_HEADER_ID_UDP, &protocol.Desc{})
if err != nil {
t.Fatalf("add failed: %v", err)
}
udpHandler := h.(*UDP)
udpHandler.header.Field(dstOffset, portBytes).SetValue(vxlan.DefaultPort)
udpHandler.header.Field(srcOffset, portBytes).SetValue(0)
if err := udpHandler.Rebuild(); err != nil {
t.Fatalf("udpHandler.Rebuild() failed: %v", err)
}
if got := udpHandler.header.Field(srcOffset, portBytes).Value(); got != 0 {
t.Errorf("Rebuild with empty payload: got srcPort %d, want 0", got)
}
})
}

6 changes: 6 additions & 0 deletions dataplane/forwarding/protocol/vxlan/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ go_library(
"//dataplane/forwarding/infra/fwdpacket",
"//dataplane/forwarding/protocol",
"//dataplane/forwarding/util/frame",
"//dataplane/forwarding/util/hash/crc16",
"//proto/forwarding",
],
)
Expand All @@ -20,6 +21,11 @@ go_test(
deps = [
"//dataplane/forwarding/infra/fwdpacket",
"//dataplane/forwarding/protocol",
"//dataplane/forwarding/protocol/ethernet",
"//dataplane/forwarding/protocol/ip",
"//dataplane/forwarding/protocol/metadata",
"//dataplane/forwarding/protocol/opaque",
"//dataplane/forwarding/protocol/tcp",
"//dataplane/forwarding/util/frame",
"//proto/forwarding",
"@com_github_google_go_cmp//cmp",
Expand Down
43 changes: 43 additions & 0 deletions dataplane/forwarding/protocol/vxlan/vxlan.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/openconfig/lemming/dataplane/forwarding/infra/fwdpacket"
"github.com/openconfig/lemming/dataplane/forwarding/protocol"
"github.com/openconfig/lemming/dataplane/forwarding/util/frame"
"github.com/openconfig/lemming/dataplane/forwarding/util/hash/crc16"
fwdpb "github.com/openconfig/lemming/proto/forwarding"
)

Expand All @@ -34,6 +35,11 @@ const (
vniOffset = 4
vniBytes = 3 // SizeUint24
iFlag = 0x08

// EphemeralPortBase is the IANA dynamic/private port range base (49152) per RFC 7348 §5.
EphemeralPortBase = 0xC000
// EphemeralPortMask is the 14-bit mask for the dynamic port range (16383).
EphemeralPortMask = 0x3FFF
)

// VXLAN represents a VXLAN header in the packet.
Expand Down Expand Up @@ -116,6 +122,43 @@ func (v *VXLAN) Rebuild() error {
return nil
}

// InnerFlowKey extracts inner header invariant fields (5-tuple and MACs) per RFC 7348.
// Only invariant header fields are included;
// (such as IP ID or TCP sequence numbers) are excluded to ensure deterministic ECMP flow stability.
func InnerFlowKey(desc *protocol.Desc) []byte {
if desc == nil {
return nil
}
// Inner fields: MACs (12B) + IPv6/IPv4 IPs (32B) + Proto (1B) + L4 Ports (4B)
key := make([]byte, 0, 49)
fields := []fwdpb.PacketFieldNum{
fwdpb.PacketFieldNum_PACKET_FIELD_NUM_ETHER_MAC_SRC,
fwdpb.PacketFieldNum_PACKET_FIELD_NUM_ETHER_MAC_DST,
fwdpb.PacketFieldNum_PACKET_FIELD_NUM_IP_ADDR_SRC,
fwdpb.PacketFieldNum_PACKET_FIELD_NUM_IP_ADDR_DST,
fwdpb.PacketFieldNum_PACKET_FIELD_NUM_IP_PROTO,
fwdpb.PacketFieldNum_PACKET_FIELD_NUM_L4_PORT_SRC,
fwdpb.PacketFieldNum_PACKET_FIELD_NUM_L4_PORT_DST,
}
for _, num := range fields {
if f, err := desc.PayloadField(fwdpacket.NewFieldIDFromNum(num, 0)); err == nil && len(f) > 0 {
key = append(key, f...)
}
}
return key
}

// ComputeEntropySourcePort calculates a 16-bit UDP source port within the dynamic range [49152, 65535]
// based on the inner packet headers per RFC 7348 §5. Returns 0 if no inner header fields are found.
func ComputeEntropySourcePort(desc *protocol.Desc) uint16 {
key := InnerFlowKey(desc)
if len(key) == 0 {
return 0
}
hashVal := crc16.ChecksumANSI(key)
return uint16(EphemeralPortBase | (hashVal & EphemeralPortMask))
}

// Parse parses a VXLAN header in the packet.
func Parse(frame *frame.Frame, desc *protocol.Desc) (protocol.Handler, fwdpb.PacketHeaderId, error) {
return parse(frame, desc)
Expand Down
Loading
Loading