Serve Let's Encrypt certificates for stratum TLS on public domains

This commit is contained in:
2026-08-01 06:31:15 +03:00
parent 703ca1b559
commit f118c153a0
11 changed files with 369 additions and 95 deletions
+46 -43
View File
@@ -15,14 +15,16 @@ import {
export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => {
// The user's stratum ports are EXTERNAL ports only. Read reactively so
// changing them in the Configure action re-runs this and updates the
// binding — the same mechanism that adds/removes the TLS interface when TLS
// is toggled. The in-container ports stay fixed (see utils.ts), so each
// change updates the existing binding rather than orphaning it.
// binding. The in-container ports stay fixed (see utils.ts), so each change
// updates the existing binding rather than orphaning it.
//
// The TLS toggle is deliberately NOT read here: both stratum interfaces now
// exist unconditionally, so toggling it no longer rebinds anything — it
// only changes which certificates stunnel serves, which is main.ts's job.
const ports = await storeJson
.read((s) => ({
stratum: s.stratumPort,
stratumTls: s.stratumTlsPort,
tlsEnabled: s.tlsEnabled,
}))
.const(effects)
@@ -75,57 +77,58 @@ export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => {
})
receipts.push(await stratumOrigin.export([stratum]))
// TLS stratum (conditional) — stunnel terminates TLS with the persisted
// package-managed certificate and forwards to ckpool's loopback-only
// second bind. The OS sees a raw TCP port; TLS lives at the app layer, so
// the noSsl scheme is deliberately stratum+ssl.
if (ports?.tlsEnabled) {
const tlsMulti = sdk.MultiHost.of(effects, stratumTlsHostId)
const tlsOrigin = await tlsMulti.bindPort(stratumTlsInternalPort, {
protocol: null,
preferredExternalPort: externalStratumTlsPort,
addSsl: null,
secure: { ssl: false },
})
const stratumTls = sdk.createInterface(effects, {
name: i18n('Stratum (TLS)'),
id: 'stratum-tls',
description: i18n(
'TLS-encrypted stratum endpoint (self-signed certificate — see the Stratum TLS Certificate action)',
),
type: 'api',
masked: false,
schemeOverride: { ssl: 'stratum+ssl', noSsl: 'stratum+ssl' },
username: null,
path: '',
query: {},
})
receipts.push(await tlsOrigin.export([stratumTls]))
}
// TLS stratum — stunnel terminates TLS and forwards to one of ckpool's
// loopback-only binds. The OS sees a raw TCP port; TLS lives at the app
// layer, so the noSsl scheme is deliberately stratum+ssl.
//
// This is bound and exported UNCONDITIONALLY, unlike the local-TLS toggle
// that used to gate it. A public domain is attached to an interface in the
// StartOS UI, so gating the interface on the toggle made the Let's Encrypt
// path unreachable for anyone who had not first enabled the self-signed
// one — there was nothing to attach the domain to. main.ts decides whether
// stunnel actually runs; when neither a certificate nor a domain is
// configured the port simply doesn't answer, and the Stratum TLS health
// check says so.
const tlsMulti = sdk.MultiHost.of(effects, stratumTlsHostId)
const tlsOrigin = await tlsMulti.bindPort(stratumTlsInternalPort, {
protocol: null,
preferredExternalPort: externalStratumTlsPort,
addSsl: null,
secure: { ssl: false },
})
const stratumTls = sdk.createInterface(effects, {
name: i18n('Stratum (TLS)'),
id: 'stratum-tls',
description: i18n(
'TLS-encrypted stratum endpoint. Miners on a public domain attached here get a CA-issued certificate automatically; on the local network the self-signed certificate is used (see the Stratum TLS Certificate action)',
),
type: 'api',
masked: false,
schemeOverride: { ssl: 'stratum+ssl', noSsl: 'stratum+ssl' },
username: null,
path: '',
query: {},
})
receipts.push(await tlsOrigin.export([stratumTls]))
// Drop bindings we no longer use — primarily the stratum-tls binding after
// the user disables TLS. With fixed internal ports this no longer has to
// clean up after port changes (the whole point of keeping them fixed), but
// it still matters for the TLS toggle, and it clears orphans left by older
// versions of this package that did move the internal port.
// Drop bindings we no longer use. With fixed internal ports and an
// unconditional TLS binding, all three are now permanent — this only
// clears orphans left by older versions of this package, which did move the
// internal port and did drop the TLS binding when the toggle was off.
await sdk.clearBindings(effects, {
except: [
{ id: uiHostId, internalPort: uiPort },
{ id: stratumHostId, internalPort: stratumInternalPort },
...(ports?.tlsEnabled
? [{ id: stratumTlsHostId, internalPort: stratumTlsInternalPort }]
: []),
{ id: stratumTlsHostId, internalPort: stratumTlsInternalPort },
],
})
// Exported service interfaces are a SECOND registry, independent of the
// bindings above and with its own cleanup effect. Clearing bindings alone
// leaves an orphaned interface record behind, which the UI still lists — so
// a port change can produce two identical "Stratum" rows. Excluding
// 'stratum-tls' when TLS is off also removes that row when the user
// disables TLS, rather than leaving a dead endpoint on display.
// a port change can produce two identical "Stratum" rows.
await effects.clearServiceInterfaces({
except: ['ui', 'stratum', ...(ports?.tlsEnabled ? ['stratum-tls'] : [])],
except: ['ui', 'stratum', 'stratum-tls'],
})
return receipts