Skip to content

Hubpoint - #405

Open
awehttam wants to merge 25 commits into
claudesbbsfrom
hubpoint
Open

Hubpoint#405
awehttam wants to merge 25 commits into
claudesbbsfrom
hubpoint

Conversation

@awehttam

@awehttam awehttam commented Aug 8, 2026

Copy link
Copy Markdown
Owner

No description provided.

awehttam and others added 25 commits August 7, 2026 18:00
Lets BinktermPHP distribute echomail to subordinate FTN nodes and
points (boss.point addresses), not just receive from uplinks. Adds
hub_nodes/hub_node_areas/hub_node_outbound tables, HubNodeManager
(CRUD + area subscriptions), HubFanout (SEEN-BY/PATH-aware fanout
into the outbound queue), and an admin UI at /admin/hub-nodes
(labeled "Downlinks" to avoid confusion with upstream uplinks).

This is enqueue-only per docs/proposals/HubPointSystemJuly2026.md
Phase 1 - actual binkp delivery of queued packets is Phase 2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Delivers the packets Phase 1 enqueued into hub_node_outbound, both
pull and push:

- BinkpSession: M_ADR matching and password lookup now also check
  hub_nodes (fixes a real bug where a multi-AKA peer could get the
  wrong address selected as remoteAddress). New sendHubNodeOutbound()
  serves pending hub_node_outbound rows to the authenticated peer,
  mirroring the existing sendFreqFiles()/sendHoldFiles() pattern.
- BinkpClient: connect()'s existing non-uplink fallback now checks
  hub_nodes (inet_host/port/session_password) before nodelist/DNS, so
  push delivery works through existing address-based call paths with
  no protocol/IPC changes. New pollAllHubNodes().
- binkp_poll.php: new --all-hub-nodes flag; a plain positional address
  already resolves hub nodes automatically via the connect() change.
- Scheduler: new runScheduledHubNodePush(), modeled on the existing
  crashmail-poll pattern, wired into the daemon loop.

Verified live end-to-end: a raw CRAM-MD5 binkp client simulating a hub
node authenticated against hub_nodes and received its queued packet
over the wire from a real binkp_server.php instance.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Inbound: netmail addressed to a registered hub node/point (not to us)
is now forwarded into hub_node_outbound as transit mail instead of
being dropped as undeliverable, gated behind HUB_ROUTE_NETMAIL (off
by default). Outbound: fixes a real bug where netmail composed by a
local user and addressed to a registered downlink (e.g. 227:1/400.1)
got misrouted through an unrelated uplink's network-pattern wildcard
match instead of going to the downlink directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The global security.allow_plaintext_fallback policy (hardened for
uplink/public-FTN security) was also blocking plaintext auth for
downlink/point connections. Registered hub nodes now always accept
plaintext in addition to CRAM-MD5 regardless of that policy, since
downlinks are systems under our own administrative control, not the
public network the policy is meant to guard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Downlink Queue viewer: new tab on the /binkp status page listing
hub_node_outbound rows (metadata only) with a packet inspector reusing
the existing queue-viewer modal. Extracted the packet-parsing logic
shared by both into src/Binkp/Protocol/PacketInspector.php so the CLI
test client below can reuse the exact same dump format instead of a
third duplicate implementation.

binkp_test_client.php: switched to the real BinkpFrame class for wire
framing, added CRAM-MD5 support (--no-cram to force plaintext), fixed
file reception (previously M_SKIPed everything the server sent - now
receives, sends M_GOT, and dumps .pkt contents), added --compose-netmail
to send a real test netmail without hand-crafting a packet file, and
fixed EOB handling to always reply and wait for the peer to close
rather than terminating on the first round-trip. Bounded EOB replies
to avoid an infinite ping-pong discovered during testing against a
peer (BinkpSession) that also always-replies with no cap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three related bugs found via live testing with binkp_test_client.php:

- Netmail from a registered point/downlink addressed to a third party
  (neither us nor another downlink) was silently swallowed into the
  local sysop's inbox instead of being relayed, because
  findTargetUser()'s name-based fallback matching ("To: sysop") never
  checked whether the destination address actually belonged to us.
  Added HubNetmailRouter::relayIfFromHubNode(), which queues such mail
  into the normal uplink-routed outbound flow, and gated the name
  fallback strategies on the destination actually being one of our own
  AKAs.

- Relayed/transit messages (echomail fanout and all three netmail hub
  routing paths) got a second PID kludge and tearline stacked on top
  of the original message's, because BinkdProcessor::writeMessage()
  unconditionally regenerates both. Added a skip_default_pid_tearline
  flag, set when the preserved kludges already carry a PID line.

- Relayed messages had no Via line recording the hop, and any existing
  Via history from earlier hops was being dropped rather than
  preserved. Added generateViaLine() plus
  EchomailSeenBy::parseViaLines()/formatViaLines() and wired them into
  every hub relay/delivery path (Via lines accumulate per hop, unlike
  PID which stays singular to the true originator).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
handleSentFileConfirmation() assumed any sent filename it didn't
recognize as an "extra" file lived in data/outbound/, but
sendHubNodeOutbound() writes its packets to a temp path and deletes
them right after sending. Register the temp file into the same
extraOutboundFilesByName lookup .req files use (without going through
addExtraFile(), which would also queue it for a duplicate send via
sendFiles()) so the eventual M_GOT confirmation resolves correctly
instead of logging a misleading warning for a delivery that actually
succeeded.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…oints

Delivering netmail addressed to a registered downlink was gated behind
a global env flag that defaulted off, unlike the other two
HubNetmailRouter directions. A real reply to a registered point got
silently dropped as undeliverable because nothing had set the flag.
This is core "deliver mail addressed to a registered downlink"
functionality, not an opt-in relay feature, so it's now gated solely
by the existing per-node allow_inbound_netmail flag, matching how
routeOutboundIfHubNode() and relayIfFromHubNode() already worked.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
openHubNodeModal() correctly set the dropdown to the point's real
boss_address, but the onHubNodeTypeChange() call right after it
unconditionally repopulated the same dropdown with no selection,
resetting it to the browser's default first option (the system's
primary AKA) every time the edit modal opened. Preserve the current
selection across the repopulate instead of dropping it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…cate kludges

storeEchomail() only ever fanned messages out to other downlinks, never
forwarded them up to the echoarea's configured uplink - a point posting
echomail was only ever seen locally and by other downlinks, never by the
real network. Adds relayEchomailToUplinkIfNeeded(), guarded against loops
via direct origin-vs-uplink comparison and a SEEN-BY containment check.

Reusing spoolOutboundEchomail()/writeMessage() for already-received
messages (rather than freshly-composed local posts, its only prior use)
exposed several duplicate-kludge bugs: a stored AREA: line getting
echoed back on top of the freshly-built one, and a relayed message's own
tearline/Origin/SEEN-BY/PATH getting duplicated by unconditional
synthesis in writeMessage(). Also reverts an earlier attempt to add Via
lines to relayed echomail: Via (FSC-0043) is netmail-only bookkeeping -
echomail already tracks hop history via PATH, and real tossers like HPT
never stamp Via onto it.
New --compose-echomail flag (paired with --area/--domain) builds and
sends a test echomail packet, matching the existing --compose-netmail
flow. Useful for exercising hub/downlink echomail delivery without a
real point client.
bg-light text-dark rendered dark-on-dark under the dark theme; bg-secondary
is theme-safe and already used elsewhere on this page.
…ce.php

hub_node_outbound had no cleanup at all - rows (including their packet_data
bytea blobs) accumulated forever regardless of status, and the per-node
queue_retention_days column was configurable but never actually enforced
anywhere. Adds a maintenance step that deletes sent/failed rows once
they're older than their node's own retention setting (measured from
sent_at when set, else created_at); pending/held rows are left alone
regardless of age.
…INTENANCE

Adds docs/Downlinks.md as the user-facing reference for the hub/point
system: node vs. point registration, area subscriptions, echomail/netmail
distribution, delivery, and the outbound queue viewer/cleanup. Links it
from docs/index.md's FTN Networking section, adds the feature summary to
docs/UPGRADING_1.10.0.md, and adds the new queue-purge maintenance step
to docs/MAINTENANCE.md.
…e 4, correct commit status

Phase 4 (previously Areafix) is now TIC / File Area Distribution, with a
full design: a new hub_node_fileareas subscription table, hub_node_outbound
extended with file_id/tic_file_data/tic_filename and a 'tic' message type,
and fanout/uplink-relay/delivery mirroring the existing echomail pattern.
Areafix moves to Phase 5 and is expanded to Areafix/FileFix to cover both
echo and file area self-service subscription management.

Also corrects Phase 2 and Phase 3's status - both were still marked
"uncommitted" despite being committed (7ad64d7, and 0e4c6a7 plus
follow-on fixes 4ddc552/b17f0398, respectively).
The wire filename for queued hub_node_outbound packets was generated
with uniqid('', true), producing names like
hubnode_6a76b13aaf26a9.35500206.pkt that break on mailers expecting
classic FTN 8.3 packet names.
Extends the hub distribution system to file areas: per-downlink file
area subscriptions, TIC fanout with Seenby-based loop prevention,
uplink relay for files received from a downlink/point, TIC-pair
delivery over binkp, admin UI, and Downlink Queue viewer support.
Also restricts the file area subscription checklist to public areas
only, excluding private ones.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ork domain

A point's boss AKA (or a node's own address) now resolves to a single
network domain via HubNodeManager::resolveDomain(), and %LIST/+TAG/-TAG
in HubAreafixProcessor only match echoareas/fileareas in that domain.
Prevents a point from subscribing across domains it has no business
seeing, and fixes a latent bug where area lookup ignored domain entirely
and could match the wrong same-tag area from a different network.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds dedicated areafix_password/filefix_password columns on hub_nodes,
separate from the binkp session_password so a subordinate's Areafix/
Filefix credential can be rotated independently. Wires up i18n strings,
admin hub_nodes UI, and BinkdProcessor/HubNetmailRouter routing needed
for HubAreafixProcessor's server-side robot.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
getAreaSubscriptions()/getFileAreaSubscriptions() previously listed every
active area regardless of domain, letting an admin subscribe a node/point
to areas outside the network its boss address actually belongs to (and,
per this DB's data, to the wrong one of two same-tag areas in different
domains). Both now resolve the node's domain via resolveDomain() and only
offer areas in that domain - already-subscribed areas stay visible
regardless of domain so a save doesn't silently drop them, since
bulkSet*Subscriptions() fully replaces the set from whatever the
checklist returns.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
%RESCAN [AREATAG] [days] re-queues historical echomail into
hub_node_outbound for a hub node's subscribed area(s), going back a
given number of days (default 182 / max 3650, matching Mystic's ~6-month
default). With no area tag it covers every currently subscribed area;
with one, just that area (must already be subscribed). Tag and day count
can appear in either order.

HubFanout::rescanForNode() always resends regardless of SEEN-BY, since
a rescan is a deliberate re-request rather than a fresh toss - unlike
live fanout(), which skips a downlink already present in a message's
SEEN-BY to avoid loops.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers the command table, design notes, implementation checklist, and
New/Modified Files tables in docs/proposals/HubPointSystemJuly2026.md,
plus the AreaFix/FileFix command reference and a short usage note in
docs/Downlinks.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rename admin menu items to "BinkP Uplinks" and "BinkP Downlinks" for
clarity, move Networks above BinkP Uplinks in the admin dropdown, and
rename the /binkp status page's Queues/Kept Packets tabs to "Uplink
Queues"/"Uplink Kept Packets". Updated across all locale catalogs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… time

Previously each pending hub_node_outbound row was sent to a downlink as
its own .pkt file. Add a message_payload JSONB column that stores the
createOutboundPacket()-ready message array at queue time, and have
BinkpSession::sendHubNodeOutbound() decode every pending row's payload
for a connecting node and combine them into a single multi-message
packet at send time, reducing per-session packet count. TIC rows and
rows queued before this column existed are unaffected (sent
individually as before).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nds, and cap bundle size

- scripts/binkp_server.php: require src/functions.php so generateTzutc()
  is defined (was undefined for inbound BinkP sessions)
- hub_nodes.compress_outbound: opt-in per-downlink ZIP compression of
  bundled outbound echomail/netmail, with a hard fallback to the raw
  .pkt if ZipArchive is unavailable or archive creation fails
- BinkpSession/Scheduler: retry hub_node_outbound rows left 'failed' by
  an interrupted session on the next connection (in or out), capped at
  10 attempts so a broken row doesn't retry forever
- BinkpSession: split bundled outbound into multiple .pkt files capped
  at hub_nodes.max_packet_kb instead of one unbounded bundle
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant