181 lines
6.7 KiB
Python
181 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Gate0 P1 broker prototype: peercred anchor minting and /proc ancestry checks."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import secrets
|
|
import socket
|
|
import stat
|
|
import struct
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
def proc_node(pid: int) -> dict[str, Any]:
|
|
text = Path(f"/proc/{pid}/stat").read_text()
|
|
close = text.rfind(")")
|
|
comm = text[text.find("(") + 1 : close]
|
|
fields = text[close + 2 :].split()
|
|
cmdline = Path(f"/proc/{pid}/cmdline").read_bytes().split(b"\0")
|
|
return {
|
|
"pid": pid,
|
|
"ppid": int(fields[1]),
|
|
"starttime_ticks": int(fields[19]),
|
|
"comm": comm,
|
|
"exe": os.readlink(f"/proc/{pid}/exe"),
|
|
"argv0": cmdline[0].decode(errors="replace") if cmdline and cmdline[0] else "",
|
|
"argc": len([part for part in cmdline if part]),
|
|
}
|
|
|
|
|
|
def ancestry(peer_pid: int, anchor: dict[str, Any] | None) -> tuple[list[dict[str, Any]], bool, str]:
|
|
chain: list[dict[str, Any]] = []
|
|
pid = peer_pid
|
|
seen: set[int] = set()
|
|
try:
|
|
while pid > 0 and pid not in seen:
|
|
seen.add(pid)
|
|
node = proc_node(pid)
|
|
chain.append(node)
|
|
if anchor and pid == anchor["pid"]:
|
|
if node["starttime_ticks"] != anchor["starttime_ticks"]:
|
|
return chain, False, "anchor-starttime-mismatch"
|
|
break
|
|
pid = node["ppid"]
|
|
else:
|
|
return chain, False, "anchor-not-reached"
|
|
|
|
if not anchor or chain[-1]["pid"] != anchor["pid"]:
|
|
return chain, False, "anchor-not-reached"
|
|
|
|
# Re-read every node after the walk. A disappearing PID or changed
|
|
# starttime invalidates the complete chain (PID-reuse/race closure).
|
|
for original in chain:
|
|
again = proc_node(original["pid"])
|
|
if again["starttime_ticks"] != original["starttime_ticks"]:
|
|
return chain, False, f"starttime-race:{original['pid']}"
|
|
return chain, True, "ancestry-reaches-registered-anchor"
|
|
except (FileNotFoundError, ProcessLookupError, PermissionError) as exc:
|
|
return chain, False, f"proc-walk-failed:{type(exc).__name__}"
|
|
|
|
|
|
def emit(log_file: Path, record: dict[str, Any]) -> None:
|
|
line = json.dumps(record, sort_keys=True)
|
|
with log_file.open("a", encoding="utf-8") as out:
|
|
out.write(line + "\n")
|
|
print(line, flush=True)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--socket", required=True)
|
|
parser.add_argument("--log", required=True)
|
|
parser.add_argument("--state", required=True)
|
|
args = parser.parse_args()
|
|
|
|
socket_path = Path(args.socket)
|
|
log_file = Path(args.log)
|
|
state_file = Path(args.state)
|
|
socket_path.parent.mkdir(parents=True, exist_ok=True)
|
|
os.chmod(socket_path.parent, 0o700)
|
|
socket_path.unlink(missing_ok=True)
|
|
log_file.unlink(missing_ok=True)
|
|
state_file.unlink(missing_ok=True)
|
|
|
|
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
server.bind(str(socket_path))
|
|
os.chmod(socket_path, 0o600)
|
|
server.listen(8)
|
|
anchor: dict[str, Any] | None = None
|
|
session_id: str | None = None
|
|
emit(
|
|
log_file,
|
|
{
|
|
"event": "broker-listen",
|
|
"pid": os.getpid(),
|
|
"socket": str(socket_path),
|
|
"directory_mode": f"{stat.S_IMODE(socket_path.parent.stat().st_mode):04o}",
|
|
"socket_mode": f"{stat.S_IMODE(socket_path.stat().st_mode):04o}",
|
|
},
|
|
)
|
|
|
|
while True:
|
|
conn, _ = server.accept()
|
|
with conn:
|
|
raw = conn.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12)
|
|
peer_pid, peer_uid, peer_gid = struct.unpack("3i", raw)
|
|
request = json.loads(conn.makefile("r", encoding="utf-8").readline())
|
|
action = request.get("action")
|
|
|
|
if action == "register-anchor" and anchor is None:
|
|
anchor = proc_node(peer_pid)
|
|
session_id = secrets.token_hex(16)
|
|
state = {"session_id": session_id, "anchor": anchor}
|
|
state_file.write_text(json.dumps(state, sort_keys=True) + "\n")
|
|
record = {
|
|
"event": "anchor-minted",
|
|
"decision": "ACCEPT",
|
|
"peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid},
|
|
"anchor": anchor,
|
|
"session_id": session_id,
|
|
}
|
|
emit(log_file, record)
|
|
conn.sendall((json.dumps(record) + "\n").encode())
|
|
continue
|
|
|
|
if action in {"resolve-hook", "claim-session"}:
|
|
chain, reaches, reason = ancestry(peer_pid, anchor)
|
|
claimed = request.get("session_id")
|
|
claim_ok = action == "resolve-hook" or claimed == session_id
|
|
accepted = bool(anchor and session_id and reaches and claim_ok)
|
|
if action == "claim-session" and claimed != session_id:
|
|
reason = "unknown-session-id"
|
|
elif action == "claim-session" and claimed == session_id and not reaches:
|
|
reason = "victim-id-known-but-ancestry-mismatch"
|
|
record = {
|
|
"event": action,
|
|
"decision": "ACCEPT" if accepted else "REJECT",
|
|
"reason": reason,
|
|
"peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid},
|
|
"claimed_session_id": claimed,
|
|
"resolved_session_id": session_id if accepted else None,
|
|
"anchor": anchor,
|
|
"ancestry": chain,
|
|
"starttimes_rechecked": reaches,
|
|
}
|
|
emit(log_file, record)
|
|
conn.sendall((json.dumps(record) + "\n").encode())
|
|
continue
|
|
|
|
if action == "shutdown":
|
|
record = {
|
|
"event": "broker-shutdown",
|
|
"peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid},
|
|
}
|
|
emit(log_file, record)
|
|
conn.sendall((json.dumps(record) + "\n").encode())
|
|
break
|
|
|
|
record = {
|
|
"event": "invalid-request",
|
|
"decision": "REJECT",
|
|
"peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid},
|
|
}
|
|
emit(log_file, record)
|
|
conn.sendall((json.dumps(record) + "\n").encode())
|
|
|
|
server.close()
|
|
socket_path.unlink(missing_ok=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except Exception as exc:
|
|
print(f"P1 broker fatal: {type(exc).__name__}: {exc}", file=sys.stderr)
|
|
raise
|