Skip to content

fix(start-os): heal a service whose SSL port took its plaintext port's number - #3648

Closed
MattDHill wants to merge 1 commit into
fix/admin-ui-plaintext-portfrom
fix/package-binding-ssl-port-heal
Closed

fix(start-os): heal a service whose SSL port took its plaintext port's number#3648
MattDHill wants to merge 1 commit into
fix/admin-ui-plaintext-portfrom
fix/package-binding-ssl-port-heal

Conversation

@MattDHill

Copy link
Copy Markdown
Member

Stacked on #3638. That PR stops a binding's ssl leg taking the plaintext leg's number; this one repairs the servers where it already happened.

The reported symptom

A user who migrated from 0.3.5.1 found Electrs healthy and fully synced, its Interfaces page showing 192.168.x.x:50001, and Sparrow unable to connect over either that address or the onion. nc reached 50001; openssl s_client against 50001 came back with the StartOS chain. Turning Sparrow's SSL toggle on connected immediately, to electrs/0.11.1, protocol 1.4.

So the binding was answering TLS on the port the package asks to serve plaintext on, and 50002 — the port the package asks for SSL, and the one its instructions name — was never allocated.

Why 50001

The 0.3.x electrs manifest bound one port and had lan-config commented out:

interfaces:
  electrum:
    tor-config:
      port-mapping:
        50001: "50001"
    # lan-config:
    #   443:
    #     ssl: true
    #     internal: 50001

SystemForEmbassy::exportNetwork maps that to {preferredExternalPort: 50001, addSsl: null} (SystemForEmbassy/index.ts:528addSsl is set only from lan-config), so BindInfo::new gave the binding assignedPort: 50001, assignedSslPort: None.

The 0.4 package uses the same host id (electrum) and the same internal port, so installing it reached BindInfo::update, not new. There carried moved the lone held number to whichever leg reclaimed first — the ssl leg:

before: {assigned_port: 50001, assigned_ssl_port: None}
after:  {assigned_port: <ephemeral>, assigned_ssl_port: 50001}

update then prefers the port it already holds, so every later rebind kept it. Same shape #3638 measured from the admin binding's side, reached from the other direction.

Any 0.3.x service that bound over Tor only and whose 0.4 package adds addSsl lands here; electrs is the one that was reported.

What this adds

v0_4_0_2::up already re-homes the admin binding. It now also walks packageData.*.hosts.*.bindings.* and puts both legs of a displaced binding back on their preferred ports.

A binding is displaced when it has addSsl, is not one we rewrap (secure.ssl), and its assignedSslPort equals options.preferredExternalPort while addSsl.preferredExternalPort is some other number. That last clause is what keeps a package that deliberately asks for one number on both legs out of scope.

availablePorts moves with it: the freed ephemeral port is dropped, the reclaimed number flips to non-ssl, and the ssl leg's preferred port is added. A binding whose ssl leg's preferred port is held by anything else is left alone rather than moved onto a port another binding is serving — including a port claimed by a binding re-homed earlier in the same walk, which is what two Electrum servers both preferring 50002 would hit.

Effect on saved addresses

An affected service's endpoint moves from :50001 to :50002. It has to: the plaintext leg is not published on an insecure LAN gateway for a secure: null binding (net/host/mod.rs:148), so :50001 was the only address shown, and it was the wrong one. Wallets pinned to :50001 need repointing once — but they needed SSL switched on to work there at all, so no configuration that was working stops working.

No down step: earlier versions reclaim the two ports a re-homed binding holds rather than carrying either, which is the shape carried left alone.

🤖 Generated with Claude Code

Dropping `carried` stops a binding's ssl leg taking the plaintext leg's
number, but `update` prefers the port it already holds, so a binding
already in that state never heals.

A 0.3.x service whose manifest carried no `lan-config` came through
`SystemForEmbassy::exportNetwork` as a single plaintext binding — electrs,
whose `lan-config` was commented out, is the reported case. Installing the
0.4 package over it reached `update` on the same host and internal port,
where `carried` handed that lone number to the ssl leg and pushed the
plaintext leg onto a fresh ephemeral port. The address wallets connect to
then served TLS on the port the package asked to serve plaintext on:
electrs answering TLS on 50001 rather than 50002, with 50002 never
allocated at all, so Sparrow could not reach it without SSL switched on.

v0_4_0_2 now also walks the package bindings and puts both legs back on
their preferred ports, leaving a binding alone when its ssl leg's preferred
port is held elsewhere rather than moving it onto a port another binding
is serving.
@MattDHill

Copy link
Copy Markdown
Member Author

Closing — this is the wrong fix.

An assigned external port must never change automatically. Integrations depend on it, so once a binding holds a port it keeps it, even when the preferred port is or later becomes free; only uninstall and reinstall may reassign. (A user-driven port change may be worth a feature later, but it would still never happen on its own.)

This migration rewrote assignedSslPort and assignedPort on existing package bindings, which is exactly that. I reasoned about whether the move was safe for saved wallet addresses and never asked whether the move was permitted at all.

The prevention half in #3638 stands and is sufficient going forward: with carried gone, each leg reclaims the port it already holds before considering its preferred one, so nothing moves.

So there is no OS-side repair for servers already in this state. A 0.3.5.1-migrated electrs keeps serving TLS on 50001 permanently, and that is the intended outcome — reinstalling the service is the only thing that reassigns. The user-facing problem was never the port number anyway; it was that the Interfaces page rendered a scheme-less host:port with nothing to say the endpoint was TLS, so wallets failed with a generic timeout. That is fixed in the package: Start9-Community/electrs-startos gains schemeOverride: { ssl: 'ssl', noSsl: 'tcp' } so addresses render ssl://, and its docs stop naming a literal external port, since the real one is per-server and fixed for the life of the install.

@MattDHill MattDHill closed this Aug 9, 2026
@MattDHill
MattDHill deleted the fix/package-binding-ssl-port-heal branch August 9, 2026 16:24
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