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_publicto 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 inTunnel::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_initiationorTunnel::handle_handshake_responseaccepts its handshake orTunnel::commit_decapsulateaccepts 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
PendingDecapsulateis 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
Errorlocally. 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 toInterface::newmust not go backwards across restarts, or peers may reject handshake initiations as replays.
Structs§
- Cookie
Reply - Data
Message - 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. - Handshake
Initiation - An authenticated handshake initiation, ready to be answered by its
Tunnel. - Handshake
Response - Interface
- A WireGuard interface whose tunnels share one static key.
- Pending
Decapsulate - A data message ready for decryption after an initial replay check.
- Pending
Encapsulate - A data message ready for encryption, with its counter already reserved.
- Preshared
Key - A WireGuard preshared key, zeroized on drop.
- Public
Key - An X25519 public key.
- Static
Secret - An X25519 static private key, zeroized on drop.
- Transport
Data - Tunnel
- A point-to-point WireGuard connection.
- Tunnel
Config - Optional settings for a
Tunnel, passed toInterface::new_tunnel. - Tunnel
Id - Identifies a
Tunnelwithin itsInterface.
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. - Received
Message - A network message parsed by
Interface::receive.