Files
neorelay/files/neoguard/neoguard_agent.py
NIA Sanitized Release Publisher 7141326ea0 Sanitized public release v1.0.3
Source-Tag: v1.0.3
Manifest-SHA256: b7b09bf48f9e092ed8ad82f99c0319d6c3837d0a1549bd0cc4a374276c0f3896
2026-07-20 22:35:36 +00:00

133 lines
4.4 KiB
Python

#!/usr/bin/python3
import hashlib
import hmac
import json
import os
import secrets
import socket
import threading
import time
import urllib.error
import urllib.request
from pathlib import Path
SOCKET_PATH = "{{NEOGUARD_CONTROL_SOCKET_PATH}}"
BASE = "{{DROPS_ORIGIN}}"
KEY_PATH = Path("{{NEOGUARD_KEY_PATH}}")
CLIENTS = set()
CLIENTS_LOCK = threading.Lock()
LAST_EVENT = 0
def signed_request(path, value=None):
body = b"" if value is None else json.dumps(value, separators=(",", ":")).encode()
method = "GET" if value is None else "POST"
for attempt in range(3):
timestamp = int(time.time())
nonce = secrets.token_hex(16)
canonical = "%d\n%s\n%s\n%s\n%s" % (
timestamp, nonce, method, path, hashlib.sha256(body).hexdigest())
signature = hmac.new(KEY_PATH.read_bytes(), canonical.encode(), hashlib.sha256).hexdigest()
request = urllib.request.Request(BASE + path, data=None if value is None else body, method=method, headers={
"Content-Type": "application/json", "X-Neo-Timestamp": str(timestamp),
"X-Neo-Nonce": nonce, "X-Neo-Signature": signature,
})
try:
with urllib.request.urlopen(request, timeout=10) as response:
return json.load(response)
except urllib.error.HTTPError as error:
if error.code not in (502, 503, 504) or attempt == 2:
raise
time.sleep(0.25 * (attempt + 1))
def broadcast(message):
payload = (message.replace("\x00", "") + "\n").encode()
dead = []
with CLIENTS_LOCK:
for client in CLIENTS:
try:
client.sendall(payload)
except OSError:
dead.append(client)
for client in dead:
CLIENTS.discard(client)
client.close()
def attempt_payload(fields):
if len(fields) == 3:
address, detail = fields[1], fields[2]
port = None
elif len(fields) == 4:
address, port_text, detail = fields[1], fields[2], fields[3]
if not port_text.isdecimal() or not 1 <= int(port_text) <= 65535:
raise ValueError("invalid destination port")
port = int(port_text)
else:
raise ValueError("invalid attempt event")
payload = {"type": "attempt", "ip": address, "source": "relay", "detail": detail}
if port is not None:
payload["destination_port"] = port
return payload
def handle_client(client):
buffer = b""
try:
client.sendall(b"STATUS\tNeoGuard agent connected\n")
while True:
data = client.recv(4096)
if not data:
break
buffer += data
while b"\n" in buffer:
line, buffer = buffer.split(b"\n", 1)
fields = line.decode("utf-8", "replace").split("\t", 3)
try:
if fields[0] == "ATTEMPT":
signed_request("/api/v1/neoguard/event", attempt_payload(fields))
elif fields[0] == "COMMAND" and len(fields) >= 2:
result = signed_request("/api/v1/neoguard/event", {"type":"command","command":fields[1]})
client.sendall(("RESPONSE\t" + result.get("response", "No response") + "\n").encode())
except Exception as error:
client.sendall(("ERROR\t" + str(error)[:300] + "\n").encode())
finally:
with CLIENTS_LOCK:
CLIENTS.discard(client)
client.close()
def socket_server():
try:
os.unlink(SOCKET_PATH)
except FileNotFoundError:
pass
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(SOCKET_PATH)
os.chmod(SOCKET_PATH, 0o660)
server.listen(4)
while True:
client, _ = server.accept()
with CLIENTS_LOCK:
CLIENTS.add(client)
threading.Thread(target=handle_client, args=(client,), daemon=True).start()
def poll_events():
global LAST_EVENT
while True:
try:
result = signed_request("/api/v1/neoguard/poll?after=%d" % LAST_EVENT)
for event in result.get("events", []):
LAST_EVENT = max(LAST_EVENT, int(event["id"]))
broadcast("EVENT\t%s\t%s" % (event["level"], event["message"]))
except Exception:
pass
time.sleep(5)
if __name__ == "__main__":
threading.Thread(target=poll_events, daemon=True).start()
socket_server()