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 +1300 -0
- LightBPF/Lexer.py +200 -0
- LightBPF/Parser.py +414 -0
- LightBPF/__init__.py +3 -0
- lightbpf-1.0.0.dist-info/METADATA +542 -0
- lightbpf-1.0.0.dist-info/RECORD +8 -0
- lightbpf-1.0.0.dist-info/WHEEL +5 -0
- lightbpf-1.0.0.dist-info/top_level.txt +1 -0
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
|