Skip to content

Two-host field rehearsal

Everything placement can be shown on one machine, the suite already shows: tests/test_placement_rehearsal.py binds the data plane to a routable address, drives a sender at it, runs a full arm pair through a TLS listener, and checks that a remote bind takes the one interface it was given.

What one machine cannot show is the part that is not code. Packets over a real link. A host firewall between the sender and the listener. A certificate that has to name the address someone else dials. A sender you trigger rather than own.

This is the half-hour that closes it, and it is worth running before an engagement rather than during one.

What you need

A second host VM or container, reachable from the operator machine and able to reach it back. A Proxmox VM on the same LAN is ideal; a container on a different physical host also works. A container on the operator machine does not. It shares too much of the network stack to prove anything.
Python 3.9+ on it Standard library only. No pip, no build tools, no container runtime.
A way to copy one file scp, a paste buffer, anything.
One port open inbound on the operator machine The listener port. This is the step that most often fails.

Nothing is installed on the target and nothing is left behind but one file.

Run it

1. On the target host, with the operator machine's address:

scp scripts/field/target_host.py researcher@192.0.2.20:~/
ssh researcher@192.0.2.20 \
  'python3 target_host.py --bind 0.0.0.0 --upstream http://192.0.2.10:8443/'

It prints two URLs and waits. --bind 0.0.0.0 is right for a disposable lab host whose whole job is being reachable; AIT itself never does that, and the rehearsal checks it doesn't.

2. On the operator machine:

python3 scripts/field/rehearse.py \
  --target 192.0.2.20 \
  --listen 192.0.2.10:8443

What each phase establishes

Preflight: the receiver answers, the sender takes triggers, and the sender can open a connection to your listener address. That last one stands up a throwaway listener on the real port and asks the target to dial it, before AIT is started. It is asked from the target on purpose: your own curl to your own address proves nothing about what a firewall does to traffic arriving from off-box.

Phase 1: an off-host sender, gated. The listener binds your routable address, a sender on another machine reaches it, the message stops at the gate, you change the amount to 900, and the separate receiver fixture ledger records 900. Two network hops establish placement. The authored ledger remains a controlled observation source, not evidence about an external product.

Phase 2: a sender that will only speak HTTPS. A certificate naming your listen address, three arms across TLS: baseline inside policy, attack refused above the receiver's ceiling, close control inside again. That pattern is what makes the finding attributable rather than a reaction to being edited. Then intercept evidence scores it.

Phase 3: the record. ait run verify over the transcript written across a real network. Same hash chain a lab run produces.

When it fails

Most failures here are not bugs, and the script says which is which.

  • "sender can reach the operator listener" fails, port free locally. Something between the hosts is dropping it. On macOS check System Settings → Network → Firewall; on Linux check ufw/firewalld; on a VLAN check the switch. This is the single most common reason a placement does not work, and on an engagement it looks exactly like the tool being broken.
  • "target receiver reachable" fails. AIT must be able to deliver, or an intercepted message is one you have taken and cannot forward. Confirm target_host.py is running and its ports are open on the target.
  • TLS phases fail with a certificate error. The certificate has to name the address the sender dials. rehearse.py mints one for --listen; if your sender uses a hostname instead, the SAN must carry it.

Reusable rehearsal environment

Keep the topology generic and disposable:

Host Responsibility Required reachability
Target host sender trigger, receiver, and controlled ledger sender can dial the operator listener; operator can dial receiver
Operator host reviewed checkout, AIT control plane, and explicit data-plane listener control plane remains loopback; only the selected listener is routable

Use addresses reserved for your lab and record them in the private run manifest, not in public documentation. Destroy or reset both hosts after the rehearsal.

A failed cross-host dial can come from host firewalls, VLAN policy, wireless client isolation, routing, or certificate names. One observed network failure does not establish which of these is common. Test reachability from the sender before diagnosing the interception process.

What it still does not cover

A sender you genuinely cannot reconfigure. Every form of getting in path here ends with someone repointing the sender at AIT, which is the design. See Taking it to a real target. If you cannot influence where the sender points, this tool cannot be placed, and no rehearsal changes that.