(local_if, info)
| 397 | # confusing "no IPv4 address" further down. |
| 398 | if not _valid_iface(interface) or interface not in _list_iface_names(include_virtual=True): |
| 399 | return {'success': False, 'error': f'Unknown interface: {interface}'} |
| 400 | iface = _egress_iface(interface) |
| 401 | source_ip = None |
| 402 | if iface: |
| 403 | v4, _ = _iface_addrs(iface) |
| 404 | if v4: |
| 405 | source_ip = v4[0].split('/')[0] |
| 406 | elif interface: |
| 407 | # Explicitly asked for an interface that can't originate traffic. |
| 408 | return {'success': False, |
| 409 | 'error': f'{iface} has no IPv4 address — it cannot originate a ' |
| 410 | f'speed test (a monitor/SPAN port has link but no address).'} |
| 411 | # Only bind when deliberately leaving the kernel's normal path. If we landed |
| 412 | # on the default-route interface anyway, binding adds nothing but risk — run |
| 413 | # exactly as we did before the interface selector existed. |
| 414 | bind = bool(iface and iface != _default_route_iface()) |
| 415 | meta = {'interface': iface, 'source_ip': source_ip if bind else None, |
| 416 | 'bound': bind} |
| 417 | # Ask the interface itself whether it can reach the internet, rather than |
| 418 | # inferring it from a default route. A wired port can hold a perfectly good |
| 419 | # default route to the same router as WiFi and still be the wrong thing to |
| 420 | # bind — and, conversely, we must never tell a user their interface "has no |
| 421 | # route to the internet" when it demonstrably does. |
| 422 | if bind: |
| 423 | reach = _probe_egress(iface) |
| 424 | if reach is False: |
| 425 | return {'success': False, **meta, |
| 426 | 'error': f'{iface} cannot reach the internet — verified by a direct ' |
| 427 | f'connection test bound to that interface. Use Auto, or pick ' |
| 428 | f'the interface that carries your internet connection.'} |
| 429 | # Self-heal: if neither client is present, install speedtest-cli on demand. |
| 430 | # A device updated from the UI may not have finished (or may have missed) |
| 431 | # background tool provisioning, and the old behaviour was to just error out |
no test coverage detected