use super::{SdkError, SdkResult};
use std::collections::HashMap;
use std::net::{SocketAddr, TcpListener, UdpSocket};
use std::ops::Range;
use std::sync::Mutex;
use tracing::{debug, info};
lazy_static::lazy_static! {
static ref ALLOCATED_PORTS: Mutex<HashMap<u16, Protocol>> = Mutex::new(HashMap::new());
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Protocol {
Tcp,
Udp,
Both,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AllocatedPort {
pub port: u16,
pub protocol: Protocol,
}
impl AllocatedPort {
pub fn new(port: u16, protocol: Protocol) -> Self {
Self { port, protocol }
}
}
pub fn allocate_port(range: Range<u16>) -> SdkResult<u16> {
allocate_port_with_protocol(range, Protocol::Tcp).map(|ap| ap.port)
}
pub fn allocate_specific_port(port: u16) -> SdkResult<u16> {
allocate_specific_port_with_protocol(port, Protocol::Tcp).map(|ap| ap.port)
}
pub fn allocate_udp_port(range: Range<u16>) -> SdkResult<AllocatedPort> {
allocate_port_with_protocol(range, Protocol::Udp)
}
pub fn allocate_specific_udp_port(port: u16) -> SdkResult<AllocatedPort> {
allocate_specific_port_with_protocol(port, Protocol::Udp)
}
pub fn allocate_game_port(range: Range<u16>) -> SdkResult<AllocatedPort> {
allocate_port_with_protocol(range, Protocol::Both)
}
pub fn allocate_specific_game_port(port: u16) -> SdkResult<AllocatedPort> {
allocate_specific_port_with_protocol(port, Protocol::Both)
}
pub fn allocate_port_with_protocol(range: Range<u16>, protocol: Protocol) -> SdkResult<AllocatedPort> {
let mut allocated = ALLOCATED_PORTS
.lock()
.map_err(|_| SdkError::port("Failed to lock port allocator"))?;
for port in range {
if allocated.contains_key(&port) {
continue;
}
if try_bind_port(port, protocol)? {
allocated.insert(port, protocol);
info!(port = port, protocol = ?protocol, "Port allocated");
return Ok(AllocatedPort::new(port, protocol));
} else {
debug!(port = port, "Port in use, trying next");
}
}
Err(SdkError::port("No available ports in range"))
}
pub fn allocate_specific_port_with_protocol(port: u16, protocol: Protocol) -> SdkResult<AllocatedPort> {
let mut allocated = ALLOCATED_PORTS
.lock()
.map_err(|_| SdkError::port("Failed to lock port allocator"))?;
if allocated.contains_key(&port) {
return Err(SdkError::port(format!("Port {} already allocated", port)));
}
if try_bind_port(port, protocol)? {
allocated.insert(port, protocol);
info!(port = port, protocol = ?protocol, "Specific port allocated");
Ok(AllocatedPort::new(port, protocol))
} else {
Err(SdkError::port(format!("Port {} unavailable", port)))
}
}
fn try_bind_port(port: u16, protocol: Protocol) -> SdkResult<bool> {
let addr: SocketAddr = ([0, 0, 0, 0], port).into();
match protocol {
Protocol::Tcp => {
match TcpListener::bind(addr) {
Ok(_listener) => Ok(true),
Err(_) => Ok(false),
}
}
Protocol::Udp => {
match UdpSocket::bind(addr) {
Ok(_socket) => Ok(true),
Err(_) => Ok(false),
}
}
Protocol::Both => {
let tcp_ok = TcpListener::bind(addr).is_ok();
let udp_ok = UdpSocket::bind(addr).is_ok();
Ok(tcp_ok && udp_ok)
}
}
}
pub fn release_port(port: u16) -> SdkResult<()> {
let mut allocated = ALLOCATED_PORTS
.lock()
.map_err(|_| SdkError::port("Failed to lock port allocator"))?;
if allocated.remove(&port).is_some() {
info!(port = port, "Port released");
} else {
debug!(port = port, "Port was not allocated");
}
Ok(())
}
pub fn allocated_ports() -> Vec<u16> {
match ALLOCATED_PORTS.lock() {
Ok(guard) => guard.keys().copied().collect(),
Err(_) => Vec::new(),
}
}
pub fn allocated_ports_detailed() -> Vec<AllocatedPort> {
match ALLOCATED_PORTS.lock() {
Ok(guard) => guard.iter().map(|(&port, &protocol)| AllocatedPort::new(port, protocol)).collect(),
Err(_) => Vec::new(),
}
}
pub fn is_port_available(port: u16) -> bool {
is_port_available_for_protocol(port, Protocol::Tcp)
}
pub fn is_udp_port_available(port: u16) -> bool {
is_port_available_for_protocol(port, Protocol::Udp)
}
pub fn is_port_available_for_protocol(port: u16, protocol: Protocol) -> bool {
try_bind_port(port, protocol).unwrap_or(false)
}
pub struct PortAllocator {
range: Range<u16>,
protocol: Protocol,
allocated: HashMap<u16, Protocol>,
}
impl PortAllocator {
pub fn new(range: Range<u16>) -> Self {
Self {
range,
protocol: Protocol::Tcp,
allocated: HashMap::new(),
}
}
pub fn udp(range: Range<u16>) -> Self {
Self {
range,
protocol: Protocol::Udp,
allocated: HashMap::new(),
}
}
pub fn game(range: Range<u16>) -> Self {
Self {
range,
protocol: Protocol::Both,
allocated: HashMap::new(),
}
}
pub fn allocate(&mut self) -> SdkResult<AllocatedPort> {
for port in self.range.clone() {
if self.allocated.contains_key(&port) {
continue;
}
if is_port_available_for_protocol(port, self.protocol) {
self.allocated.insert(port, self.protocol);
return Ok(AllocatedPort::new(port, self.protocol));
}
}
Err(SdkError::port("No available ports in range"))
}
pub fn release(&mut self, port: u16) {
self.allocated.remove(&port);
}
pub fn allocated_count(&self) -> usize {
self.allocated.len()
}
pub fn allocated(&self) -> Vec<AllocatedPort> {
self.allocated.iter().map(|(&port, &protocol)| AllocatedPort::new(port, protocol)).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tcp_port_allocation() {
let port = allocate_port(49152..49200).unwrap();
assert!(port >= 49152 && port < 49200);
release_port(port).unwrap();
}
#[test]
fn test_udp_port_allocation() {
let allocated = allocate_udp_port(49200..49250).unwrap();
assert!(allocated.port >= 49200 && allocated.port < 49250);
assert_eq!(allocated.protocol, Protocol::Udp);
release_port(allocated.port).unwrap();
}
#[test]
fn test_game_port_allocation() {
let allocated = allocate_game_port(49250..49300).unwrap();
assert!(allocated.port >= 49250 && allocated.port < 49300);
assert_eq!(allocated.protocol, Protocol::Both);
release_port(allocated.port).unwrap();
}
#[test]
fn test_port_allocator_game() {
let mut allocator = PortAllocator::game(49300..49350);
let port1 = allocator.allocate().unwrap();
let port2 = allocator.allocate().unwrap();
assert_ne!(port1.port, port2.port);
assert_eq!(allocator.allocated_count(), 2);
allocator.release(port1.port);
assert_eq!(allocator.allocated_count(), 1);
}
}