Commit Graph

6 Commits

Author SHA1 Message Date
Anso 64bf6344a3 fix(mesh): bias Sencho static IP via IPAM IPRange (F-13) (#1162)
* fix(mesh): reserve Sencho static IP via IPAM auxiliary address (F-13)

Sencho pins itself to <network>+2 on sencho_mesh, but the IPAM block only
declared Subnet, so Docker freely handed that address to any meshed
workload that restarted while Sencho was offline. A real-world hit on
arrapps-prod during a Sencho upgrade had tautulli grab 172.30.0.2, which
blocked the new Sencho container's mesh attach with "Address already in
use" and left compose in a half-state needing manual disconnect/recreate.

The fix reserves <network>+2 via AuxiliaryAddresses on the IPAM Config
when creating sencho_mesh. Aux-listed addresses are removed from the
auto-allocatable pool, so Docker refuses to hand the IP to any container
that does not explicitly request it. Sencho's own attach via
connectContainerToNetwork({ ipv4Address }) is unaffected because
explicit pins still bind aux-reserved addresses. Workload containers
without a pinned IP get .3 and up.

Wire format verified against the Docker Engine REST v1.33 OpenAPI spec:
the JSON key on POST /networks/create (and on the inspect response) is
AuxiliaryAddresses inside each IPAM.Config item, value { sencho: <ip> }.

Adopt-existing path: when Sencho boots against a sencho_mesh that
pre-dates this reservation, the data plane still comes up but a one-time
warn fires (mesh.enable activity at level: 'warn' plus a [Mesh] console
line so docker logs surfaces it). The advisory explains the squat risk
and gives the recreate recipe.

Tests: 5 cases added to mesh-setup-error-classification.test.ts covering
the explicit-env create payload, the candidate-iteration winner payload,
the adopt-legacy warn (env-unset), the adopt-already-reserved silent
path, and the TOCTOU 409 race-winner adopt-legacy warn.

Docs: one sentence added to docs/features/sencho-mesh.mdx under
"Customising the mesh subnet" describing the reservation positively.

No tier/role/capability/flag gates touched (no frontend changes).

* fix(mesh): use IPRange upper-half bias instead of aux-address reservation

The initial F-13 fix used IPAM AuxiliaryAddresses to reserve <network>+2
on sencho_mesh. Empirical probe against Docker 29.4.3 confirmed this
also blocks explicit pins via EndpointConfig.IPAMConfig.IPv4Address:
libnetwork's RequestAddress() rejects a preferred-address request when
the bit is already set by the aux reservation. Result: the freshly-
reserved network refuses Sencho's own ensureSelfAttached, and the data
plane never comes up.

The pivot uses IPRange instead. IPRange constrains Docker's auto-
allocation to the configured CIDR; preferred-address requests via
RequestAddress(prefAddress) skip the IPRange check and only consult
the subnet-wide bitmap. So setting IPRange to the upper half of the
subnet (e.g. 172.30.0.128/25 for 172.30.0.0/24) biases workloads
without an explicit IP to <network>+128 and up, while Sencho's
explicit pin to <network>+2 still succeeds.

Verified on Docker 29.4.3 with the same workstation that produced the
original audit:
  - `docker run --rm --network N --ip 10.99.99.2` against a network
    with `--aux-address sencho=10.99.99.2` → "Address already in use"
    (rejects explicit pin).
  - Same `--ip` against a network with `--ip-range 10.99.99.128/25` →
    succeeds (10.99.99.2 is outside the range but inside the subnet).
  - Auto-allocated workload on the IPRange network lands at .129.

Adopt-existing legacy detection now compares IPRange instead of
AuxiliaryAddresses. inspectExistingMeshSubnet returns { subnet,
ipRange } and the warn fires when ipRange differs from the expected
upper-half CIDR. Same once-per-process semantics as before.

createMeshNetwork now derives the IPRange via a new
getMeshIpRangeFromSubnet helper. The five regression tests assert
IPRange = <network>+128/<prefix+1> in the create payload and the
expected/actual IPRange in the legacy-warn details.
2026-05-22 21:08:56 -04:00
Anso 474290081d fix(mesh): log boot state to console so docker logs surfaces mesh status (#1159)
MeshService records its boot summary and setup failures only through
logActivity (in-memory ring buffer + WS listeners), which the Routing
tab consumes. The docker-logs surface was silent for the mesh
subsystem, so operators running Sencho-in-Docker had no boot-time
visibility into whether the data plane came up cleanly.

Mirror the existing logActivity entries to console without replacing
them:

- MeshService.start() success summary: console.log /
  console.warn / console.error gated on the summary level. Format:
  [Mesh] data plane ok, self attached at <ip>, subnet <X>
  [Mesh] data plane unavailable (<reason>: <message>)
- recordSetupFailure: console.warn for the expected dev-mode
  not_in_docker case, console.error for real failures. Format:
  [Mesh] data plane unavailable (<reason>, subnet <X>): <sanitized>

The activity entries that already drive the Routing tab banner stay
intact; the console lines are purely additive for the docker logs
workflow. Two new unit tests assert the failure and not_in_docker
console mirrors fire with the [Mesh] prefix.

Fixes F-5.
2026-05-22 16:30:10 -04:00
Anso e4fe4cfced fix(mesh): address Codex audit findings on F-1 PR (#1158)
- docs(sencho-mesh): split subnet_overlap troubleshooting into env-set
  vs env-unset paths; rewrite the "Customising the mesh subnet" intro
  to describe the candidate list and the adopt-existing behavior.
- backend(MeshService): preserve idempotent 409 handling in the
  explicit-env path. On createNetwork 409 (TOCTOU race against another
  process), re-inspect and treat the race-winner as success when its
  subnet matches the operator's request; subnet_mismatch otherwise.
- frontend(MeshDataPlaneBanner): trim the card variant to a true
  one-line strip (headline only, truncate min-w-0). Full recovery
  hint stays on the Routing tab variant and in docs.
- tests(mesh): add five cases covering the previously untested
  branches — candidate-loop non-overlap bail, adopt-existing with
  unparseable subnet, explicit-env generic createNetwork failure,
  TOCTOU 409 race-winner match, TOCTOU 409 race-winner mismatch.

Architecture map (gitignored per Directive 11) updated locally with
the new useMeshDataPlane hook node and the mesh.dashboardBanner flow
so the local interactive viewer stays accurate.
2026-05-22 13:39:12 -04:00
Anso 1a03cf82af fix(mesh): auto-fallback through candidate subnets when default overlaps (#1156)
The default mesh subnet 172.30.0.0/24 is fully contained in linuxserver/*
default networks (sonarr_default 172.30.0.0/16, etc.), so libnetwork
rejects the IPAM allocation with "Pool overlaps with other one on this
address space" on a typical homelab Docker host. The single hard-coded
default left first-run operators with a silently broken mesh.

MeshService.setupMeshNetwork now resolves the subnet via three paths:

1. Operator-explicit (SENCHO_MESH_SUBNET set): use that subnet, strict.
   Pre-existing sencho_mesh with a different subnet still raises
   subnet_mismatch.
2. Adopt-existing (env unset, sencho_mesh already on the daemon): adopt
   the existing subnet. Docker is the source of truth across restarts.
3. Candidate iteration (env unset, no existing network): walk
   172.30.0.0/24, 172.31.0.0/24, 10.42.0.0/24, 10.43.0.0/24 in order.
   First subnet Docker accepts wins. If every candidate overlaps,
   record subnet_overlap with a message naming every attempt.

The dashboard's Fleet Heartbeat card now surfaces the down state via a
compact banner above the per-node rows, plus a "mesh down" counter
suffix on the right of the title. The existing Routing-tab banner is
extracted into a shared MeshDataPlaneBanner component with tab and
card variants. Dashboard polling is gated on Admiral tier so non-paid
users do not fire the Admiral-only /mesh/status endpoint.

Six new tests in mesh-setup-error-classification cover: iterates past
first overlap, all candidates overlap, adopts existing network,
inspectNetwork non-404 failure classified as attach_failed, env-matches-
existing skip-create, and operator-explicit strict (no fallback).

Fixes F-1 in the pre-1.0 audit. Closes the silent-failure mode that
left the mesh down on the most common homelab Docker layout.
2026-05-22 13:20:27 -04:00
Anso f6e42535c8 fix(mesh): route peer→central traffic over the existing forward WS (#1094)
* fix(mesh): route peer→central traffic over the existing forward WS

The reverse mesh callback path (`/api/mesh/proxy-tunnel-from-peer`) needed
SENCHO_PRIMARY_URL on central plus a publicly reachable origin from the
peer's perspective. In a typical homelab where central sits behind NAT,
peer→central dispatch silently failed at the dialer's short-circuit and
the headline "call any service on any node by hostname" worked one way
only.

The forward WS at `/api/mesh/proxy-tunnel` is already bidirectional end
to end. Make the bridge a persistent control-plane primitive: dial every
mesh-enabled proxy peer at startup, reconcile every 60 s, never idle-close.
Peer→central traffic multiplexes over the same WS via `tcp_open_reverse`.

Removed:
- `meshProxyTunnelFromPeer.ts` WS handler and dispatch
- `MeshCentralRegistry`, `PeerToCentralMeshSessionDialer`
- `mesh_handshake` first-frame state machine in `meshProxyTunnel.ts`
- `maybeSendBootstrap`, `buildHandshakeFrame` in the dialer
- `mesh_proxy_callback_bootstrap` capability and `maybeWarnUnsetPrimaryUrl`
- `mesh_centrals` table (drop migration; greenfield, no users)
- `PilotTunnelManager.replaceOrRegisterProxyBridge` (dead after handler removal)
- twelve associated unit/integration tests plus the peer-recovery branch
  in `MeshService.openCrossNode`

Added:
- `MeshService.proactiveBridgeFanout` selects every mesh-enabled proxy
  peer (no longer gated on `mesh_stacks` rows)
- `startBridgeReconcileLoop` runs the fanout every 60 s (override via
  `SENCHO_MESH_RECONCILE_INTERVAL_MS`)
- `MeshProxyTunnelDialer` default idle TTL is now `0` and exposes
  `isDialing(nodeId)` for the status surface
- `MeshNodeStatus.reverseCallbackStatus` discriminator
  (`connected | connecting | unavailable | not_applicable`) surfaced via
  `/api/mesh/status` and rendered as a pill in the Routing tab
- `openCrossNode` error message distinguishes "no proxy target" from
  "waiting for central to dial the reverse bridge"
- New tests: `mesh-service-proxy-tunnel-reconcile`,
  `mesh-status-reverse-callback`, `mesh-proxy-tunnel-dialer-no-idle-close`

SENCHO_PRIMARY_URL is no longer required for any mesh function.

* fix(mesh): rewrite proxy-tunnel reconcile test contents

The previous commit renamed the file but the rewritten test bodies stayed
unstaged on top of the rename. This commit lands the actual rewrite: the
fanout assertion now requires every mesh-enabled proxy peer to be dialed,
not just those with `mesh_stacks` rows, and adds a reconcile-tick
repeated-call test.
2026-05-17 22:00:31 -04:00
Anso 578cac89da fix(mesh): surface data-plane failures in health, meta, and Routing tab (#1088)
The three previously-silent console.warn paths in MeshService.setupMeshNetwork
now route through a typed recordSetupFailure helper that classifies the
failure (subnet_invalid, subnet_overlap, subnet_mismatch, ip_in_use,
attach_failed, not_in_docker), emits a mesh.disable activity entry at the
matching level (error for real failures, warn for the expected dev-mode
not_in_docker case), and strips mesh_proxy_callback_bootstrap from
advertised capabilities via CapabilityRegistry.

/api/health gains a mesh.dataPlane block carrying the typed status.
/api/mesh/status carries localDataPlane at the top level so the Routing tab
renders a red banner with an operator-actionable recovery hint (set
SENCHO_MESH_SUBNET to a free /24 and recreate the container) when the data
plane is down. The success path re-enables the capability and flips the
status to ok.

Generalizes the previously-documented F-0 failure mode (IP-in-subnet
collision) to also cover the subnet-pool-overlap case where another Docker
bridge on the host already owns the requested CIDR.
2026-05-17 17:31:57 -04:00