| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130 |
- """Tests for network interface enumeration.
- Focus: the platform routing in get_network_interfaces(). macOS/BSD have fcntl
- but not the Linux SIOCGIFADDR/SIOCGIFNETMASK ioctls, so the ioctl path there
- silently returns nothing and the VP bind-interface dropdown comes up empty.
- Everything that isn't Linux must go through the cross-platform psutil path.
- """
- import socket
- from collections import namedtuple
- from unittest.mock import patch
- from backend.app.services import network_utils
- # Mimic the shape of psutil.net_if_addrs() / net_if_stats() entries we read.
- _Addr = namedtuple("snicaddr", ["family", "address", "netmask", "broadcast", "ptp"])
- _Stats = namedtuple("snicstats", ["isup", "duplex", "speed", "mtu", "flags"])
- def _fake_psutil():
- addrs = {
- "en0": [_Addr(socket.AF_INET, "192.168.1.50", "255.255.255.0", None, None)],
- "lo0": [_Addr(socket.AF_INET, "127.0.0.1", "255.0.0.0", None, None)],
- "awdl0": [_Addr(socket.AF_INET, "169.254.10.20", "255.255.0.0", None, None)],
- "utun3": [_Addr(socket.AF_INET, "100.64.0.7", "255.255.255.255", None, None)],
- "en5": [_Addr(socket.AF_INET, "10.0.0.9", "255.255.255.0", None, None)],
- }
- stats = {
- "en0": _Stats(True, 0, 0, 1500, 0),
- "lo0": _Stats(True, 0, 0, 16384, 0),
- "awdl0": _Stats(True, 0, 0, 1500, 0),
- "utun3": _Stats(True, 0, 0, 1500, 0),
- "en5": _Stats(False, 0, 0, 1500, 0), # down → skipped
- }
- return addrs, stats
- @patch("backend.app.services.network_utils.sys")
- def test_macos_routes_to_psutil(mock_sys):
- """On darwin, get_network_interfaces() must use psutil, not the ioctl path."""
- mock_sys.platform = "darwin"
- with patch.object(network_utils, "_get_network_interfaces_psutil", return_value=[{"name": "en0"}]) as psutil_path:
- result = network_utils.get_network_interfaces()
- psutil_path.assert_called_once()
- assert result == [{"name": "en0"}]
- @patch("backend.app.services.network_utils.sys")
- def test_windows_routes_to_psutil(mock_sys):
- mock_sys.platform = "win32"
- with patch.object(network_utils, "_get_network_interfaces_psutil", return_value=[]) as psutil_path:
- network_utils.get_network_interfaces()
- psutil_path.assert_called_once()
- @patch("backend.app.services.network_utils.sys")
- def test_linux_does_not_use_psutil(mock_sys):
- """Linux keeps the ioctl path — psutil helper must not be invoked."""
- mock_sys.platform = "linux"
- with patch.object(network_utils, "_get_network_interfaces_psutil") as psutil_path:
- # The ioctl path runs for real here; we only assert it wasn't short-circuited
- # to psutil. Its actual return depends on the host, so we don't assert on it.
- network_utils.get_network_interfaces()
- psutil_path.assert_not_called()
- def test_psutil_path_filters_and_returns_bindable_ips():
- """The psutil path drops loopback/link-local/down ifaces, keeps real + VPN ones."""
- addrs, stats = _fake_psutil()
- with (
- patch("psutil.net_if_addrs", return_value=addrs),
- patch("psutil.net_if_stats", return_value=stats),
- ):
- result = network_utils._get_network_interfaces_psutil()
- by_name = {i["name"]: i for i in result}
- assert "en0" in by_name # normal LAN interface
- assert by_name["en0"]["ip"] == "192.168.1.50"
- assert by_name["en0"]["subnet"] == "192.168.1.0/24"
- assert "utun3" in by_name # Tailscale/VPN — legitimately bindable
- assert "lo0" not in by_name # loopback filtered
- assert "awdl0" not in by_name # link-local (169.254) filtered
- assert "en5" not in by_name # interface down, skipped
- _IP_ADDR_JSON = """[
- {"ifname": "lo", "addr_info": [{"family": "inet", "local": "127.0.0.1", "prefixlen": 8}]},
- {"ifname": "enp3s0", "addr_info": [{"family": "inet", "local": "192.168.96.9", "prefixlen": 22}]},
- {"ifname": "enp4s0", "addr_info": [
- {"family": "inet", "local": "10.0.0.5", "prefixlen": 24},
- {"family": "inet", "local": "10.0.0.6", "prefixlen": 24, "label": "enp4s0:vp1"}
- ]},
- {"ifname": "docker0", "addr_info": [{"family": "inet", "local": "172.17.0.1", "prefixlen": 16}]}
- ]"""
- def _fake_ip_addr():
- """Patch `ip -j addr show` with a fixed multi-homed Linux host."""
- result = namedtuple("CompletedProcess", ["returncode", "stdout", "stderr"])(0, _IP_ADDR_JSON, "")
- return patch.object(network_utils, "subprocess", **{"run.return_value": result})
- class TestFindLocalIPv4Network:
- """#3092: an address carries no prefix, so it has to be read off the interface."""
- def test_reads_the_configured_prefix_not_a_guessed_24(self):
- with _fake_ip_addr(), patch.object(network_utils, "_IP_CMD", "/usr/sbin/ip"):
- assert str(network_utils.find_local_ipv4_network("192.168.96.9")) == "192.168.96.0/22"
- def test_an_alias_address_resolves_too(self):
- # The VP binds aliases; an alias is a perfectly good route source.
- with _fake_ip_addr(), patch.object(network_utils, "_IP_CMD", "/usr/sbin/ip"):
- assert str(network_utils.find_local_ipv4_network("10.0.0.6")) == "10.0.0.0/24"
- def test_an_excluded_interface_still_answers(self):
- """EXCLUDED_INTERFACE_PREFIXES keeps docker0 out of the VP dropdown.
- It must not also make the kernel's own choice of route source
- unanswerable — "unknown" would be a worse answer than the truth.
- """
- with _fake_ip_addr(), patch.object(network_utils, "_IP_CMD", "/usr/sbin/ip"):
- assert str(network_utils.find_local_ipv4_network("172.17.0.1")) == "172.17.0.0/16"
- assert not [i for i in network_utils.get_all_interface_ips() if i["name"] == "docker0"]
- def test_an_address_no_interface_holds_is_none(self):
- with _fake_ip_addr(), patch.object(network_utils, "_IP_CMD", "/usr/sbin/ip"):
- assert network_utils.find_local_ipv4_network("192.168.1.1") is None
- def test_a_hostname_is_none(self):
- assert network_utils.find_local_ipv4_network("printer.local") is None
|