FPAC 4.2: dynamic ROSE routes and idle White Pages sessions
Specification, working draft of 2026-10-13. Work in progress: the fpacrtd daemon is being tested on three nodes; its code is not released yet. Version française
1. Purpose
Today an FPAC node only knows the routes written by hand in
fpac.routes. A DNIC or a node missing from that file cannot
be reached, even when a direct neighbour knows how to reach it.
The purpose is that each node learns from its direct neighbours what
they can reach, installs the routes it lacks, and removes them when they
are no longer valid. The fpac.routes file keeps the last
word.
2. What the mechanism does, and does not do
It does:
- announce to the direct neighbours the destinations the node can reach, with a cost counted in hops;
- install in the kernel the learned routes that are missing;
- remove a learned route when the neighbour closes, stops announcing it, or announces it as lost.
It does not:
- replace a route written in
fpac.routes; - repair a link that works in one direction only;
- change the kernel rule that prefers an open neighbour;
- announce intermediate prefixes (see 4).
3. Architecture
A new daemon, fpacrtd, started by myfpac.sh
after the other FPAC daemons and stopped before them, so that its routes
are withdrawn while fpad is still running.
- It opens a ROSE circuit to each neighbour of
fpac.nodesmarkedRoute = yes, to the service callsignFPACRT, and listens on that same callsign. The first call is made 30 seconds after the start, to let the neighbours start their own daemon. - It reads the kernel routes (
/proc/net/rose_nodes), the neighbours (/proc/net/rose_neigh) andfpac.routes. - It adds and removes its routes with
SIOCADDRT/SIOCDELRT, asfpacnodealready does. - It keeps the list of its routes in
/var/ax25/fpac/fpacrt.dat, to recognise and purge them. - Only one
fpacrtdruns on a node. The daemon holds a lock on/var/ax25/fpac/fpacrtd.lockfor its whole life; a second one finds it locked, reportsanother fpacrtd is already runningand exits. Without this, the second daemon purged the routes of the first and both called the same neighbours.
Any local script that stops AX.25 or FPAC without going through
myfpac.sh must stop fpacrtd too, and first: on
2026-10-11 a restart of AX.25 on one node left the old daemon alive
beside the new one.
The same protection covers the four other daemons since 2026-10-12:
fpad, fpacwpd, fpacstat and
fpacroute each hold a lock on
/var/ax25/fpac/<name>.lock and refuse to start when
another copy is running. They also check their arguments before touching
anything: -h or -? shows a short help,
-V the version, and an unknown option or a stray argument
is an error. Until then fpad and fpacroute
ignored every argument and started: fpad -V, typed on a
running node to read its version, started a second fpad
that cleared and rebuilt the ROSE routes of the node and listened on the
same addresses as the first one. Note that fpacwpd -v is
still the verbose mode and starts the daemon.
A lock file that cannot be written (file system full or read-only) is opened read-only, which is enough to hold the lock. When it cannot be opened at all, the daemon logs a warning and starts without the protection: a node that does not come back is worse than a missing lock. A daemon built before the lock is not seen by a new one; the protection holds once the new programs run.
Why a separate daemon and not fpacwpd: a current
fpacwpd closes the session when it receives a message type
it does not know (wp_receive_pdu returns -1). Adding a
message to the White Pages sessions would break the synchronisation with
every node not yet updated. A separate service leaves the White Pages
untouched.
The service callsign is FPACRT, not ROUTE:
fpacroute, the traceroute application, already listens on
ROUTE on every FPAC node (found during the first test, 2026-10-08).
A node without fpacrtd does NOT refuse a call to
FPACRT: its fpad takes it for a user call,
records the caller in the White Pages and tries an AX.25 connection to a
station named FPACRT. A neighbour is therefore called only when its Node
block carries Route = yes (see 10).
4. What is announced
Two kinds of destinations only:
| Kind | Mask | Example |
|---|---|---|
| Node address | 10 | 2080 175526 |
| Whole DNIC | 4 | 5050 |
Intermediate prefixes (masks 5 to 9, for example
2080 44) are neither announced nor learned. Routing loops
came from them; they remain the business of
fpac.routes.
Origin of an announcement:
- node address: each node is the origin of its own address and of its local addresses, at cost 0. Nobody else can be their origin.
- whole DNIC: only a node carrying
RouteOrigin = <dnic>in itsfpac.confis its origin, at cost 0. The sysop of the node that has the real link to that country writes it.
Everything else is relaying: a node re-announces a learned
destination at the received cost plus 1, and nothing else. A route of
fpac.routes never produces an announcement by itself: a
wrong or stale line therefore has no effect outside the node.
A node relays only what its own RouteLearn lets it
retain: with RouteLearn = no it relays nothing, and with
RouteLearn = nodes it relays no DNIC.
A node declaring RouteOrigin = 5050 must itself have a
route to that DNIC in fpac.routes. Otherwise
fpacrtd reports it in the log and does not announce that
DNIC.
The DNIC is announced only while its exit link is open, that is while
a neighbour of that route is "open" in rose_neigh. When the
link closes the DNIC is withdrawn (W), and announced again
when it reopens; the state is checked every minute and each change is
written in the log. The link with the neighbour the announcement goes to
does not count: a DNIC is never announced to a node on the strength of
the link with that same node.
5. Exchanges
The dialogue is plain text, one line per message, so that it stays
readable with listen.
| Message | Meaning |
|---|---|
FPACRT 1 <call> |
opening: version, L3 callsign |
R <adr> <m> <c> |
route: address, mask, cost |
W <adr> <m> |
withdrawal of that route |
E <n> |
end of table, n routes sent |
Timing:
- full table when the circuit opens;
- full table every 30 minutes;
- single
RorWmessages about 10 seconds after a change (route learned or lost); changes close together are sent in one go; - the state of the neighbours (open or closed) is checked every minute.
6. Reception rules
For each route received from a neighbour V, with cost c:
- retained cost = c + 1; above 6 the route is ignored;
- if the destination is one of the node's own addresses: ignored;
- if
fpac.routesalready holds that address with that mask: the route is not installed, the manual route stays in place; the destination is still relayed to the other neighbours; - if V is not "open" in
rose_neigh: not installed; - otherwise it is installed if no learned route exists for that destination, or if its cost is strictly better.
A route that RouteLearn does not admit is not retained
at all: it is neither installed nor relayed (see 4).
One learned route per destination: the best one. No backup neighbour in version 1.
7. Transmission rules
- Split horizon: a destination is never announced to the neighbour through which the node really reaches it, whether by a learned route or by a manual route. This protects against the two-node loop.
- Poisoned withdrawal: when a learned route is lost,
a
Wis sent to the other neighbours about 10 seconds later. - Ceiling: no route with a cost above 6 is announced.
8. Lifetime
A learned route is removed from the kernel when:
- the
FPACRTcircuit with the neighbour closes, or the neighbour has sent no table for 90 minutes (see below); - the neighbour becomes "closed" in
rose_neigh(checked every minute); - the neighbour sends
W, or sends 3 full tables in a row without it (90 minutes at the normal rate); fpacrtdstops: it removes all its routes.
At startup, fpacrtd first removes the routes left by its
previous run, according to fpacrt.dat. Every minute it also
checks that its routes are still in the kernel and puts back any that
disappeared.
A neighbour that stays silent is handled too: when a node holds
routes learned from a neighbour and has received no full table from it
for 90 minutes, it closes the circuit. The routes learned from that
neighbour are removed, and it is called again 30 minutes later. A
neighbour that never announced anything
(RouteAnnounce = no) is silent by design and is left
alone.
9. Manual routes and learned routes
- A route of
fpac.routesis never removed nor modified. - With the same address and mask, the manual route wins.
- A learned route more specific than a manual one applies, like any
more specific route: a learned node address goes before a manual
2080 = …. This is intended (agreed by F6BVP, 2026-10-08), but it is a change of behaviour to be aware of.
10. Configuration
In fpac.conf and fpac.nodes, four settings,
all inactive by default:
| Keyword | Values | Effect |
|---|---|---|
RouteAnnounce |
no, yes |
sends its table |
RouteLearn |
no, nodes, |
learns nothing, nodes, |
all |
or nodes and DNICs | |
RouteOrigin |
one DNIC per line | exit gateway of the DNIC |
Route |
yes, in a Node |
this neighbour takes |
| block | part |
Without these lines, a 4.2 node behaves like a 4.1 node. A neighbour
without Route = yes is never called and its calls are
refused.
Example for f6bvp-8, which has the real links abroad:
RouteAnnounce = yes
RouteLearn = all
RouteOrigin = 2020
RouteOrigin = 2400
RouteOrigin = 3020
RouteOrigin = 3100
RouteOrigin = 5050
and, in fpac.nodes, one more line in the block of each
neighbour that runs fpacrtd (here f6bvp-12). Nothing is
exchanged without it:
Node = F6BVP-13
...
Route = yes
End
11. Display
Planned, not coded yet. The Routes command of
fpacnode will mark the learned routes (dyn),
and a routes dyn command will list the learned routes only,
with the neighbour that announces them.
Today the learned routes can be read in the log of
fpacrtd (lines "add route" and "del route") and in
fpacrt.dat.
12. Compatibility
- A 4.1.x node must never be called on the
FPACRTcallsign (see 3):Route = yesis written in a Node block only once the sysop of that neighbour runsfpacrtd. The two sysops agree first. - A neighbour marked
Route = yesthat does not answer is retried every 30 minutes. - The kernel is not modified.
- The White Pages are not modified.
13. Test plan
- Pair f6bvp-12 / f6bvp-8 (f6bvp-8 is the best connected node):
f6bvp-8 with
RouteAnnounce = yesand itsRouteOriginlines, f6bvp-12 withRouteLearn = all. First remove a distant DNIC, for example 5050, fromfpac.routesof f6bvp-12: it must come back by itself. Also check the removal when the neighbour closes and the purge at restart. - Enable announcing both ways on the pair, then add f6bvp-10 with
RouteLearn = all: it must receive 5050, which it lacks today. - Provoked loop test: cut the exit link of a DNIC and check that the
route disappears everywhere in less than 2 minutes, with no frame going
round (
rose-controle.py). - One week of observation before extending to the other nodes.
Status on 2026-10-12: step 1 passed on 2026-10-08, including the removal when the neighbour's daemon stops and the purge at restart. Since 2026-10-09 three nodes take part: f6bvp-8 announces, f6bvp-12 and f6bvp-10 learn, each directly from f6bvp-8. After each of four nightly restarts of FPAC the sessions and the learned routes came back by themselves within 50 seconds, and the table of 6 routes has been received every 30 minutes without a miss. Announcing both ways (step 2) is not enabled yet. Not tested on the air yet: ageing, the 90 minute rule, relaying through a third node, and a real call through a learned route.
The daemons with the single instance lock (see 3) have been running on seven nodes since 2026-10-12, built from the 4.1.6-rc7 sources plus this change. On each node the locks are held and a second start of each daemon is refused.
14. Decisions taken and open points
Decided (F6BVP, 2026-10-08):
- the exit gateway of a DNIC is declared with
RouteOrigin; a manual route alone is not enough to announce it; - a learned node address goes before a wider manual route;
- ceiling of 6 hops and period of 30 minutes as starting values;
- first tests on the pair f6bvp-12 / f6bvp-8.
Still open:
- should version 2 keep a backup neighbour per destination?
- what if two nodes declare
RouteOriginfor the same DNIC: it is allowed (two real exits), each node takes the nearest. To be confirmed in use.
Decided (F6BVP, 2026-10-13):
- a DNIC declared with
RouteOriginis announced only while its exit link is open, and withdrawn when that link closes. Reason: the kernel lets a call in transit leave only through an open neighbour (a closed one is tried for a local call only), so a DNIC announced over a closed exit leads nowhere. The kernel rule is not changed. Coded on 2026-10-13 (see 4) and on trial since then on the announcing node, whose table went from 6 routes to 2: four of its five DNICs had a closed exit.
15. Numbering
This feature adds a daemon, a protocol between nodes and configuration keywords: it justifies FPAC 4.2. Proposed order:
- release the stable 4.1.6 with the fixes of rc7;
- open 4.2-rc1 with
fpacrtdandWpIdle(see 16), both inactive by default, the change to the vectors (see 17) and theQSlcommand (see 18).
The code under test is public: branch fpac-4.2 of https://github.com/f6bvp/fpac. It is experimental and
not a release.
16. Second item: White Pages sessions closed when idle
Decided by F6BVP on 2026-10-08. Nothing is coded yet.
Today fpacwpd keeps one ROSE circuit open for ever with
each neighbour (WP-0 to WP-0), although it only carries a record now and
then and one vector exchange per hour.
New keyword in fpac.conf:
| Keyword | Value | Effect |
|---|---|---|
WpIdle |
minutes, 0 = off | closes an idle WP session |
Without the keyword, or with 0, nothing changes: the session stays
open as in 4.1. Recommended value: WpIdle = 10.
Behaviour with WpIdle = 10:
- a session with a neighbour is closed after 10 minutes without any record or vector exchanged;
- it is reopened on demand only: when a record has to be pushed to that neighbour (local change or relay), or when the hourly vector exchange is due;
- a session opened by the neighbour is always accepted;
- the blind retry every 30 minutes remains only after a failed connection, not after an idle close.
What the code needs:
- the list of records waiting for a neighbour must belong to the neighbour and no longer to the session, so that it survives a close;
- the neighbour must be recognised as a WP server (fix of 4.1.6-rc7);
- today a session that opens starts a full vector exchange; reopening one to push a single record must not start it each time.
With a neighbour still running 4.1, which reopens its session every 30 minutes and keeps it, the circuit is closed again 10 minutes later. This is harmless but shows in the logs.
The kernel can clear an idle ROSE circuit by itself (socket option
ROSE_IDLE). Closing from fpacwpd is preferred, because it
knows whether an exchange is in progress; to be settled when coding.
Test: on a pair, check that an idle session closes, that a record
changed with wpedit reopens it and reaches the neighbour,
and that the hourly vector exchange still takes place.
17. Third item: obsolete deleted records left out of the vectors
Found on 2026-10-11. Two nodes kept logging
Vectors still differ every hour although both ran the same
version.
A deleted record older than 14 days (WP_OBSOLETE) is
neither sent nor accepted between nodes, and a deleted record for an
unknown callsign is refused. Yet these records count in the vectors,
whose checksum covers the date and the address of every record. So when
two nodes mark a record deleted, or erase it, on different days, their
vectors differ and the exchange cannot repair it: the records that would
settle the difference are exactly the ones that are not exchanged. It
lasts until both nodes have erased them.
Here one node ran its nightly wpmaint -d 100 while the
others ran wpmaint -d 30: it held 12 deleted records the
others had erased, and two records still valid that were deleted
elsewhere.
Decided (F6BVP, 2026-10-11): leave out of the vector checksum the deleted records too old to be exchanged. Coded on 2026-10-13, not yet in service.
A node with the change and a node without it compute different vectors and would differ for ever. Decided (F6BVP, 2026-10-13): the vector version number changes with the new checksum; this is what it is for.
fpacwpd up to 4.1.6 closes the White Pages session when
it receives a vector of another version, and calls again 30 minutes
later. With nothing more, a 4.2 node and a 4.1.6 node would no longer
keep a session. Decided (F6BVP, 2026-10-13): 4.2 goes on speaking the
old version, with the old checksum, to a neighbour that is still older;
the new version is used between 4.2 nodes only. A network may then be
upgraded node by node.
As coded on 2026-10-13: the new vector version is 0x112, the old one 0x111. A node answers a vector request in the version it came in. For its own requests it first takes every neighbour for a 4.2 node; if the neighbour hangs up before answering, it is spoken to in the old version from then on, with one line in the log. A vector in the new version received from that neighbour proves it was upgraded. Since a session may also drop for another reason at that very moment, the new version is tried again once a week. The price with a 4.1 neighbour: one session closed at the first exchange, then once a week; it reopens within 30 minutes.
Not covered: a recently deleted record that the other node never knew is refused there and still counts in the vectors, until it becomes obsolete in turn.
Until then: use the same wpmaint delays on every node of
a network. The difference is harmless - three vector rounds an hour at
most since 4.1.6-rc7 - but it fills the log.
18. Fourth item: the QSl command of the node
Asked for on 2026-10-13: a QSL card confirming a connection to the node, in the manner of the card sent after a radio contact.
A user types QSl (QS is enough). The node
shows the card as text in the session: the heading "FPAC node Status command shows down to the FPAC version.
A session only carries text, so the image of the card cannot go through it. Decided (F6BVP, 2026-10-13): the node makes the image and gives a link to download it with a web browser; no e-mail. The image is one JPEG file, the picture of the node above (front side) and the text below (back side).
This needs a web server on the node, so it is optional:
| Keyword | Value | Effect |
|---|---|---|
QslDir |
directory | where the cards are written |
QslUrl |
web address | the same directory on the web |
QslImage |
file | picture of the front side |
QslKeep |
days, 30 by default | how long a card is kept |
They are written in fpac.conf, or in a file
fpacqsl.conf next to it. Without QslDir and
QslUrl the node gives the text card only. The front picture
is by default the file QSL_fpac.jpg placed next to
fpac.conf by the sysop; without it the card has its text
side only.
The image is drawn by a separate program, fpacqsl,
written in Python: the node itself gains no dependency. It needs the
python3-pil package.
Limits: the cards older than QslKeep days are erased; a
user who asks again within 10 minutes is given the card already made; no
image is made while the directory holds 500 cards. Each request is
written in fpac/qsl.log.
State on 2026-10-13: coded, and on trial on one node, which serves
the cards under http://f6bvp.org/qsl/. The user guide is the file
doc/qsl.txt of the sources.