Linux X.25 and XOT

I have built a small collection of hardware and software for exploring X.25 networking. It contains routers, interfaces and bridges from the 1990s through to the early 2020s.

Welcome to my Lab has an introduction to the various devices discussed below.

The Problem

Can we connect a modern Linux machine to an X.25 network? Does the x25 module still work with modern kernels?

Goals

  1. Build and install the X.25 kernel module.
  2. Build and install XOT software.
  3. Bring up a TUN interface.
  4. Use xotd to bring up a TUN interface.
  5. Serve a PAD connection from the Linux machine.
  6. Connect to the PAD server from a router.

Build and install the X.25 kernel module

Prepare the build environment

If you’re interested in X.25, there’s a good chance you’re running RHEL. I’ll use CentOS as an equivalent. If you run Debian, you already know how to install build-essential and other packages.

I have a fresh CentOS Stream 10 install, so there are a few packages to install.

First, I install the equivalent of Debian’s “Build Essential”, which on CentOS includes packages we will need such as make, gcc, git, kernel-headers, kernel-devel and others:

yum update
yum groupinstall "Development Tools"

We also need dkms to manage building the x25 module whenever the kernel is updated. This is part of the Extra Packages for Enterprise Linux repository.

dnf install epel-release
dnf install dkms

At this point we need to reboot to use our new kernel that yum update installed, so that our kernel headers match the running kernel:

shutdown -r now

After the reboot, we’re on a recent kernel:

uname -a
Linux x25build 6.12.0-218.el10.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Mar 31 12:20:48 UTC 2026 x86_64 GNU/Linux

Create the missing kernel headers

I’m not sure if this is still needed, but in the past I’ve had trouble with asm/orc_hash.h being missing. This section creates that file.

cd /usr/src/kernels/$(uname -r)
make modules_prepare

This gets stuck in a recursive loop that looks like this:

Makefile:1201: warning: ignoring old recipe for target 'built-in.a'
scripts/Makefile.build:405: warning: overriding recipe for target 'modules.order'
Makefile:1921: warning: ignoring old recipe for target 'modules.order'
scripts/Makefile.build:395: warning: overriding recipe for target 'built-in.a'
Makefile:1201: warning: ignoring old recipe for target 'built-in.a'
scripts/Makefile.build:405: warning: overriding recipe for target 'modules.order'
Makefile:1921: warning: ignoring old recipe for target 'modules.order'

If we break the loop we find that asm/orc_hash.h has been created, so we can proceed.

Create the x25 DKMS config

DKMS will rebuild the module for us whenever the kernel is updated. This process is based on the CentOS Building Kernel Modules page.

Create a DKMS config:

mkdir /usr/src/x25-$(uname -r)
cd /usr/src/x25-$(uname -r)
cat > dkms.conf << EOF
PACKAGE_NAME="x25"
PACKAGE_VERSION="6.12.0-218.el10.x86_64"
BUILT_MODULE_NAME[0]="x25"
DEST_MODULE_LOCATION[0]="/kernel/net/x25/"
AUTOINSTALL="yes"
EOF

Populate the DKMS module directory (on Debian you would use apt source linux instead of git to fetch the kernel source):

cd /tmp
git clone --branch kernel-6.12.0-218.el10 --single-branch --depth 1 https://gitlab.com/redhat/centos-stream/src/kernel/centos-stream-10/
cp centos-stream-10/net/x25/* /usr/src/x25-$(uname -r)
vi /usr/src/x25-$(uname -r)/Makefile

Add CONFIG_X25 = m to the top of /usr/src/x25-$(uname -r)/Makefile

Build and install the module

Ask DKMS to build and maintain the module:

dkms add -m x25 -v $(uname -r)
dkms build -m x25 -v $(uname -r)
dkms install -m x25 -v $(uname -r)

At this point, we have an x25 module available:

modinfo x25
filename:       /lib/modules/6.12.0-218.el10.x86_64/extra/x25.ko.xz
alias:          net-pf-9
license:        GPL
description:    The X.25 Packet Layer network layer protocol
author:         Jonathan Naylor <...>
rhelversion:    10.3
srcversion:     2616120E4F5357156C365AA
depends:        
name:           x25
retpoline:      Y
vermagic:       6.12.0-218.el10.x86_64 SMP preempt mod_unload modversions 

The module should load cleanly:

modprobe x25 

We don’t need to load the module by hand each time we use it, this just shows that it loads cleanly.

modprobe: ERROR: could not insert ‘x25’: Exec format error

If you get this error from modprobe:

modprobe x25
modprobe: ERROR: could not insert 'x25': Exec format error

Then you should reinstall kernel-headers and dkms:

yum reinstall kernel-devel kernel-headers
yum reinstall dkms

Then rebuild the module:

dkms remove -m x25 -v $( uname -r ) --all
dkms install -m x25 -v $( uname -r )

Build and install XOT software

JH-XOTD listens for incoming XOT connections and bridges them to a TUN interface. This gives us an easy way to get X.25 traffic onto the TUN.

cd ~/src
git clone --single-branch --depth 1 https://github.com/BAN-AI-X25/jh-xotd
cd jh-xotd
make
sudo cp xotd /usr/local/sbin/
cd ~/src
git clone --single-branch --depth 1 https://github.com/BAN-AI-X25/pad_svr
cd pad_svr

vi open_x25.c
# Add includes for <strings.h>
# Modify line 94 of open_x25.c to match the name of your tun device
# e.g. strcpy(subscription.device, "tun0");

vi pad_svr3.c
# Add includes for <sys/stat.h>, <strings.h> and "do_utmp.h"

vi do_copy.c
# Add includes for <string.h> and "do_utmp.h"

vi ptypair.c
# Add includes for <stdlib.h>, <stdio.h> and "all.h"

vi sighandler.c
# Add includes for <strings.h>

vi loging.c
# Add includes for <unistd.h>

vi watchdog.c
# Add includes for <strings.h>

vi cfgfile.c
# Add includes for <ctype.h>

vi do_utmp.c
# Add includes for <ctype.h>

CFLAGS=-D_XOPEN_SOURCE=500 make
sudo cp pad_svr /usr/local/sbin

Bring up a TUN interface

According to the README, jh-xotd requires a config file and a setup script.

Create the config file, providing the IP address that you want to accept connections from.

cat > /usr/local/etc/jh-xotd.conf << EOF
tun0 192.0.2.250 /usr/local/sbin/xotd-setup 256
EOF

And create the setup script:

cat > /usr/local/sbin/xotd-setup << EOF
#!/bin/sh -e
echo "Setting up $1" > /dev/console
ip link set "$1" up
route --x25 add 0/0 "$1"
echo "$1" > /var/run/xotd-tun
EOF
chmod +x /usr/local/sbin/xotd-setup

(Double check that the $1 all made it into /usr/local/bin/xotd-setup).

Use xotd to bring up a TUN interface

We can now run xotd:

/usr/local/sbin/xotd -v -f /usr/local/etc/jh-xotd.conf 

We should be able to see the interface in ip link:

ip link
...
3: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UNKNOWN mode DEFAULT group default qlen 500
    link/x25 

And we should also see the daemon listening:

netstat -anp | fgrep 1998
tcp        0      0 0.0.0.0:1998            0.0.0.0:*               LISTEN      117274/xotd    

Serve a PAD connection from the Linux machine

Now that we have a TUN interface capable of handling X.25 traffic, and we have xotd accepting connections and routing them to the TUN, we need something on the TUN to handle connections. We will use pad_svr to accept connections.

Create a config file:

mkdir /etc/pad_svr
cat > /etc/pad_svr/pad_svr.cfg << EOF
local_x121_address 999999
log_level 5
log_path /var/log/pad_svr.log
Watchdog_timeout 10
Winsize_in 2
Pacsize_in 128
Forward_idle_timeout 1
x29_profile 1:0,2:0,3:0,4:1,13:4,15:0 Buffer_size 32000
EOF

Run pad_svr:

/usr/local/sbin/pad_svr
Name of cfg file: /etc/pad_svr/pad_svr.cfg
Open log file /var/log/pad_svr.log

Check /var/log/pad_svr.log. It should end with:

04/07 14:13:46 2138906 Server pad_svr starting...
04/07 14:13:46 2138907 Monitor is watching to pad_svr (2138908)
04/07 14:13:46 2138908 Creating an x25 socket
04/07 14:13:46 2138908 Setting up x25 subscriptions
04/07 14:13:46 2138908 Setting facilities
04/07 14:13:46 2138908 Setting call user data 
04/07 14:13:46 2138908 Listineing for calls on 999999
04/07 14:13:46 2138908 Accepting for calls

If you see this, you need to modify line 94 of open_x25.c to match the name of your tun device and restart (e.g. strcpy(subscription.device, "tun0");):

04/07 13:57:10 653971 Creating an x25 socket
04/07 13:57:10 653971 Setting up x25 subscriptions
04/07 13:57:10 653971 Set x25 subscription: Invalid argument
04/07 13:57:10 117416 Monitor: pad_svr (653971) will restart after error.
04/07 13:57:10 117416 Monitor is watching to pad_svr (653972)

Note that you can also stop pad_svr with the same command:

/usr/local/sbin/pad_svr stop
Sending SIGTERM to group pid 720824

Examine state in /proc/net/x25

We currently have one X.25 route, which directs X.25 traffic to tun0:

cat /proc/net/x25/route 
Address          Digits  Device
000000000000000  0       tun0 

We have one process listening for X.25 connections, on address *:

cat /proc/net/x25/socket 
dest_addr  src_addr   dev   lci st vs vr va   t  t2 t21 t22 t23 Snd-Q Rcv-Q inode
*          999999     ???   000  0  0  0  0   0   3 200 180 180     0     0 11408

Connect to the PAD server from a router

On our router, we need to configure a route to the address that we put into /etc/pad_svr/pad_svr.cfg earlier:

c1921(config)#x25 route 999999 xot 192.0.2.100

We can then use the Cisco pad command to connect to our Linux machine (here I have connected to a Debian machine, since forwarding 1998 to my CentOS VM was too much work):

c1921#pad 999999
Trying 999999...Open
Debian GNU/Linux 13
login: 
Password: 

> uname -a
Linux debian 6.12.74+deb13+1-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.74-2 (2026-03-08) x86_64 GNU/Linux
> exit
logout

[Connection to 999999 closed by foreign host]
c1921#

With debug x25 xot event and debug x25 event enabled, that session looked like this in the logs:

Apr  7 05:56:26.165: [192.0.2.100,1998/192.0.2.250,24526]: XOT O P2 Call (21) 8 lci 1
Apr  7 05:56:26.165:   From (6): 701001 To (6): 999999
Apr  7 05:56:26.165:   Facilities: (6)
Apr  7 05:56:26.165:     Packet sizes: 2048 2048
Apr  7 05:56:26.165:     Window sizes: 7 7
Apr  7 05:56:26.165:   Call User Data (4): 0x01000000 (pad)
Apr  7 05:56:26.365: [192.0.2.100,1998/192.0.2.250,24526]: XOT I P2 Call Confirm (11) 8 lci 1
Apr  7 05:56:26.365:   From (0):  To (0): 
Apr  7 05:56:26.365:   Facilities: (6)
Apr  7 05:56:26.365:     Packet sizes: 128 128
Apr  7 05:56:26.365:     Window sizes: 2 2

Apr  7 05:56:44.545: [192.0.2.100,1998/192.0.2.250,24526]: XOT O P4 Clear (5) 8 lci 1
Apr  7 05:56:44.545:   Cause 0, Diag 0 (DTE originated/No additional information)
Apr  7 05:56:44.545: <detached>: XOT I P6 Clear Confirm (3) 8 lci 1

If a connection comes from an unexpected IP address (one not listed in /usr/local/etc/jh-xotd.conf), then /var/log/messages will log it:

Apr  7 14:17:03 x25build xotd[117274]: call from unknown address 192.0.2.59

Summary

Now that we have it set up, starting up our XOTd and PAD server is as simple as:

/usr/local/sbin/xotd -v -f /usr/local/etc/jh-xotd.conf
/var/log/pad_svr.log

We have:

  1. Built and installed the X.25 kernel module.
  2. Built and installed XOT software.
  3. Brought up the TUN interface.
  4. Used xotd to bring up a TUN interface.
  5. Served a PAD connection from the Linux machine.
  6. Connected to the PAD server from a router.

We could now:

  • Work out how to use xotd to connect from the TUN to XOT (this looks possible in the source code).