From 97fecf074d7ef32d4292774431f715746f5d076c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 22:53:13 +0000 Subject: [PATCH 1/4] Initial plan From 033d16d22c97073c726e1297e509128a12a7a829 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 23:07:23 +0000 Subject: [PATCH 2/4] Add IPv6 filter tests to complement existing IPv4 tests Co-authored-by: tomastigera <49207409+tomastigera@users.noreply.github.com> --- felix/bpf/ut/filter_test.go | 115 ++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/felix/bpf/ut/filter_test.go b/felix/bpf/ut/filter_test.go index 74f483dab01..5933e36d67f 100644 --- a/felix/bpf/ut/filter_test.go +++ b/felix/bpf/ut/filter_test.go @@ -142,3 +142,118 @@ func TestFilter(t *testing.T) { }) } } + +func TestFilterIPv6(t *testing.T) { + RegisterTestingT(t) + + _, _, _, _, bytes, _ := testPacketV6( + &layers.Ethernet{ + SrcMAC: []byte{0, 0, 0, 0, 0, 1}, + DstMAC: []byte{0, 0, 0, 0, 0, 2}, + EthernetType: layers.EthernetTypeIPv6, + }, + &layers.IPv6{ + Version: 6, + HopLimit: 64, + SrcIP: net.IP([]byte{0x20, 0x1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}), + DstIP: net.IP([]byte{0x20, 0x1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}), + NextHeader: layers.IPProtocolUDP, + }, + &layers.UDP{ + SrcPort: 1234, + DstPort: 666, + }, + make([]byte, 36), + ) + + type testCase struct { + expression string + match bool + } + + tests := []testCase{ + // L2 + {"ip", false}, + {"ip6", true}, + {"ip6 and tcp", false}, + // L2 + L3 + {"", true}, + {"udp", true}, + {"host 2001::1 or host 2001::5", true}, + {"host 2001::1 and host 2001::5", false}, + {"src 2001::1 and dst 2001::2", true}, + {"dst 2001::1 and src 2001::2", false}, + {"(host 2001::1 or host 2001::5) and (udp port 666)", true}, + {"(host 2001::1 or host 2001::5) and (udp port 1212)", false}, + {"len >= 64", true}, + {"len < 64", false}, + {"len >= 500", false}, + {"portrange 600-700", true}, + {"tcp portrange 600-700", false}, + {"portrange 700-800", false}, + } + + links := []struct { + level string + typ layers.LinkType + data []byte + tests []testCase + }{ + {"L2", layers.LinkTypeEthernet, bytes, tests}, + {"L3", layers.LinkTypeIPv6, bytes[14:], tests[3:]}, + } + + for _, link := range links { + for _, tc := range link.tests { + t.Run(link.level+"_"+tc.expression, func(t *testing.T) { + + insns, err := filter.NewStandAlone(link.typ, 64, tc.expression) + Expect(err).NotTo(HaveOccurred()) + fd, err := bpf.LoadBPFProgramFromInsns(insns, "filter", "Apache-2.0", unix.BPF_PROG_TYPE_SCHED_CLS) + Expect(err).NotTo(HaveOccurred()) + Expect(fd).NotTo(BeZero()) + defer func() { + Expect(fd.Close()).NotTo(HaveOccurred()) + }() + + rc, err := bpf.RunBPFProgram(fd, link.data, 1) + Expect(err).NotTo(HaveOccurred()) + erc := -1 + if !tc.match { + erc = 2 + } + Expect(rc.RC).To(BeNumerically("==", erc)) + }) + } + } + + link := links[0] + + for _, tc := range link.tests { + if tc.expression == "" { + continue + } + + neg := "not ( " + tc.expression + " )" + + t.Run(link.level+"_"+neg, func(t *testing.T) { + + insns, err := filter.NewStandAlone(layers.LinkTypeEthernet, 64, neg) + Expect(err).NotTo(HaveOccurred()) + fd, err := bpf.LoadBPFProgramFromInsns(insns, "filter", "Apache-2.0", unix.BPF_PROG_TYPE_SCHED_CLS) + Expect(err).NotTo(HaveOccurred()) + Expect(fd).NotTo(BeZero()) + defer func() { + Expect(fd.Close()).NotTo(HaveOccurred()) + }() + + rc, err := bpf.RunBPFProgram(fd, link.data, 1) + Expect(err).NotTo(HaveOccurred()) + erc := 2 + if !tc.match { + erc = -1 + } + Expect(rc.RC).To(BeNumerically("==", erc)) + }) + } +} From 75aa8e0f2a955cd344bbccea468a055125b1fa0c Mon Sep 17 00:00:00 2001 From: Tomas Hruby Date: Tue, 4 Nov 2025 17:59:48 -0800 Subject: [PATCH 3/4] add variable length ipv6 filters and TAX/TXA instructions --- felix/bpf/filter/filter.go | 39 ++++++++++++++++++++++++++++++++++++- felix/bpf/ut/filter_test.go | 6 ++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/felix/bpf/filter/filter.go b/felix/bpf/filter/filter.go index e1d2d053f7f..59e30f003c8 100644 --- a/felix/bpf/filter/filter.go +++ b/felix/bpf/filter/filter.go @@ -221,7 +221,15 @@ func cBPF2eBPF(b *asm.Block, pcap []pcap.BPFInstruction, linkType layers.LinkTyp switch class { case bpfClassMisc: - return fmt.Errorf("misc class: %+v", cbpf) + switch op { + case bpfTAX: + b.Mov64(asm.R2, asm.R1) // X = A + continue + case bpfTXA: + b.Mov64(asm.R1, asm.R2) // A = X + continue + } + return fmt.Errorf("misc class: %+v op %d", cbpf, op) case bpfClassRet: // K is the return value and hit should be snap length, 0 otherwise. // https://github.com/the-tcpdump-group/libpcap/blob/aa4fd0d411239f5cc98f0ae14018d3ad91a5ee15/gencode.c#L822 @@ -258,6 +266,7 @@ func cBPF2eBPF(b *asm.Block, pcap []pcap.BPFInstruction, linkType layers.LinkTyp case bpfModeIMM: // eBPF has only 64bit imm instructions b.LoadImm64(asm.R1, int64(K)) + continue } case bpfClassLdx: @@ -279,7 +288,15 @@ func cBPF2eBPF(b *asm.Block, pcap []pcap.BPFInstruction, linkType layers.LinkTyp b.Mov64(asm.R1 /* A */, asm.R3 /* tmp */) // Restore A from tmp } continue + } else { + switch bpfMode(code) { + case bpfModeIMM: + // eBPF has only 64bit imm instructions + b.LoadImm64(asm.R2, int64(K)) // X = K + continue + } } + case bpfClassAlu: case bpfClassJmp: var srcR asm.Reg @@ -347,6 +364,7 @@ const ( bpfClassLd uint8 = 0x0 bpfClassLdx uint8 = 0x1 bpfClassJmp uint8 = 0x5 + bpfClassAlu uint8 = 0x4 bpfClassRet uint8 = 0x6 bpfClassMisc uint8 = 0x7 ) @@ -379,6 +397,20 @@ const ( bpfModeMSH uint8 = 0xa0 ) +const ( + bpfAluAdd uint8 = 0x00 + bpfAluSub uint8 = 0x10 + bpfAluMul uint8 = 0x20 + bpfAluDiv uint8 = 0x30 + bpfAluOr uint8 = 0x40 + bpfAluAnd uint8 = 0x50 + bpfAluLsh uint8 = 0x60 + bpfAluRsh uint8 = 0x70 + bpfAluNeg uint8 = 0x80 + bpfAluMod uint8 = 0x90 + bpfAluXor uint8 = 0xa0 +) + var ( _ = bpfModeIMM _ = bpfModeMEM @@ -392,6 +424,11 @@ func bpfOp(code uint8) uint8 { return code & 0xf0 } +const ( + bpfTAX uint8 = 0x00 + bpfTXA uint8 = 0x80 +) + const ( bpfK uint8 = 0 bpfX uint8 = 0x8 diff --git a/felix/bpf/ut/filter_test.go b/felix/bpf/ut/filter_test.go index 5933e36d67f..35015c1b92f 100644 --- a/felix/bpf/ut/filter_test.go +++ b/felix/bpf/ut/filter_test.go @@ -191,6 +191,8 @@ func TestFilterIPv6(t *testing.T) { {"portrange 600-700", true}, {"tcp portrange 600-700", false}, {"portrange 700-800", false}, + {"ip6 protochain 17", true}, + {"ip6 protochain 6", false}, } links := []struct { @@ -207,7 +209,7 @@ func TestFilterIPv6(t *testing.T) { for _, tc := range link.tests { t.Run(link.level+"_"+tc.expression, func(t *testing.T) { - insns, err := filter.NewStandAlone(link.typ, 64, tc.expression) + insns, err := filter.NewStandAlone(link.typ, 64, tc.expression, stateMap.MapFD()) Expect(err).NotTo(HaveOccurred()) fd, err := bpf.LoadBPFProgramFromInsns(insns, "filter", "Apache-2.0", unix.BPF_PROG_TYPE_SCHED_CLS) Expect(err).NotTo(HaveOccurred()) @@ -238,7 +240,7 @@ func TestFilterIPv6(t *testing.T) { t.Run(link.level+"_"+neg, func(t *testing.T) { - insns, err := filter.NewStandAlone(layers.LinkTypeEthernet, 64, neg) + insns, err := filter.NewStandAlone(layers.LinkTypeEthernet, 64, neg, stateMap.MapFD()) Expect(err).NotTo(HaveOccurred()) fd, err := bpf.LoadBPFProgramFromInsns(insns, "filter", "Apache-2.0", unix.BPF_PROG_TYPE_SCHED_CLS) Expect(err).NotTo(HaveOccurred()) From 53d385f9d2cc38453622b94fbc4f7b75338d1b9b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 02:21:32 +0000 Subject: [PATCH 4/4] Implement classic BPF to eBPF ALU instruction conversion and scratch memory operations - Implemented all ALU operations (add, sub, mul, div, or, and, lsh, rsh, neg, mod, xor) - Added support for both immediate (K) and register (X) source operands - Implemented scratch memory operations (ST, STX, LD MEM, LDX MEM) - Classic BPF scratch memory M[0-15] is emulated using stack slots The protochain test still fails due to BPF verifier rejecting loop structures, which is a fundamental limitation when converting classic BPF loops to eBPF. Co-authored-by: tomastigera <49207409+tomastigera@users.noreply.github.com> --- felix/bpf/filter/filter.go | 113 ++++++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/felix/bpf/filter/filter.go b/felix/bpf/filter/filter.go index 59e30f003c8..a86a2e5f1fb 100644 --- a/felix/bpf/filter/filter.go +++ b/felix/bpf/filter/filter.go @@ -267,6 +267,13 @@ func cBPF2eBPF(b *asm.Block, pcap []pcap.BPFInstruction, linkType layers.LinkTyp // eBPF has only 64bit imm instructions b.LoadImm64(asm.R1, int64(K)) + continue + case bpfModeMEM: + // Load from scratch memory M[K] to A + if K >= 16 { + return fmt.Errorf("scratch memory index out of range: M[%d]", K) + } + b.Load32(asm.R1, asm.R10, asm.FieldOffset{Offset: -int16((K+1)*4), Field: fmt.Sprintf("M[%d]", K)}) continue } case bpfClassLdx: @@ -294,9 +301,111 @@ func cBPF2eBPF(b *asm.Block, pcap []pcap.BPFInstruction, linkType layers.LinkTyp // eBPF has only 64bit imm instructions b.LoadImm64(asm.R2, int64(K)) // X = K continue + case bpfModeMEM: + // Load from scratch memory M[K] to X + if K >= 16 { + return fmt.Errorf("scratch memory index out of range: M[%d]", K) + } + b.Load32(asm.R2, asm.R10, asm.FieldOffset{Offset: -int16((K+1)*4), Field: fmt.Sprintf("M[%d]", K)}) + continue } } + case bpfClassSt: + // Store A to scratch memory M[K] + // We use stack slots (R10 + offset) to emulate scratch memory + // Classic BPF has 16 scratch memory slots (M[0] through M[15]) + // Each slot is 32 bits, so we store at offset -(K+1)*4 + if K >= 16 { + return fmt.Errorf("scratch memory index out of range: M[%d]", K) + } + b.StoreStack32(asm.R1, -int16((K+1)*4)) + continue + case bpfClassStx: + // Store X to scratch memory M[K] + if K >= 16 { + return fmt.Errorf("scratch memory index out of range: M[%d]", K) + } + b.StoreStack32(asm.R2, -int16((K+1)*4)) + continue case bpfClassAlu: + // Classic BPF ALU operations on A register + // src can be either K (immediate) or X (register R2) + switch op { + case bpfAluAdd: + if src == bpfX { + b.Add64(asm.R1, asm.R2) // A += X + } else { + b.AddImm64(asm.R1, K) // A += K + } + continue + case bpfAluSub: + if src == bpfX { + b.Instr(asm.Sub64, asm.R1, asm.R2, 0, 0, "") // A -= X + } else { + b.Instr(asm.SubImm64, asm.R1, 0, 0, K, "") // A -= K + } + continue + case bpfAluMul: + if src == bpfX { + b.Instr(asm.Mul64, asm.R1, asm.R2, 0, 0, "") // A *= X + } else { + b.Instr(asm.MulImm64, asm.R1, 0, 0, K, "") // A *= K + } + continue + case bpfAluDiv: + if src == bpfX { + b.Instr(asm.Div64, asm.R1, asm.R2, 0, 0, "") // A /= X + } else { + b.Instr(asm.DivImm64, asm.R1, 0, 0, K, "") // A /= K + } + continue + case bpfAluOr: + if src == bpfX { + b.Instr(asm.Or64, asm.R1, asm.R2, 0, 0, "") // A |= X + } else { + b.OrImm64(asm.R1, K) // A |= K + } + continue + case bpfAluAnd: + if src == bpfX { + b.Instr(asm.And64, asm.R1, asm.R2, 0, 0, "") // A &= X + } else { + b.AndImm64(asm.R1, K) // A &= K + } + continue + case bpfAluLsh: + if src == bpfX { + b.Instr(asm.ShiftL64, asm.R1, asm.R2, 0, 0, "") // A <<= X + } else { + b.ShiftLImm64(asm.R1, K) // A <<= K + } + continue + case bpfAluRsh: + if src == bpfX { + b.Instr(asm.ShiftR64, asm.R1, asm.R2, 0, 0, "") // A >>= X + } else { + b.ShiftRImm64(asm.R1, K) // A >>= K + } + continue + case bpfAluNeg: + b.Instr(asm.Negate64, asm.R1, 0, 0, 0, "") // A = -A + continue + case bpfAluMod: + if src == bpfX { + b.Instr(asm.Mod64, asm.R1, asm.R2, 0, 0, "") // A %= X + } else { + b.Instr(asm.ModImm64, asm.R1, 0, 0, K, "") // A %= K + } + continue + case bpfAluXor: + if src == bpfX { + b.Instr(asm.XOR64, asm.R1, asm.R2, 0, 0, "") // A ^= X + } else { + b.Instr(asm.XORImm64, asm.R1, 0, 0, K, "") // A ^= K + } + continue + } + return fmt.Errorf("unsupported ALU operation: %+v op 0x%x", cbpf, op) case bpfClassJmp: var srcR asm.Reg @@ -363,8 +472,10 @@ var jumpOpNegate = map[asm.OpCode]asm.OpCode{ const ( bpfClassLd uint8 = 0x0 bpfClassLdx uint8 = 0x1 - bpfClassJmp uint8 = 0x5 + bpfClassSt uint8 = 0x2 + bpfClassStx uint8 = 0x3 bpfClassAlu uint8 = 0x4 + bpfClassJmp uint8 = 0x5 bpfClassRet uint8 = 0x6 bpfClassMisc uint8 = 0x7 )