diff --git a/decnet/templates/conpot/instance_seed.py b/decnet/templates/conpot/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/conpot/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/docker_api/instance_seed.py b/decnet/templates/docker_api/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/docker_api/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/imap/instance_seed.py b/decnet/templates/imap/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/imap/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/k8s/instance_seed.py b/decnet/templates/k8s/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/k8s/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/llmnr/instance_seed.py b/decnet/templates/llmnr/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/llmnr/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/pop3/instance_seed.py b/decnet/templates/pop3/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/pop3/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/rdp/instance_seed.py b/decnet/templates/rdp/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/rdp/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/sip/instance_seed.py b/decnet/templates/sip/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/sip/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/smb/instance_seed.py b/decnet/templates/smb/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/smb/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/snmp/instance_seed.py b/decnet/templates/snmp/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/snmp/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/ssh/instance_seed.py b/decnet/templates/ssh/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/ssh/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/telnet/instance_seed.py b/decnet/templates/telnet/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/telnet/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/tftp/instance_seed.py b/decnet/templates/tftp/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/tftp/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0) diff --git a/decnet/templates/vnc/instance_seed.py b/decnet/templates/vnc/instance_seed.py new file mode 100644 index 00000000..61e1fecc --- /dev/null +++ b/decnet/templates/vnc/instance_seed.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Per-instance stealth seeding for honeypot service templates. + +The whole decoy fleet looks identical to a scanner unless each decky +diverges on the boring details: cluster UUIDs, auth salts, uptime, minor +version strings, etc. This module derives a stable per-instance seed +from NODE_NAME (+ optional INSTANCE_ID) and exposes helpers that return +deterministic-per-decky-but-different-across-the-fleet values. + +Connection-time jitter is intentionally NOT seeded — two hits to the same +decky should not replay the same latency curve. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import os +import random +import time +import uuid +from typing import Sequence, TypeVar + +T = TypeVar("T") + +_HOSTNAME = ( + os.environ.get("NODE_NAME") + or os.environ.get("HOSTNAME") + or "decky" +) +_INSTANCE_ID = os.environ.get("INSTANCE_ID", "") +_SEED_MATERIAL = f"{_HOSTNAME}:{_INSTANCE_ID}".encode() +_SEED_INT = int.from_bytes(hashlib.sha256(_SEED_MATERIAL).digest()[:8], "big") + +#: Deterministic RNG seeded per decky — use for *persistent* choices +#: (versions, UUIDs, stored credentials). Never use for timing. +rng = random.Random(_SEED_INT) + +#: Process boot time — real uptime elapsed since container start. +_PROCESS_START = time.time() + +#: Deterministic per-instance fake "has been up for this long at boot" +#: offset, so every decky pretends to have a different history. +_BOOT_OFFSET = rng.randint(3600, 45 * 86400) + + +def hostname() -> str: + return _HOSTNAME + + +def uptime_seconds() -> int: + """Monotonically increasing, unique per instance.""" + return int(_BOOT_OFFSET + (time.time() - _PROCESS_START)) + + +def boot_epoch() -> int: + """Fake wall-clock boot time for this instance (seconds since epoch).""" + return int(time.time() - uptime_seconds()) + + +def instance_uuid(namespace: str = "") -> str: + """Deterministic UUID4-looking value for this instance+namespace.""" + ns = uuid.UUID("00000000-0000-0000-0000-000000000000") + return str(uuid.uuid5(ns, f"{_HOSTNAME}:{namespace}")) + + +def instance_hex(nbytes: int, namespace: str = "") -> str: + """Deterministic hex token of given byte length.""" + material = f"{_HOSTNAME}:{namespace}".encode() + digest = hashlib.sha256(material).digest() + while len(digest) < nbytes: + digest += hashlib.sha256(digest).digest() + return digest[:nbytes].hex() + + +def pick(choices: Sequence[T]) -> T: + """Deterministic choice from a sequence.""" + return rng.choice(list(choices)) + + +def pick_weighted(choices: Sequence[tuple[T, float]]) -> T: + """Deterministic weighted choice. Input: [(item, weight), ...].""" + total = sum(w for _, w in choices) + r = rng.uniform(0, total) + acc = 0.0 + for item, w in choices: + acc += w + if r <= acc: + return item + return choices[-1][0] + + +def random_bytes(n: int, namespace: str = "") -> bytes: + """Deterministic per-instance byte string of length n.""" + out = bytearray() + i = 0 + while len(out) < n: + out.extend( + hashlib.sha256(f"{_HOSTNAME}:{namespace}:{i}".encode()).digest() + ) + i += 1 + return bytes(out[:n]) + + +def fresh_bytes(n: int) -> bytes: + """Non-deterministic random bytes — for per-connection nonces/salts.""" + return os.urandom(n) + + +async def jitter(min_ms: int = 5, max_ms: int = 120) -> None: + """Async response-time jitter. Uses unseeded RNG so timing varies + across connections to the same decky — seeded jitter would leak + predictability.""" + await asyncio.sleep(random.uniform(min_ms, max_ms) / 1000.0) + + +def jitter_sync(min_ms: int = 5, max_ms: int = 120) -> None: + """Blocking jitter for non-asyncio servers.""" + time.sleep(random.uniform(min_ms, max_ms) / 1000.0)