Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pdn-libax25

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.

Why two pieces

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:

  1. libax25.so.1 provides the helper ABI apps link against - crucially with axports parsing that does NOT require a kernel ARPHRD_AX25 netdevice (upstream refuses to list a port without one; we omit that check, so every well-formed axports line is an active port). This is the critical fix.
  2. ax25-interpose.so provides the connections, transparently, by turning AF_AX25 sockets into pdn RHPv2 sessions.

The RHPv2 client (rhpv2 crate)

  • Transport: persistent, multiplexed TCP to 127.0.0.1:9000 (env PDN_RHP_ADDR). Optional auth only when PDN_RHP_USER/PDN_RHP_PASS are set (loopback needs none).
  • Framing: 2-byte big-endian length prefix + that many bytes of UTF-8 JSON (≤ 65535).
  • data fields 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/server close) carry a per-connection seqno and no id. 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).

Build

Requires a Rust toolchain (cargo).

cargo build --release
cargo test          # address round-trips, Latin-1 codec, framing, RHP client

Artifacts land in target/release/:

  • libax25.so (SONAME libax25.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)

Usage

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.

Status

Both directions are implemented and verified end-to-end against an in-process mock RHP server:

  • Outbound socket → bind → connect → write → read → close maps to RHP open → send → recv → close. connect() now honours that AX.25 connect is asynchronous: a blocking fd blocks until the status(Connected) push (not the openReply), while an O_NONBLOCK fd returns EINPROGRESS, becomes writable once the link is up, and exposes the result via getsockopt(SO_ERROR) - so the standard non-blocking-connect + select/poll idiom works.
  • Inbound socket → bind → listen → accept is wired for ax25d: accept() blocks on a condvar (no busy-wait; EAGAIN under O_NONBLOCK), returns a child fd carrying the caller's callsign, and getsockname() reports the local port callsign in fsa_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/recvfrom maps to RHP socket{mode:custom} → bind → sendto → recv. In custom mode the AX.25 PID travels as the first octet of the data field ([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. sendto uses PID 0xF0 by default (the beacon/APRS default), prepending it to the app buffer; with AX25_PIDINCL set the app already supplies [PID][info…] (e.g. 0xCC for IP-over-AX.25) and it is sent as-is. Inbound UI is delivered whole through recvfrom with the source callsign filled, the PID stripped (or kept when AX25_PIDINCL is 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.

Licence

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.

About

LGPL-3.0 drop-in libax25.so + LD_PRELOAD AF_AX25 interposer that routes native AX.25 apps to the pdn stack over RHPv2 (post-kernel-AX.25). See packet.net docs/network-integration-adr.md.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages