1
Mutation and Randomization
anti edited this page 2026-04-18 06:05:32 -04:00

Mutation and Randomization

DECNET's value as a deception network depends on the decoy fleet looking heterogeneous at deploy time and shifting over its lifetime. This page documents the two mechanisms that deliver that: randomization at build, and mutation at runtime.

See also: CLI reference, Archetypes, Distros.

Randomization at deploy time

--randomize-services

When decnet deploy is invoked with --randomize-services, each decky receives a randomly drawn service set instead of the fixed list passed via --services or the set implied by --archetype.

The selection logic lives in build_deckies() (decnet/fleet.py). For each decky:

  1. A count k is drawn uniformly from [1, min(3, len(pool))].
  2. k service names are sampled without replacement from the pool (all_service_names(), or the archetype service list if an archetype is set).
  3. The chosen set is compared against a used_combos: set[frozenset] that tracks combinations already assigned in this deploy. If it collides with an existing combination, the draw is retried.
  4. After 20 retries, the last draw is accepted even if duplicated. With small pools and many deckies, exact uniqueness is not always possible — the retry cap prevents an infinite loop.

Random hostnames

Hostnames are generated by random_hostname(distro_slug) in decnet/distros.py. The style depends on the distro profile's hostname_style field:

Style Example Distros
generic SRV-PROD-42 Debian, Ubuntu
rhel web37.localdomain Rocky, CentOS, Fedora
minimal alpha-18 Alpine
rolling nova-backup Kali, Arch

Word pool and numeric range are defined in _NAME_WORDS and the random.randint(10, 99) call in the same file.

Random distros

random_distro() picks a uniform-random entry from the DISTROS dict (decnet/distros.py). Each entry is a DistroProfile with a slug, a Docker image, a display name, a hostname style, and a build base image used for service Dockerfiles (which assume apt-get, so non-Debian distros fall back to debian:bookworm-slim for builds).

The current set: debian, ubuntu22, ubuntu20, rocky9, centos7, alpine, fedora, kali, arch.

MAC addresses

MAC addresses are not assigned by DECNET. The MACVLAN driver auto-generates a MAC for each container interface at container start. There is no knob to pin or rotate them from the deploy config; if you need deterministic MACs, attach them out-of-band at the Docker network layer.

Mutation at runtime

Randomization only fires at build time. To keep the fleet moving, DECNET supports per-decky service rotation.

Storage

Every DeckyConfig (decnet/config.py) carries two mutation-related fields:

  • mutate_interval: int | None — minutes between rotations for this decky. None disables automatic rotation for that decky.
  • last_mutated: float — Unix timestamp of the most recent successful mutation.

The top-level DecnetConfig also holds a fleet-wide mutate_interval, which defaults to DEFAULT_MUTATE_INTERVAL = 30 (minutes) from decnet/config.py. Per-decky values override the fleet default.

Engine

decnet/mutator/engine.py exposes three async entry points, all operating against a BaseRepository:

  • mutate_decky(decky_name, repo) — Intra-archetype shuffle for one decky. Rebuilds the service list by sampling 1-3 services from the decky's archetype pool (or the full registry if no archetype is set), retrying up to 20 times to avoid picking the exact same set. Updates last_mutated, persists state, rewrites the compose file, and runs docker compose up -d --remove-orphans.
  • mutate_all(repo, force=False) — Iterates all deckies. For each, computes elapsed = now - last_mutated and calls mutate_decky when elapsed >= interval * 60. force=True bypasses the schedule.
  • run_watch_loop(repo, poll_interval_secs=10) — Infinite loop that calls mutate_all every poll_interval_secs. Invoked by decnet mutate --watch.

Trigger from the CLI

# Deploy a fleet that rotates every 15 minutes
decnet deploy --mode unihost --deckies 5 --interface eth0 \
    --randomize-services --mutate-interval 15

# Mutate a single decky now (forces immediately, ignores schedule)
decnet mutate --decky decky-03

# Mutate all deckies now
decnet mutate --all

# Run the watcher in the foreground
decnet mutate --watch

# Run the watcher as a detached daemon
decnet mutate --watch --daemon

decnet deploy also starts a background watcher automatically; the standalone decnet mutate --watch is for operators who want to run the loop themselves.

Operational trade-offs

Mutation is a blunt instrument. The interval you pick is a trade between deception fidelity and observability:

  • Short intervals (≤ 5 min): The fleet looks lively and fingerprint scans will never converge, but every rotation churns containers, rewrites compose files, and wipes short-lived attacker state — half-finished brute-force sessions, partially uploaded payloads, active TCP connections. You will lose IOCs that would otherwise have been captured.
  • Default (30 min): Reasonable balance. Most scan-and-go attackers see a consistent snapshot; long-dwell attackers see the fleet shift under them, which itself is a useful signal.
  • Long intervals (hours) or None: The fleet looks static. An attacker who fingerprints twice gets identical results, which is not how real networks behave under patching and reconfiguration.

For research deployments where you want a specific attacker session recorded end-to-end, disable mutation on the decky under observation (mutate_interval=None on that decky only) while leaving the rest of the fleet rotating.

Sources

  • decnet/distros.pyDISTROS, random_hostname, random_distro
  • decnet/fleet.pybuild_deckies
  • decnet/mutator/engine.pymutate_decky, mutate_all, run_watch_loop
  • decnet/config.pyDEFAULT_MUTATE_INTERVAL, DeckyConfig, DecnetConfig
  • decnet/cli.pydeploy, mutate commands