Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "utils/reverse-traceroute-docker/revtrvp"]
path = utils/reverse-traceroute-docker/revtrvp
url = git@github.com:NEU-SNS/revtrvp.git
4 changes: 4 additions & 0 deletions utils/reverse-traceroute-docker/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
docker-compose.yml
certs/ca.crt
certs/client.crt
certs/client.key
101 changes: 101 additions & 0 deletions utils/reverse-traceroute-docker/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Docker Compose file for the Reverse Traceroute + PEERING stack.
COMPOSE_FILE := docker-compose.yml
TEMPLATES = templates/
CONFIGS = config/
# Hardcoded data-plane egress mux (see docs/superpowers/specs/2026-08-06-...).
EGRESS_MUX := ufmg01

.PHONY: clean up down build logs peering-shell announce withdraw \
bgp-status bgp-start dataplane-test setup-docker openvpn

clean:
rm -f $(COMPOSE_FILE)

# Start the stack detached, establish ufmg01 egress for all backends, then
# follow logs. Ctrl-C stops log follow only; use `make down` to tear down.
#
# Host-level prerequisite: disable bridge-nf-call-iptables first. On hosts
# where it's enabled (e.g. because libvirt turned it on for its own bridges),
# reply traffic that `peering` relays back to `revtrvp` across a backend
# Docker bridge gets silently dropped one-way -- confirmed via packet capture
# (revtrvp<->peering direct traffic works, but forwarded/relayed traffic
# doesn't) -- even though BGP/the tunnel/everything else is healthy. This is a
# host-wide sysctl (not scoped to this project's containers/networks) and
# does NOT persist across reboots, so we re-assert it every `make up` rather
# than relying on it being set already. See README Troubleshooting.
up:
sudo sysctl -w net.bridge.bridge-nf-call-iptables=0
docker compose -f $(COMPOSE_FILE) up -d
@echo "waiting for peering container..."
@for i in $$(seq 1 60); do \
if docker compose -f $(COMPOSE_FILE) exec -T peering true 2>/dev/null; then \
break; \
fi; \
if [ $$i -eq 60 ]; then \
echo "peering container not ready after 60s"; \
exit 1; \
fi; \
sleep 1; \
done
$(MAKE) openvpn mux=$(EGRESS_MUX)
docker compose -f $(COMPOSE_FILE) logs -f

# Bring up OpenVPN to mux=<name>. When mux is ufmg01, also install backend
# policy routes so all revtrvp traffic egresses via that mux.
openvpn:
@test -n "$(mux)" || (echo 'usage: make openvpn mux=<name>'; exit 1)
docker compose -f $(COMPOSE_FILE) exec peering \
/peering-openvpn-up.sh "$(mux)"

# Start BIRD/BGP toward whatever mux tunnels are already up.
bgp-start:
docker compose -f $(COMPOSE_FILE) exec peering ./peering bgp start

# Stop and remove containers, networks, and volumes created by compose.
down:
docker compose -f $(COMPOSE_FILE) down

# Build or rebuild service images before starting.
build:
docker compose -f $(COMPOSE_FILE) build

logs:
docker compose -f $(COMPOSE_FILE) logs revtrvp

peering-shell:
docker compose -f $(COMPOSE_FILE) exec peering bash

revtrvp-shell:
docker compose -f $(COMPOSE_FILE) exec revtrvp bash

# Announce the PEERING prefix to the egress mux (manual data-plane step).
# -R skips the client's built-in data-plane config; our Makefile/openvpn
# helper owns tunnel + policy routing.
announce:
docker compose -f $(COMPOSE_FILE) exec peering \
./peering prefix announce -R -m vtrtoronto 184.164.231.0/24

# Withdraw the PEERING prefix from all muxes.
withdraw:
docker compose -f $(COMPOSE_FILE) exec peering \
./peering prefix withdraw 184.164.231.0/24

# Show BGP session status (Idle before announce is normal).
bgp-status:
docker compose -f $(COMPOSE_FILE) exec peering ./peering bgp status

# Basic data-plane check: ping from inside the revtrvp container.
dataplane-test:
docker compose -f $(COMPOSE_FILE) exec revtrvp ping -c 4 1.1.1.1

# Generate docker-compose.yml from templates via scripts/setup_docker.py.
# Expands one backend-* network, peering .254 attachment, and revtrvp (.1)
# service per prefix in config/prefixes.txt.
setup-docker: clean
python3 scripts/setup_docker.py \
--compose-template $(TEMPLATES)/docker-compose.template.yml \
--backend-template $(TEMPLATES)/backend.template.yml \
--peering-attach-template $(TEMPLATES)/peering-backend-attach.template.yml \
--revtrvp-template $(TEMPLATES)/revtrvp.template.yml \
--prefixes $(CONFIGS)/prefixes.txt \
--output $(COMPOSE_FILE)
199 changes: 199 additions & 0 deletions utils/reverse-traceroute-docker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# Reverse Traceroute VP behind PEERING (Docker Compose)

This repo runs a [Reverse Traceroute](https://github.com/NEU-SNS/revtrvp) (RevTr)
vantage point (`revtrvp`) behind the [PEERING](https://peering.ee.columbia.edu/)
testbed using Docker Compose. A `peering` container acts as the gateway/router:
Makefile targets establish OpenVPN tunnels and BGP, and source-route each VP's
traffic out the hardcoded data-plane egress mux, so RevTr probes and control
connections originate from our allocated PEERING prefix(es).

- **Prefixes:** listed in `config/prefixes.txt` (one backend network + `revtrvp-*`
per prefix)
- **Data-plane egress mux:** `ufmg01` (tunnel device `tap7`, policy table `10007`)
- **Probing rate:** 100 pps

## Topology

```
host internet (NAT)
afrontend (NAT bridge, 172.16.50.0/24)
│ (peering's control plane:
│ OpenVPN->muxes, DNS, git)
┌──────┴───────┐
│ peering │ afrontend: 172.16.50.x (default route)
│ container │ tap7: OpenVPN tunnel to ufmg01 (egress)
│ (BIRD+OVPN) │ backend-*: .254 (gateway per prefix)
└──────┬───────┘
backend-* (one no-NAT bridge per prefix)
┌──────┴───────┐
│ revtrvp-* │ eth0: .1, default route -> peering .254
│ (per prefix)│
└──────────────┘
```

Networks:

- **`afrontend`** (NAT enabled): carries the `peering` container's *own* control
traffic (OpenVPN to muxes, DNS, git). Masqueraded out the host normally.
- **`backend-*`** (NAT disabled, one per prefix): carries that prefix between
`peering` (`.254`) and the matching `revtrvp-*` (`.1`). Masquerade is off so
the VP's source IP is preserved. IPAM/host-side gateway is parked at `.253`;
the real gateway each VP routes through is `peering` at `.254`.

All `revtrvp-*` forwarded traffic egresses via **`ufmg01`** (policy routing:
`iif <backend> → table 10007 → tap7`). Other mux tunnels (e.g. `vtrtoronto`) are
optional and used for BGP reachability/announcements, not data-plane egress.

## Prerequisites

- Docker + Docker Compose.
- PEERING client certificates in `certs/` (`ca.crt`, `client.crt`, `client.key`).
- Prefixes allocated to you listed in `config/prefixes.txt`.
- Passwordless (or interactive) `sudo` for `sysctl`: `make up` disables
`net.bridge.bridge-nf-call-iptables` on the host as a prerequisite step (see
Troubleshooting for why) and will prompt for a password if needed.

## Files of interest

- `templates/docker-compose.template.yml` / `templates/backend.template.yml` /
`templates/peering-backend-attach.template.yml` /
`templates/revtrvp.template.yml` — sources for the generated compose file;
one `backend-*` network, peering attachment, and `revtrvp-*` service is
expanded per prefix in `config/prefixes.txt`.
- `scripts/setup_docker.py` — renders `docker-compose.yml` from those templates
(`make setup-docker`).
- `docker-compose.yml` — generated stack (frontend + one backend network and
revtrvp per prefix).
- `scripts/peering-gateway-entrypoint.sh` — pins the default route to
`afrontend`, enables forwarding, stays alive (no OpenVPN/BGP).
- `scripts/peering-openvpn-up.sh` — brings up a mux tunnel; for `ufmg01`, also
installs backend→egress policy routes (`make openvpn` / `make up`).
- `scripts/revtrvp-entrypoint.sh` — points each VP's default route at the
gateway, then starts the VP.
- `config/prefixes.txt` / `config/prefixes6.txt` — PEERING prefix databases
(mounted into the client; also drive backend networks via `setup-docker`).
- `config/plvp.config` — RevTr VP config (interface `eth0`, scamper rate 100).
- `client/` and `revtrvp/` — git submodules for the PEERING client and the
RevTr VP.

## Usage

Regenerate `docker-compose.yml` after changing prefixes or templates:

```bash
make setup-docker
```

Build the images (first time; note `revtrvp` compiles scamper and can take a
few minutes):

```bash
make build
```

Start the stack. `make up` disables `net.bridge.bridge-nf-call-iptables`, starts
Compose detached, brings up the **`ufmg01`** egress tunnel and backend policy
routes, then follows logs. Ctrl-C stops log follow only; use `make down` to
tear down. BGP and prefix announcement are separate manual steps.

```bash
make up
# optional extra BGP muxes (tunnel only; egress stays ufmg01):
# make openvpn mux=vtrtoronto
make bgp-start
```

Announce a prefix over BGP (live action) and wait for convergence (~180s):

```bash
make announce
```

Check status and the data plane:

```bash
make bgp-status # sessions for connected muxes; Established after converge
make dataplane-test # ping 1.1.1.1 from inside a revtrvp container
make logs # revtrvp should connect to plcontroller.revtr.ccs.neu.edu
```

Tear down:

```bash
make withdraw # stop announcing the prefix
make down # stop and remove containers/networks
```

> Each `revtrvp-*` uses `restart: unless-stopped`. Before the prefix is
> announced it cannot resolve/reach the controller and will restart-loop; this
> is expected and it will connect once `make announce` has converged.

## Verification / troubleshooting

Inspect the egress tunnel and routing:

```bash
docker compose -f docker-compose.yml exec peering ip -4 addr show tap7
docker compose -f docker-compose.yml exec peering ip rule show
docker compose -f docker-compose.yml exec peering ip route show table 10007
```

Expected: `tap7` has an address in `100.71.128.0/24`; an `iif <backend-iface>`
rule points at table `10007` for each backend; table `10007` has
`default via 100.71.128.1 dev tap7`.

Inspect a VP (service name is prefix-derived, e.g. `revtrvp-184-164-231-0-24`):

```bash
docker compose -f docker-compose.yml exec revtrvp-184-164-231-0-24 ip route
# default via 184.164.231.254
```

Confirm probes carry the prefix source IP (after announce):

```bash
docker compose -f docker-compose.yml exec peering tcpdump -ni tap7
```

Common issues:

1. Prefix not announced yet → `revtrvp-*` restart-loops on DNS failure. Run
`make announce` and wait for convergence.
2. `announce` rejects the prefix → ensure it is present in `config/prefixes.txt`.
3. No egress after announce → check `rp_filter` is relaxed and source-routing is
installed (commands above); check `make bgp-status` shows `Established`.
Ensure `make up` (or `make openvpn mux=ufmg01`) ran successfully.
4. **Traceroute/ping from `revtrvp` reaches the `peering` gateway (`.254`) but
never progresses further, even though the prefix is announced, BGP shows
`Established`, and #3 above looks fine.** This is caused by
`net.bridge.bridge-nf-call-iptables=1` on the host (commonly enabled by
`libvirt` for its own bridges): it silently drops reply traffic that
`peering` *relays* back to `revtrvp` across a backend Docker bridge,
even though direct `peering`<->`revtrvp` traffic on that same bridge works
fine. Confirmed via packet capture in a real debugging session on this
exact stack: replies correctly arrived back through the tunnel and were
re-transmitted by `peering` onto the bridge with correct source/destination
MACs, yet never arrived at `revtrvp`'s interface. Scoped mitigations (a
`DOCKER-USER` iptables accept rule, `rp_filter=0`, `send_redirects=0`) did
**not** fix it in isolation — only disabling
`net.bridge.bridge-nf-call-iptables` globally did. `make up` now runs
`sudo sysctl -w net.bridge.bridge-nf-call-iptables=0` before starting
compose specifically to avoid this. If you bypassed `make up` (e.g. ran
`docker compose up` directly) and hit this, run that command manually, or
restart the stack via `make up`.
- This is a **host-wide** setting (affects all Docker bridges and any
`libvirt` VM networks on the machine, not just this project) and does
**not** persist across reboots — `make up` re-asserts it every time
rather than assuming it's already set.

## Configuration knobs

- **Egress mux** is hardcoded as `EGRESS_MUX=ufmg01` in the Makefile /
`scripts/peering-openvpn-up.sh` (tap/table derived from the mux OpenVPN
config: `tap7` → `100.71.128.1` / table `10007`).
- **Extra muxes** for BGP: `make openvpn mux=<name>` (does not change
data-plane egress).
- **Prefixes / addressing:** edit `config/prefixes.txt` and re-run
`make setup-docker`. Each prefix gets a `backend-*` network, peering at
`.254`, and a `revtrvp-*` at `.1` with `REVTR_GATEWAY=.254`.
Empty file.
18 changes: 18 additions & 0 deletions utils/reverse-traceroute-docker/config/plvp.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
environment:
debug: true
local:
autoconnect: true
proto: tcp
host: 'plcontroller.revtr.ccs.neu.edu'
port: 4380
pprofaddr: ':55557'
startscamp: true
rootca: /server.crt
interface: eth0
senderonly: false
scamper:
host: 'plcontroller.revtr.ccs.neu.edu'
binpath: /usr/local/bin/scamper
port: 4381
rate: 20
cafile: /server.crt
1 change: 1 addition & 0 deletions utils/reverse-traceroute-docker/revtrvp
Submodule revtrvp added at ce0e02
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# =============================================================================
# PEERING gateway entrypoint
# -----------------------------------------------------------------------------
# Runs inside the `peering` Compose service. Responsibilities:
# 1. Force the container's own control traffic (OpenVPN, DNS, git) out the
# NAT'd `afrontend` interface (172.16.50.0/24).
# 2. Enable IPv4 forwarding and relax global reverse-path filtering so later
# Makefile-driven mux/backend setup can install asymmetric routes.
# 3. Stay alive so `make openvpn` / `make bgp-start` / shells work.
#
# OpenVPN tunnels, backend policy routing, and BGP are NOT started here; see
# `make up` / `make openvpn` / `make bgp-start` and scripts/peering-openvpn-up.sh.
# =============================================================================
set -eu
set -x

CLIENT_DIR=/root/client
# Compose afrontend IPAM subnet; used to find the control-plane iface by addr.
AFRONTEND_PREFIX="172.16.50"

cd "$CLIENT_DIR"

# ----- 1. Identify afrontend and pin the default route ----------------------
# Interface names inside containers are not deterministic; resolve by subnet.
AF_IF=$(ip -o -4 addr show | awk -v pfx="${AFRONTEND_PREFIX}." '
$4 ~ "^"pfx { print $2; exit }
')
if [[ -z "${AF_IF}" ]]; then
echo "error: could not find afrontend iface on ${AFRONTEND_PREFIX}.0/24" >&2
exit 1
fi
# Docker assigns .1 of each user bridge subnet to the host side; use it as GW.
AF_GW=$(ip -o -4 addr show dev "$AF_IF" | awk '{print $4}' | sed 's#\.[0-9]*/.*#.1#')
ip route replace default via "$AF_GW" dev "$AF_IF"

# ----- 2. Forwarding + relaxed RPF (global) ---------------------------------
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv4.conf.all.rp_filter=0

# ----- 3. Stay alive --------------------------------------------------------
# Keep the service up for exec targets. Prefer existing up-down log if present;
# otherwise block forever.
mkdir -p var
touch var/up-down.log
exec tail -F var/up-down.log
Loading