wiki(networking): add MACVLAN/IPvlan and Deployment Modes pages

anti
2026-04-18 06:04:35 -04:00
commit 6d3cd015d5
2 changed files with 338 additions and 0 deletions

175
Deployment-Modes.md Normal file

@@ -0,0 +1,175 @@
# Deployment Modes: UNIHOST and SWARM
DECNET's `deploy` command takes a `--mode` flag that selects how the fleet of
deckies is laid out across real hardware. Two modes are defined:
- `unihost` — a single real host runs all deckies as containers.
- `swarm` — multiple real hosts each run a subset of deckies (multi-host).
The mode is validated in `decnet/cli.py` (`deploy --mode`) and stored on the
`DecnetConfig` object consumed by `decnet/engine/deployer.py`. The orchestrator
in `decnet/composer.py` emits the Docker Compose file used by either mode.
See also: [CLI reference](CLI-Reference), [INI format](INI-Config-Format),
[Networking: MACVLAN and IPvlan](Networking-MACVLAN-IPVLAN),
[Teardown](Teardown-and-State).
---
## When to use which
| Criterion | UNIHOST | SWARM |
|---------------------------------|----------------------------|-------------------------------------|
| Hosts involved | 1 | N |
| Setup complexity | low | higher (needs fleet orchestration) |
| Deckies per host | all of them | split across hosts |
| Fits home lab / single server | yes | overkill |
| Fits multi-subnet / multi-site | no | yes |
| Failure blast radius | whole fleet on one box | isolated per host |
| Recommended for first runs | yes | no |
UNIHOST is the default (`--mode unihost`) and what the README's quickstart
assumes. SWARM is the deployment posture for a real honeynet spanning multiple
boxes.
---
## UNIHOST
```
attacker LAN 192.168.1.0/24
+---------------------------------------------------+
| |
| [host] eth0 decnet_lan (MACVLAN / IPvlan L2) |
| |- decky-01 .10 |
| |- decky-02 .11 |
| |- decky-03 .12 |
| |- decky-04 .13 |
| '- decky-05 .14 |
+---------------------------------------------------+
|
(isolated mgmt path)
v
logging / SIEM network
```
One host, one MACVLAN or IPvlan L2 network, N deckies. The host also runs the
collector/API and forwards logs out-of-band to the isolated logging network.
### CLI
```
# Dry-run: generate compose, no containers
decnet deploy --mode unihost --deckies 3 --randomize-services --dry-run
# Full deploy (root required for MACVLAN)
sudo decnet deploy \
--mode unihost \
--deckies 5 \
--interface eth0 \
--randomize-services
```
Flags worth knowing (full table in [CLI reference](CLI-Reference)):
- `--deckies / -n` — number of deckies on this host.
- `--interface / -i` — parent NIC. Auto-detected via `detect_interface()`.
- `--ipvlan` — switch to IPvlan L2. Required on WiFi.
- `--services`, `--randomize-services`, `--archetype` — service selection.
- `--distro`, `--randomize-distros` — OS heterogeneity.
- `--mutate-interval` — rotate services every N minutes.
- `--dry-run` — write the compose file under `deploy/` without starting.
### INI
The same deployment can be expressed declaratively via `--config`:
```
decnet deploy --mode unihost --config ./my-fleet.ini
```
See [INI format](INI-Config-Format) for the full schema; `cli.py`'s config
path calls `load_ini` and `build_deckies_from_ini`.
---
## SWARM (multihost)
```
attacker network(s)
------------------------------
| | |
[host-A] [host-B] [host-C]
decky-01 decky-04 decky-07
decky-02 decky-05 decky-08
decky-03 decky-06 decky-09
\__________________|__________________/
|
isolated mgmt / SIEM
```
Each real host runs a UNIHOST-shaped deployment over its own slice of the IP
space. An external orchestrator (Ansible, sshpass-driven scripts, etc.)
invokes `decnet deploy --mode swarm ...` on each host in turn. The CLI
currently accepts `swarm` as a valid mode — the fleet-wide orchestration layer
lives outside the DECNET binary and is the operator's responsibility. See the
README's architecture section for the intended shape.
### CLI
Run on each host, coordinating IP ranges so deckies do not collide:
```
# host-A
sudo decnet deploy \
--mode swarm \
--deckies 3 \
--interface eth0 \
--ip-start 192.168.1.10 \
--randomize-services
# host-B
sudo decnet deploy \
--mode swarm \
--deckies 3 \
--interface eth0 \
--ip-start 192.168.1.20 \
--randomize-services
```
`--ip-start` is the operator's primary tool for partitioning the subnet across
hosts; `allocate_ips` in `decnet/network.py` starts sequentially from that
address and skips reserved / in-use IPs.
### INI
For reproducible swarm rollouts, give each host its own INI and drive the
rollout from Ansible (or similar):
```
decnet deploy --mode swarm --config ./host-A.ini
decnet deploy --mode swarm --config ./host-B.ini
```
---
## What is common to both modes
- Compose generation in `decnet/composer.py` is mode-agnostic; the `mode`
field is recorded on `DecnetConfig` for state/telemetry but the compose
shape (base container + service containers sharing its network namespace
via `network_mode: service:<base>`) is identical.
- Network driver selection (MACVLAN vs IPvlan L2) is per-host, controlled by
`--ipvlan`. See [Networking: MACVLAN and IPvlan](Networking-MACVLAN-IPVLAN).
- Teardown is per-host: run `sudo decnet teardown --all` on each host in the
swarm. See [Teardown](Teardown-and-State).
---
## Quick checklist
- Single box, one LAN -> UNIHOST.
- Multiple boxes across one or more LANs -> SWARM, driven by external
orchestration, with non-overlapping `--ip-start` per host.
- Not sure -> start with UNIHOST and a small `--deckies` count.

@@ -0,0 +1,163 @@
# Networking: MACVLAN and IPvlan
DECNET's deckies must look like real, independent machines on the LAN. Each
decky owns an IP (and ideally a MAC) drawn from the same subnet as the host's
real NIC, so that attackers scanning the network see a heterogeneous fleet of
hosts rather than a single container host.
This page covers how that is wired up: the two Docker network drivers DECNET
supports, the host-side hairpin interface it creates, and the limitations you
will hit on WiFi or WSL.
Source of truth: `decnet/network.py` (constants `MACVLAN_NETWORK_NAME`,
`HOST_MACVLAN_IFACE`, `HOST_IPVLAN_IFACE`; functions `setup_host_macvlan`,
`teardown_host_macvlan`, `setup_host_ipvlan`, `teardown_host_ipvlan`).
See also: [CLI reference](CLI-Reference), [INI format](INI-Config-Format),
[Teardown](Teardown-and-State).
---
## Topology
```
Internet / attacker
|
| (public IP, port forwards)
|
+-----------------+
| DECNET host | eth0 -> decnet_lan (MACVLAN / IPvlan L2)
+--------+--------+
|
==============+=================== LAN 192.168.1.0/24
| | |
decky-01 decky-02 decky-03 (each = own IP,
.10 .11 .12 MACVLAN = own MAC too)
|
+-------------+-------------+
| decnet_macvlan0 (host) | hairpin: host <-> deckies reachability
+---------------------------+
--- isolated mgmt path ---
host -> logging/SIEM network
(not reachable from decnet_lan)
```
The decoy LAN is attacker-facing. The logging/aggregation network is reachable
only from the host and must not be routable from `decnet_lan`.
---
## Driver 1: MACVLAN (default)
MACVLAN is the default driver. Each decky gets a kernel-assigned MAC address on
the parent interface, so it is a distinct L2 endpoint on the LAN.
- Docker network name: `decnet_lan` (constant `MACVLAN_NETWORK_NAME`).
- Parent NIC: `--interface` (auto-detected via `detect_interface()` if omitted).
- Mode: `macvlan / bridge` (see `create_macvlan_network` and the
`ip link add ... type macvlan mode bridge` call in `setup_host_macvlan`).
- Requires the parent NIC to support **promiscuous mode**. Most wired NICs do.
### Host hairpin (`decnet_macvlan0`)
MACVLAN has a well-known limitation: the host cannot talk directly to its own
MACVLAN children on the parent NIC. Without a workaround, `decnet status` and
the log collector (which run on the host) would be unable to reach deckies.
DECNET fixes this by creating a second host-side MACVLAN interface named
`decnet_macvlan0` (constant `HOST_MACVLAN_IFACE`) attached to the same parent
NIC, assigning it a /32 on the host, and routing the decky IP range out of it.
The relevant sequence in `setup_host_macvlan`:
```
ip link add decnet_macvlan0 link <iface> type macvlan mode bridge
ip addr add <host_macvlan_ip>/32 dev decnet_macvlan0
ip link set decnet_macvlan0 up
ip route add <decky_range> dev decnet_macvlan0
```
---
## Driver 2: IPvlan L2 (`--ipvlan`)
Pass `--ipvlan` to opt into IPvlan L2. Each decky gets a unique IP but all
share the host MAC. This is the fallback for environments where MACVLAN is not
workable:
- **WiFi.** Most access points drop frames whose source MAC is not registered
with the radio, so random MACVLAN MACs never reach the LAN. IPvlan L2 works
because every frame leaves with the (registered) host MAC.
- NICs that refuse promiscuous mode.
### Host hairpin (`decnet_ipvlan0`)
Same trick as MACVLAN, different link type. `setup_host_ipvlan` adds a
host-side interface named `decnet_ipvlan0` (constant `HOST_IPVLAN_IFACE`) of
type `ipvlan mode l2`, gives it a /32, and routes the decky range through it.
`teardown_host_ipvlan` removes the route and the link on teardown.
### Trade-offs
| Aspect | MACVLAN | IPvlan L2 |
|--------------------|----------------------------------|---------------------------------|
| Per-decky MAC | yes (unique, kernel-assigned) | no (shares host MAC) |
| Works on WiFi | usually no | yes |
| Promisc NIC needed | yes | no |
| Decoy realism | higher (distinct L2 identities) | lower (all share one MAC) |
| Attacker fingerprinting via `arp -a` | sees many MACs | sees one MAC, many IPs |
Prefer MACVLAN on wired, bare-metal or VM deployments. Use IPvlan L2 when the
environment forces it.
---
## WiFi and WSL limitations
- **WiFi:** MACVLAN typically does not work. Use `--ipvlan`.
- **WSL2:** MACVLAN has known limitations under WSL. Bare metal or a full Linux
VM is the supported testing environment (README "Requirements" section).
- **Promiscuous mode:** some virtualization platforms (including vSphere with
default security policies) block promiscuous mode on the vNIC; either relax
the policy on the virtual switch or use IPvlan.
---
## Teardown
Teardown of the Docker network and the host hairpin interface is automatic
when you run `decnet teardown --all`. The relevant steps, from
`decnet/engine/deployer.py`:
```
if config.ipvlan:
teardown_host_ipvlan(decky_range)
else:
teardown_host_macvlan(decky_range)
remove_macvlan_network(client)
```
`teardown_host_macvlan` / `teardown_host_ipvlan` each do:
```
ip route del <decky_range> dev <iface> # decnet_macvlan0 or decnet_ipvlan0
ip link del <iface>
```
Both use `check=False`, so a partial or previously-cleaned state is safe.
Removing the Docker network `decnet_lan` happens separately via
`remove_macvlan_network`.
See [Teardown](Teardown-and-State) for the full teardown flow including
container removal and state-file cleanup.
---
## Quick checklist
- Host NIC is wired and supports promiscuous mode -> MACVLAN, no flag needed.
- Host NIC is WiFi, or promisc is blocked -> pass `--ipvlan`.
- Running under WSL -> switch to a Linux VM or bare metal.
- `decnet status` says a decky is unreachable from the host -> confirm
`decnet_macvlan0` (or `decnet_ipvlan0`) exists with `ip link show`.