(self, shared_data=None, interface: str = None)
| 277 | |
| 278 | # Passive host discovery: periodically flush observed LAN hosts into the |
| 279 | # hosts DB so traffic-only hosts (firewalled, never scanned) get tracked. |
| 280 | PASSIVE_SYNC_INTERVAL = 30.0 # seconds between DB flushes |
| 281 | PASSIVE_MIN_PACKETS = 5 # min packets to upsert if no MAC yet |
| 282 | PASSIVE_MAX_LISTEN_PORTS = 100 # cap per host |
| 283 | PASSIVE_LAN_PREFIX = 24 # /24 around each known local/gateway IP |
| 284 | |
| 285 | # Bounded state. Alerts and DNS queries are ring buffers already, but |
| 286 | # host_stats, connections and the beacon flow history gain an entry per new |
| 287 | # peer/flow seen and have no natural ceiling — on a box left capturing for a |
| 288 | # week they are the only thing that grows without limit. Cap them and evict |
| 289 | # the least-recently-seen entries. |
| 290 | # |
| 291 | # The caps are what makes this feature honest on a 512MB Pi Zero 2 W: at the |
| 292 | # low-memory numbers below the tracked state tops out around 3MB, on top of |
| 293 | # ~18MB for the analyzer itself and ~8MB for tcpdump. |
| 294 | STATE_PRUNE_INTERVAL = 60.0 # seconds between eviction sweeps |
| 295 | MAX_TRACKED_HOSTS = 4000 |
| 296 | MAX_TRACKED_CONNECTIONS = 8000 |
| 297 | MAX_TRACKED_FLOWS = 2000 # each holds a 64-sample ring buffer |
| 298 | LOW_MEMORY_RAM_GB = 1.0 # below this, use the divided caps |
| 299 | LOW_MEMORY_DIVISOR = 8 |
| 300 | |
| 301 | # Rate limiting |
| 302 | MAX_ALERTS_PER_MINUTE = 10 |
| 303 | STATS_RETENTION_HOURS = 24 |
| 304 | |
| 305 | def __init__(self, shared_data=None, interface: str = None): |
| 306 | self.shared_data = shared_data |
| 307 | self.interface = interface or self._detect_interface() |
| 308 | |
| 309 | self._running = False |
| 310 | self._capture_process: Optional[subprocess.Popen] = None |
| 311 | self._capture_thread: Optional[threading.Thread] = None |
| 312 | self._analysis_thread: Optional[threading.Thread] = None |
| 313 | |
| 314 | self._packet_queue: queue.Queue = queue.Queue(maxsize=10000) |
| 315 | self._lock = threading.Lock() |
| 316 | |
| 317 | # Local IP addresses to exclude from alerts (Ragnar's own IPs) |
| 318 | self._local_ips: set = self._detect_local_ips() |
| 319 | |
| 320 | # Default gateway IPs: exempt from port-scan heuristic. |
| 321 | self._gateway_ips: set = self._detect_gateway_ips() |
| 322 | |
| 323 | # LAN networks for passive host discovery (derived from local+gateway IPs). |
| 324 | self._lan_networks: List[ipaddress.IPv4Network] = self._derive_lan_networks() |
| 325 | |
| 326 | # Passive discovery state: |
| 327 | # _mac_by_ip: IP -> MAC (from ARP replies) |
| 328 | # _listening_ports: IP -> set of service ports it appears to serve |
| 329 | # _hostname_by_ip: IP -> hostname (from observed DNS replies) |
| 330 | self._mac_by_ip: Dict[str, str] = {} |
| 331 | self._listening_ports: Dict[str, set] = {} |
| 332 | self._hostname_by_ip: Dict[str, str] = {} |
| 333 | self._last_passive_sync: float = time.time() |
| 334 | |
| 335 | # Statistics storage |
| 336 | self.host_stats: Dict[str, HostTrafficStats] = {} |
nothing calls this directly
no test coverage detected