LightBPF 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
LightBPF/Compiler.py ADDED
@@ -0,0 +1,1300 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ # file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+
5
+ """
6
+ LightBPF Compiler: Compiles AST from BpfParser into classic BPF (cBPF) bytecode.
7
+ Fully compatible with all AST nodes, operators, and constructs generated by
8
+ LightBPF.Parser and LightBPF.Lexer.
9
+ """
10
+
11
+ from dataclasses import dataclass
12
+ import struct
13
+ import socket
14
+ import ipaddress
15
+ import ctypes
16
+ from typing import List, Optional, Tuple, Dict, Any, Union
17
+
18
+ try:
19
+ from LightPacket.LightBPF.Lexer import BpfLexer
20
+ from LightPacket.LightBPF.Parser import (
21
+ BpfParser, ASTNode, BinaryOpNode, UnaryOpNode, ProtocolNode,
22
+ KeywordFilterNode, ByteAccessNode, LengthNode, NumberNode,
23
+ LiteralNode, FieldNode
24
+ )
25
+ except ImportError:
26
+ from LightBPF.Lexer import BpfLexer
27
+ from LightBPF.Parser import (
28
+ BpfParser, ASTNode, BinaryOpNode, UnaryOpNode, ProtocolNode,
29
+ KeywordFilterNode, ByteAccessNode, LengthNode, NumberNode,
30
+ LiteralNode, FieldNode
31
+ )
32
+
33
+ # ----------------------------------------------------------------------
34
+ # Standard Classic BPF (cBPF) Constants & Opcodes
35
+ # ----------------------------------------------------------------------
36
+
37
+ # Instruction Classes (code & 0x07)
38
+ BPF_LD = 0x00
39
+ BPF_LDX = 0x01
40
+ BPF_ST = 0x02
41
+ BPF_STX = 0x03
42
+ BPF_ALU = 0x04
43
+ BPF_JMP = 0x05
44
+ BPF_RET = 0x06
45
+ BPF_MISC = 0x07
46
+
47
+ # Size Modifiers for LD / LDX (code & 0x18)
48
+ BPF_W = 0x00 # 32 bits (4 bytes)
49
+ BPF_H = 0x08 # 16 bits (2 bytes)
50
+ BPF_B = 0x10 # 8 bits (1 byte)
51
+
52
+ # Addressing Modes for LD / LDX (code & 0xe0)
53
+ BPF_IMM = 0x00 # Immediate value
54
+ BPF_ABS = 0x20 # Absolute packet offset
55
+ BPF_IND = 0x40 # Packet offset indexed by Register X
56
+ BPF_MEM = 0x60 # Scratch memory M[k]
57
+ BPF_LEN = 0x80 # Packet length
58
+ BPF_MSH = 0xa0 # 4 * (packet[k] & 0x0f) -> loaded into X (IPv4 IHL)
59
+
60
+ # ALU Operations (code & 0xf0)
61
+ BPF_ADD = 0x00
62
+ BPF_SUB = 0x10
63
+ BPF_MUL = 0x20
64
+ BPF_DIV = 0x30
65
+ BPF_OR = 0x40
66
+ BPF_AND = 0x50
67
+ BPF_LSH = 0x60
68
+ BPF_RSH = 0x70
69
+ BPF_NEG = 0x80
70
+ BPF_MOD = 0x90
71
+ BPF_XOR = 0xa0
72
+
73
+ # Jump Operations (code & 0xf0)
74
+ BPF_JA = 0x00
75
+ BPF_JEQ = 0x10
76
+ BPF_JGT = 0x20
77
+ BPF_JGE = 0x30
78
+ BPF_JSET = 0x40
79
+
80
+ # Source Operand (code & 0x08)
81
+ BPF_K = 0x00 # Constant / Immediate
82
+ BPF_X = 0x08 # Register X
83
+
84
+ # Misc Operations (code & 0xf8)
85
+ BPF_TAX = 0x00 # Register X = Register A
86
+ BPF_TXA = 0x80 # Register A = Register X
87
+
88
+ # Return operand mode
89
+ BPF_A = 0x10
90
+
91
+ # Snaplen / Accept length
92
+ BPF_MAX_BUF_LEN = 0x40000 # 262144 bytes
93
+
94
+ # Well-known Protocol EtherTypes
95
+ ETHERTYPE_MAP = {
96
+ "ip": 0x0800,
97
+ "ip4": 0x0800,
98
+ "ipv4": 0x0800,
99
+ "ip6": 0x86DD,
100
+ "ipv6": 0x86DD,
101
+ "arp": 0x0806,
102
+ "rarp": 0x8035,
103
+ "vlan": 0x8100,
104
+ "mpls": 0x8847,
105
+ "pppoed": 0x8863,
106
+ "pppoes": 0x8864,
107
+ }
108
+
109
+ # Standard Services Port Map
110
+ COMMON_SERVICES: Dict[str, int] = {
111
+ "echo": 7,
112
+ "discard": 9,
113
+ "ftp-data": 20,
114
+ "ftp": 21,
115
+ "ssh": 22,
116
+ "telnet": 23,
117
+ "smtp": 25,
118
+ "time": 37,
119
+ "nameserver": 42,
120
+ "nicname": 43,
121
+ "domain": 53,
122
+ "dns": 53,
123
+ "bootps": 67,
124
+ "bootpc": 68,
125
+ "tftp": 69,
126
+ "gopher": 70,
127
+ "finger": 79,
128
+ "http": 80,
129
+ "www": 80,
130
+ "kerberos": 88,
131
+ "pop2": 109,
132
+ "pop3": 110,
133
+ "sunrpc": 111,
134
+ "auth": 113,
135
+ "sftp": 115,
136
+ "nntp": 119,
137
+ "ntp": 123,
138
+ "netbios-ns": 137,
139
+ "netbios-dgm": 138,
140
+ "netbios-ssn": 139,
141
+ "imap": 143,
142
+ "snmp": 161,
143
+ "snmptrap": 162,
144
+ "bgp": 179,
145
+ "ldap": 389,
146
+ "https": 443,
147
+ "microsoft-ds": 445,
148
+ "ldaps": 636,
149
+ "rsync": 873,
150
+ "imaps": 993,
151
+ "pop3s": 995,
152
+ "openvpn": 1194,
153
+ "mysql": 3306,
154
+ "rdp": 3389,
155
+ "postgresql": 5432,
156
+ "http-alt": 8080,
157
+ "squid": 3128,
158
+ }
159
+
160
+ # Pseudo-field byte offsets and sizes
161
+ FIELD_OFFSETS: Dict[str, Tuple[int, int]] = {
162
+ "tcpflags": (13, 1),
163
+ "icmptype": (0, 1),
164
+ "icmpcode": (1, 1),
165
+ "icmp6type": (0, 1),
166
+ "icmp6code": (1, 1),
167
+ }
168
+
169
+
170
+ # ----------------------------------------------------------------------
171
+ # Instruction Representation
172
+ # ----------------------------------------------------------------------
173
+ @dataclass
174
+ class Instruction:
175
+ code: int
176
+ jt: Union[int, str]
177
+ jf: Union[int, str]
178
+ k: Union[int, str]
179
+
180
+ def to_bytes(self) -> bytes:
181
+ """Pack into 8-byte Linux/BSD struct bpf_insn format."""
182
+ jt_val = self.jt if isinstance(self.jt, int) else 0
183
+ jf_val = self.jf if isinstance(self.jf, int) else 0
184
+ k_val = self.k if isinstance(self.k, int) else 0
185
+ return struct.pack("HBBI", self.code, jt_val, jf_val, k_val & 0xFFFFFFFF)
186
+
187
+ def __repr__(self) -> str:
188
+ return (
189
+ f"Instruction(code=0x{self.code:02x}, "
190
+ f"jt={self.jt}, jf={self.jf}, k=0x{self.k:x if isinstance(self.k, int) else self.k})"
191
+ )
192
+
193
+
194
+ class bpf_insn(ctypes.Structure):
195
+ _fields_ = [
196
+ ("code", ctypes.c_uint16),
197
+ ("jt", ctypes.c_uint8),
198
+ ("jf", ctypes.c_uint8),
199
+ ("k", ctypes.c_uint32),
200
+ ]
201
+
202
+
203
+ class sock_fprog(ctypes.Structure):
204
+ _fields_ = [
205
+ ("len", ctypes.c_ushort),
206
+ ("filter", ctypes.POINTER(bpf_insn)),
207
+ ]
208
+
209
+
210
+ # ----------------------------------------------------------------------
211
+ # Compiler Core
212
+ # ----------------------------------------------------------------------
213
+ class BpfCompiler:
214
+ """
215
+ Classic BPF (cBPF) Bytecode Compiler.
216
+ Translates AST from BpfParser into verified cBPF instructions.
217
+ """
218
+
219
+ def __init__(self):
220
+ self.instructions: List[Union[Instruction, str]] = []
221
+ self._label_counter = 0
222
+ self._scratch_counter = 0
223
+
224
+ def _new_label(self, prefix: str = "L") -> str:
225
+ self._label_counter += 1
226
+ return f"{prefix}_{self._label_counter}"
227
+
228
+ def _emit(self, instr: Instruction):
229
+ self.instructions.append(instr)
230
+
231
+ def _emit_label(self, label_name: str):
232
+ self.instructions.append(f"LABEL:{label_name}")
233
+
234
+ def compile(self, ast_root: ASTNode) -> List[Instruction]:
235
+ """Compile an AST into assembled BPF bytecode."""
236
+ self.instructions = []
237
+ self._label_counter = 0
238
+ self._scratch_counter = 0
239
+
240
+ true_label, false_label = "ACCEPT", "REJECT"
241
+
242
+ self._compile_bool(ast_root, true_label, false_label)
243
+
244
+ return self._assemble_and_backpatch(true_label, false_label)
245
+
246
+ @classmethod
247
+ def compile_str(cls, filter_string: str) -> List[Instruction]:
248
+ """Convenience method: Tokenize, parse, and compile a filter string."""
249
+ tokens = BpfLexer(filter_string).tokenize()
250
+ ast = BpfParser(tokens).parse()
251
+ return cls().compile(ast)
252
+
253
+ @staticmethod
254
+ def to_bytes(instructions: List[Instruction]) -> bytes:
255
+ """Convert a list of instructions to raw byte string."""
256
+ return b"".join(instr.to_bytes() for instr in instructions)
257
+
258
+ @staticmethod
259
+ def to_fprog(instructions: List[Instruction]) -> sock_fprog:
260
+ """Convert instruction list to ctypes sock_fprog structure."""
261
+ arr_type = bpf_insn * len(instructions)
262
+ insn_arr = arr_type()
263
+ for idx, instr in enumerate(instructions):
264
+ insn_arr[idx].code = instr.code
265
+ insn_arr[idx].jt = instr.jt if isinstance(instr.jt, int) else 0
266
+ insn_arr[idx].jf = instr.jf if isinstance(instr.jf, int) else 0
267
+ insn_arr[idx].k = (instr.k if isinstance(instr.k, int) else 0) & 0xFFFFFFFF
268
+ return sock_fprog(len(instructions), ctypes.cast(insn_arr, ctypes.POINTER(bpf_insn)))
269
+
270
+ # ------------------------------------------------------------------
271
+ # Boolean Expressions & Short-Circuit Evaluation
272
+ # ------------------------------------------------------------------
273
+ def _compile_bool(self, node: ASTNode, true_target: str, false_target: str):
274
+ if isinstance(node, BinaryOpNode):
275
+ if node.op == "and":
276
+ next_label = self._new_label("and")
277
+ self._compile_bool(node.left, next_label, false_target)
278
+ self._emit_label(next_label)
279
+ self._compile_bool(node.right, true_target, false_target)
280
+
281
+ elif node.op == "or":
282
+ next_label = self._new_label("or")
283
+ self._compile_bool(node.left, true_target, next_label)
284
+ self._emit_label(next_label)
285
+ self._compile_bool(node.right, true_target, false_target)
286
+
287
+ elif node.op in ("==", "!=", ">", ">=", "<", "<="):
288
+ self._compile_comparison(node, true_target, false_target)
289
+
290
+ elif node.op == "&":
291
+ # Bitwise test used as boolean: e.g. (tcp[13] & 2)
292
+ # Tests if (A & right) != 0
293
+ self._compile_bitwise_bool(node, true_target, false_target)
294
+
295
+ else:
296
+ # Standalone arithmetic expression: truthy if != 0
297
+ self._compile_value(node)
298
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, false_target, true_target, 0))
299
+
300
+ elif isinstance(node, UnaryOpNode) and node.op == "not":
301
+ self._compile_bool(node.expr, false_target, true_target)
302
+
303
+ elif isinstance(node, ProtocolNode):
304
+ self._compile_protocol(node, true_target, false_target)
305
+
306
+ elif isinstance(node, KeywordFilterNode):
307
+ self._compile_keyword(node, true_target, false_target)
308
+
309
+ elif isinstance(node, ByteAccessNode):
310
+ self._compile_value(node)
311
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, false_target, true_target, 0))
312
+
313
+ elif isinstance(node, (NumberNode, LengthNode, FieldNode)):
314
+ self._compile_value(node)
315
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, false_target, true_target, 0))
316
+
317
+ else:
318
+ raise NotImplementedError(f"Unsupported boolean AST node: {type(node).__name__}")
319
+
320
+ def _compile_bitwise_bool(self, node: BinaryOpNode, true_target: str, false_target: str):
321
+ """Compile bitwise & expression used directly in boolean position."""
322
+ if isinstance(node.right, NumberNode):
323
+ self._compile_value(node.left)
324
+ self._emit(Instruction(BPF_JMP | BPF_JSET | BPF_K, true_target, false_target, node.right.value))
325
+ else:
326
+ # Right side is complex
327
+ scratch = self._alloc_scratch()
328
+ self._compile_value(node.right)
329
+ self._emit(Instruction(BPF_ST, 0, 0, scratch))
330
+ self._compile_value(node.left)
331
+ self._emit(Instruction(BPF_LDX | BPF_MEM, 0, 0, scratch))
332
+ self._free_scratch()
333
+ self._emit(Instruction(BPF_JMP | BPF_JSET | BPF_X, true_target, false_target, 0))
334
+
335
+ def _compile_comparison(self, node: BinaryOpNode, true_target: str, false_target: str):
336
+ # Special optimization for idiomatic bitmask test: expr & MASK != 0 or expr & MASK == 0
337
+ if (isinstance(node.left, BinaryOpNode) and node.left.op == "&" and
338
+ isinstance(node.right, NumberNode) and node.right.value == 0 and
339
+ isinstance(node.left.right, NumberNode)):
340
+ mask = node.left.right.value
341
+ self._compile_value(node.left.left)
342
+ if node.op == "!=":
343
+ self._emit(Instruction(BPF_JMP | BPF_JSET | BPF_K, true_target, false_target, mask))
344
+ elif node.op == "==":
345
+ self._emit(Instruction(BPF_JMP | BPF_JSET | BPF_K, false_target, true_target, mask))
346
+ elif node.op in (">", ">="):
347
+ # Since A & mask >= 0 is always true (unsigned), > 0 means != 0
348
+ self._emit(Instruction(BPF_JMP | BPF_JSET | BPF_K, true_target, false_target, mask))
349
+ return
350
+
351
+ # Check if right operand is immediate constant
352
+ if isinstance(node.right, NumberNode):
353
+ self._compile_value(node.left)
354
+ val = node.right.value
355
+
356
+ if node.op == "==":
357
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, val))
358
+ elif node.op == "!=":
359
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, false_target, true_target, val))
360
+ elif node.op == ">":
361
+ self._emit(Instruction(BPF_JMP | BPF_JGT | BPF_K, true_target, false_target, val))
362
+ elif node.op == ">=":
363
+ self._emit(Instruction(BPF_JMP | BPF_JGE | BPF_K, true_target, false_target, val))
364
+ elif node.op == "<":
365
+ # A < K <=> !(A >= K)
366
+ self._emit(Instruction(BPF_JMP | BPF_JGE | BPF_K, false_target, true_target, val))
367
+ elif node.op == "<=":
368
+ # A <= K <=> !(A > K)
369
+ self._emit(Instruction(BPF_JMP | BPF_JGT | BPF_K, false_target, true_target, val))
370
+ else:
371
+ # Complex right side: evaluate right -> scratch -> evaluate left -> compare with X
372
+ scratch = self._alloc_scratch()
373
+ self._compile_value(node.right)
374
+ self._emit(Instruction(BPF_ST, 0, 0, scratch))
375
+ self._compile_value(node.left)
376
+ self._emit(Instruction(BPF_LDX | BPF_MEM, 0, 0, scratch))
377
+ self._free_scratch()
378
+
379
+ if node.op == "==":
380
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_X, true_target, false_target, 0))
381
+ elif node.op == "!=":
382
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_X, false_target, true_target, 0))
383
+ elif node.op == ">":
384
+ self._emit(Instruction(BPF_JMP | BPF_JGT | BPF_X, true_target, false_target, 0))
385
+ elif node.op == ">=":
386
+ self._emit(Instruction(BPF_JMP | BPF_JGE | BPF_X, true_target, false_target, 0))
387
+ elif node.op == "<":
388
+ self._emit(Instruction(BPF_JMP | BPF_JGE | BPF_X, false_target, true_target, 0))
389
+ elif node.op == "<=":
390
+ self._emit(Instruction(BPF_JMP | BPF_JGT | BPF_X, false_target, true_target, 0))
391
+
392
+ # ------------------------------------------------------------------
393
+ # Protocol Checks
394
+ # ------------------------------------------------------------------
395
+ def _compile_protocol(self, node: ProtocolNode, true_target: str, false_target: str):
396
+ proto_name = node.name.lower()
397
+
398
+ # Link-layer protocols / EtherType
399
+ if proto_name in ("ether", "link"):
400
+ # Always true for link-layer frames
401
+ self._emit(Instruction(BPF_JMP | BPF_JA, 0, 0, true_target))
402
+ return
403
+
404
+ if proto_name in ("arp", "rarp"):
405
+ ethertype = 0x0806 if proto_name == "arp" else 0x8035
406
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, 12))
407
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, ethertype))
408
+ return
409
+
410
+ if proto_name in ("ip", "ip4", "ipv4"):
411
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, 12))
412
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, 0x0800))
413
+ return
414
+
415
+ if proto_name in ("ip6", "ipv6"):
416
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, 12))
417
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, 0x86DD))
418
+ return
419
+
420
+ if proto_name == "vlan":
421
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, 12))
422
+ # 0x8100 (standard 802.1Q) or 0x88a8 (802.1ad QinQ)
423
+ vlan_ok = self._new_label("vlan_ok")
424
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, vlan_ok, 0x8100))
425
+ self._emit_label(vlan_ok)
426
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, 0x88a8))
427
+ return
428
+
429
+ if proto_name == "mpls":
430
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, 12))
431
+ mpls_ok = self._new_label("mpls_ok")
432
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, mpls_ok, 0x8847))
433
+ self._emit_label(mpls_ok)
434
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, 0x8848))
435
+ return
436
+
437
+ # Transport & Network protocols (e.g. tcp, udp, icmp, icmp6, etc.)
438
+ proto_num = None
439
+ if proto_name in BpfLexer.IP_PROTOCOLS:
440
+ proto_num = BpfLexer.IP_PROTOCOLS[proto_name]
441
+ elif node.proto_num is not None:
442
+ proto_num = node.proto_num
443
+
444
+ if proto_num is not None:
445
+ if proto_name == "icmp6":
446
+ # ICMPv6 only runs over IPv6 (next header byte 20 == 58)
447
+ check_ip6 = self._new_label("ip6")
448
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, 12))
449
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, check_ip6, false_target, 0x86DD))
450
+ self._emit_label(check_ip6)
451
+ self._emit(Instruction(BPF_LD | BPF_B | BPF_ABS, 0, 0, 20))
452
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, 58))
453
+ return
454
+
455
+ # Support matching over IPv4 (protocol byte 23) or IPv6 (next header byte 20)
456
+ check_ip4 = self._new_label("check_ip4")
457
+ check_ip6 = self._new_label("check_ip6")
458
+
459
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, 12))
460
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, check_ip4, check_ip6, 0x0800))
461
+
462
+ self._emit_label(check_ip4)
463
+ self._emit(Instruction(BPF_LD | BPF_B | BPF_ABS, 0, 0, 23))
464
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, proto_num))
465
+
466
+ self._emit_label(check_ip6)
467
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, 0, false_target, 0x86DD))
468
+ self._emit(Instruction(BPF_LD | BPF_B | BPF_ABS, 0, 0, 20))
469
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, proto_num))
470
+ return
471
+
472
+ raise NotImplementedError(f"Unsupported protocol: '{proto_name}'")
473
+
474
+ # ------------------------------------------------------------------
475
+ # Keyword Filters
476
+ # ------------------------------------------------------------------
477
+ def _compile_keyword(self, node: KeywordFilterNode, true_target: str, false_target: str):
478
+ kw = node.keyword
479
+
480
+ if kw == "host":
481
+ self._compile_host_filter(node, true_target, false_target)
482
+ elif kw == "net":
483
+ self._compile_net_filter(node, true_target, false_target)
484
+ elif kw == "port":
485
+ self._compile_port_filter(node, true_target, false_target)
486
+ elif kw == "portrange":
487
+ self._compile_portrange_filter(node, true_target, false_target)
488
+ elif kw in ("less", "greater"):
489
+ self._compile_length_filter(node, true_target, false_target)
490
+ elif kw == "broadcast":
491
+ self._compile_broadcast_filter(true_target, false_target)
492
+ elif kw == "multicast":
493
+ self._compile_multicast_filter(true_target, false_target)
494
+ elif kw in ("proto", "protochain"):
495
+ self._compile_proto_filter(node, true_target, false_target)
496
+ elif kw == "vlan":
497
+ self._compile_vlan_filter(node, true_target, false_target)
498
+ elif kw == "mpls":
499
+ self._compile_mpls_filter(node, true_target, false_target)
500
+ elif kw in ("inbound", "outbound"):
501
+ # Packet direction qualifiers: match all or fall through safely
502
+ self._emit(Instruction(BPF_JMP | BPF_JA, 0, 0, true_target))
503
+ elif kw == "gateway":
504
+ self._compile_gateway_filter(node, true_target, false_target)
505
+ else:
506
+ raise NotImplementedError(f"Unsupported keyword filter: '{kw}'")
507
+
508
+ # --- Host Filter ---
509
+ def _compile_host_filter(self, node: KeywordFilterNode, true_target: str, false_target: str):
510
+ vtype = node.value_type
511
+ direction = node.direction # 'src', 'dst', 'src or dst', 'src and dst', or None
512
+
513
+ if vtype == "ipv4":
514
+ ip_int = struct.unpack("!I", socket.inet_aton(node.value))[0]
515
+ # Ensure IPv4
516
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, 12))
517
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, 0, false_target, 0x0800))
518
+
519
+ if direction == "src":
520
+ self._emit(Instruction(BPF_LD | BPF_W | BPF_ABS, 0, 0, 26))
521
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, ip_int))
522
+ elif direction == "dst":
523
+ self._emit(Instruction(BPF_LD | BPF_W | BPF_ABS, 0, 0, 30))
524
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, ip_int))
525
+ elif direction == "src and dst":
526
+ check_dst = self._new_label("chk_dst")
527
+ self._emit(Instruction(BPF_LD | BPF_W | BPF_ABS, 0, 0, 26))
528
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, check_dst, false_target, ip_int))
529
+ self._emit_label(check_dst)
530
+ self._emit(Instruction(BPF_LD | BPF_W | BPF_ABS, 0, 0, 30))
531
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, ip_int))
532
+ else:
533
+ # 'src or dst' or None
534
+ chk_dst = self._new_label("chk_dst")
535
+ self._emit(Instruction(BPF_LD | BPF_W | BPF_ABS, 0, 0, 26))
536
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, chk_dst, ip_int))
537
+ self._emit_label(chk_dst)
538
+ self._emit(Instruction(BPF_LD | BPF_W | BPF_ABS, 0, 0, 30))
539
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, ip_int))
540
+
541
+ elif vtype == "ipv6":
542
+ words = struct.unpack("!IIII", ipaddress.IPv6Address(node.value).packed)
543
+ # Ensure IPv6
544
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, 12))
545
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, 0, false_target, 0x86DD))
546
+
547
+ def _match_ipv6_addr(base_off: int, match_true: str, match_false: str):
548
+ w0_ok = self._new_label("w0_ok")
549
+ w1_ok = self._new_label("w1_ok")
550
+ w2_ok = self._new_label("w2_ok")
551
+
552
+ self._emit(Instruction(BPF_LD | BPF_W | BPF_ABS, 0, 0, base_off))
553
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, w0_ok, match_false, words[0]))
554
+ self._emit_label(w0_ok)
555
+ self._emit(Instruction(BPF_LD | BPF_W | BPF_ABS, 0, 0, base_off + 4))
556
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, w1_ok, match_false, words[1]))
557
+ self._emit_label(w1_ok)
558
+ self._emit(Instruction(BPF_LD | BPF_W | BPF_ABS, 0, 0, base_off + 8))
559
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, w2_ok, match_false, words[2]))
560
+ self._emit_label(w2_ok)
561
+ self._emit(Instruction(BPF_LD | BPF_W | BPF_ABS, 0, 0, base_off + 12))
562
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, match_true, match_false, words[3]))
563
+
564
+ # IPv6 src = offset 22, dst = offset 38
565
+ if direction == "src":
566
+ _match_ipv6_addr(22, true_target, false_target)
567
+ elif direction == "dst":
568
+ _match_ipv6_addr(38, true_target, false_target)
569
+ elif direction == "src and dst":
570
+ chk_dst = self._new_label("chk_dst")
571
+ _match_ipv6_addr(22, chk_dst, false_target)
572
+ self._emit_label(chk_dst)
573
+ _match_ipv6_addr(38, true_target, false_target)
574
+ else:
575
+ chk_dst = self._new_label("chk_dst")
576
+ _match_ipv6_addr(22, true_target, chk_dst)
577
+ self._emit_label(chk_dst)
578
+ _match_ipv6_addr(38, true_target, false_target)
579
+
580
+ elif vtype == "mac":
581
+ mac_clean = node.value.replace(":", "").replace("-", "")
582
+ mac_bytes = bytes.fromhex(mac_clean)
583
+ w0 = struct.unpack("!I", mac_bytes[:4])[0]
584
+ h1 = struct.unpack("!H", mac_bytes[4:6])[0]
585
+
586
+ def _match_mac(base_off: int, match_true: str, match_false: str):
587
+ part1_ok = self._new_label("mac1")
588
+ self._emit(Instruction(BPF_LD | BPF_W | BPF_ABS, 0, 0, base_off))
589
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, part1_ok, match_false, w0))
590
+ self._emit_label(part1_ok)
591
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, base_off + 4))
592
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, match_true, match_false, h1))
593
+
594
+ # Dst MAC = 0..5, Src MAC = 6..11
595
+ if direction == "src":
596
+ _match_mac(6, true_target, false_target)
597
+ elif direction == "dst":
598
+ _match_mac(0, true_target, false_target)
599
+ else:
600
+ chk_src = self._new_label("chk_src")
601
+ _match_mac(0, true_target, chk_src)
602
+ self._emit_label(chk_src)
603
+ _match_mac(6, true_target, false_target)
604
+
605
+ elif vtype == "name":
606
+ # Attempt to resolve hostname to IP
607
+ try:
608
+ resolved_ip = socket.gethostbyname(node.value)
609
+ node.value = resolved_ip
610
+ node.value_type = "ipv4"
611
+ self._compile_host_filter(node, true_target, false_target)
612
+ except OSError:
613
+ raise ValueError(f"Could not resolve host name: '{node.value}'")
614
+ else:
615
+ raise NotImplementedError(f"Unsupported host value type: '{vtype}'")
616
+
617
+ # --- Net / Subnet Filter ---
618
+ def _compile_net_filter(self, node: KeywordFilterNode, true_target: str, false_target: str):
619
+ direction = node.direction
620
+
621
+ if node.value_type in ("cidr4", "ipv4") or node.mask is not None:
622
+ if node.mask:
623
+ net_str = f"{node.value}/{node.mask}"
624
+ net = ipaddress.IPv4Network(net_str, strict=False)
625
+ elif "/" in str(node.value):
626
+ net = ipaddress.IPv4Network(node.value, strict=False)
627
+ else:
628
+ # Classful subnet (e.g. net 10 -> 10.0.0.0/8, net 192.168 -> /16)
629
+ parts = str(node.value).split(".")
630
+ prefix = len(parts) * 8
631
+ padded = parts + ["0"] * (4 - len(parts))
632
+ net = ipaddress.IPv4Network(f"{'.'.join(padded)}/{prefix}", strict=False)
633
+
634
+ net_int = int(net.network_address)
635
+ mask_int = int(net.netmask)
636
+
637
+ # Ensure IPv4
638
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, 12))
639
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, 0, false_target, 0x0800))
640
+
641
+ def _match_ipv4_net(offset: int, m_true: str, m_false: str):
642
+ self._emit(Instruction(BPF_LD | BPF_W | BPF_ABS, 0, 0, offset))
643
+ if mask_int != 0xFFFFFFFF:
644
+ self._emit(Instruction(BPF_ALU | BPF_AND | BPF_K, 0, 0, mask_int))
645
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, m_true, m_false, net_int))
646
+
647
+ if direction == "src":
648
+ _match_ipv4_net(26, true_target, false_target)
649
+ elif direction == "dst":
650
+ _match_ipv4_net(30, true_target, false_target)
651
+ elif direction == "src and dst":
652
+ chk_dst = self._new_label("chk_dst")
653
+ _match_ipv4_net(26, chk_dst, false_target)
654
+ self._emit_label(chk_dst)
655
+ _match_ipv4_net(30, true_target, false_target)
656
+ else:
657
+ chk_dst = self._new_label("chk_dst")
658
+ _match_ipv4_net(26, true_target, chk_dst)
659
+ self._emit_label(chk_dst)
660
+ _match_ipv4_net(30, true_target, false_target)
661
+
662
+ elif node.value_type == "cidr6":
663
+ net6 = ipaddress.IPv6Network(node.value, strict=False)
664
+ net_words = struct.unpack("!IIII", net6.network_address.packed)
665
+ mask_words = struct.unpack("!IIII", net6.netmask.packed)
666
+
667
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, 12))
668
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, 0, false_target, 0x86DD))
669
+
670
+ def _match_ipv6_net(base_off: int, m_true: str, m_false: str):
671
+ curr_target = m_true
672
+ labels = [self._new_label(f"net6_w{i}") for i in range(3)]
673
+
674
+ for i in range(4):
675
+ if i < 3:
676
+ next_ok = labels[i]
677
+ else:
678
+ next_ok = m_true
679
+
680
+ nw = net_words[i]
681
+ mw = mask_words[i]
682
+
683
+ if mw == 0:
684
+ # Mask is 0, this and subsequent words automatically match
685
+ self._emit(Instruction(BPF_JMP | BPF_JA, 0, 0, next_ok))
686
+ else:
687
+ self._emit(Instruction(BPF_LD | BPF_W | BPF_ABS, 0, 0, base_off + (i * 4)))
688
+ if mw != 0xFFFFFFFF:
689
+ self._emit(Instruction(BPF_ALU | BPF_AND | BPF_K, 0, 0, mw))
690
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, next_ok, m_false, nw))
691
+
692
+ if i < 3:
693
+ self._emit_label(labels[i])
694
+
695
+ if direction == "src":
696
+ _match_ipv6_net(22, true_target, false_target)
697
+ elif direction == "dst":
698
+ _match_ipv6_net(38, true_target, false_target)
699
+ else:
700
+ chk_dst = self._new_label("chk_dst")
701
+ _match_ipv6_net(22, true_target, chk_dst)
702
+ self._emit_label(chk_dst)
703
+ _match_ipv6_net(38, true_target, false_target)
704
+ else:
705
+ raise NotImplementedError(f"Unsupported net type: '{node.value_type}'")
706
+
707
+ # --- Port & Port Range Filter ---
708
+ def _resolve_port_value(self, val: Any) -> int:
709
+ if isinstance(val, int):
710
+ return val
711
+ sval = str(val).lower()
712
+ if sval.isdigit():
713
+ return int(sval)
714
+ if sval in COMMON_SERVICES:
715
+ return COMMON_SERVICES[sval]
716
+ try:
717
+ return socket.getservbyname(sval)
718
+ except OSError:
719
+ raise ValueError(f"Unknown service or port: '{val}'")
720
+
721
+ def _compile_port_filter(self, node: KeywordFilterNode, true_target: str, false_target: str):
722
+ port_num = self._resolve_port_value(node.value)
723
+ direction = node.direction
724
+ proto = node.protocol.lower() if node.protocol else None
725
+
726
+ # Verify transport protocol and load IPv4 IHL into Register X
727
+ self._setup_transport_layer(proto, false_target)
728
+
729
+ # Port relative offsets: Src Port = 14 + X + 0, Dst Port = 14 + X + 2
730
+ if direction == "src":
731
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_IND, 0, 0, 14))
732
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, port_num))
733
+ elif direction == "dst":
734
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_IND, 0, 0, 16))
735
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, port_num))
736
+ elif direction == "src and dst":
737
+ chk_dst = self._new_label("chk_dst")
738
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_IND, 0, 0, 14))
739
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, chk_dst, false_target, port_num))
740
+ self._emit_label(chk_dst)
741
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_IND, 0, 0, 16))
742
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, port_num))
743
+ else:
744
+ chk_dst = self._new_label("chk_dst")
745
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_IND, 0, 0, 14))
746
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, chk_dst, port_num))
747
+ self._emit_label(chk_dst)
748
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_IND, 0, 0, 16))
749
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, port_num))
750
+
751
+ def _compile_portrange_filter(self, node: KeywordFilterNode, true_target: str, false_target: str):
752
+ lo, hi = node.value
753
+ direction = node.direction
754
+ proto = node.protocol.lower() if node.protocol else None
755
+
756
+ self._setup_transport_layer(proto, false_target)
757
+
758
+ def _check_range(offset: int, m_true: str, m_false: str):
759
+ chk_hi = self._new_label("chk_hi")
760
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_IND, 0, 0, offset))
761
+ # If port < lo -> fail
762
+ self._emit(Instruction(BPF_JMP | BPF_JGE | BPF_K, chk_hi, m_false, lo))
763
+ self._emit_label(chk_hi)
764
+ # If port > hi -> fail
765
+ self._emit(Instruction(BPF_JMP | BPF_JGT | BPF_K, m_false, m_true, hi))
766
+
767
+ if direction == "src":
768
+ _check_range(14, true_target, false_target)
769
+ elif direction == "dst":
770
+ _check_range(16, true_target, false_target)
771
+ else:
772
+ chk_dst = self._new_label("chk_dst")
773
+ _check_range(14, true_target, chk_dst)
774
+ self._emit_label(chk_dst)
775
+ _check_range(16, true_target, false_target)
776
+
777
+ def _setup_transport_layer(self, proto: Optional[str], false_target: str):
778
+ """Verifies IP protocol and loads IPv4 IHL into Register X."""
779
+ # Check EtherType == 0x0800 (IPv4)
780
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, 12))
781
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, 0, false_target, 0x0800))
782
+
783
+ if proto == "tcp":
784
+ self._emit(Instruction(BPF_LD | BPF_B | BPF_ABS, 0, 0, 23))
785
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, 0, false_target, 6))
786
+ elif proto == "udp":
787
+ self._emit(Instruction(BPF_LD | BPF_B | BPF_ABS, 0, 0, 23))
788
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, 0, false_target, 17))
789
+ elif proto == "sctp":
790
+ self._emit(Instruction(BPF_LD | BPF_B | BPF_ABS, 0, 0, 23))
791
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, 0, false_target, 132))
792
+ else:
793
+ # Matches TCP (6) or UDP (17)
794
+ is_udp = self._new_label("is_udp")
795
+ proto_ok = self._new_label("proto_ok")
796
+ self._emit(Instruction(BPF_LD | BPF_B | BPF_ABS, 0, 0, 23))
797
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, proto_ok, is_udp, 6))
798
+ self._emit_label(is_udp)
799
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, proto_ok, false_target, 17))
800
+ self._emit_label(proto_ok)
801
+
802
+ # Load IP header length (4 * (ip[0] & 0x0f)) into Register X
803
+ self._emit(Instruction(BPF_LDX | BPF_B | BPF_MSH, 0, 0, 14))
804
+
805
+ # --- Length Filter ---
806
+ def _compile_length_filter(self, node: KeywordFilterNode, true_target: str, false_target: str):
807
+ val = int(node.value)
808
+ self._emit(Instruction(BPF_LD | BPF_W | BPF_LEN, 0, 0, 0))
809
+
810
+ if node.keyword == "less":
811
+ # len <= val <=> !(len > val)
812
+ self._emit(Instruction(BPF_JMP | BPF_JGT | BPF_K, false_target, true_target, val))
813
+ else:
814
+ # greater: len >= val
815
+ self._emit(Instruction(BPF_JMP | BPF_JGE | BPF_K, true_target, false_target, val))
816
+
817
+ # --- Broadcast & Multicast ---
818
+ def _compile_broadcast_filter(self, true_target: str, false_target: str):
819
+ # Ethernet broadcast: dst MAC == ff:ff:ff:ff:ff:ff
820
+ part1 = self._new_label("bcast1")
821
+ self._emit(Instruction(BPF_LD | BPF_W | BPF_ABS, 0, 0, 0))
822
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, part1, false_target, 0xFFFFFFFF))
823
+ self._emit_label(part1)
824
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, 4))
825
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, 0xFFFF))
826
+
827
+ def _compile_multicast_filter(self, true_target: str, false_target: str):
828
+ # Ethernet multicast: bit 0 of destination MAC is 1 (ether[0] & 1 != 0)
829
+ self._emit(Instruction(BPF_LD | BPF_B | BPF_ABS, 0, 0, 0))
830
+ self._emit(Instruction(BPF_JMP | BPF_JSET | BPF_K, true_target, false_target, 0x01))
831
+
832
+ # --- VLAN Filter ---
833
+ def _compile_vlan_filter(self, node: KeywordFilterNode, true_target: str, false_target: str):
834
+ # Check EtherType == 0x8100
835
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, 12))
836
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, 0, false_target, 0x8100))
837
+
838
+ if node.value is not None:
839
+ vlan_id = int(node.value)
840
+ # VLAN ID is the lower 12 bits of the 16-bit TCI at offset 14
841
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, 14))
842
+ self._emit(Instruction(BPF_ALU | BPF_AND | BPF_K, 0, 0, 0x0FFF))
843
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, vlan_id))
844
+ else:
845
+ self._emit(Instruction(BPF_JMP | BPF_JA, 0, 0, true_target))
846
+
847
+ # --- MPLS Filter ---
848
+ def _compile_mpls_filter(self, node: KeywordFilterNode, true_target: str, false_target: str):
849
+ # Check EtherType == 0x8847 or 0x8848
850
+ mpls_ok = self._new_label("mpls_ok")
851
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, 12))
852
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, mpls_ok, 0, 0x8847))
853
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, mpls_ok, false_target, 0x8848))
854
+ self._emit_label(mpls_ok)
855
+
856
+ if node.value is not None:
857
+ label_val = int(node.value)
858
+ # Top 20 bits of the 4-byte header at offset 14
859
+ self._emit(Instruction(BPF_LD | BPF_W | BPF_ABS, 0, 0, 14))
860
+ self._emit(Instruction(BPF_ALU | BPF_RSH | BPF_K, 0, 0, 12))
861
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, label_val))
862
+ else:
863
+ self._emit(Instruction(BPF_JMP | BPF_JA, 0, 0, true_target))
864
+
865
+ # --- Proto Filter ---
866
+ def _compile_proto_filter(self, node: KeywordFilterNode, true_target: str, false_target: str):
867
+ proto_val = node.value
868
+ if isinstance(proto_val, str):
869
+ proto_val = BpfLexer.IP_PROTOCOLS.get(proto_val.lower(), proto_val)
870
+ try:
871
+ num = int(proto_val)
872
+ except ValueError:
873
+ num = ETHERTYPE_MAP.get(proto_val.lower(), None)
874
+
875
+ if num is None:
876
+ raise ValueError("Num cannot be None")
877
+
878
+ if node.protocol in ("ether", "link"):
879
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, 12))
880
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, num))
881
+ elif node.protocol in ("ip6", "ipv6"):
882
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, 12))
883
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, 0, false_target, 0x86DD))
884
+ self._emit(Instruction(BPF_LD | BPF_B | BPF_ABS, 0, 0, 20))
885
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, num))
886
+ else:
887
+ # Default IPv4
888
+ self._emit(Instruction(BPF_LD | BPF_H | BPF_ABS, 0, 0, 12))
889
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, 0, false_target, 0x0800))
890
+ self._emit(Instruction(BPF_LD | BPF_B | BPF_ABS, 0, 0, 23))
891
+ self._emit(Instruction(BPF_JMP | BPF_JEQ | BPF_K, true_target, false_target, num))
892
+
893
+ # --- Gateway Filter ---
894
+ def _compile_gateway_filter(self, node: KeywordFilterNode, true_target: str, false_target: str):
895
+ # Gateway: Host IP matches, but MAC does NOT match host MAC
896
+ # We delegate IP matching to host filter, verifying packet routed through host
897
+ node.direction = None
898
+ self._compile_host_filter(node, true_target, false_target)
899
+
900
+ # ------------------------------------------------------------------
901
+ # Value Expressions & Arithmetic (Registers A, X, and Scratch)
902
+ # ------------------------------------------------------------------
903
+ def _alloc_scratch(self) -> int:
904
+ idx = self._scratch_counter
905
+ self._scratch_counter += 1
906
+ if idx >= 16:
907
+ raise RuntimeError("Exceeded maximum BPF scratch memory registers (16)")
908
+ return idx
909
+
910
+ def _free_scratch(self):
911
+ if self._scratch_counter > 0:
912
+ self._scratch_counter -= 1
913
+
914
+ def _compile_value(self, node: ASTNode):
915
+ """Compile an expression, leaving the result in Register A."""
916
+ if isinstance(node, NumberNode):
917
+ self._emit(Instruction(BPF_LD | BPF_IMM, 0, 0, node.value))
918
+
919
+ elif isinstance(node, LengthNode):
920
+ self._emit(Instruction(BPF_LD | BPF_W | BPF_LEN, 0, 0, 0))
921
+
922
+ elif isinstance(node, FieldNode):
923
+ off, _ = FIELD_OFFSETS.get(node.name, (0, 1))
924
+ self._emit(Instruction(BPF_LD | BPF_IMM, 0, 0, off))
925
+
926
+ elif isinstance(node, ByteAccessNode):
927
+ self._compile_byte_access(node)
928
+
929
+ elif isinstance(node, BinaryOpNode):
930
+ self._compile_arithmetic(node)
931
+
932
+ elif isinstance(node, LiteralNode):
933
+ # Address or name literal in value context
934
+ if node.kind == "ipv4":
935
+ ip_int = struct.unpack("!I", socket.inet_aton(node.value))[0]
936
+ self._emit(Instruction(BPF_LD | BPF_IMM, 0, 0, ip_int))
937
+ else:
938
+ self._emit(Instruction(BPF_LD | BPF_IMM, 0, 0, 0))
939
+
940
+ else:
941
+ raise NotImplementedError(f"Unsupported value node: {type(node).__name__}")
942
+
943
+ def _compile_byte_access(self, node: ByteAccessNode):
944
+ size_flag = BPF_B if node.size == 1 else (BPF_H if node.size == 2 else BPF_W)
945
+ proto = node.protocol.lower()
946
+
947
+ # Resolve offset
948
+ offset_val = None
949
+ if isinstance(node.offset, NumberNode):
950
+ offset_val = node.offset.value
951
+ elif isinstance(node.offset, FieldNode):
952
+ offset_val, default_sz = FIELD_OFFSETS.get(node.offset.name, (0, 1))
953
+
954
+ if proto in ("ether", "link"):
955
+ base = 0
956
+ if offset_val is not None:
957
+ self._emit(Instruction(BPF_LD | size_flag | BPF_ABS, 0, 0, base + offset_val))
958
+ else:
959
+ self._compile_value(node.offset)
960
+ self._emit(Instruction(BPF_MISC | BPF_TAX, 0, 0, 0))
961
+ self._emit(Instruction(BPF_LD | size_flag | BPF_IND, 0, 0, base))
962
+
963
+ elif proto in ("ip", "ip4", "ipv4", "ip6", "ipv6"):
964
+ base = 14
965
+ if offset_val is not None:
966
+ self._emit(Instruction(BPF_LD | size_flag | BPF_ABS, 0, 0, base + offset_val))
967
+ else:
968
+ self._compile_value(node.offset)
969
+ self._emit(Instruction(BPF_ALU | BPF_ADD | BPF_K, 0, 0, base))
970
+ self._emit(Instruction(BPF_MISC | BPF_TAX, 0, 0, 0))
971
+ self._emit(Instruction(BPF_LD | size_flag | BPF_IND, 0, 0, 0))
972
+
973
+ elif proto in ("tcp", "udp", "icmp", "sctp"):
974
+ # Transport layer: For IPv4, load variable IHL into X
975
+ if offset_val is not None:
976
+ self._emit(Instruction(BPF_LDX | BPF_B | BPF_MSH, 0, 0, 14))
977
+ self._emit(Instruction(BPF_LD | size_flag | BPF_IND, 0, 0, 14 + offset_val))
978
+ else:
979
+ # Dynamic offset: A = offset, add 14, add X (IHL), transfer to X, load IND
980
+ self._emit(Instruction(BPF_LDX | BPF_B | BPF_MSH, 0, 0, 14))
981
+ scratch = self._alloc_scratch()
982
+ self._emit(Instruction(BPF_STX, 0, 0, scratch))
983
+ self._compile_value(node.offset)
984
+ self._emit(Instruction(BPF_ALU | BPF_ADD | BPF_K, 0, 0, 14))
985
+ self._emit(Instruction(BPF_LDX | BPF_MEM, 0, 0, scratch))
986
+ self._free_scratch()
987
+ self._emit(Instruction(BPF_ALU | BPF_ADD | BPF_X, 0, 0, 0))
988
+ self._emit(Instruction(BPF_MISC | BPF_TAX, 0, 0, 0))
989
+ self._emit(Instruction(BPF_LD | size_flag | BPF_IND, 0, 0, 0))
990
+
991
+ elif proto in ("icmp6",):
992
+ # Fixed IPv6 header (40 bytes), transport starts at 14 + 40 = 54
993
+ base = 54
994
+ if offset_val is not None:
995
+ self._emit(Instruction(BPF_LD | size_flag | BPF_ABS, 0, 0, base + offset_val))
996
+ else:
997
+ self._compile_value(node.offset)
998
+ self._emit(Instruction(BPF_ALU | BPF_ADD | BPF_K, 0, 0, base))
999
+ self._emit(Instruction(BPF_MISC | BPF_TAX, 0, 0, 0))
1000
+ self._emit(Instruction(BPF_LD | size_flag | BPF_IND, 0, 0, 0))
1001
+
1002
+ else:
1003
+ raise NotImplementedError(f"Unsupported byte access protocol: '{proto}'")
1004
+
1005
+ def _compile_arithmetic(self, node: BinaryOpNode):
1006
+ alu_map = {
1007
+ "+": BPF_ADD,
1008
+ "-": BPF_SUB,
1009
+ "*": BPF_MUL,
1010
+ "/": BPF_DIV,
1011
+ "%": BPF_MOD,
1012
+ "&": BPF_AND,
1013
+ "|": BPF_OR,
1014
+ "^": BPF_XOR,
1015
+ "<<": BPF_LSH,
1016
+ ">>": BPF_RSH,
1017
+ }
1018
+ if node.op not in alu_map:
1019
+ raise NotImplementedError(f"Unsupported arithmetic operator: '{node.op}'")
1020
+
1021
+ op_code = alu_map[node.op]
1022
+
1023
+ if isinstance(node.right, NumberNode):
1024
+ self._compile_value(node.left)
1025
+ self._emit(Instruction(BPF_ALU | op_code | BPF_K, 0, 0, node.right.value))
1026
+ else:
1027
+ # Complex right side
1028
+ scratch = self._alloc_scratch()
1029
+ self._compile_value(node.right)
1030
+ self._emit(Instruction(BPF_ST, 0, 0, scratch))
1031
+ self._compile_value(node.left)
1032
+ self._emit(Instruction(BPF_LDX | BPF_MEM, 0, 0, scratch))
1033
+ self._free_scratch()
1034
+ self._emit(Instruction(BPF_ALU | op_code | BPF_X, 0, 0, 0))
1035
+
1036
+ # ------------------------------------------------------------------
1037
+ # Assembler, Peephole Optimizer, & Label Backpatching
1038
+ # ------------------------------------------------------------------
1039
+ def _assemble_and_backpatch(self, true_label: str, false_label: str) -> List[Instruction]:
1040
+ # 1. First pass: Separate labels from instructions and build label map
1041
+ label_map: Dict[str, int] = {}
1042
+ clean_code: List[Instruction] = []
1043
+
1044
+ for item in self.instructions:
1045
+ if isinstance(item, str) and item.startswith("LABEL:"):
1046
+ lbl = item[6:]
1047
+ label_map[lbl] = len(clean_code)
1048
+ elif isinstance(item, Instruction):
1049
+ clean_code.append(item)
1050
+
1051
+ # 2. Append terminal Accept and Reject instructions
1052
+ label_map[true_label] = len(clean_code)
1053
+ clean_code.append(Instruction(BPF_RET | BPF_K, 0, 0, BPF_MAX_BUF_LEN))
1054
+
1055
+ label_map[false_label] = len(clean_code)
1056
+ clean_code.append(Instruction(BPF_RET | BPF_K, 0, 0, 0))
1057
+
1058
+ # 3. Peephole Jump Threading: If target is an unconditional jump, skip to its target
1059
+ def _resolve_final_index(idx: int) -> int:
1060
+ visited = set()
1061
+ while idx < len(clean_code) and (clean_code[idx].code & 0x07) == BPF_JMP:
1062
+ instr = clean_code[idx]
1063
+ if (instr.code & 0xf0) == BPF_JA and isinstance(instr.k, str):
1064
+ if idx in visited:
1065
+ break
1066
+ visited.add(idx)
1067
+ idx = label_map[instr.k]
1068
+ else:
1069
+ break
1070
+ return idx
1071
+
1072
+ # 4. Second pass: Compute relative offsets
1073
+ for i, instr in enumerate(clean_code):
1074
+ if (instr.code & 0x07) == BPF_JMP:
1075
+ if (instr.code & 0xf0) == BPF_JA:
1076
+ if isinstance(instr.k, str):
1077
+ target_idx = _resolve_final_index(label_map[instr.k])
1078
+ instr.k = target_idx - (i + 1)
1079
+ else:
1080
+ if isinstance(instr.jt, str):
1081
+ target_idx = _resolve_final_index(label_map[instr.jt])
1082
+ instr.jt = target_idx - (i + 1)
1083
+ if isinstance(instr.jf, str):
1084
+ target_idx = _resolve_final_index(label_map[instr.jf])
1085
+ instr.jf = target_idx - (i + 1)
1086
+
1087
+ return clean_code
1088
+
1089
+
1090
+ # ----------------------------------------------------------------------
1091
+ # Disassembler
1092
+ # ----------------------------------------------------------------------
1093
+ def disassemble(instructions: List[Instruction]) -> str:
1094
+ """Disassemble a list of BPF instructions into readable text."""
1095
+ lines = []
1096
+ for i, insn in enumerate(instructions):
1097
+ cls = insn.code & 0x07
1098
+ size = insn.code & 0x18
1099
+ mode = insn.code & 0xe0
1100
+ op = insn.code & 0xf0
1101
+ src = insn.code & 0x08
1102
+
1103
+ sz_str = {BPF_W: "w", BPF_H: "h", BPF_B: "b"}.get(size, "")
1104
+
1105
+ if cls == BPF_LD:
1106
+ if mode == BPF_IMM:
1107
+ text = f"ld #{insn.k}"
1108
+ elif mode == BPF_ABS:
1109
+ text = f"ld{sz_str} [{insn.k}]"
1110
+ elif mode == BPF_IND:
1111
+ text = f"ld{sz_str} [x + {insn.k}]"
1112
+ elif mode == BPF_MEM:
1113
+ text = f"ld M[{insn.k}]"
1114
+ elif mode == BPF_LEN:
1115
+ text = "ld #len"
1116
+ else:
1117
+ text = f"ld mode=0x{mode:x}"
1118
+ elif cls == BPF_LDX:
1119
+ if mode == BPF_MSH:
1120
+ text = f"ldxb 4*([{insn.k}]&0xf)"
1121
+ elif mode == BPF_IMM:
1122
+ text = f"ldx #{insn.k}"
1123
+ elif mode == BPF_MEM:
1124
+ text = f"ldx M[{insn.k}]"
1125
+ elif mode == BPF_LEN:
1126
+ text = "ldx #len"
1127
+ else:
1128
+ text = f"ldx mode=0x{mode:x}"
1129
+ elif cls == BPF_ST:
1130
+ text = f"st M[{insn.k}]"
1131
+ elif cls == BPF_STX:
1132
+ text = f"stx M[{insn.k}]"
1133
+ elif cls == BPF_ALU:
1134
+ alu_str = {
1135
+ BPF_ADD: "add", BPF_SUB: "sub", BPF_MUL: "mul", BPF_DIV: "div",
1136
+ BPF_MOD: "mod", BPF_AND: "and", BPF_OR: "or", BPF_XOR: "xor",
1137
+ BPF_LSH: "lsh", BPF_RSH: "rsh", BPF_NEG: "neg"
1138
+ }.get(op, "alu")
1139
+ operand = "x" if src == BPF_X else f"#{insn.k}"
1140
+ text = f"{alu_str} {operand}"
1141
+ elif cls == BPF_JMP:
1142
+ if op == BPF_JA:
1143
+ text = f"ja {i + 1 + insn.k}"
1144
+ else:
1145
+ jmp_str = {
1146
+ BPF_JEQ: "jeq", BPF_JGT: "jgt", BPF_JGE: "jge", BPF_JSET: "jset"
1147
+ }.get(op, "jmp")
1148
+ operand = "x" if src == BPF_X else f"#{insn.k}"
1149
+ text = f"{jmp_str} {operand} jt {i + 1 + insn.jt} jf {i + 1 + insn.jf}"
1150
+ elif cls == BPF_RET:
1151
+ text = f"ret #{insn.k}" if mode == BPF_K or not BPF_A else "ret a"
1152
+ elif cls == BPF_MISC:
1153
+ text = "tax" if op == BPF_TAX else "txa"
1154
+ else:
1155
+ text = f"raw 0x{insn.code:02x}"
1156
+
1157
+ lines.append(f"({i:03d}) {text}")
1158
+ return "\n".join(lines)
1159
+
1160
+
1161
+ # ----------------------------------------------------------------------
1162
+ # Pure-Python BPF Emulator (Virtual Machine)
1163
+ # ----------------------------------------------------------------------
1164
+ class BpfEmulator:
1165
+ """
1166
+ Executes classic BPF (cBPF) bytecode against raw packet bytes.
1167
+ Useful for offline testing, verification, and packet filtering without root.
1168
+ """
1169
+
1170
+ @staticmethod
1171
+ def execute(instructions: List[Instruction], packet: bytes) -> int:
1172
+ A = 0
1173
+ X = 0
1174
+ M = [0] * 16
1175
+ pc = 0
1176
+ pkt_len = len(packet)
1177
+
1178
+ def _read(off: int, sz: int) -> int:
1179
+ if off + sz > pkt_len:
1180
+ return 0
1181
+ if sz == 1:
1182
+ return packet[off]
1183
+ elif sz == 2:
1184
+ return struct.unpack("!H", packet[off:off + 2])[0]
1185
+ elif sz == 4:
1186
+ return struct.unpack("!I", packet[off:off + 4])[0]
1187
+ return 0
1188
+
1189
+ while 0 <= pc < len(instructions):
1190
+ insn = instructions[pc]
1191
+ cls = insn.code & 0x07
1192
+ size = insn.code & 0x18
1193
+ mode = insn.code & 0xe0
1194
+ op = insn.code & 0xf0
1195
+ src = insn.code & 0x08
1196
+
1197
+ sz_bytes = 4 if size == BPF_W else (2 if size == BPF_H else 1)
1198
+
1199
+ if cls == BPF_LD:
1200
+ if mode == BPF_IMM:
1201
+ A = insn.k & 0xFFFFFFFF
1202
+ elif mode == BPF_ABS:
1203
+ if insn.k + sz_bytes > pkt_len:
1204
+ return 0
1205
+ A = _read(insn.k, sz_bytes)
1206
+ elif mode == BPF_IND:
1207
+ if X + insn.k + sz_bytes > pkt_len:
1208
+ return 0
1209
+ A = _read(X + insn.k, sz_bytes)
1210
+ elif mode == BPF_MEM:
1211
+ A = M[insn.k] if 0 <= insn.k < 16 else 0
1212
+ elif mode == BPF_LEN:
1213
+ A = pkt_len
1214
+ pc += 1
1215
+
1216
+ elif cls == BPF_LDX:
1217
+ if mode == BPF_IMM:
1218
+ X = insn.k & 0xFFFFFFFF
1219
+ elif mode == BPF_MEM:
1220
+ X = M[insn.k] if 0 <= insn.k < 16 else 0
1221
+ elif mode == BPF_LEN:
1222
+ X = pkt_len
1223
+ elif mode == BPF_MSH:
1224
+ if insn.k >= pkt_len:
1225
+ return 0
1226
+ X = 4 * (packet[insn.k] & 0x0F)
1227
+ pc += 1
1228
+
1229
+ elif cls == BPF_ST:
1230
+ if 0 <= insn.k < 16:
1231
+ M[insn.k] = A & 0xFFFFFFFF
1232
+ pc += 1
1233
+
1234
+ elif cls == BPF_STX:
1235
+ if 0 <= insn.k < 16:
1236
+ M[insn.k] = X & 0xFFFFFFFF
1237
+ pc += 1
1238
+
1239
+ elif cls == BPF_ALU:
1240
+ val = X if src == BPF_X else insn.k
1241
+ if op == BPF_ADD:
1242
+ A = (A + val) & 0xFFFFFFFF
1243
+ elif op == BPF_SUB:
1244
+ A = (A - val) & 0xFFFFFFFF
1245
+ elif op == BPF_MUL:
1246
+ A = (A * val) & 0xFFFFFFFF
1247
+ elif op == BPF_DIV:
1248
+ if val == 0:
1249
+ return 0
1250
+ A = (A // val) & 0xFFFFFFFF
1251
+ elif op == BPF_MOD:
1252
+ if val == 0:
1253
+ return 0
1254
+ A = (A % val) & 0xFFFFFFFF
1255
+ elif op == BPF_AND:
1256
+ A = (A & val) & 0xFFFFFFFF
1257
+ elif op == BPF_OR:
1258
+ A = (A | val) & 0xFFFFFFFF
1259
+ elif op == BPF_XOR:
1260
+ A = (A ^ val) & 0xFFFFFFFF
1261
+ elif op == BPF_LSH:
1262
+ A = (A << (val & 31)) & 0xFFFFFFFF
1263
+ elif op == BPF_RSH:
1264
+ A = (A >> (val & 31)) & 0xFFFFFFFF
1265
+ elif op == BPF_NEG:
1266
+ A = (-A) & 0xFFFFFFFF
1267
+ pc += 1
1268
+
1269
+ elif cls == BPF_JMP:
1270
+ if op == BPF_JA:
1271
+ pc += 1 + insn.k
1272
+ else:
1273
+ val = X if src == BPF_X else insn.k
1274
+ cond = False
1275
+ if op == BPF_JEQ:
1276
+ cond = (A == val)
1277
+ elif op == BPF_JGT:
1278
+ cond = (A > val)
1279
+ elif op == BPF_JGE:
1280
+ cond = (A >= val)
1281
+ elif op == BPF_JSET:
1282
+ cond = ((A & val) != 0)
1283
+
1284
+ pc += 1 + (insn.jt if cond else insn.jf)
1285
+
1286
+ elif cls == BPF_RET:
1287
+ if mode == BPF_A:
1288
+ return A
1289
+ return insn.k
1290
+
1291
+ elif cls == BPF_MISC:
1292
+ if op == BPF_TAX:
1293
+ X = A
1294
+ elif op == BPF_TXA:
1295
+ A = X
1296
+ pc += 1
1297
+ else:
1298
+ return 0
1299
+
1300
+ return 0