From 7a4c37e8b673328dda59cec11ab9dce66c22a312 Mon Sep 17 00:00:00 2001 From: Dimitri Staessens Date: Wed, 4 Mar 2026 21:26:43 +0100 Subject: ouroboros: Add IRM wrapper Add ouroboros.irm module wrapping the Ouroboros IRM C API, providing Python interfaces for IPCP lifecycle (create, destroy, bootstrap, enroll, connect), name management (create, destroy, register, list), and program/process binding. Split the monolithic CFFI build into separate _ouroboros_dev_cffi and _ouroboros_irm_cffi modules, each linking only its required library. Also includes: - ouroboros.cli module with higher-level wrappers mirroring CLI tools - FRCT flag support (set/get) in the Flow API - FlowPeer event type in FEventType - QoS defaults updated to match ouroboros source - Bug fixes: flow_set_snd_timeout typo, flow_set_flags calling convention, FlowSet name mangling, fqueue_type return type - .gitignore, copyright updates, version bump to 0.23.0 --- ouroboros/cli.py | 543 +++++++++++++++++++++++++++++++++++++++++++ ouroboros/dev.py | 102 ++++++++- ouroboros/event.py | 9 +- ouroboros/irm.py | 662 +++++++++++++++++++++++++++++++++++++++++++++++++++++ ouroboros/qos.py | 78 ++----- 5 files changed, 1313 insertions(+), 81 deletions(-) create mode 100644 ouroboros/cli.py create mode 100644 ouroboros/irm.py (limited to 'ouroboros') diff --git a/ouroboros/cli.py b/ouroboros/cli.py new file mode 100644 index 0000000..7f07e56 --- /dev/null +++ b/ouroboros/cli.py @@ -0,0 +1,543 @@ +# +# Ouroboros - Copyright (C) 2016 - 2026 +# +# Python API for Ouroboros - CLI equivalents +# +# Dimitri Staessens +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License +# version 2.1 as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., http://www.fsf.org/about/contact/. +# + +""" +Higher-level wrappers that mirror CLI tool behaviour. + +The ``irm`` CLI tools perform extra steps that the raw C library API +does not, such as resolving program names via ``realpath``, looking up +IPCP pids, and the ``autobind`` flag for bootstrapping/enrolling. +This module exposes those same patterns as a Python API so that +callers do not need to re-implement them. + +Each wrapper corresponds to a specific ``irm`` sub-command: + +========================= ==================================== +Python CLI equivalent +========================= ==================================== +``create_ipcp`` ``irm ipcp create`` +``destroy_ipcp`` ``irm ipcp destroy`` +``bootstrap_ipcp`` ``irm ipcp bootstrap [autobind]`` +``enroll_ipcp`` ``irm ipcp enroll [autobind]`` +``connect_ipcp`` ``irm ipcp connect`` +``disconnect_ipcp`` ``irm ipcp disconnect`` +``list_ipcps`` ``irm ipcp list`` +``bind_program`` ``irm bind program`` +``bind_process`` ``irm bind process`` +``bind_ipcp`` ``irm bind ipcp`` +``unbind_program`` ``irm unbind program`` +``unbind_process`` ``irm unbind process`` +``unbind_ipcp`` ``irm unbind ipcp`` +``create_name`` ``irm name create`` +``destroy_name`` ``irm name destroy`` +``reg_name`` ``irm name register`` +``unreg_name`` ``irm name unregister`` +``list_names`` ``irm name list`` +``autoboot`` ``irm ipcp bootstrap autobind`` + (with implicit ``create``) +========================= ==================================== + +Usage:: + + from ouroboros.cli import create_ipcp, bootstrap_ipcp, enroll_ipcp + from ouroboros.cli import bind_program, autoboot +""" + +import shutil +from typing import List, Optional + +from ouroboros.irm import ( + DT_COMP, + MGMT_COMP, + IpcpType, + IpcpConfig, + IpcpInfo, + NameInfo, + BindError, + IrmError, + UnicastConfig, + DtConfig, + RoutingConfig, + LinkStateConfig, + LinkStatePolicy, + EthConfig, + Udp4Config, + Udp6Config, + bind_program as _irm_bind_program, + bind_process as _irm_bind_process, + bootstrap_ipcp as _irm_bootstrap_ipcp, + connect_ipcp as _irm_connect_ipcp, + create_ipcp, + create_name as _irm_create_name, + destroy_ipcp as _irm_destroy_ipcp, + destroy_name, + disconnect_ipcp as _irm_disconnect_ipcp, + enroll_ipcp as _irm_enroll_ipcp, + list_ipcps as _irm_list_ipcps, + list_names as _irm_list_names, + reg_name as _irm_reg_name, + unbind_process as _irm_unbind_process, + unbind_program as _irm_unbind_program, + unreg_name as _irm_unreg_name, +) +from ouroboros.qos import QoSSpec + + +def _pid_of(ipcp_name: str) -> int: + """Look up the pid of a running IPCP by its name.""" + for info in _irm_list_ipcps(): + if info.name == ipcp_name: + return info.pid + raise ValueError(f"No IPCP named {ipcp_name!r}") + + +def destroy_ipcp(name: str) -> None: + """ + Destroy an IPCP by name. + + Mirrors ``irm ipcp destroy name ``. + + Resolves the IPCP name to a pid, then destroys the IPCP. + + :param name: Name of the IPCP to destroy. + :raises ValueError: If no IPCP with *name* exists. + """ + _irm_destroy_ipcp(_pid_of(name)) + + +def list_ipcps(name: Optional[str] = None, + layer: Optional[str] = None, + ipcp_type: Optional[IpcpType] = None) -> List[IpcpInfo]: + """ + List running IPCPs, optionally filtered. + + Mirrors ``irm ipcp list [name ] [type ] [layer ]``. + + :param name: Filter by IPCP name (exact match). + :param layer: Filter by layer name (exact match). + :param ipcp_type: Filter by IPCP type. + :return: List of matching :class:`IpcpInfo` objects. + """ + result = _irm_list_ipcps() + if name is not None: + result = [i for i in result if i.name == name] + if layer is not None: + result = [i for i in result if i.layer == layer] + if ipcp_type is not None: + result = [i for i in result if i.type == ipcp_type] + return result + + +def reg_name(name: str, + ipcp: Optional[str] = None, + ipcps: Optional[List[str]] = None, + layer: Optional[str] = None, + layers: Optional[List[str]] = None) -> None: + """ + Register a name with IPCP(s), creating it first if needed. + + Mirrors ``irm name register ipcp [ipcp ...] + layer [layer ...]``. + + The C CLI tool resolves IPCP names and layer names to pids, + checks whether the name already exists and calls + ``irm_create_name`` before ``irm_reg_name`` for each IPCP. + + The function accepts flexible input: + + - A single *ipcp* name or list of *ipcps* names. + - A single *layer* name or list of *layers* names (registers + with every IPCP in each of those layers). + - Any combination of the above. + + :param name: The name to register. + :param ipcp: Single IPCP name to register with. + :param ipcps: List of IPCP names to register with. + :param layer: Single layer name to register with. + :param layers: List of layer names to register with. + """ + existing = {n.name for n in _irm_list_names()} + if name not in existing: + _irm_create_name(NameInfo(name=name)) + + pids = set() + + # Collect IPCP names into a single list + ipcp_names = [] + if ipcp is not None: + ipcp_names.append(ipcp) + if ipcps is not None: + ipcp_names.extend(ipcps) + + # Collect layer names into a single list + layer_names = [] + if layer is not None: + layer_names.append(layer) + if layers is not None: + layer_names.extend(layers) + + if ipcp_names or layer_names: + all_ipcps = _irm_list_ipcps() + for ipcp_name in ipcp_names: + for i in all_ipcps: + if i.name == ipcp_name: + pids.add(i.pid) + break + for lyr in layer_names: + for i in all_ipcps: + if i.layer == lyr: + pids.add(i.pid) + + for p in pids: + _irm_reg_name(name, p) + + +def bind_program(prog: str, + name: str, + opts: int = 0, + argv: Optional[List[str]] = None) -> None: + """ + Bind a program to a name, resolving bare names to full paths. + + Mirrors ``irm bind program name ``. + + The ``irm bind program`` CLI tool calls ``realpath()`` on *prog* + before passing it to the library. The raw C function + ``irm_bind_program`` contains a ``check_prog_path`` helper that + corrupts the ``PATH`` environment variable (writes NUL over ``:`` + separators) when given a bare program name. Only the first such + call would succeed in a long-running process. + + This wrapper resolves *prog* via ``shutil.which()`` before calling + the library, avoiding the bug entirely. + + :param prog: Program name or path. Bare names (without ``/``) + are resolved on ``PATH`` via ``shutil.which()``. + :param name: Name to bind to. + :param opts: Bind options (e.g. ``BIND_AUTO``). + :param argv: Arguments to pass when the program is auto-started. + :raises BindError: If the program cannot be found or the bind + call fails. + """ + if '/' not in prog: + resolved = shutil.which(prog) + if resolved is None: + raise BindError(f"Program {prog!r} not found on PATH") + prog = resolved + _irm_bind_program(prog, name, opts=opts, argv=argv) + + +def unbind_program(prog: str, name: str) -> None: + """ + Unbind a program from a name. + + Mirrors ``irm unbind program name ``. + + :param prog: Path to the program. + :param name: Name to unbind from. + """ + _irm_unbind_program(prog, name) + + +def bind_process(pid: int, name: str) -> None: + """ + Bind a running process to a name. + + Mirrors ``irm bind process name ``. + + :param pid: PID of the process. + :param name: Name to bind to. + """ + _irm_bind_process(pid, name) + + +def unbind_process(pid: int, name: str) -> None: + """ + Unbind a process from a name. + + Mirrors ``irm unbind process name ``. + + :param pid: PID of the process. + :param name: Name to unbind from. + """ + _irm_unbind_process(pid, name) + + +def bind_ipcp(ipcp: str, name: str) -> None: + """ + Bind an IPCP to a name. + + Mirrors ``irm bind ipcp name ``. + + Resolves the IPCP name to a pid, then calls ``bind_process``. + + :param ipcp: IPCP instance name. + :param name: Name to bind to. + :raises ValueError: If no IPCP with *ipcp* exists. + """ + _irm_bind_process(_pid_of(ipcp), name) + + +def unbind_ipcp(ipcp: str, name: str) -> None: + """ + Unbind an IPCP from a name. + + Mirrors ``irm unbind ipcp name ``. + + Resolves the IPCP name to a pid, then calls ``unbind_process``. + + :param ipcp: IPCP instance name. + :param name: Name to unbind from. + :raises ValueError: If no IPCP with *ipcp* exists. + """ + _irm_unbind_process(_pid_of(ipcp), name) + + +def create_name(name: str, + pol_lb: Optional[int] = None, + info: Optional[NameInfo] = None) -> None: + """ + Create a registered name. + + Mirrors ``irm name create [lb ]``. + + :param name: The name to create. + :param pol_lb: Load-balance policy (optional). + :param info: Full :class:`NameInfo` (overrides *name*/*pol_lb* + if given). + """ + if info is not None: + _irm_create_name(info) + else: + ni = NameInfo(name=name) + if pol_lb is not None: + ni.pol_lb = pol_lb + _irm_create_name(ni) + + +def list_names(name: Optional[str] = None) -> List[NameInfo]: + """ + List all registered names, optionally filtered. + + Mirrors ``irm name list []``. + + :param name: Filter by name (exact match). + :return: List of :class:`NameInfo` objects. + """ + result = _irm_list_names() + if name is not None: + result = [n for n in result if n.name == name] + return result + + +def unreg_name(name: str, + ipcp: Optional[str] = None, + ipcps: Optional[List[str]] = None, + layer: Optional[str] = None, + layers: Optional[List[str]] = None) -> None: + """ + Unregister a name from IPCP(s). + + Mirrors ``irm name unregister ipcp [ipcp ...] + layer [layer ...]``. + + Accepts the same flexible input as :func:`reg_name`. + + :param name: The name to unregister. + :param ipcp: Single IPCP name to unregister from. + :param ipcps: List of IPCP names to unregister from. + :param layer: Single layer name to unregister from. + :param layers: List of layer names to unregister from. + """ + pids = set() + + ipcp_names = [] + if ipcp is not None: + ipcp_names.append(ipcp) + if ipcps is not None: + ipcp_names.extend(ipcps) + + layer_names = [] + if layer is not None: + layer_names.append(layer) + if layers is not None: + layer_names.extend(layers) + + if ipcp_names or layer_names: + all_ipcps = _irm_list_ipcps() + for ipcp_name in ipcp_names: + for i in all_ipcps: + if i.name == ipcp_name: + pids.add(i.pid) + break + for lyr in layer_names: + for i in all_ipcps: + if i.layer == lyr: + pids.add(i.pid) + + for p in pids: + _irm_unreg_name(name, p) + + +def bootstrap_ipcp(name: str, + conf: IpcpConfig, + autobind: bool = False) -> None: + """ + Bootstrap an IPCP, optionally binding it to its name and layer. + + Mirrors ``irm ipcp bootstrap name layer [autobind]``. + + When *autobind* is ``True`` and the IPCP type is ``UNICAST`` or + ``BROADCAST``, the sequence is:: + + bind_process(pid, ipcp_name) # accept flows for ipcp name + bind_process(pid, layer_name) # accept flows for layer name + bootstrap_ipcp(pid, conf) # bootstrap into the layer + + This matches the C ``irm ipcp bootstrap`` tool exactly. If + bootstrap fails after autobind, the bindings are rolled back. + + :param name: Name of the IPCP. + :param conf: IPCP configuration (includes layer name & type). + :param autobind: Bind the IPCP process to its name and layer. + """ + pid = _pid_of(name) + layer_name = conf.layer_name + + if autobind and conf.ipcp_type in (IpcpType.UNICAST, + IpcpType.BROADCAST): + _irm_bind_process(pid, name) + _irm_bind_process(pid, layer_name) + + try: + _irm_bootstrap_ipcp(pid, conf) + except Exception: + if autobind and conf.ipcp_type in (IpcpType.UNICAST, + IpcpType.BROADCAST): + _irm_unbind_process(pid, name) + _irm_unbind_process(pid, layer_name) + raise + + +def enroll_ipcp(name: str, dst: str, + autobind: bool = False) -> None: + """ + Enroll an IPCP, optionally binding it to its name and layer. + + Mirrors ``irm ipcp enroll name layer [autobind]``. + + When *autobind* is ``True``, the sequence is:: + + enroll_ipcp(pid, dst) + bind_process(pid, ipcp_name) + bind_process(pid, layer_name) # layer learned from enrollment + + This matches the C ``irm ipcp enroll`` tool exactly. + + :param name: Name of the IPCP. + :param dst: Destination name or layer to enroll with. + :param autobind: Bind the IPCP process to its name and layer + after successful enrollment. + """ + pid = _pid_of(name) + _irm_enroll_ipcp(pid, dst) + + if autobind: + # Look up enrolled layer from the IPCP list + for info in _irm_list_ipcps(): + if info.pid == pid: + _irm_bind_process(pid, info.name) + _irm_bind_process(pid, info.layer) + break + + +def connect_ipcp(name: str, dst: str, comp: str = "*", + qos: Optional[QoSSpec] = None) -> None: + """ + Connect IPCP components to a destination. + + Mirrors ``irm ipcp connect name dst [component ] + [qos ]``. + + When *comp* is ``"*"`` (default), both ``dt`` and ``mgmt`` + components are connected, matching the CLI default. + + :param name: Name of the IPCP. + :param dst: Destination IPCP name. + :param comp: Component to connect: ``"dt"``, ``"mgmt"``, or + ``"*"`` for both (default). + :param qos: QoS specification for the dt component. + """ + pid = _pid_of(name) + if comp in ("*", "mgmt"): + _irm_connect_ipcp(pid, MGMT_COMP, dst) + if comp in ("*", "dt"): + _irm_connect_ipcp(pid, DT_COMP, dst, qos=qos) + + +def disconnect_ipcp(name: str, dst: str, comp: str = "*") -> None: + """ + Disconnect IPCP components from a destination. + + Mirrors ``irm ipcp disconnect name dst + [component ]``. + + When *comp* is ``"*"`` (default), both ``dt`` and ``mgmt`` + components are disconnected, matching the CLI default. + + :param name: Name of the IPCP. + :param dst: Destination IPCP name. + :param comp: Component to disconnect: ``"dt"``, ``"mgmt"``, or + ``"*"`` for both (default). + """ + pid = _pid_of(name) + if comp in ("*", "mgmt"): + _irm_disconnect_ipcp(pid, MGMT_COMP, dst) + if comp in ("*", "dt"): + _irm_disconnect_ipcp(pid, DT_COMP, dst) + + +def autoboot(name: str, + ipcp_type: IpcpType, + layer: str, + conf: Optional[IpcpConfig] = None) -> None: + """ + Create, autobind and bootstrap an IPCP in one step. + + Convenience wrapper equivalent to:: + + irm ipcp bootstrap name type layer autobind + + (with an implicit ``create`` if the IPCP does not yet exist). + + :param name: Name for the IPCP. + :param ipcp_type: Type of IPCP to create. + :param layer: Layer name to bootstrap into. + :param conf: Optional IPCP configuration. If *None*, a + default ``IpcpConfig`` is created for the given + *ipcp_type* and *layer*. + """ + create_ipcp(name, ipcp_type) + if conf is None: + conf = IpcpConfig(ipcp_type=ipcp_type, layer_name=layer) + else: + conf.layer_name = layer + bootstrap_ipcp(name, conf, autobind=True) diff --git a/ouroboros/dev.py b/ouroboros/dev.py index bc5d133..a2b58cf 100644 --- a/ouroboros/dev.py +++ b/ouroboros/dev.py @@ -1,7 +1,7 @@ # -# Ouroboros - Copyright (C) 2016 - 2020 +# Ouroboros - Copyright (C) 2016 - 2026 # -# Python API for applications +# Python API for Ouroboros # # Dimitri Staessens # @@ -21,14 +21,72 @@ import errno from enum import IntFlag +from math import modf +from typing import Optional -from _ouroboros_cffi import ffi, lib +from _ouroboros_dev_cffi import ffi, lib from ouroboros.qos import * -from ouroboros.qos import _qos_to_qosspec, _fl_to_timespec, _qosspec_to_qos, _timespec_to_fl # Some constants -MILLION = 1000_1000 -BILLION = 1000_1000_1000 +MILLION = 1000 * 1000 +BILLION = 1000 * 1000 * 1000 + + +def _fl_to_timespec(timeo: float): + if timeo is None: + return ffi.NULL + elif timeo <= 0: + return ffi.new("struct timespec *", [0, 0]) + else: + frac, whole = modf(timeo) + _timeo = ffi.new("struct timespec *") + _timeo.tv_sec = int(whole) + _timeo.tv_nsec = int(frac * BILLION) + return _timeo + + +def _timespec_to_fl(_timeo) -> Optional[float]: + if _timeo is ffi.NULL: + return None + elif _timeo.tv_sec <= 0 and _timeo.tv_nsec == 0: + return 0 + else: + return _timeo.tv_sec + _timeo.tv_nsec / BILLION + + +# Intentionally duplicated, dev uses a separate FFI (ouroboros-dev). +def _qos_to_qosspec(qos: QoSSpec): + if qos is None: + return ffi.NULL + else: + return ffi.new("qosspec_t *", + [qos.delay, + qos.bandwidth, + qos.availability, + qos.loss, + qos.ber, + qos.in_order, + qos.max_gap, + qos.timeout]) + + +def _qosspec_to_qos(_qos) -> Optional[QoSSpec]: + if _qos is ffi.NULL: + return None + else: + return QoSSpec(delay=_qos.delay, + bandwidth=_qos.bandwidth, + availability=_qos.availability, + loss=_qos.loss, + ber=_qos.ber, + in_order=_qos.in_order, + max_gap=_qos.max_gap, + timeout=_qos.timeout) + +# FRCT flags +FRCT_RETRANSMIT = 0o1 +FRCT_RESCNTL = 0o2 +FRCT_LINGER = 0o4 # ouroboros exceptions @@ -248,7 +306,7 @@ class Flow: """ _timeo = _fl_to_timespec(timeo) - if lib.flow_set_snd_timout(self.__fd, _timeo) != 0: + if lib.flow_set_snd_timeout(self.__fd, _timeo) != 0: raise FlowPermissionException() def get_snd_timeout(self) -> float: @@ -271,7 +329,7 @@ class Flow: """ _timeo = _fl_to_timespec(timeo) - if lib.flow_set_rcv_timout(self.__fd, _timeo) != 0: + if lib.flow_set_rcv_timeout(self.__fd, _timeo) != 0: raise FlowPermissionException() def get_rcv_timeout(self) -> float: @@ -331,9 +389,7 @@ class Flow: :param flags: """ - _flags = ffi.new("uint32_t *", int(flags)) - - if lib.flow_set_flag(self.__fd, _flags): + if lib.flow_set_flags(self.__fd, int(flags)): raise FlowPermissionException() def get_flags(self) -> FlowProperties: @@ -341,12 +397,34 @@ class Flow: Get the flags for this flow """ - flags = lib.flow_get_flag(self.__fd) + flags = lib.flow_get_flags(self.__fd) if flags < 0: raise FlowPermissionException() return FlowProperties(int(flags)) + def set_frct_flags(self, flags: int): + """ + Set FRCT flags for this flow. + :param flags: Bitmask of FRCT_RETRANSMIT, FRCT_RESCNTL, FRCT_LINGER + """ + + if lib.flow_set_frct_flags(self.__fd, flags): + raise FlowPermissionException() + + def get_frct_flags(self) -> int: + """ + Get the FRCT flags for this flow + + :return: Bitmask of FRCT flags + """ + + flags = lib.flow_get_frct_flags(self.__fd) + if flags < 0: + raise FlowPermissionException() + + return int(flags) + def flow_alloc(dst: str, qos: QoSSpec = None, diff --git a/ouroboros/event.py b/ouroboros/event.py index b707c1b..ee0127e 100644 --- a/ouroboros/event.py +++ b/ouroboros/event.py @@ -1,7 +1,7 @@ # -# Ouroboros - Copyright (C) 2016 - 2020 +# Ouroboros - Copyright (C) 2016 - 2026 # -# Python API for applications +# Python API for Ouroboros # # Dimitri Staessens # @@ -20,7 +20,7 @@ # from ouroboros.dev import * -from ouroboros.qos import _fl_to_timespec +from ouroboros.dev import _fl_to_timespec # async API @@ -34,6 +34,7 @@ class FEventType(IntFlag): FlowUp = lib.FLOW_UP FlowAlloc = lib.FLOW_ALLOC FlowDealloc = lib.FLOW_DEALLOC + FlowPeer = lib.FLOW_PEER class FEventQueue: @@ -101,7 +102,7 @@ class FlowSet: if self.__set is ffi.NULL: raise ValueError - if lib.fset_add(self.__set, flow._Flow___fd) != 0: + if lib.fset_add(self.__set, flow._Flow__fd) != 0: raise MemoryError("Failed to add flow") def zero(self): diff --git a/ouroboros/irm.py b/ouroboros/irm.py new file mode 100644 index 0000000..1e4fc2e --- /dev/null +++ b/ouroboros/irm.py @@ -0,0 +1,662 @@ +# +# Ouroboros - Copyright (C) 2016 - 2026 +# +# Python API for Ouroboros - IRM +# +# Dimitri Staessens +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License +# version 2.1 as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., http://www.fsf.org/about/contact/. +# + +from enum import IntEnum +from typing import List, Optional + +from _ouroboros_irm_cffi import ffi, lib +from ouroboros.qos import QoSSpec + + +# Intentionally duplicated: irm uses a separate FFI (ouroboros-irm). +def _qos_to_qosspec(qos: QoSSpec): + if qos is None: + return ffi.NULL + else: + return ffi.new("qosspec_t *", + [qos.delay, + qos.bandwidth, + qos.availability, + qos.loss, + qos.ber, + qos.in_order, + qos.max_gap, + qos.timeout]) + +# --- Enumerations --- + +class IpcpType(IntEnum): + """IPCP types available in Ouroboros.""" + LOCAL = lib.IPCP_LOCAL + UNICAST = lib.IPCP_UNICAST + BROADCAST = lib.IPCP_BROADCAST + ETH_LLC = lib.IPCP_ETH_LLC + ETH_DIX = lib.IPCP_ETH_DIX + UDP4 = lib.IPCP_UDP4 + UDP6 = lib.IPCP_UDP6 + + +class AddressAuthPolicy(IntEnum): + """Address authority policies for unicast IPCPs.""" + FLAT_RANDOM = lib.ADDR_AUTH_FLAT_RANDOM + + +class LinkStatePolicy(IntEnum): + """Link state routing policies.""" + SIMPLE = lib.LS_SIMPLE + LFA = lib.LS_LFA + ECMP = lib.LS_ECMP + + +class RoutingPolicy(IntEnum): + """Routing policies.""" + LINK_STATE = lib.ROUTING_LINK_STATE + + +class CongestionAvoidPolicy(IntEnum): + """Congestion avoidance policies.""" + NONE = lib.CA_NONE + MB_ECN = lib.CA_MB_ECN + + +class DirectoryPolicy(IntEnum): + """Directory policies.""" + DHT = lib.DIR_DHT + + +class DirectoryHashAlgo(IntEnum): + """Directory hash algorithms.""" + SHA3_224 = lib.DIR_HASH_SHA3_224 + SHA3_256 = lib.DIR_HASH_SHA3_256 + SHA3_384 = lib.DIR_HASH_SHA3_384 + SHA3_512 = lib.DIR_HASH_SHA3_512 + + +class LoadBalancePolicy(IntEnum): + """Load balancing policies for names.""" + ROUND_ROBIN = lib.LB_RR + SPILL = lib.LB_SPILL + + +BIND_AUTO = lib.BIND_AUTO + +# Unicast IPCP component names +DT_COMP = "Data Transfer" +MGMT_COMP = "Management" + + +# --- Exceptions --- + +class IrmError(Exception): + """General IRM error.""" + pass + + +class IpcpCreateError(IrmError): + pass + + +class IpcpBootstrapError(IrmError): + pass + + +class IpcpEnrollError(IrmError): + pass + + +class IpcpConnectError(IrmError): + pass + + +class NameError(IrmError): + pass + + +class BindError(IrmError): + pass + + +# --- Configuration classes --- + +class LinkStateConfig: + """Configuration for link state routing.""" + + def __init__(self, + pol: LinkStatePolicy = LinkStatePolicy.SIMPLE, + t_recalc: int = 4, + t_update: int = 15, + t_timeo: int = 60): + self.pol = pol + self.t_recalc = t_recalc + self.t_update = t_update + self.t_timeo = t_timeo + + +class RoutingConfig: + """Routing configuration.""" + + def __init__(self, + pol: RoutingPolicy = RoutingPolicy.LINK_STATE, + ls: LinkStateConfig = None): + self.pol = pol + self.ls = ls or LinkStateConfig() + + +class DtConfig: + """Data transfer configuration for unicast IPCPs.""" + + def __init__(self, + addr_size: int = 4, + eid_size: int = 8, + max_ttl: int = 60, + routing: RoutingConfig = None): + self.addr_size = addr_size + self.eid_size = eid_size + self.max_ttl = max_ttl + self.routing = routing or RoutingConfig() + + +class DhtConfig: + """DHT directory configuration.""" + + def __init__(self, + alpha: int = 3, + k: int = 8, + t_expire: int = 86400, + t_refresh: int = 900, + t_replicate: int = 900): + self.alpha = alpha + self.k = k + self.t_expire = t_expire + self.t_refresh = t_refresh + self.t_replicate = t_replicate + + +class DirConfig: + """Directory configuration.""" + + def __init__(self, + pol: DirectoryPolicy = DirectoryPolicy.DHT, + dht: DhtConfig = None): + self.pol = pol + self.dht = dht or DhtConfig() + + +class UnicastConfig: + """Configuration for unicast IPCPs.""" + + def __init__(self, + dt: DtConfig = None, + dir: DirConfig = None, + addr_auth: AddressAuthPolicy = AddressAuthPolicy.FLAT_RANDOM, + cong_avoid: CongestionAvoidPolicy = CongestionAvoidPolicy.MB_ECN): + self.dt = dt or DtConfig() + self.dir = dir or DirConfig() + self.addr_auth = addr_auth + self.cong_avoid = cong_avoid + + +class EthConfig: + """Configuration for Ethernet IPCPs (LLC or DIX).""" + + def __init__(self, + dev: str = "", + ethertype: int = 0xA000): + self.dev = dev + self.ethertype = ethertype + + +class Udp4Config: + """Configuration for UDP over IPv4 IPCPs.""" + + def __init__(self, + ip_addr: str = "0.0.0.0", + dns_addr: str = "0.0.0.0", + port: int = 3435): + self.ip_addr = ip_addr + self.dns_addr = dns_addr + self.port = port + + +class Udp6Config: + """Configuration for UDP over IPv6 IPCPs.""" + + def __init__(self, + ip_addr: str = "::", + dns_addr: str = "::", + port: int = 3435): + self.ip_addr = ip_addr + self.dns_addr = dns_addr + self.port = port + + +class IpcpConfig: + """ + Configuration for bootstrapping an IPCP. + + Depending on the IPCP type, set the appropriate sub-configuration: + - UNICAST: unicast (UnicastConfig) + - ETH_LLC: eth (EthConfig) + - ETH_DIX: eth (EthConfig) + - UDP4: udp4 (Udp4Config) + - UDP6: udp6 (Udp6Config) + - LOCAL: no extra config needed + - BROADCAST: no extra config needed + """ + + def __init__(self, + ipcp_type: IpcpType, + layer_name: str = "", + dir_hash_algo: DirectoryHashAlgo = DirectoryHashAlgo.SHA3_256, + unicast: UnicastConfig = None, + eth: EthConfig = None, + udp4: Udp4Config = None, + udp6: Udp6Config = None): + self.ipcp_type = ipcp_type + self.layer_name = layer_name + self.dir_hash_algo = dir_hash_algo + self.unicast = unicast + self.eth = eth + self.udp4 = udp4 + self.udp6 = udp6 + + +class NameSecPaths: + """Security paths for a name (encryption, key, certificate).""" + + def __init__(self, + enc: str = "", + key: str = "", + crt: str = ""): + self.enc = enc + self.key = key + self.crt = crt + + +class NameInfo: + """Information about a registered name.""" + + def __init__(self, + name: str, + pol_lb: LoadBalancePolicy = LoadBalancePolicy.ROUND_ROBIN, + server_sec: NameSecPaths = None, + client_sec: NameSecPaths = None): + self.name = name + self.pol_lb = pol_lb + self.server_sec = server_sec or NameSecPaths() + self.client_sec = client_sec or NameSecPaths() + + +class IpcpInfo: + """Information about a running IPCP (from list_ipcps).""" + + def __init__(self, + pid: int, + ipcp_type: IpcpType, + name: str, + layer: str): + self.pid = pid + self.type = ipcp_type + self.name = name + self.layer = layer + + def __repr__(self): + return (f"IpcpInfo(pid={self.pid}, type={self.type.name}, " + f"name='{self.name}', layer='{self.layer}')") + + +# --- Internal conversion functions --- + +def _ipcp_config_to_c(conf: IpcpConfig): + """Convert an IpcpConfig to a C struct ipcp_config *.""" + _conf = ffi.new("struct ipcp_config *") + + # Layer info + layer_name = conf.layer_name.encode() + ffi.memmove(_conf.layer_info.name, layer_name, + min(len(layer_name), 255)) + _conf.layer_info.dir_hash_algo = conf.dir_hash_algo + + _conf.type = conf.ipcp_type + + if conf.ipcp_type == IpcpType.UNICAST: + uc = conf.unicast or UnicastConfig() + _conf.unicast.dt.addr_size = uc.dt.addr_size + _conf.unicast.dt.eid_size = uc.dt.eid_size + _conf.unicast.dt.max_ttl = uc.dt.max_ttl + _conf.unicast.dt.routing.pol = uc.dt.routing.pol + _conf.unicast.dt.routing.ls.pol = uc.dt.routing.ls.pol + _conf.unicast.dt.routing.ls.t_recalc = uc.dt.routing.ls.t_recalc + _conf.unicast.dt.routing.ls.t_update = uc.dt.routing.ls.t_update + _conf.unicast.dt.routing.ls.t_timeo = uc.dt.routing.ls.t_timeo + _conf.unicast.dir.pol = uc.dir.pol + _conf.unicast.dir.dht.params.alpha = uc.dir.dht.alpha + _conf.unicast.dir.dht.params.k = uc.dir.dht.k + _conf.unicast.dir.dht.params.t_expire = uc.dir.dht.t_expire + _conf.unicast.dir.dht.params.t_refresh = uc.dir.dht.t_refresh + _conf.unicast.dir.dht.params.t_replicate = uc.dir.dht.t_replicate + _conf.unicast.addr_auth_type = uc.addr_auth + _conf.unicast.cong_avoid = uc.cong_avoid + + elif conf.ipcp_type == IpcpType.ETH_LLC or conf.ipcp_type == IpcpType.ETH_DIX: + ec = conf.eth or EthConfig() + dev = ec.dev.encode() + ffi.memmove(_conf.eth.dev, dev, min(len(dev), 255)) + _conf.eth.ethertype = ec.ethertype + + elif conf.ipcp_type == IpcpType.UDP4: + uc = conf.udp4 or Udp4Config() + _conf.udp4.port = uc.port + if lib.ipcp_config_udp4_set_ip(_conf, uc.ip_addr.encode()) != 0: + raise ValueError(f"Invalid IPv4 address: {uc.ip_addr}") + if lib.ipcp_config_udp4_set_dns(_conf, uc.dns_addr.encode()) != 0: + raise ValueError(f"Invalid IPv4 DNS address: {uc.dns_addr}") + + elif conf.ipcp_type == IpcpType.UDP6: + uc = conf.udp6 or Udp6Config() + _conf.udp6.port = uc.port + if lib.ipcp_config_udp6_set_ip(_conf, uc.ip_addr.encode()) != 0: + raise ValueError(f"Invalid IPv6 address: {uc.ip_addr}") + if lib.ipcp_config_udp6_set_dns(_conf, uc.dns_addr.encode()) != 0: + raise ValueError(f"Invalid IPv6 DNS address: {uc.dns_addr}") + + return _conf + + +def _name_info_to_c(info: NameInfo): + """Convert a NameInfo to a C struct name_info *.""" + _info = ffi.new("struct name_info *") + + name = info.name.encode() + ffi.memmove(_info.name, name, min(len(name), 255)) + _info.pol_lb = info.pol_lb + + for attr, sec in [('s', info.server_sec), ('c', info.client_sec)]: + sec_paths = getattr(_info, attr) + for field in ('enc', 'key', 'crt'): + val = getattr(sec, field).encode() + ffi.memmove(getattr(sec_paths, field), val, + min(len(val), 511)) + + return _info + + +# --- IRM API functions --- + +def create_ipcp(name: str, + ipcp_type: IpcpType) -> int: + """ + Create a new IPCP. + + :param name: Name for the IPCP + :param ipcp_type: Type of IPCP to create + :return: PID of the created IPCP + """ + ret = lib.irm_create_ipcp(name.encode(), ipcp_type) + if ret < 0: + raise IpcpCreateError(f"Failed to create IPCP '{name}' " + f"of type {ipcp_type.name}") + + # The C function returns 0 on success, not the pid. + # Look up the actual pid by name. + for info in list_ipcps(): + if info.name == name: + return info.pid + + raise IpcpCreateError(f"IPCP '{name}' created but not found in list") + + +def destroy_ipcp(pid: int) -> None: + """ + Destroy an IPCP. + + :param pid: PID of the IPCP to destroy + """ + if lib.irm_destroy_ipcp(pid) != 0: + raise IrmError(f"Failed to destroy IPCP with pid {pid}") + + +def list_ipcps() -> List[IpcpInfo]: + """ + List all running IPCPs. + + :return: List of IpcpInfo objects + """ + _ipcps = ffi.new("struct ipcp_list_info **") + n = lib.irm_list_ipcps(_ipcps) + if n < 0: + raise IrmError("Failed to list IPCPs") + + result = [] + for i in range(n): + info = _ipcps[0][i] + result.append(IpcpInfo( + pid=info.pid, + ipcp_type=IpcpType(info.type), + name=ffi.string(info.name).decode(), + layer=ffi.string(info.layer).decode() + )) + + if n > 0: + lib.free(_ipcps[0]) + + return result + + +def enroll_ipcp(pid: int, dst: str) -> None: + """ + Enroll an IPCP in a layer. + + :param pid: PID of the IPCP to enroll + :param dst: Name to use for enrollment + """ + if lib.irm_enroll_ipcp(pid, dst.encode()) != 0: + raise IpcpEnrollError(f"Failed to enroll IPCP {pid} to '{dst}'") + + +def bootstrap_ipcp(pid: int, conf: IpcpConfig) -> None: + """ + Bootstrap an IPCP. + + :param pid: PID of the IPCP to bootstrap + :param conf: Configuration for the IPCP + """ + _conf = _ipcp_config_to_c(conf) + if lib.irm_bootstrap_ipcp(pid, _conf) != 0: + raise IpcpBootstrapError(f"Failed to bootstrap IPCP {pid}") + + +def connect_ipcp(pid: int, + component: str, + dst: str, + qos: QoSSpec = None) -> None: + """ + Connect an IPCP component to a destination. + + :param pid: PID of the IPCP + :param component: Component to connect (DT_COMP or MGMT_COMP) + :param dst: Destination name + :param qos: QoS specification for the connection + """ + _qos = _qos_to_qosspec(qos) + if _qos == ffi.NULL: + _qos = ffi.new("qosspec_t *") + if lib.irm_connect_ipcp(pid, dst.encode(), component.encode(), + _qos[0]) != 0: + raise IpcpConnectError(f"Failed to connect IPCP {pid} " + f"component '{component}' to '{dst}'") + + +def disconnect_ipcp(pid: int, + component: str, + dst: str) -> None: + """ + Disconnect an IPCP component from a destination. + + :param pid: PID of the IPCP + :param component: Component to disconnect + :param dst: Destination name + """ + if lib.irm_disconnect_ipcp(pid, dst.encode(), + component.encode()) != 0: + raise IpcpConnectError(f"Failed to disconnect IPCP {pid} " + f"component '{component}' from '{dst}'") + + +def bind_program(prog: str, + name: str, + opts: int = 0, + argv: List[str] = None) -> None: + """ + Bind a program to a name. + + :param prog: Path to the program + :param name: Name to bind to + :param opts: Bind options (e.g. BIND_AUTO) + :param argv: Arguments to pass when the program is started + """ + if argv: + argc = len(argv) + _argv = ffi.new("char *[]", [ffi.new("char[]", a.encode()) + for a in argv]) + else: + argc = 0 + _argv = ffi.NULL + + if lib.irm_bind_program(prog.encode(), name.encode(), + opts, argc, _argv) != 0: + raise BindError(f"Failed to bind program '{prog}' to name '{name}'") + + +def unbind_program(prog: str, name: str) -> None: + """ + Unbind a program from a name. + + :param prog: Path to the program + :param name: Name to unbind from + """ + if lib.irm_unbind_program(prog.encode(), name.encode()) != 0: + raise BindError(f"Failed to unbind program '{prog}' " + f"from name '{name}'") + + +def bind_process(pid: int, name: str) -> None: + """ + Bind a running process to a name. + + :param pid: PID of the process + :param name: Name to bind to + """ + if lib.irm_bind_process(pid, name.encode()) != 0: + raise BindError(f"Failed to bind process {pid} to name '{name}'") + + +def unbind_process(pid: int, name: str) -> None: + """ + Unbind a process from a name. + + :param pid: PID of the process + :param name: Name to unbind from + """ + if lib.irm_unbind_process(pid, name.encode()) != 0: + raise BindError(f"Failed to unbind process {pid} " + f"from name '{name}'") + + +def create_name(info: NameInfo) -> None: + """ + Create a name in the IRM. + + :param info: NameInfo describing the name to create + """ + _info = _name_info_to_c(info) + if lib.irm_create_name(_info) != 0: + raise NameError(f"Failed to create name '{info.name}'") + + +def destroy_name(name: str) -> None: + """ + Destroy a name in the IRM. + + :param name: The name to destroy + """ + if lib.irm_destroy_name(name.encode()) != 0: + raise NameError(f"Failed to destroy name '{name}'") + + +def list_names() -> List[NameInfo]: + """ + List all registered names. + + :return: List of NameInfo objects + """ + _names = ffi.new("struct name_info **") + n = lib.irm_list_names(_names) + if n < 0: + raise IrmError("Failed to list names") + + result = [] + for i in range(n): + info = _names[0][i] + ni = NameInfo( + name=ffi.string(info.name).decode(), + pol_lb=LoadBalancePolicy(info.pol_lb) + ) + ni.server_sec = NameSecPaths( + enc=ffi.string(info.s.enc).decode(), + key=ffi.string(info.s.key).decode(), + crt=ffi.string(info.s.crt).decode() + ) + ni.client_sec = NameSecPaths( + enc=ffi.string(info.c.enc).decode(), + key=ffi.string(info.c.key).decode(), + crt=ffi.string(info.c.crt).decode() + ) + result.append(ni) + + if n > 0: + lib.free(_names[0]) + + return result + + +def reg_name(name: str, pid: int) -> None: + """ + Register an IPCP to a name. + + :param name: The name to register + :param pid: PID of the IPCP to register + """ + if lib.irm_reg_name(name.encode(), pid) != 0: + raise NameError(f"Failed to register name '{name}' " + f"with IPCP {pid}") + + +def unreg_name(name: str, pid: int) -> None: + """ + Unregister an IPCP from a name. + + :param name: The name to unregister + :param pid: PID of the IPCP to unregister + """ + if lib.irm_unreg_name(name.encode(), pid) != 0: + raise NameError(f"Failed to unregister name '{name}' " + f"from IPCP {pid}") diff --git a/ouroboros/qos.py b/ouroboros/qos.py index a43d87d..bb4ecc4 100644 --- a/ouroboros/qos.py +++ b/ouroboros/qos.py @@ -1,7 +1,7 @@ # -# Ouroboros - Copyright (C) 2016 - 2020 +# Ouroboros - Copyright (C) 2016 - 2026 # -# Python API for applications - QoS +# Python API for Ouroboros - QoS # # Dimitri Staessens # @@ -19,36 +19,35 @@ # Foundation, Inc., http://www.fsf.org/about/contact/. # -from _ouroboros_cffi import ffi -from math import modf -from typing import Optional - # Some constants MILLION = 1000 * 1000 BILLION = 1000 * 1000 * 1000 +DEFAULT_PEER_TIMEOUT = 120000 +UINT32_MAX = 0xFFFFFFFF + class QoSSpec: """ - delay: In ms, default 1000s + delay: In ms, default UINT32_MAX bandwidth: In bits / s, default 0 availability: Class of 9s, default 0 - loss: Packet loss in ppm, default MILLION - ber: Bit error rate, errors per billion bits. default BILLION + loss: Packet loss, default 1 + ber: Bit error rate, errors per billion bits, default 1 in_order: In-order delivery, enables FRCT, default 0 - max_gap: Maximum interruption in ms, default MILLION + max_gap: Maximum interruption in ms, default UINT32_MAX timeout: Peer timeout (ms), default 120000 (2 minutes) """ def __init__(self, - delay: int = MILLION, + delay: int = UINT32_MAX, bandwidth: int = 0, availability: int = 0, loss: int = 1, - ber: int = MILLION, + ber: int = 1, in_order: int = 0, - max_gap: int = MILLION, - timeout: int = 120000): + max_gap: int = UINT32_MAX, + timeout: int = DEFAULT_PEER_TIMEOUT): self.delay = delay self.bandwidth = bandwidth self.availability = availability @@ -57,54 +56,3 @@ class QoSSpec: self.in_order = in_order self.max_gap = max_gap self.timeout = timeout - - -def _fl_to_timespec(timeo: float): - if timeo is None: - return ffi.NULL - elif timeo <= 0: - return ffi.new("struct timespec *", [0, 0]) - else: - frac, whole = modf(timeo) - _timeo = ffi.new("struct timespec *") - _timeo.tv_sec = whole - _timeo.tv_nsec = frac * BILLION - return _timeo - - -def _timespec_to_fl(_timeo) -> Optional[float]: - if _timeo is ffi.NULL: - return None - elif _timeo.tv_sec <= 0 and _timeo.tv_nsec == 0: - return 0 - else: - return _timeo.tv_sec + _timeo.tv_nsec / BILLION - - -def _qos_to_qosspec(qos: QoSSpec): - if qos is None: - return ffi.NULL - else: - return ffi.new("qosspec_t *", - [qos.delay, - qos.bandwidth, - qos.availability, - qos.loss, - qos.ber, - qos.in_order, - qos.max_gap, - qos.timeout]) - - -def _qosspec_to_qos(_qos) -> Optional[QoSSpec]: - if _qos is ffi.NULL: - return None - else: - return QoSSpec(delay=_qos.delay, - bandwidth=_qos.bandwidth, - availability=_qos.availability, - loss=_qos.loss, - ber=_qos.ber, - in_order=_qos.in_order, - max_gap=_qos.max_gap, - timeout=_qos.timeout) -- cgit v1.2.3