Skip to content

Commit e2c4057

Browse files
...
1 parent 3462aba commit e2c4057

3 files changed

Lines changed: 206 additions & 0 deletions

File tree

scripts/friday_communication.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

scripts/rift_communication.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

scripts/scan_wifi.py

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Multi-method WiFi device scanner.
4+
Combines ARP, ping sweep, and nmap for best coverage.
5+
"""
6+
7+
import socket
8+
import subprocess
9+
import ipaddress
10+
import requests
11+
import sys
12+
import os
13+
from concurrent.futures import ThreadPoolExecutor, as_completed
14+
15+
# Try importing scapy
16+
try:
17+
from scapy.all import ARP, Ether, srp, conf
18+
SCAPY_AVAILABLE = True
19+
except ImportError:
20+
SCAPY_AVAILABLE = False
21+
22+
def get_local_ip_and_subnet():
23+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
24+
s.connect(("8.8.8.8", 80))
25+
ip = s.getsockname()[0]
26+
s.close()
27+
subnet = ip.rsplit('.', 1)[0] + ".0/24"
28+
return ip, subnet
29+
30+
def get_vendor(mac):
31+
try:
32+
url = f"https://api.macvendors.com/{mac}"
33+
r = requests.get(url, timeout=3)
34+
if r.status_code == 200:
35+
return r.text.strip()
36+
except:
37+
pass
38+
return "Unknown"
39+
40+
def get_hostname(ip):
41+
try:
42+
return socket.gethostbyaddr(ip)[0]
43+
except:
44+
return ""
45+
46+
def ping_host(ip):
47+
"""Ping a single IP. Returns IP if alive, else None."""
48+
result = subprocess.run(
49+
["ping", "-c", "1", "-W", "1", str(ip)],
50+
stdout=subprocess.DEVNULL,
51+
stderr=subprocess.DEVNULL
52+
)
53+
return str(ip) if result.returncode == 0 else None
54+
55+
def ping_sweep(subnet):
56+
"""Ping every host in the subnet in parallel."""
57+
print(" [1/3] Running ping sweep (wakes up sleeping devices)...")
58+
network = ipaddress.ip_network(subnet, strict=False)
59+
alive = set()
60+
hosts = list(network.hosts())
61+
with ThreadPoolExecutor(max_workers=100) as executor:
62+
futures = {executor.submit(ping_host, ip): ip for ip in hosts}
63+
for future in as_completed(futures):
64+
result = future.result()
65+
if result:
66+
alive.add(result)
67+
print(f" → Found {len(alive)} host(s) responding to ping")
68+
return alive
69+
70+
def arp_scan(subnet):
71+
"""ARP scan using scapy."""
72+
print(" [2/3] Running ARP scan...")
73+
if not SCAPY_AVAILABLE:
74+
print(" → Scapy not available, skipping")
75+
return {}
76+
77+
conf.verb = 0
78+
packet = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=subnet)
79+
answered, _ = srp(packet, timeout=3, verbose=False, retry=2)
80+
81+
results = {}
82+
for sent, received in answered:
83+
results[received.psrc] = received.hwsrc
84+
print(f" → Found {len(results)} host(s) via ARP")
85+
return results
86+
87+
def read_arp_table():
88+
"""Read the kernel ARP cache using 'ip neigh' (modern Linux)."""
89+
print(" [3/3] Reading ARP cache...")
90+
mac_map = {}
91+
try:
92+
output = subprocess.check_output(["ip", "neigh"], text=True)
93+
for line in output.splitlines():
94+
parts = line.split()
95+
# Format: <IP> dev <iface> lladdr <MAC> <STATE>
96+
if "lladdr" in parts:
97+
ip = parts[0]
98+
mac = parts[parts.index("lladdr") + 1]
99+
if "FAILED" not in line and "INCOMPLETE" not in line:
100+
mac_map[ip] = mac
101+
except Exception as e:
102+
print(f" → Could not read ARP table: {e}")
103+
print(f" → {len(mac_map)} entr(ies) in ARP cache")
104+
return mac_map
105+
106+
def is_randomized_mac(mac):
107+
"""
108+
Phones randomize their MAC. The 2nd-least-significant bit of the first octet
109+
being set (locally administered) is the giveaway.
110+
e.g. 22:... 12:... A2:... — first octet is even but bit 1 is set.
111+
"""
112+
try:
113+
first_octet = int(mac.split(":")[0], 16)
114+
return bool(first_octet & 0x02) # locally administered bit
115+
except:
116+
return False
117+
118+
def guess_device_type(mac, hostname, vendor):
119+
"""Make a best-guess at device type from available clues."""
120+
if is_randomized_mac(mac):
121+
return "📱 Phone/Tablet (randomized MAC — privacy mode)"
122+
h = (hostname + vendor).lower()
123+
if any(x in h for x in ["iphone", "apple", "ipad"]):
124+
return "🍎 Apple device"
125+
if any(x in h for x in ["android", "samsung", "xiaomi", "huawei", "oppo", "oneplus"]):
126+
return "📱 Android device"
127+
if any(x in h for x in ["router", "gateway", "dlink", "tp-link", "tplink", "asus", "netgear", "huawei", "zte", "technicolor"]):
128+
return "📡 Router/Gateway"
129+
if any(x in h for x in ["windows", "desktop", "laptop", "intel", "realtek"]):
130+
return "💻 PC/Laptop"
131+
if any(x in h for x in ["ubuntu", "linux", "debian", "arch", "raspi", "raspberry"]):
132+
return "🐧 Linux device"
133+
if any(x in h for x in ["smart", "tv", "cast", "roku", "fire", "echo", "alexa", "nest", "ring"]):
134+
return "📺 Smart TV/IoT"
135+
if vendor and vendor != "Unknown":
136+
return f"🔌 {vendor}"
137+
return "❓ Unknown"
138+
139+
def format_mac(mac):
140+
"""Normalise to upper-case colon-separated."""
141+
return mac.replace("-", ":").upper()
142+
143+
def scan(subnet):
144+
print(f"\nScanning {subnet} (this may take ~15 seconds)\n")
145+
146+
# Step 1: ping sweep — forces devices to appear in ARP table
147+
ping_sweep(subnet)
148+
149+
# Step 2: ARP scan via scapy
150+
arp_direct = arp_scan(subnet)
151+
152+
# Step 3: Read kernel ARP cache (populated by ping sweep)
153+
arp_cache = read_arp_table()
154+
155+
# Merge all sources
156+
all_ips = set(arp_direct.keys()) | set(arp_cache.keys())
157+
158+
devices = []
159+
for ip in sorted(all_ips, key=lambda x: list(map(int, x.split('.')))):
160+
mac = arp_direct.get(ip) or arp_cache.get(ip) or "??:??:??:??:??:??"
161+
mac = format_mac(mac)
162+
devices.append({"ip": ip, "mac": mac})
163+
164+
return devices
165+
166+
def enrich(devices, my_ip):
167+
print(f"\nEnriching {len(devices)} device(s) (hostname + vendor)...\n")
168+
enriched = []
169+
for d in devices:
170+
hostname = get_hostname(d["ip"])
171+
vendor = get_vendor(d["mac"]) if not is_randomized_mac(d["mac"]) else "N/A (randomized)"
172+
dtype = guess_device_type(d["mac"], hostname, vendor)
173+
label = " ← THIS MACHINE" if d["ip"] == my_ip else ""
174+
enriched.append({**d, "hostname": hostname, "vendor": vendor, "type": dtype, "label": label})
175+
return enriched
176+
177+
def print_table(devices):
178+
print("\n" + "═" * 90)
179+
print(f" {'IP':<16} {'MAC':<20} {'HOSTNAME':<22} DEVICE TYPE")
180+
print("═" * 90)
181+
for d in devices:
182+
hostname = (d["hostname"] or "-")[:21]
183+
print(f" {d['ip']:<16} {d['mac']:<20} {hostname:<22} {d['type']}{d['label']}")
184+
print("═" * 90)
185+
print(f" Total: {len(devices)} device(s) found")
186+
rand = sum(1 for d in devices if is_randomized_mac(d["mac"]))
187+
if rand:
188+
print(f" ℹ {rand} device(s) use randomized MACs (modern phones/tablets) — vendor lookup not possible")
189+
print()
190+
191+
if __name__ == "__main__":
192+
if os.geteuid() != 0:
193+
print("⚠ Run with sudo for best results: sudo -E ./venv/bin/python scan_wifi.py")
194+
sys.exit(1)
195+
196+
my_ip, subnet = get_local_ip_and_subnet()
197+
devices = scan(subnet)
198+
199+
if not devices:
200+
print("\nNo devices found. Try running with sudo.")
201+
sys.exit(0)
202+
203+
enriched = enrich(devices, my_ip)
204+
print_table(enriched)

0 commit comments

Comments
 (0)