Skip to content

v0.4.0 — Reverse-SSH transport + close-path robustness

Latest

Choose a tag to compare

@linucs linucs released this 26 Apr 12:06

Lockstep release of @raasimpact/arduino-uno-q-bridge@0.4.0 and n8n-nodes-uno-q@0.4.0. Adds a fourth transport — reverse-SSH (Variant B) — for NAT-ed Qs that can dial out but can't accept inbound connections. Ships alongside a robustness fix in Bridge.close() that prevents MCU code from hanging when an n8n bridge closes with router-forwarded requests in flight.

The Q-side autossh container ships separately under deploy/relay-ssh/ (deploy tooling shipped 2026-04-25, n8n-side this release).

@raasimpact/arduino-uno-q-bridge 0.3.0 → 0.4.0

Added

  • SshTransport — fourth transport, sitting on top of an externally-supplied Duplex stream rather than dialing a socket itself. The Variant B deployment routes each device through the n8n-side singleton (master plan §14); the bridge just receives the forwardOut channel for one specific device. Construct with new SshTransport({ connect: () => Promise<Duplex> }) and pass it via Bridge.connect({ transportInstance }).
  • 'ssh' discriminant on TransportDescriptor with listenAddress, listenPort, deviceNick (the cert KeyID — the only routing key on the n8n side per §14.4). The factory throws if a caller tries to construct an SSH transport from descriptor alone — the Duplex must come from the singleton, so transportInstance is mandatory for this kind.

Changed

  • Bridge.close() now drains in-flight router-forwarded requests before tearing down the transport. Each pending provide handler invocation gets an explicit [1, "bridge closing while handling <method>"] error response written to the wire, so any caller blocked on a synchronous reply (notably an MCU executing Bridge.call(...) inside its loop()) unblocks instead of hanging forever. The handler's own response writes use the in-flight Map's delete as a CAS guard to avoid double-sends.
  • Bridge.close() now sends \$/reset to the router (with a 500 ms cap so a slow router doesn't block close) to drop every method this connection registered. Without this, the router kept routing for a dead socket; the next caller for one of those methods would either hang or surface a transport-layer error rather than a clean method not available. Sent unconditionally because callers (tests, drift recovery paths) may have mutated providers directly, so the local view ≠ router-side truth.

n8n-nodes-uno-q 0.3.0 → 0.4.0

Added

  • Reverse-SSH transport mode on the Arduino UNO Q Router credential. Selectable from the same dropdown as the Unix / TCP / mTLS variants. Configuration: a listen address + port the n8n side binds, the host private key the embedded SSH server presents, the user CA public key it trusts, and a Required principal (defaults to tunnel). Routing is by user-cert KeyID — the only n8n-side routing key per master plan §14.4.
  • Process-singleton SshServer that accepts inbound connections from Q autossh clients, validates user certs against a single CA, evicts zombie reconnects when a new client claims the same KeyID, and exposes connect(deviceNick) to nodes — opens a forwarded-tcpip channel back through the SSH session and returns the Duplex for SshTransport to wrap. Like BridgeManager, stashes itself on globalThis under a Symbol.for(...) key so each esbuild bundle of a node file shares the same instance at process scope.
  • sshCertParser — manual OpenSSH user-cert parser + ed25519 signature verification, written because ssh2 v1.17 doesn't surface user certs through its parseKey API. Validates KeyID, principals, validity window, extensions (permit-port-forwarding required), and rejects any unrecognised critical option per the OpenSSH spec.
  • Test Connection for the SSH transport runs the standard \$/version round-trip end-to-end through the spawned forward, surfacing transport-specific failures (no device registered, cert rejected, host key mismatch) as legible messages at credential save rather than at first execution.
  • BridgeManager connection-pool entry for the 'ssh' descriptor kind — keyed by (listenAddress, listenPort, deviceNick) so the same Q reached from two nodes shares one Bridge, while two Qs on the same listener share the listener but get separate Bridges. The SshServer is brought up lazily when the first SSH-mode credential acquires, and torn down when the last reference drops.

Changed

  • Bumped @raasimpact/arduino-uno-q-bridge to ^0.4.0 — required for SshTransport. The same bump brings in the close-time drain + \$/reset behaviour that prevents MCU code from hanging when a bridge closes with router-forwarded requests in flight (relevant whenever a workflow saves, an agent deactivates, or n8n restarts mid-call).

This release was validated against a real UNO Q across all four transport variants (unix, tcp, mtls, ssh) plus the arduino-cloud suite — 56/56 integration assertions pass, and a post-suite probe confirms the MCU's loop() stays unwedged, which it didn't reliably before the close-path fix. The same orchestrator (./scripts/run-integration.sh) is now part of the repo for future releases.