├── compile_static.go ├── go.mod ├── README.md ├── .gitignore ├── elibpcap.go ├── options.go ├── go.sum ├── adjust.go ├── compile.go ├── bpf_probe_read_kernel.go └── bpf_skb_load_bytes.go /compile_static.go: -------------------------------------------------------------------------------- 1 | // +build static !dynamic 2 | 3 | package elibpcap 4 | 5 | 6 | /* 7 | #cgo LDFLAGS: -L/usr/local/lib -lpcap -static 8 | #include 9 | #include 10 | */ 11 | import "C" 12 | 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jschwinger233/elibpcap 2 | 3 | go 1.21.0 4 | 5 | require ( 6 | github.com/cilium/ebpf v0.11.0 7 | github.com/cloudflare/cbpfc v0.0.0-20230809125630-31aa294050ff 8 | golang.org/x/net v0.13.0 9 | ) 10 | 11 | require ( 12 | github.com/pkg/errors v0.9.1 // indirect 13 | golang.org/x/sys v0.10.0 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elibpcap 2 | 3 | Projects that use elibpcap: 4 | 5 | - [eCapture](https://github.com/gojue/ecapture) 6 | - [ptcpdump](https://github.com/mozillazg/ptcpdump) 7 | - [skbdist](https://github.com/Asphaltt/skbdist) 8 | - [bpfsnoop](https://github.com/bpfsnoop/bpfsnoop) 9 | - [skbdump](https://github.com/jschwinger233/skbdump) 10 | 11 | # Usage 12 | 13 | https://mozillazg.com/2025/05/ebpf-let-any-network-ebpf-programs-to-support-pcap-filter-with-elibpcap-en.html 14 | 15 | Many thanks to [@mozillazg](https://github.com/mozillazg) for the blog post. 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | -------------------------------------------------------------------------------- /elibpcap.go: -------------------------------------------------------------------------------- 1 | package elibpcap 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/cilium/ebpf/asm" 8 | ) 9 | 10 | func Inject(filter string, insns asm.Instructions, opts Options) (_ asm.Instructions, err error) { 11 | if filter == "" { 12 | return insns, nil 13 | } 14 | 15 | injectIdx := -1 16 | for idx, inst := range insns { 17 | if inst.Symbol() == opts.AtBpf2Bpf { 18 | injectIdx = idx 19 | break 20 | } 21 | } 22 | if injectIdx == -1 { 23 | return insns, fmt.Errorf("Cannot find bpf2bpf: %s", opts.AtBpf2Bpf) 24 | } 25 | 26 | filterInsns, err := CompileEbpf(filter, opts) 27 | if err != nil { 28 | return insns, err 29 | } 30 | 31 | if opts.Debug { 32 | log.Printf("injecting pcap filter: %q at %d, instructions: \n%s", 33 | filter, injectIdx, filterInsns) 34 | } 35 | 36 | filterInsns[0] = filterInsns[0].WithMetadata(insns[injectIdx].Metadata) 37 | insns[injectIdx] = insns[injectIdx].WithMetadata(asm.Metadata{}) 38 | return append(insns[:injectIdx], 39 | append(filterInsns, insns[injectIdx:]...)..., 40 | ), nil 41 | } 42 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package elibpcap 2 | 3 | import "github.com/cilium/ebpf/asm" 4 | 5 | type PacketAccessMode int 6 | 7 | const ( 8 | // Direct indicates if the injected bpf program should use "direct packet access" or not. 9 | // See https://docs.kernel.org/bpf/verifier.html#direct-packet-access 10 | Direct PacketAccessMode = iota 11 | // BpfProbeReadKernel indicates if the injected bpf program should use bpf_probe_read_kernel to read packet. 12 | BpfProbeReadKernel 13 | // BpfSkbLoadBytes indicates if the injected bpf program should use bpf_skb_load_bytes to read packet. 14 | BpfSkbLoadBytes 15 | ) 16 | 17 | type Options struct { 18 | // AtBpf2Bpf is the label where the bpf2bpf call is injected. 19 | // The rejection position requires the function to be declared as: 20 | // 21 | // static __noinline bool 22 | // filter_pcap_ebpf(void *_skb, void *__skb, void *___skb, void *data, void* data_end) 23 | // 24 | // In this case, AtBpf2Bpf is the name of the function: filter_pcap_ebpf 25 | AtBpf2Bpf string 26 | 27 | // PacketAccessMode indicates how the injected bpf program should access the packet data. 28 | PacketAccessMode PacketAccessMode 29 | 30 | // L2Skb indicates if the injected bpf program should use L2 skb or not. 31 | // The L2 skb is the one that contains the ethernet header, while the L3 skb->data points to the IP header. 32 | L2Skb bool 33 | 34 | Debug bool 35 | } 36 | 37 | func (o Options) resultLabel() string { 38 | return "_result_" + o.AtBpf2Bpf 39 | } 40 | 41 | func (o Options) labelPrefix() string { 42 | return "_prefix_" + o.AtBpf2Bpf 43 | } 44 | 45 | func (o Options) result() asm.Register { 46 | return asm.R0 47 | } 48 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cilium/ebpf v0.11.0 h1:V8gS/bTCCjX9uUnkUFUpPsksM8n1lXBAvHcpiFk1X2Y= 2 | github.com/cilium/ebpf v0.11.0/go.mod h1:WE7CZAnqOL2RouJ4f1uyNhqr2P4CCvXFIqdRDUgWsVs= 3 | github.com/cloudflare/cbpfc v0.0.0-20230809125630-31aa294050ff h1:SLLG1soGN/PYTXkYWiR1PAxWlP1URBvgZPYymC5+0WI= 4 | github.com/cloudflare/cbpfc v0.0.0-20230809125630-31aa294050ff/go.mod h1:1xLQNNpgw8yNlVjGrtUHBx0KL52fif71sKa1PleulAQ= 5 | github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= 6 | github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 7 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 8 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 9 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 10 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 11 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 12 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 13 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 14 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 15 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 16 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 17 | golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI= 18 | golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 19 | golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY= 20 | golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= 21 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 22 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 23 | -------------------------------------------------------------------------------- /adjust.go: -------------------------------------------------------------------------------- 1 | package elibpcap 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cilium/ebpf/asm" 7 | ) 8 | 9 | const ( 10 | // Negative offsets from RFP (Frame Pointer R10) 11 | BpfDataReadOffset StackOffset = -8 * (iota + 1) // -8: Temporary buffer to store data read by bpf_skb_load_bytes 12 | R1LiveSavedOffset // -16: Slot to save the live value of R1 before a helper function call 13 | R2LiveSavedOffset // -24: Slot to save the live value of R2 before a helper function call 14 | R3LiveSavedOffset // -32: Slot to save the live value of R3 before a helper function call 15 | 16 | // These slots are used for values that are present at the entry of the cbpfc-generated code block 17 | // or are set up by it. They are saved once at the beginning of the adjusted eBPF code block 18 | // if needed by helper function calls or for restoration. 19 | PacketStartSavedOnStack // -40: Slot to save R4 (data/PacketStart), saved at the beginning of the adjusted eBPF code block. 20 | PacketEndSavedOnStack // -48: Slot to save R5 (data_end/PacketEnd), saved at the beginning of the adjusted eBPF code block. 21 | 22 | // Slot to store the original _skb argument (R1) of the eBPF filter function. 23 | SkbPtrOriginalArgSlot // -56: Slot to save the original R1 (_skb pointer). 24 | 25 | // AvailableOffset defines the start of the stack space that cbpfc can use (deepest known negative offset). 26 | // cbpfc.EBPFOpts.StackOffset will be calculated based on this. 27 | AvailableOffset // -64 (this value itself is negative, representing the size of the stack frame above cbpfc's own usage) 28 | ) 29 | 30 | /* 31 | If PacketAccessMode != Direct, We have to adjust the ebpf instructions because verifier prevents us from 32 | directly loading data from memory. 33 | */ 34 | func adjustEbpf(insts asm.Instructions, opts Options) (newInsts asm.Instructions, err error) { 35 | switch opts.PacketAccessMode { 36 | case BpfProbeReadKernel: 37 | insts, err = adjustEbpfWithBpfProbeReadKernel(insts, opts) 38 | if err != nil { 39 | return nil, err 40 | } 41 | break 42 | case BpfSkbLoadBytes: 43 | insts, err = adjustEbpfWithBpfSkbLoadBytes(insts, opts) 44 | if err != nil { 45 | return nil, err 46 | } 47 | break 48 | case Direct: 49 | break 50 | default: 51 | return nil, fmt.Errorf("unsupported packet access mode: %v", opts.PacketAccessMode) 52 | } 53 | 54 | return append(insts, 55 | asm.Mov.Imm(asm.R1, 0).WithSymbol(opts.resultLabel()), // r1 = 0 (_skb) 56 | asm.Mov.Imm(asm.R2, 0), // r2 = 0 (__skb) 57 | asm.Mov.Imm(asm.R3, 0), // r3 = 0 (___skb) 58 | asm.Mov.Reg(asm.R4, opts.result()), // r4 = $result (data) 59 | asm.Mov.Imm(asm.R5, 0), // r5 = 0 (data_end) 60 | ), nil 61 | } 62 | -------------------------------------------------------------------------------- /compile.go: -------------------------------------------------------------------------------- 1 | package elibpcap 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "unsafe" 7 | 8 | "github.com/cilium/ebpf/asm" 9 | "github.com/cloudflare/cbpfc" 10 | "golang.org/x/net/bpf" 11 | ) 12 | 13 | /* 14 | #cgo LDFLAGS: -L/usr/local/lib -lpcap 15 | #include 16 | #include 17 | */ 18 | import "C" 19 | 20 | type pcapBpfProgram C.struct_bpf_program 21 | 22 | const ( 23 | MaxBpfInstructions = 4096 24 | bpfInstructionBufferSize = 8 * MaxBpfInstructions 25 | MAXIMUM_SNAPLEN = 262144 26 | 27 | RejectAllExpr = "__reject_all__" 28 | ) 29 | 30 | type StackOffset int 31 | 32 | /* 33 | Steps: 34 | 1. Compile pcap expresion to cbpf using libpcap 35 | 2. Convert cbpf to ebpf using cloudflare/cbpfc 36 | 3. [!DirectRead] Convert direct memory load to bpf_probe_read_kernel call 37 | */ 38 | func CompileEbpf(expr string, opts Options) (insts asm.Instructions, err error) { 39 | if expr == RejectAllExpr { 40 | return asm.Instructions{ 41 | asm.Mov.Reg(asm.R4, asm.R5), // r4 = r5 (data = data_end) 42 | }, nil 43 | } 44 | cbpfInsts, err := CompileCbpf(expr, opts.L2Skb) 45 | if err != nil { 46 | return 47 | } 48 | 49 | ebpfInsts, err := cbpfc.ToEBPF(cbpfInsts, cbpfc.EBPFOpts{ 50 | // skb->data is at r4, skb->data_end is at r5. 51 | PacketStart: asm.R4, 52 | PacketEnd: asm.R5, 53 | Result: opts.result(), 54 | ResultLabel: opts.resultLabel(), 55 | // _skb is at R0, __skb is at R1, ___skb is at R2. 56 | Working: [4]asm.Register{asm.R0, asm.R1, asm.R2, asm.R3}, 57 | LabelPrefix: opts.labelPrefix(), 58 | StackOffset: -int(AvailableOffset), 59 | }) 60 | if err != nil { 61 | return 62 | } 63 | 64 | if opts.Debug { 65 | log.Printf("original eBPF from cbpfc.ToEBPF for %q:\n%s", expr, ebpfInsts) 66 | } 67 | 68 | return adjustEbpf(ebpfInsts, opts) 69 | } 70 | 71 | func CompileCbpf(expr string, l2 bool) (insts []bpf.Instruction, err error) { 72 | if len(expr) == 0 { 73 | return 74 | } 75 | 76 | pcapType := C.DLT_RAW 77 | if l2 { 78 | pcapType = C.DLT_EN10MB 79 | } 80 | pcap := C.pcap_open_dead(C.int(pcapType), MAXIMUM_SNAPLEN) 81 | if pcap == nil { 82 | return nil, fmt.Errorf("failed to pcap_open_dead: %+v\n", C.PCAP_ERROR) 83 | } 84 | defer C.pcap_close(pcap) 85 | 86 | cexpr := C.CString(expr) 87 | defer C.free(unsafe.Pointer(cexpr)) 88 | 89 | var bpfProg pcapBpfProgram 90 | if C.pcap_compile(pcap, (*C.struct_bpf_program)(&bpfProg), cexpr, 1, C.PCAP_NETMASK_UNKNOWN) < 0 { 91 | return nil, fmt.Errorf("failed to pcap_compile '%s': %+v", expr, C.GoString(C.pcap_geterr(pcap))) 92 | } 93 | defer C.pcap_freecode((*C.struct_bpf_program)(&bpfProg)) 94 | 95 | for _, v := range (*[bpfInstructionBufferSize]C.struct_bpf_insn)(unsafe.Pointer(bpfProg.bf_insns))[0:bpfProg.bf_len:bpfProg.bf_len] { 96 | insts = append(insts, bpf.RawInstruction{ 97 | Op: uint16(v.code), 98 | Jt: uint8(v.jt), 99 | Jf: uint8(v.jf), 100 | K: uint32(v.k), 101 | }.Disassemble()) 102 | } 103 | return 104 | } 105 | -------------------------------------------------------------------------------- /bpf_probe_read_kernel.go: -------------------------------------------------------------------------------- 1 | package elibpcap 2 | 3 | import "github.com/cilium/ebpf/asm" 4 | 5 | /* 6 | If PacketAccessMode == BpfProbeReadKernel, We have to adjust the ebpf instructions because verifier prevents us from 7 | directly loading data from memory. For example, the instruction "r0 = *(u8 *)(r4 +0)" 8 | will break verifier with error "R4 invalid mem access 'scalar", we therefore 9 | need to convert this direct memory load to bpf_probe_read_kernel function call: 10 | 11 | - r1 = r10 // r10 is stack top 12 | - r1 += -8 // r1 = r10-8 13 | - r2 = 1 // r2 = sizeof(u8) 14 | - r3 = r4 // r4 is start of packet data, aka L3 header 15 | - r3 += 0 // r3 = r4+0 16 | - call bpf_probe_read_kernel // *(r10-8) = *(u8 *)(r4+0) 17 | - r0 = *(u8 *)(r10 -8) // r0 = *(r10-8) 18 | 19 | To safely borrow R1, R2 and R3 for setting up the arguments for 20 | bpf_probe_read_kernel(), we need to save the original values of R1, R2 and R3 21 | on stack, and restore them after the function call. 22 | */ 23 | func adjustEbpfWithBpfProbeReadKernel(insts asm.Instructions, opts Options) (newInsts asm.Instructions, err error) { 24 | replaceIdx := []int{} 25 | replaceInsts := map[int]asm.Instructions{} 26 | for idx, inst := range insts { 27 | if inst.OpCode.Class().IsLoad() { 28 | replaceIdx = append(replaceIdx, idx) 29 | replaceInsts[idx] = append(replaceInsts[idx], 30 | 31 | // Store R1, R2, R3 on stack. 32 | asm.StoreMem(asm.RFP, int16(R1LiveSavedOffset), asm.R1, asm.DWord), 33 | asm.StoreMem(asm.RFP, int16(R2LiveSavedOffset), asm.R2, asm.DWord), 34 | asm.StoreMem(asm.RFP, int16(R3LiveSavedOffset), asm.R3, asm.DWord), 35 | 36 | // bpf_probe_read_kernel(RFP-8, size, inst.Src) 37 | asm.Mov.Reg(asm.R1, asm.RFP), 38 | asm.Add.Imm(asm.R1, int32(BpfDataReadOffset)), 39 | asm.Mov.Imm(asm.R2, int32(inst.OpCode.Size().Sizeof())), 40 | asm.Mov.Reg(asm.R3, inst.Src), 41 | asm.Add.Imm(asm.R3, int32(inst.Offset)), 42 | asm.FnProbeReadKernel.Call(), 43 | 44 | // inst.Dst = *(RFP-8) 45 | asm.LoadMem(inst.Dst, asm.RFP, int16(BpfDataReadOffset), inst.OpCode.Size()), 46 | 47 | // Restore R4, R5 from stack. This is needed because bpf_probe_read_kernel always resets R4 and R5 even if they are not used by bpf_probe_read_kernel. 48 | asm.LoadMem(asm.R4, asm.RFP, int16(PacketStartSavedOnStack), asm.DWord), 49 | asm.LoadMem(asm.R5, asm.RFP, int16(PacketEndSavedOnStack), asm.DWord), 50 | ) 51 | 52 | // Restore R1, R2, R3 from stack 53 | restoreInsts := asm.Instructions{ 54 | asm.LoadMem(asm.R1, asm.RFP, int16(R1LiveSavedOffset), asm.DWord), 55 | asm.LoadMem(asm.R2, asm.RFP, int16(R2LiveSavedOffset), asm.DWord), 56 | asm.LoadMem(asm.R3, asm.RFP, int16(R3LiveSavedOffset), asm.DWord), 57 | } 58 | 59 | switch inst.Dst { 60 | case asm.R1, asm.R2, asm.R3: 61 | restoreInsts = append(restoreInsts[:inst.Dst-1], restoreInsts[inst.Dst:]...) 62 | } 63 | 64 | replaceInsts[idx] = append(replaceInsts[idx], restoreInsts...) 65 | 66 | // Metadata is crucial for adjusting jump offsets. We 67 | // ditched original instructions, which could hold symbol 68 | // names targeted by other jump instructions, so here we 69 | // inherit the metadata from the ditched ones. 70 | replaceInsts[idx][0].Metadata = inst.Metadata 71 | } 72 | } 73 | 74 | // Replace the memory load instructions with the new ones 75 | for i := len(replaceIdx) - 1; i >= 0; i-- { 76 | idx := replaceIdx[i] 77 | insts = append(insts[:idx], append(replaceInsts[idx], insts[idx+1:]...)...) 78 | } 79 | 80 | // Store R4, R5 on stack. 81 | insts = append([]asm.Instruction{ 82 | asm.StoreMem(asm.RFP, int16(PacketStartSavedOnStack), asm.R4, asm.DWord), 83 | asm.StoreMem(asm.RFP, int16(PacketEndSavedOnStack), asm.R5, asm.DWord), 84 | }, insts...) 85 | return insts, err 86 | } 87 | -------------------------------------------------------------------------------- /bpf_skb_load_bytes.go: -------------------------------------------------------------------------------- 1 | package elibpcap 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cilium/ebpf/asm" 7 | ) 8 | 9 | /* 10 | If PacketAccessMode == BpfSkbLoadBytes, We have to adjust the ebpf instructions because verifier prevents us from 11 | directly loading data from memory using raw pointers (except for specific cases like TC BPF direct packet access). 12 | For example, the instruction "r0 = *(u8 *)(r4 +0)" (where R4 is skb->data) 13 | will be converted to use bpf_skb_load_bytes(skb, offset, buffer, size). 14 | 15 | The conversion involves: 16 | 1. At the beginning of the filtered block, save the original R1 (sk_buff*), R4 (data), and R5 (data_end) to stack slots. 17 | 2. For each memory load instruction (`inst`): 18 | a. Save live registers (R1-R4) that will be clobbered by arguments setup for bpf_skb_load_bytes or by the call itself. 19 | b. Setup arguments for bpf_skb_load_bytes: 20 | - R1 (arg1): original sk_buff pointer (from SkbPtrOriginalArgSlot). 21 | - R2 (arg2): offset relative to skb->data. 22 | - If inst.Src is PacketStart (R4): offset = inst.Offset. 23 | - If inst.Src is a dynamic pointer P (e.g., R3 holding skb->data + L4_offset): 24 | offset = (P_value - PacketStart_original_value) + inst.Offset. 25 | - R3 (arg3): pointer to a temporary buffer on stack (e.g., RFP + BpfDataReadOffset). 26 | - R4 (arg4): size of data to read (from the load instruction's size). 27 | c. Call bpf_skb_load_bytes. 28 | d. Check R0 (return value). If error, jump to nomatch. 29 | e. Load the data from the temporary buffer on stack into the original destination register (inst.Dst). 30 | f. Restore R4 (data/PacketStart) and R5 (data_end/PacketEnd) from their saved slots on stack. 31 | g. Restore other saved live registers (R1-R3), taking care not to overwrite inst.Dst. 32 | */ 33 | func adjustEbpfWithBpfSkbLoadBytes(insts asm.Instructions, opts Options) (newInsts asm.Instructions, err error) { 34 | // If !DirectRead, prepend instructions to save critical initial registers 35 | // These are assumed to be R1 (_skb), R4 (data), R5 (data_end) at the entry of the filter function code. 36 | var prefixInsts asm.Instructions 37 | prefixInsts = asm.Instructions{ 38 | // Save original R1 (which is _skb, the first argument to the eBPF program/filter function) 39 | asm.StoreMem(asm.RFP, int16(SkbPtrOriginalArgSlot), asm.R1, asm.DWord), 40 | // Save R4 (data pointer / PacketStart) and R5 (data_end pointer / PacketEnd) 41 | // These are used by cBPF translated code and need to be restored after helper calls. 42 | asm.StoreMem(asm.RFP, int16(PacketStartSavedOnStack), asm.R4, asm.DWord), 43 | asm.StoreMem(asm.RFP, int16(PacketEndSavedOnStack), asm.R5, asm.DWord), 44 | } 45 | 46 | // tempReplaceIdx stores the indices of the original load instructions in `insts` that need replacement. 47 | tempReplaceIdx := []int{} 48 | // tempReplaceInstsMap maps the original index of a load instruction to its replacement instructions. 49 | tempReplaceInstsMap := map[int]asm.Instructions{} 50 | 51 | // cbpfc.EBPFOpts.PacketStart is hardcoded to R4 in CompileEbpf. 52 | cbpfPacketStartReg := asm.R4 53 | 54 | for originalIdx, inst := range insts { 55 | if inst.OpCode.Class().IsLoad() { 56 | tempReplaceIdx = append(tempReplaceIdx, originalIdx) 57 | 58 | var currentReplacement asm.Instructions 59 | 60 | // Save live R1, R2, R3, R4 before setting up args for bpf_skb_load_bytes. 61 | currentReplacement = append(currentReplacement, 62 | asm.StoreMem(asm.RFP, int16(R1LiveSavedOffset), asm.R1, asm.DWord), 63 | asm.StoreMem(asm.RFP, int16(R2LiveSavedOffset), asm.R2, asm.DWord), 64 | asm.StoreMem(asm.RFP, int16(R3LiveSavedOffset), asm.R3, asm.DWord), 65 | ) 66 | 67 | // --- Setup arguments for bpf_skb_load_bytes --- 68 | // R1 (arg1): original sk_buff pointer (from SkbPtrOriginalArgSlot) 69 | currentReplacement = append(currentReplacement, 70 | asm.LoadMem(asm.R1, asm.RFP, int16(SkbPtrOriginalArgSlot), asm.DWord), 71 | ) 72 | 73 | // R2 (arg2): offset_from_skb_data 74 | if inst.Src == cbpfPacketStartReg { 75 | // Case 1: Original eBPF load was relative to PacketStart (R4). 76 | // inst.Offset is the direct offset from skb->data. 77 | currentReplacement = append(currentReplacement, 78 | asm.Mov.Imm(asm.R2, int32(inst.Offset)), 79 | ) 80 | } else { 81 | // Case 2: Original eBPF load used a source register (inst.Src, e.g., R0-R3) 82 | // that holds an absolute pointer to the data to be loaded, 83 | // and inst.Offset is relative to that absolute pointer. 84 | // We need to calculate offset_for_skb_load_bytes = 85 | // (value_in_inst.Src_reg - value_of_PacketStart_at_entry) + inst.Offset. 86 | 87 | // Step A: Copy the value from inst.Src register into R2. 88 | // inst.Src contains the absolute pointer calculated by cbpfc. 89 | // Need to ensure this Mov doesn't conflict if inst.Src is R2 itself. 90 | // If inst.Src is R2, R2 already has the correct base pointer. 91 | // If inst.Src is R0, R1, or R3, copy it to R2. 92 | switch inst.Src { 93 | case asm.R0, asm.R1, asm.R3: 94 | currentReplacement = append(currentReplacement, 95 | asm.Mov.Reg(asm.R2, inst.Src), // R2 = absolute_pointer_base 96 | ) 97 | case asm.R2: 98 | // R2 already holds the absolute_pointer_base, no Mov needed. 99 | default: 100 | // This case should ideally not be reached if cbpfc uses R0-R3 as working regs 101 | // and R4 as PacketStart for its load instructions. 102 | return nil, fmt.Errorf("adjustEbpf: unhandled inst.Src (%v) for dynamic pointer calculation", inst.Src) 103 | } 104 | 105 | // Step B: Load original PacketStart value (skb->data pointer at entry) into a temporary register. 106 | // R0 is suitable as it's clobbered by the helper call / used for return value. 107 | currentReplacement = append(currentReplacement, 108 | asm.LoadMem(asm.R0, asm.RFP, int16(PacketStartSavedOnStack), asm.DWord), // R0 = original PacketStart value 109 | ) 110 | 111 | // Step C: R2 = R2 - R0 (i.e., absolute_pointer_base - original_PacketStart_value) 112 | // This results in offset_of_absolute_pointer_base_from_original_PacketStart. 113 | currentReplacement = append(currentReplacement, 114 | asm.Sub.Reg(asm.R2, asm.R0), 115 | ) 116 | 117 | // Step D: R2 = R2 + inst.Offset (add the offset relative to the absolute_pointer_base) 118 | // Now R2 holds the final offset relative to original PacketStart (skb->data). 119 | if inst.Offset != 0 { // Optimization: skip if inst.Offset is zero 120 | currentReplacement = append(currentReplacement, 121 | asm.Add.Imm(asm.R2, int32(inst.Offset)), 122 | ) 123 | } 124 | } 125 | 126 | // R3 (arg3): to_buf_on_stack (RFP + BpfDataReadOffset) 127 | currentReplacement = append(currentReplacement, 128 | asm.Mov.Reg(asm.R3, asm.RFP), 129 | asm.Add.Imm(asm.R3, int32(BpfDataReadOffset)), 130 | ) 131 | // R4 (arg4): len (from inst.OpCode.Size().Sizeof()) 132 | currentReplacement = append(currentReplacement, 133 | asm.Mov.Imm(asm.R4, int32(inst.OpCode.Size().Sizeof())), 134 | ) 135 | // --- End of arguments setup --- 136 | 137 | currentReplacement = append(currentReplacement, asm.FnSkbLoadBytes.Call()) 138 | 139 | // Check return value of bpf_skb_load_bytes (in R0) 140 | nomatchLabel := opts.labelPrefix() + "_nomatch" 141 | currentReplacement = append(currentReplacement, 142 | asm.JNE.Imm(asm.R0, 0, nomatchLabel).WithReference(nomatchLabel), 143 | ) 144 | 145 | // Load data from stack buffer to original inst.Dst 146 | currentReplacement = append(currentReplacement, 147 | asm.LoadMem(inst.Dst, asm.RFP, int16(BpfDataReadOffset), inst.OpCode.Size()), 148 | ) 149 | 150 | // Restore R4 (PacketStart) and R5 (PacketEnd) from their initially saved values 151 | currentReplacement = append(currentReplacement, 152 | asm.LoadMem(asm.R4, asm.RFP, int16(PacketStartSavedOnStack), asm.DWord), 153 | asm.LoadMem(asm.R5, asm.RFP, int16(PacketEndSavedOnStack), asm.DWord), 154 | ) 155 | 156 | // Restore original live R1, R2, R3 (unless inst.Dst was one of them) 157 | var restoreLiveRegs asm.Instructions 158 | if inst.Dst != asm.R1 { 159 | restoreLiveRegs = append(restoreLiveRegs, asm.LoadMem(asm.R1, asm.RFP, int16(R1LiveSavedOffset), asm.DWord)) 160 | } 161 | if inst.Dst != asm.R2 { 162 | // If R2 was inst.Src in the dynamic case, its value might have been changed by the offset calculation. 163 | // However, the R2LiveSavedOffset holds the value R2 had *before* this entire replacement block. 164 | // So, restoring from R2LiveSavedOffset is correct. 165 | restoreLiveRegs = append(restoreLiveRegs, asm.LoadMem(asm.R2, asm.RFP, int16(R2LiveSavedOffset), asm.DWord)) 166 | } 167 | if inst.Dst != asm.R3 { 168 | restoreLiveRegs = append(restoreLiveRegs, asm.LoadMem(asm.R3, asm.RFP, int16(R3LiveSavedOffset), asm.DWord)) 169 | } 170 | currentReplacement = append(currentReplacement, restoreLiveRegs...) 171 | 172 | currentReplacement[0].Metadata = inst.Metadata 173 | tempReplaceInstsMap[originalIdx] = currentReplacement 174 | } 175 | } 176 | 177 | finalProcessedInsts := make(asm.Instructions, 0, len(insts)+len(tempReplaceIdx)*15) // Rough estimate 178 | nextOriginalIdxToProcess := 0 179 | for _, originalIdxToReplace := range tempReplaceIdx { 180 | finalProcessedInsts = append(finalProcessedInsts, insts[nextOriginalIdxToProcess:originalIdxToReplace]...) 181 | finalProcessedInsts = append(finalProcessedInsts, tempReplaceInstsMap[originalIdxToReplace]...) 182 | nextOriginalIdxToProcess = originalIdxToReplace + 1 183 | } 184 | finalProcessedInsts = append(finalProcessedInsts, insts[nextOriginalIdxToProcess:]...) 185 | 186 | resultWithPrefix := append(prefixInsts, finalProcessedInsts...) 187 | 188 | return resultWithPrefix, nil 189 | } 190 | --------------------------------------------------------------------------------