//! The **one** prefix-containment primitive in this constellation.
//!
//! # Why a range needs its own containment test, and why it lives here
//!
//! Every fact this crate ships about Mullvad is a *range*: the addresses their
//! device registration assigns, the resolvers they run, the ports their relays
//! listen on. A range with no containment test is a comment. So the crate that
//! ships the ranges ships the test for them, and there is exactly one copy of
//! the bit arithmetic.
//!
//! What deliberately does **not** live here is the parse of a `10.64.0.0/10`
//! *string* out of a wg-quick config file. That is config hygiene, it applies
//! to every provider and to plain WireGuard, and it belongs to whoever owns the
//! config surface (in this constellation, tunnr's `wg-appliance` payload).
//! That parser delegates the bit comparison to [`prefix_eq`] rather than
//! keeping a second copy — one writer of the arithmetic, one writer of the
//! parse.
//!
//! # The bug this primitive exists to not have
//!
//! `10.64.0.0/10` is not byte-aligned. A containment check written as "does the
//! address start with `10.64`" accepts `10.64.*` and rejects `10.67.114.172` —
//! an address Mullvad's own registration hands out. A check written as "does it
//! start with `10.`" accepts `10.200.0.1`, which Mullvad never issues. Both
//! look right in a test that only ever tries `10.64.0.1`, which is why the
//! tests below drive **both sides of every boundary**.
/// Do `a` and `b` agree on their first `bits` bits?
///
/// Byte-wise on raw octets, so it works for a v4 `[u8; 4]` and a v6 `[u8; 16]`
/// without a second implementation and without pulling in an IP-network crate.
///
/// `bits` beyond `8 * a.len()` is a caller error and returns `false` rather
/// than panicking or wrapping: a nonsense prefix must not be read as "matches
/// everything".
pub fn prefix_eq(a: &[u8], b: &[u8], bits: u32) -> bool {
if a.len() != b.len() || bits as usize > a.len() * 8 {
return false;
}
let whole = (bits / 8) as usize;
if a[..whole] != b[..whole] {
return false;
}
let rest = bits % 8;
if rest == 0 {
return true;
}
// `rest` is 1..=7 here, so the shift is in range and the mask is never 0.
let mask = 0xffu8 << (8 - rest);
a[whole] & mask == b[whole] & mask
}
/// An IPv4 network: a base address and a prefix length.
///
/// A tuple would have done, and did in the first version of this code, which is
/// exactly why it is a type now: `(Ipv4Addr, u32)` and `(Ipv4Addr, u8)` were
/// both in the tree and nothing stopped a caller passing the prefix where the
/// address went.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct V4Net {
/// The base address. Host bits below `prefix` are ignored, not asserted to
/// be zero — `10.64.0.1/10` and `10.64.0.0/10` describe the same set and a
/// constant written the first way must not silently mean something else.
pub addr: std::net::Ipv4Addr,
/// The prefix length in bits, 0..=32.
pub prefix: u32,
}
impl V4Net {
/// A network, written the way a constant reads.
pub const fn new(a: u8, b: u8, c: u8, d: u8, prefix: u32) -> V4Net {
V4Net {
addr: std::net::Ipv4Addr::new(a, b, c, d),
prefix,
}
}
/// Does this network contain `ip`?
///
/// An IPv6 address is never contained in a v4 network. That is a real
/// answer and not an error: a v4 prefix genuinely covers no v6 address.
pub fn contains(&self, ip: std::net::IpAddr) -> bool {
match ip {
std::net::IpAddr::V4(v4) => {
prefix_eq(&self.addr.octets(), &v4.octets(), self.prefix)
}
std::net::IpAddr::V6(_) => false,
}
}
}
impl std::fmt::Display for V4Net {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}/{}", self.addr, self.prefix)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::IpAddr;
fn ip(s: &str) -> IpAddr {
s.parse().expect("test address parses")
}
/// RED-WHEN-BROKEN — the non-byte-aligned prefix, driven on **both** sides
/// of every boundary. A whole-octet comparison passes the first two of
/// these and fails the rest.
#[test]
fn a_sub_octet_prefix_is_exact_on_both_sides_of_its_boundary() {
let ten_64 = V4Net::new(10, 64, 0, 0, 10);
// Inside: the first address, a real Mullvad-issued one, and the last.
assert!(ten_64.contains(ip("10.64.0.0")));
assert!(ten_64.contains(ip("10.64.0.1")));
assert!(ten_64.contains(ip("10.67.114.172")), "a real registration");
assert!(ten_64.contains(ip("10.127.255.255")), "the last address");
// Outside, by ONE on each side. These are the two assertions a
// `starts_with("10.64")` or `starts_with("10.")` check gets wrong.
assert!(!ten_64.contains(ip("10.63.255.255")));
assert!(!ten_64.contains(ip("10.128.0.0")));
assert!(!ten_64.contains(ip("10.200.0.1")));
assert!(!ten_64.contains(ip("11.64.0.1")));
let hundred_64 = V4Net::new(100, 64, 0, 0, 10);
assert!(hundred_64.contains(ip("100.64.0.7")));
assert!(hundred_64.contains(ip("100.127.255.255")));
assert!(!hundred_64.contains(ip("100.63.255.255")));
assert!(!hundred_64.contains(ip("100.128.0.1")));
// The two /10s are genuinely different networks — a test that only
// ever used one could not tell them apart.
assert!(!ten_64.contains(ip("100.64.0.7")));
assert!(!hundred_64.contains(ip("10.64.0.1")));
}
#[test]
fn a_v4_network_covers_no_v6_address_and_that_is_an_answer() {
assert!(!V4Net::new(0, 0, 0, 0, 0).contains(ip("::1")));
assert!(!V4Net::new(0, 0, 0, 0, 0).contains(ip("2606:4700::1111")));
// …while a v4 /0 does cover every v4 address.
assert!(V4Net::new(0, 0, 0, 0, 0).contains(ip("8.8.8.8")));
assert!(V4Net::new(0, 0, 0, 0, 0).contains(ip("10.64.0.1")));
}
#[test]
fn a_thirty_two_covers_itself_and_nothing_next_to_it() {
let one = V4Net::new(10, 64, 0, 1, 32);
assert!(one.contains(ip("10.64.0.1")));
assert!(!one.contains(ip("10.64.0.0")));
assert!(!one.contains(ip("10.64.0.2")));
}
/// RED-WHEN-BROKEN — host bits in the base address must be ignored, or a
/// constant written `10.64.0.1/10` (which is how the gateway is spelled in
/// Mullvad's own relay list) would describe a different set from
/// `10.64.0.0/10`.
#[test]
fn host_bits_in_the_base_address_do_not_change_the_set() {
let written_sloppily = V4Net::new(10, 67, 114, 172, 10);
let written_cleanly = V4Net::new(10, 64, 0, 0, 10);
for probe in ["10.64.0.1", "10.127.255.255", "10.128.0.0", "10.63.0.0"] {
assert_eq!(
written_sloppily.contains(ip(probe)),
written_cleanly.contains(ip(probe)),
"{probe} must be judged the same either way"
);
}
}
/// RED-WHEN-BROKEN — a nonsense prefix must not read as "matches
/// everything". A `<<` with an out-of-range shift is a panic in debug and
/// nonsense in release; the guard is checked, not assumed.
#[test]
fn an_impossible_prefix_matches_nothing_rather_than_everything() {
assert!(!prefix_eq(&[10, 64, 0, 0], &[10, 64, 0, 0], 33));
assert!(!prefix_eq(&[10, 64, 0, 0], &[10, 64, 0, 0], 999));
// …and length mismatch (a v4 octet array against a v6 one) is false,
// never a slice-index panic.
assert!(!prefix_eq(&[10, 64, 0, 0], &[0u8; 16], 8));
// The identity case still works, or every assertion above is vacuous.
assert!(prefix_eq(&[10, 64, 0, 0], &[10, 64, 0, 0], 32));
assert!(prefix_eq(&[10, 64, 0, 0], &[10, 99, 0, 0], 8));
assert!(!prefix_eq(&[10, 64, 0, 0], &[10, 99, 0, 0], 16));
}
#[test]
fn a_zero_prefix_covers_the_whole_family() {
assert!(prefix_eq(&[1, 2, 3, 4], &[255, 255, 255, 255], 0));
assert!(prefix_eq(&[0u8; 16], &[0xffu8; 16], 0));
}
#[test]
fn display_reads_the_way_the_range_is_written_down() {
assert_eq!(V4Net::new(10, 64, 0, 0, 10).to_string(), "10.64.0.0/10");
assert_eq!(V4Net::new(100, 64, 0, 0, 10).to_string(), "100.64.0.0/10");
}
}