Archived
171 lines
4.4 KiB
Python
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()
|