mod beacon;
pub use beacon::*;
use std::io;
use std::net::{SocketAddr, ToSocketAddrs, UdpSocket};
pub const PROTOCOL_VERSION: u8 = 49;
pub const MAX_PEERS: usize = 40;
pub const MAX_DATAGRAM: usize = 1200;
pub const MAX_BEACH_BYTES: usize = 823;
pub struct UdpTransport {
socket: UdpSocket,
peers: Vec<SocketAddr>,
accept_new: bool,
max_peers: usize,
turned_away: Vec<(SocketAddr, NetMsg)>,
}
const TURNED_AWAY: usize = 64;
const GOODBYES: usize = 3;
impl Drop for UdpTransport {
fn drop(&mut self) {
for _ in 0..GOODBYES {
self.send(NetMsg::Leaving);
}
}
}
pub fn ping_stamp() -> u32 {
if let Some(ms) = STEERED_PING_CLOCK.get() {
return ms;
}
static ORIGIN: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
let elapsed = ORIGIN.get_or_init(std::time::Instant::now).elapsed();
elapsed.as_millis() as u32
}
thread_local! {
static STEERED_PING_CLOCK: std::cell::Cell<Option<u32>> = const { std::cell::Cell::new(None) };
}
pub fn steer_ping_clock(ms: u32) {
STEERED_PING_CLOCK.set(Some(ms));
}
const SLOWEST_ROUND_TRIP_MS: u32 = 5_000;
pub fn round_trip_since(stamp: u32) -> Option<f32> {
let ms = ping_stamp().wrapping_sub(stamp);
(ms <= SLOWEST_ROUND_TRIP_MS).then(|| ms as f32 / 1000.0)
}
pub fn bind_udp(port: u16) -> io::Result<UdpSocket> {
match port {
0 => off_the_ladder(&LOBBY_PORTS, || UdpSocket::bind(("0.0.0.0", 0))),
port => UdpSocket::bind(("0.0.0.0", port)),
}
}
fn off_the_ladder(
ladder: &[u16],
mut bind: impl FnMut() -> io::Result<UdpSocket>,
) -> io::Result<UdpSocket> {
let mut turned_down = Vec::new();
loop {
let socket = bind()?;
if !ladder.contains(&socket.local_addr()?.port()) {
return Ok(socket);
}
turned_down.push(socket);
}
}
pub fn local_ip() -> Option<std::net::IpAddr> {
let probe = UdpSocket::bind(("0.0.0.0", 0)).ok()?;
probe.connect(("203.0.113.1", 9)).ok()?;
let ip = probe.local_addr().ok()?.ip();
(!ip.is_unspecified() && !ip.is_loopback()).then_some(ip)
}
impl UdpTransport {
pub fn host(port: u16) -> io::Result<UdpTransport> {
let socket = bind_udp(port)?;
socket.set_nonblocking(true)?;
Ok(UdpTransport {
socket,
peers: Vec::new(),
accept_new: true,
max_peers: MAX_PEERS,
turned_away: Vec::new(),
})
}
pub fn join(host: impl ToSocketAddrs) -> io::Result<UdpTransport> {
let socket = bind_udp(0)?;
socket.set_nonblocking(true)?;
let peer = host
.to_socket_addrs()?
.next()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "unresolvable host"))?;
Ok(UdpTransport {
socket,
peers: vec![peer],
accept_new: false,
max_peers: 1,
turned_away: Vec::new(),
})
}
pub fn connected(&self) -> bool {
!self.peers.is_empty()
}
pub fn peer_count(&self) -> usize {
self.peers.len()
}
pub fn local_addr(&self) -> io::Result<SocketAddr> {
self.socket.local_addr()
}
pub fn peer_addr(&self) -> Option<SocketAddr> {
match self.peers.as_slice() {
[only] => Some(*only),
_ => None,
}
}
pub fn send(&self, msg: NetMsg) {
let bytes = msg.encode();
for peer in &self.peers {
let _ = self.socket.send_to(&bytes, peer);
}
}
pub fn send_batch(&self, batch: &Batch, mut hears: impl FnMut(usize) -> bool) {
let audience: Vec<&SocketAddr> = self
.peers
.iter()
.enumerate()
.filter(|(index, _)| hears(*index))
.map(|(_, peer)| peer)
.collect();
if audience.is_empty() {
return;
}
write_pieces(batch, |bytes| {
for peer in &audience {
let _ = self.socket.send_to(bytes, peer);
}
});
}
#[cfg(test)]
fn send_raw(&self, bytes: &[u8]) {
for peer in &self.peers {
let _ = self.socket.send_to(bytes, peer);
}
}
pub fn forget(&mut self, index: usize) {
if index < self.peers.len() {
self.peers.remove(index);
}
}
pub fn turn_away(&mut self, index: usize) {
self.turn_away_saying(index, NetMsg::Kicked);
}
pub fn turn_away_saying(&mut self, index: usize, word: NetMsg) {
let Some(&addr) = self.peers.get(index) else {
return;
};
for _ in 0..3 {
self.send_to(index, word.clone());
}
self.forget(index);
if self.turned_away.len() >= TURNED_AWAY {
self.turned_away.remove(0);
}
self.turned_away.push((addr, word));
}
pub fn send_to(&self, index: usize, msg: NetMsg) {
debug_assert!(
index < self.peers.len(),
"sending to peer {index} of {}",
self.peers.len()
);
if let Some(peer) = self.peers.get(index) {
let _ = self.socket.send_to(&msg.encode(), peer);
}
}
pub fn recv_all(&mut self) -> Vec<(NetMsg, usize)> {
let mut out = Vec::new();
let mut buf = [0u8; MAX_DATAGRAM];
loop {
match self.socket.recv_from(&mut buf) {
Ok((len, from)) => {
let Some(msg) = NetMsg::decode(&buf[..len]) else {
if NetMsg::peek_version(&buf[..len])
.is_some_and(|version| version != PROTOCOL_VERSION)
{
let reply = NetMsg::Incompatible {
version: PROTOCOL_VERSION,
};
let _ = self.socket.send_to(&reply.encode(), from);
}
continue;
};
if let Some((_, word)) = self.turned_away.iter().find(|(gone, _)| *gone == from)
{
let _ = self.socket.send_to(&word.clone().encode(), from);
continue;
}
let index = match self.peers.iter().position(|p| *p == from) {
Some(index) => index,
None if self.accept_new && msg.greets() => {
if self.peers.len() >= self.max_peers {
let _ = self.socket.send_to(&NetMsg::Full.encode(), from);
continue;
}
self.peers.push(from);
self.peers.len() - 1
}
None => continue,
};
out.push((msg, index));
}
Err(e) if drain_goes_on(&e) => {}
Err(_) => break,
}
}
out
}
}
pub(crate) fn drain_goes_on(e: &io::Error) -> bool {
matches!(
e.kind(),
io::ErrorKind::ConnectionReset | io::ErrorKind::ConnectionRefused
)
}
mod msg;
mod wire;
pub use msg::*;
pub use wire::*;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_free_port_is_never_a_lobby_port() {
let mut drawn: Vec<UdpSocket> = (0..3)
.map(|_| UdpSocket::bind(("127.0.0.1", 0)).expect("bind"))
.collect();
let ports: Vec<u16> = drawn
.iter()
.map(|socket| socket.local_addr().expect("port").port())
.collect();
drawn.reverse();
let mut draws = 0;
let kept = off_the_ladder(&ports[..2], || {
draws += 1;
Ok(drawn.pop().expect("a socket left"))
})
.expect("a socket");
assert_eq!(kept.local_addr().expect("port").port(), ports[2]);
assert_eq!(draws, 3, "both ladder ports turned down");
let socket = bind_udp(0).expect("bind");
let port = socket.local_addr().expect("port").port();
assert!(!LOBBY_PORTS.contains(&port), "{port} is a lobby port");
}
#[test]
fn a_peer_turned_away_is_not_taken_back() {
let mut host = UdpTransport::host(0).expect("bind");
let port = host.local_addr().expect("addr").port();
let mut joiner = UdpTransport::join(("127.0.0.1", port)).expect("join");
let settle = |host: &mut UdpTransport| {
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(5));
if !host.recv_all().is_empty() {
break;
}
}
};
joiner.send(NetMsg::hello("Bo"));
settle(&mut host);
assert_eq!(host.peer_count(), 1, "Bo is aboard");
host.turn_away(0);
assert_eq!(host.peer_count(), 0);
joiner.send(NetMsg::hello("Bo"));
for _ in 0..20 {
std::thread::sleep(std::time::Duration::from_millis(5));
assert!(host.recv_all().is_empty(), "nothing from Bo is taken in");
}
assert_eq!(host.peer_count(), 0, "and Bo has no place at the table");
let kicked = joiner
.recv_all()
.into_iter()
.filter(|(msg, _)| *msg == NetMsg::Kicked)
.count();
assert!(kicked > 3, "told on the way out and told again: {kicked}");
}
#[test]
fn a_stray_goodbye_admits_nobody() {
let mut host = UdpTransport::host(0).expect("bind");
let port = host.local_addr().expect("addr").port();
let stranger = UdpTransport::join(("127.0.0.1", port)).expect("join");
for _ in 0..3 {
stranger.send(NetMsg::Leaving);
}
for _ in 0..20 {
std::thread::sleep(std::time::Duration::from_millis(5));
assert!(host.recv_all().is_empty(), "nothing to act on");
}
assert_eq!(host.peer_count(), 0, "and nobody new at the table");
stranger.send(NetMsg::hello("Bo"));
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(5));
if !host.recv_all().is_empty() {
break;
}
}
assert_eq!(host.peer_count(), 1);
}
#[test]
fn only_a_greeting_admits_a_newcomer() {
let mut host = UdpTransport::host(0).expect("bind");
let port = host.local_addr().expect("addr").port();
let stranger = UdpTransport::join(("127.0.0.1", port)).expect("join");
stranger.send(NetMsg::Pause {
round: 0,
frame: 40,
});
stranger.send(NetMsg::Resume {
round: 0,
frame: 40,
});
stranger.send(NetMsg::Hash {
round: 0,
frame: 30,
hash: 1,
});
stranger.send(NetMsg::chat("Bo", "hi"));
stranger.send_batch(
&Batch {
inputs: vec![crate::sim::InputMsg {
player: 1,
frame: 3,
action: crate::sim::PlayerAction::None,
}],
..Batch::default()
},
|_| true,
);
for _ in 0..20 {
std::thread::sleep(std::time::Duration::from_millis(5));
assert!(host.recv_all().is_empty(), "nothing from a stranger");
}
assert_eq!(host.peer_count(), 0, "and nobody new at the table");
let watcher = UdpTransport::join(("127.0.0.1", port)).expect("join");
stranger.send(NetMsg::hello_bot("Greedy"));
watcher.send(NetMsg::watch("Dee"));
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(5));
host.recv_all();
if host.peer_count() == 2 {
break;
}
}
assert_eq!(host.peer_count(), 2, "both greetings were taken");
}
#[test]
fn a_mismatched_greeting_is_answered_not_swallowed() {
let mut host = UdpTransport::host(0).expect("bind");
let port = host.local_addr().expect("addr").port();
let mut joiner = UdpTransport::join(("127.0.0.1", port)).expect("join");
let mut hello = NetMsg::hello("Anna").encode();
hello[1] = PROTOCOL_VERSION.wrapping_add(1); let mut answer = None;
for _ in 0..40 {
joiner.send_raw(&hello);
std::thread::sleep(std::time::Duration::from_millis(5));
assert!(host.recv_all().is_empty(), "nothing the host can act on");
if let Some((msg, _)) = joiner.recv_all().into_iter().next() {
answer = Some(msg);
break;
}
}
assert_eq!(
answer,
Some(NetMsg::Incompatible {
version: PROTOCOL_VERSION
})
);
assert_eq!(host.peer_count(), 0, "and it claimed no seat at the table");
}
#[test]
fn a_pong_is_timed_by_its_own_stamp() {
let now = ping_stamp();
assert!(round_trip_since(now).is_some_and(|seconds| seconds < 1.0));
assert_eq!(round_trip_since(now.wrapping_add(60_000)), None);
assert_eq!(round_trip_since(now.wrapping_sub(60_000)), None);
}
#[test]
fn a_closing_socket_says_goodbye() {
let mut host = UdpTransport::host(0).expect("bind");
let port = host.local_addr().expect("addr").port();
let joiner = UdpTransport::join(("127.0.0.1", port)).expect("join");
joiner.send(NetMsg::hello("Bo"));
let moved = joiner;
std::thread::sleep(std::time::Duration::from_millis(20));
let heard: Vec<NetMsg> = host.recv_all().into_iter().map(|(msg, _)| msg).collect();
assert_eq!(heard, [NetMsg::hello("Bo")], "nothing said on the move");
drop(moved);
let mut goodbyes = 0;
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(5));
goodbyes += host
.recv_all()
.into_iter()
.filter(|(msg, peer)| *msg == NetMsg::Leaving && *peer == 0)
.count();
if goodbyes == GOODBYES {
break;
}
}
assert_eq!(goodbyes, GOODBYES);
}
#[test]
fn a_bounced_send_does_not_end_the_drain() {
use io::ErrorKind::*;
for kind in [ConnectionReset, ConnectionRefused] {
assert!(drain_goes_on(&io::Error::from(kind)), "{kind:?}");
}
for kind in [WouldBlock, PermissionDenied, Other] {
assert!(!drain_goes_on(&io::Error::from(kind)), "{kind:?}");
}
}
#[test]
fn host_accepts_peers_up_to_its_cap() {
let mut host = UdpTransport::host(0).expect("bind");
let port = host.local_addr().expect("addr").port();
let mut joiners: Vec<UdpTransport> = (0..MAX_PEERS + 2)
.map(|_| UdpTransport::join(("127.0.0.1", port)).expect("join"))
.collect();
for joiner in &joiners {
joiner.send(NetMsg::hello(""));
}
for _ in 0..40 {
std::thread::sleep(std::time::Duration::from_millis(5));
let _ = host.recv_all();
if host.peer_count() == MAX_PEERS {
break;
}
}
assert_eq!(
host.peer_count(),
MAX_PEERS,
"the ones past the cap are refused"
);
std::thread::sleep(std::time::Duration::from_millis(20));
let told: Vec<bool> = joiners
.iter_mut()
.map(|joiner| {
joiner
.recv_all()
.iter()
.any(|(msg, _)| *msg == NetMsg::Full)
})
.collect();
assert_eq!(
told.iter().filter(|full| **full).count(),
2,
"and each of them is told the beach is full"
);
}
}