iotsploit-platforms 0.0.6__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.
@@ -0,0 +1,10 @@
1
+ """
2
+ IoTSploit Platform Adapters Package.
3
+
4
+ This package provides platform-specific implementations for WiFi, Input, and SSH backends.
5
+ """
6
+
7
+ from iotsploit_platforms.selector import build_context, get_wifi_backend
8
+
9
+ __version__ = "0.1.0"
10
+ __all__ = ["get_wifi_backend", "build_context"]
@@ -0,0 +1,5 @@
1
+ """
2
+ Platform adapter implementations.
3
+
4
+ This module contains platform-specific adapter implementations.
5
+ """
@@ -0,0 +1,5 @@
1
+ """
2
+ Platform-specific adapter implementations.
3
+
4
+ This module contains platform-specific implementations organized by platform.
5
+ """
@@ -0,0 +1,7 @@
1
+ """
2
+ Darwin (macOS) platform adapters.
3
+ """
4
+
5
+ from iotsploit_platforms.adapters.platforms.darwin.wifi_backend import wifi_backend
6
+
7
+ __all__ = ["wifi_backend"]
@@ -0,0 +1,93 @@
1
+ """
2
+ Darwin (macOS) WiFi Backend Implementation.
3
+
4
+ This module provides macOS-specific WiFi backend implementation.
5
+ Currently a placeholder implementation.
6
+ """
7
+
8
+ import logging
9
+ from typing import List, Optional, Dict, Any, Tuple
10
+
11
+ from iotsploit_core.ports.wifi_backend import WifiBackend
12
+ from iotsploit_core.utils.exceptions import NotSupportedError
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class DarwinWifiBackend(WifiBackend):
18
+ """
19
+ Darwin (macOS) WiFi backend implementation.
20
+
21
+ This is a placeholder implementation. Full implementation would use
22
+ macOS-specific APIs (e.g., airport, CoreWLAN framework).
23
+ """
24
+
25
+ def __init__(self, wifi_iface_name: Optional[str] = None):
26
+ """
27
+ Initialize Darwin WiFi backend.
28
+
29
+ Args:
30
+ wifi_iface_name: WiFi interface name (optional on macOS)
31
+ """
32
+ self.wifi_iface_name = wifi_iface_name
33
+ self._wifi_mode = "IDLE"
34
+
35
+ def scan(self) -> List[Dict[str, Any]]:
36
+ """
37
+ Scan for available WiFi networks.
38
+
39
+ Raises:
40
+ NotSupportedError: macOS WiFi scanning not yet implemented
41
+ """
42
+ raise NotSupportedError("WiFi scanning is not yet implemented on macOS")
43
+
44
+ def sta_connect(self, ssid: str, passwd: str) -> None:
45
+ """
46
+ Connect to a WiFi network in station (STA) mode.
47
+
48
+ Raises:
49
+ NotSupportedError: macOS WiFi connection not yet implemented
50
+ """
51
+ raise NotSupportedError("WiFi station mode connection is not yet implemented on macOS")
52
+
53
+ def sta_disconnect(self) -> None:
54
+ """
55
+ Disconnect from the current WiFi network in station mode.
56
+
57
+ Raises:
58
+ NotSupportedError: macOS WiFi disconnection not yet implemented
59
+ """
60
+ raise NotSupportedError("WiFi station mode disconnection is not yet implemented on macOS")
61
+
62
+ def ap_start(self, ssid: Optional[str] = None, passwd: Optional[str] = None, wpa_mode: int = 2) -> Tuple[str, str]:
63
+ """
64
+ Start WiFi access point (AP) mode.
65
+
66
+ Raises:
67
+ NotSupportedError: macOS does not support WiFi AP mode
68
+ """
69
+ raise NotSupportedError("WiFi access point mode is not supported on macOS")
70
+
71
+ def ap_stop(self) -> None:
72
+ """
73
+ Stop WiFi access point (AP) mode.
74
+
75
+ Raises:
76
+ NotSupportedError: macOS does not support WiFi AP mode
77
+ """
78
+ raise NotSupportedError("WiFi access point mode is not supported on macOS")
79
+
80
+ def status(self) -> Dict[str, Any]:
81
+ """
82
+ Get current WiFi status.
83
+
84
+ Returns:
85
+ Dictionary containing current WiFi status information.
86
+ """
87
+ return {
88
+ "wifi_mode": self._wifi_mode,
89
+ }
90
+
91
+
92
+ # Export the backend class
93
+ wifi_backend = DarwinWifiBackend
@@ -0,0 +1,7 @@
1
+ """
2
+ Linux platform adapters.
3
+ """
4
+
5
+ from iotsploit_platforms.adapters.platforms.linux.wifi_backend import wifi_backend
6
+
7
+ __all__ = ["wifi_backend"]
@@ -0,0 +1,640 @@
1
+ """
2
+ Linux WiFi Backend Implementation.
3
+
4
+ This module provides Linux-specific WiFi backend implementation using
5
+ NetworkManager's native libnm API via GObject Introspection.
6
+ """
7
+
8
+ import logging
9
+ import time
10
+ import uuid
11
+ from pathlib import Path
12
+ from typing import List, Optional, Dict, Any, Tuple
13
+
14
+ import gi
15
+ gi.require_version('NM', '1.0')
16
+ from gi.repository import NM, GLib
17
+
18
+ from iotsploit_core.ports.wifi_backend import WifiBackend
19
+ from iotsploit_core.utils.exceptions import NotSupportedError
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # NetworkManager DBus constants (from NM headers / docs)
24
+ _NM_ACTIVE_CONNECTION_STATE_ACTIVATED = 2
25
+
26
+
27
+ class LinuxWifiBackend(WifiBackend):
28
+ """
29
+ Linux WiFi backend implementation using NetworkManager.
30
+
31
+ Uses libnm (NetworkManager's native library) for all WiFi operations.
32
+ Supports station mode, access point mode, and network scanning.
33
+ """
34
+ # Keep consistent with upstream DBus hotspot example
35
+ _HOTSPOT_UUID = "2b0d0f1d-b79d-43af-bde1-71744625642e"
36
+
37
+ def __init__(self, wifi_iface_name: str = "wlan0", forward_eth_name: Optional[str] = None):
38
+ """
39
+ Initialize NetworkManager WiFi backend.
40
+
41
+ Args:
42
+ wifi_iface_name: WiFi interface name (e.g., "wlan0", "wlp0s20f3")
43
+ forward_eth_name: Ethernet interface for forwarding (used for AP mode NAT)
44
+ """
45
+ self.wifi_iface_name = wifi_iface_name
46
+ self.forward_eth_name = forward_eth_name
47
+
48
+ self._wifi_mode = "IDLE"
49
+ self._sta_conn_wifi_ssid = ""
50
+ self._sta_conn_wifi_passwd = ""
51
+ self._ap_ssid = ""
52
+ self._ap_passwd = ""
53
+
54
+ # Connection IDs for tracking
55
+ self._active_connection_id: Optional[str] = None
56
+ self._hotspot_connection_id: Optional[str] = None
57
+ # Hotspot connection UUID/path (DBus profile reuse)
58
+ self._hotspot_connection_uuid: Optional[str] = None
59
+ self._hotspot_connection_path: Optional[str] = None
60
+ self._hotspot_active_connection_path: Optional[str] = None
61
+
62
+ # Initialize NetworkManager client
63
+ self._client: Optional[NM.Client] = None
64
+ self._device: Optional[NM.DeviceWifi] = None
65
+ self._init_client()
66
+
67
+ def _init_client(self):
68
+ """Initialize NetworkManager client and find WiFi device."""
69
+ try:
70
+ self._client = NM.Client.new(None)
71
+ self._device = self._find_wifi_device()
72
+ logger.info(f"NetworkManager client initialized for {self.wifi_iface_name}")
73
+ except Exception as e:
74
+ logger.error(f"Failed to initialize NetworkManager client: {e}")
75
+ raise NotSupportedError(f"NetworkManager initialization failed: {e}")
76
+
77
+ def _find_wifi_device(self) -> NM.DeviceWifi:
78
+ """Find the WiFi device by interface name."""
79
+ devices = self._client.get_devices()
80
+ for device in devices:
81
+ if device.get_iface() == self.wifi_iface_name:
82
+ if isinstance(device, NM.DeviceWifi):
83
+ return device
84
+ else:
85
+ raise NotSupportedError(f"{self.wifi_iface_name} is not a WiFi device")
86
+
87
+ # List available WiFi devices for debugging
88
+ wifi_devices = [d.get_iface() for d in devices if isinstance(d, NM.DeviceWifi)]
89
+ raise NotSupportedError(
90
+ f"WiFi device {self.wifi_iface_name} not found. "
91
+ f"Available WiFi devices: {wifi_devices}"
92
+ )
93
+
94
+ def _get_security_flags_string(self, ap: NM.AccessPoint) -> str:
95
+ """Convert AP security flags to string."""
96
+ wpa_flags = ap.get_wpa_flags()
97
+ rsn_flags = ap.get_rsn_flags()
98
+
99
+ # Use integer constants instead of enum attributes for compatibility
100
+ # KEY_MGMT_PSK = 0x00000002 (from NetworkManager headers)
101
+ KEY_MGMT_PSK = 0x00000002
102
+
103
+ if rsn_flags & KEY_MGMT_PSK:
104
+ return "WPA2"
105
+ elif wpa_flags & KEY_MGMT_PSK:
106
+ return "WPA"
107
+ elif ap.get_flags() & 0x0001: # PRIVACY flag
108
+ return "WEP"
109
+ else:
110
+ return "OPEN"
111
+
112
+ def _wait_for_connection(self, timeout: int = 30) -> bool:
113
+ """Wait for connection to be established."""
114
+ start_time = time.time()
115
+ while time.time() - start_time < timeout:
116
+ state = self._device.get_state()
117
+ if state == NM.DeviceState.ACTIVATED:
118
+ return True
119
+ elif state in (NM.DeviceState.FAILED, NM.DeviceState.DISCONNECTED):
120
+ return False
121
+ time.sleep(0.5)
122
+ return False
123
+
124
+ def _find_connection_by_id(self, conn_id: str) -> Optional[NM.RemoteConnection]:
125
+ """Find a connection profile by ID."""
126
+ connections = self._client.get_connections()
127
+ for conn in connections:
128
+ if conn.get_id() == conn_id:
129
+ return conn
130
+ return None
131
+
132
+ def _delete_connection_by_id(self, conn_id: str) -> bool:
133
+ """Delete a connection profile by ID."""
134
+ conn = self._find_connection_by_id(conn_id)
135
+ if conn:
136
+ try:
137
+ conn.delete(None)
138
+ logger.info(f"Deleted connection: {conn_id}")
139
+ return True
140
+ except Exception as e:
141
+ logger.error(f"Failed to delete connection {conn_id}: {e}")
142
+ return False
143
+
144
+ def _get_or_make_hotspot_uuid(self) -> str:
145
+ """
146
+ Return a stable UUID for the hotspot profile.
147
+
148
+ We intentionally keep the UUID stable across runs so we can reuse the
149
+ same NetworkManager connection profile (per the upstream DBus example).
150
+ """
151
+ # Force using the same UUID as the provided example for consistency.
152
+ self._hotspot_connection_uuid = self._HOTSPOT_UUID
153
+ return self._hotspot_connection_uuid
154
+
155
+ def _ensure_hotspot_profile_dbus(
156
+ self,
157
+ ssid: str,
158
+ passwd: str,
159
+ band: str = "bg",
160
+ channel: int = 6,
161
+ ) -> Tuple["dbus.SystemBus", str, str]:
162
+ """
163
+ Ensure a NetworkManager hotspot profile exists (DBus), return (bus, devpath, connection_path).
164
+ """
165
+ try:
166
+ import dbus # type: ignore
167
+ except Exception as e: # pragma: no cover
168
+ raise NotSupportedError(f"dbus-python not available: {e}")
169
+
170
+ hotspot_uuid = self._get_or_make_hotspot_uuid()
171
+
172
+ # Build settings dicts (matches upstream example structure)
173
+ s_con = dbus.Dictionary(
174
+ {
175
+ "type": "802-11-wireless",
176
+ "uuid": hotspot_uuid,
177
+ # Keep a stable, human-readable id in NetworkManager UI
178
+ "id": f"iotsploit-hotspot-{self.wifi_iface_name}",
179
+ "autoconnect": dbus.Boolean(False),
180
+ }
181
+ )
182
+
183
+ s_wifi = dbus.Dictionary(
184
+ {
185
+ "ssid": dbus.ByteArray(ssid.encode("utf-8")),
186
+ "mode": "ap",
187
+ "band": band,
188
+ "channel": dbus.UInt32(int(channel)),
189
+ }
190
+ )
191
+
192
+ s_ip4 = dbus.Dictionary({"method": "shared"})
193
+ s_ip6 = dbus.Dictionary({"method": "ignore"})
194
+
195
+ con: Dict[str, Any] = {
196
+ "connection": s_con,
197
+ "802-11-wireless": s_wifi,
198
+ "ipv4": s_ip4,
199
+ "ipv6": s_ip6,
200
+ }
201
+
202
+ # WPA2-PSK (same as example). If passwd empty, we create an open AP.
203
+ if passwd:
204
+ s_wsec = dbus.Dictionary({"key-mgmt": "wpa-psk", "psk": passwd})
205
+ con["802-11-wireless-security"] = s_wsec
206
+
207
+ con = dbus.Dictionary(con)
208
+
209
+ bus = dbus.SystemBus()
210
+
211
+ # Find or create the connection profile under Settings
212
+ settings_proxy = bus.get_object(
213
+ "org.freedesktop.NetworkManager",
214
+ "/org/freedesktop/NetworkManager/Settings",
215
+ )
216
+ settings = dbus.Interface(settings_proxy, "org.freedesktop.NetworkManager.Settings")
217
+
218
+ connection_path = None
219
+ for path in settings.ListConnections():
220
+ proxy = bus.get_object("org.freedesktop.NetworkManager", path)
221
+ settings_connection = dbus.Interface(
222
+ proxy,
223
+ "org.freedesktop.NetworkManager.Settings.Connection",
224
+ )
225
+ config = settings_connection.GetSettings()
226
+ if config.get("connection", {}).get("uuid") == hotspot_uuid:
227
+ connection_path = path
228
+ break
229
+
230
+ if not connection_path:
231
+ connection_path = settings.AddConnection(con)
232
+ logger.info(f"Created hotspot profile via DBus: uuid={hotspot_uuid} path={connection_path}")
233
+ else:
234
+ logger.info(f"Reusing hotspot profile via DBus: uuid={hotspot_uuid} path={connection_path}")
235
+
236
+ # Resolve device path by interface name
237
+ nm_proxy = bus.get_object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
238
+ nm = dbus.Interface(nm_proxy, "org.freedesktop.NetworkManager")
239
+ devpath = nm.GetDeviceByIpIface(self.wifi_iface_name)
240
+
241
+ # Cache for stop/status usage
242
+ self._hotspot_connection_path = str(connection_path)
243
+
244
+ return bus, str(devpath), str(connection_path)
245
+
246
+ def _wait_hotspot_activated_dbus(self, bus: "dbus.SystemBus", acpath: str, timeout: int = 10) -> bool:
247
+ """Wait until the active connection reaches ACTIVATED (DBus)."""
248
+ import dbus # type: ignore
249
+
250
+ proxy = bus.get_object("org.freedesktop.NetworkManager", acpath)
251
+ active_props = dbus.Interface(proxy, "org.freedesktop.DBus.Properties")
252
+
253
+ start = time.time()
254
+ while time.time() < start + timeout:
255
+ try:
256
+ state = int(
257
+ active_props.Get(
258
+ "org.freedesktop.NetworkManager.Connection.Active",
259
+ "State",
260
+ )
261
+ )
262
+ except Exception:
263
+ state = -1
264
+ if state == _NM_ACTIVE_CONNECTION_STATE_ACTIVATED:
265
+ return True
266
+ time.sleep(1)
267
+
268
+ return False
269
+
270
+ def _get_ap_clients(self) -> List[Dict[str, Any]]:
271
+ """
272
+ Get connected clients from DHCP leases.
273
+
274
+ NetworkManager uses dnsmasq for DHCP in shared mode.
275
+ This method parses the DHCP lease files to get connected clients.
276
+ """
277
+ client_list = []
278
+ try:
279
+ # NetworkManager stores leases in /var/lib/NetworkManager/dnsmasq-*.leases
280
+ lease_dir = Path("/var/lib/NetworkManager")
281
+ if lease_dir.exists():
282
+ lease_files = list(lease_dir.glob("dnsmasq-*.leases"))
283
+ for lease_file in lease_files:
284
+ try:
285
+ with open(lease_file, 'r') as f:
286
+ for line in f:
287
+ parts = line.strip().split()
288
+ if len(parts) >= 4:
289
+ client_list.append({
290
+ "mac": parts[1],
291
+ "ip": parts[2],
292
+ "name": parts[3] if len(parts) > 3 else "",
293
+ })
294
+ except Exception as e:
295
+ logger.warning(f"Failed to read lease file {lease_file}: {e}")
296
+
297
+ # Fallback: try traditional dnsmasq location
298
+ if not client_list:
299
+ traditional_lease = Path("/var/lib/misc/dnsmasq.leases")
300
+ if traditional_lease.exists():
301
+ try:
302
+ with open(traditional_lease, 'r') as f:
303
+ for line in f:
304
+ parts = line.strip().split()
305
+ if len(parts) >= 4:
306
+ client_list.append({
307
+ "mac": parts[1],
308
+ "ip": parts[2],
309
+ "name": parts[3] if len(parts) > 3 else "",
310
+ })
311
+ except Exception as e:
312
+ logger.warning(f"Failed to read traditional lease file: {e}")
313
+ except Exception as e:
314
+ logger.warning(f"Failed to get AP clients from DHCP leases: {e}")
315
+
316
+ return client_list
317
+
318
+ # ==================== Public API ====================
319
+
320
+ def scan(self) -> List[Dict[str, Any]]:
321
+ """
322
+ Scan for available WiFi networks.
323
+
324
+ Returns:
325
+ List of dictionaries containing WiFi network information.
326
+ """
327
+ logger.info("Start WiFi scan via NetworkManager")
328
+
329
+ # Request a new scan
330
+ try:
331
+ self._device.request_scan(None)
332
+ except Exception as e:
333
+ logger.warning(f"Scan request failed (may already be scanning): {e}")
334
+
335
+ # Wait for scan to complete
336
+ time.sleep(3)
337
+
338
+ # Get access points
339
+ access_points = self._device.get_access_points()
340
+
341
+ result_list = []
342
+ seen_ssids = set() # Deduplicate by SSID
343
+
344
+ for ap in access_points:
345
+ ssid_bytes = ap.get_ssid()
346
+ if ssid_bytes is None:
347
+ continue
348
+
349
+ ssid = ssid_bytes.get_data().decode('utf-8', errors='ignore')
350
+ if not ssid or ssid in seen_ssids:
351
+ continue
352
+
353
+ seen_ssids.add(ssid)
354
+
355
+ result_list.append({
356
+ "ssid": ssid,
357
+ "bssid": ap.get_bssid(),
358
+ "signal": ap.get_strength(),
359
+ "security": self._get_security_flags_string(ap),
360
+ "frequency": ap.get_frequency(),
361
+ })
362
+ logger.info(f"Found WiFi: {ssid} ({ap.get_bssid()})")
363
+
364
+ logger.info(f"Scan complete. Found {len(result_list)} networks.")
365
+ return result_list
366
+
367
+ def sta_connect(self, ssid: str, passwd: str) -> None:
368
+ """
369
+ Connect to a WiFi network in station (STA) mode.
370
+
371
+ Args:
372
+ ssid: Network SSID to connect to
373
+ passwd: Network password (empty string for open networks)
374
+ """
375
+ logger.info(f"Connecting to WiFi: {ssid}")
376
+
377
+ # Check if already connected to this network
378
+ if (self._wifi_mode == "STA" and
379
+ self._sta_conn_wifi_ssid == ssid and
380
+ self._sta_conn_wifi_passwd == passwd):
381
+ if self._device.get_state() == NM.DeviceState.ACTIVATED:
382
+ logger.info(f"Already connected to {ssid}, skipping")
383
+ return
384
+
385
+ # Disconnect any existing connection first
386
+ self.sta_disconnect()
387
+
388
+ # Store connection info
389
+ self._sta_conn_wifi_ssid = ssid
390
+ self._sta_conn_wifi_passwd = passwd
391
+
392
+ # Create connection profile
393
+ connection_id = f"iotsploit-{ssid}-{uuid.uuid4().hex[:8]}"
394
+
395
+ # Build connection settings
396
+ connection = NM.SimpleConnection.new()
397
+
398
+ # Connection settings
399
+ s_con = NM.SettingConnection.new()
400
+ s_con.set_property(NM.SETTING_CONNECTION_ID, connection_id)
401
+ s_con.set_property(NM.SETTING_CONNECTION_UUID, str(uuid.uuid4()))
402
+ s_con.set_property(NM.SETTING_CONNECTION_TYPE, "802-11-wireless")
403
+ s_con.set_property(NM.SETTING_CONNECTION_AUTOCONNECT, False)
404
+ connection.add_setting(s_con)
405
+
406
+ # Wireless settings
407
+ s_wifi = NM.SettingWireless.new()
408
+ s_wifi.set_property(NM.SETTING_WIRELESS_SSID, GLib.Bytes.new(ssid.encode('utf-8')))
409
+ s_wifi.set_property(NM.SETTING_WIRELESS_MODE, "infrastructure")
410
+ connection.add_setting(s_wifi)
411
+
412
+ # Security settings (if password provided)
413
+ if passwd:
414
+ s_wifi_sec = NM.SettingWirelessSecurity.new()
415
+ s_wifi_sec.set_property(NM.SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-psk")
416
+ s_wifi_sec.set_property(NM.SETTING_WIRELESS_SECURITY_PSK, passwd)
417
+ connection.add_setting(s_wifi_sec)
418
+
419
+ # IPv4 settings (auto/DHCP)
420
+ s_ip4 = NM.SettingIP4Config.new()
421
+ s_ip4.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto")
422
+ connection.add_setting(s_ip4)
423
+
424
+ # IPv6 settings
425
+ s_ip6 = NM.SettingIP6Config.new()
426
+ s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto")
427
+ connection.add_setting(s_ip6)
428
+
429
+ try:
430
+ # Add and activate connection
431
+ self._client.add_and_activate_connection(
432
+ connection,
433
+ self._device,
434
+ None, # specific_object (AP path, optional)
435
+ None # cancellable
436
+ )
437
+
438
+ self._active_connection_id = connection_id
439
+ self._wifi_mode = "STA"
440
+
441
+ # Wait for connection
442
+ if self._wait_for_connection(timeout=30):
443
+ logger.info(f"Successfully connected to {ssid}")
444
+ else:
445
+ logger.error(f"Connection to {ssid} timed out or failed")
446
+ raise ConnectionError(f"Failed to connect to {ssid}")
447
+
448
+ except Exception as e:
449
+ logger.error(f"Failed to connect to {ssid}: {e}")
450
+ self._wifi_mode = "IDLE"
451
+ raise ConnectionError(f"Failed to connect to {ssid}: {e}")
452
+
453
+ def sta_disconnect(self) -> None:
454
+ """
455
+ Disconnect from the current WiFi network in station mode.
456
+ """
457
+ logger.info("Disconnecting from WiFi")
458
+
459
+ if self._wifi_mode != "STA":
460
+ logger.info(f"Not in STA mode (current: {self._wifi_mode}), skipping disconnect")
461
+ return
462
+
463
+ try:
464
+ # Disconnect the device
465
+ self._device.disconnect(None)
466
+
467
+ # Wait for disconnection
468
+ time.sleep(1)
469
+
470
+ # Clean up connection profile
471
+ if self._active_connection_id:
472
+ self._delete_connection_by_id(self._active_connection_id)
473
+ self._active_connection_id = None
474
+
475
+ self._wifi_mode = "IDLE"
476
+ self._sta_conn_wifi_ssid = ""
477
+ self._sta_conn_wifi_passwd = ""
478
+
479
+ logger.info("Disconnected from WiFi")
480
+
481
+ except Exception as e:
482
+ logger.error(f"Error during disconnect: {e}")
483
+ self._wifi_mode = "IDLE"
484
+
485
+ def ap_start(self, ssid: Optional[str] = None, passwd: Optional[str] = None, wpa_mode: int = 2) -> Tuple[str, str]:
486
+ """
487
+ Start WiFi access point (AP) mode using NetworkManager hotspot.
488
+
489
+ Args:
490
+ ssid: AP SSID (if None, a default will be generated)
491
+ passwd: AP password (if None, a default will be used)
492
+ wpa_mode: WPA mode (ignored, NetworkManager uses WPA2 by default)
493
+
494
+ Returns:
495
+ Tuple of (ssid, passwd) that were actually used
496
+ """
497
+ # First disconnect any existing connection
498
+ self.sta_disconnect()
499
+ self.ap_stop()
500
+
501
+ # Generate default SSID if not provided
502
+ if ssid is None:
503
+ hw_addr = self._device.get_hw_address()
504
+ ssid = f"SAT_{hw_addr[-5:-3]}{hw_addr[-2:]}"
505
+
506
+ if passwd is None:
507
+ passwd = "12345678"
508
+
509
+ logger.info(f"Starting AP mode: SSID={ssid}")
510
+
511
+ try:
512
+ import dbus # type: ignore
513
+ from dbus.exceptions import DBusException # type: ignore
514
+
515
+ bus, devpath, connection_path = self._ensure_hotspot_profile_dbus(
516
+ ssid=ssid,
517
+ passwd=passwd,
518
+ band="bg",
519
+ channel=6,
520
+ )
521
+
522
+ nm_proxy = bus.get_object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
523
+ nm = dbus.Interface(nm_proxy, "org.freedesktop.NetworkManager")
524
+
525
+ # ActivateConnection(connection, device, specific_object="/")
526
+ acpath = nm.ActivateConnection(connection_path, devpath, "/")
527
+ self._hotspot_active_connection_path = str(acpath)
528
+
529
+ if not self._wait_hotspot_activated_dbus(bus, str(acpath), timeout=10):
530
+ raise NotSupportedError("Failed to start access point (timeout waiting for ACTIVATED)")
531
+
532
+ logger.info(f"AP started successfully via DBus: {ssid}")
533
+
534
+ except DBusException as e:
535
+ # Common in headless/CLI: polkit cannot prompt -> PermissionDenied
536
+ dbus_name = ""
537
+ try:
538
+ dbus_name = e.get_dbus_name() or ""
539
+ except Exception:
540
+ dbus_name = ""
541
+
542
+ if dbus_name.endswith(".PermissionDenied") or "PermissionDenied" in str(e):
543
+ self._wifi_mode = "IDLE"
544
+ raise NotSupportedError(
545
+ "启动热点被 NetworkManager 拒绝授权:Not authorized to share connections via wifi.\n"
546
+ "这通常需要 polkit 授权(桌面环境会弹窗)或以 root 运行。\n"
547
+ "如果你是在终端/无图形界面运行,请用 sudo 运行当前程序/命令再试。"
548
+ ) from e
549
+
550
+ self._wifi_mode = "IDLE"
551
+ raise NotSupportedError(f"Failed to start AP mode via DBus: {e}") from e
552
+ except Exception as e:
553
+ self._wifi_mode = "IDLE"
554
+ raise NotSupportedError(f"Failed to start AP mode via DBus: {e}") from e
555
+
556
+ # Update local state (common for both DBus/libnm paths)
557
+ self._ap_ssid = ssid
558
+ self._ap_passwd = passwd
559
+ self._wifi_mode = "AP"
560
+ return ssid, passwd
561
+
562
+ def ap_stop(self) -> None:
563
+ """
564
+ Stop WiFi access point (AP) mode.
565
+ """
566
+ logger.info("Stopping AP mode")
567
+
568
+ if self._wifi_mode != "AP":
569
+ logger.info(f"Not in AP mode (current: {self._wifi_mode}), skipping")
570
+ return
571
+
572
+ try:
573
+ import dbus # type: ignore
574
+
575
+ bus = dbus.SystemBus()
576
+ nm_proxy = bus.get_object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
577
+ nm = dbus.Interface(nm_proxy, "org.freedesktop.NetworkManager")
578
+ devpath = nm.GetDeviceByIpIface(self.wifi_iface_name)
579
+
580
+ proxy = bus.get_object("org.freedesktop.NetworkManager", devpath)
581
+ device = dbus.Interface(proxy, "org.freedesktop.NetworkManager.Device")
582
+ device.Disconnect()
583
+
584
+ time.sleep(1)
585
+
586
+ # 重要:按 DBus 示例逻辑,默认不删除 hotspot profile,方便下次复用
587
+ self._hotspot_active_connection_path = None
588
+
589
+ self._wifi_mode = "IDLE"
590
+ self._ap_ssid = ""
591
+ self._ap_passwd = ""
592
+
593
+ logger.info("AP stopped successfully")
594
+
595
+ except Exception as e:
596
+ logger.error(f"Error stopping AP: {e}")
597
+ self._wifi_mode = "IDLE"
598
+
599
+ def status(self) -> Dict[str, Any]:
600
+ """
601
+ Get current WiFi status.
602
+
603
+ Returns:
604
+ Dictionary containing current WiFi status information.
605
+ """
606
+ status_dict = {
607
+ "wifi_mode": self._wifi_mode,
608
+ "device_state": str(self._device.get_state().value_nick),
609
+ "hw_address": self._device.get_hw_address(),
610
+ }
611
+
612
+ if self._wifi_mode == "STA":
613
+ status_dict["sta_conn_wifi_ssid"] = self._sta_conn_wifi_ssid
614
+ status_dict["sta_conn_wifi_passwd"] = self._sta_conn_wifi_passwd
615
+
616
+ # Get active connection info
617
+ active_conn = self._device.get_active_connection()
618
+ if active_conn:
619
+ ip4_config = active_conn.get_ip4_config()
620
+ if ip4_config:
621
+ addresses = ip4_config.get_addresses()
622
+ if addresses:
623
+ status_dict["sta_status"] = {
624
+ "ip_address": addresses[0].get_address(),
625
+ "state": active_conn.get_state().value_nick,
626
+ }
627
+
628
+ if self._wifi_mode == "AP":
629
+ status_dict["ap_ssid"] = self._ap_ssid
630
+ status_dict["ap_passwd"] = self._ap_passwd
631
+
632
+ # Get connected clients from DHCP leases
633
+ status_dict["client_list"] = self._get_ap_clients()
634
+
635
+ logger.info(f"Status: {status_dict}")
636
+ return status_dict
637
+
638
+
639
+ # Export the backend class
640
+ wifi_backend = LinuxWifiBackend
@@ -0,0 +1,7 @@
1
+ """
2
+ Windows platform adapters.
3
+ """
4
+
5
+ from iotsploit_platforms.adapters.platforms.windows.wifi_backend import wifi_backend
6
+
7
+ __all__ = ["wifi_backend"]
@@ -0,0 +1,93 @@
1
+ """
2
+ Windows WiFi Backend Implementation.
3
+
4
+ This module provides Windows-specific WiFi backend implementation.
5
+ Currently a placeholder implementation.
6
+ """
7
+
8
+ import logging
9
+ from typing import List, Optional, Dict, Any, Tuple
10
+
11
+ from iotsploit_core.ports.wifi_backend import WifiBackend
12
+ from iotsploit_core.utils.exceptions import NotSupportedError
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class WindowsWifiBackend(WifiBackend):
18
+ """
19
+ Windows WiFi backend implementation.
20
+
21
+ This is a placeholder implementation. Full implementation would use
22
+ Windows-specific APIs (e.g., netsh, Windows WLAN API).
23
+ """
24
+
25
+ def __init__(self, wifi_iface_name: Optional[str] = None):
26
+ """
27
+ Initialize Windows WiFi backend.
28
+
29
+ Args:
30
+ wifi_iface_name: WiFi interface name (optional on Windows)
31
+ """
32
+ self.wifi_iface_name = wifi_iface_name
33
+ self._wifi_mode = "IDLE"
34
+
35
+ def scan(self) -> List[Dict[str, Any]]:
36
+ """
37
+ Scan for available WiFi networks.
38
+
39
+ Raises:
40
+ NotSupportedError: Windows WiFi scanning not yet implemented
41
+ """
42
+ raise NotSupportedError("WiFi scanning is not yet implemented on Windows")
43
+
44
+ def sta_connect(self, ssid: str, passwd: str) -> None:
45
+ """
46
+ Connect to a WiFi network in station (STA) mode.
47
+
48
+ Raises:
49
+ NotSupportedError: Windows WiFi connection not yet implemented
50
+ """
51
+ raise NotSupportedError("WiFi station mode connection is not yet implemented on Windows")
52
+
53
+ def sta_disconnect(self) -> None:
54
+ """
55
+ Disconnect from the current WiFi network in station mode.
56
+
57
+ Raises:
58
+ NotSupportedError: Windows WiFi disconnection not yet implemented
59
+ """
60
+ raise NotSupportedError("WiFi station mode disconnection is not yet implemented on Windows")
61
+
62
+ def ap_start(self, ssid: Optional[str] = None, passwd: Optional[str] = None, wpa_mode: int = 2) -> Tuple[str, str]:
63
+ """
64
+ Start WiFi access point (AP) mode.
65
+
66
+ Raises:
67
+ NotSupportedError: Windows does not support WiFi AP mode
68
+ """
69
+ raise NotSupportedError("WiFi access point mode is not supported on Windows")
70
+
71
+ def ap_stop(self) -> None:
72
+ """
73
+ Stop WiFi access point (AP) mode.
74
+
75
+ Raises:
76
+ NotSupportedError: Windows does not support WiFi AP mode
77
+ """
78
+ raise NotSupportedError("WiFi access point mode is not supported on Windows")
79
+
80
+ def status(self) -> Dict[str, Any]:
81
+ """
82
+ Get current WiFi status.
83
+
84
+ Returns:
85
+ Dictionary containing current WiFi status information.
86
+ """
87
+ return {
88
+ "wifi_mode": self._wifi_mode,
89
+ }
90
+
91
+
92
+ # Export the backend class
93
+ wifi_backend = WindowsWifiBackend
@@ -0,0 +1,21 @@
1
+ """
2
+ Platform Distribution Module (Scapy-Style).
3
+
4
+ This module provides platform-specific backend implementations based on the
5
+ current platform. It uses conditional imports to select the appropriate
6
+ platform adapters at import time.
7
+ """
8
+
9
+ from iotsploit_core.platforms.consts import LINUX, DARWIN, WINDOWS, PLATFORM
10
+ from iotsploit_core.utils.exceptions import NotSupportedError
11
+
12
+ if LINUX:
13
+ from iotsploit_platforms.adapters.platforms.linux import wifi_backend
14
+ elif DARWIN:
15
+ from iotsploit_platforms.adapters.platforms.darwin import wifi_backend
16
+ elif WINDOWS:
17
+ from iotsploit_platforms.adapters.platforms.windows import wifi_backend
18
+ else:
19
+ raise NotSupportedError(f"Platform {PLATFORM} not supported")
20
+
21
+ __all__ = ["wifi_backend"]
@@ -0,0 +1,132 @@
1
+ """
2
+ Platform Backend Selector.
3
+
4
+ This module provides functions to select and instantiate platform-specific
5
+ backends based on the current operating system. It serves as the main entry
6
+ point for applications (CLI/Django/MCP) to obtain backend instances.
7
+
8
+ The selector automatically chooses the appropriate implementation for the
9
+ current platform and handles backend instantiation with proper parameters.
10
+
11
+ Configuration is read from environment variables:
12
+ - WiFi: IOTSPLOIT_WIFI_IFACE, IOTSPLOIT_WIFI_FORWARD_ETH
13
+ - (Add more backends here as they are implemented)
14
+ """
15
+
16
+ import inspect
17
+ import os
18
+ from typing import Optional
19
+
20
+ from iotsploit_core.context import PluginContext
21
+ from iotsploit_core.platforms.consts import LINUX
22
+ from iotsploit_platforms.platforms import wifi_backend
23
+
24
+
25
+ # =============================================================================
26
+ # Backend Configuration from Environment
27
+ # =============================================================================
28
+
29
+ def _get_wifi_config() -> dict:
30
+ """Get WiFi backend configuration from environment variables."""
31
+ return {
32
+ "wifi_iface_name": os.getenv("IOTSPLOIT_WIFI_IFACE", "wlp6s0"),
33
+ "forward_eth_name": os.getenv("IOTSPLOIT_WIFI_FORWARD_ETH"),
34
+ }
35
+
36
+
37
+ # TODO: Add more backend config functions as they are implemented
38
+ # def _get_bluetooth_config() -> dict:
39
+ # return {
40
+ # "adapter": os.getenv("IOTSPLOIT_BLUETOOTH_ADAPTER", "hci0"),
41
+ # }
42
+
43
+
44
+ # =============================================================================
45
+ # Backend Factory Functions
46
+ # =============================================================================
47
+
48
+ def get_wifi_backend(
49
+ wifi_iface_name: Optional[str] = None,
50
+ forward_eth_name: Optional[str] = None
51
+ ) -> wifi_backend:
52
+ """
53
+ Get a WiFi backend instance for the current platform.
54
+
55
+ This function automatically selects the appropriate WiFi backend
56
+ implementation based on the current operating system (Linux/Windows/macOS)
57
+ and returns an instantiated backend.
58
+
59
+ Args:
60
+ wifi_iface_name: WiFi interface name. If None, reads from IOTSPLOIT_WIFI_IFACE env var.
61
+ forward_eth_name: Ethernet interface for forwarding. If None, reads from IOTSPLOIT_WIFI_FORWARD_ETH.
62
+
63
+ Returns:
64
+ An instance of the platform-specific WiFi backend class
65
+
66
+ Example:
67
+ >>> backend = get_wifi_backend() # Uses env vars
68
+ >>> backend = get_wifi_backend(wifi_iface_name="wlan0") # Override
69
+ """
70
+ # Get config from env if not provided
71
+ if wifi_iface_name is None or forward_eth_name is None:
72
+ env_config = _get_wifi_config()
73
+ if wifi_iface_name is None:
74
+ wifi_iface_name = env_config["wifi_iface_name"]
75
+ if forward_eth_name is None:
76
+ forward_eth_name = env_config["forward_eth_name"]
77
+
78
+ # wifi_backend is the class imported from platforms/__init__.py
79
+ # which is already platform-specific (LinuxWifiBackend, WindowsWifiBackend, etc.)
80
+
81
+ # Check if the constructor accepts forward_eth_name parameter
82
+ # Linux backend accepts it, Windows/Darwin don't
83
+ sig = inspect.signature(wifi_backend.__init__)
84
+ params = sig.parameters
85
+
86
+ if 'forward_eth_name' in params:
87
+ # Linux backend - supports forward_eth_name
88
+ return wifi_backend(
89
+ wifi_iface_name=wifi_iface_name,
90
+ forward_eth_name=forward_eth_name
91
+ )
92
+ else:
93
+ # Windows/Darwin backend - only accepts wifi_iface_name
94
+ return wifi_backend(wifi_iface_name=wifi_iface_name)
95
+
96
+
97
+ # =============================================================================
98
+ # Context Builder (Main Entry Point)
99
+ # =============================================================================
100
+
101
+ def build_context() -> PluginContext:
102
+ """
103
+ Build a PluginContext with all available backends.
104
+
105
+ This function creates a structured context object containing all
106
+ platform-specific backends. Configuration is automatically read from
107
+ environment variables.
108
+
109
+ This is the recommended way to inject backends into plugins.
110
+ Adding a new backend only requires:
111
+ 1. Add field to PluginContext
112
+ 2. Add _get_xxx_config() function
113
+ 3. Add get_xxx_backend() function
114
+ 4. Add backend instance to PluginContext here
115
+
116
+ Environment variables:
117
+ - WiFi: IOTSPLOIT_WIFI_IFACE, IOTSPLOIT_WIFI_FORWARD_ETH
118
+ - (More backends as implemented)
119
+
120
+ Returns:
121
+ A PluginContext instance with all backends initialized
122
+
123
+ Example:
124
+ >>> ctx = build_context() # Reads config from env
125
+ >>> plugin.initialize(ctx)
126
+ >>> # Plugin can access ctx.wifi, ctx.bluetooth, etc.
127
+ """
128
+ return PluginContext(
129
+ wifi=get_wifi_backend(), # Uses env vars automatically
130
+ # bluetooth=get_bluetooth_backend(), # TODO: Add when implemented
131
+ # camera=get_camera_backend(), # TODO: Add when implemented
132
+ )
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.3
2
+ Name: iotsploit-platforms
3
+ Version: 0.0.6
4
+ Summary: IoTSploit platform-specific adapters (WiFi, Input, SSH backends)
5
+ License: GPL-3.0-or-later
6
+ Keywords: iot,security,testing,pentest,wifi,platform
7
+ Author: IoTSploit Team
8
+ Author-email: support@iotsploit.org
9
+ Requires-Python: >=3.10,<4.0
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Information Technology
13
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Security
20
+ Classifier: Topic :: System :: Hardware
21
+ Requires-Dist: PyGObject (<3.51)
22
+ Requires-Dist: dbus-python (>=1.4.0,<2.0.0)
23
+ Requires-Dist: iotsploit-core (>=0.0.6,<0.0.7)
24
+ Project-URL: Documentation, https://www.iotsploit.org/
25
+ Project-URL: Homepage, https://www.iotsploit.org/
26
+ Project-URL: Repository, https://github.com/TKXB/iotsploit
27
+ Description-Content-Type: text/markdown
28
+
29
+ # iotsploit-platforms
30
+
31
+ IoTSploit platform-specific adapters package.
32
+
33
+ This package provides platform-specific implementations for WiFi, Input, and SSH backends across different operating systems.
34
+
35
+ ## Structure
36
+
37
+ ```
38
+ iotsploit-platforms/
39
+ ├── src/
40
+ │ └── iotsploit_platforms/
41
+ │ ├── adapters/
42
+ │ │ └── platforms/
43
+ │ │ ├── linux/
44
+ │ │ │ └── wifi_backend.py
45
+ │ │ ├── windows/
46
+ │ │ │ └── wifi_backend.py
47
+ │ │ └── darwin/
48
+ │ │ └── wifi_backend.py
49
+ │ └── platforms/
50
+ │ └── __init__.py # Platform distribution module
51
+ └── pyproject.toml
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ The platform distribution module automatically selects the appropriate backend based on the current platform:
57
+
58
+ ```python
59
+ from iotsploit_platforms.platforms import wifi_backend
60
+
61
+ # wifi_backend is the appropriate backend class for the current platform
62
+ backend = wifi_backend(wifi_iface_name="wlan0")
63
+ networks = backend.scan()
64
+ ```
65
+
66
+ ## Platform Support
67
+
68
+ - **Linux**: Full WiFi backend implementation using pywifi, hostapd, and dnsmasq
69
+ - **Windows**: Placeholder implementation (not yet implemented)
70
+ - **Darwin (macOS)**: Placeholder implementation (not yet implemented)
71
+
72
+ ## Dependencies
73
+
74
+ - `iotsploit-core`: Core interfaces and utilities
75
+ - `pywifi`: WiFi operations on Linux
76
+ - `netifaces`: Network interface information
77
+
@@ -0,0 +1,14 @@
1
+ iotsploit_platforms/__init__.py,sha256=qIdcGSBfms7xHrTdjngKWHHyxlAwfHwfjMnUzYx3MKk,282
2
+ iotsploit_platforms/adapters/__init__.py,sha256=t5j3JOE_1IwKyPZR52ZJS6vCzWQ6S6b60SLWdc4x_Zw,107
3
+ iotsploit_platforms/adapters/platforms/__init__.py,sha256=cd_FlS_tHhdY8IevxcRajB62wZxUh7qV-9pJpjQ7mEM,130
4
+ iotsploit_platforms/adapters/platforms/darwin/__init__.py,sha256=Kf9rdA5Iys-ETfusqwwwRyVsjsSgRtZKEQS8oJDwiJs,155
5
+ iotsploit_platforms/adapters/platforms/darwin/wifi_backend.py,sha256=nwKtHp0UwoisLAh4Oe8ik1Q8FKCj3LqpOQt8veg3Amo,2839
6
+ iotsploit_platforms/adapters/platforms/linux/__init__.py,sha256=sk8_KV-TUxowSisdml8XP9CnMLk7T2mD_3qcPh34erU,145
7
+ iotsploit_platforms/adapters/platforms/linux/wifi_backend.py,sha256=6asQ8gNhRFk2xXweXR2_zUlWzP4cCWh07oCwSw4AFLg,24539
8
+ iotsploit_platforms/adapters/platforms/windows/__init__.py,sha256=orEvxsa_mEK-Agp0IXdn679JLotLvum3aTP5SW2toV4,149
9
+ iotsploit_platforms/adapters/platforms/windows/wifi_backend.py,sha256=MH9oytRnlegOiThDwqZH75HhTYToTUMer_mpq1dKkX4,2850
10
+ iotsploit_platforms/platforms/__init__.py,sha256=-pOe3_AunsxV3qtFqb8ZDcVpnRKATOsqp4BJJHoG4-I,738
11
+ iotsploit_platforms/selector.py,sha256=4r77t4U3CUtP5EJfD3d_BdewpId2Iwwz5YOW79ET1-Q,4930
12
+ iotsploit_platforms-0.0.6.dist-info/METADATA,sha256=GzKDRfYw4z-lc0O51uUIEqu6wdmfeaD9eHuYUiOFLpA,2665
13
+ iotsploit_platforms-0.0.6.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
14
+ iotsploit_platforms-0.0.6.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.0.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any