Triage a capture file: summary (packets/bytes/duration/rate), protocol hierarchy, top talkers (by bytes), and tshark's expert info (errors/warnings /notes — retransmissions, resets, dup-ACKs, malformed …). Read-only analysis via tshark/capinfos, the way you'd eyeball a pcap in Wireshark'
(path)
| 1621 | m = re.match(r'Domain Name:\s*(.+)', line) |
| 1622 | if m: |
| 1623 | cur['domain'] = m.group(1).strip(); continue |
| 1624 | m = re.match(r'Interface:\s*(\S+)', line) |
| 1625 | if m: |
| 1626 | cur['iface'] = m.group(1); continue |
| 1627 | _flush() |
| 1628 | return offers |
| 1629 | |
| 1630 | |
| 1631 | def _dhcp_discover(interface, timeout_s=_DHCP_DISCOVER_TIMEOUT_S): |
| 1632 | """Provoke every DHCP server on the segment to OFFER, via nmap's |
| 1633 | broadcast-dhcp-discover. Returns (offers, error).""" |
| 1634 | if not _have('nmap'): |
| 1635 | return [], 'nmap is not installed (needed for rogue-DHCP discovery)' |
| 1636 | cmd = ['nmap', '--script', 'broadcast-dhcp-discover', |
| 1637 | '--script-args', f'broadcast-dhcp-discover.timeout={int(timeout_s)}'] |
| 1638 | if interface and _valid_iface(interface): |
| 1639 | cmd += ['-e', interface] |
| 1640 | res = _run(cmd, timeout=int(timeout_s) + 25) |
| 1641 | if res['rc'] == 127: |
| 1642 | return [], 'nmap not found' |
| 1643 | return _parse_dhcp_discover(res['out']), None |
| 1644 | |
| 1645 | |
| 1646 | def _parse_dhcp_capture(output): |
| 1647 | """Parse verbose `tcpdump` DHCP output into client-request stats: |
| 1648 | (requests, distinct_client_macs). Counts DISCOVER/REQUEST (client→server) |
| 1649 | and the distinct BOOTP client hardware addresses (chaddr) behind them — |
| 1650 | chaddr is what a starvation tool spoofs, so distinct chaddrs is the signal.""" |
| 1651 | requests = 0 |
| 1652 | clients = set() |
| 1653 | # tcpdump prints the BOOTP fixed fields (Client-Ethernet-Address) BEFORE the |
| 1654 | # option that carries the message type (DHCP-Message: Discover), so hold the |
| 1655 | # chaddr as it goes by and attribute it once the message type is known. The |
| 1656 | # earlier order-assumption dropped the first client and mis-shifted the rest. |
| 1657 | pending_mac = None |
| 1658 | for raw in output.splitlines(): |
| 1659 | line = raw.strip() |
| 1660 | m = re.search(r'Client-Ethernet-Address\s+([0-9a-fA-F:]{17})', line) |
| 1661 | if m: |
| 1662 | pending_mac = m.group(1).lower() |
| 1663 | continue |
| 1664 | m = re.search(r'DHCP-Message.*?:\s*(\w+)', line) |
| 1665 | if m: |
| 1666 | kind = m.group(1).lower() |
| 1667 | if kind in ('discover', 'request'): |
| 1668 | requests += 1 |
| 1669 | if pending_mac: |
| 1670 | clients.add(pending_mac) |
| 1671 | pending_mac = None # each option block ends this packet's chaddr |
| 1672 | return requests, clients |
| 1673 | |
| 1674 | |
| 1675 | def _dhcp_capture(interface, seconds): |
| 1676 | """Passively capture DHCP client requests for `seconds` and return |
| 1677 | (requests, distinct_clients, error). No traffic generated.""" |
| 1678 | if not _have('tcpdump'): |
| 1679 | return 0, 0, 'tcpdump is not installed (needed for starvation capture)' |
| 1680 | iface = interface if _valid_iface(interface or '') else _capture_iface() |
no test coverage detected