1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
|
# pyOuroboros
> a Python API for the Ouroboros recursive network prototype
## Dependencies
pyOuroboros requires <a href="https://ouroboros.rocks">Ouroboros</a>
to be installed (matching `major.minor` version — see [Versioning](#versioning)).
## Installation
To build and install pyOuroboros:
```shell
pip install .
```
Or for an editable install during development:
```shell
pip install -e .
```
## Basic Usage
Applications import each part of the API from its submodule
explicitly — flows, events and the control plane are three distinct
concerns, and the import line says which one is in use:
```Python
from ouroboros.dev import Flow, flow_alloc, flow_accept, flow_join
from ouroboros.event import FEventQueue, FEventType, FlowSet
from ouroboros.irm import create_ipcp, bootstrap_ipcp, reg_name
# or, for IRM operations that mirror the `irm` CLI tool:
from ouroboros.cli import autoboot, destroy_ipcp
```
The pure-Python `errors` and `qos` modules are also re-exported at
the top level for convenience (`from ouroboros import QoSSpec,
OuroborosError`), but `dev`, `event`, `irm` and `cli` are only
available through their submodule path — importing them eagerly
from the top-level package would load the libouroboros CFFI
extensions on every `import ouroboros`, which is the wrong default
for a binding layer.
Server side: accept a flow.
```Python
f = flow_accept()
```
returns a new allocated `Flow` object.
Client side: allocate a flow to a certain _name_.
```Python
f = flow_alloc("name")
```
Broadcast: join a broadcast layer.
```Python
f = flow_join("name")
```
Deallocate a flow:
```Python
f.dealloc()
```
`dealloc()` is idempotent. To avoid calling it explicitly, use a
`with` statement — and any `Flow` that is garbage-collected without
being deallocated will be cleaned up too:
```Python
with flow_alloc("dst") as f:
f.writeline("line")
print(f.readline())
```
`Flow()` constructs an empty wrapper; `Flow(fd)` wraps an existing
flow descriptor. `f.alloc("name")` populates an empty wrapper:
```Python
f = Flow()
f.alloc("name")
```
To read / write on a flow:
```Python
f.read(count) # read up to `count` bytes, return bytes
f.readline() # read and decode as UTF-8, return str
f.write(buf, count) # write up to `count` bytes from buffer
f.writeline(ln) # encode `ln` as UTF-8 and write, return bytes written
```
## Quality of Service (QoS)
`QoSSpec` describes the QoS requested for a flow. It is a **frozen**
dataclass; construct a new spec to vary fields, or use
`dataclasses.replace(qos, ...)` to derive one.
```Python
from ouroboros.qos import QoSSpec, QoSService
qos = QoSSpec(service=QoSService.MESSAGE, loss=0, timeout=60000)
f = flow_alloc("name", qos)
```
The `service` field selects the framing / reliability class and
enables FRCT for values > 0:
```Python
class QoSService(IntEnum):
RAW # No FRCT; best-effort raw messages
MESSAGE # FRCT, reliable ordered messages
STREAM # FRCT, reliable ordered byte stream
```
A handful of predefined QoS specs mirror the ones in
`ouroboros/qos.h` (`_safe` enables an integrity check by setting
`ber=0`; `rt` trades reliability for latency):
```Python
from ouroboros import (
QOS_RAW, QOS_RAW_SAFE, # Raw best-effort
QOS_RT, QOS_RT_SAFE, # Real-time, low latency
QOS_MSG, # Reliable ordered messages
QOS_STREAM, # Reliable ordered byte stream
)
f = flow_alloc("name", QOS_STREAM)
```
## Manipulating flows
A number of methods are available to inspect and tune a `Flow`:
```Python
f.set_snd_timeout(0.5) # set timeout for blocking write (seconds)
f.set_rcv_timeout(1.0) # set timeout for blocking read (seconds)
f.get_snd_timeout() # get timeout for blocking write
f.get_rcv_timeout() # get timeout for blocking read
f.get_qos() # get the QoSSpec for this flow
f.get_rx_queue_len() # bytes pending in the rx buffer
f.get_tx_queue_len() # bytes pending in the tx buffer
f.get_mtu() # per-packet MTU (0 if unknown)
f.set_flags(flags) # replace the full set of flags
f.add_flags(flags) # OR new flags into the current flow flags
f.remove_flags(flags) # clear flags while preserving the rest
f.get_flags() # get the flags for this flow
f.fileno() # underlying ouroboros flow descriptor
```
The flags are specified as an `IntFlag` enum, `FlowProperties`:
```Python
class FlowProperties(IntFlag):
READ_ONLY
WRITE_ONLY
READ_WRITE
DOWN
NON_BLOCKING_READ
NON_BLOCKING_WRITE
NON_BLOCKING # NON_BLOCKING_READ | NON_BLOCKING_WRITE
NO_PARTIAL_READ
NO_PARTIAL_WRITE
```
For FRCT-enabled flows (`service > 0`), the FRCT state can be tuned:
```Python
from ouroboros.dev import FrctFlags
f.set_frct_flags(FrctFlags.RESCNTL | FrctFlags.LINGER)
f.get_frct_flags()
f.set_frct_max_sdu(size) # max reassembly SDU (bytes)
f.get_frct_max_sdu()
f.set_frct_rcv_ring_size(size) # stream rcv ring (bytes, pow2)
f.get_frct_rcv_ring_size()
f.set_frct_rto_min(rto_ns) # RTO floor in nanoseconds
f.get_frct_rto_min()
```
FRCT flags: `FrctFlags.RETRANSMIT` (fixed at flow alloc),
`FrctFlags.RESCNTL`, `FrctFlags.LINGER`.
See the Ouroboros fccntl documentation for more details.
```shell
man fccntl
```
## Event API
Multiple flows can be monitored for activity in parallel using
`FlowSet` and `FEventQueue` objects.
A `FlowSet` groups `Flow` objects together. It can be constructed
with an optional list of flows; flows can be added or removed at any
time:
```Python
from ouroboros.event import FlowSet
fs = FlowSet() # create an empty flow set
fs.add(f) # add a Flow `f` to this set
fs.remove(f) # remove a Flow `f` from this set
fs.zero() # remove all Flows from this set
```
An `FEventQueue` stores pending events on flows. Event types:
```Python
class FEventType(IntFlag):
FLOW_PKT
FLOW_DOWN
FLOW_UP
FLOW_ALLOC
FLOW_DEALLOC
FLOW_PEER
```
`FlowSet.wait()` populates an `FEventQueue` from a set; pending
events are then drained via `FEventQueue.next()`:
```Python
from ouroboros.event import FEventQueue, FEventType, FlowSet
fq = FEventQueue()
fs = FlowSet([f1, f2, f3])
fs.wait(fq, timeo=1.0) # block up to 1 second
while True:
try:
f, t = fq.next()
except OuroborosError:
break # queue drained
if t == FEventType.FLOW_PKT:
msg = f.readline()
...
fs.destroy()
```
Both `FlowSet` and `FEventQueue` are context managers (preferred):
```Python
with FEventQueue() as fq, FlowSet([f]) as fs:
fs.wait(fq)
f2, t = fq.next()
if t == FEventType.FLOW_PKT:
line = f2.readline()
```
`destroy()` is idempotent; GC will also reclaim the underlying C
handles when the wrapper goes out of scope.
## Error handling
All pyOuroboros exceptions derive from `OuroborosError`. The
hierarchy mirrors stdlib conventions: flow-plane and IRM-plane errors
have their own base classes, and several specific subclasses inherit
from Python builtins so they can be caught either way.
```
OuroborosError
├── IrmError
│ ├── IpcpCreateError, IpcpBootstrapError, IpcpEnrollError
│ ├── IpcpConnectError, IpcpTypeError (ValueError), IpcpStateError
│ ├── IrmdError, IpcpdError, BindError
│ └── NameNotFoundError, NameExistsError, InvalidNameError (ValueError)
└── FlowError
├── FlowAlreadyAllocatedError, FlowNotAllocatedError
├── FlowDownError (ConnectionError), FlowPeerError (FlowDownError)
├── FlowPermissionError (PermissionError)
├── FlowTimeout (TimeoutError)
├── FlowCryptError, FlowAuthError, FlowReplayError
└── FlowEventError
FlowDeallocWarning (Warning)
```
ouroboros errno codes (`ENOTALLOC`, `EFLOWDOWN`, `EFLOWPEER`,
`ENAME`, `ECRYPT`, ...) translate to the matching semantic subclass;
ambiguous libc errnos (`ETIMEDOUT`, `EAGAIN`, `ENOTCONN`,
`ECONNRESET`) translate to the stdlib base class.
```Python
from ouroboros import OuroborosError, FlowDownError
try:
f.read()
except TimeoutError:
... # rcv timeout elapsed
except FlowDownError:
... # peer went away
except OuroborosError as e:
log.warning("flow error: %s", e)
```
## IRM API
The IRM (IPC Resource Manager) module exposes the raw C API for
managing IPCPs, names, and bindings:
```Python
from ouroboros.irm import (
IpcpType, IpcpConfig, NameInfo, BIND_AUTO, DT_COMP, MGMT_COMP,
create_ipcp, bootstrap_ipcp, enroll_ipcp, destroy_ipcp, list_ipcps,
connect_ipcp, disconnect_ipcp,
create_name, destroy_name, list_names, reg_name, unreg_name,
bind_program, unbind_program, bind_process, unbind_process,
)
```
For most use cases, the higher-level [`ouroboros.cli`](#cli-helpers)
wrappers are easier to work with — they mirror the `irm` CLI tool and
take IPCP names rather than pids.
### IPCP Management
Create, bootstrap, enroll, and destroy 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
`IpcpConfig` is a dataclass used to bootstrap an IPCP. It takes
the following fields:
```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
)
```
`dir_hash_algo` can be `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:
```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 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
```Python
connect_ipcp(pid, DT_COMP, "destination") # data transfer plane
connect_ipcp(pid, MGMT_COMP, "destination") # management plane
disconnect_ipcp(pid, DT_COMP, "destination")
```
`connect_ipcp` optionally takes a `qos=QoSSpec(...)` argument.
### Name Management
Create, destroy, and list 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")
```
## CLI helpers
`ouroboros.cli` mirrors the C-side `ouroboros/tools/irm` command-line
tool. The functions take IPCP _names_ (not pids), call `realpath()`
on programs, and handle the `autobind` flag for bootstrap / enroll:
```Python
from ouroboros.cli import (
create_ipcp, destroy_ipcp, bootstrap_ipcp, enroll_ipcp,
connect_ipcp, disconnect_ipcp,
bind_program, bind_ipcp, autoboot,
reg_name, unreg_name,
IpcpType, IpcpConfig, # re-exported for convenience
)
# Create + bootstrap + autobind in one step
autoboot("my_ipcp", IpcpType.LOCAL, layer="my_layer")
# Bootstrap with autobind (binds the IPCP to its name and layer first)
conf = IpcpConfig(ipcp_type=IpcpType.UNICAST, layer_name="dc")
bootstrap_ipcp("my_ipcp", conf, autobind=True)
# Register a name with multiple IPCPs at once
reg_name("server", ipcps=["ipcp1", "ipcp2"])
```
See the module docstring for the full CLI-command ↔ Python mapping.
## Examples
See the [`examples/`](examples/) folder for runnable client/server
demos.
## Versioning
pyOuroboros uses `setuptools_scm` to derive its version from git tags.
**Compatibility contract across Ouroboros repositories:**
| Scope | Rule |
|---|---|
| ouroboros (C) ↔ pyouroboros / rumba | Shared `major.minor` — pyouroboros requires at least the same ouroboros `major.minor` |
| pyouroboros ↔ rumba ↔ ouroboros-integration | Strict lockstep `major.minor.patch` — always released together |
## License
pyOuroboros is LGPLv2.1. The examples are 3-clause BSD.
|