Show domains in pool status
This commit is contained in:
+85
-74
@@ -19,11 +19,11 @@ import {
|
||||
parseCookie,
|
||||
tlsDir,
|
||||
ckpoolTlsLoopbackPort,
|
||||
ckpoolPublicTlsLoopbackPort,
|
||||
publicCertPath,
|
||||
endpointPorts,
|
||||
portAssignmentSignature,
|
||||
publicDomains,
|
||||
stratumInternalPort,
|
||||
stratumTlsHostId,
|
||||
stratumPublicTlsHostId,
|
||||
stratumTlsInternalPort,
|
||||
stratumServers,
|
||||
stratumServerUrls,
|
||||
@@ -66,11 +66,68 @@ export const main = sdk.setupMain(async ({ effects }) => {
|
||||
// changes ports — and never on a routine bitcoind update.
|
||||
const bitcoind = await bitcoindBridge(effects)
|
||||
|
||||
// Clearnet domains the user attached to the Stratum (TLS) interface. There
|
||||
// is no config field for this: the domain is added in the StartOS interface
|
||||
// UI, and attaching or removing one restarts main through the same reactive
|
||||
// mechanism as everything else above.
|
||||
const tlsDomains = await publicDomains(effects, stratumTlsHostId).const()
|
||||
// Clearnet domains attached to the Stratum (TLS, Public Domain) interface.
|
||||
// There is no config field for this: the domain is added in the StartOS
|
||||
// interface UI, and attaching or removing one restarts main through the same
|
||||
// reactive mechanism as everything else above.
|
||||
//
|
||||
// Used only to label the connection in the dashboard — the certificates
|
||||
// themselves are the OS's concern now, so this never gates anything starting.
|
||||
const tlsDomains = await publicDomains(
|
||||
effects,
|
||||
stratumPublicTlsHostId,
|
||||
).const()
|
||||
|
||||
// Warn when the OS could not grant a port we asked for.
|
||||
//
|
||||
// `preferredExternalPort` is a request: if the number is already claimed the
|
||||
// OS silently assigns another, and the first symptom is a miner that cannot
|
||||
// connect on the port the config form shows. Surfacing it here turns a
|
||||
// silent substitution into something the user is told about once, naming
|
||||
// both numbers. Pool Status prints the effective ports on demand.
|
||||
//
|
||||
// Read `.const()` so a later reassignment re-fires this; the store field
|
||||
// that de-dupes the warning is read `.once()` and is deliberately absent
|
||||
// from the projection above, so writing it cannot restart the service.
|
||||
const portRequests = await storeJson
|
||||
.read((s) => ({
|
||||
stratum: s.stratumPort,
|
||||
tls: s.stratumTlsPort,
|
||||
publicTls: s.stratumPublicTlsPort,
|
||||
}))
|
||||
.const(effects)
|
||||
if (portRequests) {
|
||||
const ports = await endpointPorts(effects, portRequests, 'const')
|
||||
const mismatched = ports.filter(
|
||||
(p) => p.assigned !== null && p.assigned !== p.requested,
|
||||
)
|
||||
const signature = portAssignmentSignature(ports)
|
||||
const lastNotified = await storeJson
|
||||
.read((s) => s.notifiedPortAssignment)
|
||||
.once()
|
||||
|
||||
if (mismatched.length > 0 && signature !== lastNotified) {
|
||||
await sdk.notification.create(effects, {
|
||||
level: 'warning',
|
||||
title: i18n('Stratum port changed by StartOS'),
|
||||
message: mismatched
|
||||
.map((p) =>
|
||||
i18n('{label}: requested {requested}, assigned {assigned}')
|
||||
.replace('{label}', p.label)
|
||||
.replace('{requested}', String(p.requested))
|
||||
.replace('{assigned}', String(p.assigned)),
|
||||
)
|
||||
.concat(
|
||||
i18n(
|
||||
'The port you asked for was already in use, so StartOS assigned another one. Point your miners at the assigned port, or pick a free one in Configure.',
|
||||
),
|
||||
)
|
||||
.join('\n'),
|
||||
})
|
||||
}
|
||||
if (signature !== lastNotified)
|
||||
await storeJson.merge(effects, { notifiedPortAssignment: signature })
|
||||
}
|
||||
|
||||
// All Kamado processes (kamado-api, ckpool, stunnel) share ONE
|
||||
// subcontainer, mirroring the single 0.3.x container: kamado-api reaches
|
||||
@@ -172,44 +229,21 @@ export const main = sdk.setupMain(async ({ effects }) => {
|
||||
ckpoolConfTemplate,
|
||||
)
|
||||
|
||||
// Pull the OS-managed certificate for each attached public domain. StartOS
|
||||
// provisions these over ACME, so they chain to a public CA and any miner
|
||||
// with a normal root store validates them with nothing pasted in.
|
||||
// Public-domain TLS is StartOS's job, not ours — see the stratum-tls-public
|
||||
// interface in interfaces.ts. The OS terminates ACME-backed TLS and forwards
|
||||
// plaintext into ckpool's third bind, so nothing here fetches, writes or
|
||||
// serves a certificate for a public domain.
|
||||
//
|
||||
// Failures are per-domain and non-fatal: right after a domain is added the
|
||||
// certificate may not be issued yet (DNS still propagating, ACME challenge
|
||||
// pending). Skipping it leaves the rest of the pool running and the Stratum
|
||||
// TLS health check reports the shortfall, rather than a domain typo taking
|
||||
// the whole stratum server down.
|
||||
// This package used to do that itself with sdk.getSslCertificate() plus
|
||||
// stunnel SNI sections, which cannot work: StartOS only provisions ACME
|
||||
// certificates for bindings it terminates TLS for, so a raw TCP binding was
|
||||
// handed no CA-issued certificate to serve and miners got the self-signed
|
||||
// one (mbedtls -0x2700, X509_CERT_VERIFY_FAILED).
|
||||
//
|
||||
// stunnel is therefore left with exactly one job: the self-signed
|
||||
// certificate for miners on the local network.
|
||||
await mkdir(`${kamadoSub.rootfs}${stunnelConfDir}`, { recursive: true })
|
||||
const publicCerts: { fqdn: string; path: string }[] = []
|
||||
for (const fqdn of tlsDomains) {
|
||||
try {
|
||||
const chain = await sdk.getSslCertificate(effects, [fqdn]).const()
|
||||
const key = await sdk.getSslKey(effects, { hostnames: [fqdn] })
|
||||
const path = publicCertPath(fqdn)
|
||||
// stunnel takes the chain and the key from a single file. Concatenating
|
||||
// the whole fullchain means the server presents its intermediates,
|
||||
// which is what lets a miner validate without a pinned copy.
|
||||
await writeFile(
|
||||
`${kamadoSub.rootfs}${path}`,
|
||||
[...chain, key].join('\n'),
|
||||
{ mode: 0o600 },
|
||||
)
|
||||
publicCerts.push({ fqdn, path })
|
||||
console.info(`kamado-tls: serving CA-issued certificate for ${fqdn}`)
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`kamado-tls: no certificate available for ${fqdn} yet — skipping`,
|
||||
e,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// stunnel runs when there is at least one certificate to serve. The two
|
||||
// paths are independent: local TLS is the user's toggle, public TLS follows
|
||||
// whatever domains are attached to the interface.
|
||||
const stunnelEnabled = store.tlsEnabled || publicCerts.length > 0
|
||||
const stunnelEnabled = store.tlsEnabled
|
||||
|
||||
// stunnel.conf is rendered here rather than shipped as a static asset so it
|
||||
// stays next to the ports it references. `accept` is the fixed in-container
|
||||
@@ -226,19 +260,6 @@ export const main = sdk.setupMain(async ({ effects }) => {
|
||||
// tags the two paths with different serverurl indices, which is how the
|
||||
// dashboard's lock badge can name the certificate in use.
|
||||
if (stunnelEnabled) {
|
||||
// Local TLS owns the primary service when it's on. With it off, the first
|
||||
// public certificate takes over as the default so the port still answers
|
||||
// a no-SNI client (encrypted, just not verifiable against a bare IP).
|
||||
const primary = store.tlsEnabled
|
||||
? { cert: `${tlsDir}/stratum.pem`, connect: ckpoolTlsLoopbackPort }
|
||||
: {
|
||||
cert: publicCerts[0].path,
|
||||
connect: ckpoolPublicTlsLoopbackPort,
|
||||
}
|
||||
// Whichever domain was promoted to primary must not also appear as a
|
||||
// secondary — stunnel would be routing an SNI name to itself.
|
||||
const sniCerts = store.tlsEnabled ? publicCerts : publicCerts.slice(1)
|
||||
|
||||
const stunnelConf = [
|
||||
'foreground = yes',
|
||||
'pid =',
|
||||
@@ -259,20 +280,12 @@ export const main = sdk.setupMain(async ({ effects }) => {
|
||||
'',
|
||||
'[stratum]',
|
||||
`accept = 0.0.0.0:${stratumTlsInternalPort}`,
|
||||
`connect = 127.0.0.1:${primary.connect}`,
|
||||
`cert = ${primary.cert}`,
|
||||
`connect = 127.0.0.1:${ckpoolTlsLoopbackPort}`,
|
||||
`cert = ${tlsDir}/stratum.pem`,
|
||||
// No client-cert auth — stratum over TLS is opportunistic encryption;
|
||||
// the stratum protocol layer handles miner auth via username.
|
||||
'verify = 0',
|
||||
'',
|
||||
...sniCerts.flatMap(({ fqdn, path }) => [
|
||||
`[stratum-${fqdn}]`,
|
||||
`sni = stratum:${fqdn}`,
|
||||
`connect = 127.0.0.1:${ckpoolPublicTlsLoopbackPort}`,
|
||||
`cert = ${path}`,
|
||||
'verify = 0',
|
||||
'',
|
||||
]),
|
||||
].join('\n')
|
||||
|
||||
await writeFile(
|
||||
@@ -325,9 +338,7 @@ export const main = sdk.setupMain(async ({ effects }) => {
|
||||
// the lock badge can name the certificate a miner is using instead
|
||||
// of assuming a bind order. Labels only mention domains we actually
|
||||
// managed to load a certificate for.
|
||||
STRATUM_SERVERS: JSON.stringify(
|
||||
stratumServers(publicCerts.map((c) => c.fqdn)),
|
||||
),
|
||||
STRATUM_SERVERS: JSON.stringify(stratumServers(tlsDomains)),
|
||||
},
|
||||
},
|
||||
ready: {
|
||||
@@ -492,10 +503,10 @@ export const main = sdk.setupMain(async ({ effects }) => {
|
||||
},
|
||||
),
|
||||
},
|
||||
// The self-signed generator only runs when local TLS is on; with
|
||||
// only public certificates configured there is no 'tls-cert'
|
||||
// oneshot to wait for (they were written during setup above).
|
||||
requires: store.tlsEnabled ? ['tls-cert'] : ['dirs'],
|
||||
// stunnel now runs only when local TLS is on, and that is exactly
|
||||
// when the self-signed certificate it serves is generated — so the
|
||||
// oneshot is always the dependency.
|
||||
requires: ['tls-cert'],
|
||||
}
|
||||
: null,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user