Per-connection kernel telemetry from `ss -ti`: RTT, min-RTT and TCP retransmits for every established flow — the light, dependency-free version of the eBPF per-flow visibility big shops run. A flow with retransmits or an RTT far above its min-RTT is where loss/bufferbloat is biting.
(limit=15)
| 1753 | if i not in _list_iface_names(include_virtual=True): |
| 1754 | return {'success': False, 'error': f'{i} not found'} |
| 1755 | m = _iface_master(i) |
| 1756 | if m and m != _DHCP_SNOOP_BRIDGE: |
| 1757 | return {'success': False, 'error': f'{i} is already enslaved to {m}'} |
| 1758 | if iface_a == iface_b: |
| 1759 | return {'success': False, 'error': 'need two different interfaces'} |
| 1760 | |
| 1761 | with _dhcp_snoop_lock: |
| 1762 | # Create the bridge if absent; enslave both members; bring everything up. |
| 1763 | if _DHCP_SNOOP_BRIDGE not in [b['name'] for b in _list_bridges()]: |
| 1764 | r = _run(['ip', 'link', 'add', 'name', _DHCP_SNOOP_BRIDGE, 'type', 'bridge'], timeout=5) |
| 1765 | if r['rc'] != 0: |
| 1766 | return {'success': False, 'error': r['err'] or 'failed to create bridge'} |
| 1767 | for i in (iface_a, iface_b): |
| 1768 | _run(['ip', 'link', 'set', i, 'master', _DHCP_SNOOP_BRIDGE], timeout=5) |
| 1769 | _run(['ip', 'link', 'set', i, 'up'], timeout=5) |
| 1770 | _run(['ip', 'link', 'set', _DHCP_SNOOP_BRIDGE, 'up'], timeout=5) |
| 1771 | members = _bridge_members(_DHCP_SNOOP_BRIDGE) |
| 1772 | |
| 1773 | return {'success': True, 'bridge': _DHCP_SNOOP_BRIDGE, 'members': members} |
| 1774 | |
| 1775 | |
| 1776 | # DHCP message types by the direction they flow, so we can tell a *server* |
| 1777 | # message (only a DHCP server sends these) from a *client* request. |
| 1778 | _DHCP_SERVER_MSGS = {'offer', 'ack', 'nak'} |
| 1779 | _DHCP_CLIENT_MSGS = {'discover', 'request', 'decline', 'release', 'inform'} |
| 1780 | |
| 1781 | |
| 1782 | def _parse_dhcp_snoop(output, members=None): |
| 1783 | """Parse `tcpdump -i any -Q in -e -n -v` DHCP output into per-packet records: |
| 1784 | {iface, src_ip, src_port, dst_port, is_server, msg_type, chaddr, yiaddr, |
| 1785 | server_id, router, lease}. `members` (if given) filters to those ingress |
| 1786 | interfaces. Each packet is a header line (timestamp + ingress iface + IP |
| 1787 | src>dst) followed by indented option lines until the next header.""" |
| 1788 | packets = [] |
| 1789 | cur = None |
| 1790 | |
| 1791 | def _flush(): |
| 1792 | if cur and cur.get('msg_type'): |
| 1793 | packets.append(cur) |