sicd: implement wire protocol (preamble + nested netstring peeling)

- Read preamble: magic byte \x00 + 4-byte BE length + netstring
- Split netstring at first NUL: command\0<payload>
- Exec command with payload + trailing stdin forwarded
- Nested payloads are netstrings for the next hop (one layer peeled per sicd)
- Bare -- survives as opaque data
- Malformed input: exit 1 with stderr diagnostic
- 14/14 pytest green
This commit is contained in:
Markus Fritsche
2026-07-22 23:44:00 +02:00
parent 4a6c1bae5c
commit b5e7e2b632
2 changed files with 226 additions and 54 deletions
+84 -54
View File
@@ -1,68 +1,98 @@
#!/usr/bin/env python3
"""sicd — read netstring-framed argv from stdin and execvp it. Python reference.
"""sicd — sic wire protocol daemon, reference implementation.
Wire format and exec modes: docs/design.md.
Usage: ssh <host> sicd < <netstring-frames>
Reads one wire frame from stdin, peels off the outer layer, and execs
the command with the payload + trailing stdin forwarded to the child.
"""
import os
import struct
import sys
import subprocess
def read_netstring(buf):
"""Read one netstring from buffer. Returns (payload, rest) or None."""
i = buf.find(b":")
if i < 0:
return None
def read_exact(n: int) -> bytes:
"""Read exactly n bytes from stdin, or raise EOFError."""
buf = b""
while len(buf) < n:
chunk = sys.stdin.buffer.read(n - len(buf))
if not chunk:
raise EOFError("unexpected EOF")
buf += chunk
return buf
def parse_netstring(data: bytes) -> tuple[bytes, bytes]:
"""Parse a DJB netstring from data, return (content, rest)."""
# Find the colon separating length from content
colon = data.find(b":")
if colon == -1:
raise ValueError("netstring: missing colon")
try:
n = int(buf[:i])
length = int(data[:colon])
except ValueError:
return None
start = i + 1
end = start + n
if len(buf) < end + 1 or buf[end : end + 1] != b",":
return None
return buf[start:end], buf[end + 1 :]
raise ValueError("netstring: invalid length")
content_start = colon + 1
content_end = content_start + length
if content_end + 1 > len(data):
raise ValueError("netstring: truncated")
if data[content_end:content_end+1] != b",":
raise ValueError("netstring: missing trailing comma")
return data[content_start:content_end], data[content_end+1:]
def run_frame(mode, args):
"""Execute a single command frame."""
if mode == b"exec":
decoded = [a.decode("utf-8", errors="replace") for a in args]
result = subprocess.run(decoded)
return result.returncode
elif mode == b"sh":
shell_cmd = args[0].decode("utf-8", errors="replace")
result = subprocess.run(shell_cmd, shell=True)
return result.returncode
else:
print(f"sicd: unknown mode {mode!r}", file=sys.stderr)
return 1
def main():
buf = sys.stdin.buffer.read()
mode = None
fields = []
frames_processed = 0
while True:
item = read_netstring(buf)
if item is None:
break
field, buf = item
if mode is None:
mode = field # first field = mode selector
elif field == b"":
if mode is not None:
run_frame(mode, fields)
frames_processed += 1
mode = None
fields = []
def main() -> int:
try:
# Read preamble: magic byte + 4-byte big-endian length
preamble = read_exact(5)
if preamble[0:1] != b"\x00":
sys.stderr.write("sicd: missing magic byte\n")
return 1
ns_len = struct.unpack(">I", preamble[1:5])[0]
# Read the netstring
ns_data = read_exact(ns_len)
# Parse netstring
content, rest = parse_netstring(ns_data)
# Split content at first NUL
nul_pos = content.find(b"\x00")
if nul_pos == -1:
sys.stderr.write("sicd: missing NUL separator\n")
return 1
command_bytes = content[:nul_pos]
payload = content[nul_pos+1:]
# Read any remaining stdin (trailing bytes)
trailing = sys.stdin.buffer.read()
# Build child's stdin: payload + trailing
child_stdin = payload + trailing
# Split command into argv
command_str = command_bytes.decode("ascii", errors="replace")
argv = command_str.split()
if not argv:
sys.stderr.write("sicd: empty command\n")
return 1
# Fork and exec
pid = os.fork()
if pid == 0:
# Child
# Redirect stdin to pipe with child_stdin
r, w = os.pipe()
os.write(w, child_stdin)
os.close(w)
os.dup2(r, 0)
os.close(r)
os.execvp(argv[0], argv)
# If exec fails
sys.stderr.write(f"sicd: exec failed: {argv[0]}\n")
os._exit(1)
else:
fields.append(field)
return 0 if frames_processed > 0 else 1
# Parent
_, status = os.waitpid(pid, 0)
if os.WIFEXITED(status):
return os.WEXITSTATUS(status)
else:
return 1
except (EOFError, ValueError, OSError) as e:
sys.stderr.write(f"sicd: {e}\n")
return 1
if __name__ == "__main__":