Extract Power-over-Ethernet state from an lldpctl port's Power-via-MDI TLV (dot3 / LLDP-MED). A PoE-capable switch advertises this, so it tells us whether the port supplies power and at what class/wattage. Returns None when the neighbour advertised no power TLV.
(port)
| 326 | |
| 327 | The only trustworthy 'can it reach the internet' signal we can get without |
| 328 | sending traffic. NOT `ip route get <ip> oif <iface>`: forcing oif makes the |
| 329 | kernel synthesize an answer for an interface that has no route at all (it |
| 330 | will happily report a route out of an address-less eth0, borrowing another |
| 331 | interface's src), so that check silently passes for exactly the interfaces |
| 332 | we need to exclude.""" |
| 333 | res = _run(['ip', 'route', 'show', 'default', 'dev', iface], timeout=5) |
| 334 | return res['rc'] == 0 and bool(res['out'].strip()) |
| 335 | |
| 336 | |
| 337 | def _iface_default_gateway(iface): |
| 338 | """The gateway of this interface's own default route, or None. Lets a test |
| 339 | pinned to a NIC ping *that* link's gateway instead of the global one (which |
| 340 | may sit on a different segment entirely).""" |
| 341 | res = _run(['ip', 'route', 'show', 'default', 'dev', iface], timeout=5) |
| 342 | m = re.search(r'\bvia\s+(\S+)', res['out'] or '') |
| 343 | return m.group(1) if m else None |
| 344 | |
| 345 | |
| 346 | def _egress_iface(preferred=None): |
| 347 | """Best interface to originate an *egress* test (speedtest) from: the |
| 348 | caller's pin, else a wired port that can actually reach the internet, else |
| 349 | the default-route interface. |
| 350 | |
| 351 | Deliberately NOT _capture_iface(): that one prefers any wired port with |
| 352 | carrier, which for the sensor deployment is exactly the SPAN/monitor leg — |
| 353 | fine to sniff, useless to originate from. An address alone isn't enough |
| 354 | either: a Ragnar plugged into a switch gets a DHCP lease on that segment but |
| 355 | reaches the internet over WiFi, so a wired port only qualifies as egress if |
| 356 | it has a default route. Binding to one that doesn't is what produces |
| 357 | speedtest-cli's 'Cannot retrieve speedtest configuration / urlopen error |
| 358 | timed out'.""" |
| 359 | if _valid_iface(preferred or ''): |
| 360 | return preferred |
| 361 | dflt = _default_route_iface() |
| 362 | wired = [] |
| 363 | for name in _list_iface_names(include_virtual=False): |
| 364 | if _is_wireless(name) or name.startswith(('tun', 'tap', 'wg', 'zt', 'tailscale')): |
| 365 | continue |
| 366 | try: |
| 367 | with open(f'/sys/class/net/{name}/carrier') as f: |
| 368 | if f.read().strip() != '1': |
| 369 | continue |
| 370 | except OSError: # admin-down or vanished — not a candidate |
| 371 | continue |
| 372 | v4, _ = _iface_addrs(name) |
| 373 | if v4 and _iface_has_default_route(name): |
| 374 | wired.append(name) |
| 375 | if wired: |
| 376 | return dflt if dflt in wired else sorted(wired)[0] |
| 377 | return dflt |
| 378 | |
| 379 | |
| 380 | def do_speedtest(interface=None): |
| 381 | """Run a bandwidth test. Supports both the Ookla `speedtest` CLI and the |
| 382 | python `speedtest-cli`; returns download/upload in Mbps. |
| 383 | |
| 384 | `interface` pins which local interface the test originates from; '' / None |
| 385 | auto-selects (a wired port that can reach the internet first — see |
no test coverage detected