Add a `tipc` guide page, roster it in the docs
Plan 01 §8's docs deliverable: `docs/guide/tipc.rst`, leading
w/ the `tipc nametable show` demo as the plan asked.
Frames the backend by what makes it different — every other tpt
gives you a pipe and leaves discovery to the registrar, whereas
TIPC's service names live in a kernel-maintained cluster-wide
name table, so a `.bind()` IS registration and a `.connect()` IS
the lookup. Then: push-based discovery via
`open_topology_events()`, scope-as-`.bindspace`, bearer setup
for spanning hosts, and the gotchas.
Also,
- roster it in `guide/index.rst` (prose list + toctree)
- `api/ipc.rst`'s transport line said `['tcp' | 'uds']` and
described only 2 unwrapped-addr shapes; now mentions `tipc`
and its proto-keyed `('tipc', stype, instance, scope)`.
Verified w/ a full `sphinx -b html` build: succeeded, page
renders, internal refs resolve.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-16 00:08:12 +00:00
|
|
|
TIPC: when the kernel does discovery
|
|
|
|
|
====================================
|
|
|
|
|
|
|
|
|
|
Every other ``tractor`` transport gives you a *pipe* and leaves
|
|
|
|
|
discovery to us: the registrar actor, the ``find_actor()``
|
|
|
|
|
round-trip, the whole :doc:`discovery` story. TIPC_
|
|
|
|
|
(Transparent Inter-Process Communication) is different — it's a
|
|
|
|
|
linux-kernel cluster protocol whose **service names live in a
|
|
|
|
|
cluster-wide name table the kernel itself maintains**.
|
|
|
|
|
|
|
|
|
|
Which flips the model:
|
|
|
|
|
|
|
|
|
|
- an actor's IPC address *is* a service name ``(stype,
|
|
|
|
|
instance)`` — no host, no port,
|
|
|
|
|
- ``.bind()``-ing that name **is** service registration,
|
|
|
|
|
- a peer's ``.connect()``-by-name **is** the lookup, resolved
|
|
|
|
|
and load-balanced in-kernel.
|
|
|
|
|
|
|
|
|
|
So for TIPC-capable deployments the registrar round-trip stops
|
|
|
|
|
being the only way peers find each other. Enable it per actor
|
|
|
|
|
like any other backend,
|
|
|
|
|
|
|
|
|
|
.. code:: python
|
|
|
|
|
|
|
|
|
|
async with tractor.open_nursery(
|
|
|
|
|
enable_transports=['tipc'],
|
|
|
|
|
) as an:
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
.. warning::
|
|
|
|
|
|
|
|
|
|
TIPC is **opt-in and linux-only**. The ``tipc`` kernel module
|
|
|
|
|
is not loaded on most boxes (``sudo modprobe tipc``), and the
|
|
|
|
|
address family doesn't exist off-linux at all. Check
|
|
|
|
|
:func:`tractor.ipc._tipc.is_tipc_available` before assuming;
|
|
|
|
|
``tractor`` never selects this backend for you.
|
|
|
|
|
|
|
|
|
|
.. _TIPC: https://en.wikipedia.org/wiki/Transparent_Inter-process_Communication
|
|
|
|
|
|
|
|
|
|
Your actor tree, in the kernel's name table
|
|
|
|
|
-------------------------------------------
|
|
|
|
|
|
|
|
|
|
The single best demo this backend has needs no ``tractor`` API
|
|
|
|
|
at all — boot a tree and ask ``tipc(8)`` what it sees:
|
|
|
|
|
|
|
|
|
|
.. code:: bash
|
|
|
|
|
|
|
|
|
|
sudo modprobe tipc
|
|
|
|
|
python examples/multihost/tipc_cluster/single_host.py
|
|
|
|
|
|
|
|
|
|
.. code:: text
|
|
|
|
|
|
|
|
|
|
--- `tipc nametable show` :: root + 3 subactors ---
|
|
|
|
|
Type Lower Upper Scope Port
|
|
|
|
|
1953628160 1616 1616 cluster 3161982128
|
|
|
|
|
1953628160 1219427151 1219427151 cluster 1587358717
|
|
|
|
|
1953628160 2641339936 2641339936 cluster 1864021571
|
|
|
|
|
1953628160 3344505866 3344505866 cluster 3816483388
|
|
|
|
|
|
|
|
|
|
--- `tipc nametable show` :: after teardown (all withdrawn) ---
|
|
|
|
|
Type Lower Upper Scope Port
|
|
|
|
|
|
|
|
|
|
Reading the rows,
|
|
|
|
|
|
|
|
|
|
- ``1953628160`` is ``0x74720000``, ``tractor``'s reserved
|
|
|
|
|
service *type* — ascii ``tr`` in the high half, with the low
|
|
|
|
|
16 bits free so an app can partition its own service classes
|
|
|
|
|
via ``TIPCAddress._stype``,
|
|
|
|
|
- ``1616`` is the host-singleton registrar instance, the same
|
|
|
|
|
"1616 is tractor's registrar" idiom as the TCP port and the
|
|
|
|
|
``registry@1616.sock`` UDS filename,
|
|
|
|
|
- the other three are per-actor instances derived from a
|
|
|
|
|
``blake2b`` digest of the actor's identity (see
|
|
|
|
|
`Silent crosstalk`_),
|
|
|
|
|
- ``Scope`` is the address' :attr:`bindspace` — see `Scope is
|
|
|
|
|
the bindspace`_.
|
|
|
|
|
|
|
|
|
|
Push-based discovery
|
|
|
|
|
--------------------
|
|
|
|
|
|
|
|
|
|
TIPC also exposes a *topology service*: subscribe and the kernel
|
|
|
|
|
pushes you name-table transitions as they happen.
|
|
|
|
|
:func:`tractor.ipc._tipc.open_topology_events` wraps it as an
|
|
|
|
|
``@acm`` yielding a ``trio`` receive-channel,
|
|
|
|
|
|
|
|
|
|
.. code:: python
|
|
|
|
|
|
|
|
|
|
from tractor.ipc._tipc import open_topology_events
|
|
|
|
|
|
|
|
|
|
async with open_topology_events() as events:
|
|
|
|
|
async for ev in events:
|
|
|
|
|
print(f'{ev.kind}: {ev.addr}')
|
|
|
|
|
|
|
|
|
|
.. code:: text
|
|
|
|
|
|
|
|
|
|
watching the TIPC name table..
|
|
|
|
|
[+] published instance=1616 port=0x00000000:2375440573
|
|
|
|
|
spawning subactors..
|
|
|
|
|
[+] published instance=186947472 port=0x00000000:3960753074
|
|
|
|
|
[+] published instance=2191362136 port=0x00000000:2263898853
|
|
|
|
|
tearing down..
|
|
|
|
|
[-] withdrawn instance=186947472 port=0x00000000:3960753074
|
|
|
|
|
|
|
|
|
|
No polling, no registrar round-trip — this is the groundwork for
|
|
|
|
|
a registrar that keeps a live view of the actor set without ever
|
|
|
|
|
calling ``find_actor()``.
|
|
|
|
|
|
|
|
|
|
``filt`` picks the granularity: ``TIPC_SUB_SERVICE`` gives one
|
|
|
|
|
event per *name* becoming (un)available, ``TIPC_SUB_PORTS`` one
|
|
|
|
|
per *publisher* — which is what makes the duplicate-name case
|
|
|
|
|
below externally observable.
|
|
|
|
|
|
|
|
|
|
Scope is the bindspace
|
|
|
|
|
----------------------
|
|
|
|
|
|
|
|
|
|
Every ``tractor`` address type has a ``.bindspace`` — "the set
|
|
|
|
|
of hosts this bind is reachable from". For TCP that's the IP,
|
|
|
|
|
for UDS the socket-file directory. For TIPC it's the *scope*,
|
|
|
|
|
which is about as literal a reading of that docstring as exists:
|
|
|
|
|
|
|
|
|
|
.. list-table::
|
|
|
|
|
:header-rows: 1
|
|
|
|
|
:widths: 30 70
|
|
|
|
|
|
|
|
|
|
* - scope
|
|
|
|
|
- meaning
|
|
|
|
|
* - ``TIPC_NODE_SCOPE``
|
|
|
|
|
- same host only — the UDS analogue
|
|
|
|
|
* - ``TIPC_CLUSTER_SCOPE``
|
|
|
|
|
- cluster-visible (the default)
|
|
|
|
|
|
|
|
|
|
``TIPC_ZONE_SCOPE`` is deprecated and aliased to cluster-scope
|
|
|
|
|
by modern kernels; ``tractor`` accepts it on input, folds it to
|
|
|
|
|
cluster and logs at ``transport`` level.
|
|
|
|
|
|
|
|
|
|
Spanning hosts
|
|
|
|
|
--------------
|
|
|
|
|
|
|
|
|
|
Single-host TIPC needs only ``modprobe``. Crossing hosts needs a
|
|
|
|
|
**bearer** enabled on both — an ethernet (L2) or UDP underlay
|
|
|
|
|
the kernel routes service names over:
|
|
|
|
|
|
|
|
|
|
.. code:: bash
|
|
|
|
|
|
|
|
|
|
# on BOTH hosts
|
|
|
|
|
sudo tipc bearer enable media eth device eth0
|
|
|
|
|
# ..or, when L2 isn't available:
|
|
|
|
|
sudo tipc bearer enable media udp name uc localip 10.0.11.1
|
|
|
|
|
|
|
|
|
|
tipc link list # must list the peer before you proceed
|
|
|
|
|
|
|
|
|
|
The two-host example pair then talks with **no IP, hostname or
|
|
|
|
|
port anywhere in either script** — both sides name the same
|
|
|
|
|
service and the kernel routes it. Move the server to a third
|
|
|
|
|
node and the client's dial keeps working, unchanged. See
|
|
|
|
|
``examples/multihost/tipc_cluster/`` for the full walkthrough
|
|
|
|
|
(that directory is excluded from CI precisely because it needs
|
|
|
|
|
real hardware).
|
|
|
|
|
|
Document TIPC-over-`wg`, add a handoff doc
Anticipating gh #502 — TIPC over a WireGuard mesh as our go-to
multihost tpt deployment — plus a cold-start handoff for whoever
(or whatever) picks this up next.
The wg deats, both verified locally,
- a wg iface is L3/`tun` (`POINTOPOINT,NOARP`, `link/none`, no
L2 addr) so TIPC's `eth` media **cannot** bind it; the udp
bearer is *mandatory* over wg, not merely an alternative.
Also its ~1420 MTU sits under ethernet's 1500.
- the composed deployment maddr is
`/ip4/<pub>/udp/51820/wg/u<key>/tipc/<stype>/<inst>/<scope>`.
XXX note the tipc segment has NO locative part unlike tcp's
inner `/ip4/../tcp/..` — a service name is
location-independent, so wg carries routing and tipc carries
identity. That's the argument for one `/tipc` proto w/ a
structured value in the #498 spec proposal.
XXX ALSO correcting a premise: TIPC is **not** unencrypted. It
ships AES-GCM crypto (`tipc node set key`, linux 5.9+) w/
cluster/master/per-node keys + rekeying. Those keys are
symmetric+pre-shared tho, so wg is still preferred for
public-key identity, NAT traversal, and one overlay every tpt
can share.
`01_tipc_HANDOFF.md` is deliberately provider-neutral: env
setup, the hard-won kernel facts table, the two closed design
decisions (+why), what landed, the pre-land TODOs and the repo's
working conventions.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-17 21:38:05 +00:00
|
|
|
Over a WireGuard mesh
|
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
|
|
TIPC over a `wg` mesh is the intended reference deployment for
|
|
|
|
|
multihost ``tractor`` (see gh #502), composing with the tunnel
|
|
|
|
|
examples in ``examples/multihost/wg_lan/``.
|
|
|
|
|
|
|
|
|
|
.. warning::
|
|
|
|
|
|
|
|
|
|
A wg interface is L3/``tun`` — ``POINTOPOINT,NOARP`` with
|
|
|
|
|
``link/none`` and no L2 address — so TIPC's ``eth`` media
|
|
|
|
|
**cannot** bind it. Over wg the udp bearer is *mandatory*,
|
|
|
|
|
not merely an alternative:
|
|
|
|
|
|
|
|
|
|
.. code:: bash
|
|
|
|
|
|
|
|
|
|
# NOT possible over wg
|
|
|
|
|
sudo tipc bearer enable media eth device wg0
|
|
|
|
|
|
|
|
|
|
# required instead, bound to the wg overlay IP
|
|
|
|
|
sudo tipc bearer enable media udp name wgmesh \
|
|
|
|
|
localip 10.0.11.1
|
|
|
|
|
|
|
|
|
|
Mind the MTU too: wg links typically sit at 1420, under
|
|
|
|
|
ethernet's 1500.
|
|
|
|
|
|
Add a `tipc` guide page, roster it in the docs
Plan 01 §8's docs deliverable: `docs/guide/tipc.rst`, leading
w/ the `tipc nametable show` demo as the plan asked.
Frames the backend by what makes it different — every other tpt
gives you a pipe and leaves discovery to the registrar, whereas
TIPC's service names live in a kernel-maintained cluster-wide
name table, so a `.bind()` IS registration and a `.connect()` IS
the lookup. Then: push-based discovery via
`open_topology_events()`, scope-as-`.bindspace`, bearer setup
for spanning hosts, and the gotchas.
Also,
- roster it in `guide/index.rst` (prose list + toctree)
- `api/ipc.rst`'s transport line said `['tcp' | 'uds']` and
described only 2 unwrapped-addr shapes; now mentions `tipc`
and its proto-keyed `('tipc', stype, instance, scope)`.
Verified w/ a full `sphinx -b html` build: succeeded, page
renders, internal refs resolve.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-16 00:08:12 +00:00
|
|
|
.. note::
|
|
|
|
|
|
Document TIPC-over-`wg`, add a handoff doc
Anticipating gh #502 — TIPC over a WireGuard mesh as our go-to
multihost tpt deployment — plus a cold-start handoff for whoever
(or whatever) picks this up next.
The wg deats, both verified locally,
- a wg iface is L3/`tun` (`POINTOPOINT,NOARP`, `link/none`, no
L2 addr) so TIPC's `eth` media **cannot** bind it; the udp
bearer is *mandatory* over wg, not merely an alternative.
Also its ~1420 MTU sits under ethernet's 1500.
- the composed deployment maddr is
`/ip4/<pub>/udp/51820/wg/u<key>/tipc/<stype>/<inst>/<scope>`.
XXX note the tipc segment has NO locative part unlike tcp's
inner `/ip4/../tcp/..` — a service name is
location-independent, so wg carries routing and tipc carries
identity. That's the argument for one `/tipc` proto w/ a
structured value in the #498 spec proposal.
XXX ALSO correcting a premise: TIPC is **not** unencrypted. It
ships AES-GCM crypto (`tipc node set key`, linux 5.9+) w/
cluster/master/per-node keys + rekeying. Those keys are
symmetric+pre-shared tho, so wg is still preferred for
public-key identity, NAT traversal, and one overlay every tpt
can share.
`01_tipc_HANDOFF.md` is deliberately provider-neutral: env
setup, the hard-won kernel facts table, the two closed design
decisions (+why), what landed, the pre-land TODOs and the repo's
working conventions.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-17 21:38:05 +00:00
|
|
|
TIPC is **not** unencrypted — it ships AES-GCM crypto of its
|
|
|
|
|
own (``tipc node set key``, linux 5.9+) with cluster, master
|
|
|
|
|
and per-node keys plus rekeying intervals. Those keys are
|
|
|
|
|
symmetric and pre-shared though, so a wg mesh is still
|
|
|
|
|
preferred for public-key identity, NAT traversal, and an
|
|
|
|
|
overlay every transport can share.
|
Add a `tipc` guide page, roster it in the docs
Plan 01 §8's docs deliverable: `docs/guide/tipc.rst`, leading
w/ the `tipc nametable show` demo as the plan asked.
Frames the backend by what makes it different — every other tpt
gives you a pipe and leaves discovery to the registrar, whereas
TIPC's service names live in a kernel-maintained cluster-wide
name table, so a `.bind()` IS registration and a `.connect()` IS
the lookup. Then: push-based discovery via
`open_topology_events()`, scope-as-`.bindspace`, bearer setup
for spanning hosts, and the gotchas.
Also,
- roster it in `guide/index.rst` (prose list + toctree)
- `api/ipc.rst`'s transport line said `['tcp' | 'uds']` and
described only 2 unwrapped-addr shapes; now mentions `tipc`
and its proto-keyed `('tipc', stype, instance, scope)`.
Verified w/ a full `sphinx -b html` build: succeeded, page
renders, internal refs resolve.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-16 00:08:12 +00:00
|
|
|
|
|
|
|
|
Gotchas
|
|
|
|
|
-------
|
|
|
|
|
|
|
|
|
|
.. _Silent crosstalk:
|
|
|
|
|
|
|
|
|
|
**Silent crosstalk.** Unlike every other backend, a duplicate
|
|
|
|
|
bind does *not* raise ``EADDRINUSE``. TIPC accepts multiple
|
|
|
|
|
publishers of one name and **round-robins** connects between
|
|
|
|
|
them — verified: six dials alternated strictly between two
|
|
|
|
|
listeners. So an instance collision splits traffic silently
|
|
|
|
|
instead of erroring. That's why ``TIPCAddress.get_random()``
|
|
|
|
|
derives its instance from a ``blake2b`` digest of the actor
|
|
|
|
|
identity rather than a counter, and why two ``tractor`` trees
|
|
|
|
|
sharing both a cluster **and** an ``_stype`` share a namespace —
|
|
|
|
|
partition them with a distinct ``_stype``.
|
|
|
|
|
|
|
|
|
|
**Graceful close looks like a reset.** A peer closing cleanly
|
|
|
|
|
surfaces as ``BrokenResourceError``/``ECONNRESET`` rather than
|
|
|
|
|
the clean 0-byte EOF TCP and UDS give you. Benign — the
|
|
|
|
|
transport layer already classifies it as a normal disconnect —
|
|
|
|
|
but it does look alarming in ``transport``-level logs.
|
|
|
|
|
|
|
|
|
|
**Dialing an unpublished name** answers ``EHOSTUNREACH``
|
|
|
|
|
*instantly*, with no SYN-timeout wait. That's markedly better
|
|
|
|
|
discovery-ping behaviour than TCP; ``tractor`` normalizes it to
|
|
|
|
|
``ConnectionError`` so the usual lookup paths work unchanged.
|
|
|
|
|
|
|
|
|
|
**Multiaddrs are interim.** There's no registered ``/tipc``
|
|
|
|
|
protocol in the multiaddr table yet, so the grammar is
|
|
|
|
|
``str``-only:
|
|
|
|
|
|
|
|
|
|
.. code:: text
|
|
|
|
|
|
|
|
|
|
/tipc/<stype>/<instance>/<scope>
|
|
|
|
|
|
Document TIPC-over-`wg`, add a handoff doc
Anticipating gh #502 — TIPC over a WireGuard mesh as our go-to
multihost tpt deployment — plus a cold-start handoff for whoever
(or whatever) picks this up next.
The wg deats, both verified locally,
- a wg iface is L3/`tun` (`POINTOPOINT,NOARP`, `link/none`, no
L2 addr) so TIPC's `eth` media **cannot** bind it; the udp
bearer is *mandatory* over wg, not merely an alternative.
Also its ~1420 MTU sits under ethernet's 1500.
- the composed deployment maddr is
`/ip4/<pub>/udp/51820/wg/u<key>/tipc/<stype>/<inst>/<scope>`.
XXX note the tipc segment has NO locative part unlike tcp's
inner `/ip4/../tcp/..` — a service name is
location-independent, so wg carries routing and tipc carries
identity. That's the argument for one `/tipc` proto w/ a
structured value in the #498 spec proposal.
XXX ALSO correcting a premise: TIPC is **not** unencrypted. It
ships AES-GCM crypto (`tipc node set key`, linux 5.9+) w/
cluster/master/per-node keys + rekeying. Those keys are
symmetric+pre-shared tho, so wg is still preferred for
public-key identity, NAT traversal, and one overlay every tpt
can share.
`01_tipc_HANDOFF.md` is deliberately provider-neutral: env
setup, the hard-won kernel facts table, the two closed design
decisions (+why), what landed, the pre-land TODOs and the repo's
working conventions.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-17 21:38:05 +00:00
|
|
|
Composed with a wg bearer — the form that actually matters for
|
|
|
|
|
multihost — that becomes:
|
|
|
|
|
|
|
|
|
|
.. code:: text
|
|
|
|
|
|
|
|
|
|
/ip4/<pub>/udp/51820/wg/u<key>/tipc/<stype>/<inst>/<scope>
|
|
|
|
|
|
|
|
|
|
Note the tipc segment carries **no** locative component, unlike
|
|
|
|
|
the ``/ip4/../tcp/..`` inner segment of the equivalent tcp maddr
|
|
|
|
|
— a TIPC service name is location-independent by design, so the
|
|
|
|
|
wg segments carry all the routing and the tipc segment is pure
|
|
|
|
|
identity.
|
|
|
|
|
|
Add a `tipc` guide page, roster it in the docs
Plan 01 §8's docs deliverable: `docs/guide/tipc.rst`, leading
w/ the `tipc nametable show` demo as the plan asked.
Frames the backend by what makes it different — every other tpt
gives you a pipe and leaves discovery to the registrar, whereas
TIPC's service names live in a kernel-maintained cluster-wide
name table, so a `.bind()` IS registration and a `.connect()` IS
the lookup. Then: push-based discovery via
`open_topology_events()`, scope-as-`.bindspace`, bearer setup
for spanning hosts, and the gotchas.
Also,
- roster it in `guide/index.rst` (prose list + toctree)
- `api/ipc.rst`'s transport line said `['tcp' | 'uds']` and
described only 2 unwrapped-addr shapes; now mentions `tipc`
and its proto-keyed `('tipc', stype, instance, scope)`.
Verified w/ a full `sphinx -b html` build: succeeded, page
renders, internal refs resolve.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-16 00:08:12 +00:00
|
|
|
Running the suite over TIPC
|
|
|
|
|
---------------------------
|
|
|
|
|
|
|
|
|
|
The backend is a first-class suite mode — the *entire* existing
|
|
|
|
|
test suite runs over it unmodified, which is the acceptance bar
|
|
|
|
|
for any ``tractor`` transport:
|
|
|
|
|
|
|
|
|
|
.. code:: bash
|
|
|
|
|
|
|
|
|
|
sudo modprobe tipc
|
|
|
|
|
pytest --tpt-proto tipc
|
|
|
|
|
|
|
|
|
|
Without the module that fails loudly and immediately with an
|
|
|
|
|
actionable message rather than a few hundred confusing connect
|
|
|
|
|
timeouts. Backend-specific unit tests live in
|
|
|
|
|
``tests/ipc/test_tipc.py`` and self-skip when the module is
|
|
|
|
|
absent.
|
|
|
|
|
|
|
|
|
|
Normative references
|
|
|
|
|
--------------------
|
|
|
|
|
|
|
|
|
|
The tipc.io documentation is stale in places. Treat the kernel
|
|
|
|
|
sources as the only authority:
|
|
|
|
|
|
|
|
|
|
- ``include/uapi/linux/tipc.h`` — address flavours, sockopts,
|
|
|
|
|
the topology ``struct``\s
|
|
|
|
|
- ``net/tipc/socket.c``, ``net/tipc/topsrv.c``
|
|
|
|
|
- ``man 8 tipc``
|