Skip to content
Draft
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
150 changes: 149 additions & 1 deletion felix/bpf/filter/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -258,6 +266,14 @@ 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 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:
Expand All @@ -279,7 +295,117 @@ 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 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
Expand Down Expand Up @@ -346,6 +472,9 @@ var jumpOpNegate = map[asm.OpCode]asm.OpCode{
const (
bpfClassLd uint8 = 0x0
bpfClassLdx uint8 = 0x1
bpfClassSt uint8 = 0x2
bpfClassStx uint8 = 0x3
bpfClassAlu uint8 = 0x4
bpfClassJmp uint8 = 0x5
bpfClassRet uint8 = 0x6
bpfClassMisc uint8 = 0x7
Expand Down Expand Up @@ -379,6 +508,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
Comment on lines +512 to +522
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot implement classic bpf to ebpf instruction conversion in function cBPF2eBPF in the class bpfClassAlu branch

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in commit 53d385f. Added conversion for all 11 ALU operations (add, sub, mul, div, or, and, lsh, rsh, neg, mod, xor) with support for both immediate (K) and register (X) source operands. Also implemented scratch memory operations (ST, STX, LD MEM, LDX MEM) using stack-based emulation.

The protochain test still fails due to BPF verifier rejecting loop structures - this is a known limitation when converting classic BPF loops to eBPF.

)

var (
_ = bpfModeIMM
_ = bpfModeMEM
Expand All @@ -392,6 +535,11 @@ func bpfOp(code uint8) uint8 {
return code & 0xf0
}

const (
bpfTAX uint8 = 0x00
bpfTXA uint8 = 0x80
)

const (
bpfK uint8 = 0
bpfX uint8 = 0x8
Expand Down
117 changes: 117 additions & 0 deletions felix/bpf/ut/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,120 @@ 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},
{"ip6 protochain 17", true},
{"ip6 protochain 6", 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, 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())
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, 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())
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))
})
}
}