@le-space/rootfs 0.6.40 → 0.6.42
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 +1 -1
- package/reference/orbitdb-relay/rootfs/orbitdb-relay-bootstrap-refresh.py +108 -20
- package/reference/orbitdb-relay/rootfs/orbitdb-relay-describe.py +65 -28
- package/reference/uc-go-peer/rootfs/__pycache__/uc-go-peer-describe.cpython-312.pyc +0 -0
- package/reference/uc-go-peer/rootfs/uc-go-peer-bootstrap-refresh.py +91 -20
- package/reference/uc-go-peer/rootfs/uc-go-peer-describe.py +78 -3
- package/reference/uc-go-peer/rootfs/__pycache__/uc-go-peer-describe.cpython-310.pyc +0 -0
- package/reference/uc-go-peer/rootfs/__pycache__/uc-go-peer-describe.cpython-314.pyc +0 -0
package/package.json
CHANGED
|
@@ -24,6 +24,20 @@ ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/orbitdb-relay")
|
|
|
24
24
|
DESCRIBE_SCRIPT = os.environ.get(
|
|
25
25
|
"DESCRIBE_SCRIPT", "/usr/local/sbin/orbitdb-relay-describe.py"
|
|
26
26
|
)
|
|
27
|
+
# The initial registration publish (triggered once by configure.sh) must not
|
|
28
|
+
# give up before the browser path is dialable: Caddy's ACME can only complete
|
|
29
|
+
# after the 2n6 DNS record propagates publicly (observed up to ~6-10 min under
|
|
30
|
+
# load), while a single describe run waits at most its own 240 s window. The
|
|
31
|
+
# next timer firing is 20 min away, so a one-shot publish leaves a dead zone
|
|
32
|
+
# in which the registration advertises no browser-dialable address. Retry
|
|
33
|
+
# describe+publish until a secure browser address exists or this deadline
|
|
34
|
+
# passes (then publish whatever is available, matching the old behaviour).
|
|
35
|
+
PUBLISH_RETRY_TIMEOUT_SECONDS = int(
|
|
36
|
+
os.environ.get("BOOTSTRAP_PUBLISH_RETRY_TIMEOUT_SECONDS", "900")
|
|
37
|
+
)
|
|
38
|
+
PUBLISH_RETRY_INTERVAL_SECONDS = int(
|
|
39
|
+
os.environ.get("BOOTSTRAP_PUBLISH_RETRY_INTERVAL_SECONDS", "15")
|
|
40
|
+
)
|
|
27
41
|
DEFAULT_API_HOST = os.environ.get("ALEPH_BOOTSTRAP_API_HOST", "https://api.aleph.im")
|
|
28
42
|
DEFAULT_CHANNEL = os.environ.get("ALEPH_BOOTSTRAP_CHANNEL", "simple-todo")
|
|
29
43
|
DEFAULT_REF = os.environ.get("ALEPH_BOOTSTRAP_REF", "simple-todo-bootstrap")
|
|
@@ -78,6 +92,20 @@ def is_browser_dialable(addr: str) -> bool:
|
|
|
78
92
|
)
|
|
79
93
|
|
|
80
94
|
|
|
95
|
+
def is_secure_browser_dialable(addr: str) -> bool:
|
|
96
|
+
"""Mirror of the consumer-side address policy (testkit/app): a secure
|
|
97
|
+
websocket, or webtransport/webrtc-direct carrying a certhash. Plain /ws
|
|
98
|
+
does not count — an HTTPS page cannot dial it."""
|
|
99
|
+
normalized = addr.lower()
|
|
100
|
+
if "/tls/ws" in normalized or "/wss" in normalized:
|
|
101
|
+
return True
|
|
102
|
+
if (
|
|
103
|
+
"/webtransport" in normalized or "/webrtc-direct" in normalized
|
|
104
|
+
) and "/certhash/" in normalized:
|
|
105
|
+
return True
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
81
109
|
def is_public_multiaddr(addr: str) -> bool:
|
|
82
110
|
parts = split_multiaddr(addr)
|
|
83
111
|
if not parts:
|
|
@@ -603,6 +631,62 @@ def main() -> None:
|
|
|
603
631
|
)
|
|
604
632
|
return
|
|
605
633
|
|
|
634
|
+
deadline = time.monotonic() + PUBLISH_RETRY_TIMEOUT_SECONDS
|
|
635
|
+
result: dict[str, object] | None = None
|
|
636
|
+
while True:
|
|
637
|
+
result = attempt_publish(
|
|
638
|
+
env_values,
|
|
639
|
+
publisher_private_key,
|
|
640
|
+
publisher_address,
|
|
641
|
+
registration_id,
|
|
642
|
+
channel,
|
|
643
|
+
ref,
|
|
644
|
+
post_type,
|
|
645
|
+
api_host,
|
|
646
|
+
require_secure_browser_dialable=True,
|
|
647
|
+
)
|
|
648
|
+
if result is not None:
|
|
649
|
+
break
|
|
650
|
+
if time.monotonic() >= deadline:
|
|
651
|
+
# Deadline passed without a secure browser address — publish what
|
|
652
|
+
# exists anyway (probe addresses still serve the Actions path).
|
|
653
|
+
result = attempt_publish(
|
|
654
|
+
env_values,
|
|
655
|
+
publisher_private_key,
|
|
656
|
+
publisher_address,
|
|
657
|
+
registration_id,
|
|
658
|
+
channel,
|
|
659
|
+
ref,
|
|
660
|
+
post_type,
|
|
661
|
+
api_host,
|
|
662
|
+
require_secure_browser_dialable=False,
|
|
663
|
+
)
|
|
664
|
+
break
|
|
665
|
+
time.sleep(PUBLISH_RETRY_INTERVAL_SECONDS)
|
|
666
|
+
|
|
667
|
+
if result is None:
|
|
668
|
+
print(json_dumps({"status": "skipped", "reason": "no public browser multiaddrs"}))
|
|
669
|
+
return
|
|
670
|
+
print(json_dumps(result))
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def attempt_publish(
|
|
674
|
+
env_values: dict[str, str],
|
|
675
|
+
publisher_private_key: str,
|
|
676
|
+
publisher_address: str,
|
|
677
|
+
registration_id: str | None,
|
|
678
|
+
channel: str,
|
|
679
|
+
ref: str,
|
|
680
|
+
post_type: str,
|
|
681
|
+
api_host: str,
|
|
682
|
+
require_secure_browser_dialable: bool,
|
|
683
|
+
) -> dict[str, object] | None:
|
|
684
|
+
"""One describe→publish cycle. Returns the result record when a
|
|
685
|
+
registration was published, or None when the caller should retry (no
|
|
686
|
+
public addresses yet, or — while `require_secure_browser_dialable` — none
|
|
687
|
+
of them is dialable from an HTTPS page). Nothing is published on the
|
|
688
|
+
retry path: an early registration without a browser-dialable address
|
|
689
|
+
makes consumers resolve too soon and fail."""
|
|
606
690
|
describe = subprocess.run([DESCRIBE_SCRIPT], check=True, capture_output=True, text=True)
|
|
607
691
|
metadata = json.loads(describe.stdout.strip() or "{}")
|
|
608
692
|
peer_id = metadata.get("peer_id")
|
|
@@ -614,14 +698,22 @@ def main() -> None:
|
|
|
614
698
|
metadata.get("browser_bootstrap_multiaddrs") or [], browser_only=True
|
|
615
699
|
)
|
|
616
700
|
|
|
701
|
+
now_ms = int(time.time() * 1000)
|
|
617
702
|
now_seconds = now_ms / 1000
|
|
618
703
|
profile = env_values.get("ALEPH_BOOTSTRAP_PROFILE", DEFAULT_PROFILE).strip() or DEFAULT_PROFILE
|
|
619
704
|
version = env_values.get("ALEPH_BOOTSTRAP_VERSION", "").strip() or None
|
|
620
705
|
published_multiaddrs = select_compact_multiaddrs(browser_multiaddrs or multiaddrs)
|
|
706
|
+
# Intentionally left empty: consumers read content.multiaddrs (fallback),
|
|
707
|
+
# and the relayProof signature is byte-order-sensitive — populating
|
|
708
|
+
# browserMultiaddrs would change the signed payload shape. Cadence, not
|
|
709
|
+
# wire format, is what this retry loop fixes.
|
|
621
710
|
published_browser_multiaddrs: list[str] = []
|
|
622
711
|
if not published_multiaddrs:
|
|
623
|
-
|
|
624
|
-
|
|
712
|
+
return None
|
|
713
|
+
if require_secure_browser_dialable and not any(
|
|
714
|
+
is_secure_browser_dialable(addr) for addr in published_multiaddrs
|
|
715
|
+
):
|
|
716
|
+
return None
|
|
625
717
|
|
|
626
718
|
owner_authorization = load_owner_authorization(
|
|
627
719
|
env_values,
|
|
@@ -684,24 +776,20 @@ def main() -> None:
|
|
|
684
776
|
api_host, publisher_address, publisher_private_key, previous_hashes, channel
|
|
685
777
|
)
|
|
686
778
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
"response": response,
|
|
702
|
-
}
|
|
703
|
-
)
|
|
704
|
-
)
|
|
779
|
+
return {
|
|
780
|
+
"status": "published",
|
|
781
|
+
"httpStatus": http_status,
|
|
782
|
+
"itemHash": unsigned_message["item_hash"],
|
|
783
|
+
"sender": publisher_address,
|
|
784
|
+
"peerId": peer_id,
|
|
785
|
+
"publishedMultiaddrs": published_multiaddrs,
|
|
786
|
+
"publishedBrowserMultiaddrs": published_browser_multiaddrs or published_multiaddrs,
|
|
787
|
+
"forgottenHashes": previous_hashes,
|
|
788
|
+
"forgetStaleOlderThanSeconds": STALE_RECORD_MAX_AGE_SECONDS,
|
|
789
|
+
"ownerAuthorizationPresent": owner_authorization is not None,
|
|
790
|
+
"forgetResponse": forget_response[1] if forget_response else None,
|
|
791
|
+
"response": response,
|
|
792
|
+
}
|
|
705
793
|
|
|
706
794
|
|
|
707
795
|
if __name__ == "__main__":
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
|
+
import re
|
|
4
5
|
import socket
|
|
5
6
|
import ssl
|
|
6
7
|
import time
|
|
@@ -16,33 +17,58 @@ WAIT_INTERVAL_SECONDS = float(os.environ.get("DESCRIBE_WAIT_INTERVAL_SECONDS", "
|
|
|
16
17
|
AUTOTLS_EXTRA_WAIT_SECONDS = int(os.environ.get("DESCRIBE_AUTOTLS_EXTRA_WAIT_SECONDS", "120"))
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
def
|
|
20
|
-
"""True only once
|
|
21
|
-
for `hostname`.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
20
|
+
def tls_endpoint_serves(hostname: str, port: int) -> bool:
|
|
21
|
+
"""True only once the local listener on `port` serves a publicly-trusted
|
|
22
|
+
TLS certificate for `hostname`.
|
|
23
|
+
|
|
24
|
+
The recurring failure shape ("advertise before the certificate serves"):
|
|
25
|
+
an address is announced as browser-dialable while its Let's Encrypt cert
|
|
26
|
+
is still being acquired, so browsers dialing it fail the TLS handshake
|
|
27
|
+
with net::ERR_SSL_PROTOCOL_ERROR. This bit twice — first for Caddy's 2n6
|
|
28
|
+
proxy (cert acquired asynchronously after the caddy restart), then for
|
|
29
|
+
libp2p AutoTLS (multiaddrs appear before the *.libp2p.direct cert is
|
|
30
|
+
live). Verify the real browser-facing certificate with a TLS handshake to
|
|
31
|
+
the local listener using the public hostname as SNI — done locally so
|
|
32
|
+
hairpin-NAT to the VM's own public IP is irrelevant, and validated
|
|
33
|
+
against the system CA store so any self-signed placeholder (Caddy's
|
|
34
|
+
pre-ACME fallback) is rejected.
|
|
32
35
|
"""
|
|
33
36
|
if not hostname:
|
|
34
37
|
return False
|
|
35
38
|
context = ssl.create_default_context()
|
|
36
39
|
try:
|
|
37
|
-
with socket.create_connection(("127.0.0.1",
|
|
40
|
+
with socket.create_connection(("127.0.0.1", port), timeout=8) as raw:
|
|
38
41
|
with context.wrap_socket(raw, server_hostname=hostname) as tls:
|
|
39
|
-
# A validated peer cert here means the handshake a browser
|
|
40
|
-
# perform
|
|
42
|
+
# A validated peer cert here means the handshake a browser
|
|
43
|
+
# will perform against this hostname succeeds too.
|
|
41
44
|
return bool(tls.getpeercert())
|
|
42
45
|
except (ssl.SSLError, OSError, ValueError):
|
|
43
46
|
return False
|
|
44
47
|
|
|
45
48
|
|
|
49
|
+
def caddy_cert_serves(hostname: str) -> bool:
|
|
50
|
+
return tls_endpoint_serves(hostname, CADDY_HTTPS_PORT)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def autotls_cert_serves(autotls_addrs: list[str]) -> bool:
|
|
54
|
+
"""Verify the AutoTLS *.libp2p.direct certificate actually serves.
|
|
55
|
+
|
|
56
|
+
The libp2p WebSocket listener terminates the AutoTLS certificate itself,
|
|
57
|
+
so a CA-validated handshake to 127.0.0.1:<port> with the libp2p.direct
|
|
58
|
+
hostname as SNI proves what a browser's wss:// dial will experience. Both
|
|
59
|
+
the hostname AND the port must come from the multiaddr — the TLS is
|
|
60
|
+
served on the announced port (e.g. .../tcp/24007/tls/ws/...), NOT on the
|
|
61
|
+
plain-ws upstream Caddy proxies to.
|
|
62
|
+
"""
|
|
63
|
+
for addr in autotls_addrs:
|
|
64
|
+
host_match = re.search(r"/dns[46]/([^/]+)/", addr) or re.search(r"/sni/([^/]+)/", addr)
|
|
65
|
+
port_match = re.search(r"/tcp/(\d+)/", addr)
|
|
66
|
+
if host_match and port_match:
|
|
67
|
+
if tls_endpoint_serves(host_match.group(1), int(port_match.group(1))):
|
|
68
|
+
return True
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
46
72
|
def parse_env_file(path: str) -> dict[str, str]:
|
|
47
73
|
values: dict[str, str] = {}
|
|
48
74
|
if not os.path.exists(path):
|
|
@@ -184,27 +210,38 @@ def main() -> None:
|
|
|
184
210
|
grouped = build_grouped_multiaddrs(env_values, all_multiaddrs, peer_id)
|
|
185
211
|
proxy_hostname = env_values.get("PROXY_HOSTNAME", "").strip()
|
|
186
212
|
|
|
187
|
-
#
|
|
188
|
-
# Caddy
|
|
189
|
-
#
|
|
190
|
-
#
|
|
213
|
+
# Symmetric certificate gate for BOTH browser paths. Whichever of
|
|
214
|
+
# Caddy/2n6 and libp2p AutoTLS finishes its ACME first varies per
|
|
215
|
+
# VM; breaking on the mere *presence* of AutoTLS multiaddrs made
|
|
216
|
+
# the outcome a per-VM coin flip — a fast AutoTLS zone produced an
|
|
217
|
+
# acknowledgement containing only libp2p.direct addresses whose
|
|
218
|
+
# cert was not serving yet (ERR_SSL_PROTOCOL_ERROR in browsers),
|
|
219
|
+
# while the verifiable 2n6 address never made it in. Only
|
|
220
|
+
# cert-verified addresses are advertised, and the wait ends as
|
|
221
|
+
# soon as at least one verified browser path exists.
|
|
191
222
|
proxy_cert_ready = bool(
|
|
192
223
|
proxy_hostname
|
|
193
224
|
and grouped["proxy_wss_multiaddrs"]
|
|
194
225
|
and caddy_cert_serves(proxy_hostname)
|
|
195
226
|
)
|
|
227
|
+
autotls_cert_ready = bool(
|
|
228
|
+
grouped["autotls_wss_multiaddrs"]
|
|
229
|
+
and autotls_cert_serves(grouped["autotls_wss_multiaddrs"])
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
unverified: set[str] = set()
|
|
196
233
|
if grouped["proxy_wss_multiaddrs"] and not proxy_cert_ready:
|
|
197
|
-
|
|
234
|
+
unverified.update(grouped["proxy_wss_multiaddrs"])
|
|
235
|
+
if grouped["autotls_wss_multiaddrs"] and not autotls_cert_ready:
|
|
236
|
+
unverified.update(grouped["autotls_wss_multiaddrs"])
|
|
237
|
+
if unverified:
|
|
198
238
|
grouped["browser_bootstrap_multiaddrs"] = [
|
|
199
|
-
addr
|
|
239
|
+
addr
|
|
240
|
+
for addr in grouped["browser_bootstrap_multiaddrs"]
|
|
241
|
+
if addr not in unverified
|
|
200
242
|
]
|
|
201
243
|
|
|
202
|
-
|
|
203
|
-
# AutoTLS libp2p.direct (only announced once its cert is live) or a
|
|
204
|
-
# cert-verified 2n6 proxy address.
|
|
205
|
-
if grouped["autotls_wss_multiaddrs"]:
|
|
206
|
-
break
|
|
207
|
-
if proxy_cert_ready:
|
|
244
|
+
if proxy_cert_ready or autotls_cert_ready:
|
|
208
245
|
break
|
|
209
246
|
if not proxy_hostname and time.monotonic() - started_at >= AUTOTLS_EXTRA_WAIT_SECONDS:
|
|
210
247
|
break
|
|
Binary file
|
|
@@ -22,6 +22,20 @@ except ImportError as error: # pragma: no cover - runtime dependency
|
|
|
22
22
|
|
|
23
23
|
ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/uc-go-peer")
|
|
24
24
|
DESCRIBE_SCRIPT = os.environ.get("DESCRIBE_SCRIPT", "/usr/local/sbin/uc-go-peer-describe.py")
|
|
25
|
+
# Port of the orbitdb-relay cadence fix (relay-button #75): the initial
|
|
26
|
+
# registration publish is triggered once by configure.sh, while the refresh
|
|
27
|
+
# timer's first firing is 20 minutes away — a one-shot publish that catches
|
|
28
|
+
# the peer before a browser-dialable address exists leaves consumers a dead
|
|
29
|
+
# zone. Retry describe+publish until a secure browser address exists or the
|
|
30
|
+
# deadline passes (then publish whatever is available, the old behaviour).
|
|
31
|
+
# The go peer usually announces certhash-bearing webtransport/webrtc-direct
|
|
32
|
+
# addresses right at startup, so the first attempt normally succeeds.
|
|
33
|
+
PUBLISH_RETRY_TIMEOUT_SECONDS = int(
|
|
34
|
+
os.environ.get("BOOTSTRAP_PUBLISH_RETRY_TIMEOUT_SECONDS", "900")
|
|
35
|
+
)
|
|
36
|
+
PUBLISH_RETRY_INTERVAL_SECONDS = int(
|
|
37
|
+
os.environ.get("BOOTSTRAP_PUBLISH_RETRY_INTERVAL_SECONDS", "15")
|
|
38
|
+
)
|
|
25
39
|
DEFAULT_API_HOST = os.environ.get("ALEPH_BOOTSTRAP_API_HOST", "https://api.aleph.im")
|
|
26
40
|
DEFAULT_CHANNEL = os.environ.get("ALEPH_BOOTSTRAP_CHANNEL", "simple-todo")
|
|
27
41
|
DEFAULT_REF = os.environ.get("ALEPH_BOOTSTRAP_REF", "simple-todo-bootstrap")
|
|
@@ -75,6 +89,20 @@ def is_browser_dialable(addr: str) -> bool:
|
|
|
75
89
|
return "/ws" in normalized or "/wss" in normalized
|
|
76
90
|
|
|
77
91
|
|
|
92
|
+
def is_secure_browser_dialable(addr: str) -> bool:
|
|
93
|
+
"""Mirror of the consumer-side address policy (testkit/app): a secure
|
|
94
|
+
websocket, or webtransport/webrtc-direct carrying a certhash. Plain /ws
|
|
95
|
+
does not count — an HTTPS page cannot dial it."""
|
|
96
|
+
normalized = addr.lower()
|
|
97
|
+
if "/tls/ws" in normalized or "/wss" in normalized:
|
|
98
|
+
return True
|
|
99
|
+
if (
|
|
100
|
+
"/webtransport" in normalized or "/webrtc-direct" in normalized
|
|
101
|
+
) and "/certhash/" in normalized:
|
|
102
|
+
return True
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
|
|
78
106
|
def is_public_multiaddr(addr: str) -> bool:
|
|
79
107
|
parts = split_multiaddr(addr)
|
|
80
108
|
if not parts:
|
|
@@ -567,6 +595,47 @@ def main() -> None:
|
|
|
567
595
|
print(json_dumps({"status": "skipped", "reason": "missing publisher key"}))
|
|
568
596
|
return
|
|
569
597
|
|
|
598
|
+
deadline = time.monotonic() + PUBLISH_RETRY_TIMEOUT_SECONDS
|
|
599
|
+
result: dict[str, object] | None = None
|
|
600
|
+
while True:
|
|
601
|
+
result = attempt_publish(
|
|
602
|
+
env_values,
|
|
603
|
+
publisher_private_key,
|
|
604
|
+
registration_id,
|
|
605
|
+
require_secure_browser_dialable=True,
|
|
606
|
+
)
|
|
607
|
+
if result is not None:
|
|
608
|
+
break
|
|
609
|
+
if time.monotonic() >= deadline:
|
|
610
|
+
# Deadline passed without a secure browser address — publish what
|
|
611
|
+
# exists anyway (probe addresses still serve the Actions path).
|
|
612
|
+
result = attempt_publish(
|
|
613
|
+
env_values,
|
|
614
|
+
publisher_private_key,
|
|
615
|
+
registration_id,
|
|
616
|
+
require_secure_browser_dialable=False,
|
|
617
|
+
)
|
|
618
|
+
break
|
|
619
|
+
time.sleep(PUBLISH_RETRY_INTERVAL_SECONDS)
|
|
620
|
+
|
|
621
|
+
if result is None:
|
|
622
|
+
print(json_dumps({"status": "skipped", "reason": "no public browser multiaddrs"}))
|
|
623
|
+
return
|
|
624
|
+
print(json_dumps(result))
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def attempt_publish(
|
|
628
|
+
env_values: dict[str, str],
|
|
629
|
+
publisher_private_key: str,
|
|
630
|
+
registration_id: str | None,
|
|
631
|
+
require_secure_browser_dialable: bool,
|
|
632
|
+
) -> dict[str, object] | None:
|
|
633
|
+
"""One describe→publish cycle. Returns the result record when a
|
|
634
|
+
registration was published, or None when the caller should retry (no
|
|
635
|
+
public addresses yet, or — while `require_secure_browser_dialable` — none
|
|
636
|
+
of them is dialable from an HTTPS page). Nothing is published on the
|
|
637
|
+
retry path: an early registration without a browser-dialable address
|
|
638
|
+
makes consumers resolve too soon and fail."""
|
|
570
639
|
describe = subprocess.run([DESCRIBE_SCRIPT], check=True, capture_output=True, text=True)
|
|
571
640
|
metadata = json.loads(describe.stdout.strip() or "{}")
|
|
572
641
|
peer_id = metadata.get("peer_id")
|
|
@@ -592,10 +661,16 @@ def main() -> None:
|
|
|
592
661
|
)
|
|
593
662
|
api_host = env_values.get("ALEPH_BOOTSTRAP_API_HOST", DEFAULT_API_HOST).strip() or DEFAULT_API_HOST
|
|
594
663
|
published_multiaddrs = select_compact_multiaddrs(browser_multiaddrs or multiaddrs)
|
|
664
|
+
# Intentionally left empty: consumers read content.multiaddrs (fallback),
|
|
665
|
+
# and the relayProof signature is byte-order-sensitive — populating
|
|
666
|
+
# browserMultiaddrs would change the signed payload shape.
|
|
595
667
|
published_browser_multiaddrs: list[str] = []
|
|
596
668
|
if not published_multiaddrs:
|
|
597
|
-
|
|
598
|
-
|
|
669
|
+
return None
|
|
670
|
+
if require_secure_browser_dialable and not any(
|
|
671
|
+
is_secure_browser_dialable(addr) for addr in published_multiaddrs
|
|
672
|
+
):
|
|
673
|
+
return None
|
|
599
674
|
|
|
600
675
|
owner_authorization = load_owner_authorization(
|
|
601
676
|
env_values,
|
|
@@ -658,24 +733,20 @@ def main() -> None:
|
|
|
658
733
|
api_host, publisher_address, publisher_private_key, previous_hashes, channel
|
|
659
734
|
)
|
|
660
735
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
"response": response,
|
|
676
|
-
}
|
|
677
|
-
)
|
|
678
|
-
)
|
|
736
|
+
return {
|
|
737
|
+
"status": "published",
|
|
738
|
+
"httpStatus": http_status,
|
|
739
|
+
"itemHash": unsigned_message["item_hash"],
|
|
740
|
+
"sender": publisher_address,
|
|
741
|
+
"peerId": peer_id,
|
|
742
|
+
"publishedMultiaddrs": published_multiaddrs,
|
|
743
|
+
"publishedBrowserMultiaddrs": published_browser_multiaddrs or published_multiaddrs,
|
|
744
|
+
"forgottenHashes": previous_hashes,
|
|
745
|
+
"forgetStaleOlderThanSeconds": STALE_RECORD_MAX_AGE_SECONDS,
|
|
746
|
+
"ownerAuthorizationPresent": owner_authorization is not None,
|
|
747
|
+
"forgetResponse": forget_response[1] if forget_response else None,
|
|
748
|
+
"response": response,
|
|
749
|
+
}
|
|
679
750
|
|
|
680
751
|
|
|
681
752
|
if __name__ == "__main__":
|
|
@@ -4,16 +4,58 @@ from __future__ import annotations
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
|
+
import socket
|
|
8
|
+
import ssl
|
|
7
9
|
import subprocess
|
|
8
10
|
import time
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/uc-go-peer")
|
|
12
14
|
SERVICE_NAME = os.environ.get("SERVICE_NAME", "uc-go-peer.service")
|
|
15
|
+
CADDY_HTTPS_PORT = int(os.environ.get("CADDY_HTTPS_PORT", "443"))
|
|
13
16
|
WAIT_TIMEOUT_SECONDS = int(os.environ.get("DESCRIBE_WAIT_TIMEOUT_SECONDS", "240"))
|
|
14
17
|
WAIT_INTERVAL_SECONDS = float(os.environ.get("DESCRIBE_WAIT_INTERVAL_SECONDS", "2"))
|
|
15
18
|
AUTOTLS_EXTRA_WAIT_SECONDS = int(os.environ.get("DESCRIBE_AUTOTLS_EXTRA_WAIT_SECONDS", "120"))
|
|
16
19
|
|
|
20
|
+
|
|
21
|
+
def tls_endpoint_serves(hostname: str, port: int) -> bool:
|
|
22
|
+
"""True only once the local listener on `port` serves a publicly-trusted
|
|
23
|
+
TLS certificate for `hostname`.
|
|
24
|
+
|
|
25
|
+
Port of the orbitdb-relay fix (relay-button #74/#75): announcing a wss
|
|
26
|
+
address before its Let's Encrypt certificate actually serves makes
|
|
27
|
+
browsers fail the TLS handshake with net::ERR_SSL_PROTOCOL_ERROR. The
|
|
28
|
+
handshake runs against the local listener with the public hostname as
|
|
29
|
+
SNI — local, so hairpin-NAT to the VM's own public IP is irrelevant, and
|
|
30
|
+
CA-validated, so self-signed placeholders are rejected.
|
|
31
|
+
"""
|
|
32
|
+
if not hostname:
|
|
33
|
+
return False
|
|
34
|
+
context = ssl.create_default_context()
|
|
35
|
+
try:
|
|
36
|
+
with socket.create_connection(("127.0.0.1", port), timeout=8) as raw:
|
|
37
|
+
with context.wrap_socket(raw, server_hostname=hostname) as tls:
|
|
38
|
+
return bool(tls.getpeercert())
|
|
39
|
+
except (ssl.SSLError, OSError, ValueError):
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def caddy_cert_serves(hostname: str) -> bool:
|
|
44
|
+
return tls_endpoint_serves(hostname, CADDY_HTTPS_PORT)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def autotls_cert_serves(autotls_addrs: list[str]) -> bool:
|
|
48
|
+
"""Verify the AutoTLS *.libp2p.direct certificate actually serves. Host
|
|
49
|
+
AND port come from the multiaddr — TLS is terminated on the announced
|
|
50
|
+
port; every candidate is tried until one verifies."""
|
|
51
|
+
for addr in autotls_addrs:
|
|
52
|
+
host_match = re.search(r"/dns[46]/([^/]+)/", addr) or re.search(r"/sni/([^/]+)/", addr)
|
|
53
|
+
port_match = re.search(r"/tcp/(\d+)/", addr)
|
|
54
|
+
if host_match and port_match:
|
|
55
|
+
if tls_endpoint_serves(host_match.group(1), int(port_match.group(1))):
|
|
56
|
+
return True
|
|
57
|
+
return False
|
|
58
|
+
|
|
17
59
|
PEER_ID_PATTERNS = [
|
|
18
60
|
re.compile(r"PeerID:\s+(\S+)"),
|
|
19
61
|
re.compile(r"Host created with PeerID:\s+(\S+)"),
|
|
@@ -221,9 +263,42 @@ def main() -> None:
|
|
|
221
263
|
|
|
222
264
|
grouped = build_probe_multiaddrs(env_values, peer_id, listening_addrs)
|
|
223
265
|
proxy_hostname = env_values.get("PROXY_HOSTNAME", "").strip()
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
266
|
+
|
|
267
|
+
# Symmetric certificate gate (port of relay-button #74/#75): both wss
|
|
268
|
+
# paths — Caddy/2n6 proxy and libp2p AutoTLS — are only advertised
|
|
269
|
+
# once a CA-validated local TLS handshake proves their certificate
|
|
270
|
+
# serves; breaking on the mere presence of AutoTLS multiaddrs made
|
|
271
|
+
# the outcome a per-VM ACME race. certhash-bearing webtransport /
|
|
272
|
+
# webrtc-direct addresses need no ACME (the hash IS the
|
|
273
|
+
# authentication), so they count as browser-dialable immediately and
|
|
274
|
+
# end the wait on their own — unverified wss addresses join the
|
|
275
|
+
# registration on a later refresh cycle once their cert is live.
|
|
276
|
+
proxy_cert_ready = bool(
|
|
277
|
+
proxy_hostname
|
|
278
|
+
and grouped["proxy_wss_multiaddrs"]
|
|
279
|
+
and caddy_cert_serves(proxy_hostname)
|
|
280
|
+
)
|
|
281
|
+
autotls_cert_ready = bool(
|
|
282
|
+
grouped["autotls_wss_multiaddrs"]
|
|
283
|
+
and autotls_cert_serves(grouped["autotls_wss_multiaddrs"])
|
|
284
|
+
)
|
|
285
|
+
certhash_ready = bool(
|
|
286
|
+
grouped["webtransport_multiaddrs"] or grouped["webrtc_direct_multiaddrs"]
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
unverified: set[str] = set()
|
|
290
|
+
if grouped["proxy_wss_multiaddrs"] and not proxy_cert_ready:
|
|
291
|
+
unverified.update(grouped["proxy_wss_multiaddrs"])
|
|
292
|
+
if grouped["autotls_wss_multiaddrs"] and not autotls_cert_ready:
|
|
293
|
+
unverified.update(grouped["autotls_wss_multiaddrs"])
|
|
294
|
+
if unverified:
|
|
295
|
+
grouped["browser_bootstrap_multiaddrs"] = [
|
|
296
|
+
addr
|
|
297
|
+
for addr in grouped["browser_bootstrap_multiaddrs"]
|
|
298
|
+
if addr not in unverified
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
if proxy_cert_ready or autotls_cert_ready or certhash_ready:
|
|
227
302
|
break
|
|
228
303
|
if not proxy_hostname and time.monotonic() - started_at >= AUTOTLS_EXTRA_WAIT_SECONDS:
|
|
229
304
|
break
|
|
Binary file
|