""" Honeypot Geo-IP & MAC Vendor Lookup. Provides IP geolocation (via ip-api.com with SQLite cache) and MAC address vendor identification (OUI database). Background worker thread resolves public IPs without blocking. """ import ipaddress import json import os import queue import sqlite3 import threading import time import urllib.request import urllib.error # ============================================================ # OUI vendor map — top ~200 manufacturers by IEEE prefix # ============================================================ OUI_MAP = { # Apple "AC:DE:48": "Apple", "3C:22:FB": "Apple", "F0:18:98": "Apple", "DC:A9:04": "Apple", "A4:83:E7": "Apple", "F4:5C:89": "Apple", "78:7B:8A": "Apple", "88:66:A5": "Apple", "40:A6:D9": "Apple", # Samsung "00:07:AB": "Samsung", "AC:5F:3E": "Samsung", "78:47:1D": "Samsung", "D0:22:BE": "Samsung", "C4:73:1E": "Samsung", "50:01:BB": "Samsung", # Espressif (ESP32/ESP8266) "24:0A:C4": "Espressif", "30:AE:A4": "Espressif", "AC:67:B2": "Espressif", "24:62:AB": "Espressif", "A4:CF:12": "Espressif", "CC:50:E3": "Espressif", "84:CC:A8": "Espressif", "10:52:1C": "Espressif", "7C:DF:A1": "Espressif", "3C:61:05": "Espressif", "EC:FA:BC": "Espressif", "08:3A:F2": "Espressif", "34:AB:95": "Espressif", "C8:C9:A3": "Espressif", "E8:DB:84": "Espressif", # Raspberry Pi "B8:27:EB": "Raspberry Pi", "DC:A6:32": "Raspberry Pi", "E4:5F:01": "Raspberry Pi", "28:CD:C1": "Raspberry Pi", "D8:3A:DD": "Raspberry Pi", # TP-Link "50:C7:BF": "TP-Link", "14:CF:92": "TP-Link", "E8:DE:27": "TP-Link", "60:E3:27": "TP-Link", "B0:BE:76": "TP-Link", "A4:2B:B0": "TP-Link", # Cisco "00:1A:A1": "Cisco", "00:1B:0D": "Cisco", "00:22:BD": "Cisco", "00:25:B5": "Cisco", "00:1E:F7": "Cisco", "64:F6:9D": "Cisco", # Intel "00:1E:67": "Intel", "3C:97:0E": "Intel", "8C:8D:28": "Intel", "A4:C3:F0": "Intel", "48:51:B7": "Intel", "80:86:F2": "Intel", # Realtek "00:E0:4C": "Realtek", "52:54:00": "Realtek/QEMU", # Qualcomm / Atheros "00:03:7F": "Atheros", "1C:B7:2C": "Qualcomm", # Broadcom "00:10:18": "Broadcom", "20:10:7A": "Broadcom", # MediaTek "00:0C:E7": "MediaTek", "9C:D2:1E": "MediaTek", # Huawei "00:E0:FC": "Huawei", "48:46:FB": "Huawei", "88:53:D4": "Huawei", "70:8A:09": "Huawei", "CC:A2:23": "Huawei", # Xiaomi "64:09:80": "Xiaomi", "28:6C:07": "Xiaomi", "78:11:DC": "Xiaomi", "F8:A4:5F": "Xiaomi", "7C:1C:4E": "Xiaomi", # Google / Nest "54:60:09": "Google", "F4:F5:D8": "Google", "A4:77:33": "Google", # Amazon "44:65:0D": "Amazon", "68:54:FD": "Amazon", "40:B4:CD": "Amazon", # Microsoft (Xbox, Surface) "28:18:78": "Microsoft", "7C:1E:52": "Microsoft", # Dell "00:14:22": "Dell", "18:03:73": "Dell", "F8:DB:88": "Dell", # HP / HPE "00:1A:4B": "HP", "3C:D9:2B": "HP", "00:1E:0B": "HP", # Lenovo "00:06:1B": "Lenovo", "28:D2:44": "Lenovo", "E8:6A:64": "Lenovo", # Asus "00:1A:92": "ASUS", "04:D4:C4": "ASUS", "1C:87:2C": "ASUS", # Netgear "00:1E:2A": "Netgear", "20:E5:2A": "Netgear", "C4:04:15": "Netgear", # D-Link "00:1B:11": "D-Link", "1C:7E:E5": "D-Link", "B0:C5:54": "D-Link", # Ubiquiti "04:18:D6": "Ubiquiti", "24:5A:4C": "Ubiquiti", "FC:EC:DA": "Ubiquiti", "68:D7:9A": "Ubiquiti", "78:8A:20": "Ubiquiti", # MikroTik "00:0C:42": "MikroTik", "4C:5E:0C": "MikroTik", "64:D1:54": "MikroTik", # Synology "00:11:32": "Synology", # Hikvision "C0:56:E3": "Hikvision", "44:47:CC": "Hikvision", # Dahua "3C:EF:8C": "Dahua", "A0:BD:1D": "Dahua", # Aruba / HPE Networking "00:0B:86": "Aruba", "24:DE:C6": "Aruba", # VMware "00:50:56": "VMware", "00:0C:29": "VMware", # Linux KVM / VirtualBox "08:00:27": "VirtualBox", # ZTE "00:19:CB": "ZTE", "34:4B:50": "ZTE", # Honeywell "00:40:84": "Honeywell", # Siemens "00:0E:8C": "Siemens", # ABB "00:80:F4": "ABB", # Schneider Electric "00:80:F4": "Schneider", # Sonos "B8:E9:37": "Sonos", "54:2A:1B": "Sonos", # Sony "00:1D:BA": "Sony", "AC:9B:0A": "Sony", # LG "00:1E:75": "LG", "C4:36:55": "LG", # OnePlus / Oppo "94:65:2D": "OnePlus", "A4:3B:FA": "Oppo", # Ring / Blink "CC:9E:A2": "Ring/Amazon", # Tuya IoT "D8:1F:12": "Tuya", # Shelly "E8:68:E7": "Shelly", "EC:FA:BC": "Shelly/Espressif", # Nordic Semiconductor "E7:3E:B6": "Nordic Semi", # Texas Instruments "00:12:4B": "TI", "D4:F5:13": "TI", } class HpGeoLookup: """ IP geolocation with SQLite cache + MAC vendor lookup. Background thread resolves public IPs via ip-api.com. """ def __init__(self, db_path=None): if db_path is None: db_path = os.path.join(os.path.dirname(__file__), "honeypot_geo.db") self._db_path = db_path self._lock = threading.Lock() self._queue = queue.Queue(maxsize=500) self._cache = {} # in-memory hot cache: ip -> dict self._rate_count = 0 self._rate_reset = time.time() + 60 self._init_db() self._load_cache() # Background worker thread self._worker = threading.Thread(target=self._resolver_worker, daemon=True) self._worker.start() def _init_db(self): conn = sqlite3.connect(self._db_path) conn.execute("""CREATE TABLE IF NOT EXISTS geo_cache ( ip TEXT PRIMARY KEY, country TEXT, country_code TEXT, city TEXT, isp TEXT, lat REAL, lon REAL, is_private INTEGER DEFAULT 0, cached_at REAL )""") conn.commit() conn.close() def _load_cache(self): """Load recent cache entries into memory.""" try: conn = sqlite3.connect(self._db_path) conn.row_factory = sqlite3.Row cutoff = time.time() - 86400 * 7 # 7 days rows = conn.execute( "SELECT * FROM geo_cache WHERE cached_at > ?", (cutoff,) ).fetchall() for r in rows: self._cache[r["ip"]] = dict(r) conn.close() except Exception: pass def lookup_ip(self, ip): """ Look up IP geolocation. Returns cached result or enqueues for background resolution. Non-blocking. """ if not ip or ip == "0.0.0.0": return {"country": "", "country_code": "", "is_private": True} # Check memory cache (with 7-day TTL) with self._lock: cached = self._cache.get(ip) if cached: if time.time() - cached.get("cached_at", 0) < 86400 * 7: return cached del self._cache[ip] # Private IP — instant result try: if ipaddress.ip_address(ip).is_private: result = { "ip": ip, "country": "LAN", "country_code": "", "city": "", "isp": "Private Network", "lat": 0, "lon": 0, "is_private": 1, "cached_at": time.time(), } with self._lock: self._cache[ip] = result self._persist(result) return result except ValueError: return {"country": "", "country_code": "", "is_private": False} # Enqueue for background resolution try: self._queue.put_nowait(ip) except queue.Full: pass return {"country": "...", "country_code": "", "is_private": False, "pending": True} def lookup_mac_vendor(self, mac): """Instant MAC vendor lookup from OUI map.""" if not mac or mac == "00:00:00:00:00:00": return "" prefix = mac[:8].upper() return OUI_MAP.get(prefix, "") def enrich_attacker(self, attacker): """Add geo and vendor info to an attacker dict.""" geo = self.lookup_ip(attacker.get("ip", "")) attacker["country"] = geo.get("country", "") attacker["country_code"] = geo.get("country_code", "") attacker["city"] = geo.get("city", "") attacker["isp"] = geo.get("isp", "") attacker["is_private"] = geo.get("is_private", False) attacker["vendor"] = self.lookup_mac_vendor(attacker.get("mac", "")) return attacker def _persist(self, result): """Write geo result to SQLite cache.""" try: conn = sqlite3.connect(self._db_path) conn.execute( """INSERT OR REPLACE INTO geo_cache (ip, country, country_code, city, isp, lat, lon, is_private, cached_at) VALUES (?,?,?,?,?,?,?,?,?)""", (result["ip"], result.get("country", ""), result.get("country_code", ""), result.get("city", ""), result.get("isp", ""), result.get("lat", 0), result.get("lon", 0), result.get("is_private", 0), result.get("cached_at", time.time())) ) conn.commit() conn.close() except Exception: pass def _resolver_worker(self): """Background thread: resolve IPs from queue via ip-api.com.""" while True: try: ip = self._queue.get(timeout=5) except queue.Empty: continue # Already cached? with self._lock: if ip in self._cache: continue # Rate limit: 45 requests per minute now = time.time() if now > self._rate_reset: self._rate_count = 0 self._rate_reset = now + 60 if self._rate_count >= 45: # Wait until reset time.sleep(max(0, self._rate_reset - now)) self._rate_count = 0 self._rate_reset = time.time() + 60 try: url = f"http://ip-api.com/json/{ip}?fields=status,country,countryCode,city,isp,lat,lon" req = urllib.request.Request(url, headers={"User-Agent": "Espilon-HP/1.0"}) resp = urllib.request.urlopen(req, timeout=5) data = json.loads(resp.read().decode()) self._rate_count += 1 if data.get("status") == "success": result = { "ip": ip, "country": data.get("country", ""), "country_code": data.get("countryCode", ""), "city": data.get("city", ""), "isp": data.get("isp", ""), "lat": data.get("lat", 0), "lon": data.get("lon", 0), "is_private": 0, "cached_at": time.time(), } with self._lock: # Cap cache at 50,000 entries if len(self._cache) >= 50000: oldest = min(self._cache, key=lambda k: self._cache[k].get("cached_at", 0)) del self._cache[oldest] self._cache[ip] = result self._persist(result) except Exception: pass