98 lines
3.4 KiB
Python
98 lines
3.4 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Project a fresh protected native-relay receipt through one pinned SSH route."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import os
|
||
|
|
import pathlib
|
||
|
|
import subprocess
|
||
|
|
import time
|
||
|
|
|
||
|
|
|
||
|
|
def parse_receipt(path: pathlib.Path) -> tuple[dict[str, str], float]:
|
||
|
|
if path.is_symlink():
|
||
|
|
raise ValueError("relay receipt cannot be a symlink")
|
||
|
|
stat = path.stat()
|
||
|
|
values: dict[str, str] = {}
|
||
|
|
for line in path.read_text(encoding="utf-8").splitlines():
|
||
|
|
if not line or line[:1].isspace() or ": " not in line:
|
||
|
|
continue
|
||
|
|
key, value = line.split(": ", 1)
|
||
|
|
if key in values:
|
||
|
|
raise ValueError(f"duplicate relay receipt key: {key}")
|
||
|
|
values[key] = value
|
||
|
|
return values, time.time() - stat.st_mtime
|
||
|
|
|
||
|
|
|
||
|
|
def projection_action(
|
||
|
|
values: dict[str, str], age: float, max_age: float, expected_source: str
|
||
|
|
) -> str:
|
||
|
|
ready = (
|
||
|
|
age >= -5
|
||
|
|
and age <= max_age
|
||
|
|
and values.get("schema")
|
||
|
|
== "guanghu.physical-native-final-resident-relay/v1"
|
||
|
|
and values.get("status") == "READY_NATIVE_RESIDENT"
|
||
|
|
and values.get("recovery_capability_sent") == "false"
|
||
|
|
and values.get("acknowledged_sequences") == "1,2,3,4,5,6"
|
||
|
|
and values.get("observed_nat_sources") == expected_source
|
||
|
|
)
|
||
|
|
return "ready" if ready else "dormant"
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> int:
|
||
|
|
parser = argparse.ArgumentParser()
|
||
|
|
parser.add_argument("--relay-receipt", required=True)
|
||
|
|
parser.add_argument("--max-receipt-age-seconds", type=float, default=15.0)
|
||
|
|
parser.add_argument("--expected-nat-source", required=True)
|
||
|
|
parser.add_argument("--node", required=True)
|
||
|
|
parser.add_argument("--relay", required=True)
|
||
|
|
parser.add_argument("--candidate-sha256", required=True)
|
||
|
|
parser.add_argument("--source-commit", required=True)
|
||
|
|
parser.add_argument("--ssh-bin", default="/usr/bin/ssh")
|
||
|
|
parser.add_argument("--ssh-config", required=True)
|
||
|
|
parser.add_argument("--ssh-target", required=True)
|
||
|
|
args = parser.parse_args()
|
||
|
|
if args.max_receipt_age_seconds <= 0:
|
||
|
|
raise SystemExit("max receipt age must be greater than zero")
|
||
|
|
ssh_bin = pathlib.Path(args.ssh_bin)
|
||
|
|
ssh_config = pathlib.Path(args.ssh_config)
|
||
|
|
if not ssh_bin.is_absolute() or not os.access(ssh_bin, os.X_OK):
|
||
|
|
raise SystemExit("SSH binary must be an executable absolute path")
|
||
|
|
if not ssh_config.is_absolute() or not ssh_config.is_file():
|
||
|
|
raise SystemExit("SSH config must be an existing absolute file")
|
||
|
|
try:
|
||
|
|
values, age = parse_receipt(pathlib.Path(args.relay_receipt))
|
||
|
|
action = projection_action(
|
||
|
|
values,
|
||
|
|
age,
|
||
|
|
args.max_receipt_age_seconds,
|
||
|
|
args.expected_nat_source,
|
||
|
|
)
|
||
|
|
except (OSError, UnicodeError, ValueError):
|
||
|
|
action = "dormant"
|
||
|
|
command = [
|
||
|
|
str(ssh_bin),
|
||
|
|
"-F",
|
||
|
|
str(ssh_config),
|
||
|
|
"-o",
|
||
|
|
"BatchMode=yes",
|
||
|
|
"-o",
|
||
|
|
"ConnectTimeout=5",
|
||
|
|
args.ssh_target,
|
||
|
|
action,
|
||
|
|
args.node,
|
||
|
|
args.candidate_sha256,
|
||
|
|
args.source_commit,
|
||
|
|
args.relay,
|
||
|
|
]
|
||
|
|
completed = subprocess.run(command, check=False, timeout=10)
|
||
|
|
if completed.returncode != 0:
|
||
|
|
raise SystemExit("native projection transport failed closed")
|
||
|
|
print(f"NATIVE_PUBLIC_PROJECTION_RELAY_OK action={action}")
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
raise SystemExit(main())
|