feat(server): add bridge forwarding mode and per-client destination policy overrides

This commit is contained in:
2026-03-31 21:34:49 +00:00
parent 17af7ab289
commit fdeba5eeb5
12 changed files with 583 additions and 25 deletions

View File

@@ -13,6 +13,10 @@ pub struct IpPool {
allocated: HashMap<Ipv4Addr, String>,
/// Next candidate offset (skipping .0 network and .1 gateway)
next_offset: u32,
/// Minimum allocation offset (inclusive). Default: 2 (skip .0 network and .1 gateway).
min_offset: u32,
/// Maximum allocation offset (exclusive). Default: broadcast offset.
max_offset: u32,
}
impl IpPool {
@@ -28,11 +32,47 @@ impl IpPool {
anyhow::bail!("Prefix too long for VPN pool: /{}", prefix_len);
}
let host_bits = 32 - prefix_len as u32;
let max_offset = (1u32 << host_bits) - 1; // broadcast offset
Ok(Self {
network,
prefix_len,
allocated: HashMap::new(),
next_offset: 2, // Skip .0 (network) and .1 (server/gateway)
min_offset: 2,
max_offset,
})
}
/// Create a new IP pool with a restricted allocation range within the subnet.
/// `range_start` and `range_end` are host offsets (e.g., 200 and 250 for .200-.250).
pub fn new_with_range(subnet: &str, range_start: u32, range_end: u32) -> Result<Self> {
let parts: Vec<&str> = subnet.split('/').collect();
if parts.len() != 2 {
anyhow::bail!("Invalid subnet format: {}", subnet);
}
let network: Ipv4Addr = parts[0].parse()?;
let prefix_len: u8 = parts[1].parse()?;
if prefix_len > 30 {
anyhow::bail!("Prefix too long for VPN pool: /{}", prefix_len);
}
if range_start >= range_end {
anyhow::bail!("Invalid IP range: start ({}) must be less than end ({})", range_start, range_end);
}
let host_bits = 32 - prefix_len as u32;
let broadcast_offset = (1u32 << host_bits) - 1;
if range_end > broadcast_offset {
anyhow::bail!("IP range end ({}) exceeds subnet broadcast ({})", range_end, broadcast_offset);
}
Ok(Self {
network,
prefix_len,
allocated: HashMap::new(),
next_offset: range_start,
min_offset: range_start,
max_offset: range_end + 1, // exclusive
})
}
@@ -44,22 +84,17 @@ impl IpPool {
/// Total number of usable client addresses in the pool.
pub fn capacity(&self) -> u32 {
let host_bits = 32 - self.prefix_len as u32;
let total = 1u32 << host_bits;
total.saturating_sub(3) // minus network, gateway, broadcast
self.max_offset.saturating_sub(self.min_offset)
}
/// Allocate an IP for a client. Returns the assigned IP.
pub fn allocate(&mut self, client_id: &str) -> Result<Ipv4Addr> {
let host_bits = 32 - self.prefix_len as u32;
let max_offset = (1u32 << host_bits) - 1; // broadcast offset
// Try to find a free IP starting from next_offset
let start = self.next_offset;
let mut offset = start;
loop {
if offset >= max_offset {
offset = 2; // wrap around
if offset >= self.max_offset {
offset = self.min_offset; // wrap around
}
let ip = Ipv4Addr::from(u32::from(self.network) + offset);