This repository has been archived on 2026-02-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
data-smasher/dns-mesher-push.py
2026-01-08 11:46:33 +01:00

171 lines
4.4 KiB
Python

#!/usr/bin/env python3
"""Push signed zone data to dns-mesher/data-smasher nodes.
This tool reads a zone.conf file and sends it signed to one or more
data-smasher instances for distribution across the clan.
"""
import argparse
import base64
import struct
import sys
import time
from pathlib import Path
import requests
from cryptography.hazmat.primitives import serialization
def load_private_key(key_file: Path | None, key_string: str | None):
"""Load private key from file or string (OpenSSH format)."""
if key_string:
key_data = key_string.encode()
elif key_file:
with key_file.open("rb") as f:
key_data = f.read()
else:
raise ValueError("Either --key-file or --key must be provided")
return serialization.load_ssh_private_key(key_data, password=None)
def send_data(
host: str,
port: str,
filename: str,
data: bytes,
key,
timeout: int = 30,
) -> bool:
"""Send signed data to a data-smasher instance."""
timestamp = int(time.time())
# Build message: 8-byte big-endian timestamp + filename + data
filename_bytes = filename.encode()
message = struct.pack(">q", timestamp) + filename_bytes + data
# Sign
signature = key.sign(message)
signature_b64 = base64.b64encode(signature).decode()
url = f"http://{host}:{port}/data"
print(f"Sending to {url}")
print(f" Filename: {filename}")
print(f" Timestamp: {timestamp}")
print(f" Data length: {len(data)} bytes")
try:
resp = requests.post(
url,
headers={
"X-Filename": filename,
"X-Signature": signature_b64,
"X-Timestamp": str(timestamp),
},
data=data,
timeout=timeout,
)
print(f" Response: {resp.status_code}")
if resp.text:
print(f" Message: {resp.text}")
return resp.status_code == 200
except requests.exceptions.RequestException as e:
print(f" Error: {e}", file=sys.stderr)
return False
def main() -> None:
parser = argparse.ArgumentParser(
description="Push signed zone data to dns-mesher/data-smasher nodes"
)
parser.add_argument(
"--zone-file",
type=Path,
required=True,
help="Path to the zone.conf file to send",
)
parser.add_argument(
"--key-file",
type=Path,
help="Path to SSH ed25519 private key file",
)
parser.add_argument(
"--key",
help="SSH ed25519 private key as string (e.g., from $(passage show ...))",
)
parser.add_argument(
"--domain",
required=True,
help="Domain name (used as filename identifier)",
)
parser.add_argument(
"--host",
action="append",
dest="hosts",
help="Host to send to (can be specified multiple times)",
)
parser.add_argument(
"--port",
default="8080",
help="Port to connect to (default: 8080)",
)
parser.add_argument(
"--timeout",
type=int,
default=30,
help="Request timeout in seconds (default: 30)",
)
args = parser.parse_args()
# Validate arguments
if not args.key_file and not args.key:
print("Error: Either --key-file or --key must be provided", file=sys.stderr)
sys.exit(1)
if not args.zone_file.exists():
print(f"Error: Zone file not found: {args.zone_file}", file=sys.stderr)
sys.exit(1)
if args.key_file and not args.key_file.exists():
print(f"Error: Key file not found: {args.key_file}", file=sys.stderr)
sys.exit(1)
# Load the private key
try:
key = load_private_key(args.key_file, args.key)
except Exception as e:
print(f"Error loading private key: {e}", file=sys.stderr)
sys.exit(1)
data = args.zone_file.read_bytes()
print(f"Read {len(data)} bytes from {args.zone_file}")
hosts = args.hosts or ["localhost"]
success_count = 0
for host in hosts:
print()
if send_data(
host=host,
port=args.port,
filename=args.domain,
data=data,
key=key,
timeout=args.timeout,
):
success_count += 1
print()
print(f"Successfully sent to {success_count}/{len(hosts)} hosts")
if success_count == 0:
sys.exit(1)
elif success_count < len(hosts):
sys.exit(2)
if __name__ == "__main__":
main()