Add packaged Insta360 X4 integration and recover paired Node channels

Discover independent camera instances and prepare their versioned runtime
from Node or remote Core. Add isolated SDK workers, camera controls, raw
dual-fisheye WebRTC preview, and shared action/region loading states.

Recover existing Node bindings over known Tailscale addresses after a Core
LAN address change. Preserve identities and trust, pin both peers, migrate
endpoints with revision checks, and require real heartbeats for online status.
Fix the Python client certificate profile for Go X509 verification.

Pin Design Guideline 8c53f73 and retain installer/build/acceptance history.
Node 0.8.19 is installed; X4 0.1.3-3 is bundled but hardware activation is pending.

Validation: qualified DG/Node builds and Go race tests; 31 fleet tests;
Python-to-Go certificate interoperability and live tailnet recovery with five
fresh heartbeats; prior 38 X4 tests and bounded remote WebRTC acceptance.
Clean-OS, replug/power autonomy, local X4 video and long-run stability remain open.
This commit is contained in:
DCCONSTRUCTIONS
2026-09-10 09:21:24 +03:00
parent 54a85fdf50
commit a3c15e11e9
125 changed files with 11916 additions and 251 deletions
+162
View File
@@ -0,0 +1,162 @@
"""Recover only an existing binding over a previously authenticated tailnet address.
No peer discovery, invitation secrets, trust replacement, or provider API. The
Node pins the Core CA public key through mTLS; Core pins the saved Node key.
"""
from __future__ import annotations
import http.client
import ipaddress
import json
import ssl
import time
from cryptography import x509
from cryptography.hazmat.primitives import serialization
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
from .trust import PairingError, atomic_private, endpoint, pem, verify_node_certificate
SCHEMA = "missioncore.node-channel-recovery/v1"
def tailnet_address(value):
try:
return ipaddress.IPv4Address(value) in ipaddress.ip_network("100.64.0.0/10")
except (ValueError, TypeError):
return False
def node_addresses(row):
addresses = set()
for network in (row.get("inventory") or {}).get("networks", [])[:64]:
if not isinstance(network, dict) or not network.get("up"):
continue
for alias in network.get("addresses", [])[:32]:
if isinstance(alias, str) and tailnet_address(alias.split("/")[0]):
addresses.add(alias.split("/")[0])
return sorted(addresses)[:2]
def client_context(trust):
# Keep the pinned Core key, but distinguish the leaf from its issuer.
# Go rejects same-subject/same-SPKI certificates without SAN as a chain loop.
subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Mission Core channel recovery")])
cert = (
trust.builder(subject, trust.key.public_key(), 1)
.issuer_name(trust.ca.subject)
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
.add_extension(x509.ExtendedKeyUsage([ExtendedKeyUsageOID.CLIENT_AUTH]), critical=False)
.sign(trust.key, None)
)
cert_path, key_path = trust.root / "recovery-client.pem", trust.root / "recovery-key.pem"
atomic_private(cert_path, (pem(cert) + pem(trust.ca)).encode())
atomic_private(
key_path,
trust.key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption(),
),
)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.minimum_version = ssl.TLSVersion.TLSv1_3
# Bootstrap certificate is self-signed by the already pinned Node key.
# verify_node_certificate must succeed before any application request.
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
context.load_cert_chain(cert_path, key_path)
return context
def request(connection, path, body):
connection.request("POST", path, json.dumps(body), {"Content-Type": "application/json"})
response = connection.getresponse()
raw = response.read(16385)
if response.status != 200 or len(raw) > 16384:
raise PairingError("БК пока не подтвердил восстановление канала.")
value = json.loads(raw)
if not isinstance(value, dict):
raise PairingError("Некорректное подтверждение канала.")
return value
def recover(registry, row, address, context):
if not tailnet_address(address) or address not in node_addresses(row):
raise PairingError("Нет подтверждённого адреса БК.")
connection = http.client.HTTPSConnection(address, 8781, timeout=4, context=context)
try:
connection.connect()
verify_node_certificate(connection.sock.getpeercert(binary_form=True), row["node_id"])
local = connection.sock.getsockname()[0]
if not tailnet_address(local):
raise PairingError("Обратный адрес Core не относится к частному каналу.")
body = {"schema": SCHEMA, "binding_id": row["binding"]["binding_id"]}
value = request(connection, "/v1/channel/inspect", body)
if (
value.get("schema") != SCHEMA
or value.get("node_id") != row["node_id"]
or value.get("core_id") != registry.trust.core_id
or value.get("binding_id") != body["binding_id"]
or type(value.get("endpoint_revision")) is not int
or not 0 <= value["endpoint_revision"] < 1000000000
):
raise PairingError("Подтверждена другая привязка БК.")
endpoint(value.get("endpoint"), 8782)
with registry.lock:
current = registry.find(row["id"])
if (
registry.stop.is_set()
or current["enrollment"] != "paired"
or current["binding"]["binding_id"] != body["binding_id"]
):
return
registry.listen(local)
desired = f"https://{local}:8782"
if value["endpoint"] != desired:
request(
connection,
"/v1/channel/migrate",
{
**body,
"expected_endpoint": value["endpoint"],
"expected_revision": value["endpoint_revision"],
"endpoint": desired,
},
)
# Only a subsequent authenticated heartbeat proves the new endpoint.
# Lost migration acknowledgements are resolved by inspect, never replay.
finally:
connection.close()
def reconcile(registry):
attempts = {}
context, refresh = None, 0
while not registry.stop.is_set():
with registry.lock:
rows = [r for r in registry.rows() if r["enrollment"] == "paired"]
attempts = {key: value for key, value in attempts.items() if key in {r["id"] for r in rows}}
for row in rows:
if registry.stop.is_set():
return
now = time.monotonic()
if now < attempts.get(row["id"], 0):
continue
attempts[row["id"]] = now + 30
if tailnet_address(row["core_address"]) and (row.get("last_seen") or 0) >= max(
registry.started_at, time.time() - 20
):
continue
for address in node_addresses(row):
try:
if context is None or now >= refresh:
context, refresh = client_context(registry.trust), now + 12 * 3600
recover(registry, row, address, context)
break
except Exception:
# Bounded retry, no untrusted peer data or private addresses in logs.
# The ordinary heartbeat remains the connectivity authority.
continue
registry.stop.wait(2)
+39 -2
View File
@@ -54,6 +54,7 @@ class FleetRegistry:
self.listeners: dict[str, object] = {}
self.stop = threading.Event()
self.worker: threading.Thread | None = None
self.recovery_worker: threading.Thread | None = None
def rows(self):
return [
@@ -86,11 +87,22 @@ class FleetRegistry:
target=self.reconcile, name="mission-core-fleet", daemon=True
)
self.worker.start()
from .recovery import reconcile as recover_channels
self.recovery_worker = threading.Thread(
target=recover_channels,
args=(self,),
name="mission-core-channel-recovery",
daemon=True,
)
self.recovery_worker.start()
def close(self):
self.stop.set()
if self.worker:
self.worker.join(timeout=20)
if self.recovery_worker:
self.recovery_worker.join(timeout=30)
for server in self.listeners.values():
server.shutdown()
server.server_close()
@@ -341,7 +353,7 @@ class FleetRegistry:
self.save(row)
self.stop.wait(2)
def receive(self, certificate: bytes, path: str, value: dict):
def receive(self, certificate: bytes, path: str, value: dict, *, address: str | None = None):
cert = x509.load_der_x509_certificate(certificate)
key = cert.public_key()
if not isinstance(key, Ed25519PublicKey):
@@ -380,6 +392,25 @@ class FleetRegistry:
return 200, {"ok": True}
if path != "/v1/node/heartbeat":
return 404, {"error": "Unknown operation"}
# Endpoint revisions prevent an old in-flight heartbeat or lost ack
# from reversing a migration. Use the actual listener, not a claimed IP.
from .recovery import tailnet_address
revision = value.get("endpoint_revision", 0)
if type(revision) is not int or not 0 <= revision <= 1000000000:
return 400, {"error": "Invalid endpoint revision"}
previous_revision = row["binding"].get("endpoint_revision", 0)
if revision < previous_revision:
return 409, {"error": "Endpoint superseded"}
if revision > previous_revision:
if (
not tailnet_address(address)
or value.get("core_endpoint") != f"https://{address}:8782"
):
return 400, {"error": "Endpoint does not match authenticated channel"}
row["core_address"] = address
row["binding"]["endpoint"] = value["core_endpoint"]
row["binding"]["endpoint_revision"] = revision
binding = ExecutionBinding.model_validate(value["execution_binding"])
if binding.node_id != node_id or binding.platform.value != "linux":
return 400, {"error": "Invalid Node inventory"}
@@ -417,4 +448,10 @@ class FleetRegistry:
except (ValueError, TypeError, KeyError, sqlite3.Error):
# Telemetry persistence failure must not block device control.
pass
return 200, {"monitor_ack": monitor_ack, "ok": True, "client_pem": row["binding"]["client_pem"], **sensor_response, **enrollment_response}
return 200, {
"monitor_ack": monitor_ack,
"ok": True,
"client_pem": row["binding"]["client_pem"],
**sensor_response,
**enrollment_response,
}
+16 -2
View File
@@ -20,12 +20,22 @@ ACTIONS = {
"option",
"offer",
"close-peer",
"preview.start",
"preview.stop",
"record.start",
"record.stop",
"photo.capture",
"settings.read",
"settings.apply",
"files.list",
}
MAX_INVENTORY_ITEMS = 500
MAX_SENSOR_STATE_BYTES = 3 * 1024 * 1024
def validate_inventory(value, node_id):
items = value.get("devices", [])
if not isinstance(items, list) or len(items) > 16:
if not isinstance(items, list) or len(items) > MAX_INVENTORY_ITEMS:
raise ValueError("Invalid device inventory")
seen = set()
for item in items:
@@ -38,7 +48,11 @@ def validate_inventory(value, node_id):
raise ValueError("Invalid device execution binding")
seen.add(item["id"])
state = value.get("sensor_state", {"items": items, "operations": [], "preparation": None})
if state.get("items") != items or len(json.dumps(state)) > 262144:
if (
not isinstance(state, dict)
or state.get("items") != items
or len(json.dumps(state, ensure_ascii=False).encode()) > MAX_SENSOR_STATE_BYTES
):
raise ValueError("Invalid sensor state")
return state
+7 -2
View File
@@ -57,14 +57,19 @@ class NodeChannelHandler(BaseHTTPRequestHandler):
if (
self.headers.get("Content-Type") != "application/json"
or self.headers.get("Origin")
or not 0 < size <= 1048576
# A bounded 500-camera summary is carried both in devices and
# sensor_state for the existing paired heartbeat contract.
or not 0 < size <= 8 * 1024 * 1024
):
raise ValueError
value = json.loads(self.rfile.read(size))
if not isinstance(value, dict):
raise ValueError
status, data = self.server.registry.receive(
self.connection.getpeercert(binary_form=True), self.path, value
self.connection.getpeercert(binary_form=True),
self.path,
value,
address=self.server.address,
)
except (ValueError, KeyError, TypeError):
self.close_connection = True
+17 -10
View File
@@ -206,6 +206,20 @@ class CoreTrust:
return context
def verify_node_certificate(raw: bytes, node_id: str) -> Ed25519PublicKey:
cert = x509.load_der_x509_certificate(raw)
key = cert.public_key()
now = datetime.now(UTC)
if (
not isinstance(key, Ed25519PublicKey)
or public_id("node_", key) != node_id
or not cert.not_valid_before_utc <= now < cert.not_valid_after_utc
):
raise PairingError("Идентичность БК не совпала с сохранённой.")
key.verify(cert.signature, cert.tbs_certificate_bytes)
return key
def node_request(invitation: dict, path: str, payload: dict) -> tuple[dict, Ed25519PublicKey, str]:
address, port = endpoint(invitation["endpoint"], 8781)
# The one-use secret is sent only AFTER authenticating the Node identity.
@@ -217,16 +231,9 @@ def node_request(invitation: dict, path: str, payload: dict) -> tuple[dict, Ed25
connection = http.client.HTTPSConnection(address, port, timeout=5, context=context)
try:
connection.connect()
cert = x509.load_der_x509_certificate(connection.sock.getpeercert(binary_form=True))
key = cert.public_key()
now = datetime.now(UTC)
if (
not isinstance(key, Ed25519PublicKey)
or public_id("node_", key) != invitation["node_id"]
or not cert.not_valid_before_utc <= now < cert.not_valid_after_utc
):
raise PairingError("Идентичность БК не совпала с приглашением.")
key.verify(cert.signature, cert.tbs_certificate_bytes)
key = verify_node_certificate(
connection.sock.getpeercert(binary_form=True), invitation["node_id"]
)
local = connection.sock.getsockname()[0]
if not private_address(local):
raise PairingError("Core не получил частный обратный адрес.")