- Rust 62.4%
- Python 32%
- Dockerfile 5.6%
Remove the 'Why a relay' and 'Is RFC 1086 relevant?' sections, reword the intro to describe how libxotpad's Svc/XotLink are used (without framing it as the 'hard part'), and document the Docker Compose confidence tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| src | ||
| testbed | ||
| tests | ||
| tools | ||
| .dockerignore | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
xotrelay
A small relay between RFC 1006 (ISO TP0 over TCP, what ISODE speaks) and XOT (X.25 over TCP, RFC 1613). It lets an RFC-1006-only OSI stack reach X.25 peers over an XOT network without a kernel X.25 stack and without modifying the OSI stack.
The X.25 packet-layer state machine and the XOT (RFC 1613) framing come from
libxotpad -- the reusable crate
inside the xotpad project (the PAD application itself is not used). The relay
drives libxotpad's Svc (an X.25 switched virtual circuit over an XotLink)
on the XOT side and adds the RFC 1006 (TPKT) framing on the other, shuffling
TP0 packets between the two.
How it works
TP0 is identical on both sides; only the network-service framing differs:
RFC 1006 : one TPDU per TPKT envelope on a TCP stream
TP0/X.25 : one TPDU per X.25 complete-packet-sequence (NSDU)
So per connection the relay does just two things:
TPKT in --> strip 4-byte TPKT header --> Svc::send(tpdu) (RFC1006 -> X.25)
Svc::recv() --> prepend 4-byte TPKT header --> TPKT out (X.25 -> RFC1006)
libxotpad handles X.25 segmentation/reassembly (M-bit), windowing, and the call/clear lifecycle.
Build
cargo build --release
Run
Outbound -- ISODE initiates, relay places the X.25 call over XOT:
xotrelay outbound \
--listen 0.0.0.0:10006 \
--xot-gateway xot-router.example:1998 \
--called 234219200300 \
--cud 03 # optional: X.25 call user data (hex)
Point the peer's RFC 1006 presentation address at host:10006.
Inbound -- an XOT peer calls in, relay connects to ISODE over RFC 1006:
xotrelay inbound \
--listen 0.0.0.0:1998 \
--isode 127.0.0.1:17003
Using it with ISODE
No ISODE source changes or recompile are needed -- ISODE already speaks RFC 1006 (it is the only transport compiled into the Linux build, listening on ITOT port 17003 in the Quipu image). You integrate the relay purely through ISODE's presentation-address configuration.
ISODE writes a presentation address as [selectors]/netaddr|netaddr|..., where
each netaddr is one transport stack. The RFC 1006 forms ISODE understands are
(see docker/run.sh and quipu/quiputailor for real examples):
Internet=<host>+<port> # short form
TELEX+00728722+RFC-1006+03+<host>+<port> # NSAP form (as used in the EDB template)
Outbound: ISODE calls an X.25/XOT peer
Run an outbound relay, one instance per remote peer (the called DTE and XOT gateway are fixed per instance):
xotrelay outbound --listen 0.0.0.0:10006 \
--xot-gateway xot-router.example:1998 --called 234219200300 --cud 03
Then, in the peer's directory entry / knowledge config, set its
presentationAddress so the RFC 1006 network address points at the relay
instead of the peer's real (X.25) address -- keeping the peer's real
selectors, because the T-selector and AE title travel inside the TP0 CR and
flow end-to-end through the relay:
'0101'H/Internet=relay-host+10006
For example, a Quipu superior/subordinate reference in quiputailor that today
reads like the X.25 line in the bundled sample:
parent "cn=Eel" '0101'H/TELEX+00728722+X.25(80)+06+20434240001003+PID+03018100
becomes, routed through the relay:
parent "cn=Eel" '0101'H/Internet=relay-host+10006
ISODE now opens RFC 1006 to relay-host:10006; the relay places the X.25 call
(DTE 234219200300, CUD 03) over XOT and shuffles TP0 both ways.
Inbound: an X.25/XOT peer calls ISODE
Run an inbound relay pointed at ISODE's RFC 1006 listener (e.g. the Quipu DSA's 17003):
xotrelay inbound --listen 0.0.0.0:1998 --isode 127.0.0.1:17003
ISODE needs no change: it keeps listening for RFC 1006 and keeps its own
presentationAddress (the TELEX+...+RFC-1006+03+...+17003 line in the EDB).
The remote X.25 peer is configured to call the relay's XOT endpoint; the relay
accepts the X.25 call and connects RFC 1006 to ISODE. The called/calling
selectors the peer sends arrive at ISODE unchanged.
Notes
--called/--cudmust match what the X.25 peer expects. For OSI transport over X.25 the call user data conventionally begins with a protocol id (often03); match your peer rather than assuming.- Run the relay on the same host as ISODE (or on the same trusted network): the RFC 1006 leg is unauthenticated cleartext, exactly like ITOT itself.
- The relay is point-to-point per remote peer. To reach several X.25 peers,
run several outbound instances on different ports and give each ISODE peer
entry the matching
Internet=relay-host+<port>address.
Confidence tests
cargo test
src/tpkt.rsunit tests -- TPKT framing.tests/loopback.rs-- a full end-to-end test: an RFC 1006 client -> relay (outbound) -> real XOT framing + real X.25 SVC call setup/data/clear via libxotpad -> an in-process XOT echo peer, and back. Payloads up to 4096 octets exercise X.25 M-bit segmentation. No external dependencies.
Docker Compose testbed
testbed/ wires two relay instances back to back and drives a real ISO TP0
handshake the whole way through:
client ──RFC1006──> relay-outbound ──XOT──> relay-inbound ──RFC1006──> server
cd testbed
# independent Python COTP stack (real CR/CC/DT/DR; TSDUs up to 20 KB)
docker compose up --build --abort-on-container-exit --exit-code-from cotp-client
# real industrial stack (Siemens S7 via snap7 + python-snap7)
docker compose -f docker-compose.snap7.yml up --build \
--abort-on-container-exit --exit-code-from snap7-client
Exit code 0 means every round-trip was verified. The relay services are
identical in both files -- only the end peers differ. See
testbed/README.md.
External probe
tools/rfc1006_probe.py is a dependency-free RFC 1006 (TPKT) round-trip probe.
Run the relay's loopback peer (or any far-side echo), point the relay at it,
and:
python3 tools/rfc1006_probe.py 127.0.0.1 10006 0e0e0000000100
See the comments in that script for the difference between a byte-transparency test (what the relay's job actually is) and a full TP0/COTP handshake test (which needs a real OSI responder on the far side).