Paths and path policies
Path awareness is the feature that sets SCION apart. On the Internet you hand a packet to the network and it decides, hop by hop, how to get there. You get one route and no say in it. On SCION the sender sees the available paths to a destination and picks which one to use, per packet if it wants. That control is what buys you multipath, fast failover, and the ability to steer around networks you would rather avoid.
What a path is
Between your AS and a destination AS there is usually more than one path, and they differ in the ISDs and ASes they cross, their MTU, and their latency. A path is not discovered by the network on your behalf. The SDK fetches the pieces and assembles end-to-end paths that you can inspect and select.
Those pieces are segments. SCION paths are built by combining an up segment (from your AS toward a core AS), an optional core segment (between core ASes, possibly across ISDs), and a down segment (from a core AS down to the destination). Different combinations yield different end-to-end paths:
You do not have to think in segments to use paths. The SDK combines them for you and hands you finished paths. It matters because it is why several distinct paths exist and why they have the shapes they do.
Three ways to choose a path
Let the stack choose (the default)
If you do not care, do nothing:
send_to
on a path-aware socket picks a path for you. The stack keeps the set of paths to each destination
fresh and, by default, orders them sensibly (short and diverse first) and sends over the top one.
This ranking is built in, with nothing to configure, and it is what the
getting-started echo program relies on.
Choose explicitly, per send
When you want to see and decide, ask the stack's path fetcher
(create_path_fetcher)
for every path to the destination AS and inspect them yourself:
// 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, its MTU, and latency hints, which is exactly what you would base a
decision on (the example sorts by hop count). Once you hold a path, send over it explicitly 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");
}
Choose with a policy (a standing rule)
Inspecting paths by hand is right for a one-off decision, but most applications have a rule: "never
leave this ISD", "at most N hops", "avoid AS X". Express it once as a path policy and attach it
to the socket. Then plain send_to honors it automatically.
A PathPolicy is
a predicate over a ScionPath: return true for the paths you are willing to use. This one forbids
any path through a given AS:
/// A path policy that rejects any path traversing the AS `avoid`.
///
/// A [`PathPolicy`](scion_stack::path::policy::PathPolicy) is just a predicate over
/// a [`ScionPath`]: return `true` to keep the path, `false` to discard it.
struct AvoidAs {
avoid: IsdAsn,
}
impl PathPolicy for AvoidAs {
fn predicate(&self, path: &ScionPath) -> bool {
let Some(interfaces) = path.metadata().and_then(|m| m.interfaces.as_ref()) else {
// Without interface metadata we cannot tell which ASes the path
// crosses, so we keep it rather than filtering blindly.
return true;
};
!interfaces
.iter()
.any(|hop| hop.interface.isd_asn == self.avoid)
}
}
Attach it when you bind, via
SocketConfig::with_path_policy
passed to
bind_with_config,
after which send_to only ever routes over paths that satisfy it:
// Attach the policy to the socket when binding. From now on `send_to` only
// ever routes over paths the policy accepts, here anything except the detour
// through 2-ff00:0:222. No explicit path selection is needed.
let config = SocketConfig::new().with_path_policy(AvoidAs { avoid: IA222 });
let client_socket = client_stack.bind_with_config(None, config).await?;
let reply = ping(
&client_socket,
server_addr,
b"hello via a policy-approved path",
)
.await?;
You do not have to write predicates from scratch:
sciparse ships reusable policies such as hop-pattern and ACL matchers
that implement PathPolicy directly.
When to use which
- Default: the common case. You want connectivity and reasonable routing, not control.
- Policy (
with_path_policy): you have a durable constraint such as compliance, reachability, or cost. Set it once and the stack enforces it on every send. - Explicit (
send_to_via): you are making a deliberate, situational choice, probing multiple paths, or implementing your own selection logic on top of the metadata.
Where to go next
- Addressing: paths are looked up per destination AS, so this is how you name that AS.
- The ScionStack model: where path management sits among the stack's responsibilities.
scion-stackpath API:ScionPath, the path fetcher, and the policy types in full.