Add the `tipc_cluster` example set
Plan 01 §8's deployment deliverable, under
`examples/multihost/` (like the `wg_lan` set) since these need
the `tipc` kernel module — and, for the 2-host pair, a live
bearer — so they can't satisfy `test_docs_examples.py`'s
"walk `examples/` and assert rc == 0". `'multihost'` is already
in that test's exclusion list.
- `single_host.py` — boots a 4-actor tree and shells out to
`tipc nametable show` before/during/after. Watching 4 service
names appear in the KERNEL's table and vanish on teardown,
entirely outside any `tractor` API, is the single best demo
this backend has.
- `watch_nametable.py` — the same story push-based, via
`open_topology_events()`: live `[+] published` / `[-]
withdrawn` as actors come and go.
- `host_a_srv.py` + `host_b_client.py` — the cross-node pair.
Note what's absent from both: any IP, hostname or port. Both
sides name the same *service* and the kernel routes it.
- `README.md` — the manual smoke test (bearer setup, `tipc link
list` verify) per §7.3, plus the gotchas: silent crosstalk,
graceful-close-looks-like-`ECONNRESET`, the interim maddr.
Both single-host scripts were RUN against a live kernel and
their real output is what's pasted in the README.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-16 00:07:24 +00:00
|
|
|
# `tractor` over `AF_TIPC`, where the address *is* the service name
|
|
|
|
|
|
|
|
|
|
TIPC is a linux-kernel cluster IPC protocol whose service names
|
|
|
|
|
live in a **cluster-wide name table maintained by the kernel**.
|
|
|
|
|
For `tractor` that means:
|
|
|
|
|
|
|
|
|
|
- an actor's IPC address is a service name `(stype, instance)`,
|
|
|
|
|
not a host/port,
|
|
|
|
|
- `.bind()`ing it **is** service registration,
|
|
|
|
|
- a peer's `.connect()`-by-name **is** the lookup.
|
|
|
|
|
|
|
|
|
|
So the discovery machinery `tractor.discovery` normally
|
|
|
|
|
implements with a registrar actor comes for free, in-kernel —
|
|
|
|
|
which is the ask in gh
|
|
|
|
|
[#378](https://github.com/goodboy/tractor/issues/378).
|
|
|
|
|
|
|
|
|
|
> **Why `examples/multihost/`?** `tests/test_docs_examples.py`
|
|
|
|
|
> walks `examples/` recursively and runs everything it collects
|
|
|
|
|
> as a subproc, asserting `rc == 0`. These need the `tipc`
|
|
|
|
|
> kernel module (and, for the two-host pair, a live bearer), so
|
|
|
|
|
> they can't satisfy that; `'multihost' not in p[0]` is already
|
|
|
|
|
> in the test's exclusion list, which is what keeps them out of
|
|
|
|
|
> CI. See "CI" below for the separate matrix-entry plan.
|
|
|
|
|
|
|
|
|
|
## the single best demo
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo modprobe tipc
|
|
|
|
|
python single_host.py
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Four actors boot, four service names appear in the kernel's
|
|
|
|
|
table, and all four are withdrawn on teardown — observed with
|
|
|
|
|
`tipc(8)`, entirely outside `tractor`:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
--- `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
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`1953628160` is `0x74720000` — `tractor`'s reserved service
|
|
|
|
|
type, ascii `tr` in the high half. `1616` is the host-singleton
|
|
|
|
|
registrar, the same idiom as the TCP port and the
|
|
|
|
|
`registry@1616.sock` UDS filename. The other three instances are
|
|
|
|
|
per-actor digests (see "silent crosstalk" below).
|
|
|
|
|
|
|
|
|
|
## push-based discovery
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python watch_nametable.py
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Subscribes to the kernel's *topology service* and prints name
|
|
|
|
|
table transitions as they happen — no polling, no registrar
|
|
|
|
|
round-trip:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
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
|
|
|
|
|
[+] published instance=3484369663 port=0x00000000:2126817956
|
|
|
|
|
tearing down..
|
|
|
|
|
[-] withdrawn instance=186947472 port=0x00000000:3960753074
|
|
|
|
|
...
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
This is the groundwork for a push registry in
|
|
|
|
|
`tractor.discovery._registry` (gh
|
|
|
|
|
[#184](https://github.com/goodboy/tractor/issues/184),
|
|
|
|
|
[#216](https://github.com/goodboy/tractor/issues/216)) — a
|
|
|
|
|
registrar that *never polls* `find_actor()`.
|
|
|
|
|
|
|
|
|
|
## two hosts
|
|
|
|
|
|
|
|
|
|
Everything above is single-node (`modprobe` is enough). To span
|
|
|
|
|
hosts you need a **bearer** on both, which is the one thing that
|
|
|
|
|
can't be CI'd.
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
# on BOTH hosts
|
|
|
|
|
sudo modprobe tipc
|
|
|
|
|
|
|
|
|
|
# over ethernet (L2) — simplest when the hosts share a segment
|
|
|
|
|
sudo tipc bearer enable media eth device eth0
|
|
|
|
|
|
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
|
|
|
# ..or over UDP when L2 isn't available — and MANDATORY over a
|
|
|
|
|
# `wg` mesh, see below
|
Add the `tipc_cluster` example set
Plan 01 §8's deployment deliverable, under
`examples/multihost/` (like the `wg_lan` set) since these need
the `tipc` kernel module — and, for the 2-host pair, a live
bearer — so they can't satisfy `test_docs_examples.py`'s
"walk `examples/` and assert rc == 0". `'multihost'` is already
in that test's exclusion list.
- `single_host.py` — boots a 4-actor tree and shells out to
`tipc nametable show` before/during/after. Watching 4 service
names appear in the KERNEL's table and vanish on teardown,
entirely outside any `tractor` API, is the single best demo
this backend has.
- `watch_nametable.py` — the same story push-based, via
`open_topology_events()`: live `[+] published` / `[-]
withdrawn` as actors come and go.
- `host_a_srv.py` + `host_b_client.py` — the cross-node pair.
Note what's absent from both: any IP, hostname or port. Both
sides name the same *service* and the kernel routes it.
- `README.md` — the manual smoke test (bearer setup, `tipc link
list` verify) per §7.3, plus the gotchas: silent crosstalk,
graceful-close-looks-like-`ECONNRESET`, the interim maddr.
Both single-host scripts were RUN against a live kernel and
their real output is what's pasted in the README.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-16 00:07:24 +00:00
|
|
|
sudo tipc bearer enable media udp name uc localip 10.0.11.1
|
|
|
|
|
|
|
|
|
|
# verify BEFORE running anything: this must list the peer
|
|
|
|
|
tipc link list
|
|
|
|
|
tipc node list
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Then:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
# host A
|
|
|
|
|
python host_a_srv.py
|
|
|
|
|
|
|
|
|
|
# host B
|
|
|
|
|
python host_b_client.py
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Note what's absent from both scripts: any IP, hostname or port.
|
|
|
|
|
Both sides name the *same service*, and the kernel routes it.
|
|
|
|
|
Move `host_a_srv.py` to a third node and host B's dial keeps
|
|
|
|
|
working, unchanged.
|
|
|
|
|
|
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 `wg` mesh
|
|
|
|
|
|
|
|
|
|
TIPC over WireGuard is the intended reference multihost
|
|
|
|
|
deployment (gh #502). One hard constraint: a wg interface is
|
|
|
|
|
L3/`tun` — `POINTOPOINT,NOARP`, `link/none`, no L2 address — so
|
|
|
|
|
TIPC's `eth` media **cannot** bind it. The udp bearer is
|
|
|
|
|
mandatory there, bound to the wg overlay IP, and wg's typical
|
|
|
|
|
1420 MTU sits under ethernet's 1500 so link MTU wants checking.
|
|
|
|
|
|
|
|
|
|
Composed, the deployment address is:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
/ip4/<pub>/udp/51820/wg/u<key>/tipc/<stype>/<inst>/<scope>
|
|
|
|
|
\____ wg bearer ________/\_key_/\______ tractor ep ________/
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Note the tipc segment has no locative part, unlike tcp's inner
|
|
|
|
|
`/ip4/../tcp/..` — a service name is location-independent, so wg
|
|
|
|
|
carries the routing and tipc carries identity.
|
|
|
|
|
|
2026-08-17 21:44:58 +00:00
|
|
|
**wg here is not about confidentiality.** TIPC ships AES-GCM
|
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
|
|
|
crypto of its own (`tipc node set key`, linux 5.9+) with
|
2026-08-17 21:44:58 +00:00
|
|
|
cluster/master/per-node keys and rekeying, so "wg adds the
|
|
|
|
|
encryption TIPC lacks" is wrong.
|
|
|
|
|
|
|
|
|
|
The motivation is different but real: TIPC's keys are *symmetric
|
|
|
|
|
and pre-shared* — distribution, rotation and revocation are all
|
|
|
|
|
on you — whereas wg gives public-key identity and a handshake,
|
|
|
|
|
NAT traversal, and one overlay shared by every transport instead
|
|
|
|
|
of a TIPC-only mechanism. Worth benchmarking either way; native
|
|
|
|
|
crypto skips the tunnel hop.
|
|
|
|
|
|
|
|
|
|
Note too that the ethernet-bearer pairing #378 imagined does
|
|
|
|
|
**not** apply over wg: on a given link the L2 path and the wg
|
|
|
|
|
path are mutually exclusive.
|
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
|
|
|
|
Add the `tipc_cluster` example set
Plan 01 §8's deployment deliverable, under
`examples/multihost/` (like the `wg_lan` set) since these need
the `tipc` kernel module — and, for the 2-host pair, a live
bearer — so they can't satisfy `test_docs_examples.py`'s
"walk `examples/` and assert rc == 0". `'multihost'` is already
in that test's exclusion list.
- `single_host.py` — boots a 4-actor tree and shells out to
`tipc nametable show` before/during/after. Watching 4 service
names appear in the KERNEL's table and vanish on teardown,
entirely outside any `tractor` API, is the single best demo
this backend has.
- `watch_nametable.py` — the same story push-based, via
`open_topology_events()`: live `[+] published` / `[-]
withdrawn` as actors come and go.
- `host_a_srv.py` + `host_b_client.py` — the cross-node pair.
Note what's absent from both: any IP, hostname or port. Both
sides name the same *service* and the kernel routes it.
- `README.md` — the manual smoke test (bearer setup, `tipc link
list` verify) per §7.3, plus the gotchas: silent crosstalk,
graceful-close-looks-like-`ECONNRESET`, the interim maddr.
Both single-host scripts were RUN against a live kernel and
their real output is what's pasted in the README.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-16 00:07:24 +00:00
|
|
|
### scope
|
|
|
|
|
|
|
|
|
|
`TIPCAddress._scope` is the backend's `.bindspace` — literally
|
|
|
|
|
"the set of hosts this published name is reachable from":
|
|
|
|
|
|
|
|
|
|
| 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 by modern
|
|
|
|
|
kernels; `tractor` accepts it on input and folds it, logging at
|
|
|
|
|
`transport` level.
|
|
|
|
|
|
|
|
|
|
## gotchas worth knowing before you deploy
|
|
|
|
|
|
|
|
|
|
**Silent crosstalk.** Unlike every other backend, a duplicate
|
|
|
|
|
bind does **not** raise `EADDRINUSE` — TIPC happily accepts
|
|
|
|
|
multiple publishers of one name and *round-robins* connects
|
|
|
|
|
between them (verified: 6 dials alternated `b,a,b,a,b,a`). So an
|
|
|
|
|
instance collision is silent traffic-splitting, not an error.
|
|
|
|
|
That's why `TIPCAddress.get_random()` derives the instance from
|
|
|
|
|
a `blake2b` digest of the actor identity rather than a counter.
|
|
|
|
|
Two `tractor` trees sharing both a cluster **and** an `_stype`
|
|
|
|
|
share a name space; partition them by passing a distinct
|
|
|
|
|
`_stype`.
|
|
|
|
|
|
|
|
|
|
**Graceful close looks like a reset.** A peer closing cleanly
|
|
|
|
|
surfaces as `BrokenResourceError`/`ECONNRESET` rather than the
|
|
|
|
|
clean 0-byte EOF you get from TCP/UDS. It's 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* (no SYN-timeout wait), which is much better
|
|
|
|
|
discovery-ping behaviour than TCP. `tractor` normalizes it to
|
|
|
|
|
`ConnectionError`.
|
|
|
|
|
|
|
|
|
|
**It's opt-in, never a default.** The module isn't loaded on
|
|
|
|
|
most boxes and doesn't exist off-linux, so
|
|
|
|
|
`enable_transports=['tipc']` is always explicit. Check
|
|
|
|
|
`tractor.ipc._tipc.is_tipc_available()` before assuming.
|
|
|
|
|
|
|
|
|
|
## maddr form
|
|
|
|
|
|
|
|
|
|
There is no registered `/tipc` protocol in the multiaddr table
|
|
|
|
|
yet (upstream track: gh
|
|
|
|
|
[#483](https://github.com/goodboy/tractor/issues/483) +
|
|
|
|
|
multiformats/py-multiaddr#107), so the grammar is interim and
|
|
|
|
|
`str`-only:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
/tipc/<stype>/<instance>/<scope>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`parse_maddr()` special-cases this prefix *before* handing
|
|
|
|
|
anything to `Multiaddr()`, which would otherwise reject the
|
|
|
|
|
unregistered name outright. Registering it upstream is what
|
|
|
|
|
would unblock gh
|
|
|
|
|
[#443](https://github.com/goodboy/tractor/issues/443)'s
|
|
|
|
|
"return `Multiaddr` everywhere" item.
|
|
|
|
|
|
|
|
|
|
## running the suite over TIPC
|
|
|
|
|
|
|
|
|
|
The whole test suite runs under the backend:
|
|
|
|
|
|
|
|
|
|
```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 connect timeouts.
|
|
|
|
|
Backend-specific unit tests live in `tests/ipc/test_tipc.py` and
|
|
|
|
|
self-skip when the module is absent.
|
|
|
|
|
|
|
|
|
|
## CI
|
|
|
|
|
|
|
|
|
|
Single-host TIPC *is* CI-able — the module ships with the
|
|
|
|
|
standard Ubuntu kernel package, so a `sudo modprobe tipc` step
|
|
|
|
|
plus a `--tpt-proto tipc` matrix entry should work. That's not
|
|
|
|
|
wired up yet; verify in a throwaway workflow first, and fall
|
|
|
|
|
back to a container job with `--cap-add NET_ADMIN` if the
|
|
|
|
|
runners refuse. Cross-node (bearer) testing stays manual — this
|
|
|
|
|
README is that smoke test.
|
|
|
|
|
|
|
|
|
|
## normative refs
|
|
|
|
|
|
|
|
|
|
The tipc.io docs are stale in places (gh #378 says as much).
|
|
|
|
|
Treat the kernel sources as the only normative reference:
|
|
|
|
|
|
|
|
|
|
- `include/uapi/linux/tipc.h` — address flavours, sockopts, the
|
|
|
|
|
topology `struct`s
|
|
|
|
|
- `net/tipc/socket.c`, `net/tipc/topsrv.c`
|
|
|
|
|
- `man 8 tipc`
|