Skip to main content

Crate sanstun

Crate sanstun 

Source
Expand description

A sans-IO implementation of the WireGuard protocol.

§Example

Alice and Bob complete a handshake, then Alice sends Bob a packet. In an application, your event loop sends datagrams over UDP, handles events from Tunnel::poll_event and drives timers with Tunnel::poll_timeout and Tunnel::handle_timeout.

use std::time::{Instant, SystemTime, UNIX_EPOCH};

use sanstun::{Committed, Interface, Received, ReceivedMessage, TunnelConfig};
use zeroize::Zeroizing;

let now = Instant::now();
let unix = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
let alice_addr = "192.0.2.1:51820".parse().unwrap();
let bob_addr = "192.0.2.2:51820".parse().unwrap();

let mut alice = Interface::new(Zeroizing::new(rand::random()), now, unix);
let mut bob = Interface::new(Zeroizing::new(rand::random()), now, unix);
let mut alice_to_bob = alice.new_tunnel(bob.static_public(), TunnelConfig::default(), now)?;
let mut bob_to_alice = bob.new_tunnel(alice.static_public(), TunnelConfig::default(), now)?;

// Alice initiates the handshake and Bob responds.
let initiation = alice_to_bob.initiate_handshake(now).unwrap();
let response = match bob.receive(alice_addr, &initiation, now)? {
    Received::Message(ReceivedMessage::HandshakeInitiation(initiation)) => {
        bob_to_alice.handle_handshake_initiation(initiation, now)?
    }
    // Under load, send the cookie reply back to the peer.
    Received::SendCookieReply(_) => unreachable!("not under load"),
    received => unreachable!("expected an initiation, got {received:?}"),
};

// Alice completes the handshake and confirms it with a keepalive for Bob.
let _keepalive = match alice.receive(bob_addr, &response, now)? {
    Received::Message(ReceivedMessage::HandshakeResponse(response)) => {
        alice_to_bob.handle_handshake_response(response, now)?
    }
    received => unreachable!("expected a response, got {received:?}"),
};

// Alice encrypts a packet.
let packet = b"an IP packet";
let mut buf = [0; 64];
let data = alice_to_bob.begin_encapsulate(now)?.encrypt(packet, &mut buf)?;

// Bob decrypts and accepts it.
let decrypted = match bob.receive(alice_addr, &data, now)? {
    Received::Message(ReceivedMessage::TransportData(message)) => {
        bob_to_alice.begin_decapsulate(message, now)?.decrypt(&data, [0; 64])
    }
    received => unreachable!("expected data, got {received:?}"),
};
match bob_to_alice.commit_decapsulate(decrypted, now)? {
    // The plaintext is zero-padded to a multiple of 16 bytes.
    Committed::Data { buffer, len } => assert_eq!(&buffer[..len], b"an IP packet\0\0\0\0"),
    // A keepalive carries no packet to forward.
    Committed::Keepalive => unreachable!("Alice sent a packet"),
}

§Caller responsibilities

sanstun handles the protocol and cryptography. Your application is responsible for:

  • Cryptokey routing: check that each decrypted packet’s source IP is allowed for the peer that sent it. Authentication alone does not grant permission to use an IP address.
  • Handshake routing: use HandshakeInitiation::peer_static_public to look up an authorized peer. Anyone can generate a key pair, so an authenticated initiation is no reason to create a tunnel for an unknown key. Replay checks only apply to keys you have created a tunnel for, and only take effect in Tunnel::handle_handshake_initiation: do not act on an initiation before its tunnel accepts it.
  • Endpoint updates: only update a peer’s endpoint after Tunnel::handle_handshake_initiation or Tunnel::handle_handshake_response accepts its handshake or Tunnel::commit_decapsulate accepts its data. A datagram’s source address alone does not identify the peer.
  • Bounded queues: limit queues used to send encryption or decryption work to other threads. A PendingDecapsulate is cheap to create but costly to decrypt, and a flood can exhaust memory.
  • Packet validation: parse decrypted IP packets, check their declared lengths and strip padding before forwarding them. Data messages are zero-padded to a multiple of 16 bytes.
  • Errors: handle Error locally. Never send errors to peers; they can reveal internal state such as the replay window.
  • Time: use a monotonic clock for now. The Unix time passed to Interface::new must not go backwards across restarts, or peers may reject handshake initiations as replays.

Structs§

CookieReply
DataMessage
An encrypted data message, borrowing its output buffer.
Datagram
Bytes to send to the peer as one UDP datagram.
Decrypted
A decryption result awaiting Tunnel::commit_decapsulate.
HandshakeInitiation
An authenticated handshake initiation, ready to be answered by its Tunnel.
HandshakeResponse
Interface
A WireGuard interface whose tunnels share one static key.
PendingDecapsulate
A data message ready for decryption after an initial replay check.
PendingEncapsulate
A data message ready for encryption, with its counter already reserved.
PresharedKey
A WireGuard preshared key, zeroized on drop.
PublicKey
An X25519 public key.
StaticSecret
An X25519 static private key, zeroized on drop.
TransportData
Tunnel
A point-to-point WireGuard connection.
TunnelConfig
Optional settings for a Tunnel, passed to Interface::new_tunnel.
TunnelId
Identifies a Tunnel within its Interface.

Enums§

Committed
The result of committing a decrypted packet.
Error
Event
An action for the caller, returned by Tunnel::poll_event.
Received
The result of Interface::receive.
ReceivedMessage
A network message parsed by Interface::receive.