Show domains in pool status
This commit is contained in:
+145
-16
@@ -40,8 +40,17 @@ export const stratumTlsInternalPort = 3334
|
||||
*/
|
||||
/** 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
|
||||
/**
|
||||
* CA-issued certificate for a StartOS public domain (ACME / Let's Encrypt).
|
||||
*
|
||||
* Unlike the self-signed bind above this one listens on 0.0.0.0, because the
|
||||
* terminator in front of it is StartOS itself rather than our in-container
|
||||
* stunnel: the OS decrypts on the public interface and forwards over the LXC
|
||||
* bridge, which never arrives on loopback. It has to be the OS — StartOS only
|
||||
* provisions ACME certificates for bindings it terminates TLS for, so a raw
|
||||
* TCP binding is offered no certificate authority at all in the UI.
|
||||
*/
|
||||
export const ckpoolPublicTlsPort = 3438
|
||||
|
||||
/**
|
||||
* Defaults for the user-facing *external* ports (see store.json). These are
|
||||
@@ -51,6 +60,8 @@ export const ckpoolPublicTlsLoopbackPort = 3438
|
||||
*/
|
||||
export const defaultStratumPort = 3333
|
||||
export const defaultStratumTlsPort = 3334
|
||||
/** External port for the OS-terminated (CA-issued) TLS endpoint. */
|
||||
export const defaultStratumPublicTlsPort = 3335
|
||||
|
||||
/**
|
||||
* Reject external-port choices that cannot work. Since the user no longer
|
||||
@@ -66,11 +77,20 @@ export const defaultStratumTlsPort = 3334
|
||||
export function validatePorts(opts: {
|
||||
stratumPort: number
|
||||
stratumTlsPort: number
|
||||
stratumPublicTlsPort: number
|
||||
}): string | null {
|
||||
const { stratumPort, stratumTlsPort } = opts
|
||||
const { stratumPort, stratumTlsPort, stratumPublicTlsPort } = opts
|
||||
|
||||
if (stratumPort === stratumTlsPort)
|
||||
return `The stratum port and the stratum TLS port must differ (both are ${stratumPort}).`
|
||||
const claimed: [number, string][] = [
|
||||
[stratumPort, 'Preferred Stratum Port'],
|
||||
[stratumTlsPort, 'Preferred Stratum TLS Port (Local Network)'],
|
||||
[stratumPublicTlsPort, 'Preferred Stratum TLS Port (Public Domain)'],
|
||||
]
|
||||
|
||||
for (let i = 0; i < claimed.length; i++)
|
||||
for (let j = i + 1; j < claimed.length; j++)
|
||||
if (claimed[i][0] === claimed[j][0])
|
||||
return `${claimed[i][1]} and ${claimed[j][1]} must differ (both are ${claimed[i][0]}).`
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -101,7 +121,7 @@ export function stratumServerUrls(): string[] {
|
||||
return [
|
||||
`0.0.0.0:${stratumInternalPort}`,
|
||||
`127.0.0.1:${ckpoolTlsLoopbackPort}`,
|
||||
`127.0.0.1:${ckpoolPublicTlsLoopbackPort}`,
|
||||
`0.0.0.0:${ckpoolPublicTlsPort}`,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -130,10 +150,125 @@ export function stratumServers(publicDomains: string[]): StratumServer[] {
|
||||
]
|
||||
}
|
||||
|
||||
// ── Effective (OS-assigned) external ports ───────────────────────────────────
|
||||
|
||||
/**
|
||||
* What a stratum endpoint is actually reachable on, versus what we asked for.
|
||||
*
|
||||
* `preferredExternalPort` is a request, not a reservation: if the port is
|
||||
* already claimed the OS grants a different one and nothing fails. Every
|
||||
* consumer of that fact reads it here, so the config form can express intent
|
||||
* while the Pool Status action and the mismatch warning report the truth.
|
||||
*/
|
||||
export type EndpointPort = {
|
||||
/** Interface name as it appears in the StartOS UI. */
|
||||
label: string
|
||||
hostId: string
|
||||
/** What the user asked for in Configure. */
|
||||
requested: number
|
||||
/** What the OS granted, or null while the binding has no assignment yet. */
|
||||
assigned: number | null
|
||||
/**
|
||||
* Public domains attached to this binding, with the port each is published
|
||||
* on. That port follows the *requested* value, so it can differ from
|
||||
* `assigned` — the domain and the IP addresses of one interface are not
|
||||
* necessarily reachable on the same number.
|
||||
*/
|
||||
domains: { fqdn: string; port: number }[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve all three stratum endpoints. `mode` picks the read strategy: 'const'
|
||||
* in main (so a port reassignment re-fires it), 'once' in an action.
|
||||
*
|
||||
* The public-domain endpoint is read from `assignedSslPort` — the OS terminates
|
||||
* TLS there, so the port miners connect to is the SSL one; `assignedPort` on
|
||||
* that binding is the decrypted side, which is not published.
|
||||
*/
|
||||
export async function endpointPorts(
|
||||
effects: T.Effects,
|
||||
requested: { stratum: number; tls: number; publicTls: number },
|
||||
mode: 'const' | 'once',
|
||||
): Promise<EndpointPort[]> {
|
||||
const specs: {
|
||||
label: string
|
||||
hostId: string
|
||||
internalPort: number
|
||||
requested: number
|
||||
ssl: boolean
|
||||
}[] = [
|
||||
{
|
||||
label: 'Stratum',
|
||||
hostId: stratumHostId,
|
||||
internalPort: stratumInternalPort,
|
||||
requested: requested.stratum,
|
||||
ssl: false,
|
||||
},
|
||||
{
|
||||
label: 'Stratum (TLS, Local Network)',
|
||||
hostId: stratumTlsHostId,
|
||||
internalPort: stratumTlsInternalPort,
|
||||
requested: requested.tls,
|
||||
ssl: false,
|
||||
},
|
||||
{
|
||||
label: 'Stratum (TLS, Public Domain)',
|
||||
hostId: stratumPublicTlsHostId,
|
||||
internalPort: ckpoolPublicTlsPort,
|
||||
requested: requested.publicTls,
|
||||
ssl: true,
|
||||
},
|
||||
]
|
||||
|
||||
return Promise.all(
|
||||
specs.map(async (spec) => {
|
||||
const watch = sdk.host.getOwn(effects, spec.hostId, (host) => {
|
||||
const binding = host?.bindings[spec.internalPort]
|
||||
const net = binding?.net
|
||||
// Public domains are published on the port the binding *requested*
|
||||
// (addSsl.preferredExternalPort), not the one the OS assigned — so a
|
||||
// domain and an IP address on the same interface can advertise
|
||||
// different ports. Read the domain entries rather than deriving them.
|
||||
const domains = (binding?.addresses.available ?? [])
|
||||
.filter((a) => a.metadata.kind === 'public-domain' && a.port !== null)
|
||||
.map((a) => ({ fqdn: a.hostname, port: a.port as number }))
|
||||
return {
|
||||
assigned:
|
||||
(spec.ssl ? net?.assignedSslPort : net?.assignedPort) ?? null,
|
||||
domains,
|
||||
}
|
||||
})
|
||||
const info = mode === 'const' ? await watch.const() : await watch.once()
|
||||
return {
|
||||
label: spec.label,
|
||||
hostId: spec.hostId,
|
||||
requested: spec.requested,
|
||||
assigned: info?.assigned ?? null,
|
||||
domains: info?.domains ?? [],
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stable signature of the current assignment, used to notify about a
|
||||
* mismatch exactly once per distinct outcome rather than on every start.
|
||||
*/
|
||||
export function portAssignmentSignature(ports: EndpointPort[]): string {
|
||||
return ports.map((p) => `${p.hostId}:${p.requested}>${p.assigned}`).join('|')
|
||||
}
|
||||
|
||||
// ── Host ids (the `sdk.MultiHost.of` groups) ─────────────────────────────────
|
||||
export const uiHostId = 'ui'
|
||||
export const stratumHostId = 'stratum'
|
||||
export const stratumTlsHostId = 'stratum-tls'
|
||||
/**
|
||||
* Host for the OS-terminated TLS interface. Separate from stratumTlsHostId
|
||||
* because the two differ in exactly the way StartOS cares about: this one
|
||||
* declares `addSsl`, so the OS owns the certificate and offers Let's Encrypt
|
||||
* when a domain is attached; that one is opaque TCP that stunnel terminates.
|
||||
*/
|
||||
export const stratumPublicTlsHostId = 'stratum-tls-public'
|
||||
|
||||
// ── In-container paths ───────────────────────────────────────────────────────
|
||||
|
||||
@@ -153,19 +288,13 @@ 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.
|
||||
* The config is re-rendered on every main run, so — like ckpool.conf and its
|
||||
* RPC credentials — it lives somewhere ephemeral. The self-signed certificate
|
||||
* it serves is the one thing that stays 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',
|
||||
|
||||
Reference in New Issue
Block a user