@le-space/rootfs 0.6.38 → 0.6.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/rootfs",
3
- "version": "0.6.38",
3
+ "version": "0.6.40",
4
4
  "description": "Shared rootfs contract parsing, reference profile assets, and build helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -120,7 +120,15 @@ write_caddyfile() {
120
120
  https://${hostname} {
121
121
  tls {
122
122
  issuer acme {
123
- disable_tlsalpn_challenge
123
+ # Solve the ACME challenge over TLS-ALPN-01 on :443, NOT HTTP-01.
124
+ # HTTP-01 needs a plaintext listener on :80, but the guest's setup
125
+ # server frees :80 after configuration and Caddy only binds :443, so
126
+ # Let's Encrypt's HTTP-01 probe gets a 503 and no cert is ever issued —
127
+ # browsers then dial wss://.../443 and fail with ERR_SSL_PROTOCOL_ERROR.
128
+ # :443 is already bound and CRN-port-mapped, so TLS-ALPN-01 succeeds
129
+ # (verified live: cert issued in ~45 s). Do NOT flip this back to
130
+ # disable_tlsalpn_challenge — that was b8b8c53 and it broke provisioning.
131
+ disable_http_challenge
124
132
  }
125
133
  }
126
134
 
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
  import json
3
3
  import os
4
+ import socket
5
+ import ssl
4
6
  import time
5
7
  import urllib.error
6
8
  import urllib.request
@@ -8,11 +10,39 @@ import urllib.request
8
10
 
9
11
  ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/orbitdb-relay")
10
12
  METRICS_PORT = int(os.environ.get("METRICS_PORT", "9090"))
13
+ CADDY_HTTPS_PORT = int(os.environ.get("CADDY_HTTPS_PORT", "443"))
11
14
  WAIT_TIMEOUT_SECONDS = int(os.environ.get("DESCRIBE_WAIT_TIMEOUT_SECONDS", "240"))
12
15
  WAIT_INTERVAL_SECONDS = float(os.environ.get("DESCRIBE_WAIT_INTERVAL_SECONDS", "2"))
13
16
  AUTOTLS_EXTRA_WAIT_SECONDS = int(os.environ.get("DESCRIBE_AUTOTLS_EXTRA_WAIT_SECONDS", "120"))
14
17
 
15
18
 
19
+ def caddy_cert_serves(hostname: str) -> bool:
20
+ """True only once Caddy actually serves a publicly-trusted TLS certificate
21
+ for `hostname`.
22
+
23
+ Mode-B failure: describe.py used to advertise the 2n6 proxy-wss address as
24
+ soon as PROXY_HOSTNAME was configured, but Caddy acquires the Let's Encrypt
25
+ certificate asynchronously *after* it (re)starts (the caddy-ready file only
26
+ marks the restart). Browsers dialing before the cert is live got
27
+ net::ERR_SSL_PROTOCOL_ERROR. Verify the real browser-facing certificate with
28
+ a TLS handshake to the local Caddy (127.0.0.1:443) using the public
29
+ hostname as SNI — done locally so hairpin-NAT to the VM's own public IP is
30
+ irrelevant, and validated against the system CA store so Caddy's internal
31
+ self-signed fallback (served before ACME completes) is rejected.
32
+ """
33
+ if not hostname:
34
+ return False
35
+ context = ssl.create_default_context()
36
+ try:
37
+ with socket.create_connection(("127.0.0.1", CADDY_HTTPS_PORT), timeout=8) as raw:
38
+ with context.wrap_socket(raw, server_hostname=hostname) as tls:
39
+ # A validated peer cert here means the handshake a browser will
40
+ # perform to wss://<hostname>:443 succeeds too.
41
+ return bool(tls.getpeercert())
42
+ except (ssl.SSLError, OSError, ValueError):
43
+ return False
44
+
45
+
16
46
  def parse_env_file(path: str) -> dict[str, str]:
17
47
  values: dict[str, str] = {}
18
48
  if not os.path.exists(path):
@@ -153,9 +183,28 @@ def main() -> None:
153
183
  all_multiaddrs = normalize_multiaddrs(multiaddrs_payload)
154
184
  grouped = build_grouped_multiaddrs(env_values, all_multiaddrs, peer_id)
155
185
  proxy_hostname = env_values.get("PROXY_HOSTNAME", "").strip()
186
+
187
+ # Only treat the 2n6 proxy-wss address as browser-dialable once
188
+ # Caddy actually serves a valid certificate for it. Until then, drop
189
+ # the synthetic (not-yet-serving) proxy address so the relay never
190
+ # advertises an endpoint that would fail a browser's TLS handshake.
191
+ proxy_cert_ready = bool(
192
+ proxy_hostname
193
+ and grouped["proxy_wss_multiaddrs"]
194
+ and caddy_cert_serves(proxy_hostname)
195
+ )
196
+ if grouped["proxy_wss_multiaddrs"] and not proxy_cert_ready:
197
+ proxy_set = set(grouped["proxy_wss_multiaddrs"])
198
+ grouped["browser_bootstrap_multiaddrs"] = [
199
+ addr for addr in grouped["browser_bootstrap_multiaddrs"] if addr not in proxy_set
200
+ ]
201
+
202
+ # Break as soon as a genuinely dialable browser address exists:
203
+ # AutoTLS libp2p.direct (only announced once its cert is live) or a
204
+ # cert-verified 2n6 proxy address.
156
205
  if grouped["autotls_wss_multiaddrs"]:
157
206
  break
158
- if proxy_hostname and grouped["proxy_wss_multiaddrs"] and time.monotonic() - started_at >= AUTOTLS_EXTRA_WAIT_SECONDS:
207
+ if proxy_cert_ready:
159
208
  break
160
209
  if not proxy_hostname and time.monotonic() - started_at >= AUTOTLS_EXTRA_WAIT_SECONDS:
161
210
  break