diff options
| author | Dimitri Staessens <dimitri@ouroboros.rocks> | 2026-03-04 21:26:43 +0100 |
|---|---|---|
| committer | Dimitri Staessens <dimitri@ouroboros.rocks> | 2026-03-07 15:27:04 +0100 |
| commit | 7a4c37e8b673328dda59cec11ab9dce66c22a312 (patch) | |
| tree | d2a05d2fa022d97ba79d8af2d32ec9e7cc3bd9e1 | |
| parent | 62924a033cb2a0130cc6a072e03590f8eec5ac72 (diff) | |
| download | pyouroboros-7a4c37e8b673328dda59cec11ab9dce66c22a312.tar.gz pyouroboros-7a4c37e8b673328dda59cec11ab9dce66c22a312.zip | |
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
| -rw-r--r-- | .gitignore | 7 | ||||
| -rw-r--r-- | README.md | 200 | ||||
| -rwxr-xr-x | examples/oecho.py | 2 | ||||
| -rwxr-xr-x | examples/oecho_set.py | 2 | ||||
| -rw-r--r-- | ffi/fccntl_wrap.h | 21 | ||||
| -rw-r--r-- | ffi/irm_wrap.h | 90 | ||||
| -rw-r--r-- | ffi/pyouroboros_build_dev.py (renamed from ffi/pyouroboros_build.py) | 52 | ||||
| -rw-r--r-- | ffi/pyouroboros_build_irm.py | 291 | ||||
| -rw-r--r-- | ouroboros/cli.py | 543 | ||||
| -rw-r--r-- | ouroboros/dev.py | 102 | ||||
| -rw-r--r-- | ouroboros/event.py | 9 | ||||
| -rw-r--r-- | ouroboros/irm.py | 662 | ||||
| -rw-r--r-- | ouroboros/qos.py | 78 | ||||
| -rwxr-xr-x | setup.py | 5 |
14 files changed, 1956 insertions, 108 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c363780 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.pyc +*.pyo +build/ +dist/ +*.egg-info/ +*.so @@ -153,6 +153,7 @@ class FEventType(IntFlag): FlowUp FlowAlloc FlowDealloc + FlowPeer ``` and can be obtained by calling the next method: @@ -186,9 +187,206 @@ if t == FEventType.FlowPkt: line = f2.readline() ``` +## IRM API + +The IRM (IPC Resource Manager) module allows managing IPCPs, names, +and bindings programmatically. + +```Python +from ouroboros.irm import * +``` + +### IPCP Management + +Creating, bootstrapping, enrolling, and destroying IPCPs: + +```Python +# Create a local IPCP +pid = create_ipcp("my_ipcp", IpcpType.LOCAL) + +# Bootstrap it into a layer +conf = IpcpConfig(ipcp_type=IpcpType.LOCAL, layer_name="my_layer") +bootstrap_ipcp(pid, conf) + +# List all running IPCPs +for ipcp in list_ipcps(): + print(ipcp) + +# Enroll an IPCP +enroll_ipcp(pid, "enrollment_dst") + +# Destroy an IPCP +destroy_ipcp(pid) +``` + +IPCP types: `LOCAL`, `UNICAST`, `BROADCAST`, `ETH_LLC`, `ETH_DIX`, +`UDP4`, `UDP6`. + +### IPCP Configuration + +The `IpcpConfig` class is used to bootstrap an IPCP. It takes +the following parameters: + +```Python +IpcpConfig( + ipcp_type, # IpcpType (required) + layer_name="", # Layer name (string) + dir_hash_algo=DirectoryHashAlgo.SHA3_256, # Hash algorithm + unicast=None, eth=None, udp4=None, udp6=None # Type-specific config +) +``` + +The `dir_hash_algo` can be set to `SHA3_224`, `SHA3_256`, `SHA3_384`, +or `SHA3_512`. + +#### Local and Broadcast IPCPs + +Local and Broadcast IPCPs need no type-specific configuration: + +```Python +conf = IpcpConfig(ipcp_type=IpcpType.LOCAL, layer_name="local_layer") +conf = IpcpConfig(ipcp_type=IpcpType.BROADCAST, layer_name="bc_layer") +``` + +#### Unicast IPCPs + +Unicast IPCPs have the most detailed configuration, structured as +follows: + +```Python +conf = IpcpConfig( + ipcp_type=IpcpType.UNICAST, + layer_name="my_layer", + unicast=UnicastConfig( + dt=DtConfig( + addr_size=4, # Address size in bytes (default: 4) + eid_size=8, # Endpoint ID size in bytes (default: 8) + max_ttl=60, # Maximum time-to-live (default: 60) + routing=RoutingConfig( + pol=RoutingPolicy.LINK_STATE, + ls=LinkStateConfig( + pol=LinkStatePolicy.SIMPLE, # SIMPLE, LFA, or ECMP + t_recalc=4, # Recalculation interval (s) + t_update=15, # Update interval (s) + t_timeo=60 # Timeout (s) + ) + ) + ), + dir=DirConfig( + pol=DirectoryPolicy.DHT, + dht=DhtConfig( + alpha=3, # Concurrency parameter + k=8, # Replication factor + t_expire=86400, # Entry expiry time (s) + t_refresh=900, # Refresh interval (s) + t_replicate=900 # Replication interval (s) + ) + ), + addr_auth=AddressAuthPolicy.FLAT_RANDOM, + cong_avoid=CongestionAvoidPolicy.MB_ECN # or CA_NONE + ) +) +``` + +All sub-configs have sensible defaults, so for most cases a simpler +form suffices: + +```Python +conf = IpcpConfig( + ipcp_type=IpcpType.UNICAST, + layer_name="my_layer", + unicast=UnicastConfig() +) +``` + +#### Ethernet IPCPs (LLC and DIX) + +```Python +conf = IpcpConfig( + ipcp_type=IpcpType.ETH_LLC, # or IpcpType.ETH_DIX + layer_name="eth_layer", + eth=EthConfig( + dev="eth0", # Network device name + ethertype=0xA000 # Ethertype (mainly for DIX) + ) +) +``` + +#### UDP IPCPs + +For UDP over IPv4: + +```Python +conf = IpcpConfig( + ipcp_type=IpcpType.UDP4, + layer_name="udp4_layer", + udp4=Udp4Config( + ip_addr="192.168.1.1", # Local IP address + dns_addr="192.168.1.254", # DNS server address + port=3435 # UDP port (default: 3435) + ) +) +``` + +For UDP over IPv6: + +```Python +conf = IpcpConfig( + ipcp_type=IpcpType.UDP6, + layer_name="udp6_layer", + udp6=Udp6Config( + ip_addr="fd00::1", # Local IPv6 address + dns_addr="fd00::fe", # DNS server address + port=3435 # UDP port (default: 3435) + ) +) +``` + +### Connecting IPCP Components + +Connecting and disconnecting IPCP components: + +```Python +connect_ipcp(pid, DT_COMP, "destination") +disconnect_ipcp(pid, DT_COMP, "destination") +``` + +### Name Management + +Creating, destroying, and listing names: + +```Python +# Create a name +info = NameInfo(name="my_name", pol_lb=LoadBalancePolicy.ROUND_ROBIN) +create_name(info) + +# Register/unregister an IPCP to a name +reg_name("my_name", pid) +unreg_name("my_name", pid) + +# List all registered names +for name in list_names(): + print(name.name) + +# Destroy a name +destroy_name("my_name") +``` + +### Binding Programs and Processes + +```Python +# Bind a program to a name (auto-start on flow allocation) +bind_program("/usr/bin/my_server", "my_name", BIND_AUTO) +unbind_program("/usr/bin/my_server", "my_name") + +# Bind a running process to a name +bind_process(pid, "my_name") +unbind_process(pid, "my_name") +``` + ## Examples Some example code is in the examples folder. ## License -pyOuorboros is LGPLv2.1. The examples are 3-clause BSD. +pyOuroboros is LGPLv2.1. The examples are 3-clause BSD. diff --git a/examples/oecho.py b/examples/oecho.py index 395930c..d3fccfe 100755 --- a/examples/oecho.py +++ b/examples/oecho.py @@ -1,6 +1,6 @@ #!/bin/python -# Ouroboros - Copyright (C) 2016 - 2020 +# Ouroboros - Copyright (C) 2016 - 2026 # # A simple echo application # diff --git a/examples/oecho_set.py b/examples/oecho_set.py index 4d9d1f7..a389cc3 100755 --- a/examples/oecho_set.py +++ b/examples/oecho_set.py @@ -1,6 +1,6 @@ #!/bin/python -# Ouroboros - Copyright (C) 2016 - 2020 +# Ouroboros - Copyright (C) 2016 - 2026 # # A simple echo application # diff --git a/ffi/fccntl_wrap.h b/ffi/fccntl_wrap.h index ab227ea..f9a137e 100644 --- a/ffi/fccntl_wrap.h +++ b/ffi/fccntl_wrap.h @@ -1,10 +1,10 @@ /* - * Ouroboros - Copyright (C) 2016 - 2020 + * Ouroboros - Copyright (C) 2016 - 2026 * * An fccntl wrapper * - * Dimitri Staessens <dimitri.staessens@ugent.be> - * Sander Vrijders <sander.vrijders@ugent.be> + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License @@ -71,3 +71,18 @@ int flow_get_flags(int fd) return (int) flags; } + +int flow_set_frct_flags(int fd, uint16_t flags) +{ + return fccntl(fd, FRCTSFLAGS, flags); +} + +int flow_get_frct_flags(int fd) +{ + uint16_t flags; + + if (fccntl(fd, FRCTGFLAGS, &flags)) + return -EPERM; + + return (int) flags; +} diff --git a/ffi/irm_wrap.h b/ffi/irm_wrap.h new file mode 100644 index 0000000..23b64a0 --- /dev/null +++ b/ffi/irm_wrap.h @@ -0,0 +1,90 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * An IRM wrapper for Python bindings + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * + * 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/. + */ + +#include <ouroboros/irm.h> +#include <arpa/inet.h> +#include <string.h> +#include <stdlib.h> + +static int ipcp_config_udp4_set_ip(struct ipcp_config * conf, + const char * ip_str) +{ + return inet_pton(AF_INET, ip_str, &conf->udp4.ip_addr) == 1 ? 0 : -1; +} + +static int ipcp_config_udp4_set_dns(struct ipcp_config * conf, + const char * dns_str) +{ + return inet_pton(AF_INET, dns_str, &conf->udp4.dns_addr) == 1 ? 0 : -1; +} + +static int ipcp_config_udp6_set_ip(struct ipcp_config * conf, + const char * ip_str) +{ + return inet_pton(AF_INET6, ip_str, &conf->udp6.ip_addr) == 1 ? 0 : -1; +} + +static int ipcp_config_udp6_set_dns(struct ipcp_config * conf, + const char * dns_str) +{ + return inet_pton(AF_INET6, dns_str, &conf->udp6.dns_addr) == 1 ? 0 : -1; +} + +static void ipcp_config_init_uni(struct ipcp_config * conf, + uint8_t addr_size, uint8_t eid_size, + uint8_t max_ttl, + enum pol_link_state ls_pol, + long t_recalc, long t_update, long t_timeo, + enum pol_dir dir_pol, + uint32_t dht_alpha, uint32_t dht_k, + uint32_t dht_t_expire, uint32_t dht_t_refresh, + uint32_t dht_t_replicate, + enum pol_addr_auth addr_auth, + enum pol_cong_avoid cong_avoid) +{ + memset(conf, 0, sizeof(*conf)); + conf->type = IPCP_UNICAST; + conf->unicast.dt.addr_size = addr_size; + conf->unicast.dt.eid_size = eid_size; + conf->unicast.dt.max_ttl = max_ttl; + conf->unicast.dt.routing.pol = ROUTING_LINK_STATE; + conf->unicast.dt.routing.ls.pol = ls_pol; + conf->unicast.dt.routing.ls.t_recalc = t_recalc; + conf->unicast.dt.routing.ls.t_update = t_update; + conf->unicast.dt.routing.ls.t_timeo = t_timeo; + conf->unicast.dir.pol = dir_pol; + conf->unicast.dir.dht.params.alpha = dht_alpha; + conf->unicast.dir.dht.params.k = dht_k; + conf->unicast.dir.dht.params.t_expire = dht_t_expire; + conf->unicast.dir.dht.params.t_refresh = dht_t_refresh; + conf->unicast.dir.dht.params.t_replicate = dht_t_replicate; + conf->unicast.addr_auth_type = addr_auth; + conf->unicast.cong_avoid = cong_avoid; +} + +static void ipcp_config_init_eth(struct ipcp_config * conf, + const char * dev, + uint16_t ethertype) +{ + memset(conf, 0, sizeof(*conf)); + strncpy(conf->eth.dev, dev, DEV_NAME_SIZE); + conf->eth.ethertype = ethertype; +} diff --git a/ffi/pyouroboros_build.py b/ffi/pyouroboros_build_dev.py index a7a6a0d..751b492 100644 --- a/ffi/pyouroboros_build.py +++ b/ffi/pyouroboros_build_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 <dimitri@ouroboros.rocks> # @@ -24,15 +24,24 @@ from cffi import FFI ffibuilder: FFI = FFI() ffibuilder.cdef(""" +/* System types */ +typedef long... time_t; + +struct timespec { + time_t tv_sec; + long tv_nsec; + ...; +}; + /* OUROBOROS QOS.H */ typedef struct qos_spec { - uint32_t delay; /* In ms */ - uint64_t bandwidth; /* In bits/s */ - uint8_t availability; /* Class of 9s */ - uint32_t loss; /* Packet loss */ - uint32_t ber; /* Bit error rate, errors per billion bits */ - uint8_t in_order; /* In-order delivery, enables FRCT */ - uint32_t max_gap; /* In ms */ + uint32_t delay; + uint64_t bandwidth; + uint8_t availability; + uint32_t loss; + uint32_t ber; + uint8_t in_order; + uint32_t max_gap; uint32_t timeout; /* Timeout in ms */ } qosspec_t; @@ -46,7 +55,7 @@ int flow_alloc(const char * dst_name, int flow_accept(qosspec_t * qs, const struct timespec * timeo); -/* Returns flow descriptor, qs updates to supplied QoS. */ +/* Returns flow descriptor. */ int flow_join(const char * bc, const struct timespec * timeo); @@ -60,7 +69,7 @@ ssize_t flow_read(int fd, void * buf, size_t count); -/*OUROBOROS FCCNTL.H, VIA WRAPPER */ +/* OUROBOROS FCCNTL.H, VIA WRAPPER */ int flow_set_snd_timeout(int fd, struct timespec * ts); int flow_set_rcv_timeout(int fd, struct timespec * ts); @@ -79,13 +88,18 @@ int flow_set_flags(int fd, uint32_t flags); int flow_get_flags(int fd); -/*OUROBOROS FQUEUE.H */ +int flow_set_frct_flags(int fd, uint16_t flags); + +int flow_get_frct_flags(int fd); + +/* OUROBOROS FQUEUE.H */ enum fqtype { - FLOW_PKT = (1 << 0), - FLOW_DOWN = (1 << 1), - FLOW_UP = (1 << 2), - FLOW_ALLOC = (1 << 3), - FLOW_DEALLOC = (1 << 4) + FLOW_PKT = ..., + FLOW_DOWN = ..., + FLOW_UP = ..., + FLOW_ALLOC = ..., + FLOW_DEALLOC = ..., + FLOW_PEER = ... }; struct flow_set; @@ -116,14 +130,14 @@ void fset_del(fset_t * set, int fqueue_next(fqueue_t * fq); -int fqueue_type(fqueue_t * fq); +enum fqtype fqueue_type(fqueue_t * fq); ssize_t fevent(fset_t * set, fqueue_t * fq, const struct timespec * timeo); """) -ffibuilder.set_source("_ouroboros_cffi", +ffibuilder.set_source("_ouroboros_dev_cffi", """ #include "ouroboros/qos.h" #include "ouroboros/dev.h" diff --git a/ffi/pyouroboros_build_irm.py b/ffi/pyouroboros_build_irm.py new file mode 100644 index 0000000..e29287e --- /dev/null +++ b/ffi/pyouroboros_build_irm.py @@ -0,0 +1,291 @@ +# +# Ouroboros - Copyright (C) 2016 - 2026 +# +# Python API for Ouroboros +# +# Dimitri Staessens <dimitri@ouroboros.rocks> +# +# 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 cffi import FFI + +ffibuilder: FFI = FFI() + +ffibuilder.cdef(""" +/* System types */ +typedef int... pid_t; +typedef long... time_t; + +struct timespec { + time_t tv_sec; + long tv_nsec; + ...; +}; + +/* Network types */ +struct in_addr { ...; }; +struct in6_addr { ...; }; + +/* OUROBOROS QOS.H */ +typedef struct qos_spec { + uint32_t delay; + uint64_t bandwidth; + uint8_t availability; + uint32_t loss; + uint32_t ber; + uint8_t in_order; + uint32_t max_gap; + uint32_t timeout; /* Timeout in ms */ +} qosspec_t; + +/* OUROBOROS IPCP.H */ +enum ipcp_type { + IPCP_LOCAL = ..., + IPCP_UNICAST = ..., + IPCP_BROADCAST = ..., + IPCP_ETH_LLC = ..., + IPCP_ETH_DIX = ..., + IPCP_UDP4 = ..., + IPCP_UDP6 = ..., + IPCP_INVALID = ... +}; + +/* Unicast IPCP policies */ +enum pol_addr_auth { + ADDR_AUTH_FLAT_RANDOM = ..., + ADDR_AUTH_INVALID = ... +}; + +enum pol_link_state { + LS_SIMPLE = ..., + LS_LFA = ..., + LS_ECMP = ..., + LS_INVALID = ... +}; + +struct ls_config { + enum pol_link_state pol; + time_t t_recalc; + time_t t_update; + time_t t_timeo; +}; + +enum pol_routing { + ROUTING_LINK_STATE = ..., + ROUTING_INVALID = ... +}; + +struct routing_config { + enum pol_routing pol; + union { + struct ls_config ls; + }; +}; + +enum pol_cong_avoid { + CA_NONE = ..., + CA_MB_ECN = ..., + CA_INVALID = ... +}; + +struct dt_config { + struct { + uint8_t addr_size; + uint8_t eid_size; + uint8_t max_ttl; + }; + struct routing_config routing; +}; + +enum pol_dir { + DIR_DHT = ..., + DIR_INVALID = ... +}; + +enum pol_dir_hash { + DIR_HASH_SHA3_224 = ..., + DIR_HASH_SHA3_256 = ..., + DIR_HASH_SHA3_384 = ..., + DIR_HASH_SHA3_512 = ..., + DIR_HASH_INVALID = ... +}; + +struct dir_dht_config { + struct { + uint32_t alpha; + uint32_t k; + uint32_t t_expire; + uint32_t t_refresh; + uint32_t t_replicate; + } params; + uint64_t peer; +}; + +struct dir_config { + enum pol_dir pol; + union { + struct dir_dht_config dht; + }; +}; + +struct uni_config { + struct dt_config dt; + struct dir_config dir; + enum pol_addr_auth addr_auth_type; + enum pol_cong_avoid cong_avoid; +}; + +struct eth_config { + char dev[256]; /* DEV_NAME_SIZE + 1 */ + uint16_t ethertype; +}; + +struct udp4_config { + struct in_addr ip_addr; + struct in_addr dns_addr; + uint16_t port; +}; + +struct udp6_config { + struct in6_addr ip_addr; + struct in6_addr dns_addr; + uint16_t port; +}; + +struct layer_info { + char name[256]; /* LAYER_NAME_SIZE + 1 */ + enum pol_dir_hash dir_hash_algo; +}; + +struct ipcp_config { + struct layer_info layer_info; + enum ipcp_type type; + + union { + struct uni_config unicast; + struct udp4_config udp4; + struct udp6_config udp6; + struct eth_config eth; + }; +}; + +/* OUROBOROS NAME.H */ +#define BIND_AUTO ... + +enum pol_balance { + LB_RR = ..., + LB_SPILL = ..., + LB_INVALID = ... +}; + +struct name_sec_paths { + char enc[512]; /* NAME_PATH_SIZE + 1 */ + char key[512]; /* NAME_PATH_SIZE + 1 */ + char crt[512]; /* NAME_PATH_SIZE + 1 */ +}; + +struct name_info { + char name[256]; /* NAME_SIZE + 1 */ + enum pol_balance pol_lb; + + struct name_sec_paths s; + struct name_sec_paths c; +}; + +/* OUROBOROS IRM.H */ + +struct ipcp_list_info { + pid_t pid; + enum ipcp_type type; + char name[255]; /* NAME_SIZE */ + char layer[255]; /* LAYER_NAME_SIZE */ +}; + +pid_t irm_create_ipcp(const char * name, + enum ipcp_type type); + +int irm_destroy_ipcp(pid_t pid); + +ssize_t irm_list_ipcps(struct ipcp_list_info ** ipcps); + +int irm_enroll_ipcp(pid_t pid, + const char * dst); + +int irm_bootstrap_ipcp(pid_t pid, + const struct ipcp_config * conf); + +int irm_connect_ipcp(pid_t pid, + const char * component, + const char * dst, + qosspec_t qs); + +int irm_disconnect_ipcp(pid_t pid, + const char * component, + const char * dst); + +int irm_bind_program(const char * prog, + const char * name, + uint16_t opts, + int argc, + char ** argv); + +int irm_unbind_program(const char * progr, + const char * name); + +int irm_bind_process(pid_t pid, + const char * name); + +int irm_unbind_process(pid_t pid, + const char * name); + +int irm_create_name(struct name_info * info); + +int irm_destroy_name(const char * name); + +ssize_t irm_list_names(struct name_info ** names); + +int irm_reg_name(const char * name, + pid_t pid); + +int irm_unreg_name(const char * name, + pid_t pid); + +/* IRM WRAPPER HELPERS */ +int ipcp_config_udp4_set_ip(struct ipcp_config * conf, + const char * ip_str); + +int ipcp_config_udp4_set_dns(struct ipcp_config * conf, + const char * dns_str); + +int ipcp_config_udp6_set_ip(struct ipcp_config * conf, + const char * ip_str); + +int ipcp_config_udp6_set_dns(struct ipcp_config * conf, + const char * dns_str); + +/* libc */ +void free(void *ptr); +""") + +ffibuilder.set_source("_ouroboros_irm_cffi", + """ +#include "ouroboros/qos.h" +#include "irm_wrap.h" + """, + libraries=['ouroboros-irm'], + extra_compile_args=["-I./ffi/"]) + +if __name__ == "__main__": + ffibuilder.compile(verbose=True) 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 <dimitri@ouroboros.rocks> +# +# 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 <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 <n>] [type <t>] [layer <l>]``. + + :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 <name> ipcp <ipcp> [ipcp ...] + layer <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 <prog> name <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 <prog> name <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 <pid> name <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 <pid> name <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 <ipcp> name <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 <ipcp> name <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 <name> [lb <policy>]``. + + :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 [<name>]``. + + :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 <name> ipcp <ipcp> [ipcp ...] + layer <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 <n> layer <l> [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 <n> layer <dst> [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 <n> dst <dst> [component <c>] + [qos <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 <n> dst <dst> + [component <c>]``. + + 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 <name> type <type> layer <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 <dimitri@ouroboros.rocks> # @@ -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 <dimitri@ouroboros.rocks> # @@ -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 <dimitri@ouroboros.rocks> +# +# 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 <dimitri@ouroboros.rocks> # @@ -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) @@ -4,7 +4,7 @@ import setuptools setuptools.setup( name='PyOuroboros', - version=0.19, + version='0.23.0', url='https://ouroboros.rocks', keywords='ouroboros IPC subsystem', author='Dimitri Staessens', @@ -18,7 +18,8 @@ setuptools.setup( "cffi>=1.0.0" ], cffi_modules=[ - "ffi/pyouroboros_build.py:ffibuilder" + "ffi/pyouroboros_build_dev.py:ffibuilder", + "ffi/pyouroboros_build_irm.py:ffibuilder" ], install_requires=[ "cffi>=1.0.0" |
