Getting started
This guide takes you from nothing to a running SCION program in a few minutes. You do not need access to a real SCION network, and you do not need to know anything about SCION yet. Everything runs locally against PocketSCION, a SCION network simulator that ships with the SDK.
By the end you will have compiled and run a UDP echo client/server that talks over a simulated SCION network you started yourself.
What SCION gives you as an application developer
SCION is an inter-domain networking architecture — think of it as an alternative to today's BGP-routed Internet — with one property that matters most to application developers: the application, not the network, chooses the path its packets take.
On the SCION network you can:
- See every path to a destination and their properties (which ISDs and ASes they cross, MTU, latency hints) instead of being handed one opaque route.
- Pick a path per packet — steer traffic away from a provider, prefer a low-latency route, or spread load across several paths — from application code.
- Fail over instantly when a path breaks, because you already hold the alternatives.
- Trust the source, because SCION paths are cryptographically authenticated.
The SDK's scion-stack crate exposes
this through an API deliberately shaped like the sockets you already know:
bind,
send_to,
recv_from.
Path awareness is opt-in — the simplest programs ignore it and let the stack
choose, and you reach for explicit path selection only when you want it (see
Going further: let your app pick the path).
Prerequisites
- Rust — the SDK pins its toolchain in
rust-toolchain.toml, so if you haverustupinstalled the right version is fetched automatically. - git, to clone the repository.
- A supported platform: Linux, macOS, or Windows.
No SCION installation, no root, no network access beyond fetching crates.
Run your first SCION program
The fastest path to "it works" is to run the udp_echo example that ships with
the SDK. Clone the repository and run it with a single command:
git clone https://github.com/Anapaya/scion-sdk.git
cd scion-sdk
cargo run -p scion-stack --example udp_echo
You should see:
echo server listening on <address in 2-ff00:0:212>
client received echo: Hello, SCION!
That's a full SCION round-trip: a client in one autonomous system (AS) sent a datagram to a server in a different AS, across a SCION network that the example started itself, and read back the echo.
What just happened
The example builds this two-AS network in PocketSCION — a client AS and a server AS joined by one link:
Here is the whole program. We'll walk through it step by step below.
// Copyright 2026 Anapaya Systems
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! "Hello, SCION" over UDP.
//!
//! A client in one AS sends a datagram to an echo server in another AS and prints
//! the reply. Everything runs against a [PocketSCION] network that this example
//! starts itself.
//!
//! The network is two ASes joined by a single link:
//!
//! ```text
//! 1-ff00:0:132 #1 ───────── #3 2-ff00:0:212
//! (client) (server)
//! ```
//!
//! Run it with:
//!
//! ```text
//! cargo run -p scion-stack --example udp_echo
//! ```
//!
//! [PocketSCION]: pocketscion
mod common;
use std::time::Duration;
use pocketscion::util::topologies::{IA132, IA212, UnderlayType, minimal::minimal_topology};
use scion_stack::stack::UdpScionSocket;
use sciparse::address::ip_socket_addr::ScionSocketIpAddr;
use tokio::time::timeout;
/// Largest datagram we bother to buffer in this example.
const MAX_DATAGRAM: usize = 2048;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
run().await
}
/// Starts a two-AS SCION network, runs an echo server, and pings it once.
async fn run() -> anyhow::Result<()> {
// PocketSCION uses rustls for its control plane; pick a crypto backend.
scion_sdk_utils::rustls::select_ring_crypto_provider();
// Start a minimal SCION network: two ASes joined by a single link. The returned
// handle owns the whole simulation.
let ps = minimal_topology(UnderlayType::Snap).await;
// Bring up the echo server in AS 2-ff00:0:212 and let it run in the background.
let server_stack = common::build_stack(&ps, IA212).await?;
let server_socket = server_stack.bind(None).await?;
let server_addr = server_socket.local_addr();
println!("echo server listening on {server_addr}");
let server = tokio::spawn(echo_server(server_socket));
// Bring up the client in AS 1-ff00:0:132 and send one datagram.
let client_stack = common::build_stack(&ps, IA132).await?;
let client_socket = client_stack.bind(None).await?;
let reply = ping(&client_socket, server_addr, b"Hello, SCION!").await?;
println!("client received echo: {}", String::from_utf8_lossy(&reply));
server.abort();
Ok(())
}
/// Echoes every datagram straight back to whoever sent it, forever.
///
/// [`recv_from`](UdpScionSocket::recv_from) yields the sender's SCION address, and
/// the socket automatically remembers a return path to it, so [`send_to`] can
/// reply without the application ever touching path selection.
///
/// [`send_to`]: UdpScionSocket::send_to
async fn echo_server(socket: UdpScionSocket) -> anyhow::Result<()> {
let mut buffer = [0u8; MAX_DATAGRAM];
loop {
let (len, from) = socket.recv_from(&mut buffer).await?;
socket.send_to(&buffer[..len], from).await?;
}
}
/// Sends `payload` to `destination` and waits for the echoed reply.
///
/// UDP datagrams can be dropped, so we resend a handful of times before giving up.
async fn ping(
socket: &UdpScionSocket,
destination: ScionSocketIpAddr,
payload: &[u8],
) -> anyhow::Result<Vec<u8>> {
let mut buffer = [0u8; MAX_DATAGRAM];
for attempt in 1..=5 {
socket.send_to(payload, destination).await?;
match timeout(Duration::from_millis(500), socket.recv_from(&mut buffer)).await {
Ok(result) => {
let (len, _from) = result?;
return Ok(buffer[..len].to_vec());
}
Err(_elapsed) => tracing::debug!(attempt, "no echo yet, resending"),
}
}
anyhow::bail!("no echo received from {destination} after 5 attempts")
}
#[cfg(test)]
mod tests {
use test_log::test;
/// End-to-end smoke test: the example must complete a full echo round-trip.
#[test(tokio::test)]
#[ntest::timeout(30_000)]
async fn udp_echo_roundtrip() {
super::run().await.expect("udp_echo example should succeed");
}
}
1. Start a SCION network
minimal_topology brings up the two-AS network shown above and returns a handle
that owns the whole simulation. This is the only PocketSCION-specific line —
against real SCION you would skip straight to building the stack.
// Start a minimal SCION network: two ASes joined by a single link. The returned
// handle owns the whole simulation.
let ps = minimal_topology(UnderlayType::Snap).await;
2. Attach a SCION stack to an AS
A ScionStack is the SCION equivalent of your OS networking stack: the object
you open sockets on. You build one per AS you want to run code in. The examples
wrap this in a build_stack helper:
pub async fn build_stack(ps: &PsSetup, isd_as: IsdAsn) -> anyhow::Result<ScionStack> {
let endhost_api = ps
.endhost_api(isd_as)
.with_context(|| format!("PocketSCION has no endhost API for {isd_as}"))?;
ScionStackBuilder::new()
.with_endhost_api(endhost_api)
.with_auth_token(dev_auth_token())
.build()
.await
.with_context(|| format!("building SCION stack for {isd_as}"))
}
3. Bind a socket and serve
Bind a UDP socket on the server's stack and hand it to a task that echoes every
datagram back to its sender. recv_from yields the sender's SCION address, and
the socket selects a return path automatically, so send_to can reply without
the application ever touching SCION path selection:
// Bring up the echo server in AS 2-ff00:0:212 and let it run in the background.
let server_stack = common::build_stack(&ps, IA212).await?;
let server_socket = server_stack.bind(None).await?;
let server_addr = server_socket.local_addr();
println!("echo server listening on {server_addr}");
let server = tokio::spawn(echo_server(server_socket));
async fn echo_server(socket: UdpScionSocket) -> anyhow::Result<()> {
let mut buffer = [0u8; MAX_DATAGRAM];
loop {
let (len, from) = socket.recv_from(&mut buffer).await?;
socket.send_to(&buffer[..len], from).await?;
}
}
4. Send from the client
Bind a socket on the client's stack and send one datagram to the server's
address. send_to picks a SCION path for you; because UDP datagrams can be
dropped, the helper resends a few times before giving up:
// Bring up the client in AS 1-ff00:0:132 and send one datagram.
let client_stack = common::build_stack(&ps, IA132).await?;
let client_socket = client_stack.bind(None).await?;
let reply = ping(&client_socket, server_addr, b"Hello, SCION!").await?;
println!("client received echo: {}", String::from_utf8_lossy(&reply));
async fn ping(
socket: &UdpScionSocket,
destination: ScionSocketIpAddr,
payload: &[u8],
) -> anyhow::Result<Vec<u8>> {
let mut buffer = [0u8; MAX_DATAGRAM];
for attempt in 1..=5 {
socket.send_to(payload, destination).await?;
match timeout(Duration::from_millis(500), socket.recv_from(&mut buffer)).await {
Ok(result) => {
let (len, _from) = result?;
return Ok(buffer[..len].to_vec());
}
Err(_elapsed) => tracing::debug!(attempt, "no echo yet, resending"),
}
}
anyhow::bail!("no echo received from {destination} after 5 attempts")
}
That is the whole surface you need for basic SCION networking: build a stack,
bind a socket, send_to / recv_from. Path awareness — SCION's defining
feature — is one method away when you want it.
Going further: let your app pick the path
Above, send_to chose a path for you. SCION's defining feature is that your
application can choose a path from all available ones. Ask the stack's
path fetcher for every path to the destination:
// Ask the SDK for *every* path to the server's AS and inspect them ourselves,
// rather than letting `send_to` silently pick one for us.
let mut paths = client_stack
.create_path_fetcher()
.fetch_paths(IA132, IA212)
.await?;
anyhow::ensure!(
!paths.is_empty(),
"expected at least one path to the server"
);
// Order shortest-first so the deliberate choice below is reproducible.
paths.sort_by_key(hop_count);
Each ScionPath carries metadata — the interfaces it traverses, MTU, latency
hints — which is exactly what you'd base a routing decision on. Once you hold a
path, send a datagram on the path with
send_to_via
instead of send_to:
// Send over each path in turn, selecting it explicitly with `send_to_via`. The
// payload carries the path index so we can confirm the echo we accept is the
// reply to *this* send, not a delayed echo from an earlier path.
for (i, path) in paths.iter().enumerate() {
let payload = format!("hello via path {i}");
ping_via(&client_socket, server_addr, path, payload.as_bytes()).await?;
println!(" [{i}] echo confirmed over this path");
}
You can also express a preference declaratively when building the stack, via
ScionStackBuilder::with_path_policy and with_path_scoring, in which case
send_to applies it automatically. The udp_paths example puts this all
together against a topology with two paths to the server — run it the same way:
cargo run -p scion-stack --example udp_paths
Add the SDK to your own project
When you're ready to write your own program, add the stack to your crate:
cargo add scion-stack
To exercise it against a simulated network in your tests and examples, add PocketSCION as a dev-dependency:
cargo add --dev pocketscion
Then follow the same three steps as above: build a ScionStack, bind a
socket, and send_to / recv_from.
Where to go next
- Browse the examples — every example under
scion-stack/examples/is runnable withcargo run -p scion-stack --example <name>and is compiled and tested in CI. - API reference — the full API for the stack is on docs.rs/scion-stack.
- PocketSCION — see
pocketscion/README.mdfor the other topologies and underlays you can simulate.