Build a DTN Satellite Network
Simulate a 3-node satellite constellation using Bundle Protocol v7 and Rust.
Introduction
30 Second Summary
When a satellite orbits Earth, it can only talk to a ground station for a few minutes at a time. NASA and other space agencies solved this with a special kind of networking that stores messages during blackouts and delivers them when the link comes back.
In this project, you will build a simulated satellite constellation using Delay-Tolerant Networking, the same protocol family used in real space communications. You'll relay messages across three nodes, break the link, and watch them deliver themselves when the relay comes back online.
What You'll Build
You'll send a message from a simulated satellite, watch it relay through a chain to a ground station, then kill the relay and see the message queue up, survive the disruption, and deliver itself when the link recovers.
By the end of this project, you'll have:
- A 3-node satellite constellation running the Bundle Protocol v7 (RFC 9171), where you send a message from orbit and watch it hop through a relay satellite to a ground station.
- A live store-and-forward demonstration where you kill a relay satellite, see bundles queue up at the sender, and watch them auto-deliver when the link recovers.
- A monitoring dashboard powered by a custom Python metrics exporter, Prometheus, and Grafana, showing bundle delivery rates, peer connections, and node liveness across the constellation.
- An optional Secret Mission to push your skills further.
Do I need any experience before starting?
This project expects familiarity with terminal commands, basic networking concepts like ports and TCP, and some Python. You'll need Docker, Rust with Cargo, and Python 3 installed on your machine.
Before We Start
Before you start compiling daemons and wiring up satellite nodes, take a moment to commit to what you are building and why it matters.
Build dtn7-rs and Launch Your First Satellite
Before you can simulate a satellite constellation, you need the DTN daemon compiled and running. dtn7-rs is a Rust implementation of the Bundle Protocol v7 (RFC 9171), the same protocol family NASA uses for space communications.
In this step, you will clone the dtn7-rs source code, compile it into a set of command-line tools, and launch your very first satellite node.
In this step, get ready to:
- Clone and compile dtn7-rs from source.
- Create a configuration file for the first satellite node.
- Start the DTN daemon and verify it is running.
Clone the dtn7-rs repository and build from source
The dtn7-rs project provides the dtnd daemon and a set of CLI tools for sending, receiving, and querying bundles. You will build everything from source using Cargo, the Rust package manager.
- Move to your Desktop by running this command:
cd ~/Desktop
- Clone the dtn7-rs repository at the v0.20.0 tag by running this command:
git clone --branch v0.20.0 https://github.com/dtn7/dtn7-rs.git
- Confirm the repository was cloned by running:
ls
You should see a dtn7-rs folder in the output.
- Move into the repository and build the project by running these commands:
cd dtn7-rs
cargo build --release --locked
What does this build?
The --release flag compiles optimized binaries. The --locked flag pins all dependency versions to exactly what the project's Cargo.lock specifies, preventing build failures from upstream dependency changes.
This produces four binaries in target/release/: dtnd (the daemon), dtnsend (sends bundles), dtnrecv (receives bundles), and dtnquery (queries node status).
Build takes 5-8 minutes
Rust compiles everything from source, so this takes a while. Read the next substep while it compiles so you are ready to go as soon as it finishes.
- Once the build finishes, verify the binaries exist by running:
ls target/release/dtnd target/release/dtnsend target/release/dtnrecv target/release/dtnquery
You should see all four file paths listed in the output.
Build failed with a serde error?
If you see an error mentioning serde::__private::TryFrom, make sure you included the --locked flag. Without it, Cargo may pull a newer version of the serde crate that is incompatible with v0.20.0.
Delete the target folder and rebuild:
rm -rf target
cargo build --release --locked
help me debug a dtn7-rs build failure
Create the sat-alpha configuration file
Each DTN node needs a TOML configuration file that defines its identity, networking, and routing behavior. Your first satellite, sat-alpha, will use epidemic routing and listen for connections on a convergence layer port.
- Open the dtn7-rs folder in your code editor.
- Create a new file called sat-alpha.toml in the root of the dtn7-rs folder.
- Paste the following configuration into sat-alpha.toml:
# Node identity and storage
nodeid = "sat-alpha"
debug = false
webport = 4001
workdir = "/tmp/dtn7-sat-alpha"
db = "mem"
[routing]
strategy = "epidemic"
[core]
janitor = "10s"
# Disable UDP discovery, use static peers instead
[discovery]
interval = "0s"
# MTCP convergence layer on port 16161
[convergencylayers]
cla.0.id = "mtcp"
cla.0.port = 16161
# Next hop: sat-bravo
[statics]
peers = [
"mtcp://127.0.0.1:16162/sat-bravo",
]
[endpoints]
local.0 = "incoming"
What does this configuration do?
- nodeid = "sat-alpha" names this node. It becomes reachable at dtn://sat-alpha/.
- db = "mem" stores bundles in memory. No data persists after the daemon stops.
- strategy = "epidemic" replicates bundles to every known peer, guaranteeing delivery without complex contact schedules.
- interval = "0s" disables automatic peer discovery. You will define peers explicitly in the [statics] section instead.
- cla.0.id = "mtcp" enables the Minimal TCP convergence layer, the transport protocol DTN nodes use to exchange bundles.
- The [statics] section pre-configures sat-bravo as a peer. Sat-bravo does not exist yet, so this connection will come alive in Step 2.
- Save sat-alpha.toml.
- Confirm the file exists by running this in your terminal:
ls sat-alpha.toml
You should see sat-alpha.toml in the output.
File not found?
Make sure your terminal is inside the dtn7-rs folder (the same folder where Cargo.toml lives). Also confirm you saved the file in your editor, not just pasted it.
help me find my sat-alpha.toml file
✔️ Awesome, I've got everything!
Great. Double check you have saved sat-alpha.toml and you are ready to launch the daemon.
ⓧ I'd like to double check the full code
Here is the complete sat-alpha.toml file. Compare it character-by-character with yours:
nodeid = "sat-alpha"
debug = false
webport = 4001
workdir = "/tmp/dtn7-sat-alpha"
db = "mem"
[routing]
strategy = "epidemic"
[core]
janitor = "10s"
[discovery]
interval = "0s"
[convergencylayers]
cla.0.id = "mtcp"
cla.0.port = 16161
[statics]
peers = [
"mtcp://127.0.0.1:16162/sat-bravo",
]
[endpoints]
local.0 = "incoming"
Start the daemon and verify the web interface
With the binary compiled and the config file ready, you can launch your first satellite. The dtnd daemon runs in the foreground and takes over the terminal window.
- Start the sat-alpha daemon by running this command:
./target/release/dtnd -c sat-alpha.toml
What does this command do?
The -c flag tells dtnd to load configuration from your TOML file instead of using CLI flags. The daemon starts a web interface on port 4001, opens the MTCP convergence layer on port 16161, and begins its janitor cycle to manage stored bundles.
This process runs in the foreground. You will need to open a new terminal tab or window for any further commands.
Before you check the browser, predict what you expect to see at http://localhost:4001. Will it show a blank page, an error, or something else?
- Open your browser and navigate to http://localhost:4001.
You should see the dtn7 status page showing your node sat-alpha running. The page displays the node's endpoint ID, known peers, and bundle statistics.
Page not loading?
- Check that the dtnd process is still running in your terminal. If it exited with an error, re-read the error message for clues.
- Make sure no other process is using port 4001. You can check with lsof -i :4001 (macOS/Linux).
- Verify the URL is exactly http://localhost:4001 (not https).
help me get the dtnd web interface working
Your first satellite is in orbit. Next up, you will launch two more nodes and send a message across the constellation.
Deploy a 3-Node Satellite Constellation
Your first satellite is online. sat-alpha is running, its web UI confirms the daemon works, and it can store bundles. But a single node has no one to talk to.
DTN's real power is multi-hop relay networking. In this step, you'll add two more nodes to form a chain: sat-alpha relays through a second satellite to a ground station. By the end, a message sent from orbit will hop through the relay and land at its destination.
In this step, get ready to:
- Create configuration files for a relay satellite and a ground station.
- Launch all three nodes in a chain topology.
- Send a bundle across the constellation and receive it at the ground station.
Create the relay satellite and ground station configs
Your three nodes form a chain. sat-alpha connects to sat-bravo, and sat-bravo connects to ground-station. The relay satellite in the middle is the only node that knows about both neighbors.
- Create a new file called sat-bravo.toml in the same directory as sat-alpha.toml and paste this configuration:
nodeid = "sat-bravo"
debug = false
webport = 4002
workdir = "/tmp/dtn7-sat-bravo"
db = "mem"
[routing]
strategy = "epidemic"
[core]
janitor = "10s"
[discovery]
interval = "0s"
[convergencylayers]
cla.0.id = "mtcp"
cla.0.port = 16162
[statics]
peers = [
"mtcp://127.0.0.1:16161/sat-alpha",
"mtcp://127.0.0.1:16163/ground-station",
]
[endpoints]
local.0 = "incoming"
What makes sat-bravo the relay?
Look at the [statics] section. sat-bravo lists two peers: sat-alpha on port 16161 and ground-station on port 16163. This makes it the bridge between the two endpoints.
The MTCP convergence layer runs on port 16162 so it doesn't collide with sat-alpha's port 16161. The web UI uses port 4002 for the same reason.
- Save sat-bravo.toml.
- Confirm the file exists by running:
ls sat-bravo.toml
You should see sat-bravo.toml listed in the output.
File not found?
- Make sure you saved the file in the same directory where sat-alpha.toml lives. Run ls *.toml to see all TOML files in your current directory.
- Check that your terminal is in the dtn7-rs directory where you built the project.
Help me find where my config file was saved.
✔️ Awesome, I've got everything!
Great. Make sure you saved sat-bravo.toml and move on to the ground station config.
ⓧ I'd like to double check the full code
nodeid = "sat-bravo"
debug = false
webport = 4002
workdir = "/tmp/dtn7-sat-bravo"
db = "mem"
[routing]
strategy = "epidemic"
[core]
janitor = "10s"
[discovery]
interval = "0s"
[convergencylayers]
cla.0.id = "mtcp"
cla.0.port = 16162
[statics]
peers = [
"mtcp://127.0.0.1:16161/sat-alpha",
"mtcp://127.0.0.1:16163/ground-station",
]
[endpoints]
local.0 = "incoming"
- Create another new file called ground-station.toml in the same directory and paste this configuration:
nodeid = "ground-station"
debug = false
webport = 4003
workdir = "/tmp/dtn7-ground-station"
db = "mem"
[routing]
strategy = "epidemic"
[core]
janitor = "10s"
[discovery]
interval = "0s"
[convergencylayers]
cla.0.id = "mtcp"
cla.0.port = 16163
[statics]
peers = [
"mtcp://127.0.0.1:16162/sat-bravo",
]
[endpoints]
local.0 = "incoming"
How does the ground station differ from sat-bravo?
The ground station only peers with sat-bravo. It has no direct connection to sat-alpha. Any bundle from sat-alpha must hop through sat-bravo first.
It runs its MTCP convergence layer on port 16163 and its web UI on port 4003. Each node in the constellation gets its own unique pair of ports.
- Save ground-station.toml.
- Confirm all three config files exist by running:
ls *.toml
You should see sat-alpha.toml, sat-bravo.toml, and ground-station.toml listed.
Missing a config file?
- If you see only one or two files, check that you saved each file with the correct name and the .toml extension.
- Make sure all three files are in the same directory. Run pwd to confirm your terminal is in the dtn7-rs directory.
Help me find my missing config files.
✔️ Awesome, I've got everything!
Great. Make sure both files are saved and let's launch the constellation.
ⓧ I'd like to double check the full code
Compare your ground-station.toml against this:
nodeid = "ground-station"
debug = false
webport = 4003
workdir = "/tmp/dtn7-ground-station"
db = "mem"
[routing]
strategy = "epidemic"
[core]
janitor = "10s"
[discovery]
interval = "0s"
[convergencylayers]
cla.0.id = "mtcp"
cla.0.port = 16163
[statics]
peers = [
"mtcp://127.0.0.1:16162/sat-bravo",
]
[endpoints]
local.0 = "incoming"
Launch the full constellation
sat-alpha is already running from Step 1. You need to start sat-bravo and ground-station in their own terminals so all three daemons run simultaneously.
- Open a new terminal tab or window.
- Navigate to your dtn7-rs directory (the same directory where your TOML config files live).
- Start the sat-bravo daemon by running this command:
./target/release/dtnd -c sat-bravo.toml
- Open http://localhost:4002 in your browser.
You should see the dtn7 status page showing node "sat-bravo" running.
- Open a third terminal tab or window.
- Navigate to your dtn7-rs directory again.
- Start the ground-station daemon by running this command:
./target/release/dtnd -c ground-station.toml
- Open http://localhost:4003 in your browser.
You should see the dtn7 status page showing node "ground-station" running. You now have three DTN nodes running across three terminals.
Node won't start?
- If you see a port conflict error, check that no other process is using ports 4002/16162 or 4003/16163.
- Double check that the port numbers in your TOML file match the values above. Each node must have unique web and MTCP ports.
- Make sure you're running the command from the dtn7-rs directory where ./target/release/dtnd exists.
Help me debug why my DTN node won't start.
Send a bundle from orbit to ground
Your constellation is live. Three nodes are running in a chain, but you haven't proven that bundles actually relay through the middle. Time to send a message from sat-alpha all the way to ground-station.
- Open a fourth terminal tab or window.
- Navigate to your dtn7-rs directory.
- Send a bundle from sat-alpha to the ground station by running this command:
echo "Hello Ground" | ./target/release/dtnsend -p 4001 -r dtn://ground-station/incoming
What does this command do?
- echo "Hello Ground" pipes the text as the bundle payload.
- -p 4001 tells dtnsend to connect to sat-alpha's web port. This is the sending node.
- -r dtn://ground-station/incoming sets the destination endpoint. The bundle needs to reach the incoming endpoint on ground-station.
sat-alpha has no direct connection to ground-station. Epidemic routing replicates the bundle to every known peer, so it hops through sat-bravo automatically.
The bundle is now somewhere in the constellation. Before you retrieve it, think about what you expect to happen. sat-alpha has no direct peer connection to ground-station. Will the message make it there?
- Receive the bundle at the ground station by running this command:
./target/release/dtnrecv -p 4003 -e incoming
You should see Hello Ground printed in the terminal. The bundle traveled from sat-alpha, hopped through sat-bravo, and arrived at ground-station. Multi-hop DTN relay networking is working.
Not seeing the bundle?
- Make sure all three dtnd processes are still running. Check each terminal to confirm none of them crashed.
- Verify that sat-bravo's [statics] section lists both sat-alpha and ground-station as peers. If sat-bravo only peers with one neighbor, it can't relay.
- Wait a few seconds and try dtnrecv again. The janitor cycle runs every 10 seconds, so bundles may take a moment to forward through the relay.
Help me debug why my bundle didn't arrive.
Your constellation is fully operational. Three nodes, one relay chain, and a bundle that just traveled from orbit to the ground. Next up, you'll break the relay and see if DTN can survive the disruption.
Survive an Orbital Disruption
Your constellation is online and bundles are flowing from orbit to the ground. In a real satellite network, that happy state lasts until a satellite dips below the horizon or a solar storm knocks out a transponder.
The whole point of Delay-Tolerant Networking is surviving exactly that kind of disruption. In this step, you will kill the relay satellite, try to send a bundle across the broken link, and then bring the relay back to see DTN's store-and-forward mechanism rescue the delivery.
In this step, get ready to:
- Kill the relay satellite and break the constellation's only forwarding path.
- Send a bundle into the broken link and see it queue up at the sender.
- Restart the relay and watch store-and-forward automatically deliver the waiting bundle.
Kill the relay satellite
sat-bravo is the only link between sat-alpha and ground-station. Killing it simulates a satellite dropping out of communication range.
- Switch to the terminal where sat-bravo is running.
- Press Ctrl+C to stop the dtnd process.
The sat-bravo daemon shuts down immediately. The chain between sat-alpha and ground-station is now severed.
- Confirm sat-bravo is gone by opening http://localhost:4002 in your browser.
The page should fail to load. That confirms the relay satellite is offline.
Page still loading at port 4002?
If the web UI at port 4002 still responds, the wrong process may have stopped. Check that the terminal you pressed Ctrl+C in was running sat-bravo.toml, not one of the other nodes.
Need help identifying the right process?
Send a bundle into the broken link
With sat-bravo gone, there is no path from sat-alpha to ground-station. What happens to a bundle you send right now?
Before you run the next command, predict what will happen. Will the bundle reach ground-station, or will something else happen to it?
- In the terminal that was running sat-bravo (now free), send a bundle from sat-alpha to ground-station by running this command:
echo "Surviving disruption" | ./target/release/dtnsend -p 4001 -r dtn://ground-station/incoming -l 3600
What does this command do?
- echo "Surviving disruption" pipes the payload text into dtnsend.
- -p 4001 tells dtnsend to connect to sat-alpha's web port.
- -r dtn://ground-station/incoming sets the destination endpoint on the ground station.
- -l 3600 gives the bundle a lifetime of 3600 seconds (one hour). If the bundle cannot be delivered immediately, the DTN node holds onto it until a path opens up or the lifetime expires.
The command returns without an error. The bundle did not reach ground-station, though. It is stuck at sat-alpha because there is no forwarding path.
- Confirm the bundle is queued by opening http://localhost:4001 in your browser to see sat-alpha's status overview.
You should see that sat-alpha has stored bundles waiting for delivery. The bundle is patiently sitting in memory, waiting for a route to open.
What is store-and-forward?
In traditional TCP networking, if the destination is unreachable, the message is dropped. DTN works differently.
Each DTN node stores bundles locally until a viable next hop appears. When a peer reconnects, the stored bundles are automatically forwarded. This is what makes DTN suitable for space communications where inter-satellite links go up and down as satellites orbit.
Restart the relay and watch store-and-forward deliver
The bundle is sitting at sat-alpha, waiting patiently. Time to bring the relay back online and see if DTN keeps its promise.
- In the terminal where sat-bravo was running, restart it by running this command:
./target/release/dtnd -c sat-bravo.toml
- Confirm sat-bravo is back by opening http://localhost:4002 in your browser. The status page should load again.
- Wait about 15 seconds for the janitor cycle to run and peers to reconnect.
Why wait 15 seconds?
Each node's janitor runs every 10 seconds (set by janitor = "10s" in the config). The janitor checks for stored bundles and attempts to forward them.
After sat-bravo reconnects, the next janitor cycle on sat-alpha detects the restored path and pushes the queued bundle through sat-bravo to ground-station.
Do you think the bundle made it through to ground-station? There is one way to find out.
- Open a new terminal and navigate to the dtn7-rs directory.
- Receive the bundle at the ground station by running this command:
./target/release/dtnrecv -p 4003 -e incoming
You should see Surviving disruption printed to the terminal. The bundle survived the outage, waited at sat-alpha, and was automatically delivered once the relay came back online.
Not seeing the bundle?
If dtnrecv returns without output, the bundle may not have been forwarded yet. Wait another 15 seconds for the next janitor cycle, then run the dtnrecv command again.
If you still see nothing, check that all three nodes are running by visiting their web UIs at ports 4001, 4002, and 4003. Make sure sat-bravo's config still has peers pointing to both sat-alpha and ground-station.
Help me troubleshoot my DTN bundle delivery.
You just proved that DTN can survive a broken link and automatically recover. Next up, you will build a metrics exporter so you can watch this happening in real time on a dashboard.
Build a DTN Metrics Exporter
Your constellation can send bundles across multiple hops and survive link outages. That is real progress. But right now you have zero visibility into what is happening inside each node.
Since dtn7-rs does not expose a native Prometheus endpoint, you will build your own in Python. Your exporter polls each node's REST API, translates the response into Prometheus-format metrics, and serves them on a single HTTP port.
In this step, get ready to:
- Set up a Python virtual environment with prometheus-client and requests.
- Write a custom metrics exporter that polls all three DTN nodes.
- Verify Prometheus-format metrics at http://localhost:8000.
Set up the Python environment
Your three dtnd processes are still running in their own terminals. You need a fresh terminal for the Python work.
- Open a new terminal window. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or cmd (Windows) and press Enter to open it.
- Navigate to the same directory where your TOML config files live by running:
cd ~/Desktop/dtn7-rs
- Create a Python virtual environment by running:
macOS
python3 -m venv venv
source venv/bin/activate
Windows
python -m venv venv
venv\Scripts\activate
Your terminal prompt now shows (venv) at the beginning, confirming you are inside the virtual environment.
- Create a requirements.txt file in your code editor with the following contents:
prometheus-client
requests
- Install the dependencies by running:
pip install -r requirements.txt
You should see pip download and install both packages along with their dependencies.
- Confirm the packages are installed by running:
pip list
You should see prometheus-client and requests in the output list.
Don't see the packages?
Make sure your virtual environment is active. Your prompt should show (venv) at the beginning. If not, re-run the activation command from the platform tab above.
help me activate my Python virtual environment and install prometheus-client and requests
Write the metrics definitions
Each dtnd node exposes a /status/info REST endpoint that returns JSON with bundle statistics. Your exporter needs to poll that endpoint and translate the numbers into Prometheus gauge metrics.
- Create a new file called exporter.py in the same directory as your TOML files and paste the following code:
import time
import requests
from prometheus_client import start_http_server, Gauge
# Map each DTN node name to its REST API base URL
NODES = {
"sat_alpha": "http://127.0.0.1:4001",
"sat_bravo": "http://127.0.0.1:4002",
"ground_station": "http://127.0.0.1:4003",
}
# Define a Prometheus Gauge for each bundle statistic, labeled by node
bundles_incoming = Gauge("dtn_bundles_incoming", "Incoming bundle count", ["node"])
bundles_outgoing = Gauge("dtn_bundles_outgoing", "Outgoing bundle count", ["node"])
bundles_delivered = Gauge("dtn_bundles_delivered", "Delivered bundle count", ["node"])
bundles_broken = Gauge("dtn_bundles_broken", "Broken bundle count", ["node"])
node_up = Gauge("dtn_node_up", "Whether the DTN node is reachable", ["node"])
What does this code do?
- NODES maps a friendly name for each satellite or ground station to the URL where its dtnd web interface listens. The ports match the webport values in your TOML configs.
- Each Gauge is a Prometheus metric that can go up or down. The third argument ["node"] adds a label so Prometheus can distinguish which satellite reported the value.
- dtn_node_up tracks whether a node is reachable at all. If a node goes down (like when you killed sat-bravo), this gauge drops to 0.
- Save exporter.py.
Add the collection loop and start the exporter
The gauge definitions tell Prometheus what metrics exist. Now you need the logic that actually fetches the data from each node and updates those gauges in a loop.
- In exporter.py, add the following code below the node_up gauge definition:
def collect_metrics():
for name, url in NODES.items():
try:
# Poll the node's /status/info REST endpoint
resp = requests.get(f"{url}/status/info", timeout=2)
data = resp.json()
# Update each gauge from the JSON response
bundles_incoming.labels(node=name).set(data.get("incoming", 0))
bundles_outgoing.labels(node=name).set(data.get("outgoing", 0))
bundles_delivered.labels(node=name).set(data.get("delivered", 0))
bundles_broken.labels(node=name).set(data.get("broken", 0))
node_up.labels(node=name).set(1)
except Exception:
# If the request fails, mark the node as down
node_up.labels(node=name).set(0)
if __name__ == "__main__":
# Expose metrics on port 8000 and poll every 5 seconds
start_http_server(8000)
print("DTN metrics exporter running on http://localhost:8000")
while True:
collect_metrics()
time.sleep(5)
What does this code do?
- collect_metrics() loops through every node in the NODES dictionary. For each one, it sends a GET request to /status/info and reads the JSON response.
- The JSON fields incoming, outgoing, delivered, and broken come directly from the dtn7-rs REST API. Each value updates the corresponding Prometheus gauge.
- If a request fails (the node is down or unreachable), the except block sets dtn_node_up to 0 for that node. This is exactly how you will detect outages in Grafana later.
- start_http_server(8000) launches a lightweight HTTP server in a background thread. Prometheus (or your browser) can scrape the metrics by hitting port 8000.
- Save exporter.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved exporter.py before continuing.
ⓧ I'd like to double check the full code
Your exporter.py should contain two sections you built above, in this order:
- Chunk 1: The imports, the NODES dictionary, and five Gauge definitions (bundles_incoming through node_up).
- Chunk 2: The collect_metrics() function and the if __name__ == "__main__" block at the bottom.
Scroll up and confirm both chunks are present and in order. Make sure there are no leftover placeholder lines.
Before you run the exporter, think about what you expect to see. All three nodes are running, so what should dtn_node_up show for each one?
- Start the exporter by running:
python exporter.py
You should see the message DTN metrics exporter running on http://localhost:8000 printed in your terminal. The exporter is now polling your three nodes every 5 seconds.
- Open http://localhost:8000 in your browser.
You should see a wall of Prometheus-format text. Scroll through and look for lines like dtn_bundles_delivered, dtn_bundles_incoming, and dtn_node_up. Each metric line includes a node label identifying which satellite or ground station reported it.
What should I see?
Each of your custom metrics appears with # HELP and # TYPE header lines, followed by one sample line per node. For example, dtn_node_up should show three lines with node="sat_alpha", node="sat_bravo", and node="ground_station", all set to 1.0 because every node is reachable.
You will also see Python runtime metrics that prometheus-client adds automatically. Those are normal.
Seeing connection refused or empty metrics?
- Make sure all three dtnd processes are still running. Check that their web UIs load at ports 4001, 4002, and 4003.
- Confirm your virtual environment is active ((venv) in your prompt) and that you ran pip install -r requirements.txt inside it.
- If port 8000 is already in use, you will see an OSError when starting the exporter. Stop whatever process is using that port and try again.
help me debug my DTN metrics exporter that is not showing metrics at localhost:8000
Your constellation now has eyes. Every five seconds, the exporter captures a snapshot of bundle traffic and node health across all three satellites. Next up, you will pipe these metrics into Prometheus and Grafana to build a live monitoring dashboard.
Launch Prometheus and Grafana
Your Python exporter is collecting metrics from all three DTN nodes and exposing them on port 8000. That is great for debugging, but staring at raw text is not how you monitor a satellite constellation.
Prometheus scrapes and stores those metrics as time-series data. Grafana turns them into live dashboards you can watch while you break and fix the network. In this step, you will spin up both using Docker Compose and build a real-time view of the entire constellation.
In this step, get ready to:
- Create a Docker Compose stack with Prometheus and Grafana.
- Verify Prometheus is scraping the DTN metrics exporter.
- Build a live Grafana dashboard and test it against a simulated disruption.
Create the Docker Compose file
You need two services running side by side. Prometheus will poll your exporter every few seconds and store the results. Grafana will query Prometheus and render the data as panels.
- Create a file called docker-compose.yml in your project folder by pasting the following:
services:
# Prometheus scrapes the DTN exporter and stores time-series data
prometheus:
image: prom/prometheus:v3.14.0
container_name: dtn-prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
extra_hosts:
- "host.docker.internal:host-gateway"
# Grafana visualizes Prometheus metrics as live dashboards
grafana:
image: grafana/grafana:13.2.0
container_name: dtn-grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
volumes:
- ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro
What does this file do?
The prometheus service runs Prometheus v3.14.0, mounts a config file you will create next, and exposes port 9090. The extra_hosts line maps host.docker.internal to your host machine so Prometheus inside Docker can reach the exporter running on your host.
The grafana service runs Grafana 13.2.0 on port 3000. The environment variables enable anonymous admin access so you can skip the login screen. The volumes line auto-provisions Prometheus as a datasource from a YAML file you will create shortly.
- Save docker-compose.yml.
Create the Prometheus and Grafana config files
Prometheus needs a scrape config telling it where to find your exporter. Grafana needs a datasource provisioning file so it automatically connects to Prometheus on startup.
- Create a file called prometheus.yml in the same folder by pasting the following:
global:
scrape_interval: 5s
scrape_configs:
- job_name: "dtn-exporter"
static_configs:
- targets: ["host.docker.internal:8000"]
What does this config do?
The scrape_interval: 5s tells Prometheus to pull metrics from every target every 5 seconds. The single job dtn-exporter points at host.docker.internal:8000, which is your Python exporter running on the host machine.
- Save prometheus.yml.
- Create the Grafana provisioning directory by running this command:
mkdir -p grafana/provisioning/datasources
- Create a file called grafana/provisioning/datasources/prometheus.yml by pasting the following:
apiVersion: 1
datasources:
# Auto-configure Prometheus as Grafana's default data source
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true
Why provision a datasource this way?
Without this file you would need to manually configure Prometheus as a datasource through Grafana's UI every time you start the stack. The provisioning file does it automatically. The url uses http://prometheus:9090 because inside the Docker Compose network, services reference each other by name.
- Save grafana/provisioning/datasources/prometheus.yml.
- Confirm all the new files exist by running:
ls
You should see docker-compose.yml and prometheus.yml alongside your existing files. Also confirm the nested directory:
ls grafana/provisioning/datasources/
You should see prometheus.yml listed inside.
✔️ Awesome, I've got everything!
Great. Make sure all three files are saved before you continue.
ⓧ I'd like to double check the full code
Compare your files against these references.
services:
prometheus:
image: prom/prometheus:v3.14.0
container_name: dtn-prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
extra_hosts:
- "host.docker.internal:host-gateway"
grafana:
image: grafana/grafana:13.2.0
container_name: dtn-grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
volumes:
- ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro
global:
scrape_interval: 5s
scrape_configs:
- job_name: "dtn-exporter"
static_configs:
- targets: ["host.docker.internal:8000"]
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true
Missing a file?
If grafana/provisioning/datasources/prometheus.yml is missing, make sure you ran the mkdir -p command before creating the file. The -p flag creates all intermediate directories.
Still stuck?
Start the monitoring stack and verify Prometheus
With the config files in place, you can bring both services up. Make sure your Python exporter from the previous step is still running on port 8000 before you continue.
- Start the Docker Compose stack by running:
docker compose up -d
What does this command do?
The up command creates and starts both the Prometheus and Grafana containers. The -d flag runs them in detached mode so your terminal stays free. Docker pulls the images the first time, which may take a minute.
- Verify both containers are running:
docker ps
You should see two containers: dtn-prometheus and dtn-grafana, both with status Up.
Before you check the Prometheus targets page, what do you expect to see for the exporter target? Will it show as UP or DOWN?
- Open http://localhost:9090/targets in your browser.
Under the dtn-exporter job, you should see an endpoint with the state UP in green. This confirms Prometheus inside Docker is successfully reaching your host-based Python exporter through the host.docker.internal mapping.
Target showing as DOWN?
Make sure your Python exporter (exporter.py) is still running in a separate terminal. If you closed it, restart it with python exporter.py.
On Linux, the extra_hosts line in docker-compose.yml is required for host.docker.internal to resolve. Verify that extra_hosts: ["host.docker.internal:host-gateway"] is present under the prometheus service.
Help me debug my Prometheus target
Build a Grafana dashboard and break the constellation
Prometheus is scraping your exporter. Now you need a dashboard to visualize what is happening across the constellation in real time.
- Open http://localhost:3000 in your browser.
- If prompted to log in, enter admin for both username and password.
- Click Dashboards in the main menu on the left.
- Click New and select New Dashboard.
- Click + Add visualization.
- Select Prometheus as the data source.
You are now in the panel editor. This is where you define which metric to display.
- In the query editor at the bottom, enter dtn_bundles_delivered as the metric.
- In the Legend field, enter {{node}} so each node gets its own line.
- Click Refresh to query the data source.
- Under Panel options on the right, set the title to Bundles Delivered.
- Click Save in the top-right corner.
You should see a time-series graph with a line for each of your three DTN nodes. Now add two more panels.
- Click Back to dashboard.
- Click the Add new element icon (blue plus sign) and add a new panel.
- Set the metric to dtn_node_up, the legend to {{node}}, and the title to Node Liveness.
- Click Save.
- Click Back to dashboard and add one more panel.
- Set the metric to dtn_bundles_incoming, the legend to {{node}}, and the title to Bundles Incoming.
- Click Save.
- Click Back to dashboard, then click the save icon at the top of the dashboard.
- Enter DTN Constellation as the dashboard name and click Save.
Your dashboard now shows three panels tracking the health and activity of every node in the constellation.
What do these panels show?
The dtn_bundles_delivered panel shows how many bundles each node has successfully delivered. The dtn_node_up panel shows 1 when a node is reachable and 0 when it is down. The dtn_bundles_incoming panel tracks how many bundles have arrived at each node.
Time to put the dashboard to the test. What do you think will happen to the Node Liveness panel when you kill sat-bravo?
- Switch to the terminal where sat-bravo is running and press Ctrl+C to kill it.
- Return to your Grafana dashboard in the browser and watch the Node Liveness panel.
Within 10 seconds, the dtn_node_up line for sat_bravo drops from 1 to 0. You are watching a simulated satellite disappear from the network in real time.
- Restart sat-bravo by running:
./target/release/dtnd -c sat-bravo.toml
- Watch the Node Liveness panel again.
The sat_bravo line jumps back to 1 as the relay satellite comes back online. Your Grafana dashboard is now a live mission control for the constellation.
Dashboard not updating?
Make sure your Python exporter (exporter.py) is still running. The exporter polls every 5 seconds, and Prometheus scrapes every 5 seconds, so updates can take up to 10 seconds to appear.
If panels show no data, click on a panel and check that the data source is set to Prometheus and the metric name is spelled correctly.
Help me fix my Grafana dashboard
Secret mission
Implement Contact-Schedule-Aware Routing
Your constellation uses epidemic routing, which blindly copies every bundle to every connected peer. Real satellite networks cannot afford that bandwidth. In this Secret Mission, you will replace epidemic routing on sat-alpha with a custom Python routing engine that connects to the dtn7-rs external routing WebSocket API and makes intelligent forwarding decisions based on which peers are currently reachable.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Three dtnd daemon processes (sat-alpha, sat-bravo, ground-station).
- Python processes (exporter.py and contact_router.py if you completed the Secret Mission).
- Prometheus and Grafana Docker containers.
- Cloned dtn7-rs repository and project files on your Desktop.
- DTN working directories under /tmp/dtn7-*.
Keep everything running
No action needed. Choose this if you are still actively experimenting with your DTN constellation.
- Leave the three dtnd daemons and the Python exporter running to keep sending bundles and watching metrics.
- Leave the Prometheus and Grafana containers running to continue monitoring your dashboard.
- All processes use in-memory storage and run locally, so there are no costs.
Pause - I'll come back to this later
Shut down running processes to free up memory and ports. Your project files stay on disk so you can restart anytime.
- Press Ctrl+C in each terminal running a dtnd process, the Python exporter, and the contact router (if running).
- Stop the Docker containers by running:
docker compose down
- To restart later, re-run each dtnd daemon with its TOML config, start exporter.py, and run docker compose up -d.
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes containers, project files, and temporary data.
- Kill all running dtnd processes by running:
pkill dtnd
- Kill the Python exporter and contact router processes by running:
pkill -f exporter.py
pkill -f contact_router.py
- Remove the Docker containers and volumes by running:
docker compose down -v
- Remove the cloned dtn7-rs repository and your project files from your Desktop by running:
cd ~/Desktop
rm -rf dtn7-rs
- Remove the DTN working directories that the daemons created under /tmp by running:
rm -rf /tmp/dtn7-*
That's a Wrap!
That's a Wrap!
Nice work! You just built a simulated LEO satellite constellation running Delay-Tolerant Networking, broke it on purpose, watched it heal itself, and monitored the whole thing on a live dashboard.
You've learned how to:
- Build dtn7-rs from source and deploy a 3-node DTN satellite constellation using the Bundle Protocol v7 (RFC 9171), routing bundles across a multi-hop relay chain.
- Kill a relay satellite and witness store-and-forward in action, with bundles queuing at the sender during the outage and automatically delivering once the relay recovered.
- Build a custom Python Prometheus metrics exporter that polls the dtn7-rs REST API, and deploy a live Grafana dashboard to monitor bundle delivery and node liveness across the entire constellation.
- Secret Mission: Implement a contact-schedule-aware routing strategy using the dtn7-rs external routing WebSocket API, replacing blind epidemic flooding with intelligent forwarding decisions.
Ready to quiz yourself?