RFC 1006 (ISO TP0 over TCP) <-> XOT (X.25 over TCP, RFC 1613) relay
  • Rust 62.4%
  • Python 32%
  • Dockerfile 5.6%
Find a file
Hans Hübner fcbdad33ce docs: trim README, note libxotpad usage and Docker testbeds
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>
2026-06-08 16:17:08 +02:00
src Initial commit: RFC 1006 <-> XOT relay 2026-06-08 16:02:46 +02:00
testbed Initial commit: RFC 1006 <-> XOT relay 2026-06-08 16:02:46 +02:00
tests Initial commit: RFC 1006 <-> XOT relay 2026-06-08 16:02:46 +02:00
tools Initial commit: RFC 1006 <-> XOT relay 2026-06-08 16:02:46 +02:00
.dockerignore Initial commit: RFC 1006 <-> XOT relay 2026-06-08 16:02:46 +02:00
.gitignore Initial commit: RFC 1006 <-> XOT relay 2026-06-08 16:02:46 +02:00
Cargo.lock Initial commit: RFC 1006 <-> XOT relay 2026-06-08 16:02:46 +02:00
Cargo.toml Initial commit: RFC 1006 <-> XOT relay 2026-06-08 16:02:46 +02:00
README.md docs: trim README, note libxotpad usage and Docker testbeds 2026-06-08 16:17:08 +02:00

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 / --cud must match what the X.25 peer expects. For OSI transport over X.25 the call user data conventionally begins with a protocol id (often 03); 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.rs unit 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).