Let unmodified Linux ham-radio applications (ax25-apps, ax25-tools, FBB,
…) use the pdn AX.25 stack (packet.net) instead of the Linux kernel
AF_AX25 stack, which was removed from mainline in Linux 7.1.
This is the native AF_AX25 seam (your app addresses a callsign). Its
counterpart for ordinary IP software (any unmodified IP app, addressed by IP,
carried over radio) is pdn-net;
packet.net's network-integration ADR
covers when to use which.
It ships two native .so artifacts (both Rust cdylibs), licence
AGPL-3.0-or-later:
| Artifact | Role |
|---|---|
libax25.so.1 |
Drop-in replacement for ve7fet libax25's helper library - address parsing (ax25_aton*/ax25_ntoa/ax25_cmp/ax25_validate) and axports config parsing. libax25 has no connection code; apps link -lax25 only for these helpers. SONAME is libax25.so.1 (upstream real file libax25.so.1.0.1, pkg 1.2.2). |
ax25-interpose.so |
An LD_PRELOAD libc interposer. It wraps the socket/IO calls, detects AF_AX25 (family 3) sockets - SOCK_SEQPACKET (connected sessions) and SOCK_DGRAM (connectionless UI) - routes them to a pdn RHPv2 connection, and passes every other call straight through to real libc via dlsym(RTLD_NEXT, …). Build output is libax25_interpose.so. |
Both share the rhpv2 crate - an RHPv2 client over loopback
TCP 127.0.0.1:9000 (override with PDN_RHP_ADDR). It is a standalone,
publishable crate in its own right.
Upstream libax25 is only helpers; the actual connection work is done by apps
talking to the kernel AF_AX25 socket API. With the kernel stack gone:
libax25.so.1provides the helper ABI apps link against - crucially withaxportsparsing that does NOT require a kernelARPHRD_AX25netdevice (upstream refuses to list a port without one; we omit that check, so every well-formedaxportsline is an active port). This is the critical fix.ax25-interpose.soprovides the connections, transparently, by turningAF_AX25sockets into pdn RHPv2 sessions.
- Transport: persistent, multiplexed TCP to
127.0.0.1:9000(envPDN_RHP_ADDR). Optionalauthonly whenPDN_RHP_USER/PDN_RHP_PASSare set (loopback needs none). - Framing: 2-byte big-endian length prefix + that many bytes of UTF-8 JSON (≤ 65535).
datafields are Latin-1 (one byte ↔ one code point), not base64. Binary-safe over all 256 byte values (unit-tested).- Correlation: each request carries a monotonic non-zero
id; replies echo it. Async pushes (recv/accept/status/serverclose) carry a per-connectionseqnoand noid. A single reader thread demultiplexes them.
Implemented clean-room from the RHPv2 wire spec (rhp2lib-net/docs/protocol.md,
PWP-0222 / PWP-0245) and the MIT reference client RhpV2.Client (used as a model
only - pdn's GPL/AGPL Packet.Rhp2 is not linked).
Requires a Rust toolchain (cargo).
cargo build --release
cargo test # address round-trips, Latin-1 codec, framing, RHP clientArtifacts land in target/release/:
libax25.so(SONAMElibax25.so.1)libax25_interpose.so
Or use the convenience Makefile, which also creates the versioned symlinks
(libax25.so, libax25.so.1, libax25.so.1.0.1) and an ax25-interpose.so
alias:
make # build + symlinks in target/release
make install # install into $(PREFIX)/lib (default /usr/local)Point apps at the helper lib and preload the interposer:
# so the dynamic linker finds our libax25.so.1 ahead of the distro package:
export LD_LIBRARY_PATH=/path/to/target/release:$LD_LIBRARY_PATH
# route AF_AX25 sockets to pdn:
export LD_PRELOAD=/path/to/target/release/libax25_interpose.so
# optional: non-default engine address
export PDN_RHP_ADDR=127.0.0.1:9000
axcall radio GB7RDG # example: an ax25-apps client, unmodified/etc/ax25/axports is parsed as usual (name callsign speed paclen window description); set AX25_AXPORTS to point at an alternative file for testing.
Both directions are implemented and verified end-to-end against an in-process mock RHP server:
- Outbound
socket → bind → connect → write → read → closemaps to RHPopen → send → recv → close.connect()now honours that AX.25 connect is asynchronous: a blocking fd blocks until thestatus(Connected)push (not the openReply), while an O_NONBLOCK fd returnsEINPROGRESS, becomes writable once the link is up, and exposes the result viagetsockopt(SO_ERROR)- so the standard non-blocking-connect +select/pollidiom works. - Inbound
socket → bind → listen → acceptis wired forax25d:accept()blocks on a condvar (no busy-wait;EAGAINunder O_NONBLOCK), returns a child fd carrying the caller's callsign, andgetsockname()reports the local port callsign infsa_digipeater[0]. - Receive back-pressure: inbound bytes are never silently dropped - a per-handle buffer plus a flusher thread drains into the socketpair as the app reads, without stalling the shared RHP reader thread.
- Connectionless UI (
SOCK_DGRAM):socket → bind → sendto/recvfrommaps to RHPsocket{mode:custom} → bind → sendto → recv. Incustommode the AX.25 PID travels as the first octet of thedatafield ([PID][info…]), not a separate field. This is G8PZT's clarification of the carriage; PWP-0222 §1.2 only says the PID is a "user specified protocol", so it is not spelled out in the spec.sendtouses PID0xF0by default (the beacon/APRS default), prepending it to the app buffer; withAX25_PIDINCLset the app already supplies[PID][info…](e.g.0xCCfor IP-over-AX.25) and it is sent as-is. Inbound UI is delivered whole throughrecvfromwith the source callsign filled, the PID stripped (or kept whenAX25_PIDINCLis set), and all UI heard on the bound port is seen (promiscuous, matching pdn's connectionless RX).
Worked examples of every one of these paths - using only the standard AF_AX25
socket API - live in samples/ (connected client/listener, UI
beacon/monitor), with a "connected vs UI, when to use which" guide.
The former TODO(N1) items are now implemented: AX.25 timer/window socket
options (T1/T2/T3/N2/EXTSEQ) are stored and returned via getsockopt
(node-side-only until RHPv2 grows wire fields); connect-via-digipeater paths
are parsed from fsa_digipeater[] and carried as a via field in the RHPv2
open request; get_call resolves uid→callsign via AX25_CALLSIGN env,
/etc/ax25/ax25_calls, or the first axports port as fallback.
AGPL-3.0-or-later — all three crates (rhpv2, libax25, ax25-interpose). See
COPYING (AGPL-3.0). Address/config logic was reimplemented
clean-room from ve7fet libax25 (GPL) read as a semantic reference only; no GPL
code is copied in. New dependencies (serde, serde_json, libc) are
MIT/Apache-2.0. The example programs under samples/ are 0BSD so you
can copy them freely.