Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add macOS network interface plugin #839

Merged
merged 13 commits into from
Sep 24, 2024
3 changes: 2 additions & 1 deletion dissect/target/helpers/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ def DynamicDescriptor(types): # noqa
[
*COMMON_INTERFACE_ELEMENTS,
("varint", "vlan"),
("string", "proxy"),
("net.ipnetwork[]", "network"),
("varint", "interface_service_order"),
("boolean", "dhcp"),
],
)
20 changes: 2 additions & 18 deletions dissect/target/plugins/os/unix/bsd/osx/_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,8 @@ def hostname(self) -> Optional[str]:
@export(property=True)
def ips(self) -> Optional[list[str]]:
ips = set()

# Static configured IP-addresses
if (preferences := self.target.fs.path(self.SYSTEM)).exists():
network = plistlib.load(preferences.open()).get("NetworkServices")

for interface in network.values():
for addresses in [interface.get("IPv4"), interface.get("IPv6")]:
ips.update(addresses.get("Addresses", []))

# IP-addresses configured by DHCP
if (dhcp := self.target.fs.path("/private/var/db/dhcpclient/leases")).exists():
for lease in dhcp.iterdir():
if lease.is_file():
lease = plistlib.load(lease.open())

if ip := lease.get("IPAddress"):
ips.add(ip)

for ip in self.target.network.ips():
ips.add(str(ip))
return list(ips)

@export(property=True)
Expand Down
90 changes: 90 additions & 0 deletions dissect/target/plugins/os/unix/bsd/osx/network.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from __future__ import annotations

import plistlib
from functools import cache, lru_cache
from typing import Iterator

from dissect.target.helpers.record import MacInterfaceRecord
from dissect.target.plugins.general.network import NetworkPlugin
from dissect.target.target import Target


class MacNetworkPlugin(NetworkPlugin):
def __init__(self, target: Target):
super().__init__(target)
self._plistnetwork = cache(self._plistnetwork)
self._plistlease = lru_cache(32)(self._plistlease)

def _plistlease(self, devname: str) -> dict:
for lease in self.target.fs.glob_ext(f"/private/var/db/dhcpclient/leases/{devname}*"):
return plistlib.load(lease.open())
return {}

Check warning on line 21 in dissect/target/plugins/os/unix/bsd/osx/network.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/os/unix/bsd/osx/network.py#L21

Added line #L21 was not covered by tests

def _plistnetwork(self) -> dict:
if (preferences := self.target.fs.path("/Library/Preferences/SystemConfiguration/preferences.plist")).exists():
return plistlib.load(preferences.open())

def _interfaces(self) -> Iterator[MacInterfaceRecord]:
plistnetwork = self._plistnetwork()
current_set = plistnetwork.get("CurrentSet")
sets = plistnetwork.get("Sets", {})
for name, _set in sets.items():
if f"/Sets/{name}" == current_set:
item = _set
for key in ["Network", "Global", "IPv4", "ServiceOrder"]:
item = item.get(key, {})
service_order = item
break

network = plistnetwork.get("NetworkServices", {})
vlans = plistnetwork.get("VirtualNetworkInterfaces", {}).get("VLAN", {})

vlan_lookup = {key: vlan.get("Tag") for key, vlan in vlans.items()}

for _id, interface in network.items():
dns = set()
gateways = set()
ips = set()
device = interface.get("Interface", {})
name = device.get("DeviceName")
_type = device.get("Type")
vlan = vlan_lookup.get(name)
dhcp = False
subnetmask = []
network = []
interface_service_order = service_order.index(_id) if _id in service_order else None
try:
for addr in interface.get("DNS", {}).get("ServerAddresses", {}):
dns.add(addr)
for addresses in [interface.get("IPv4", {}), interface.get("IPv6", {})]:
subnetmask += filter(lambda mask: mask != "", addresses.get("SubnetMasks", []))
if router := addresses.get("Router"):
gateways.add(router)
if addresses.get("ConfigMethod", "") == "DHCP":
ips.add(self._plistlease(name).get("IPAddress"))
dhcp = True
else:
for addr in addresses.get("Addresses", []):
ips.add(addr)

if subnetmask:
network = self.calculate_network(ips, subnetmask)

yield MacInterfaceRecord(
name=name,
type=_type,
enabled=not interface.get("__INACTIVE__", False),
dns=list(dns),
ip=list(ips),
gateway=list(gateways),
source="NetworkServices",
vlan=vlan,
network=network,
interface_service_order=interface_service_order,
dhcp=dhcp,
_target=self.target,
)

except Exception as e:
self.target.log.warning("Error reading configuration for network device %s: %s", name, e)
continue

Check warning on line 90 in dissect/target/plugins/os/unix/bsd/osx/network.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/os/unix/bsd/osx/network.py#L88-L90

Added lines #L88 - L90 were not covered by tests
172 changes: 172 additions & 0 deletions tests/plugins/os/unix/bsd/osx/test_network.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
from typing import Iterator
from unittest import mock

import pytest

from dissect.target.plugins.os.unix.bsd.osx.network import MacNetworkPlugin
from dissect.target.target import Target


@pytest.fixture
def almost_empty_plist() -> Iterator[dict]:
yield {"CurrentSet": {}}


@pytest.fixture
def fake_plist() -> Iterator[dict]:
yield {
"CurrentSet": "/Sets/1",
"NetworkServices": {
"1": {
"DNS": {"ServerAddresses": ["8.8.8.8"]},
"IPv4": {
"Addresses": ["192.122.13.34"],
"Router": "8.8.8.8",
},
"Interface": {
"DeviceName": "en0",
"Type": "Ethernet",
},
},
},
"Sets": {
"1": {
"Network": {
"Global": {"IPv4": {"ServiceOrder": ["1"]}},
},
},
},
"VirtualNetworkInterfaces": {"VLAN": {"vlan0": {"Tag": 2}}},
}


@pytest.fixture
def vlan0(fake_plist: dict) -> Iterator[dict]:
fake_plist["NetworkServices"]["1"]["Interface"].update({"DeviceName": "vlan0"})
yield fake_plist


@pytest.fixture
def inactive(fake_plist: dict) -> Iterator[dict]:
fake_plist["NetworkServices"]["1"].update({"__INACTIVE__": True})
yield fake_plist


@pytest.fixture
def ipv6(fake_plist: dict) -> Iterator[dict]:
del fake_plist["NetworkServices"]["1"]["IPv4"]
fake_plist["NetworkServices"]["1"]["IPv6"] = {"Addresses": ["::1"]}
yield fake_plist


@pytest.fixture
def reorder(fake_plist: dict) -> Iterator[dict]:
fake_plist["Sets"]["1"]["Network"]["Global"]["IPv4"]["ServiceOrder"] = ["2", "1"]
yield fake_plist


@pytest.fixture
def double(fake_plist: dict) -> Iterator[dict]:
fake_plist["NetworkServices"]["2"] = fake_plist["NetworkServices"]["1"]
yield fake_plist


@pytest.fixture
def dhcp(fake_plist: dict) -> Iterator[dict]:
fake_plist["NetworkServices"]["1"]["IPv4"].update({"ConfigMethod": "DHCP"})
yield fake_plist


@pytest.mark.parametrize(
"lease,netinfo_param,expected,count",
[
({"IPAddress": None}, "almost_empty_plist", [], 0),
(
{},
"fake_plist",
[
(0, "hostname", ["dummys Mac"]),
(0, "domain", ["None"]),
(0, "name", ["en0"]),
(0, "type", ["Ethernet"]),
(0, "ip", ["192.122.13.34"]),
(0, "gateway", ["8.8.8.8"]),
(0, "dns", ["8.8.8.8"]),
(0, "vlan", ["None"]),
(0, "enabled", ["True"]),
(0, "interface_service_order", ["0"]),
(0, "mac", ["None"]),
(0, "vlan", ["None"]),
],
1,
),
(
{"IPAddress": "10.0.0.2"},
"dhcp",
[
(0, "ip", sorted(["10.0.0.2"])),
],
1,
),
(
{},
"vlan0",
[
(0, "vlan", ["2"]),
],
1,
),
(
{},
"inactive",
[
(0, "enabled", ["False"]),
],
1,
),
(
{},
"ipv6",
[
(0, "ip", ["::1"]),
],
1,
),
(
{},
"reorder",
[
(0, "interface_service_order", ["1"]),
],
1,
),
(
{},
"double",
[
(0, "enabled", ["True"]),
(1, "enabled", ["True"]),
],
2,
),
],
)
def test_macos_network(
target_osx: Target, lease: dict, netinfo_param: str, expected: dict, count: int, request: pytest.FixtureRequest
) -> None:
plistnetwork = request.getfixturevalue(netinfo_param)
with mock.patch(
"dissect.target.plugins.os.unix.bsd.osx.network.MacNetworkPlugin._plistlease", return_value=lease
), mock.patch(
"dissect.target.plugins.os.unix.bsd.osx.network.MacNetworkPlugin._plistnetwork", return_value=plistnetwork
):
network = MacNetworkPlugin(target_osx)

interfaces = list(network.interfaces())
assert len(interfaces) == count
for index, key, value in expected:
attr = getattr(interfaces[index], key)
if not isinstance(attr, list):
attr = [attr]
attr = list(sorted(map(str, attr)))
assert attr == value