Serve Let's Encrypt certificates for stratum TLS on public domains
This commit is contained in:
+119
-8
@@ -29,13 +29,19 @@ export const stratumInternalPort = 3333
|
||||
export const stratumTlsInternalPort = 3334
|
||||
|
||||
/**
|
||||
* ckpool's second, loopback-only stratum bind. stunnel forwards decrypted TLS
|
||||
* traffic here. ckpool tags clients by serverurl index (server == 1 -> TLS),
|
||||
* which the dashboard reads to render a lock icon next to encrypted miners —
|
||||
* no source-IP heuristics needed. The bind is harmless when TLS is disabled
|
||||
* (nothing connects to it).
|
||||
* ckpool's loopback-only stratum binds. stunnel forwards decrypted TLS traffic
|
||||
* to one of these depending on which certificate terminated the connection,
|
||||
* which is how the dashboard tells the two TLS paths apart: ckpool tags every
|
||||
* client with the index of the bind it arrived on, and index -> meaning is
|
||||
* declared to kamado-api via STRATUM_SERVERS (see stratumServers below).
|
||||
*
|
||||
* Both are bound unconditionally, even when nothing is listening in front of
|
||||
* them. That is deliberate — see stratumServerUrls.
|
||||
*/
|
||||
/** Self-signed certificate, for miners on the local network. */
|
||||
export const ckpoolTlsLoopbackPort = 3437
|
||||
/** CA-issued certificate for a StartOS public domain (ACME / Let's Encrypt). */
|
||||
export const ckpoolPublicTlsLoopbackPort = 3438
|
||||
|
||||
/**
|
||||
* Defaults for the user-facing *external* ports (see store.json). These are
|
||||
@@ -51,20 +57,79 @@ export const defaultStratumTlsPort = 3334
|
||||
* picks any container-side port, the only real conflict left is asking for the
|
||||
* same external port twice. Returns a human-readable reason, or null when the
|
||||
* pair is usable.
|
||||
*
|
||||
* Deliberately not conditional on the local-TLS toggle: the TLS interface is
|
||||
* bound unconditionally now (so a public domain can be attached to it), which
|
||||
* means the two external ports always collide if they match — even with the
|
||||
* toggle off.
|
||||
*/
|
||||
export function validatePorts(opts: {
|
||||
stratumPort: number
|
||||
stratumTlsPort: number
|
||||
tlsEnabled: boolean
|
||||
}): string | null {
|
||||
const { stratumPort, stratumTlsPort, tlsEnabled } = opts
|
||||
const { stratumPort, stratumTlsPort } = opts
|
||||
|
||||
if (tlsEnabled && stratumPort === stratumTlsPort)
|
||||
if (stratumPort === stratumTlsPort)
|
||||
return `The stratum port and the stratum TLS port must differ (both are ${stratumPort}).`
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// ── ckpool serverurl[] contract ──────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* How a miner reached the pool. Mirrors state.StratumServer in kamado-api;
|
||||
* the dashboard switches on `kind` to label its lock badge.
|
||||
*/
|
||||
export type StratumServerKind = 'plain' | 'tls-local' | 'tls-public'
|
||||
export type StratumServer = { kind: StratumServerKind; label: string }
|
||||
|
||||
/**
|
||||
* ckpool's serverurl[] array, in a FIXED order, with every entry bound
|
||||
* unconditionally.
|
||||
*
|
||||
* ckpool tags each client with the index of the bind it arrived on, and the
|
||||
* dashboard turns that index into a connection badge. Emitting the array
|
||||
* conditionally (only the binds currently in use) would renumber the indices
|
||||
* whenever the user toggles local TLS or attaches a domain, silently
|
||||
* relabelling every connected miner. Two idle loopback listeners are a much
|
||||
* cheaper price than an index that means different things over time.
|
||||
*
|
||||
* Keep in lockstep with stratumServers() below.
|
||||
*/
|
||||
export function stratumServerUrls(): string[] {
|
||||
return [
|
||||
`0.0.0.0:${stratumInternalPort}`,
|
||||
`127.0.0.1:${ckpoolTlsLoopbackPort}`,
|
||||
`127.0.0.1:${ckpoolPublicTlsLoopbackPort}`,
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* The meaning of each stratumServerUrls() entry, handed to kamado-api as
|
||||
* STRATUM_SERVERS so the dashboard can name the transport on hover instead of
|
||||
* assuming a bind order. `publicDomains` only affects the label text — the
|
||||
* array shape is fixed.
|
||||
*
|
||||
* These strings surface in the (English-only) dashboard, not the StartOS UI,
|
||||
* so they deliberately skip i18n.
|
||||
*/
|
||||
export function stratumServers(publicDomains: string[]): StratumServer[] {
|
||||
return [
|
||||
{ kind: 'plain', label: 'Plaintext — not encrypted' },
|
||||
{
|
||||
kind: 'tls-local',
|
||||
label: 'TLS — self-signed certificate (local network)',
|
||||
},
|
||||
{
|
||||
kind: 'tls-public',
|
||||
label: publicDomains.length
|
||||
? `TLS — CA-issued certificate for ${publicDomains.join(', ')}`
|
||||
: 'TLS — CA-issued certificate for a public domain',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// ── Host ids (the `sdk.MultiHost.of` groups) ─────────────────────────────────
|
||||
export const uiHostId = 'ui'
|
||||
export const stratumHostId = 'stratum'
|
||||
@@ -86,6 +151,21 @@ export const kamadoDataDir = `${kamadoRoot}/data`
|
||||
export const kamadoDbPath = `${kamadoDataDir}/kamado.db`
|
||||
export const tlsDir = `${kamadoRoot}/tls`
|
||||
|
||||
/**
|
||||
* stunnel's config directory, on the subcontainer rootfs rather than a volume.
|
||||
* The OS-managed certificates for public domains are re-fetched from StartOS
|
||||
* on every main run, so — like ckpool.conf and its RPC credentials — they are
|
||||
* written somewhere ephemeral and never persisted. Only the self-signed
|
||||
* certificate lives on the volume, because its fingerprint has to survive
|
||||
* restarts for miners that pin it.
|
||||
*/
|
||||
export const stunnelConfDir = '/etc/stunnel'
|
||||
|
||||
/** Path of the PEM bundle (chain + key) stunnel serves for `fqdn`. */
|
||||
export function publicCertPath(fqdn: string): string {
|
||||
return `${stunnelConfDir}/public-${fqdn.replace(/[^a-zA-Z0-9._-]/g, '_')}.pem`
|
||||
}
|
||||
|
||||
/** Files that make up the persisted stratum TLS certificate (relative to the main volume) */
|
||||
export const tlsVolumeFiles = [
|
||||
'tls/stratum.crt',
|
||||
@@ -184,6 +264,37 @@ export function bridgeAddress(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public (clearnet) domains the user has attached to a host, as a minimal
|
||||
* reactive value — the same pattern as bridgeAddress.
|
||||
*
|
||||
* The user adds these in the StartOS interface UI, so this is the package's
|
||||
* only source of truth for "is there a domain to get a CA-issued certificate
|
||||
* for": no config field to drift out of sync with what the OS actually has,
|
||||
* and adding or removing one heals the service with a restart.
|
||||
*
|
||||
* The watched projection is a sorted, comma-joined *string* rather than an
|
||||
* array on purpose: a fresh array is a new reference on every poll, which
|
||||
* would restart main continuously.
|
||||
*/
|
||||
export function publicDomains(
|
||||
effects: T.Effects,
|
||||
hostId: string,
|
||||
): { const(): Promise<string[]>; once(): Promise<string[]> } {
|
||||
const split = (joined: string | null) =>
|
||||
joined ? joined.split(',').filter(Boolean) : []
|
||||
const watchable = async () =>
|
||||
sdk.host.get(effects, { hostId }, (host) =>
|
||||
Object.keys(host?.publicDomains ?? {})
|
||||
.sort()
|
||||
.join(','),
|
||||
)
|
||||
return {
|
||||
const: async () => split(await (await watchable()).const()),
|
||||
once: async () => split(await (await watchable()).once()),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* bitcoind's RPC and ZMQ-block endpoints over the LXC bridge. Two reactive
|
||||
* bridge-address watches — one per bitcoind host — each chained `.const()`,
|
||||
|
||||
Reference in New Issue
Block a user