diff options
author | sopium <sopium@mysterious.site> | 2017-03-22 16:46:06 +0800 |
---|---|---|
committer | Sascha Grunert <saschagrunert@users.noreply.github.com> | 2017-03-23 17:55:05 +0100 |
commit | 724772ec6dbc88cc88f73d87120307635824c362 (patch) | |
tree | 6aa883f506158e7ab7cf79fd557bddf5329c5ab9 /src/protocol | |
parent | Warn users, due to already incoming emails (diff) | |
download | wireguard-rs-724772ec6dbc88cc88f73d87120307635824c362.tar.xz wireguard-rs-724772ec6dbc88cc88f73d87120307635824c362.zip |
Add sopium's protocol implementation and wg-standalone
Diffstat (limited to 'src/protocol')
-rw-r--r-- | src/protocol/README.md | 136 | ||||
-rw-r--r-- | src/protocol/anti_replay.rs | 148 | ||||
-rw-r--r-- | src/protocol/controller.rs | 1107 | ||||
-rw-r--r-- | src/protocol/cookie.rs | 139 | ||||
-rw-r--r-- | src/protocol/handshake.rs | 298 | ||||
-rw-r--r-- | src/protocol/ip.rs | 51 | ||||
-rw-r--r-- | src/protocol/mod.rs | 46 | ||||
-rw-r--r-- | src/protocol/re_exports.rs | 23 | ||||
-rw-r--r-- | src/protocol/timer.rs | 197 | ||||
-rw-r--r-- | src/protocol/types.rs | 139 |
10 files changed, 2284 insertions, 0 deletions
diff --git a/src/protocol/README.md b/src/protocol/README.md new file mode 100644 index 0000000..c6b4512 --- /dev/null +++ b/src/protocol/README.md @@ -0,0 +1,136 @@ +This is a walk-through of the protocol implementation. Hope it will be +helpful if you want to review/hack on it. + +## `anti_replay.rs` + +Anti replay algorithm from RFC 6479. It is mostly a straightforward +translation from the C code there, with only one notable difference: +the handling of seq number zero. + +It is reasonably well tested. + +## `cookie.rs` + +Cookie signing, verification, cookie reply message generation and +parsing. + +It is reasonably well tested. + +## `timer.rs` + +Timer facility. Uses the hashed timing wheel algorithm. It is +optimized for WireGuard use cases (frequent operations on a mostly +fixed set of timers) with the "activated" atomic boolean flag. + +It is under tested. + +## `ip.rs` + +Parsing of IP(v6) packets. + +## `types.rs` + +Some common types and constants. + +## `handshake.rs` + +Handshake initiation and response message generation and parsing. + +It is reasonably well tested. + +## `controller.rs` + +This beast is the most complex... It manages all the states, timers +and does the actuall IO. It also has a lot of locks and `Arc`s. + +`WgState` represents the state of a WireGuard interface. It contains a +hash table that maps pubkeys to peers, another hash table that maps +session IDs to peers, and routing tables that map allowed IP(v6) +addresses to peers. + +The ID table changes often and needs to be carefully kept in sync with +actuall peer states. (Or we will leak memory.) For this we use +`IdMapGuard` which when dropped will automatically remove an ID from +the map. + +`PeerState` represents the state of a peer. It is shared across the +maps and timers with `Arc<RwLock<_>>` (or `Weak<RwLock<>>` for +timers). + +The actually IO happens in two threads, one for processing UDP +datagrams, another for processing packets from the TUN device. (It is +possible to use more threads, but there does not seem to be much +benefit without also using SO_REUSEPORT or multi queue tun device.) + +The UDP thread repeatedly `recv_from` the socket, and take action +based the type of the message. + +The TUN thread repeatedly `read` packets from the TUN device, find the +corresponding peer by looking up the route tables, encrypt them and +send them out if a valid session exists, and/or initiate handshake if +necessary. + +### Lock Order + +Because we use a lot of locks, care must be taken to avoid deadlock. +We adhere to the following partial order: + + info > pubkey_map > any peers > id_map > anything else + any peers > rt4 > rt6 + +Locks whose order are not defined should not be held at the same time. + +### Timer Management + +Each peer is associated with the following timers: + +#### `rekey_no_recv` + +Initiate handshake because we have send a transport message but +haven't received any in 15s (KEEPALIVE_TIMEOUT + REKEY_TIMEOUT). + +It is activated and set to 15s when we send a (non-keep-alive) +transport message, unless it is already activated. + +It is de-activated when we receive a transport message. + +#### `keep_alive` + +Send a keep-alive message because we have received a transport message +but haven't sent any in 10s (KEEPALIVE_TIMEOUT). + +It is activated and set to 10s when we receive a (non-keep-alive) +transport message, unless it is already activated. + +It is de-activated when we send a transport message. + +#### `persistent_keep_alive` + +Persistent keep-alive. + +It is activated and set to the configured interval when a new session +is established, or when we send a transport message. + +#### `clear` + +Clear handshake and all transport sessions, and de-activate all +timers, because no new session has been established in 9min (3 * +REJECT_AFTER_TIME). + +It is activated and set to 9 min when we initiate a handshake, unless +it is already activated. + +It is activated and set to 9 min when a new session is established. + +#### Not managed by timer + +`REKEY_AFTER_TIME` is not managed by a timer. Instead, a `created` +time-stamp is stored within each transport session, and compared with +when sending messages, to decide on whether to rekey. + +The secret used to calculate cookies, which is supposed to change +every two minutes, is also handled this way. + +### Testing + +This module is under tested. diff --git a/src/protocol/anti_replay.rs b/src/protocol/anti_replay.rs new file mode 100644 index 0000000..4fb2e11 --- /dev/null +++ b/src/protocol/anti_replay.rs @@ -0,0 +1,148 @@ +// Copyright 2017 Sopium + +// This file is part of WireGuard.rs. + +// WireGuard.rs is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. + +// WireGuard.rs is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with WireGuard.rs. If not, see <https://www.gnu.org/licenses/>. + + +// This is RFC 6479. + +use std::cmp::min; + +// Power of 2. +const BITMAP_BITLEN: u64 = 2048; + +const SIZE_OF_INTEGER: u64 = 32; +const BITMAP_LEN: usize = (BITMAP_BITLEN / SIZE_OF_INTEGER) as usize; +const BITMAP_INDEX_MASK: u64 = BITMAP_LEN as u64 - 1; +// REDUNDANT_BIT_SHIFTS = log2(SIZE_OF_INTEGER). +const REDUNDANT_BIT_SHIFTS: u64 = 5; +const BITMAP_LOC_MASK: u64 = SIZE_OF_INTEGER - 1; +/// Size of anti-replay window. +pub const WINDOW_SIZE: u64 = BITMAP_BITLEN - SIZE_OF_INTEGER; + +pub struct AntiReplay { + last: u64, + bitmap: [u32; BITMAP_LEN], +} + +impl Default for AntiReplay { + fn default() -> Self { + AntiReplay::new() + } +} + +impl AntiReplay { + pub fn new() -> Self { + AntiReplay { + last: 0, + bitmap: [0; BITMAP_LEN], + } + } + + /// Returns true if check is passed, i.e., not a replay or too old. + /// + /// Unlike RFC 6479, zero is allowed. + pub fn check(&self, seq: u64) -> bool { + // Larger is always good. + if seq > self.last { + return true; + } + + if self.last - seq > WINDOW_SIZE { + return false; + } + + let bit_location = seq & BITMAP_LOC_MASK; + let index = (seq >> REDUNDANT_BIT_SHIFTS) & BITMAP_INDEX_MASK; + + self.bitmap[index as usize] & (1 << bit_location) == 0 + } + + /// Should only be called if check returns true. + pub fn update(&mut self, seq: u64) { + debug_assert!(self.check(seq)); + + let index = seq >> REDUNDANT_BIT_SHIFTS; + + if seq > self.last { + let index_cur = self.last >> REDUNDANT_BIT_SHIFTS; + let diff = min(index - index_cur, BITMAP_LEN as u64); + + for i in 0..diff { + let real_index = (index_cur + i + 1) & BITMAP_INDEX_MASK; + self.bitmap[real_index as usize] = 0; + } + self.last = seq; + } + + let index = index & BITMAP_INDEX_MASK; + let bit_location = seq & BITMAP_LOC_MASK; + self.bitmap[index as usize] |= 1 << bit_location; + } + + pub fn check_and_update(&mut self, seq: u64) -> bool { + if self.check(seq) { + self.update(seq); + true + } else { + false + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn anti_replay() { + let mut ar = AntiReplay::new(); + + for i in 0..20000 { + assert!(ar.check_and_update(i)); + } + + for i in (0..20000).rev() { + assert!(!ar.check(i)); + } + + assert!(ar.check_and_update(65536)); + for i in (65536 - WINDOW_SIZE)..65535 { + assert!(ar.check_and_update(i)); + } + for i in (65536 - 10 * WINDOW_SIZE)..65535 { + assert!(!ar.check(i)); + } + + ar.check_and_update(66000); + for i in 65537..66000 { + assert!(ar.check_and_update(i)); + } + for i in 65537..66000 { + assert!(!ar.check_and_update(i)); + } + + // Test max u64. + let next = u64::max_value(); + assert!(ar.check_and_update(next)); + assert!(!ar.check(next)); + for i in (next - WINDOW_SIZE)..next { + assert!(ar.check_and_update(i)); + } + for i in (next - 20 * WINDOW_SIZE)..next { + assert!(!ar.check(i)); + } + } +} diff --git a/src/protocol/controller.rs b/src/protocol/controller.rs new file mode 100644 index 0000000..ed3b4e3 --- /dev/null +++ b/src/protocol/controller.rs @@ -0,0 +1,1107 @@ +// Copyright 2017 Sopium + +// This file is part of WireGuard.rs. + +// WireGuard.rs is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. + +// WireGuard.rs is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with WireGuard.rs. If not, see <https://www.gnu.org/licenses/>. + +extern crate byteorder; +extern crate noise_protocol; +extern crate noise_sodiumoxide; +extern crate sodiumoxide; +extern crate tai64; +extern crate treebitmap; + +use self::byteorder::{ByteOrder, LittleEndian}; +use self::noise_protocol::{Cipher, U8Array}; +use self::noise_sodiumoxide::ChaCha20Poly1305; +use self::sodiumoxide::randombytes::randombytes_into; +use self::tai64::TAI64N; +use self::treebitmap::{IpLookupTable, IpLookupTableOps}; +use protocol::*; +use std::collections::HashMap; +use std::mem::uninitialized; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket}; +use std::ops::Deref; +use std::sync::{Arc, Mutex, RwLock}; +use std::sync::atomic::{AtomicBool, AtomicU64}; +use std::sync::atomic::Ordering::Relaxed; +use std::thread::{Builder, JoinHandle, spawn}; +use std::time::{Duration, Instant, SystemTime}; +use tun::Tun; + +// Some Constants. + +// That is, 2 ^ 64 - 2 ^ 16 - 1; +const REKEY_AFTER_MESSAGES: u64 = 0xfffffffffffeffff; +// That is, 2 ^ 64 - 2 ^ 4 - 1; +const REJECT_AFTER_MESSAGES: u64 = 0xffffffffffffffef; + +// Timeouts, in seconds. + +const REKEY_AFTER_TIME: u64 = 120; +const REJECT_AFTER_TIME: u64 = 180; +const REKEY_TIMEOUT: u64 = 5; +const KEEPALIVE_TIMEOUT: u64 = 10; + +// Increase if your MTU is larger... +const BUFSIZE: usize = 1500; + +type SharedPeerState = Arc<RwLock<PeerState>>; + +// Locking order: +// +// info > pubkey_map > any peers > id_map > anything else +// any peers > rt4 > rt6 + +/// State of a WG interface. +pub struct WgState { + info: RwLock<WgInfo>, + + pubkey_map: RwLock<HashMap<X25519Pubkey, SharedPeerState>>, + id_map: RwLock<HashMap<Id, SharedPeerState>>, + // Also should be keep in sync. But these should change less often. + rt4: RwLock<IpLookupTable<Ipv4Addr, SharedPeerState>>, + rt6: RwLock<IpLookupTable<Ipv6Addr, SharedPeerState>>, + + // The secret used to calc cookie. + cookie_secret: Mutex<([u8; 32], Instant)>, + + timer_controller: TimerController, +} + +/// Removes `Id` from `id_map` when dropped. +struct IdMapGuard { + wg: Arc<WgState>, + id: Id, +} + +impl Drop for IdMapGuard { + fn drop(&mut self) { + if let Ok(mut id_map) = self.wg.id_map.try_write() { + id_map.remove(&self.id); + } else { + let wg = self.wg.clone(); + let id = self.id; + spawn(move || { wg.id_map.write().unwrap().remove(&id); }); + } + } +} + +impl IdMapGuard { + fn new(wg: Arc<WgState>, id: Id) -> Self { + Self { wg: wg, id: id } + } +} + +/// State of a paticular peer. +struct PeerState { + info: PeerInfo, + last_handshake: Option<TAI64N>, + cookie: Option<(Cookie, Instant)>, + last_mac1: Option<[u8; 16]>, + handshake: Option<Handshake>, + + // XXX: use a Vec? or ArrayVec? + transport0: Option<Transport>, + transport1: Option<Transport>, + transport2: Option<Transport>, + + // Rekey because of send but not recv in... + rekey_no_recv: TimerHandle, + // Keep alive because of recv but not send in... + keep_alive: TimerHandle, + // Persistent keep-alive. + persistent_keep_alive: TimerHandle, + // Clear all sessions if no new handshake in REJECT_AFTER_TIME * 3. + clear: TimerHandle, +} + +struct Handshake { + self_id: IdMapGuard, + hs: HS, + // Resend after REKEY_TIMEOUT. + #[allow(dead_code)] + resend: TimerHandle, +} + +type SecretKey = <ChaCha20Poly1305 as Cipher>::Key; + +/// A WireGuard transport session. +struct Transport { + self_id: IdMapGuard, + peer_id: Id, + is_initiator: bool, + // If we are responder, should not send until received one packet. + is_initiator_or_has_received: AtomicBool, + // Also should not send after REJECT_AFTER_TIME, + // or after REJECT_AFTER_MESSAGES. + not_too_old: AtomicBool, + created: Instant, + + send_key: SecretKey, + send_counter: AtomicU64, + + recv_key: SecretKey, + recv_ar: Mutex<AntiReplay>, +} + +// TODO determine / detect load. +fn is_under_load() -> bool { + false +} + +fn udp_process_handshake_init(wg: Arc<WgState>, sock: &UdpSocket, p: &[u8], addr: SocketAddr) { + if p.len() != 148 { + return; + } + + // Lock info. + let info = wg.info.read().unwrap(); + + if is_under_load() { + let cookie = calc_cookie(&wg.get_cookie_secret(), addr.to_string().as_bytes()); + if !cookie_verify(p, &cookie) { + debug!("Mac2 verify failed, send cookie reply."); + let peer_id = Id::from_slice(&p[4..8]); + let mac1 = &p[116..132]; + let reply = cookie_reply(info.psk.as_ref(), &info.pubkey, &cookie, peer_id, mac1); + sock.send_to(&reply, addr).unwrap(); + return; + } else { + debug!("Mac2 verify OK."); + } + } + + if let Ok(mut r) = process_initiation(info.deref(), p) { + let r_pubkey = r.handshake_state.get_rs().unwrap(); + if let Some(peer0) = wg.find_peer_by_pubkey(&r_pubkey) { + // Lock peer. + let mut peer = peer0.write().unwrap(); + + // Compare timestamp. + if Some(r.timestamp) > peer.last_handshake { + peer.last_handshake = Some(r.timestamp); + } else { + debug!("Handshake timestamp smaller."); + return; + } + + let self_id = Id::gen(); + let mut response = responde(info.deref(), &mut r, self_id); + + // Save mac1. + let mut mac1 = [0u8; 16]; + mac1.copy_from_slice(&response[60..76]); + peer.last_mac1 = Some(mac1); + + cookie_sign(&mut response, peer.get_cookie()); + sock.send_to(&response, addr).unwrap(); + + let t = Transport::new_from_hs(IdMapGuard::new(wg.clone(), self_id), + r.peer_id, + r.handshake_state); + peer.set_endpoint(addr); + peer.push_transport(t); + // Lock id_map. + wg.id_map.write().unwrap().insert(self_id, peer0.clone()); + debug!("Handshake successful as responder."); + } else { + debug!("Get handshake init, but can't find peer by pubkey."); + } + } else { + debug!("Get handshake init, but authentication/decryption failed."); + } +} + +fn udp_process_handshake_resp(wg: &WgState, sock: &UdpSocket, p: &[u8], addr: SocketAddr) { + if p.len() != 92 { + return; + } + + // Lock info. + let info = wg.info.read().unwrap(); + + if is_under_load() { + let cookie = calc_cookie(&wg.get_cookie_secret(), addr.to_string().as_bytes()); + if !cookie_verify(p, &cookie) { + debug!("Mac2 verify failed, send cookie reply."); + let peer_id = Id::from_slice(&p[4..8]); + let mac1 = &p[60..76]; + let reply = cookie_reply(info.psk.as_ref(), &info.pubkey, &cookie, peer_id, mac1); + sock.send_to(&reply, addr).unwrap(); + return; + } else { + debug!("Mac2 verify OK."); + } + } + + let self_id = Id::from_slice(&p[8..12]); + + if let Some(peer0) = wg.find_peer_by_id(self_id) { + let (peer_id, hs) = { + // Lock peer. + let peer = peer0.read().unwrap(); + if peer.handshake.is_none() { + debug!("Get handshake response message, but don't know id."); + return; + } + let handshake = peer.handshake.as_ref().unwrap(); + if handshake.self_id.id != self_id { + debug!("Get handshake response message, but don't know id."); + return; + } + + let mut hs = handshake.hs.clone(); + if let Ok(peer_id) = process_response(info.deref(), &mut hs, p) { + (peer_id, hs) + } else { + debug!("Get handshake response message, auth/decryption failed."); + return; + } + // Release peer. + }; + debug!("Handshake successful as initiator."); + // Lock peer. + let mut peer = peer0.write().unwrap(); + let handle = peer.handshake.take().unwrap().self_id; + let t = Transport::new_from_hs(handle, peer_id, hs); + peer.push_transport(t); + peer.set_endpoint(addr); + // Send an empty packet for key confirmation. + do_keep_alive(&peer, sock); + // Lock id_map. + wg.id_map.write().unwrap().insert(self_id, peer0.clone()); + } else { + debug!("Get handshake response message, but don't know id."); + } +} + +fn udp_process_cookie_reply(wg: &WgState, p: &[u8]) { + let self_id = Id::from_slice(&p[4..8]); + + // Lock info. + let info = wg.info.read().unwrap(); + + if let Some(peer) = wg.find_peer_by_id(self_id) { + // Lock peer. + let mut peer = peer.write().unwrap(); + if let Some(mac1) = peer.last_mac1 { + if let Ok(cookie) = process_cookie_reply(info.psk.as_ref(), + &peer.info.peer_pubkey, + &mac1, + p) { + peer.cookie = Some((cookie, Instant::now())); + } else { + debug!("Process cookie reply: auth/decryption failed."); + } + } + } +} + +fn udp_process_transport(wg: &WgState, tun: &Tun, p: &[u8], addr: SocketAddr) { + if p.len() < 32 { + return; + } + + let self_id = Id::from_slice(&p[4..8]); + + let maybe_peer0 = wg.find_peer_by_id(self_id); + + if maybe_peer0.is_none() { + debug!("Get transport message, but don't know id."); + return; + } + + let peer0 = maybe_peer0.unwrap(); + let should_set_endpoint = { + // Lock peer. + let peer = peer0.read().unwrap(); + if let Some(t) = peer.find_transport_by_id(self_id) { + let mut buff: [u8; BUFSIZE] = unsafe { uninitialized() }; + let decrypted = &mut buff[..p.len() - 32]; + if t.decrypt(p, decrypted).is_ok() { + if let Ok((len, src, _)) = parse_ip_packet(decrypted) { + // Reverse path filtering. + let peer1 = wg.find_peer_by_ip(src); + if peer1.is_none() || !Arc::ptr_eq(&peer0, &peer1.unwrap()) { + debug!("Get transport message: allowed IPs check failed."); + } else { + tun.write(&decrypted[..len as usize]).unwrap(); + } + } + peer.on_recv(decrypted.len() == 0); + peer.info.endpoint != Some(addr) + } else { + debug!("Get transport message, decryption failed."); + false + } + } else { + false + } + // Release peer. + }; + if should_set_endpoint { + // Lock peer. + peer0.write() + .unwrap() + .set_endpoint(addr); + } +} + +/// Start a new thread to recv and process UDP packets. +/// +/// This thread runs forever. +pub fn start_udp_processing(wg: Arc<WgState>, sock: Arc<UdpSocket>, tun: Arc<Tun>) -> JoinHandle<()> { + Builder::new().name("UDP".to_string()).spawn(move || { + let mut p = [0u8; BUFSIZE]; + loop { + let (len, addr) = sock.recv_from(&mut p).unwrap(); + + if len < 12 { + continue; + } + + let type_ = p[0]; + let p = &p[..len]; + + match type_ { + 1 => udp_process_handshake_init(wg.clone(), sock.as_ref(), p, addr), + 2 => udp_process_handshake_resp(wg.as_ref(), sock.as_ref(), p, addr), + 3 => udp_process_cookie_reply(wg.as_ref(), p), + 4 => udp_process_transport(wg.as_ref(), tun.as_ref(), p, addr), + _ => (), + } + } + }).unwrap() +} + +// Packets >= MAX_PADDING won't be padded. +// 1280 should be a reasonable conservative choice. +const MAX_PADDING: usize = 1280; + +const PADDING_MASK: usize = 0b1111; + +fn pad_len(len: usize) -> usize { + if len >= MAX_PADDING { + len + } else { + // Next multiply of 16. + (len & !PADDING_MASK) + if len & PADDING_MASK == 0 { + 0 + } else { + 16 + } + } +} + +#[cfg(test)] +#[test] +fn padding() { + assert_eq!(pad_len(0), 0); + for i in 1..16 { + assert_eq!(pad_len(i), 16); + } + + for i in 17..32 { + assert_eq!(pad_len(i), 32); + } + + for i in 1265..1280 { + assert_eq!(pad_len(i), 1280); + } +} + +/// Start a new thread to read and process packets from TUN device. +/// +/// This thread runs forever. +pub fn start_tun_packet_processing(wg: Arc<WgState>, sock: Arc<UdpSocket>, tun: Arc<Tun>) -> JoinHandle<()> { + Builder::new().name("TUN".to_string()).spawn(move || { + let mut pkt = [0u8; BUFSIZE]; + loop { + let len = tun.read(&mut pkt).unwrap(); + let padded_len = pad_len(len); + // Do not leak other packets' data! + for b in &mut pkt[len..padded_len] { + *b = 0; + } + let pkt = &pkt[..padded_len]; + + let parse_result = parse_ip_packet(pkt); + if parse_result.is_err() { + error!("Get packet from TUN device, but failed to parse it!"); + continue; + } + let dst = parse_result.unwrap().2; + + let peer = wg.find_peer_by_ip(dst); + if peer.is_none() { + // TODO ICMP no route to host. + debug!("No route to host: {}", dst); + continue; + } + let peer0 = peer.unwrap(); + let should_handshake = { + // Lock peer. + let peer = peer0.read().unwrap(); + if peer.get_endpoint().is_none() { + // TODO ICMP host unreachable? + continue; + } + + if let Some(t) = peer.find_transport_to_send() { + let mut encrypted: [u8; BUFSIZE] = unsafe { uninitialized() }; + let encrypted = &mut encrypted[..pkt.len() + 32]; + let (result, should_handshake) = t.encrypt(pkt, encrypted); + if result.is_ok() { + sock.send_to(encrypted, peer.get_endpoint().unwrap()).unwrap(); + peer.on_send(false); + } + // Optimization: don't bother `do_handshake` if there is already + // an ongoing handshake. + should_handshake && peer.handshake.is_none() + } else { + // TODO: queue packets. + + // Optimization: don't bother `do_handshake` if there is already + // an ongoing handshake. + peer.handshake.is_none() + } + // Release peer. + }; + + if should_handshake { + do_handshake(wg.clone(), peer0, sock.clone()); + } + } + }).unwrap() +} + +/// Start handshake. +/// +/// Better not hold any locks when calling this. +// +/// Nothing happens if there is already an ongoing handshake for this peer. +/// Nothing happens if we don't know peer endpoint. +fn do_handshake(wg: Arc<WgState>, peer0: SharedPeerState, sock: Arc<UdpSocket>) { + // Lock info. + let info = wg.info.read().unwrap(); + + // Lock peer. + let mut peer = peer0.write().unwrap(); + if peer.handshake.is_some() { + return; + } + let endpoint = if peer.get_endpoint().is_none() { + return; + } else { + peer.get_endpoint().unwrap() + }; + + debug!("Handshake init."); + + let id = Id::gen(); + // Lock id_map. + wg.id_map.write().unwrap().insert(id, peer0.clone()); + let handle = IdMapGuard::new(wg.clone(), id); + + let (mut i, hs) = initiate(info.deref(), &peer.info, id); + cookie_sign(&mut i, peer.get_cookie()); + + sock.send_to(&i, endpoint).unwrap(); + let mut mac1 = [0u8; 16]; + mac1.copy_from_slice(&i[116..132]); + peer.last_mac1 = Some(mac1); + + let resend = { + let wg = wg.clone(); + let sock = sock.clone(); + let peer = Arc::downgrade(&peer0); + Box::new(move || { + debug!("Timer: resend."); + peer.upgrade().map(|p| { + p.write().unwrap().handshake = None; + do_handshake(wg.clone(), p, sock.clone()); + }); + }) + }; + + let resend = wg.timer_controller.register_delay(Duration::from_secs(REKEY_TIMEOUT), resend); + resend.activate(); + + peer.handshake = Some(Handshake { + self_id: handle, + hs: hs, + resend: resend, + }); + + peer.clear.adjust_and_activate_if_not_activated(3 * REJECT_AFTER_TIME); +} + +fn do_keep_alive(peer: &PeerState, sock: &UdpSocket) { + let e = peer.get_endpoint(); + if e.is_none() { + return; + } + let e = e.unwrap(); + + let t = peer.find_transport_to_send(); + if t.is_none() { + return; + } + let t = t.unwrap(); + + let mut out = [0u8; 32]; + if t.encrypt(&[], &mut out).0.is_err() { + return; + } + + debug!("Keep alive."); + sock.send_to(&out, e).unwrap(); + + peer.on_send(true); +} + +// Cannot be methods because we need `Arc<WgState>`. + +/// Query state of the WG interface. +pub fn wg_query_state(wg: Arc<WgState>) -> WgStateOut { + let peers = { + // Lock pubkey map. + let pubkey_map = wg.pubkey_map.read().unwrap(); + + pubkey_map.values().map(|p| { + // Lock peer. + let peer = p.read().unwrap(); + + PeerStateOut { + public_key: peer.info.peer_pubkey, + endpoint: peer.info.endpoint, + last_handshake_time: peer.get_last_handshake_time(), + // TODO: Implement tx/rx bytes counting. + rx_bytes: 0, + tx_bytes: 0, + persistent_keepalive_interval: peer.info.keep_alive_interval, + allowed_ips: peer.info.allowed_ips.clone(), + } + // Release peer. + }).collect() + // Release pubkey map. + }; + + // Lock info. + let info = wg.info.read().unwrap(); + WgStateOut { + private_key: info.key.clone(), + public_key: info.pubkey, + preshared_key: info.psk, + peers: peers, + } + // Release info. +} + +/// Change WG interface configuration. +/// +/// All existing sessions and handshakes will be cleared, on the assumption that +/// our crypto keys has changed! +pub fn wg_change_info<F>(wg: Arc<WgState>, f: F) + where F: FnOnce(&mut WgInfo), +{ + // Lock info. + let mut info = wg.info.write().unwrap(); + + // Lock pubkey map. + let pubkey_map = wg.pubkey_map.read().unwrap(); + + for p in pubkey_map.values() { + // Lock peer. + p.write().unwrap().clear(); + // Release peer. + } + + drop(pubkey_map); + // Release pubkey_map. + + f(&mut info); + // Release info. +} + +/// Remove a peer. +/// +/// Returns: whether there was indeed such a peer, with that public key, +/// that has been removed. +pub fn wg_remove_peer(wg: Arc<WgState>, peer_pubkey: &X25519Pubkey) -> bool { + // Remove from pubkey_map. + // Lock pubkey_map. + let mut pubkey_map = wg.pubkey_map.write().unwrap(); + let p = pubkey_map.remove(peer_pubkey); + if p.is_none() { + // Release pubkey_map. + return false; + } + let p = p.unwrap(); + drop(pubkey_map); + // Release pubkey_map. + + // Lock peer. + let mut peer = p.write().unwrap(); + // This will remove peer from `id_map` through `IdMapGuard`. + peer.clear(); + + // Remove from rt4 / rt6. + + // Lock rt4. + let mut rt4 = wg.rt4.write().unwrap(); + // Lock rt6. + let mut rt6 = wg.rt6.write().unwrap(); + for &(a, m) in &peer.info.allowed_ips { + match a { + IpAddr::V4(a) => rt4.remove(a, m), + IpAddr::V6(a) => rt6.remove(a, m), + }; + } + + true +} + +/// Change configuration of a peer. +/// +/// If (and only if) peer public key is changed, ongoing handshake and all +/// transport sessions will be cleared. +/// +/// Returns whether there was indeed such a peer, with that public key, +/// that has been changed. +pub fn wg_change_peer<F>(wg: Arc<WgState>, peer_pubkey: &X25519Pubkey, f: F) -> bool + where F: FnOnce(&mut PeerInfo), +{ + let peer = wg.find_peer_by_pubkey(peer_pubkey); + if peer.is_none() { + return false; + } + let peer0 = peer.unwrap(); + + // Lock peer. + let mut peer = peer0.write().unwrap(); + let old_pubkey = peer.info.peer_pubkey; + let old_allowed_ips = peer.info.allowed_ips.clone(); + + f(&mut peer.info); + + if old_pubkey != peer.info.peer_pubkey { + peer.clear(); + } + + if old_allowed_ips != peer.info.allowed_ips { + // Lock rt4. + let mut rt4 = wg.rt4.write().unwrap(); + // Lock rt6. + let mut rt6 = wg.rt6.write().unwrap(); + + for (a, m) in old_allowed_ips { + match a { + IpAddr::V4(a) => rt4.remove(a, m), + IpAddr::V6(a) => rt6.remove(a, m), + }; + } + + for &(a, m) in &peer.info.allowed_ips { + match a { + IpAddr::V4(a4) => rt4.insert(a4, m, peer0.clone()), + IpAddr::V6(a6) => rt6.insert(a6, m, peer0.clone()), + }; + } + } + + true +} + +/// Add a peer to a WG interface. +/// The peer should not already exist. +pub fn wg_add_peer(wg: Arc<WgState>, peer: &PeerInfo, sock: Arc<UdpSocket>) { + let register = |a| wg.timer_controller.register_delay(Duration::from_secs(0), a); + let dummy_action = || Box::new(|| {}); + + // Lock pubkey_map. + let mut pubkey_map = wg.pubkey_map.write().unwrap(); + + let ps = PeerState { + info: peer.clone(), + last_handshake: None, + last_mac1: None, + cookie: None, + handshake: None, + transport0: None, + transport1: None, + transport2: None, + rekey_no_recv: register(dummy_action()), + keep_alive: register(dummy_action()), + persistent_keep_alive: register(dummy_action()), + clear: register(dummy_action()), + }; + let ps = Arc::new(RwLock::new(ps)); + + // Init timers. + { + let weak_ps = Arc::downgrade(&ps); + // Lock peer. + let mut psw = ps.write().unwrap(); + // Same with rekey. + psw.rekey_no_recv = { + let wg = wg.clone(); + let weak_ps = weak_ps.clone(); + let sock = sock.clone(); + register(Box::new(move || { + weak_ps.upgrade().map(|p| { + debug!("Timer: rekey_no_recv."); + do_handshake(wg.clone(), p, sock.clone()); + }); + })) + }; + psw.keep_alive = { + let weak_ps = weak_ps.clone(); + let sock = sock.clone(); + register(Box::new(move || { + weak_ps.upgrade().map(|p| { + debug!("Timer: keep_alive."); + do_keep_alive(&p.read().unwrap(), &sock); + }); + })) + }; + psw.persistent_keep_alive = { + let weak_ps = weak_ps.clone(); + let sock = sock.clone(); + register(Box::new(move || { + weak_ps.upgrade().map(|p| { + debug!("Timer: persistent_keep_alive."); + do_keep_alive(&p.read().unwrap(), &sock); + }); + })) + }; + psw.clear = { + let weak_ps = weak_ps.clone(); + register(Box::new(move || { + weak_ps.upgrade().map(|p| { + debug!("Timer: clear."); + p.write().unwrap().clear(); + }); + })) + }; + } + + let mut rt4 = wg.rt4.write().unwrap(); + let mut rt6 = wg.rt6.write().unwrap(); + + for &(a, prefix) in &peer.allowed_ips { + match a { + IpAddr::V4(a4) => rt4.insert(a4, prefix, ps.clone()), + IpAddr::V6(a6) => rt6.insert(a6, prefix, ps.clone()), + }; + } + pubkey_map.insert(peer.peer_pubkey, ps); +} + +impl WgState { + /// Create a new `WgState` from `WgInfo`. + pub fn new(info: WgInfo) -> WgState { + let mut cookie = [0u8; 32]; + randombytes_into(&mut cookie); + + WgState { + info: RwLock::new(info), + pubkey_map: RwLock::new(HashMap::with_capacity(1)), + id_map: RwLock::new(HashMap::with_capacity(4)), + rt4: RwLock::new(IpLookupTable::new()), + rt6: RwLock::new(IpLookupTable::new()), + cookie_secret: Mutex::new((cookie, Instant::now())), + timer_controller: TimerController::new(), + } + } + + /// Create a new `WgState`, and add some peers. + pub fn new_with_peers(info: WgInfo, peers: &[PeerInfo], sock: Arc<UdpSocket>) -> Arc<WgState> { + let wg = Arc::new(WgState::new(info)); + + for p in peers { + wg_add_peer(wg.clone(), p, sock.clone()) + } + + wg + } + + // These methods helps a lot in avoiding deadlocks. + + fn find_peer_by_id(&self, id: Id) -> Option<SharedPeerState> { + self.id_map.read().unwrap().get(&id).cloned() + } + + fn find_peer_by_pubkey(&self, pk: &X25519Pubkey) -> Option<SharedPeerState> { + self.pubkey_map.read().unwrap().get(pk).cloned() + } + + /// Find peer by ip address, consulting the routing tables. + fn find_peer_by_ip(&self, addr: IpAddr) -> Option<SharedPeerState> { + match addr { + IpAddr::V4(ip4) => { + self.rt4.read().unwrap().longest_match(ip4).map(|x| x.2.clone()) + } + IpAddr::V6(ip6) => { + self.rt6.read().unwrap().longest_match(ip6).map(|x| x.2.clone()) + } + } + } + + fn get_cookie_secret(&self) -> [u8; 32] { + let mut cs = self.cookie_secret.lock().unwrap(); + let now = Instant::now(); + if now.duration_since(cs.1) <= Duration::from_secs(120) { + cs.0 + } else { + randombytes_into(&mut cs.0); + cs.1 = now; + cs.0 + } + } +} + +impl PeerState { + fn get_endpoint(&self) -> Option<SocketAddr> { + self.info.endpoint + } + + fn set_endpoint(&mut self, a: SocketAddr) { + self.info.endpoint = Some(a) + } + + fn get_cookie(&self) -> Option<&Cookie> { + if self.cookie.is_none() { + return None; + } + if self.cookie.as_ref().unwrap().1.elapsed() >= Duration::from_secs(120) { + return None; + } + Some(&self.cookie.as_ref().unwrap().0) + } + + fn get_last_handshake_time(&self) -> Option<SystemTime> { + self.transport0.as_ref().map(|t| { + let dur = t.created.elapsed(); + SystemTime::now() - dur + }) + } + + fn clear(&mut self) { + self.handshake = None; + self.transport0 = None; + self.transport1 = None; + self.transport2 = None; + + self.rekey_no_recv.de_activate(); + self.keep_alive.de_activate(); + self.persistent_keep_alive.de_activate(); + self.clear.de_activate(); + } + + // rekey = is_initiator. + fn on_new_transport(&self) { + self.clear.adjust_and_activate(3 * REJECT_AFTER_TIME); + self.info.keep_alive_interval.as_ref().map(|i| { + self.persistent_keep_alive.adjust_and_activate(*i as u64); + }); + } + + fn on_recv(&self, is_keepalive: bool) { + self.rekey_no_recv.de_activate(); + if !is_keepalive { + self.keep_alive.adjust_and_activate_if_not_activated(KEEPALIVE_TIMEOUT); + } + } + + fn on_send(&self, is_keepalive: bool) { + self.keep_alive.de_activate(); + if !is_keepalive { + self.rekey_no_recv. + adjust_and_activate_if_not_activated(KEEPALIVE_TIMEOUT + REKEY_TIMEOUT); + } + self.info.keep_alive_interval.as_ref().map(|i| { + self.persistent_keep_alive.adjust_and_activate(*i as u64); + }); + } + + fn push_transport(&mut self, t: Transport) { + self.on_new_transport(); + + self.transport2 = self.transport1.take(); + self.transport1 = self.transport0.take(); + self.transport0 = Some(t); + } + + /// Find a transport to send packet. + fn find_transport_to_send(&self) -> Option<&Transport> { + // If there exists any transport, we rely on timers to init handshake. + + if let Some(ref t) = self.transport0 { + if t.get_should_send() { + return Some(t); + } + } else { + return None; + } + + if let Some(ref t) = self.transport1 { + if t.get_should_send() { + return Some(t); + } + } else { + return None; + } + + if let Some(ref t) = self.transport2 { + if t.get_should_send() { + return Some(t); + } + } + + None + } + + fn find_transport_by_id(&self, id: Id) -> Option<&Transport> { + if let Some(ref t) = self.transport0 { + if t.get_self_id() == id { + return Some(t); + } + } else { + return None; + } + + if let Some(ref t) = self.transport1 { + if t.get_self_id() == id { + return Some(t); + } + } else { + return None; + } + + if let Some(ref t) = self.transport2 { + if t.get_self_id() == id { + return Some(t); + } + } + None + } +} + +impl Transport { + fn new_from_hs(self_id: IdMapGuard, peer_id: Id, hs: HS) -> Self { + let (x, y) = hs.get_ciphers(); + let (s, r) = if hs.get_is_initiator() { + (x, y) + } else { + (y, x) + }; + let sk = s.extract().0; + let rk = r.extract().0; + + Transport { + self_id: self_id, + peer_id: peer_id, + is_initiator: hs.get_is_initiator(), + is_initiator_or_has_received: AtomicBool::new(hs.get_is_initiator()), + not_too_old: AtomicBool::new(true), + send_key: sk, + recv_key: rk, + created: Instant::now(), + recv_ar: Mutex::new(AntiReplay::new()), + send_counter: AtomicU64::new(0), + } + } + + fn get_should_send(&self) -> bool { + self.is_initiator_or_has_received.load(Relaxed) && self.not_too_old.load(Relaxed) + } + + fn get_self_id(&self) -> Id { + self.self_id.id + } + + /// Expect packet with padding. + /// + /// Returns: Whether the operation is successful. Whether we should initiate handshake. + /// + /// Length: out.len() = msg.len() + 32. + fn encrypt(&self, msg: &[u8], out: &mut [u8]) -> (Result<(), ()>, bool) { + let c = self.send_counter.fetch_add(1, Relaxed); + let mut should_rekey = false; + if self.is_initiator && c >= REKEY_AFTER_MESSAGES { + should_rekey = true; + } + if c >= REJECT_AFTER_MESSAGES { + self.not_too_old.store(false, Relaxed); + return (Err(()), should_rekey); + } + + let age = self.created.elapsed(); + + if age >= Duration::from_secs(REKEY_AFTER_TIME) { + should_rekey = true; + } + if age >= Duration::from_secs(REJECT_AFTER_TIME) { + self.not_too_old.store(false, Relaxed); + return (Err(()), should_rekey); + } + + out[0..4].copy_from_slice(&[4, 0, 0, 0]); + out[4..8].copy_from_slice(self.peer_id.as_slice()); + LittleEndian::write_u64(&mut out[8..16], c); + + <ChaCha20Poly1305 as Cipher>::encrypt(&self.send_key, c, &[], msg, &mut out[16..]); + + (Ok(()), should_rekey) + } + + /// Returns packet maybe with padding. + /// + /// Length: out.len() + 32 = msg.len(). + fn decrypt(&self, msg: &[u8], out: &mut [u8]) -> Result<(), ()> { + if self.created.elapsed() >= Duration::from_secs(REJECT_AFTER_TIME) { + return Err(()); + } + + if msg.len() < 32 { + return Err(()); + } + + if msg[0..4] != [4, 0, 0, 0] { + return Err(()); + } + + if self.created.elapsed() >= Duration::from_secs(REJECT_AFTER_TIME) { + return Err(()); + } + + let counter = LittleEndian::read_u64(&msg[8..16]); + + if counter >= REJECT_AFTER_MESSAGES { + return Err(()); + } + + <ChaCha20Poly1305 as Cipher>::decrypt(&self.recv_key, counter, &[], &msg[16..], out)?; + + if !self.recv_ar.lock().unwrap().check_and_update(counter) { + return Err(()); + } + + self.is_initiator_or_has_received.store(true, Relaxed); + + Ok(()) + } +} diff --git a/src/protocol/cookie.rs b/src/protocol/cookie.rs new file mode 100644 index 0000000..008d03f --- /dev/null +++ b/src/protocol/cookie.rs @@ -0,0 +1,139 @@ +// Copyright 2017 Sopium + +// This file is part of WireGuard.rs. + +// WireGuard.rs is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. + +// WireGuard.rs is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with WireGuard.rs. If not, see <https://www.gnu.org/licenses/>. + +extern crate blake2_rfc; +extern crate sodiumoxide; + +use self::blake2_rfc::blake2s::blake2s; +use self::sodiumoxide::randombytes::randombytes_into; +use self::sodiumoxide::utils::memcmp; +use crypto::xchacha20poly1305::{decrypt, encrypt}; +use protocol::{Id, X25519Pubkey}; + +pub type Cookie = [u8; 16]; + +/// Calc cookie according to a secret and a bytes representation of peer address. +/// +/// This is a pure function. +pub fn calc_cookie(secret: &[u8], remote_addr: &[u8]) -> Cookie { + let mut out = [0u8; 16]; + let r = blake2s(16, secret, remote_addr); + out.copy_from_slice(r.as_bytes()); + out +} + +/// Generate cookie reply message (64 bytes). +pub fn cookie_reply(psk: Option<&[u8; 32]>, + pubkey: &X25519Pubkey, + cookie: &Cookie, + peer_index: Id, + mac1: &[u8]) + -> [u8; 64] { + let mut out = [0u8; 64]; + + // Type and zeros. + out[0..4].copy_from_slice(&[3, 0, 0, 0]); + // Receiver index. + out[4..8].copy_from_slice(peer_index.as_slice()); + + { + let (nonce, encrypted_cookie) = out[8..64].split_at_mut(24); + randombytes_into(nonce); + + // Calc encryption key. + let temp = blake2s(32, psk.map_or(&[], |p| p.as_ref()), pubkey.as_ref()); + + // Encrypt cookie. + encrypt(temp.as_bytes(), nonce, mac1, cookie, encrypted_cookie); + } + + out +} + +pub fn process_cookie_reply(psk: Option<&[u8; 32]>, + peer_pubkey: &X25519Pubkey, + mac1: &[u8], + msg: &[u8]) + -> Result<Cookie, ()> { + if msg.len() != 64 { + return Err(()); + } + + if &msg[..4] != &[3, 0, 0, 0] { + return Err(()); + } + + // msg[4..8] is sender index, skip. + + let nonce = &msg[8..32]; + + let ciphertext = &msg[32..64]; + + // Calc encryption key. + let temp = blake2s(32, psk.map_or(&[], |p| p.as_ref()), peer_pubkey); + + let mut cookie = [0u8; 16]; + decrypt(temp.as_bytes(), nonce, mac1, ciphertext, &mut cookie)?; + Ok(cookie) +} + +pub fn cookie_sign(m: &mut [u8], cookie: Option<&Cookie>) { + if cookie.is_none() { + return; + } + let len = m.len(); + let (m1, m2) = m.split_at_mut(len - 16); + let mac2 = blake2s(16, cookie.unwrap(), m1); + m2.copy_from_slice(mac2.as_bytes()); +} + +pub fn cookie_verify(m: &[u8], cookie: &Cookie) -> bool { + if m.len() < 16 { + return false; + } + let (m, mac2) = m.split_at(m.len() - 16); + let mac2_ = blake2s(16, cookie, m); + memcmp(mac2_.as_bytes(), mac2) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cookie() { + let mut psk = [0u8; 32]; + randombytes_into(&mut psk); + + let mut pk = [0u8; 32]; + randombytes_into(&mut pk); + + let mut mac1 = [0u8; 16]; + randombytes_into(&mut mac1); + + let mut secret = [0u8; 32]; + randombytes_into(&mut secret); + + let cookie = calc_cookie(&secret, b"1.2.3.4"); + + let reply = cookie_reply(Some(&psk), &pk, &cookie, Id::gen(), &mac1); + + let cookie1 = process_cookie_reply(Some(&psk), &pk, &mac1, &reply).unwrap(); + + assert_eq!(&cookie, &cookie1); + } +} diff --git a/src/protocol/handshake.rs b/src/protocol/handshake.rs new file mode 100644 index 0000000..f8023ce --- /dev/null +++ b/src/protocol/handshake.rs @@ -0,0 +1,298 @@ +// Copyright 2017 Sopium + +// This file is part of WireGuard.rs. + +// WireGuard.rs is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. + +// WireGuard.rs is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with WireGuard.rs. If not, see <https://www.gnu.org/licenses/>. + +extern crate blake2_rfc; +extern crate noise_protocol; +extern crate noise_sodiumoxide; +extern crate sodiumoxide; +extern crate tai64; + +use self::blake2_rfc::blake2s::Blake2s; +use self::noise_protocol::*; +use self::noise_protocol::patterns::noise_ik; +use self::noise_sodiumoxide::{ChaCha20Poly1305, X25519}; +use self::sodiumoxide::utils::memcmp; +use self::tai64::TAI64N; +use protocol::*; + +const PROLOGUE: &'static [u8] = b"WireGuard v0 zx2c4 Jason@zx2c4.com"; + +pub type HS = HandshakeState<X25519, ChaCha20Poly1305, NoiseBlake2s>; + +#[derive(Clone)] +pub struct NoiseBlake2s(Blake2s); + +impl Default for NoiseBlake2s { + fn default() -> Self { + NoiseBlake2s(Blake2s::new(32)) + } +} + +impl Hash for NoiseBlake2s { + type Output = [u8; 32]; + type Block = [u8; 64]; + + fn name() -> &'static str { + "BLAKE2s" + } + + fn input(&mut self, data: &[u8]) { + self.0.update(data); + } + + fn result(&mut self) -> Self::Output { + Self::Output::from_slice(self.0 + .clone() + .finalize() + .as_bytes()) + } +} + +/// Calc mac/hash of some data, with an optional key. +/// +/// This is exactly the mac function defined in WireGuard paper. +fn mac<K>(key: Option<K>, data: &[&[u8]]) -> [u8; 16] + where K: AsRef<[u8]> +{ + let mut mac = [0u8; 16]; + let mut blake2s = Blake2s::with_key(16, key.as_ref().map_or(&[], |k| k.as_ref())); + for d in data { + blake2s.update(d); + } + mac.copy_from_slice(blake2s.finalize().as_bytes()); + mac +} + +/// Generate handshake initiation message. +/// +/// Will generate a new ephemeral key and use current timestamp. +/// +/// Returns: Message, noise handshake state. +pub fn initiate(wg: &WgInfo, peer: &PeerInfo, self_index: Id) -> ([u8; 148], HS) { + let mut msg = [0u8; 148]; + + let mut hs = { + let mut hsbuilder = HandshakeStateBuilder::<X25519>::new(); + hsbuilder.set_pattern(noise_ik()); + hsbuilder.set_is_initiator(true); + hsbuilder.set_prologue(PROLOGUE); + if let Some(ref psk) = wg.psk { + hsbuilder.set_psk(psk.as_slice()); + } + hsbuilder.set_s(wg.key.clone()); + hsbuilder.set_rs(peer.peer_pubkey); + hsbuilder.build_handshake_state() + }; + + // Type and reserved zeros. + msg[0..4].copy_from_slice(&[1, 0, 0, 0]); + // Self index. + msg[4..8].copy_from_slice(self_index.as_slice()); + + // Noise part: e, s, timestamp. + let timestamp = TAI64N::now(); + hs.write_message(×tamp.to_external(), &mut msg[8..116]); + + // Mac1. + let mac1 = mac(wg.psk.as_ref(), + &[peer.peer_pubkey.as_slice(), &msg[..116]]); + msg[116..132].copy_from_slice(&mac1); + + (msg, hs) +} + +pub struct InitProcessResult { + pub peer_id: Id, + pub timestamp: TAI64N, + pub handshake_state: HS, +} + +/// Process a handshake initiation message. +/// +/// Will generate a new ephemeral key. +pub fn process_initiation(wg: &WgInfo, msg: &[u8]) -> Result<InitProcessResult, ()> { + if msg.len() != 148 { + return Err(()); + } + + // Check mac1. + let mac1 = mac(wg.psk.as_ref(), &[&wg.pubkey, &msg[..116]]); + if !memcmp(&mac1, &msg[116..132]) { + return Err(()); + } + + // Check type and zeros. + if &msg[0..4] != &[1, 0, 0, 0] { + return Err(()); + } + + // Peer index. + let peer_index = Id::from_slice(&msg[4..8]); + + let mut hs: HS = { + let mut hsbuilder = HandshakeStateBuilder::<X25519>::new(); + hsbuilder.set_is_initiator(false); + hsbuilder.set_prologue(PROLOGUE); + hsbuilder.set_pattern(noise_ik()); + if let Some(ref psk) = wg.psk { + hsbuilder.set_psk(psk); + } + hsbuilder.set_s(wg.key.clone()); + hsbuilder.build_handshake_state() + }; + + // Noise message, contains encrypted timestamp. + let mut timestamp = [0u8; 12]; + hs.read_message(&msg[8..116], &mut timestamp).map_err(|_| ())?; + let timestamp = TAI64N::from_external(×tamp).ok_or(())?; + + Ok(InitProcessResult { + peer_id: peer_index, + timestamp: timestamp, + handshake_state: hs, + }) +} + +/// Generate handshake response message. +pub fn responde(wg: &WgInfo, result: &mut InitProcessResult, self_id: Id) -> [u8; 92] { + let mut response = [0u8; 92]; + + // Type and zeros. + response[0..4].copy_from_slice(&[2, 0, 0, 0]); + response[4..8].copy_from_slice(self_id.as_slice()); + response[8..12].copy_from_slice(result.peer_id.as_slice()); + + let mut hs = &mut result.handshake_state; + + hs.write_message(&[], &mut response[12..60]); + + let mac1 = mac(wg.psk.as_ref(), &[hs.get_rs().as_ref().unwrap(), &response[..60]]); + response[60..76].copy_from_slice(&mac1); + + response +} + +/// Process handshake response message. +/// +/// Returns peer index. +pub fn process_response(wg: &WgInfo, hs: &mut HS, msg: &[u8]) -> Result<Id, ()> { + if msg.len() != 92 { + return Err(()); + } + + // Check mac1. + let mac1 = mac(wg.psk.as_ref(), &[&wg.pubkey, &msg[..60]]); + + if !memcmp(&mac1, &msg[60..76]) { + return Err(()); + } + + // Check type and zeros. + if &msg[0..4] != &[2, 0, 0, 0] { + return Err(()); + } + + // Peer index. + let peer_index = Id::from_slice(&msg[4..8]); + + // msg[8..12] is self index, skip. + + let mut out = []; + + hs.read_message(&msg[12..60], &mut out).map_err(|_| ())?; + + Ok(peer_index) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn wg_handshake_init_responde() { + let k = X25519::genkey(); + let init = WgInfo { + psk: None, + pubkey: X25519::pubkey(&k), + key: k, + }; + + let k = X25519::genkey(); + let resp = WgInfo { + psk: None, + pubkey: X25519::pubkey(&k), + key: k, + }; + + let init_peer = PeerInfo { + peer_pubkey: Clone::clone(&resp.pubkey), + endpoint: None, + allowed_ips: vec![], + keep_alive_interval: None, + }; + + let si = Id::gen(); + let (m0, mut ihs) = initiate(&init, &init_peer, si); + let mut result0 = process_initiation(&resp, &m0).unwrap(); + let ri = Id::gen(); + let m1 = responde(&resp, &mut result0, ri); + let ri1 = process_response(&init, &mut ihs, &m1).unwrap(); + + assert_eq!(result0.peer_id, si); + assert_eq!(ri1, ri); + + assert_eq!(ihs.get_hash(), result0.handshake_state.get_hash()); + } + + #[test] + fn wg_handshake_init_responde_with_psk() { + let psk = [0xc7; 32]; + + let k = X25519::genkey(); + let init = WgInfo { + psk: Some(psk), + pubkey: X25519::pubkey(&k), + key: k, + }; + + let k = X25519::genkey(); + let resp = WgInfo { + psk: Some(psk), + pubkey: X25519::pubkey(&k), + key: k, + }; + + let init_peer = PeerInfo { + peer_pubkey: Clone::clone(&resp.pubkey), + endpoint: None, + allowed_ips: vec![], + keep_alive_interval: None, + }; + + let si = Id::gen(); + let (m0, mut ihs) = initiate(&init, &init_peer, si); + let mut result0 = process_initiation(&resp, &m0).unwrap(); + let ri = Id::gen(); + let m1 = responde(&resp, &mut result0, ri); + let ri1 = process_response(&init, &mut ihs, &m1).unwrap(); + + assert_eq!(result0.peer_id, si); + assert_eq!(ri1, ri); + + assert_eq!(ihs.get_hash(), result0.handshake_state.get_hash()); + } +} diff --git a/src/protocol/ip.rs b/src/protocol/ip.rs new file mode 100644 index 0000000..f6b9920 --- /dev/null +++ b/src/protocol/ip.rs @@ -0,0 +1,51 @@ +// Copyright 2017 Sopium + +// This file is part of WireGuard.rs. + +// WireGuard.rs is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. + +// WireGuard.rs is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with WireGuard.rs. If not, see <https://www.gnu.org/licenses/>. + +extern crate pnet; + +use self::pnet::packet::ipv4::Ipv4Packet; +use self::pnet::packet::ipv6::Ipv6Packet; +use std::net::IpAddr; + +// IP packet parsing. + +/// Parses IPv4/v6 packet, returns total length, source, destination. +pub fn parse_ip_packet(packet: &[u8]) -> Result<(u16, IpAddr, IpAddr), ()> { + if packet.len() < 20 { + return Err(()); + } + + let v = packet[0] >> 4; + + if v == 4 { + // IPv4. + let p = Ipv4Packet::new(packet).ok_or(())?; + let len = p.get_total_length(); + let src = p.get_source(); + let dst = p.get_destination(); + Ok((len, IpAddr::V4(src), IpAddr::V4(dst))) + } else if v == 6 { + // IPv6. + let p = Ipv6Packet::new(packet).ok_or(())?; + let len = p.get_payload_length() + 40; + let src = p.get_source(); + let dst = p.get_destination(); + Ok((len, IpAddr::V6(src), IpAddr::V6(dst))) + } else { + Err(()) + } +} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs new file mode 100644 index 0000000..c8eff1b --- /dev/null +++ b/src/protocol/mod.rs @@ -0,0 +1,46 @@ +// Copyright 2017 Sopium + +// This file is part of WireGuard.rs. + +// WireGuard.rs is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. + +// WireGuard.rs is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with WireGuard.rs. If not, see <https://www.gnu.org/licenses/>. + +//! WireGuard protocol implementation. + +/// Handshake messages generation and parsing. +mod handshake; +/// Anti-Replay algorithm. +mod anti_replay; +/// Cookie reply messages generation and parsing. +mod cookie; +/// Common types. +mod types; +/// IP packet parsing. +mod ip; +/// The timer state machine, and actual IO stuff. +mod controller; +/// A generic timer, but optimised for operations mostly used in WG. +mod timer; + +/// Re-export some types and functions from other crates, so users +/// of this module won't have to manually pull in all these crates. +pub mod re_exports; + +use self::anti_replay::*; +pub use self::controller::*; +use self::cookie::*; +use self::handshake::*; +use self::ip::*; +use self::timer::*; +pub use self::types::{WgInfo, PeerInfo}; +use self::types::*; diff --git a/src/protocol/re_exports.rs b/src/protocol/re_exports.rs new file mode 100644 index 0000000..08a3b0c --- /dev/null +++ b/src/protocol/re_exports.rs @@ -0,0 +1,23 @@ +// Copyright 2017 Sopium + +// This file is part of WireGuard.rs. + +// WireGuard.rs is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. + +// WireGuard.rs is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with WireGuard.rs. If not, see <https://www.gnu.org/licenses/>. + +extern crate noise_protocol; +extern crate noise_sodiumoxide; + +pub use self::noise_protocol::{Cipher, DH, Hash, U8Array}; +pub use self::noise_sodiumoxide::{ChaCha20Poly1305, Sensitive, X25519}; +pub use self::noise_sodiumoxide::init as sodium_init; diff --git a/src/protocol/timer.rs b/src/protocol/timer.rs new file mode 100644 index 0000000..d2bcfcb --- /dev/null +++ b/src/protocol/timer.rs @@ -0,0 +1,197 @@ +// Copyright 2017 Sopium + +// This file is part of WireGuard.rs. + +// WireGuard.rs is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. + +// WireGuard.rs is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with WireGuard.rs. If not, see <https://www.gnu.org/licenses/>. + +use std::collections::HashSet; +use std::hash::{Hash, Hasher}; +use std::ops::Deref; +use std::sync::{Arc, Mutex}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::thread; +use std::time::{Duration, Instant}; + +type Action = Box<Fn() + Send + Sync>; + +struct Timer { + activated: AtomicBool, + // Actually, this is not used outside the big whole wheel mutex. + // But how to tell the compiler that??? + rounds: AtomicUsize, + action: Action, +} + +pub struct TimerHandle { + controller: Arc<Mutex<Wheel>>, + pos: AtomicUsize, + timer: ArcTimer, +} + +// A single round will be ~ 16 seconds. +// WireGuard timers are mostly in this range. +const WHEEL_SLOTS: usize = 128; +const TICK_MS: usize = 128; + +// This is hashed timing wheel. +pub struct TimerController(Arc<Mutex<Wheel>>); + +struct Wheel { + // Usually a linked list is used in each slot. + // Use hash table for now, for implementation simplicity. + // It's slower but should still have same complexity. + wheel: Vec<HashSet<ArcTimer>>, + cur: usize, +} + +impl TimerController { + pub fn new() -> Self { + let con = Arc::new(Mutex::new(Wheel { + wheel: ::std::iter::repeat(HashSet::new()).take(WHEEL_SLOTS).collect(), + cur: 0, + })); + let con1 = con.clone(); + + thread::Builder::new().name("timer".to_string()).spawn(move || { + loop { + let tick_start = Instant::now(); + + let mut to_run = Vec::new(); + let mut wheel = con.lock().unwrap(); + let cur = wheel.cur; + wheel.cur = (wheel.cur + 1) % WHEEL_SLOTS; + { + let slot = &mut wheel.wheel[cur]; + + slot.retain(|t| { + Arc::strong_count(&t.0) > 1 + }); + + for t in slot.iter() { + // `fetch_sub` always wraps, doesn't it? + let rounds = t.rounds.fetch_sub(1, Ordering::Relaxed); + if t.activated.load(Ordering::Relaxed) && rounds == 0 { + to_run.push(t.clone()); + t.activated.store(false, Ordering::Relaxed); + } + } + } + drop(wheel); + + for t in to_run { + (t.action)(); + } + + let spent = tick_start.elapsed(); + #[allow(non_snake_case)] + let TICK = Duration::from_millis(TICK_MS as u64); + + if spent >= TICK { + warn!("timer tick processing time exceeds TICK!"); + } else { + thread::sleep(TICK - spent); + } + } + }).unwrap(); + + TimerController(con1) + } + + pub fn register_delay(&self, delay: Duration, action: Action) -> TimerHandle { + let (offset, rounds) = calc_offset_and_rounds(delay); + + let mut wheel = self.0.lock().unwrap(); + let pos = (wheel.cur + offset) % WHEEL_SLOTS; + + let timer = Arc::new(Timer { + activated: AtomicBool::new(false), + rounds: AtomicUsize::new(rounds), + action: action, + }); + + wheel.wheel[pos].insert(ArcTimer(timer.clone())); + + TimerHandle { + controller: self.0.clone(), + pos: AtomicUsize::new(pos), + timer: ArcTimer(timer), + } + } +} + +impl TimerHandle { + pub fn activate(&self) { + self.timer.activated.store(true, Ordering::Relaxed); + } + + pub fn de_activate(&self) { + self.timer.activated.store(false, Ordering::Relaxed); + } + + pub fn adjust_and_activate(&self, secs: u64) { + let (offset, rounds) = calc_offset_and_rounds(Duration::from_secs(secs)); + + let mut wheel = self.controller.lock().unwrap(); + let old_pos = self.pos.load(Ordering::Relaxed); + let new_pos = (wheel.cur + offset) % WHEEL_SLOTS; + self.pos.store(new_pos, Ordering::Relaxed); + self.timer.rounds.store(rounds, Ordering::Relaxed); + + let t = wheel.wheel[old_pos].take(&self.timer).unwrap(); + wheel.wheel[new_pos].insert(t); + + self.timer.activated.store(true, Ordering::Release); + } + + pub fn adjust_and_activate_if_not_activated(&self, secs: u64) { + if !self.timer.activated.load(Ordering::Relaxed) { + self.adjust_and_activate(secs); + } + } +} + +fn calc_offset_and_rounds(delay: Duration) -> (usize, usize) { + let delay_ms = delay.as_secs() * 1000 + delay.subsec_nanos() as u64 / 1000000; + let ticks = ::std::cmp::max(delay_ms as usize / TICK_MS, 1); + let offset = ticks % WHEEL_SLOTS; + let rounds = ticks / WHEEL_SLOTS; + (offset, rounds) +} + +/// Hash and Eq by pointer address. +#[derive(Clone)] +struct ArcTimer(Arc<Timer>); + +impl Deref for ArcTimer { + type Target = Timer; + + fn deref(&self) -> &Timer { + self.0.deref() + } +} + +impl Hash for ArcTimer { + fn hash<H: Hasher>(&self, state: &mut H) { + (self.0.deref() as *const Timer).hash(state); + } +} + +impl PartialEq for ArcTimer { + fn eq(&self, other: &ArcTimer) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl Eq for ArcTimer { +} diff --git a/src/protocol/types.rs b/src/protocol/types.rs new file mode 100644 index 0000000..cc8db4d --- /dev/null +++ b/src/protocol/types.rs @@ -0,0 +1,139 @@ +// Copyright 2017 Sopium + +// This file is part of WireGuard.rs. + +// WireGuard.rs is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. + +// WireGuard.rs is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with WireGuard.rs. If not, see <https://www.gnu.org/licenses/>. + +extern crate noise_protocol; +extern crate noise_sodiumoxide; +extern crate sodiumoxide; + +use self::noise_protocol::DH; +use self::noise_sodiumoxide::X25519; +use self::sodiumoxide::randombytes::randombytes_into; +use std::net::{IpAddr, SocketAddr}; +use std::ops::Deref; +use std::time::SystemTime; + +/// X25519 private key. +pub type X25519Key = <X25519 as DH>::Key; +/// X25519 pubkey key. +pub type X25519Pubkey = <X25519 as DH>::Pubkey; + +/// Config info about a WireGuard peer. +#[derive(Clone)] +pub struct PeerInfo { + /// Peer public key. + pub peer_pubkey: X25519Pubkey, + /// Peer endpoint. + pub endpoint: Option<SocketAddr>, + /// Allowed source IPs. + pub allowed_ips: Vec<(IpAddr, u32)>, + /// Persistent keep-alive interval. + /// Valid values: 1 - 0xfffe. + pub keep_alive_interval: Option<u16>, +} + +/// Config info about a WireGuard interface. +pub struct WgInfo { + /// Optional pre-shared key. + pub psk: Option<[u8; 32]>, + /// Self private key. + pub key: X25519Key, + /// Self public key. + /// Should correspond to self private key. + pub pubkey: X25519Pubkey, +} + +/// State of WireGuard interface. +pub struct WgStateOut { + /// Self public key. + pub public_key: X25519Pubkey, + /// Self private key. + pub private_key: X25519Key, + /// Pre-shared key. + pub preshared_key: Option<[u8; 32]>, + /// Peers. + pub peers: Vec<PeerStateOut>, +} + +/// State of a peer. +pub struct PeerStateOut { + /// Public key. + pub public_key: X25519Pubkey, + /// Endpoint. + pub endpoint: Option<SocketAddr>, + /// Last handshake time. + pub last_handshake_time: Option<SystemTime>, + /// Received bytes. + pub rx_bytes: u64, + /// Sent bytes. + pub tx_bytes: u64, + /// Persistent keep-alive interval. + pub persistent_keepalive_interval: Option<u16>, + /// Allowed IP addresses. + pub allowed_ips: Vec<(IpAddr, u32)>, +} + +impl WgInfo { + /// Create a new `WgInfo`. + pub fn new(psk: Option<[u8; 32]>, key: X25519Key) -> Self { + let pk = <X25519 as DH>::pubkey(&key); + WgInfo { + psk: psk, + key: key, + pubkey: pk, + } + } +} + +/// Sender index or receiver index. +/// +/// WireGuard treats an index as a `u32` in little endian. +/// Why not just treat it as a 4-byte array? +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Id(pub [u8; 4]); + +impl Id { + /// Generate a new random ID. + pub fn gen() -> Id { + let mut id = [0u8; 4]; + randombytes_into(&mut id); + Id(id) + } + + /// Create Id from a slice. + /// + /// # Panics + /// + /// Slice must be 4 bytes long. + pub fn from_slice(id: &[u8]) -> Id { + let mut ret = Id([0u8; 4]); + ret.0.copy_from_slice(id); + ret + } + + /// As slice. + pub fn as_slice(&self) -> &[u8] { + &self.0 + } +} + +impl Deref for Id { + type Target = [u8]; + + fn deref(&self) -> &[u8] { + self.as_slice() + } +} |