Save an uploaded capture to a temp file (size-guarded, magic-checked) and analyze it, then delete it.
(file_storage, max_bytes=100 * 1024 * 1024)
| 1717 | # OFFER wait — a legitimate local server answers in well under a second, so |
| 1718 | # 4s is plenty and keeps the interactive path snappy. |
| 1719 | offers, offer_err = _dhcp_discover(iface, 4 if quick else _DHCP_DISCOVER_TIMEOUT_S) |
| 1720 | server_ids = sorted({o['server_id'] for o in offers if o.get('server_id')}) |
| 1721 | learned = False |
| 1722 | rogue_servers = [] |
| 1723 | |
| 1724 | with _dhcp_baseline_lock: |
| 1725 | baseline = _dhcp_baseline_load() |
| 1726 | trusted = baseline.setdefault('servers', {}) |
| 1727 | # First scan with exactly one server learns it as the trusted baseline |
| 1728 | # (mirrors the ARP gateway-baseline learn-on-first-run behaviour). |
| 1729 | if learn and not trusted and len(server_ids) == 1: |
| 1730 | sid = server_ids[0] |
| 1731 | o = next(o for o in offers if o.get('server_id') == sid) |
| 1732 | trusted[sid] = {'router': o.get('router'), 'dns': o.get('dns') or []} |
| 1733 | _dhcp_baseline_save(baseline) |
| 1734 | learned = True |
| 1735 | trusted_ids = set(trusted.keys()) |
| 1736 | |
| 1737 | # A server that isn't the trusted one — or a trusted one whose offered |
| 1738 | # gateway changed — is rogue. If nothing is trusted yet, ≥2 servers is the |
| 1739 | # rogue signal on its own. |
| 1740 | for o in offers: |
| 1741 | sid = o.get('server_id') |
| 1742 | if not sid: |
| 1743 | continue |
| 1744 | base = trusted.get(sid) |
| 1745 | if trusted_ids and sid not in trusted_ids: |
| 1746 | rogue_servers.append(o) |
| 1747 | reasons.append(f"rogue DHCP server {sid} on the segment " |
| 1748 | f"(offers gateway {o.get('router') or '?'}, " |
| 1749 | f"DNS {', '.join(o.get('dns') or []) or '?'})") |
| 1750 | elif base and o.get('router') and base.get('router') and o['router'] != base['router']: |
| 1751 | rogue_servers.append(o) |
no test coverage detected