whad-lab 0.1.0__tar.gz

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.
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: whad-lab
3
+ Version: 0.1.0
4
+ Summary: WHAD Lab environment for hands-on exercises (Experimental)
5
+ Author-email: Damien Cauquil <virtualabs@gmail.com>
6
+ License-Expression: MIT
7
+ Keywords: whad,lab,hands-on,exercise,learn
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Security
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: websockets>=13
18
+ Requires-Dist: pyyaml>=6
19
+ Requires-Dist: whad>=1.2
20
+ Requires-Dist: scapy>=2.5
@@ -0,0 +1,4 @@
1
+ WHAD Lab Environment
2
+ ====================
3
+
4
+ This project is EXPERIMENTAL and not ready for production.
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+
4
+ [project]
5
+ name = "whad-lab"
6
+ version = "0.1.0"
7
+ description = "WHAD Lab environment for hands-on exercises (Experimental)"
8
+ requires-python = ">=3.10"
9
+ dependencies = [
10
+ "websockets>=13",
11
+ "pyyaml>=6",
12
+ "whad>=1.2",
13
+ "scapy>=2.5",
14
+ ]
15
+ readme = "README.md"
16
+ license = "MIT"
17
+ keywords = ["whad", "lab", "hands-on", "exercise", "learn"]
18
+ classifiers = [
19
+ "Development Status :: 3 - Alpha",
20
+ "Environment :: Console",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Security",
26
+ ]
27
+ authors = [
28
+ {name = "Damien Cauquil", email = "virtualabs@gmail.com"},
29
+ ]
30
+
31
+ [project.scripts]
32
+ wlab = "wlab.server:cli"
33
+
34
+ [tool.setuptools.package-data]
35
+ wlab = ['assets/*', 'static/*', 'exercises/*', 'exercises/ble_tracker/*']
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: whad-lab
3
+ Version: 0.1.0
4
+ Summary: WHAD Lab environment for hands-on exercises (Experimental)
5
+ Author-email: Damien Cauquil <virtualabs@gmail.com>
6
+ License-Expression: MIT
7
+ Keywords: whad,lab,hands-on,exercise,learn
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Security
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: websockets>=13
18
+ Requires-Dist: pyyaml>=6
19
+ Requires-Dist: whad>=1.2
20
+ Requires-Dist: scapy>=2.5
@@ -0,0 +1,19 @@
1
+ README.rst
2
+ pyproject.toml
3
+ whad_lab.egg-info/PKG-INFO
4
+ whad_lab.egg-info/SOURCES.txt
5
+ whad_lab.egg-info/dependency_links.txt
6
+ whad_lab.egg-info/entry_points.txt
7
+ whad_lab.egg-info/requires.txt
8
+ whad_lab.egg-info/top_level.txt
9
+ wlab/arena.py
10
+ wlab/progress.py
11
+ wlab/server.py
12
+ wlab/tracker_v2.py
13
+ wlab/assets/tracker-profile.json
14
+ wlab/assets/tracker.png
15
+ wlab/exercises/ble_tracker/device.py
16
+ wlab/exercises/ble_tracker/lab.yaml
17
+ wlab/static/index.html
18
+ wlab/static/landing.html
19
+ wlab/static/marked.min.js
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ wlab = wlab.server:cli
@@ -0,0 +1,4 @@
1
+ websockets>=13
2
+ pyyaml>=6
3
+ whad>=1.2
4
+ scapy>=2.5
@@ -0,0 +1 @@
1
+ wlab
@@ -0,0 +1,381 @@
1
+ """
2
+ WHAD wireless arena
3
+
4
+ This arena can host multiple nodes and ensures communication between them
5
+ as it would be the case in a real wireless environment. The implementation
6
+ of nodes and the messages they exchange is entirely flexible and can be
7
+ defined to fit any scenario.
8
+
9
+ Combined with a custom control node and a specific PHY layer, it is possible
10
+ to equip any node with a protocol stack and make it interact with a control
11
+ node, a specific node acting as an interface between the arena and its nodes
12
+ and the outside world.
13
+ """
14
+ from threading import Thread
15
+ from typing import Optional, Any, Union, Generator, Tuple, List
16
+ from queue import Queue, Empty
17
+ from scapy.packet import Packet
18
+
19
+ class NodeMessage:
20
+ """Root class for all node messages.
21
+
22
+ This class shall be used to define any message designed to be exchanged
23
+ between nodes in a wireless arena.
24
+ """
25
+
26
+ def __init__(self, from_id: str, to_id: Optional[str] = None):
27
+ """Initialization of a node message.
28
+
29
+ This message keeps track of the originating node and optionally of
30
+ its destination node.
31
+
32
+ :param from_id: Identifier of the origin node
33
+ :type from_id: str
34
+ :param to_id: Identifier of the destination node
35
+ :type to_id: str, optional
36
+ """
37
+ self.__origin = from_id
38
+ self.__dest = to_id
39
+
40
+ @property
41
+ def origin(self) -> str:
42
+ """Originating node's identifier."""
43
+ return self.__origin
44
+
45
+ @property
46
+ def destination(self) -> Optional[str]:
47
+ """Destination node's identifier."""
48
+ return self.__dest
49
+
50
+ class MetadataMsg(NodeMessage):
51
+ """This node message class holds a set of metadata in the form
52
+ of a set of parameters. It provides a very flexible way to store
53
+ and access metadata.
54
+ """
55
+
56
+ def __init__(self, from_id: str, to_id: str, **parameters):
57
+ """Initialization.
58
+
59
+ :param from_id: Identifier of the origin node
60
+ :type from_id: str
61
+ :param to_id: Identifier of the destination node
62
+ :type to_id: str, optional
63
+ :param **parameters: set of parameters to attach to the message as metadata
64
+ :type **parameters: dict
65
+ """
66
+ super().__init__(from_id, to_id)
67
+ self.__parameters = parameters
68
+
69
+ @property
70
+ def parameters(self) -> dict:
71
+ """Set of parameters as dict.
72
+
73
+ :return: Dictionary of parameters (read-only)
74
+ :rtype: dict
75
+ """
76
+ return self.__parameters
77
+
78
+ def __getitem__(self, key: str) -> Any:
79
+ """Parameters read primitives, implementing an array behavior."""
80
+ if key in self.__parameters:
81
+ return self.__parameters[key]
82
+ raise IndexError
83
+
84
+ class ConnectionMsg(MetadataMsg):
85
+ """Generic connection request message."""
86
+
87
+ class ConnectionResultMsg(MetadataMsg):
88
+ """Generic connection response message."""
89
+
90
+ class DisconnectionMsg(MetadataMsg):
91
+ """Generic disconnection request."""
92
+
93
+ class DisconnectionResultMsg(MetadataMsg):
94
+ """Generic disconnection response."""
95
+
96
+ class DataMsg(MetadataMsg):
97
+ """Generic data (PDU) message."""
98
+
99
+ def __init__(self, from_id: str, to_id: str, data: Packet):
100
+ super().__init__(from_id, to_id, data=data)
101
+
102
+ @property
103
+ def data(self) -> Packet:
104
+ return self['data']
105
+
106
+ class Arena:
107
+ """Wireless arena where devices are placed."""
108
+
109
+ def __init__(self):
110
+ """Initialize wireless arena."""
111
+ self.__nodes = {}
112
+
113
+ def send(self, message: NodeMessage, node_id: Optional[str]) -> bool:
114
+ """Send message to one or more nodes in the arena.
115
+
116
+ :param message: Message to send to other nodes.
117
+ :type message: NodeMessage
118
+ :param node_id: Destination node's identifier (set to `None` to broadcast to other nodes)
119
+ :type node_id: str, optional
120
+ :return: `True` if message was successfully sent, `False` otherwise.
121
+ :rtype: bool
122
+ """
123
+ if node_id is not None:
124
+ # Add message into the target node's message queue, if found
125
+ if node_id in self.__nodes:
126
+ self.__nodes[node_id].add_message(message)
127
+ return True
128
+
129
+ # Could not find node, cannot send.
130
+ return False
131
+ else:
132
+ # Broadcast message to other nodes
133
+ for node_id, node in self.__nodes.items():
134
+ if node_id != message.origin:
135
+ node.add_message(message)
136
+ return True
137
+
138
+ def find_node(self, node_id: str) -> Optional['Node']:
139
+ """Find a node in the arena based on its identifier.
140
+
141
+ :param node_id: Node's identifier
142
+ :type node_id: str
143
+
144
+ :return: Corresponding node if found, `None` otherwise.
145
+ :rtype: Node, optional
146
+ """
147
+ if node_id in self.__nodes:
148
+ return self.__nodes[node_id]
149
+ return None
150
+
151
+ def add_node(self, node: 'Node') -> bool:
152
+ """Add node into the arena.
153
+
154
+ :param node: Node to put inside the arena.
155
+ :type node: Node
156
+ :return: `True` on success, `False` otherwise.
157
+ :rtype: bool
158
+ """
159
+ if node.id not in self.__nodes:
160
+ self.__nodes[node.id] = node
161
+ node.enter_arena(self)
162
+ return True
163
+ return False
164
+
165
+ def remove_node(self, node: Union[str, 'Node']) -> bool:
166
+ """Remove a node from the arena.
167
+
168
+ :param node: Node to remove from the arena.
169
+ :type node: Node
170
+ :type node: str
171
+ :return: `True` on success, `False` otherwise.
172
+ :rtype: bool
173
+ """
174
+ if isinstance(node, Node):
175
+ if node.id in self.__nodes:
176
+ self.__nodes[node.id].leave_arena()
177
+ del self.__nodes[node.id]
178
+ return True
179
+ elif isinstance(node, str):
180
+ if node in self.__nodes:
181
+ self.__nodes[node].leave_arena()
182
+ del self.__nodes[node]
183
+ return True
184
+ return False
185
+
186
+ def enable_node(self, node: Union[str, 'Node']) -> bool:
187
+ """Enable a node present in the arena.
188
+
189
+ :param node: Node to enable
190
+ :type node: Node
191
+ :type node: str
192
+
193
+ :return: `True` if node has been enabled, `False` otherwise.
194
+ :rtype: bool
195
+ """
196
+ if isinstance(node, str):
197
+ n = self.find_node(node)
198
+ else:
199
+ n = node
200
+
201
+ # Node found, enable.
202
+ if n is not None:
203
+ n.enable()
204
+ return True
205
+
206
+ # Failure.
207
+ return False
208
+
209
+ def disable_node(self, node: Union[str, 'Node']) -> bool:
210
+ """Disable a node present in the arena.
211
+
212
+ :param node: Node to enable
213
+ :type node: Node
214
+ :type node: str
215
+
216
+ :return: `True` if node has been disabled, `False` otherwise.
217
+ :rtype: bool
218
+ """
219
+ if isinstance(node, str):
220
+ n = self.find_node(node)
221
+ else:
222
+ n = node
223
+
224
+ # Node found, enable.
225
+ if n is not None:
226
+ n.disable()
227
+ return True
228
+
229
+ # Failure.
230
+ return False
231
+
232
+
233
+ def list_all_nodes(self) -> Generator[Tuple[str, 'Node'], None, None]:
234
+ """Enumerate all nodes (active and inactive)."""
235
+ yield from self.__nodes.items()
236
+
237
+
238
+ def list_active_nodes(self) -> Generator[Tuple[str, 'Node'], None, None]:
239
+ """Enumerate only active nodes."""
240
+ for node_id,node in self.__nodes.items():
241
+ if node.active:
242
+ yield (node_id, node)
243
+
244
+
245
+ class Node:
246
+ """Wireless device present in the arena.
247
+
248
+ Each device has its own thread to manage its state at its own
249
+ pace. Communication with a node is done through message queues,
250
+ as generic WHAD devices do.
251
+
252
+ The arena acts as an arbiter for nodes and dispatch messages
253
+ between nodes.
254
+ """
255
+
256
+ def __init__(self, node_id: str):
257
+ """Initialize a node with its own identifier. A node is not
258
+ placed into a specific arena when created.
259
+
260
+ :param node_id: Node identifier
261
+ :type node_id: str
262
+ """
263
+ self.__id = node_id
264
+ self.__arena: Optional[Arena] = None
265
+ self.__msg_queue = Queue()
266
+ self.__active = True
267
+
268
+ @property
269
+ def id(self) -> str:
270
+ """Node identifier"""
271
+ return self.__id
272
+
273
+ @property
274
+ def arena(self) -> Optional[Arena]:
275
+ """Current arena."""
276
+ return self.__arena
277
+
278
+ @property
279
+ def active(self) -> bool:
280
+ """Node active state"""
281
+ return self.__active
282
+
283
+ def disable(self):
284
+ """Disable node."""
285
+ self.__active = False
286
+
287
+ def enable(self):
288
+ """Enable node."""
289
+ self.__active = True
290
+
291
+ def add_message(self, message: NodeMessage):
292
+ """Add a node message into our queue.
293
+
294
+ :param message: Message to add into the node's message queue.
295
+ :type message: NodeMessage
296
+ """
297
+ self.__msg_queue.put(message)
298
+
299
+ def get_message(self, timeout: Optional[float] = None) -> Optional[NodeMessage]:
300
+ """Retrieve a message from the message queue.
301
+ This method blocks until a message is available in
302
+ the queue.
303
+
304
+ :param timeout: Timeout in seconds
305
+ :type timeout: float, optional
306
+
307
+ :return: Node message to process if any, `None` otherwise.
308
+ :rtype: NodeMessage, optional
309
+ """
310
+ if timeout is None:
311
+ return self.__msg_queue.get()
312
+ else:
313
+ try:
314
+ return self.__msg_queue.get(timeout=timeout)
315
+ except Empty:
316
+ return None
317
+
318
+ def enter_arena(self, arena: Arena):
319
+ """Set the node's arena.
320
+
321
+ :param arena: Arena where the node is placed.
322
+ :type arena: Arena
323
+ """
324
+ self.__arena = arena
325
+
326
+ def leave_arena(self):
327
+ """Clear the node's arena."""
328
+ self.__arena = None
329
+
330
+ def send(self, message: NodeMessage, node_id: Optional[str] = None) -> bool:
331
+ """Send a packet to a node identified by `node_id`.
332
+
333
+ :param message: Message to send to other node(s).
334
+ :type message: NodeMessage
335
+ :param node_id: Destination node's identifier
336
+ :type node_id: str, optional
337
+
338
+ :return: `True` if message has been sent, `False` otherwise
339
+ :rtype: bool
340
+ """
341
+ if self.__arena is not None:
342
+ return self.__arena.send(message, node_id)
343
+ return False
344
+
345
+ class DeviceNode(Node, Thread):
346
+ """This class defines a node that has its own thread. This configuration
347
+ helps when a node needs to have its own protocol stack, like an emulated
348
+ device.
349
+ """
350
+ def __init__(self, node_id: str):
351
+ """Initialization.
352
+
353
+ :param node_id: Node identifier
354
+ :type node_id: str
355
+ """
356
+ Node.__init__(self, node_id)
357
+ Thread.__init__(self)
358
+
359
+ class ControlNode(Node):
360
+ """This class defines a control node, a node that is driven by a component
361
+ outside the arena and that interacts with other nodes inside the arena. It
362
+ acts like a bridge and has access to the whole arena.
363
+
364
+ It shares the same communication mechanism all nodes in the arena use, is able
365
+ to send and receive messages, but has control on other nodes as well.
366
+ """
367
+
368
+ def __init__(self, node_id: str):
369
+ """Initialize our control node."""
370
+ super().__init__(node_id)
371
+
372
+ def list_all_nodes(self) -> List[Node]:
373
+ """Return a list of all nodes present in the arena.
374
+
375
+ :return: List of all nodes.
376
+ :rtype: list
377
+ """
378
+ if self.arena is not None:
379
+ return [n[1] for n in list(self.arena.list_all_nodes())]
380
+ return []
381
+
@@ -0,0 +1 @@
1
+ {"services": [{"uuid": "1800", "type_uuid": "2800", "start_handle": 1, "end_handle": 5, "characteristics": [{"handle": 2, "uuid": "2803", "properties": 18, "security": 0, "value": {"handle": 3, "uuid": "2A00", "data": "69544147202020202020202020202020"}, "descriptors": []}, {"handle": 4, "uuid": "2803", "properties": 2, "security": 0, "value": {"handle": 5, "uuid": "2A01", "data": "0000"}, "descriptors": []}]}, {"uuid": "180F", "type_uuid": "2800", "start_handle": 6, "end_handle": 8, "characteristics": [{"handle": 7, "uuid": "2803", "properties": 18, "security": 0, "value": {"handle": 8, "uuid": "2A19", "data": "63"}, "descriptors": []}]}, {"uuid": "1802", "type_uuid": "2800", "start_handle": 9, "end_handle": 11, "characteristics": [{"handle": 10, "uuid": "2803", "properties": 28, "security": 0, "value": {"handle": 11, "uuid": "2A06", "data": ""}, "descriptors": []}]}, {"uuid": "FFE0", "type_uuid": "2800", "start_handle": 12, "end_handle": 14, "characteristics": [{"handle": 13, "uuid": "2803", "properties": 18, "security": 0, "value": {"handle": 14, "uuid": "FFE1", "data": "00"}, "descriptors": []}]}], "devinfo": {"adv_data": "020105020a000319c1030302e0ff", "bd_addr": "ff:ff:30:03:70:72", "addr_type": 0, "scan_rsp": "110969544147202020202020202020202020"}}
Binary file