Linux X.25 Programmers Guide
Interfacing with Linux X.25 and TUN Interfaces
This technical document describes interfacing with the Linux kernel’s X.25 implementations via either the AF_X25 socket family or the TUN network devices. Most projects will only need to select and implement one of these two methods:
- AF_X25 sockets: Present a familiar socket interface to get up and running quickly without delving into X.25 packets and handshaking. Imposes limitations (e.g. on available X.25 Facilities).
- TUN: Provides for raw packet creation and handling, but requires the user space code to carefully manage connection state and packet encoding.
The Linux X.25 Stack
The Linux kernel supports X.25 over various link layers, including LAPB (standard serial) and TUN (virtual encapsulation).
The AF_X25 socket family that implements the X.25 Packet Layer Protocol (PLP) is the simpler option. The alternative TUN interface requires that the application implement X.25 packet and protocol handling.
AF_X25 Sockets
Standard POSIX socket calls are used:
- Socket Creation:
socket(AF_X25, SOCK_SEQPACKET, 0). This is the only supported socket type for AF_X25. The protocol argument must be 0. - Addressing: Uses
struct sockaddr_x25.
Open a Connection
This describes the steps for a DTE application opening an outbound X.25 SVC via an AF_X25 socket.
- Create the socket:
sockfd = socket(AF_X25, SOCK_SEQPACKET, 0);Creates an AF_X25 socket in
X25_STATE_0/TCP_CLOSE. Initialises internal queues (ack_queue, fragment_queue, interrupt queues), default facilities (window size 2, packet size 128), and timers T21/T22/T23/T2. The socket is markedSOCK_ZAPPEDuntil bound. - Optionally configure facilities using IOCTLs (detailed later); (
SIOCX25SFACILITIES), DTE facilities (SIOCX25SDTEFACILITIES), Accept Approval (SIOCX25CALLACCPTAPPRV) and Call User Data (SIOCX25SCALLUSERDATA,SIOCX25SCUDMATCHLEN). These must be set before connect, while socket is inTCP_CLOSEorTCP_LISTEN(returnsEINVALotherwise):ioctl(sockfd, SIOCX25SFACILITIES, &fac);SIOCX25SFACILITIESsets the facilities (window size, packet size, throughput, reverse charging) to be requested in the outgoing calls or negotiated on incoming calls. Values are validated against allowed ranges (af_x25.c:1468–1494). - Bind a source X.121 address:
bind(sockfd, &src_sockaddr_x25, sizeof(src_sockaddr_x25));Registers the socket’s source X.121 address. Adds the socket to the global
x25_list(protected byx25_list_lock), clearsSOCK_ZAPPED. The address must consist only of ASCII digit characters. - Connect to the remote address (blocking):
connect(sockfd, &dst_sockaddr_x25, sizeof(dst_sockaddr_x25));Looks up the route for the destination address, acquires a neighbour, allocates a unique LCI via
x25_new_lci(), sets state toX25_STATE_1/TCP_SYN_SENT, and sends aCALL_REQUESTviax25_write_internal(). Starts T21 timer. Blocks inx25_wait_for_connection_establishment()until aCALL_ACCEPTEDorCLEAR_REQUESTis received, or T21 fires. Side effect: if the link is inX25_LINK_STATE_0, this triggers the L2 connect handshake.
If you’re interested in the facilities that were negotiated for the connection, use SIOCX25GFACILITIES to retrieve them after connect() or accept().
Send and Receive Data
In order to send data, the application should send data with send(sockfd, buf, len, MSG_EOR). MSG_EOR indicates that the current record is complete, which helps to maintain X.25 packet boundaries.
Every read() call on a SOCK_SEQPACKET socket is expected to read an entire packet. This means that calls to read should have sufficiently large buffers, otherwise received packets will be truncated.
X.25 also supports INTERRUPT packets, which can contain one byte of data under older specifications (and more or less under newer specs). To send an interrupt packet, the application should send(sockfd, buf, len, MSG_OOB).
Upon receiving an OOB INTERRUPT packet, the Linux kernel sends a SIGURG to the socket owner. The interrupt packet data can then be read with recv(sockfd, buf, len, MSG_OOB). If a particular thread is associated with a socket, that thread should be set as the owner of the socket so that it can handle SIGURG (OOB Interrupt data) and SIGPIPE (write to closed socket):
#define _GNU_SOURCE // Required for F_SETOWN_EX
#include <fcntl.h>
#include <unistd.h>
#include <sys/syscall.h>
// Within the specific thread intended to receive signals:
struct f_owner_ex owner;
owner.type = F_OWNER_TID;
owner.pid = syscall(SYS_gettid); // Get current thread's TID
fcntl(sockfd, F_SETOWN_EX, &owner);
The X.25 Q-Bit (Qualified Data) indicates that a data packet is meant for packet layer control rather than user data. If you want to send/receive the Q-Bit in data packet headers, you need to set X25_QBITINCL socket option. With this option enabled, the first byte of each send and receive buffer contains the Q-Bit flag (1 = Q-Bit set, 0 = Q-Bit clear). If X25_QBITINCL is not set, the Q-Bit on received packets is ignored.
int one = 1;
setsockopt(sockfd, SOL_X25, X25_QBITINCL, &one, sizeof(one));
Close a Connection
This describes the DTE-initiated close sequence.
-
Application calls
close(sockfd)or the gateway decides to clear. Kernelx25_release()runs. -
If socket is in
X25_STATE_3(data transfer): Kernel clears queues, sends CLEAR_REQUEST, entersX25_STATE_2(TCP_CLOSE), starts T23 timer. -
If T23 expires (180 s default) with no confirmation: kernel destroys socket unconditionally.
Handle a Remotely Closed Connection
- Application on the socket receives EOF or error from
recv()/recvmsg(), then callsclose(sockfd). - Application receives a
SIGPIPEfromsend(), which also returns an error, then callsclose(sockfd).
Note that applications can choose to signal(SIGPIPE, SIG_IGN) and handle the error code returned by read() instead of processing SIGPIPE
Standard Socket Functions
This section documents the behaviour of standard libc socket functions when used with AF_X25 sockets, sourced from the kernel implementation in net/x25/af_x25.c.
socket()
sockfd = socket(AF_X25, SOCK_SEQPACKET, 0);
SOCK_SEQPACKET is the only supported socket type; SOCK_STREAM, SOCK_DGRAM, and others return ESOCKTNOSUPPORT. The protocol argument must be 0; any other value returns EINVAL. AF_X25 does not support network namespaces: creating a socket outside init_net returns EAFNOSUPPORT.
bind()
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
Registers the socket’s local X.121 address and inserts it into the global socket list. Binding is mandatory before connect() — autobinding is not supported (af_x25.c:800). The address string must consist entirely of ASCII decimal digit characters; any non-digit character returns EINVAL. Binding the null X.25 address ("" / all spaces) is accepted and acts as a wildcard. The socket may only be bound once; a second bind() returns EINVAL.
connect()
connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));
Looks up a route for the destination address, acquires a neighbour, allocates a unique LCI, and sends a CALL_REQUEST. Blocks until CALL_ACCEPTED is received or T21 expires (default 200 s).
Returns:
| Return value | Condition |
|---|---|
0 |
Connection established |
EINPROGRESS |
O_NONBLOCK set and connection in progress |
EISCONN |
Socket is already in TCP_ESTABLISHED (no reconnect on SOCK_SEQPACKET) |
EALREADY |
Connection attempt already in progress (TCP_SYN_SENT) |
EINVAL |
Not bound, bad address length, or non-digit characters in address |
ENETUNREACH |
No route to destination |
ECONNREFUSED |
CLEAR_REQUEST received from remote |
After connect() returns EINPROGRESS, poll for EPOLLOUT (writeable = connected) or EPOLLERR/EPOLLHUP (failed). Call connect() a second time after EPOLLOUT to retrieve the final result: it returns 0 on success or a negative error code.
listen()
listen(sockfd, backlog);
Places a bound socket into the listening state (TCP_LISTEN). Must be called on a socket that is in SS_UNCONNECTED state; returns EINVAL otherwise. backlog sets sk->sk_max_ack_backlog, which caps the number of pending unaccepted calls queued by the kernel.
accept()
newfd = accept(sockfd, (struct sockaddr *)&peer_addr, &addrlen);
Dequeues one pending incoming call from the listening socket’s receive queue. Blocks until a call arrives (or sk_rcvtimeo expires, returning EAGAIN). Returns a new, already-connected socket in TCP_ESTABLISHED / X25_STATE_3. The peer_addr / addrlen arguments are not filled in by accept() itself — use getpeername() on the returned socket to obtain the caller’s X.121 address.
When SIOCX25CALLACCPTAPPRV has been set on the listening socket, incoming calls land in X25_STATE_5 and the application must call ioctl(newfd, SIOCX25SENDCALLACCPT) before data transfer begins.
getsockname() and getpeername()
getsockname(sockfd, (struct sockaddr *)&addr, &addrlen); /* local address */
getpeername(sockfd, (struct sockaddr *)&addr, &addrlen); /* remote address */
Both are served by x25_getname() (af_x25.c:916).
getsockname()returnsx25->source_addras astruct sockaddr_x25. Works in any socket state; returns an empty string (x25_addr[0] == '\0') if the socket has not been bound.getpeername()returnsx25->dest_addr. ReturnsENOTCONNunlesssk->sk_state == TCP_ESTABLISHED. On the server side,dest_addris populated with the caller’s address duringx25_rx_call_request().
Both calls return sizeof(struct sockaddr_x25) (18 bytes) on success.
send(), sendto(), sendmsg()
send(sockfd, buf, len, MSG_EOR); /* normal data */
send(sockfd, buf, len, MSG_OOB); /* interrupt packet */
Accepted flags: MSG_EOR, MSG_OOB, MSG_DONTWAIT. Any other flag combination returns EINVAL.
MSG_EOR is required for normal data sends. The flag signals that the current record is complete. Omitting it (i.e., passing flags = 0) returns EINVAL, because the kernel does not support partial records at the userspace interface.
If the payload exceeds the negotiated packet size (default 128 bytes, log₂-encoded in x25->facilities.pacsize_out), x25_output() fragments it into multiple X.25 data packets automatically, setting the M-bit on all but the last fragment. The maximum single send() length is 65535 bytes.
MSG_OOB sends an X.25 INTERRUPT packet. The payload is silently truncated to 32 bytes.
sendto() with a destination address: the destination must exactly match the already-connected x25->dest_addr; a different address returns EISCONN.
SIGPIPE is raised (and EPIPE returned) if SEND_SHUTDOWN is set on the socket — i.e., after close() begins teardown or after a remote clear.
recv(), recvfrom(), recvmsg()
recv(sockfd, buf, sizeof(buf), 0); /* normal data */
recv(sockfd, buf, sizeof(buf), MSG_OOB); /* interrupt packet */
Each call returns exactly one reassembled X.25 record (all M-bit fragments have been coalesced by the kernel before delivery). MSG_EOR is always set in msg->msg_flags.
If buf is smaller than the record, the excess is silently discarded and MSG_TRUNC is set. Unlike TCP, there is no way to read the remainder in a subsequent call — size the buffer to at least the negotiated pacsize_in (default 128 bytes).
MSG_OOB receives from the interrupt queue. Returns EINVAL if SOCK_URGINLINE is set or the interrupt queue is empty. The kernel strips the X.25 header; if X25_QBITINCL is set, a leading zero byte (Q-bit = 0) is prepended.
recvfrom() / recvmsg() with a non-NULL src_addr: fills msg_name with a struct sockaddr_x25 containing x25->dest_addr (the remote peer address) and sets msg_namelen to sizeof(struct sockaddr_x25).
Blocks until data arrives unless MSG_DONTWAIT or O_NONBLOCK is set, in which case EAGAIN is returned if no data is available.
poll(), select(), epoll()
AF_X25 uses the generic datagram_poll() (net/core/datagram.c). Events reported:
| Event | Condition |
|---|---|
EPOLLIN \| EPOLLRDNORM |
Data in sk_receive_queue, or RCV_SHUTDOWN set |
EPOLLOUT \| EPOLLWRNORM |
Send buffer has space (sock_writeable()) |
EPOLLHUP |
sk_state == TCP_CLOSE or both shutdown directions set |
EPOLLRDHUP |
RCV_SHUTDOWN set |
EPOLLERR |
sk_err non-zero, or error queue non-empty |
Important: while a non-blocking connect() is in progress (sk_state == TCP_SYN_SENT), datagram_poll() returns the current mask without EPOLLOUT, even if the send buffer is nominally free. Wait for EPOLLOUT to confirm the connection is established, or EPOLLHUP/EPOLLERR to detect failure.
For interrupt (OOB) data notification, use fcntl(F_SETOWN_EX) to direct SIGURG to the correct thread (see the Send and Receive Data section above).
shutdown()
shutdown(sockfd, how); /* returns EOPNOTSUPP */
shutdown() is not supported (sock_no_shutdown); it always returns EOPNOTSUPP. To half-close or fully close a connection, use close().
close()
close(sockfd);
Behaviour depends on the current X.25 state:
State at close() |
Kernel action |
|---|---|
X25_STATE_0 (idle) or X25_STATE_2 (awaiting clear confirmation) |
x25_disconnect() and socket freed immediately |
X25_STATE_1 (awaiting call accepted), X25_STATE_3 (data transfer), or X25_STATE_4 (awaiting reset confirmation) |
Queues cleared, CLEAR_REQUEST sent, T23 started (default 180 s). Socket enters X25_STATE_2 and is orphaned with SOCK_DESTROY set; freed when CLEAR_CONFIRMATION arrives or T23 fires |
X25_STATE_5 (call accepted pending) |
CLEAR_REQUEST sent, x25_disconnect() called, socket freed immediately |
socketpair()
Not supported; always returns EOPNOTSUPP.
mmap()
Not supported; always returns EOPNOTSUPP.
getsockopt() and setsockopt()
Only SOL_X25 / X25_QBITINCL is handled. Any other level or optname returns ENOPROTOOPT.
int one = 1;
setsockopt(sockfd, SOL_X25, X25_QBITINCL, &one, sizeof(one));
See the Send and Receive Data section for the effect of X25_QBITINCL on the data layout.
ioctl(TIOCOUTQ) and ioctl(TIOCINQ)
int bytes;
ioctl(sockfd, TIOCOUTQ, &bytes); /* send buffer space remaining */
ioctl(sockfd, TIOCINQ, &bytes); /* bytes in next received packet */
TIOCOUTQreturnssk_sndbuf - sk_wmem_alloc, clamped to ≥ 0. This is the remaining space in the send buffer, not the number of bytes pending transmission.TIOCINQreturns theskb->lenof the first socket buffer insk_receive_queue, or 0 if the queue is empty. Because AF_X25 delivers one complete record perrecv(), this value equals the size of the next record.
AF_X25 Socket IOCTLs
The kernel module supports several IOCTLs for management. All X.25-specific IOCTLs are in the SIOCPROTOPRIVATE range starting at 0x89E0.
Complete IOCTL Table
Code should prefer #include <linux/x25.h> to get these constants where possible:
| IOCTL | Value | Structure | Description |
|---|---|---|---|
SIOCX25GSUBSCRIP |
0x89E0 |
x25_subscrip_struct |
Get interface global facility mask and extended mode setting. |
SIOCX25SSUBSCRIP |
0x89E1 |
x25_subscrip_struct |
Set global facility mask and extended mode setting. Requires CAP_NET_ADMIN. |
SIOCX25GFACILITIES |
0x89E2 |
x25_facilities |
Get the negotiated facilities on a connected socket. |
SIOCX25SFACILITIES |
0x89E3 |
x25_facilities |
Set requested facilities. Socket must be in TCP_LISTEN or TCP_CLOSE state (af_x25.c:1465). |
SIOCX25GCALLUSERDATA |
0x89E4 |
x25_calluserdata |
Get the Call User Data from an incoming call. |
SIOCX25SCALLUSERDATA |
0x89E5 |
x25_calluserdata |
Set Call User Data for an outgoing Call Request. |
SIOCX25GCAUSEDIAG |
0x89E6 |
x25_causediag |
Get the last received Cause/Diagnostic codes. |
SIOCX25SCUDMATCHLEN |
0x89E7 |
x25_subaddr |
Set how many CUD bytes a listening socket matches on. Socket must be in TCP_CLOSE. |
SIOCX25CALLACCPTAPPRV |
0x89E8 |
(none) | Enable manual call acceptance mode (clears X25_ACCPT_APPRV_FLAG). Socket must be in TCP_CLOSE. |
SIOCX25SENDCALLACCPT |
0x89E9 |
(none) | Send a Call Accepted for a manually-held incoming call. Socket must be TCP_ESTABLISHED. Requires SIOCX25CALLACCPTAPPRV to have been called first. |
SIOCX25GDTEFACILITIES |
0x89EA |
x25_dte_facilities |
Get DTE (OSI network address extension) facilities. |
SIOCX25SDTEFACILITIES |
0x89EB |
x25_dte_facilities |
Set DTE facilities. Socket must be in TCP_LISTEN or TCP_CLOSE state. |
Managing X.25 Routes
Standard routing IOCTLs can be used with AF_X25 sockets:
| IOCTL | Structure | Description |
|---|---|---|
SIOCADDRT |
x25_route_struct |
Add a prefix-based route to an interface. Requires CAP_NET_ADMIN. |
SIOCDELRT |
x25_route_struct |
Remove a route. Requires CAP_NET_ADMIN. |
ioctl(sockfd, SIOCADDRT, &x25_route_struct):
Adds an X.25 routing entry. The kernel (in x25_route.c:x25_add_route) stores a prefix+sigdigits→device mapping. When an AF_X25 socket connect() is called to a matching address, the kernel uses this route to determine which TUN interface to use. Requires an open AF_X25 socket (for the IOCTL dispatcher) and CAP_NET_ADMIN.
ioctl(sockfd, SIOCDELRT, &x25_route_struct):
Removes an X.25 routing entry. Existing connected sockets are not affected.
struct sockaddr_x25
struct sockaddr_x25 {
sa_family_t sx25_family; /* Must be AF_X25 */
struct x25_address sx25_addr; /* X.121 address */
};
struct x25_address
struct x25_address {
char x25_addr[16]; /* NUL-terminated ASCII string of digits */
};
struct x25_facilities
struct x25_facilities {
unsigned int winsize_in, winsize_out;
unsigned int pacsize_in, pacsize_out;
unsigned int throughput;
unsigned int reverse;
};
Note: Packet sizes in pacsize_in/pacsize_out are log2 values (e.g., 9 for 512 bytes). Window sizes are in packets (1–127).
struct x25_causediag
struct x25_causediag {
unsigned char cause;
unsigned char diagnostic;
};
struct x25_calluserdata
struct x25_calluserdata {
unsigned int cudlength;
unsigned char cuddata[128];
};
For incoming calls, the kernel first checks that the call’s destination address matches the socket’s bound address (or the socket is bound to the null/wildcard address). It then applies CUD matching via x25_find_listener():
- If the socket’s
cudmatchlength(set bySIOCX25SCUDMATCHLEN) is zero, or the incoming call CUD is shorter thancudmatchlength, the socket is recorded asnext_best(address-only match). - If
cudmatchlength > 0and the firstcudmatchlengthbytes of the call CUD match the socket’s stored CUD (calluserdata.cuddata), the socket is a direct match and the call is routed to it immediately. - If
cudmatchlength > 0and the call CUD does not match, the socket is skipped entirely (not evennext_best).
If no direct CUD match is found, the next_best socket (address-only match) receives the call. Note that SIOCX25SCALLUSERDATA sets the CUD used for outgoing calls and stored for comparison; the cudlength field in x25_calluserdata is only used for outgoing calls. The effective match length is always cudmatchlength from SIOCX25SCUDMATCHLEN.
This behaviour differs from the code comment (“Note: if a listening socket has cud set it must only get calls with matching cud”). In practice, a socket with cudmatchlength > 0 that fails CUD matching is skipped, but an address-only socket (cudmatchlength == 0) will still receive the call as next_best. To reliably filter by CUD:
- Set
cudmatchlengthwithSIOCX25SCUDMATCHLEN. - Set CUD bytes with
SIOCX25SCALLUSERDATA. - After
accept(), useSIOCX25GCALLUSERDATAto verify the call’s CUD matched as expected.
struct x25_subscrip_struct
struct x25_subscrip_struct {
char device[200-sizeof(unsigned long)]; /* 192 bytes on x86_64 */
unsigned long global_facil_mask;
unsigned int extended;
};
global_facil_mask gets or sets the neighbour facilities mask:
X25_MASK_REVERSE(0x01): Includereversein created facilities. (default on link device up).X25_MASK_THROUGHPUT(0x02): Includethroughputin created facilities. (default on link device up).X25_MASK_PACKET_SIZE(0x04): Includepacsize_inandpacsize_outin created facilities. (default on link device up).X25_MASK_WINDOW_SIZE(0x08): Includewinsize_inandwinsize_outin created facilities. (default on link device up).X25_MASK_CALLING_AE(0x10) /X25_MASK_CALLED_AE(0x20): IncludeX25_MARKER+X25_DTE_SERVICESin created facilities.X25_MASK_CALLING_AE: IncludeX25_FAC_CALLING_AEif it is set when creating facilities.dte_facs->calling_aecan be set withSIOCX25SDTEFACILITIES.X25_MASK_CALLED_AE: IncludeX25_FAC_CALLED_AEif it is set when creating facilities.dte_facs->called_aecan be set withSIOCX25SDTEFACILITIES.
extended gets or sets extended window modulus support (0 = 8, 1 = 128), as well as extended GFI and M bit handling. It does not affect LCI mapping.
SIOCX25GSUBSCRIP and SIOCX25SSUBSCRIP require device to be the name of an up ARPHRD_X25 device with a registered neighbour; otherwise they have no effect.
struct x25_dte_facilities
struct x25_dte_facilities {
__u16 delay_cumul; // unused
__u16 delay_target; // unused
__u16 delay_max; // unused
__u8 min_throughput; // unused
__u8 expedited; // unused
__u8 calling_len;
__u8 called_len;
__u8 calling_ae[20];
__u8 called_ae[20];
};
DTE Facilities can only be set on sockets in TCP_LISTEN or TCP_CLOSE state.
struct x25_route_struct
struct x25_route_struct {
struct x25_address address;
unsigned int sigdigits;
char device[200-sizeof(unsigned long)]; /* 192 bytes on x86_64 */
};
X.25 over TUN (ARPHRD_X25)
Software can interface with the kernel by creating a TUN device and setting its link type to ARPHRD_X25. This tells the kernel to treat the interface as a native X.25 packet device.
Encapsulation and Handshake
In order to provide consistent detection of X.25 packets and maintain the kernel state machine for connections, the TUN device must be opened without IFF_NO_PI so that the 4-byte Protocol Information header is included in every frame. PI packets exchanged with the TUN device include a 4-byte PI header ([0x00, 0x00, 0x08, 0x05], referred to as [PI] below) followed by a 1-byte control header.
Control Headers
The following headers are defined (source: net/x25/x25_dev.c, constants from include/net/x25device.h):
| Value | Name | Purpose |
|---|---|---|
0x00 |
TunHeaderData |
Standard X.25 PLP packet data follows. |
0x01 |
TunHeaderConnect |
Link Layer (L2) connection request/ack. |
0x02 |
TunHeaderDisconnect |
Link Layer (L2) disconnection. |
0x03 |
TunHeaderParam |
Exchange of link parameters. Not used in practice for ARPHRD_X25. |
The Connect Handshake
When the kernel’s X.25 stack needs to transmit a frame and the link is down (X25_LINK_STATE_0), it sends a TunHeaderConnect (0x01) frame with an empty payload (x25_dev.c:x25_establish_link). The gateway must respond with an identical TunHeaderConnect (0x01) frame. On receiving the echo, the kernel calls x25_link_established(), transitions the link to X25_LINK_STATE_2, and immediately sends a RESTART_REQUEST packet (LCI=0, type 0xFB) as a TunHeaderData frame. The gateway must respond to the RESTART_REQUEST with a RESTART_CONFIRMATION (LCI=0, type 0xFF). Only then does the kernel transition to X25_LINK_STATE_3 and begin forwarding queued packets.
All CALL_REQUESTs (from connect()), CALL_ACCEPTEDs (for inbound calls), CLR_REQs, CLR_CONFs, and data frames are queued until STATE_3. They are flushed by x25_link_control() (x25_link.c:124–126) when STATE_3 is entered. (COMPAT003)
If the kernel receives a RESTART_CONFIRMATION while already in STATE_3, it kills all active sockets with ENETUNREACH, sends a new RESTART_REQUEST, and returns to STATE_2. (COMPAT004)
When the kernel receives a RESTART_REQUEST while in X25_LINK_STATE_3, all AF_X25 sockets are killed (ENETUNREACH), but the link state stays at STATE_3 and the kernel immediately sends RESTART_CONFIRMATION. The kernel also remains in STATE_3 after this (it does not transition back to STATE_2). The gateway reads the resulting RESTART_CONFIRMATION from TUN. (COMPAT005)
The Disconnect Handshake
The kernel sends TunHeaderDisconnect (0x02) with an empty payload when the link is terminated (x25_dev.c:x25_link_terminated). On receipt, the gateway must immediately clean up all active sessions. No echo or response is sent back to the kernel. The kernel has already called x25_kill_by_neigh() internally, which disconnects every AF_X25 socket on that interface with ENETUNREACH. Sending CLR_REQ packets back to the kernel after this point is unnecessary.
When the TUN fd is closed, the TUN driver calls netif_carrier_off(), which fires NETDEV_CHANGE with no carrier. The X.25 NETDEV_CHANGE handler calls x25_link_terminated(), which calls x25_kill_by_neigh(), disconnecting all remaining AF_X25 sockets with ENETUNREACH. This is the same cleanup that writing TunHeaderDisconnect achieves. Writing TunHeaderDisconnect before closing the fd is still preferable: it provides a deterministic point at which the gateway can complete session teardown before handing off to the process exit path.
Kernel Link State Machine
The kernel maintains an internal link state for each neighbor device (x25_link.c):
| State | Name | Description |
|---|---|---|
X25_LINK_STATE_0 |
Down | No link. Frame transmission triggers link establishment. |
X25_LINK_STATE_1 |
Connect Sent | Kernel sent TunHeaderConnect, awaiting echo. |
X25_LINK_STATE_2 |
Restart Sent | Echo received; RESTART_REQUEST sent, awaiting RESTART_CONFIRMATION. |
X25_LINK_STATE_3 |
Operational | RESTART_CONFIRMATION received; ready for data. |
TUN Operations
This section provides step-by-step procedures for common X.25 connection management tasks, including the required control header handshakes with the kernel. All TUN frames use the 4-byte PI header [0x00, 0x00, 0x08, 0x05] as prefix.
Open an X.25 TUN in PI Mode
This establishes a TUN interface ready for X.25 traffic. “PI mode” means the TUN device includes the 4-byte Protocol Information header in every frame (i.e., IFF_NO_PI is not set).
- Open the TUN character device:
tunfd = open("/dev/net/tun", O_RDWR)Opens the TUN/TAP control file. Returns a file descriptor that is used for all subsequent configuration and I/O on the virtual interface. The interface does not yet exist.
- Configure TUN mode with PI headers (do NOT include
IFF_NO_PI):ioctl(tunfd, TUNSETIFF, ifr) /* ifr.ifr_flags = IFF_TUN */Creates or attaches to a named TUN interface.
IFF_TUNselects layer-3 (IP-like) framing, as opposed toIFF_TAP(Ethernet). OmittingIFF_NO_PIcauses the kernel to prepend a 4-byte Protocol Information header[0x00, 0x00, type_hi, type_lo]to every frame, where the type field isETH_P_X25(0x0805) for X.25. - Set link type to ARPHRD_X25 (271):
ioctl(tunfd, TUNSETLINK, ARPHRD_X25)The kernel registers a new neighbor object in
X25_LINK_STATE_0. Sets the hardware type of the TUN interface toARPHRD_X25. This causes the kernel’s AF_X25 packet handler (x25_lapb_receive_frameinx25_dev.c) to recognise frames written to this TUN interface as X.25 LAPB frames. It also triggersNETDEV_POST_TYPE_CHANGE, which callsx25_link_device_up()to register a neighbor object for the device inX25_LINK_STATE_0. - Bring the interface UP (requires setting up a temporary socket for the IOCTL):
ioctl(sockfd, SIOCSIFFLAGS, ifr) /* flags |= IFF_UP | IFF_RUNNING */Brings the network interface up operationally. Note:
NETDEV_UPis not handled by the X.25 stack; the neighbour object was already registered during theTUNSETLINKstep (viaNETDEV_POST_TYPE_CHANGE). The interface is now visible to the X.25 routing layer but the L2 link is still inX25_LINK_STATE_0. - Optionally add X.25 routes (requires
CAP_NET_ADMIN; uses a temporary AF_X25 socket):ioctl(x25_sock, SIOCADDRT, &x25_route_struct) -
L2 Connect Handshake — triggered the first time the kernel needs to transmit (e.g., on the first incoming
CALL_REQwritten to TUN, or on a socketconnect()call):Kernel → Gateway:
[PI][0x01](TunHeaderConnect, empty payload)Gateway → Kernel:
[PI][0x01](echo TunHeaderConnect back)Kernel transitions to
X25_LINK_STATE_2and sendsRESTART_REQUEST. Any frames queued while the link was down remain queued untilX25_LINK_STATE_3. -
L3 Restart Handshake:
Kernel → Gateway (via TunHeaderData):
[PI][0x00][0x10, 0x00, 0xFB, 0x00, 0x00](GFI=0x10, LCI=0, Type=RESTART_REQUEST, cause=0x00, diag=0x00)Gateway → Kernel (via TunHeaderData):
[PI][0x00][0x10, 0x00, 0xFF](GFI=0x10, LCI=0, Type=RESTART_CONFIRMATION)Kernel transitions to
X25_LINK_STATE_3. The socket is now operational.RESTART_CONFIRMATIONis sent asTunHeaderDatawith LCI=0 and packet type0xFF. When the kernel’sx25_link_control()receives this inX25_LINK_STATE_2, it transitions toX25_LINK_STATE_3and flushes all queued outbound frames to the device. Failure to sendRESTART_CONFIRMATIONleaves the link inX25_LINK_STATE_2and the T20 restart timer (default 180 s) retransmits theRESTART_REQUESTrepeatedly.
Establishing a call using an X.25 Packet Socket in PI Mode
- CALL_REQUEST — Kernel → TUN Gateway (TunHeaderData):
[PI][0x00][GFI|LCI_H, LCI_L, 0x0B, addr_block, fac_block, CUD...] - CALL_ACCEPTED — Remote DCE → TUN Gateway → Kernel (TunHeaderData):
[PI][0x00][GFI|LCI_H, LCI_L, 0x0F, addr_block, fac_block]Kernel state machine (
x25_state1_machine) transitions toX25_STATE_3/TCP_ESTABLISHED.connect()returns 0 (or the socket becomes readable for non-blocking callers).
Close an X.25 TUN Packet Connection
-
If socket is in
X25_STATE_3(data transfer): Kernel clears queues, sendsCLEAR_REQUEST, entersX25_STATE_2(TCP_CLOSE), starts T23 timer. - CLEAR_REQUEST — Kernel → TUN Gateway (TunHeaderData):
[PI][0x00][GFI|LCI_H, LCI_L, 0x13, cause, diag]Gateway relays to remote DCE.
- CLEAR_CONFIRMATION — Remote DCE → TUN Gateway → Kernel (TunHeaderData):
[PI][0x00][GFI|LCI_H, LCI_L, 0x17]Kernel
x25_state2_machinecallsx25_disconnect(), moves toX25_STATE_0, socket is freed. - If T23 expires (180 s default) with no confirmation: kernel destroys socket unconditionally.
Clear All Connections on a Packet Socket and Shut Down
Gateway-initiated graceful shutdown.
-
For each active session in the session manager: a. Send
CLEAR_REQUESTto the remote peer (over TCP) with an appropriate cause code. b. Remove the session from the session manager. - Send TunHeaderDisconnect to the kernel to instruct it to close all connections on the packet socket:
write(tunfd, [0x00, 0x00, 0x08, 0x05, 0x02])Instructs the kernel to terminate the L2 link.
x25_lapb_receive_frame()callsx25_link_terminated(nb)which: sets neighbor state toX25_LINK_STATE_0, purges the neighbor’s outbound queue, stops the T20 timer, and callsx25_kill_by_neigh(nb).x25_kill_by_neighiterates all sockets and callsx25_disconnect(s, ENETUNREACH, 0, 0)for every socket associated with this neighbor. This effectively clears all connections on the packet socket. Any pendingconnect()orrecv()call on those sockets returns immediately withENETUNREACH. - Close the TUN file descriptor:
close(tunfd)The kernel fires
NETDEV_UNREGISTER, cleaning up neighbor and route entries.
Receive a Notification that an X.25 Connection Was Closed Remotely and Clean Up
The remote DCE initiates clearing.
- CLEAR_REQUEST — Remote DCE → TUN Gateway:
Gateway writes to TUN as TunHeaderData:
[PI][0x00][GFI|LCI_H, LCI_L, 0x13, cause, diag]The remote peer has initiated clearing. The gateway decodes the CLEAR_REQUEST from the remote DCE and writes it to the kernel via TUN. The kernel’s
x25_state3_machinereceives it and processes the remote-initiated clear. - Kernel
x25_state3_machinereceivesCLEAR_REQUEST:- Sends CLEAR_CONFIRMATION back via TunHeaderData:
[PI][0x00][GFI|LCI_H, LCI_L, 0x17] - Calls
x25_disconnect(sk, 0, cause, diag)→ socket moves toX25_STATE_0,sk_state = TCP_CLOSE - Wakes any blocked
recv()with EOF or error.
- Sends CLEAR_CONFIRMATION back via TunHeaderData:
- Gateway reads CLEAR_CONFIRMATION from TUN (TunHeaderData). Gateway forwards
CLR_CONFto remote and removes the LCI mapping from the session manager.
Receive a Notification that an X.25 Packet Socket Was Disconnected Remotely and Clean Up
The link layer (L2) is terminated by the kernel. This affects all connections on the interface.
- Kernel sends TunHeaderDisconnect with empty payload to the TUN device:
[PI][0x02]The kernel sends this (via
x25_link_terminated()) when the link is terminated (onNETDEV_CHANGEcarrier-off,NETDEV_DOWN, or receipt ofX25_IFACE_DISCONNECT). The frame has an empty payload; only the 5-byte[PI][0x02]sequence is written to the TUN fd. On receipt, the gateway must clean up all sessions. The kernel has already killed all associated AF_X25 sockets internally; no acknowledgement orCLR_REQto the kernel is required. -
Gateway reads the frame. The payload is empty, only the control byte
0x02is present. - Gateway calls
closeAllSessions():- For each active session: send
CLEAR_REQUESTto the remote peer (cause:NetworkCongestionorOutOfOrder). - Remove all sessions from the session manager.
- For each active session: send
-
No response is sent back to the kernel. The kernel has already called
x25_kill_by_neigh()internally, disconnecting all AF_X25 sockets on that interface withENETUNREACH. Any further writes to those sockets will fail. - The TUN gateway may continue running and await a new L2 connect handshake (step 6–7 in Use Case 1) before accepting further calls.
References
man 7 x25: Linux X.25 protocol implementation.- Linux Kernel:
net/x25/af_x25.c - Linux Kernel:
net/x25/x25_dev.c - Linux Kernel:
net/x25/x25_link.c - Linux Kernel:
net/x25/x25_in.c - Linux Kernel:
include/uapi/linux/x25.h - Linux Kernel:
include/net/x25device.h