Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ ARG BASE_IMAGE="${REGISTRY}/ubuntu:22.04"
ARG VPN_VERSION="1.0.25"
ARG BUSYBOX_VERSION="0.0.15"
ARG LINUX_VERSION="3.5.18-beta"
ARG IGLOO_DRIVER_VERSION="0.0.38"
ARG IGLOO_DRIVER_VERSION="0.0.42"
ARG LIBNVRAM_VERSION="0.0.23"
ARG CONSOLE_VERSION="1.0.7"
ARG GUESTHOPPER_VERSION="1.0.20"
Expand Down
1 change: 1 addition & 0 deletions pyplugins/apis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@

---
"""
from .net import Netdev
3 changes: 0 additions & 3 deletions pyplugins/apis/kffi.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,6 @@ class KFFI(Plugin):
~~~~~~~

- ``call_function``: Call a kernel function with arguments.
- ``read_kernel_memory``: Read bytes from kernel memory.
- ``write_kernel_memory``: Write bytes to kernel memory.
- ``cdef``: Dynamically compile and load C definitions (structs/enums) for the target ABI.
"""

def __init__(self) -> None:
Expand Down
268 changes: 268 additions & 0 deletions pyplugins/apis/net.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
from penguin import Plugin, plugins, getColoredLogger
from typing import Optional, List, Iterator, Generator, Set, Dict, Type
from hyper.portal import PortalCmd
from hyper.consts import HYPER_OP as hop


class Netdev:
"""
Base class for all network devices.
Custom netdev plugins should inherit from this.
"""
name: str
netdev_ptr: int

def __init__(self, name: str, *args, **kwargs):
self.name = name

@property
def logger(self):
if hasattr(self, "_logger"):
return self._logger
self._logger = getColoredLogger(f"net.{self.__class__.__name__}.{self.name}")
return self._logger

def setup(self, netdev_struct) -> Optional[Iterator]:
"""
Called when the netdev is initialized in the kernel.
"""
pass


class Netdevs(Plugin):
def __init__(self):
self._pending_netdevs = []

# Keep track of the classes and the instantiated elements separately
self._netdev_classes: Dict[str, Type[Netdev]] = {}
self._netdev_instances: Dict[str, Netdev] = {}
self._netdev_structs = {} # name -> net_device pointer
self._exist_ok = {} # name -> bool

self._netdev_ops = self._build_netdev_ops_lookup()
plugins.portal.register_interrupt_handler(
"netdevs", self._netdevs_interrupt_handler)

netdevs = self.get_arg("conf").get("netdevs", [])
for nd in netdevs:
self.register_netdev(nd)

self._packet_queue = [] # List of (name, buf)

def _is_function_pointer(self, attr) -> bool:
"""Check if an attribute is a function pointer."""
return (hasattr(attr, "_subtype_info") and
attr._subtype_info.get("kind") == "function")

def _get_ops_functions(self, struct_name: str) -> Dict[str, Optional[str]]:
"""
Inspect a top-level struct (eg. "net_device") and return a mapping:
- function_name -> None (direct function pointer on top-level struct)
- function_name -> 'ops_struct' (function pointer belonging to an ops struct)
"""
lookup: Dict[str, Optional[str]] = {}
try:
sample = plugins.kffi.new(struct_name)
except Exception as e:
self.logger.debug(f"Failed to instantiate {struct_name}: {e}")
return lookup

# Collect top-level function pointers
top_funcs: Set[str] = set()
seen_ops: Set[str] = set()

for mem in dir(sample):
if mem.startswith("_") or not hasattr(sample, mem):
continue
try:
attr = getattr(sample, mem)
except Exception:
continue

# Direct function pointer on the top-level struct
if self._is_function_pointer(attr):
top_funcs.add(mem)
continue

# Try to determine if this member points to an *_ops struct
attr_type_str = str(type(attr))
ops_struct_name = None

# Prefer attribute name if it ends with _ops (common pattern)
if mem.endswith("_ops"):
ops_struct_name = mem
else:
# Fallback: try to extract from the type string
ops_struct_name = self._extract_ops_struct_name(attr_type_str)

if not ops_struct_name or ops_struct_name in seen_ops:
continue

# Instantiate the ops struct and enumerate its function pointers
try:
ops_sample = plugins.kffi.new(ops_struct_name)
except Exception:
# Could not instantiate this ops struct; skip it
continue

funcs: Set[str] = set()
for of in dir(ops_sample):
if of.startswith("_") or not hasattr(ops_sample, of):
continue
try:
ofattr = getattr(ops_sample, of)
except Exception:
continue
if self._is_function_pointer(ofattr):
funcs.add(of)

if funcs:
for f in funcs:
lookup[f] = ops_struct_name
seen_ops.add(ops_struct_name)

# Finally map top-level functions to None
for f in top_funcs:
lookup[f] = None

return lookup

def _build_netdev_ops_lookup(self) -> Dict[str, Optional[str]]:
"""
Build a lookup mapping function_name -> ops_struct_name (or None) by inspecting
the top-level 'net_device' structure and its *_ops sub-structures.
"""
try:
return self._get_ops_functions("net_device")
except Exception as e:
self.logger.debug(f"Failed to build netdev ops lookup: {e}")
return {}

def _extract_ops_struct_name(self, attr_str: str) -> Optional[str]:
"""Extract ops struct name from type string."""
import re
match = re.search(r'(\w*_ops)', attr_str)
return match.group(1) if match else None

def _net_setup(self, name, dev_ptr):
# Look up the *instantiated* element rather than the class
netdev_instance = self._netdev_instances.get(name, self._netdev_instances.get("*", None))
if netdev_instance is None:
return
netdev = yield from plugins.kffi.read_type(dev_ptr, "net_device")

if hasattr(netdev_instance, "setup"):
netdev_instance.name = name
netdev_instance.netdev_ptr = dev_ptr
fn_ret = netdev_instance.setup(netdev)
if isinstance(fn_ret, Iterator):
fn_ret = yield from fn_ret

def lookup_netdev(self, name: str) -> Generator[PortalCmd, Optional[int], Optional[int]]:
"""
Look up a network device by name using the portal.
Returns the pointer to net_device struct or None if not found.
"""
buf = name.encode("latin-1", errors="ignore") + b"\0"
result = yield PortalCmd(hop.HYPER_OP_LOOKUP_NETDEV, 0, len(buf), None, buf)
if result == 0 or result is None:
self.logger.debug(f"Netdev '{name}' not found (kernel returned 0)")
return None
self.logger.debug(f"Netdev '{name}' found at {result:#x}")
return result

def register(self, name: str, backing_class: Optional[Type[Netdev]] = None, exist_ok: bool = False, *args, **kwargs):
self.register_netdev(name, backing_class, exist_ok, *args, **kwargs)

def register_netdev(self, name: str, backing_class: Optional[Type[Netdev]] = None, exist_ok: bool = False, *args, **kwargs):
'''
Register a network device with the given name.
'''
if name not in self._netdev_classes and name not in self._pending_netdevs:
plugins.portal.queue_interrupt("netdevs")
if name != "*":
self._pending_netdevs.append(name)
self._exist_ok[name] = exist_ok
if backing_class:
self._netdev_classes[name] = backing_class
# Instantiate the class using any passed arguments and store the instance
self._netdev_instances[name] = backing_class(name, *args, **kwargs)

def _register_netdevs(self, names: List[str]) -> Iterator[int]:
"""
Build a NUL-terminated buffer of interface names and send to kernel.
New portal implementation registers a single device per hypercall and
returns a non-zero pointer on success (or zero/False on failure).
Call the hypercall once per name and return the number of successful
registrations.
"""
# New implementation: kernel returns pointer to net_device struct on success, 0/null on failure
if not names:
return 0

for name in names:
buf = name.encode("latin-1", errors="ignore") + b"\0"
result = yield PortalCmd(hop.HYPER_OP_REGISTER_NETDEV, 0, len(buf), None, buf)
is_up = yield from self.set_netdev_state(name, True)
if not is_up:
self.logger.error(f"Failed to set netdev '{name}' UP state")

if result == 0 or result is None:
if self._exist_ok.get(name, False) or self._exist_ok.get("*", False):
result = yield from self.lookup_netdev(name)
if result == 0 or result is None:
self.logger.error(f"Failed to register or look up '{name}'")
return
else:
self.logger.error(f"Failed to register netdev '{name}' (kernel returned 0)")
return
self._netdev_structs[name] = result
yield from self._net_setup(name, result)

def _netdevs_interrupt_handler(self) -> Iterator[bool]:
"""
Process pending network device registrations and queued packet sends.
"""
# Process pending network device registrations. Generator-style like _uprobe_interrupt_handler.
# Processes each pending (name, backing_class) and attempts kernel registration.
if not self._pending_netdevs:
return False

pending = self._pending_netdevs[:]

while pending:
name = pending.pop(0)
yield from self._register_netdevs([name])
self._pending_netdevs.remove(name)

# No more pending registrations or packets
return False

def set_netdev_state(self, name: str, up: bool) -> Generator[PortalCmd, Optional[int], Optional[bool]]:
"""
Set the state (up/down) of a network device.
Returns True if successful, False otherwise.
"""
buf = name.encode("latin-1", errors="ignore") + b"\0"
requested_state = 1 if up else 0
result = yield PortalCmd(hop.HYPER_OP_SET_NETDEV_STATE, 0, requested_state, None, buf)
if result == requested_state:
self.logger.debug(f"Netdev '{name}' state set to {requested_state}")
return True
else:
self.logger.error(f"Failed to set netdev '{name}' state to {requested_state}")
return False

def get_netdev_state(self, name: str) -> Generator[PortalCmd, Optional[int], Optional[bool]]:
"""
Get the state (up/down) of a network device.
Returns True if up, False if down, or None if not found.
"""
buf = name.encode("latin-1", errors="ignore") + b"\0"
result = yield PortalCmd(hop.HYPER_OP_GET_NETDEV_STATE, 0, len(buf), None, buf)
if result is None:
self.logger.error(f"Failed to get state for netdev '{name}'")
return None
state = bool(result)
self.logger.debug(f"Netdev '{name}' state is {'up' if state else 'down'}")
return state
2 changes: 1 addition & 1 deletion pyplugins/apis/syscalls.py
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ def _get_proto(self, cpu: int, sce: Any, on_all: bool) -> SyscallPrototype:
generic_args = [("int", f"unknown{i+1}") for i in range(6)]
proto = SyscallPrototype(name=f'sys_{cleaned_name}', args=generic_args)
self._syscall_info_table[cleaned_name] = proto
self.logger.error(
self.logger.debug(
f"Syscall {name} not registered {cleaned_name=}, created generic prototype with {len(generic_args)} args")

# Update caches
Expand Down
1 change: 1 addition & 0 deletions pyplugins/core/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def __init__(self) -> None:
"live_image",
"igloodriver",
"kmods",
"net",
]

for essential_plugin in essential_plugins:
Expand Down
9 changes: 0 additions & 9 deletions pyplugins/interventions/pseudofiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,15 +466,6 @@ def populate_hf_config(self):
# introspection
self.need_ioctl_hooks = True

if len(self.get_arg("conf").get("netdevs", [])):
# If we have netdevs in our config, we'll make the /proc/penguin_net pseudofile with the contents of it
# Here we'll use our make_rwif closure
netdev_val = " ".join(self.get_arg("conf")["netdevs"])
hf_config["/proc/penguin_net"] = {
HYP_READ: make_rwif({"val": netdev_val}, self.read_const_buf),
"size": len(netdev_val),
}

hf_config["/proc/mtd"] = {
# Note we don't use our make_rwif closure helper here because these
# are static
Expand Down
70 changes: 70 additions & 0 deletions pyplugins/testing/netdevs_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from penguin import Plugin, plugins
from apis.net import Netdev

kffi = plugins.kffi
mem = plugins.mem


class SampleNetDev(Netdev):
def setup(self, netdev):
self.logger.info(f"Setting up altered network device {self.name}")
netdev_ops = yield from kffi.deref(netdev.netdev_ops)
netdev_ops.ndo_do_ioctl = yield from kffi.callback(self.ioctl_handler)
netdev_ops.ndo_get_stats64 = yield from kffi.callback(self.stats64_handler)
yield from mem.write_bytes(netdev.netdev_ops.address, bytes(netdev_ops))

def stats64_handler(self, pt_regs, netdev_ptr, stats64_ptr):
netdevs = yield from kffi.read_type(netdev_ptr, "net_device")
stats64 = yield from kffi.read_type(stats64_ptr, "rtnl_link_stats64")
self.logger.info(f"Getting stats64 for device {kffi.ffi.string(netdevs.name)}")
stats64.rx_packets = 1337
stats64.tx_packets = 1338
stats64.rx_bytes = 1339
stats64.tx_bytes = 1340
# Just return zeroed stats for now
yield from mem.write_bytes(stats64_ptr, bytes(stats64))
return stats64_ptr

def ioctl_handler(self, pt_regs, netdev_ptr, ifreq_ptr, cmd):
args = yield from plugins.osi.get_args()
netdevs = yield from kffi.read_type(netdev_ptr, "net_device")
name = kffi.ffi.string(netdevs.name)
self.logger.info((name, args, cmd))
return 0


class SampleNetDev2(Netdev):
def setup(self, netdev):
self.logger.info(f"Setting up sample network device {self.name}")
netdev_ops = yield from kffi.deref(netdev.netdev_ops)
netdev_ops.ndo_do_ioctl = yield from kffi.callback(self.ioctl_handler)
netdev_ops.ndo_get_stats64 = yield from kffi.callback(self.stats64_handler)
yield from mem.write_bytes(netdev.netdev_ops.address, bytes(netdev_ops))

def stats64_handler(self, pt_regs, netdev_ptr, stats64_ptr):
netdevs = yield from kffi.read_type(netdev_ptr, "net_device")
stats64 = yield from kffi.read_type(stats64_ptr, "rtnl_link_stats64")
self.logger.info(f"Getting stats64 for device {kffi.ffi.string(netdevs.name)}")
stats64.rx_packets = 7331
stats64.tx_packets = 8331
stats64.rx_bytes = 9331
stats64.tx_bytes = 431
# Just return zeroed stats for now
yield from mem.write_bytes(stats64_ptr, bytes(stats64))
return stats64_ptr

def ioctl_handler(self, pt_regs, netdev_ptr, ifreq_ptr, cmd):
args = yield from plugins.osi.get_args()
netdevs = yield from kffi.read_type(netdev_ptr, "net_device")
name = kffi.ffi.string(netdevs.name)
self.logger.info((name, args, cmd))
return 0


class NetworkDeviceTest(Plugin):
def __init__(self):
# Register some sample netdevs for testing
# Register is an alias for register_netdev, so both should work
plugins.net.register("sample0", SampleNetDev)
plugins.net.register_netdev("sample1", SampleNetDev)
plugins.net.register_netdev("sample2", SampleNetDev2)
Loading
Loading