diff --git a/dataplane/forwarding/protocol/handler.go b/dataplane/forwarding/protocol/handler.go index 109fdde6..6b7e8070 100644 --- a/dataplane/forwarding/protocol/handler.go +++ b/dataplane/forwarding/protocol/handler.go @@ -15,6 +15,8 @@ package protocol import ( + "fmt" + "github.com/openconfig/lemming/dataplane/forwarding/infra/fwdpacket" fwdpb "github.com/openconfig/lemming/proto/forwarding" ) @@ -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 diff --git a/dataplane/forwarding/protocol/packet.go b/dataplane/forwarding/protocol/packet.go index 1206c02d..d33eef80 100644 --- a/dataplane/forwarding/protocol/packet.go +++ b/dataplane/forwarding/protocol/packet.go @@ -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] diff --git a/dataplane/forwarding/protocol/udp/BUILD b/dataplane/forwarding/protocol/udp/BUILD index 7abefc81..cf616feb 100644 --- a/dataplane/forwarding/protocol/udp/BUILD +++ b/dataplane/forwarding/protocol/udp/BUILD @@ -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", @@ -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", diff --git a/dataplane/forwarding/protocol/udp/udp.go b/dataplane/forwarding/protocol/udp/udp.go index 15d0dee6..d763d2bd 100644 --- a/dataplane/forwarding/protocol/udp/udp.go +++ b/dataplane/forwarding/protocol/udp/udp.go @@ -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" @@ -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. @@ -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 { @@ -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) } @@ -126,6 +149,9 @@ 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() @@ -133,6 +159,9 @@ func (udp *UDP) Rebuild() error { 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)) diff --git a/dataplane/forwarding/protocol/udp/udp_test.go b/dataplane/forwarding/protocol/udp/udp_test.go index 319a4dfa..08bff795 100644 --- a/dataplane/forwarding/protocol/udp/udp_test.go +++ b/dataplane/forwarding/protocol/udp/udp_test.go @@ -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" ) @@ -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) + } + }) +} + diff --git a/dataplane/forwarding/protocol/vxlan/BUILD b/dataplane/forwarding/protocol/vxlan/BUILD index 1228fdfc..7729ed5e 100644 --- a/dataplane/forwarding/protocol/vxlan/BUILD +++ b/dataplane/forwarding/protocol/vxlan/BUILD @@ -9,6 +9,7 @@ go_library( "//dataplane/forwarding/infra/fwdpacket", "//dataplane/forwarding/protocol", "//dataplane/forwarding/util/frame", + "//dataplane/forwarding/util/hash/crc16", "//proto/forwarding", ], ) @@ -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", diff --git a/dataplane/forwarding/protocol/vxlan/vxlan.go b/dataplane/forwarding/protocol/vxlan/vxlan.go index d5bfd028..96ceaa43 100644 --- a/dataplane/forwarding/protocol/vxlan/vxlan.go +++ b/dataplane/forwarding/protocol/vxlan/vxlan.go @@ -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" ) @@ -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. @@ -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) diff --git a/dataplane/forwarding/protocol/vxlan/vxlan_test.go b/dataplane/forwarding/protocol/vxlan/vxlan_test.go index c1ebc3b0..cab892e4 100644 --- a/dataplane/forwarding/protocol/vxlan/vxlan_test.go +++ b/dataplane/forwarding/protocol/vxlan/vxlan_test.go @@ -15,11 +15,17 @@ package vxlan import ( + "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/util/frame" fwdpb "github.com/openconfig/lemming/proto/forwarding" ) @@ -154,3 +160,54 @@ func TestVXLANRemove(t *testing.T) { } } +func TestVXLANEntropyCalculation(t *testing.T) { + vxlanHdr := []byte{0x08, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x00} + innerEth1 := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x08, 0x00} + innerEth2 := []byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x08, 0x00} + + innerIPAndTCP := []byte{ + 0x45, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x40, 0x06, 0x00, 0x00, + 10, 0, 0, 1, + 10, 0, 0, 2, + 0x1f, 0x90, 0x00, 0x50, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x50, 0x02, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, + } + + data1 := []byte("HTTP payload 1") + data2 := []byte("HTTP payload 2 different") + + flow1Pkt1 := slices.Concat(vxlanHdr, innerEth1, innerIPAndTCP, data1) + flow1Pkt2 := slices.Concat(vxlanHdr, innerEth1, innerIPAndTCP, data2) + flow2Pkt := slices.Concat(vxlanHdr, innerEth2, innerIPAndTCP, data1) + + pkt1, err := fwdpacket.New(fwdpb.PacketHeaderId_PACKET_HEADER_ID_VXLAN, flow1Pkt1) + if err != nil { + t.Fatalf("fwdpacket.New failed: %v", err) + } + pkt2, err := fwdpacket.New(fwdpb.PacketHeaderId_PACKET_HEADER_ID_VXLAN, flow1Pkt2) + if err != nil { + t.Fatalf("fwdpacket.New failed: %v", err) + } + pkt3, err := fwdpacket.New(fwdpb.PacketHeaderId_PACKET_HEADER_ID_VXLAN, flow2Pkt) + if err != nil { + t.Fatalf("fwdpacket.New failed: %v", err) + } + + port1 := ComputeEntropySourcePort(pkt1.(*protocol.Packet).FirstDesc().PayloadDesc()) + port2 := ComputeEntropySourcePort(pkt2.(*protocol.Packet).FirstDesc().PayloadDesc()) + port3 := ComputeEntropySourcePort(pkt3.(*protocol.Packet).FirstDesc().PayloadDesc()) + + if port1 < EphemeralPortBase { + t.Errorf("port1 = %d, want >= %d", port1, EphemeralPortBase) + } + if port1 != port2 { + t.Errorf("expected identical port for same flow with different payload: got %d vs %d", port1, port2) + } + if port1 == port3 { + t.Errorf("expected different ports for different flows: got %d for both", port1) + } + if got := ComputeEntropySourcePort(nil); got != 0 { + t.Errorf("ComputeEntropySourcePort(nil) = %d, want 0", got) + } +} +