#![deny(missing_docs)]
#![forbid(unsafe_code)]
#[macro_use]
extern crate failure;
#[macro_use]
extern crate log;
#[macro_use]
extern crate serde_derive;
use std::collections::BTreeMap;
use std::fs::File;
use std::io::prelude::*;
use std::io::BufReader;
use std::path::Path;
use byteorder::{NetworkEndian, ReadBytesExt};
use eui48::MacAddress;
use failure::{Error, ResultExt};
use regex::Regex;
type OuiMap = BTreeMap<(u64, u64), OuiEntry>;
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct OuiEntry {
pub name_short: String,
pub name_long: Option<String>,
pub comment: Option<String>,
}
impl Default for OuiEntry {
fn default() -> OuiEntry {
OuiEntry {
name_short: String::new(),
name_long: None,
comment: None,
}
}
}
pub struct OuiDatabase {
database: OuiMap,
}
impl OuiDatabase {
pub fn new_from_file<P: AsRef<Path>>(dbfile: P) -> Result<OuiDatabase, Error> {
let db = create_oui_db_from_file(dbfile.as_ref())?;
info!("Created a new OUI Vendor database from file {:?}", dbfile.as_ref());
Ok(OuiDatabase { database: db })
}
pub fn new_from_str(dbstr: &str) -> Result<OuiDatabase, Error> {
let db = create_oui_db_from_str(dbstr)?;
info!("Created a new OUI Vendor database from string");
Ok(OuiDatabase { database: db })
}
pub fn new_from_export(data: &[u8]) -> Result<OuiDatabase, Error> {
let deserialized = bincode::deserialize(data).context("could not deserialize data")?;
info!("Created a new OUI Vendor database from previously exported data");
Ok(OuiDatabase {
database: deserialized,
})
}
pub fn export(&self) -> Result<Vec<u8>, Error> {
let data = bincode::serialize(&self.database).context("could not serialize database")?;
info!("Created a dump of the OUI Vendor database for export");
Ok(data)
}
pub fn query_by_mac(&self, mac_addr: &MacAddress) -> Result<Option<OuiEntry>, Error> {
let mac_int = mac_to_u64(mac_addr)?;
debug!(
"Querying OUI Vendor database for {:?} ({})",
mac_addr, mac_int
);
self.query(&mac_int)
}
pub fn query_by_str(&self, mac_str: &str) -> Result<Option<OuiEntry>, Error> {
let mac_addr = MacAddress::parse_str(&mac_str)
.context(format!("could not parse MAC address from str: {}", mac_str))?;
self.query_by_mac(&mac_addr)
}
pub fn len(&self) -> usize {
self.database.len()
}
pub fn is_empty(&self) -> bool {
self.database.is_empty()
}
fn query(&self, query: &u64) -> Result<Option<OuiEntry>, Error> {
let mut results = Vec::<((u64, u64), OuiEntry)>::new();
for ((lo, hi), value) in &self.database {
if query >= lo && query <= hi {
results.push(((*lo, *hi), value.clone()));
}
}
if results.len() > 2 {
return Err(format_err!(
"more than two oui matches - possible database error? {:?}",
results
));
}
match results.pop() {
Some(oui_res) => Ok(Some(oui_res.1)),
_ => Ok(None),
}
}
}
fn mac_to_u64(mac: &MacAddress) -> Result<u64, Error> {
let mac_bytes = mac.as_bytes();
let padded = vec![
0,
0,
mac_bytes[0],
mac_bytes[1],
mac_bytes[2],
mac_bytes[3],
mac_bytes[4],
mac_bytes[5],
];
let mut padded_mac = &padded[..8];
let mac_num = padded_mac.read_u64::<NetworkEndian>().context(format!(
"could not read_u64 from padded MAC byte array: {:?}",
padded_mac
))?;
Ok(mac_num)
}
fn create_oui_db_from_file<P: AsRef<Path>>(dbfile: P) -> Result<OuiMap, Error> {
let file = File::open(dbfile.as_ref()).context(format!("could not open database file: {:?}", dbfile.as_ref()))?;
let re = Regex::new("[\t]+").context("could not compile regex")?;
let mut vendor_data = OuiMap::new();
for line in BufReader::new(file).lines() {
parse_entry(&line.context("could not get data line")?, &re, &mut vendor_data)?
}
Ok(vendor_data)
}
fn create_oui_db_from_str(dbstring: &str) -> Result<OuiMap, Error> {
let re = Regex::new("[\t]+").context("could not compile regex")?;
let mut vendor_data = OuiMap::new();
for entry in dbstring.lines() {
parse_entry(entry, &re, &mut vendor_data)?
}
Ok(vendor_data)
}
#[inline(always)]
fn parse_entry(entry: &str, re: &Regex, vendor_data: &mut OuiMap) -> Result<(), Error> {
if !(entry.starts_with('#') || entry.is_empty()) {
let data = re.replace_all(&entry, "|");
let fields_raw: Vec<&str> = data.split('|').collect();
let fields_cleaned: Vec<_> = fields_raw
.into_iter()
.map(|field| {
let f = field.replace('#', "");
f.trim().to_owned()
})
.collect();
if !(fields_cleaned.len() >= 2 && fields_cleaned.len() <= 4) {
return Err(format_err!(
"unexpected number of fields extracted: {:?}",
fields_cleaned
));
}
let mask: u8;
let oui_and_mask: Vec<_> = fields_cleaned[0].split('/').collect();
match oui_and_mask.len() {
1 => mask = 24,
2 => {
mask = u8::from_str_radix(&oui_and_mask[1], 10)
.context(format!("could not parse mask: {}", &oui_and_mask[1]))?;
if !(mask >= 8 && mask <= 48) {
return Err(format_err!("incorrect mask value: {}", mask));
}
}
_ => {
return Err(format_err!(
"invalid number of mask separators: {:?}",
oui_and_mask
))
}
};
let oui = oui_and_mask[0]
.to_owned()
.to_uppercase()
.replace(":", "")
.replace("-", "")
.replace(".", "");
let oui_int = u64::from_str_radix(&oui, 16)
.context(format!("could not parse stripped OUI: {}", oui))?;
let oui_start: u64;
if mask == 24 {
oui_start = oui_int << 24;
} else {
oui_start = oui_int
};
let oui_end: u64 = oui_start | 0xFFFF_FFFF_FFFF >> mask;
let comment: Option<String>;
if fields_cleaned.len() == 4 {
comment = Some(fields_cleaned[3].to_owned())
} else {
comment = None
}
let name_long: Option<String>;
if fields_cleaned.len() >= 3 {
name_long = Some(fields_cleaned[2].to_owned())
} else {
name_long = None
}
let name_short: String = fields_cleaned[1].to_owned();
let entry_data = OuiEntry {
name_short,
name_long,
comment,
};
trace!(
"Inserting entry for vendor: Range {}-{} is {:?}",
oui_start,
oui_end,
entry_data
);
vendor_data.insert((oui_start, oui_end), entry_data);
};
Ok(())
}