├── .gitignore ├── src ├── lib.rs ├── sync │ ├── netbox │ │ ├── model.rs │ │ ├── address.rs │ │ ├── range.rs │ │ ├── prefix.rs │ │ ├── config.rs │ │ └── mod.rs │ ├── windhcp │ │ ├── error.rs │ │ ├── subnet │ │ │ ├── reservation.rs │ │ │ ├── elements.rs │ │ │ ├── options.rs │ │ │ └── mod.rs │ │ └── mod.rs │ ├── mac.rs │ ├── config.rs │ └── mod.rs ├── cli.rs ├── server │ ├── webhook.rs │ ├── signal.rs │ ├── shared.rs │ ├── interval.rs │ ├── mod.rs │ ├── service.rs │ ├── config.rs │ ├── sync.rs │ └── web.rs ├── bin │ ├── netbox-windhcp-server.rs │ ├── netbox-windhcp-sync.rs │ └── netbox-windhcp-log.rs ├── config.rs └── logging.rs ├── .github └── workflows │ ├── rust-release.yml │ └── rust.yml ├── netbox-windhcp.cfg ├── Cargo.toml ├── README.md ├── wix ├── main.wxs └── LICENSE.rtf └── LICENSE.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.cfg -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub use config::Config; 3 | pub mod logging; 4 | pub mod server; 5 | pub mod sync; 6 | #[cfg(target_os = "windows")] 7 | pub use sync::Sync; 8 | pub mod cli; 9 | -------------------------------------------------------------------------------- /src/sync/netbox/model.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize)] 4 | pub struct Pageination { 5 | pub count: usize, 6 | pub next: Option, 7 | pub results: Vec, 8 | } 9 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::net::Ipv4Addr; 2 | 3 | use clap::Parser; 4 | 5 | /// Netbxo to Windows DHCP Syncer 6 | #[derive(Parser, Debug)] 7 | #[command(author, version, about, long_about = None)] 8 | pub struct Sync { 9 | /// Do not change anything 10 | #[arg(short, long, default_value_t = false)] 11 | pub noop: bool, 12 | #[arg(short, long)] 13 | pub scope: Option, 14 | } 15 | 16 | impl Sync { 17 | pub fn init() -> Self { Sync::parse() } 18 | } 19 | -------------------------------------------------------------------------------- /src/server/webhook.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::Deserialize; 3 | 4 | #[derive(Debug, Deserialize, PartialEq, Eq)] 5 | pub struct NetboxWebHook { 6 | pub event: NetboxWebHookEvent, 7 | pub timestamp: DateTime, 8 | pub model: String, 9 | pub username: String, 10 | pub request_id: String, 11 | pub data: serde_json::Map, 12 | } 13 | 14 | #[derive(Debug, Deserialize, PartialEq, Eq)] 15 | #[serde(rename_all = "lowercase")] 16 | pub enum NetboxWebHookEvent { 17 | Created, 18 | Updated, 19 | Deleted, 20 | } 21 | -------------------------------------------------------------------------------- /src/server/signal.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | use tokio::{signal, sync::broadcast, task::JoinHandle}; 3 | 4 | use super::shared::Message; 5 | 6 | pub fn spawn(sync_tx: &broadcast::Sender) -> JoinHandle<()> { 7 | let sync_tx = sync_tx.clone(); 8 | 9 | tokio::spawn(async move { 10 | while (signal::ctrl_c().await).is_ok() { 11 | info!("Received Ctrl+C send Shutdown message."); 12 | match sync_tx.send(Message::Shutdown) { 13 | Ok(_) => {}, 14 | Err(_) => { break; }, 15 | } 16 | } 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /src/bin/netbox-windhcp-server.rs: -------------------------------------------------------------------------------- 1 | use netbox_windhcp::{server, Config}; 2 | 3 | fn main() { 4 | let config = match Config::load_from_file() { 5 | Ok(config) => config, 6 | Err(e) => { 7 | println!("Error reading config: {}", e); 8 | return; 9 | } 10 | }; 11 | 12 | config.log.setup("server"); 13 | 14 | #[cfg(target_os = "windows")] 15 | let service = server::service::running_as_service(); 16 | #[cfg(not(target_os = "windows"))] 17 | let service = false; 18 | 19 | if service { 20 | #[cfg(target_os = "windows")] 21 | server::service::run(); 22 | } else { 23 | server::run(None); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/server/shared.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use chrono::{DateTime, Utc}; 4 | use serde::Serialize; 5 | use tokio::sync::Mutex; 6 | 7 | #[derive(Debug, Default, Serialize)] 8 | pub enum SyncStatus { 9 | #[default] 10 | Unknown, 11 | SyncOk, 12 | SyncFailed, 13 | } 14 | 15 | #[derive(Debug, Default, Serialize)] 16 | pub struct ServerStatus { 17 | pub needs_sync: bool, 18 | pub syncing: bool, 19 | pub last_sync: Option>, 20 | pub last_sync_status: SyncStatus, 21 | } 22 | 23 | impl ServerStatus { 24 | pub fn new() -> Self { Self { ..Default::default() } } 25 | } 26 | 27 | pub type SharedServerStatus = Arc>; 28 | 29 | #[derive(Debug, Clone, PartialEq, Eq)] 30 | pub enum Message { 31 | Shutdown, 32 | TriggerSync, 33 | } 34 | -------------------------------------------------------------------------------- /src/sync/windhcp/error.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt::Display}; 2 | 3 | use windows::Win32::Foundation::WIN32_ERROR; 4 | 5 | pub type WinDhcpResult<'a, T> = Result>; 6 | 7 | #[derive(Debug)] 8 | pub struct WinDhcpError { 9 | message: &'static str, 10 | error: WIN32_ERROR, 11 | } 12 | 13 | impl WinDhcpError { 14 | pub fn new(message: &'static str, error: u32) -> Box { 15 | let message = message; 16 | let error = WIN32_ERROR(error); 17 | Box::new(Self { message, error }) 18 | } 19 | } 20 | 21 | impl Error for WinDhcpError {} 22 | 23 | impl Display for WinDhcpError { 24 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 25 | write!(f, "Error {}: {}", self.message, ::windows::core::Error::from(self.error).message()) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/rust-release.yml: -------------------------------------------------------------------------------- 1 | name: Rust-Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: windows-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | - name: Rust Cache 21 | uses: Swatinem/rust-cache@v2 22 | - name: Build 23 | run: cargo build --verbose 24 | - name: Install Cargo Wix 25 | run: cargo install --verbose cargo-wix 26 | - name: Build MSI 27 | run: cargo wix --verbose --dbg-build 28 | 29 | - name: Create Release 30 | uses: softprops/action-gh-release@v1 31 | if: startsWith(github.ref, 'refs/tags/') 32 | with: 33 | files: | 34 | LICENSE.md 35 | target/release/*.exe 36 | target/wix/*.msi -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: windows-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 21 | - name: Rust Cache 22 | uses: Swatinem/rust-cache@v2 23 | - name: Build 24 | run: cargo build --verbose 25 | - name: Run tests 26 | run: cargo test --verbose 27 | - uses: actions/upload-artifact@v4 28 | with: 29 | name: binaries 30 | path: target\\debug\\*.exe 31 | - name: Install Cargo Wix 32 | run: cargo install --verbose cargo-wix 33 | - name: Build MSI 34 | run: cargo wix --verbose --dbg-build 35 | - uses: actions/upload-artifact@v4 36 | with: 37 | name: msi 38 | path: target\\wix\\*.msi 39 | -------------------------------------------------------------------------------- /netbox-windhcp.cfg: -------------------------------------------------------------------------------- 1 | --- 2 | webhook: 3 | listen: 127.0.0.1:6969 4 | #sync_interval: 900 5 | #sync_standoff_time: 5 6 | #sync_timeout: 30 7 | #secret: SECRET 8 | sync: 9 | logs: 10 | #dir: C:\path\of\dhcp\audit\logs 11 | dhcp: 12 | server: localhost 13 | #default_dns_flags: 14 | # enabled: true 15 | # cleanup_expired: true 16 | # update_dhcid: true 17 | #default_dns_servers: 18 | # - 8.8.8.8 19 | #default_dns_domain: example.com 20 | #default_failover_relation: DHCPFailover 21 | netbox: 22 | apiurl: https://netbox.example.com/api/ 23 | token: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 24 | #prefix_filter: 25 | # tag: dhcp 26 | #range_filter: 27 | # role: dhcp-pool 28 | #reservation_filter: 29 | # tag: dhcp 30 | #router_filter: 31 | # description: Gateway 32 | log: 33 | level: Info -------------------------------------------------------------------------------- /src/bin/netbox-windhcp-sync.rs: -------------------------------------------------------------------------------- 1 | use log::error; 2 | use netbox_windhcp::Config; 3 | #[cfg(target_os = "windows")] 4 | use netbox_windhcp::{cli, Sync}; 5 | 6 | fn main() { 7 | let config = match Config::load_from_file() { 8 | Ok(config) => config, 9 | Err(e) => { 10 | println!("Error reading config: {}", e); 11 | std::process::exit(exitcode::CONFIG); 12 | } 13 | }; 14 | 15 | config.log.setup("sync"); 16 | 17 | #[cfg(target_os = "windows")] 18 | let cli_args = cli::Sync::init(); 19 | 20 | #[cfg(target_os = "windows")] 21 | match Sync::new(config.sync, cli_args.noop, cli_args.scope).run() { 22 | Ok(_) => std::process::exit(exitcode::OK), 23 | Err(e) => { 24 | error!("{}", e); 25 | std::process::exit(exitcode::DATAERR); 26 | } 27 | } 28 | 29 | #[cfg(not(target_os = "windows"))] 30 | { 31 | error!("Only works on Windows"); 32 | std::process::exit(exitcode::DATAERR); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fs::File; 3 | 4 | use serde::Deserialize; 5 | 6 | use crate::logging::LogConfig; 7 | 8 | use super::server::config::WebhookConfig; 9 | #[cfg(target_os = "windows")] 10 | use super::sync::config::SyncConfig; 11 | 12 | #[derive(Debug, Deserialize)] 13 | pub struct Config { 14 | pub webhook: WebhookConfig, 15 | #[cfg(target_os = "windows")] 16 | pub sync: SyncConfig, 17 | #[serde(default)] 18 | pub log: LogConfig, 19 | } 20 | 21 | impl Config { 22 | const CONFIG_FILE: &'static str = concat!( 23 | "C:\\ProgramData\\", 24 | env!("CARGO_PKG_NAME"), 25 | "\\", 26 | env!("CARGO_PKG_NAME"), 27 | ".cfg" 28 | ); 29 | const CONFIG_FILE_LOCAL: &'static str = concat!("./", env!("CARGO_PKG_NAME"), ".cfg"); 30 | 31 | pub fn load_from_file() -> Result> { 32 | let file = File::open(Self::CONFIG_FILE).or(File::open(Self::CONFIG_FILE_LOCAL))?; 33 | 34 | Ok(serde_yaml_ng::from_reader::(file)?) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "netbox-windhcp" 3 | version = "0.10.0" 4 | edition = "2021" 5 | 6 | [features] 7 | rpc_free = [] 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | bytes = "1" 13 | chrono = { version = "0.4", features = ["serde"] } 14 | clap = { version = "4", features = ["derive"] } 15 | exitcode = "1.1.2" 16 | git-version = "0.3" 17 | glob = "0.3.1" 18 | hmac = "0.12" 19 | hyper = "1" 20 | ipnet = { version = "2", features = ["serde"] } 21 | log = { version = "0.4", features = ["serde"] } 22 | log4rs = { version = "1.3", features = ["rolling_file_appender", "compound_policy", "fixed_window_roller", "size_trigger"] } 23 | num = "0.4" 24 | regex = "1" 25 | serde = { version = "1.0", features = ["derive"] } 26 | serde_json = "1.0" 27 | serde_yaml_ng = "0.10" 28 | sha2 = "0.10" 29 | tokio = { version = "1", features = ["rt", "macros", "rt-multi-thread", "signal", "process", "test-util"] } 30 | ureq = { version = "3", features = ["json", "platform-verifier"] } 31 | warp = { version = "0.3", features = ["tokio-rustls", "tls"] } 32 | windows = { version = "0", features = ["Win32_System_Console", "Win32_Foundation", "Win32_NetworkManagement_Dhcp"] } 33 | windows-service = "0.6" 34 | 35 | [dev-dependencies] 36 | libc = "0.2.140" 37 | -------------------------------------------------------------------------------- /src/sync/mac.rs: -------------------------------------------------------------------------------- 1 | pub trait MacAddr { 2 | fn as_mac(&self) -> String; 3 | fn from_mac(mac: &str) -> Vec; 4 | } 5 | impl MacAddr for Vec { 6 | fn as_mac(&self) -> String { 7 | self.iter() 8 | .map(|d| format!("{:02X}", d)).collect::>() 9 | .join(":") 10 | } 11 | 12 | fn from_mac(mac: &str) -> Vec { 13 | regex::Regex::new(r"[0-9A-Fa-f]{2}").unwrap().captures_iter(mac) 14 | .map(|h| T::from_str_radix(&h[0], 16).unwrap_or_default()) 15 | .collect::>() 16 | } 17 | } 18 | 19 | #[cfg(test)] 20 | mod tests { 21 | use super::*; 22 | 23 | #[test] 24 | fn it_parses_mac() { 25 | let mac = Vec::::from_mac("00:aa:bB:CC"); 26 | assert_eq!(mac, vec!(0x00, 0xaa, 0xbb, 0xcc)) 27 | } 28 | 29 | #[test] 30 | fn it_parses_mac_w_colon() { 31 | let mac = Vec::::from_mac("00:11:22:33"); 32 | assert_eq!(mac, vec!(0x00, 0x11, 0x22, 0x33)) 33 | } 34 | 35 | #[test] 36 | fn it_parses_mac_w_dash() { 37 | let mac = Vec::::from_mac("00-11-2233"); 38 | assert_eq!(mac, vec!(0x00, 0x11, 0x22, 0x33)) 39 | } 40 | 41 | #[test] 42 | fn it_parses_mac_wo_separation() { 43 | let mac = Vec::::from_mac("00112233"); 44 | assert_eq!(mac, vec!(0x00, 0x11, 0x22, 0x33)) 45 | } 46 | 47 | #[test] 48 | fn it_builds_mac() { 49 | let mac = vec![0x22, 0x33]; 50 | assert_eq!(mac.as_mac(), "22:33") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/server/interval.rs: -------------------------------------------------------------------------------- 1 | use chrono::Utc; 2 | use log::{debug, error, info}; 3 | use tokio::{sync::broadcast, task::JoinHandle, time::sleep}; 4 | 5 | use super::{ 6 | config::WebhookConfig, 7 | shared::{Message, SharedServerStatus}, 8 | }; 9 | 10 | pub fn spawn( 11 | config: &WebhookConfig, 12 | status: &SharedServerStatus, 13 | sync_tx: &broadcast::Sender, 14 | ) -> JoinHandle<()> { 15 | let status = status.clone(); 16 | let sync_tx = sync_tx.clone(); 17 | let interval = config.sync_interval(); 18 | 19 | tokio::spawn(async move { 20 | loop { 21 | let last_sync = { 22 | status.lock().await.last_sync 23 | }.unwrap_or(Utc::now() - interval); 24 | 25 | let next_sync = last_sync + interval; 26 | 27 | if next_sync <= Utc::now() { 28 | { 29 | let mut status = status.lock().await; 30 | status.needs_sync = true; 31 | } 32 | match sync_tx.send(Message::TriggerSync) { 33 | Ok(_) => info!("Intervall Sync triggerd"), 34 | Err(e) => error!("Triggering Intervall Sync: {:?}", e), 35 | } 36 | 37 | sleep(interval.to_std().unwrap()).await; 38 | 39 | continue; 40 | } 41 | 42 | let delta = next_sync.signed_duration_since(Utc::now()); 43 | debug!("Wait for next Intervall Sync {}", delta); 44 | 45 | sleep(delta.to_std().unwrap()).await 46 | } 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /src/sync/windhcp/subnet/reservation.rs: -------------------------------------------------------------------------------- 1 | use std::{net::Ipv4Addr}; 2 | 3 | use windows::Win32::NetworkManagement::Dhcp::DHCP_IP_RESERVATION_V4; 4 | 5 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 6 | #[repr(u8)] 7 | pub enum ReservationClientTypes { 8 | Dhcp = 1, 9 | Bootp = 2, 10 | Both = 3 11 | } 12 | 13 | impl From for ReservationClientTypes { 14 | fn from(value: u8) -> Self { 15 | match value { 16 | 1 => ReservationClientTypes::Dhcp, 17 | 2 => ReservationClientTypes::Bootp, 18 | _n => ReservationClientTypes::Both, 19 | } 20 | } 21 | } 22 | 23 | impl From for u8 { 24 | fn from(value: ReservationClientTypes) -> Self { 25 | match value { 26 | ReservationClientTypes::Dhcp => 1, 27 | ReservationClientTypes::Bootp => 2, 28 | ReservationClientTypes::Both => 3, 29 | } 30 | } 31 | } 32 | 33 | #[derive(Debug, PartialEq, Eq)] 34 | pub struct Reservation { 35 | pub ip_address: Ipv4Addr, 36 | pub for_client: Vec, 37 | pub allowed_client_types: ReservationClientTypes, 38 | } 39 | 40 | impl From for Reservation { 41 | fn from(value: DHCP_IP_RESERVATION_V4) -> Self { 42 | let len: usize = (unsafe{(*value.ReservedForClient).DataLength}-5).try_into().unwrap(); 43 | Reservation { 44 | ip_address: Ipv4Addr::from(value.ReservedIpAddress), 45 | for_client: { 46 | let mut for_client = Vec::with_capacity(len); 47 | for idx in 0..len { 48 | for_client.insert(idx, unsafe{*(*value.ReservedForClient).Data.offset((5+idx).try_into().unwrap())}) 49 | } 50 | for_client 51 | }, 52 | allowed_client_types: value.bAllowedClientTypes.into(), 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/server/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | mod interval; 3 | #[cfg(target_os = "windows")] 4 | pub mod service; 5 | mod shared; 6 | mod signal; 7 | mod sync; 8 | mod web; 9 | mod webhook; 10 | 11 | use log::debug; 12 | use std::sync::{mpsc as std_mpsc, Arc}; 13 | use tokio::sync::{broadcast, Mutex}; 14 | 15 | use crate::{server::shared::ServerStatus, Config}; 16 | 17 | use self::shared::{Message, SharedServerStatus}; 18 | 19 | pub fn run(shutdown_rx: Option>) { 20 | let config = match Config::load_from_file() { 21 | Ok(config) => config.webhook, 22 | Err(e) => { 23 | println!("Error reading config: {}", e); 24 | return; 25 | } 26 | }; 27 | 28 | tokio::runtime::Builder::new_multi_thread() 29 | .enable_all() 30 | .build() 31 | .unwrap() 32 | .block_on(async { 33 | let status: SharedServerStatus = Arc::new(Mutex::new(ServerStatus::new())); 34 | let (message_tx, message_rx) = broadcast::channel(16); 35 | 36 | let _interval_handle = self::interval::spawn(&config, &status, &message_tx); 37 | let _signal_handle = self::signal::spawn(&message_tx); 38 | 39 | if let Some(shutdown_rx) = shutdown_rx { 40 | let message_tx = message_tx.clone(); 41 | debug!("Start Proxy"); 42 | tokio::spawn(async move { 43 | while let Ok(msg) = shutdown_rx.recv() { 44 | debug!("Channel proxy got: {:?}", &msg); 45 | message_tx.send(msg).unwrap(); 46 | } 47 | }); 48 | debug!("Start Proxy Done"); 49 | } 50 | 51 | tokio::join!( 52 | self::web::server(&config, &status, &message_tx), 53 | self::sync::worker(&config, &status, &message_tx, message_rx) 54 | ); 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /src/sync/netbox/address.rs: -------------------------------------------------------------------------------- 1 | use std::net::Ipv4Addr; 2 | 3 | use chrono::NaiveDate; 4 | use ipnet::Ipv4Net; 5 | use serde::Deserialize; 6 | 7 | #[derive(Debug, Deserialize)] 8 | pub struct IpAddress { 9 | url: String, 10 | address: Ipv4Net, 11 | dns_name: String, 12 | description: String, 13 | custom_fields: IpAddressCustomField, 14 | assigned_object: Option, 15 | } 16 | 17 | #[derive(Debug, Deserialize)] 18 | struct IpAddressCustomField { 19 | dhcp_reservation_mac: Option, 20 | dhcp_reservation_last_active: Option, 21 | } 22 | 23 | #[derive(Debug, Deserialize)] 24 | struct IpAddressAssignedObject { 25 | url: Option, 26 | } 27 | 28 | impl IpAddress { 29 | pub fn url(&self) -> &str { 30 | self.url.as_ref() 31 | } 32 | 33 | pub fn address(&self) -> Ipv4Addr { 34 | self.address.addr() 35 | } 36 | 37 | pub fn dns_name(&self) -> &str { 38 | self.dns_name.as_ref() 39 | } 40 | 41 | pub fn description(&self) -> &str { 42 | self.description.as_ref() 43 | } 44 | 45 | pub fn reservation_mac(&self) -> Option<&String> { 46 | self.custom_fields.dhcp_reservation_mac.as_ref() 47 | } 48 | 49 | pub fn dhcp_reservation_last_active(&self) -> Option { 50 | self.custom_fields.dhcp_reservation_last_active 51 | } 52 | 53 | pub fn assigned_object_url(&self) -> Option<&String> { 54 | match &self.assigned_object { 55 | Some(ao) => match ao.url.as_ref() { 56 | Some(url) => Some(url), 57 | None => None, 58 | } 59 | None => None, 60 | } 61 | } 62 | } 63 | 64 | #[derive(Debug, Deserialize)] 65 | pub struct AssignedObject { 66 | mac_address: Option, 67 | } 68 | 69 | impl AssignedObject { 70 | pub fn mac_address(&self) -> Option<&String> { 71 | self.mac_address.as_ref() 72 | } 73 | } -------------------------------------------------------------------------------- /src/sync/netbox/range.rs: -------------------------------------------------------------------------------- 1 | use std::net::Ipv4Addr; 2 | 3 | use ipnet::Ipv4Net; 4 | use serde::Deserialize; 5 | 6 | use super::prefix::Prefix; 7 | 8 | #[derive(Debug, Deserialize)] 9 | pub struct IpRange { 10 | start_address: Ipv4Net, 11 | end_address: Ipv4Net, 12 | } 13 | 14 | impl IpRange { 15 | pub fn start_address(&self) -> Ipv4Addr { 16 | self.start_address.addr() 17 | } 18 | 19 | pub fn end_address(&self) -> Ipv4Addr { 20 | self.end_address.addr() 21 | } 22 | 23 | pub fn is_contained(&self, prefix: &Prefix) -> bool { 24 | prefix.prefix().contains(&self.start_address) && prefix.prefix().contains(&self.end_address) 25 | } 26 | } 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use super::*; 31 | 32 | #[test] 33 | fn it_parses_netbox_ip_range() { 34 | let range = serde_json::from_str::(r#"{ 35 | "start_address": "192.168.1.100/22", 36 | "end_address": "192.168.1.149/22", 37 | "description": "Future use" 38 | }"#); 39 | assert!(range.is_ok()); 40 | let range = range.unwrap(); 41 | 42 | assert_eq!(range.start_address(), "192.168.1.100".parse::().unwrap()); 43 | assert_eq!(range.end_address(), "192.168.1.149".parse::().unwrap()); 44 | } 45 | 46 | #[test] 47 | fn returns_true_if_it_is_contained() { 48 | let prefix = serde_json::from_str::(r#"{ 49 | "prefix": "10.112.130.0/24", 50 | "description": "foo", 51 | "custom_fields": {} 52 | }"#).unwrap(); 53 | let range = serde_json::from_str::(r#"{ 54 | "start_address": "10.112.130.100/24", 55 | "end_address": "10.112.130.149/24" 56 | }"#).unwrap(); 57 | 58 | assert!(range.is_contained(&prefix)); 59 | } 60 | 61 | #[test] 62 | fn returns_false_if_it_is_not_contained() { 63 | let prefix = serde_json::from_str::(r#"{ 64 | "prefix": "10.112.130.0/24", 65 | "description": "foo", 66 | "custom_fields": {} 67 | }"#).unwrap(); 68 | let range = serde_json::from_str::(r#"{ 69 | "start_address": "192.168.1.100/22", 70 | "end_address": "192.168.1.149/22" 71 | }"#).unwrap(); 72 | 73 | assert!(!range.is_contained(&prefix)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/sync/config.rs: -------------------------------------------------------------------------------- 1 | use std::net::Ipv4Addr; 2 | use std::path::PathBuf; 3 | 4 | use serde::Deserialize; 5 | 6 | use super::netbox::config::SyncNetboxConfig; 7 | 8 | use super::windhcp::DnsFlags; 9 | 10 | #[derive(Debug, Deserialize, Clone)] 11 | pub struct SyncConfig { 12 | pub netbox: SyncNetboxConfig, 13 | pub dhcp: SyncDhcpConfig, 14 | pub logs: SyncLogConfig, 15 | } 16 | 17 | impl SyncConfig { 18 | pub fn netbox(&self) -> &SyncNetboxConfig { 19 | &self.netbox 20 | } 21 | } 22 | 23 | #[derive(Debug, Deserialize, Clone)] 24 | pub struct SyncDhcpConfig { 25 | server: String, 26 | lease_duration: Option, 27 | #[serde(default)] 28 | default_dns_flags: DnsFlags, 29 | default_dns_domain: Option, 30 | #[serde(default)] 31 | default_dns_servers: Vec, 32 | default_failover_relation: Option, 33 | } 34 | 35 | impl SyncDhcpConfig { 36 | pub fn server(&self) -> &str { 37 | self.server.as_ref() 38 | } 39 | 40 | pub fn lease_duration(&self) -> u32 { 41 | self.lease_duration.unwrap_or(7 * 24 * 60 * 60) 42 | } 43 | 44 | pub fn default_dns_flags(&self) -> Option { 45 | Some(self.default_dns_flags.clone()) 46 | } 47 | 48 | pub fn default_dns_domain(&self) -> Option<&String> { 49 | self.default_dns_domain.as_ref() 50 | } 51 | 52 | pub fn default_dns_servers(&self) -> &[Ipv4Addr] { 53 | self.default_dns_servers.as_ref() 54 | } 55 | 56 | pub fn default_failover_relation(&self) -> Option<&String> { 57 | self.default_failover_relation.as_ref() 58 | } 59 | } 60 | 61 | #[derive(Debug, Deserialize, Clone)] 62 | pub struct SyncLogConfig { 63 | pub dir: Option, 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::*; 69 | 70 | #[test] 71 | fn it_parses_dhcp_config() { 72 | let cfg = serde_yaml_ng::from_str::(r#"--- 73 | server: dhcp.example.com 74 | lease_duration: 3600 75 | default_dns_flags: 76 | enabled: true 77 | cleanup_expired: true 78 | update_dhcid: true 79 | "#); 80 | assert!(cfg.is_ok()); 81 | let cfg = cfg.unwrap(); 82 | assert_eq!(cfg.server(), "dhcp.example.com"); 83 | assert_eq!(cfg.lease_duration(), 3600); 84 | } 85 | 86 | #[test] 87 | fn it_parses_dhcp_config_without_lease_duration() { 88 | let cfg = serde_yaml_ng::from_str::(r#"--- 89 | server: dhcp.example.com 90 | "#); 91 | dbg!(&cfg); 92 | assert!(cfg.is_ok()); 93 | let cfg = cfg.unwrap(); 94 | assert_eq!(cfg.server(), "dhcp.example.com"); 95 | assert_eq!(cfg.lease_duration(), 604800); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Netbox to Windows DHCP Sync 2 | 3 | Syncs IPv4 Subnet, Ranges and Reservations from [Netbox](https://github.com/netbox-community/netbox) into a Windows DHCP server. 4 | 5 | ## Sync 6 | 7 | Als Prefixes match the filter will be created as Scope on the DHCP server. Each Prefix needs a corresponding IP-Range which defines the pool. 8 | IP-Addresses matching the filter within a Prefix will be set as reservations. 9 | 10 | ## Netbox Customisation 11 | 12 | ### Per Prefix/Subnet lease duration 13 | To set the Prefix/Subnet DHCP lease duration a Integer Custom Field `dhcp_lease_duration` on the Ipam>Prefix can be added to override the default lease duration on a per Prefix/Subnet basis. 14 | 15 | ### Per Prefix/Subnet DNS Update configuration 16 | To set the Prefix/Subnet DNS settings a Multiple selection Custom Field `dhcp_dns_flags` with the choises `['enabled', 'update_downlevel', 'cleanup_expired', 'update_both_always', 'update_dhcid', 'disable_ptr_update', 'disabled']` on the Ipam>Prefix can be added to override the default on a per Prefix/Subnet basis. 17 | 18 | ### Per Prefix/Subnet DHCP Failover configuration 19 | To assign the Prefix/Subnet to a Failover relation the Custom Field `dhcp_failover_relation` as a text field in the Ipam>Prefix can be added to override the default on a per Prefix/Subnet basis. 20 | 21 | ### Reservations without Device 22 | To make a reservation without a Device to assigne the IP-Address to a Text Custom Field `dhcp_reservation_mac` can be added to provide the MAC address. 23 | 24 | ## Webhook Server 25 | 26 | The Hook Server can run as a Windows Servive to listen for WebHooks from Netbox. The Sync is started by an Intervall and on receiving Hooks. Multiple hooks in short succession will only trigger one sync. 27 | 28 | ## Config 29 | 30 | The configfile is read from `C:\ProgramData\netbox_windhcp\netbox_windhcp.cfg` 31 | 32 | ``` 33 | --- 34 | webhook: 35 | listen: 0.0.0.0:6969 36 | sync: 37 | dhcp: 38 | server: dhcp.example.com 39 | # lease_duration: 691200 40 | # default_dns_domain: example.com 41 | # default_dns_servers: 42 | # - 192.168.0.1 43 | # - 192.168.0.2 44 | default_dns_flags: 45 | enabled: true 46 | cleanup_expired: true 47 | update_dhcid: true 48 | #default_failover_relation: DHCP-Failover 49 | netbox: 50 | apiurl: https://netbox.example.ch/api/ 51 | token: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 52 | prefix_filter: 53 | tag: dhcp 54 | state: active 55 | range_filter: 56 | role: dhcp-pool 57 | state: active 58 | reservation_filter: 59 | tag: dhcp 60 | log: 61 | dir: C:\ProgramData\netbox_windhcp\ 62 | level: Info 63 | max_size: 10240000 64 | keep_logs: 10 65 | ``` 66 | 67 | # Development 68 | 69 | * Install Rust with Rust-Up 70 | * Compile with `cargo build` 71 | * Test run with `cargo run --bin netbox-windhcp-sync -- --noop` 72 | * Build MSI with `cargo wix` -------------------------------------------------------------------------------- /src/server/service.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::OsString, time::Duration}; 2 | 3 | use log::{debug, error, info}; 4 | use std::sync::mpsc; 5 | use windows::Win32::System::Console::{GetStdHandle, STD_ERROR_HANDLE}; 6 | use windows_service::{ 7 | define_windows_service, 8 | service::*, 9 | service_control_handler::{self, ServiceControlHandlerResult, ServiceStatusHandle}, 10 | service_dispatcher, 11 | }; 12 | 13 | use crate::server::Message; 14 | 15 | define_windows_service!(ffi_service_main, service_main); 16 | 17 | const SERICE_NAME: &str = env!("CARGO_PKG_NAME"); 18 | 19 | pub fn run() { 20 | service_dispatcher::start(SERICE_NAME, ffi_service_main).unwrap(); 21 | } 22 | 23 | pub fn service_main(arguments: Vec) { 24 | debug!("Started service_main: {:?}", arguments); 25 | 26 | let (shutdown_tx, shutdown_rx) = mpsc::channel(); 27 | 28 | let event_handler = move |control_event| -> ServiceControlHandlerResult { 29 | info!("Received ServiceConrtoll Event: {:?}", control_event); 30 | match control_event { 31 | ServiceControl::Stop => { 32 | // Handle stop event and return control back to the system. 33 | shutdown_tx.send(Message::Shutdown).unwrap(); 34 | ServiceControlHandlerResult::NoError 35 | } 36 | // All services must accept Interrogate even if it's a no-op. 37 | ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, 38 | _ => ServiceControlHandlerResult::NotImplemented, 39 | } 40 | }; 41 | 42 | let status_handle = match service_control_handler::register(SERICE_NAME, event_handler) { 43 | Ok(status_handle) => status_handle, 44 | Err(e) => { 45 | error!("Register service event handler failed: {:?}", e); 46 | return; 47 | }, 48 | }; 49 | 50 | debug!("Started service_main: {:?}", arguments); 51 | 52 | if set_service_status(&status_handle, ServiceState::Running).is_err() { 53 | return; 54 | } 55 | 56 | crate::server::run(Some(shutdown_rx)); 57 | 58 | let _ = set_service_status(&status_handle, ServiceState::Stopped); 59 | } 60 | 61 | fn set_service_status( 62 | status_handle: &ServiceStatusHandle, 63 | current_state: ServiceState, 64 | ) -> Result<(), windows_service::Error> { 65 | let controls_accepted = match current_state { 66 | ServiceState::Stopped => ServiceControlAccept::empty(), 67 | ServiceState::Running => ServiceControlAccept::STOP, 68 | _ => ServiceControlAccept::STOP, 69 | }; 70 | 71 | let status_stopped = ServiceStatus { 72 | service_type: ServiceType::OWN_PROCESS, 73 | current_state, 74 | controls_accepted, 75 | exit_code: ServiceExitCode::Win32(0), 76 | checkpoint: 0, 77 | wait_hint: Duration::default(), 78 | process_id: None, 79 | }; 80 | match status_handle.set_service_status(status_stopped) { 81 | Ok(_) => { 82 | debug!("Switched service to {:?}", current_state); 83 | Ok(()) 84 | } 85 | Err(e) => { 86 | error!("Updating service status to {:?} failed: {:?}", current_state, e); 87 | Err(e) 88 | } 89 | } 90 | } 91 | 92 | pub fn running_as_service() -> bool { 93 | unsafe { GetStdHandle(STD_ERROR_HANDLE) }.is_err() 94 | } 95 | -------------------------------------------------------------------------------- /src/bin/netbox-windhcp-log.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, fs::File, io::{self, BufRead}, net::IpAddr}; 2 | 3 | use chrono::NaiveDate; 4 | use log::{debug, error, info}; 5 | use glob::glob; 6 | use netbox_windhcp::Config; 7 | 8 | use netbox_windhcp::sync::netbox::NetboxApi; 9 | 10 | 11 | fn main() { 12 | let config = match Config::load_from_file() { 13 | Ok(config) => config, 14 | Err(e) => { 15 | println!("Error reading config: {}", e); 16 | std::process::exit(exitcode::CONFIG); 17 | } 18 | }; 19 | 20 | let dir = match config.sync.logs.dir { 21 | Some(dir) => dir, 22 | None => { 23 | error!("No log dir configure"); 24 | return; 25 | }, 26 | }; 27 | let pattern = format!("{}\\DhcpSrvLog-*.log", dir.to_str().unwrap()); 28 | 29 | config.log.setup("log"); 30 | 31 | debug!("Parse Logfiles: {:?}", pattern); 32 | 33 | let mut last_lease = HashMap::new(); 34 | 35 | for file in glob(&pattern).expect("Failed to read glob pattern") { 36 | match file { 37 | Ok(filename) => { 38 | debug!("Parse Logfile: {:?}", filename.display()); 39 | let file = File::open(filename).unwrap(); 40 | 41 | for line in io::BufReader::new(file).lines() { 42 | let line = line.unwrap(); 43 | let values: Vec<&str> = line.split(",").collect(); 44 | if values[0] != "10" && values[0] != "11" { 45 | continue; 46 | } 47 | 48 | let ip: IpAddr = values[4].parse().unwrap(); 49 | let date = NaiveDate::parse_from_str(values[1], "%m/%d/%y").unwrap(); 50 | 51 | let old_entry = last_lease.get(&ip); 52 | 53 | match old_entry { 54 | Some(olddate) if (&date > &olddate) => { 55 | last_lease.insert(ip, date); 56 | }, 57 | Some(_) => {}, 58 | None => { 59 | last_lease.insert(ip, date); 60 | }, 61 | } 62 | } 63 | }, 64 | Err(e) => println!("{:?}", e), 65 | } 66 | } 67 | 68 | let api = NetboxApi::new(&config.sync.netbox); 69 | for ip in api.get_reservations().unwrap() { 70 | let addr: IpAddr = std::net::IpAddr::V4(ip.address()); 71 | let last = last_lease.get(&addr); 72 | 73 | match (last, ip.dhcp_reservation_last_active()) { 74 | (None, _) => { 75 | debug!("{:?} no lease action found", addr); 76 | }, 77 | (Some(dhcp_date), Some(netbox_date)) if dhcp_date > &netbox_date => { 78 | info!("{:?} update dhcp_reservation_last_active to {}", addr, dhcp_date); 79 | match api.set_ip_last_active(&ip, dhcp_date) { 80 | Ok(_) => info!("{:?} update dhcp_reservation_last_active to {}", addr, dhcp_date), 81 | Err(e) => error!(" Reservation {}: Updating last used. {}", addr, e), 82 | } 83 | }, 84 | (Some(_), _) => { 85 | debug!("{:?} no lease action found", addr); 86 | }, 87 | } 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/sync/netbox/prefix.rs: -------------------------------------------------------------------------------- 1 | use std::net::Ipv4Addr; 2 | 3 | use ipnet::Ipv4Net; 4 | use serde::Deserialize; 5 | 6 | #[derive(Debug, Deserialize)] 7 | pub struct Prefix { 8 | prefix: Ipv4Net, 9 | description: String, 10 | custom_fields: PrefixCustomField, 11 | } 12 | 13 | impl Prefix { 14 | pub fn prefix(&self) -> Ipv4Net { 15 | self.prefix 16 | } 17 | 18 | pub fn addr(&self) -> Ipv4Addr { 19 | self.prefix.addr() 20 | } 21 | 22 | pub fn netmask(&self) -> Ipv4Addr { 23 | self.prefix.netmask() 24 | } 25 | 26 | pub fn description(&self) -> &str { 27 | self.description.as_ref() 28 | } 29 | 30 | pub fn lease_duration(&self) -> Option { 31 | self.custom_fields.dhcp_lease_duration 32 | } 33 | 34 | pub fn dns_flags(&self) -> Option<&Vec> { 35 | self.custom_fields.dhcp_dns_flags.as_ref() 36 | } 37 | 38 | pub fn routers(&self) -> Option> { 39 | self.custom_fields.dhcp_routers.as_ref() 40 | .map(|routers| routers.iter().map(|n| n.address.addr()) 41 | .collect::>()) 42 | } 43 | 44 | pub fn dns_domain(&self) -> Option<&String> { 45 | self.custom_fields.dhcp_dns_domain.as_ref() 46 | } 47 | 48 | pub fn dns_servers(&self) -> Option> { 49 | self.custom_fields.dhcp_dns_servers.as_ref() 50 | .map(|dns| dns.iter().map(|n| n.address.addr()) 51 | .collect::>()) 52 | } 53 | 54 | pub fn failover_relation(&self) -> Option<&String> { 55 | self.custom_fields.dhcp_failover_relation.as_ref() 56 | } 57 | } 58 | 59 | #[derive(Debug, Deserialize)] 60 | struct PrefixCustomField { 61 | dhcp_lease_duration: Option, 62 | dhcp_dns_flags: Option>, 63 | dhcp_routers: Option>, 64 | dhcp_dns_domain: Option, 65 | dhcp_dns_servers: Option>, 66 | dhcp_failover_relation: Option, 67 | } 68 | 69 | #[derive(Debug, Deserialize)] 70 | struct PrefixCustomFieldIp { 71 | address: Ipv4Net, 72 | } 73 | 74 | #[cfg(test)] 75 | mod tests { 76 | use super::*; 77 | 78 | #[test] 79 | fn it_parses_netbox_prefix() { 80 | let prefix = serde_json::from_str::(r#"{ 81 | "prefix": "10.112.130.0/24", 82 | "description": "foo", 83 | "custom_fields": {} 84 | }"#); 85 | assert!(prefix.is_ok()); 86 | let prefix = prefix.unwrap(); 87 | 88 | assert_eq!(prefix.addr(), "10.112.130.0".parse::().unwrap()); 89 | assert_eq!(prefix.netmask(), "255.255.255.0".parse::().unwrap()); 90 | assert_eq!(prefix.description(), "foo"); 91 | } 92 | 93 | #[test] 94 | fn it_parses_netbox_prefix_w_cf() { 95 | let prefix = serde_json::from_str::(r#"{ 96 | "prefix": "10.112.130.0/24", 97 | "description": "foo", 98 | "custom_fields": { 99 | "dhcp_lease_duration": 86400, 100 | "dhcp_dns_flags": ["enabled"], 101 | "dhcp_routers": [ 102 | { "address": "10.112.130.1/24" } 103 | ], 104 | "dhcp_dns_domain": "example.com", 105 | "dhcp_dns_servers": [ 106 | { "address": "10.112.130.2/24" }, 107 | { "address": "10.112.130.3/24" } 108 | ] 109 | } 110 | }"#); 111 | dbg!(&prefix); 112 | assert!(prefix.is_ok()); 113 | let prefix = prefix.unwrap(); 114 | 115 | assert_eq!(prefix.addr(), "10.112.130.0".parse::().unwrap()); 116 | assert_eq!(prefix.netmask(), "255.255.255.0".parse::().unwrap()); 117 | assert_eq!(prefix.description(), "foo"); 118 | assert_eq!(prefix.lease_duration(), Some(86400)); 119 | assert_eq!(prefix.dns_flags(), Some(&vec!(String::from("enabled")))); 120 | assert_eq!(prefix.routers(), Some(vec!("10.112.130.1".parse().unwrap()))); 121 | assert_eq!(prefix.dns_domain(), Some(&String::from("example.com"))); 122 | assert_eq!(prefix.dns_servers(), Some(vec!("10.112.130.2".parse().unwrap(), "10.112.130.3".parse().unwrap()))); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/server/config.rs: -------------------------------------------------------------------------------- 1 | use std::{net::SocketAddr, time::Duration}; 2 | 3 | use serde::Deserialize; 4 | 5 | #[derive(Debug, Deserialize, Clone, PartialEq, Eq)] 6 | pub struct WebhookConfig { 7 | pub listen: SocketAddr, 8 | sync_interval: Option, 9 | sync_standoff_time: Option, 10 | sync_timeout: Option, 11 | secret: Option, 12 | cert: Option, 13 | key: Option, 14 | } 15 | 16 | impl WebhookConfig { 17 | pub fn sync_interval(&self) -> chrono::Duration { 18 | chrono::Duration::try_seconds(self.sync_interval.unwrap_or(900)).expect("Should never fail") 19 | } 20 | 21 | pub fn sync_standoff_time(&self) -> Duration { 22 | Duration::from_secs(self.sync_standoff_time.unwrap_or(5)) 23 | } 24 | 25 | pub fn sync_timeout(&self) -> Duration { 26 | Duration::from_secs(self.sync_timeout.unwrap_or(60)) 27 | } 28 | 29 | pub fn secret(&self) -> Option<&String> { 30 | self.secret.as_ref() 31 | } 32 | 33 | pub fn enable_tls(&self) -> bool { 34 | self.cert.is_some() && self.key.is_some() 35 | } 36 | 37 | pub fn cert(&self) -> Option<&String> { 38 | self.cert.as_ref() 39 | } 40 | 41 | pub fn key(&self) -> Option<&String> { 42 | self.key.as_ref() 43 | } 44 | } 45 | 46 | impl Default for WebhookConfig { 47 | fn default() -> Self { 48 | Self { 49 | listen: "9.9.9.9:1111".parse().unwrap(), 50 | sync_interval: Default::default(), 51 | sync_standoff_time: Default::default(), 52 | sync_timeout: Default::default(), 53 | secret: Default::default(), 54 | cert: Default::default(), 55 | key: Default::default(), 56 | } 57 | } 58 | } 59 | 60 | #[cfg(test)] 61 | mod tests { 62 | use super::*; 63 | 64 | #[test] 65 | fn it_parses_with_only_listen() { 66 | let cfg = serde_yaml_ng::from_str::(r#"--- 67 | listen: 127.0.0.1:12345 68 | "#); 69 | assert_eq!(cfg.unwrap(), WebhookConfig { 70 | listen: "127.0.0.1:12345".parse().unwrap(), 71 | ..Default::default() 72 | }); 73 | } 74 | 75 | #[test] 76 | fn it_parses_with_all_options() { 77 | let cfg = serde_yaml_ng::from_str::(r#"--- 78 | listen: 127.0.0.1:12345 79 | sync_interval: 42 80 | sync_standoff_time: 42 81 | sync_timeout: 42 82 | secret: SECRET 83 | cert: cert.pem 84 | key: key.pem 85 | "#); 86 | assert_eq!(cfg.unwrap(), WebhookConfig { 87 | listen: "127.0.0.1:12345".parse().unwrap(), 88 | sync_interval: Some(42), 89 | sync_standoff_time: Some(42), 90 | sync_timeout: Some(42), 91 | secret: Some(String::from("SECRET")), 92 | cert: Some(String::from("cert.pem")), 93 | key: Some(String::from("key.pem")), 94 | }); 95 | } 96 | 97 | #[test] 98 | fn it_returns_sync_interval_as_durations() { 99 | let cfg = WebhookConfig { 100 | sync_interval: Some(42), 101 | ..Default::default() 102 | }; 103 | assert_eq!(cfg.sync_interval(), chrono::Duration::seconds(42)) 104 | } 105 | 106 | #[test] 107 | fn it_returns_sync_interval_default () { 108 | let cfg = WebhookConfig::default(); 109 | assert_eq!(cfg.sync_interval(), chrono::Duration::seconds(900)) 110 | } 111 | 112 | #[test] 113 | fn it_returns_sync_standoff_time_as_durations() { 114 | let cfg = WebhookConfig { 115 | sync_standoff_time: Some(42), 116 | ..Default::default() 117 | }; 118 | assert_eq!(cfg.sync_standoff_time(), Duration::from_secs(42)) 119 | } 120 | 121 | #[test] 122 | fn it_returns_sync_standoff_time_default() { 123 | let cfg = WebhookConfig::default(); 124 | assert_eq!(cfg.sync_standoff_time(), Duration::from_secs(5)) 125 | } 126 | 127 | #[test] 128 | fn it_returns_sync_timeout_as_durations() { 129 | let cfg = WebhookConfig { 130 | sync_timeout: Some(42), 131 | ..Default::default() 132 | }; 133 | assert_eq!(cfg.sync_timeout(), Duration::from_secs(42)) 134 | } 135 | 136 | #[test] 137 | fn it_returns_sync_timeout_default () { 138 | let cfg = WebhookConfig::default(); 139 | assert_eq!(cfg.sync_timeout(), Duration::from_secs(60)) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/sync/netbox/config.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use ipnet::Ipv4Net; 4 | use serde::Deserialize; 5 | 6 | #[derive(Debug, Deserialize, Clone)] 7 | #[serde(default)] 8 | pub struct SyncNetboxConfig { 9 | apiurl: String, 10 | token: String, 11 | prefix_filter: HashMap, 12 | range_filter: HashMap, 13 | reservation_filter: HashMap, 14 | router_filter: HashMap, 15 | } 16 | 17 | impl Default for SyncNetboxConfig { 18 | fn default() -> Self { 19 | Self { 20 | apiurl: Default::default(), 21 | token: Default::default(), 22 | prefix_filter: HashMap::from([ 23 | (String::from("tag"), String::from("dhcp")), 24 | (String::from("status"), String::from("active")), 25 | (String::from("family"), String::from("4")), 26 | ]), 27 | range_filter: HashMap::from([ 28 | (String::from("role"), String::from("dhcp-pool")), 29 | (String::from("status"), String::from("active")), 30 | (String::from("family"), String::from("4")), 31 | ]), 32 | reservation_filter: HashMap::from([ 33 | (String::from("tag"), String::from("dhcp")), 34 | (String::from("status"), String::from("active")), 35 | ]), 36 | router_filter: HashMap::from([ 37 | (String::from("tag"), String::from("router")), 38 | (String::from("status"), String::from("active")), 39 | ]), 40 | } 41 | } 42 | } 43 | 44 | impl SyncNetboxConfig { 45 | pub fn apiurl(&self) -> &str { 46 | self.apiurl.as_ref() 47 | } 48 | 49 | pub fn token(&self) -> &str { 50 | self.token.as_ref() 51 | } 52 | 53 | pub fn prefix_filter(&self) -> &HashMap { 54 | &self.prefix_filter 55 | } 56 | 57 | pub fn range_filter(&self) -> &HashMap { 58 | &self.range_filter 59 | } 60 | 61 | pub fn reservation_filter(&self, parent: Option<&Ipv4Net>) -> HashMap { 62 | let mut filter = self.reservation_filter.clone(); 63 | if let Some(parent) = parent { 64 | filter.insert(String::from("parent"), parent.to_string()); 65 | } 66 | filter 67 | } 68 | 69 | pub fn router_filter(&self, parent: &Ipv4Net) -> HashMap { 70 | let mut filter = self.router_filter.clone(); 71 | filter.insert(String::from("parent"), parent.to_string()); 72 | filter 73 | } 74 | } 75 | 76 | #[cfg(test)] 77 | mod tests { 78 | use std::str::FromStr; 79 | 80 | use super::*; 81 | 82 | #[test] 83 | fn it_parses_netbox_config() { 84 | let cfg = serde_yaml_ng::from_str::(r#"--- 85 | apiurl: https://netbox.example.com/api/ 86 | token: SECRET 87 | prefix_filter: 88 | foo: bar 89 | range_filter: 90 | alice: bob 91 | reservation_filter: 92 | tag: value 93 | router_filter: 94 | ip: addr 95 | "#); 96 | assert!(cfg.is_ok()); 97 | let cfg = cfg.unwrap(); 98 | assert_eq!(cfg.apiurl, "https://netbox.example.com/api/"); 99 | assert_eq!(cfg.token, "SECRET"); 100 | assert_eq!(cfg.prefix_filter.get("foo").unwrap(), "bar"); 101 | assert_eq!(cfg.range_filter.get("alice").unwrap(), "bob"); 102 | assert_eq!(cfg.reservation_filter.get("tag").unwrap(), "value"); 103 | assert_eq!(cfg.router_filter.get("ip").unwrap(), "addr"); 104 | } 105 | 106 | #[test] 107 | fn it_parses_minimal_netbox_config() { 108 | let cfg = serde_yaml_ng::from_str::(r#"--- 109 | apiurl: https://netbox.example.com/api/ 110 | token: SECRET 111 | "#); 112 | assert!(cfg.is_ok()); 113 | let cfg = cfg.unwrap(); 114 | assert_eq!(cfg.apiurl, "https://netbox.example.com/api/"); 115 | assert_eq!(cfg.token, "SECRET"); 116 | assert_eq!(cfg.prefix_filter.get("tag").unwrap(), "dhcp"); 117 | assert_eq!(cfg.prefix_filter.get("status").unwrap(), "active"); 118 | } 119 | 120 | #[test] 121 | fn it_builds_the_reservation_filter() { 122 | let cfg = SyncNetboxConfig::default(); 123 | let filter = cfg.reservation_filter(Some(&Ipv4Net::from_str("127.0.0.1/8").unwrap())); 124 | assert_eq!(filter.get("parent").unwrap(), "127.0.0.1/8"); 125 | } 126 | 127 | #[test] 128 | fn it_builds_the_router_filter() { 129 | let cfg = SyncNetboxConfig::default(); 130 | let filter = cfg.router_filter(&Ipv4Net::from_str("127.0.0.1/8").unwrap()); 131 | assert_eq!(filter.get("parent").unwrap(), "127.0.0.1/8"); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | use std::path::{PathBuf, Path}; 2 | 3 | use log::LevelFilter; 4 | use log4rs::{ 5 | append::{console::ConsoleAppender, rolling_file::{RollingFileAppender, policy::compound::{CompoundPolicy, trigger::size::SizeTrigger, roll::fixed_window::FixedWindowRoller}}}, 6 | config::{Appender, Root}, 7 | encode::pattern::PatternEncoder, 8 | Config, Handle, 9 | }; 10 | use serde::Deserialize; 11 | 12 | #[derive(Debug, Deserialize)] 13 | pub struct LogConfig { 14 | #[serde(default = "LogConfig::default_dir")] 15 | dir: Option, 16 | #[serde(default = "LogConfig::default_levelfilter")] 17 | level: LevelFilter, 18 | #[serde(default = "LogConfig::default_max_size")] 19 | max_size: u64, 20 | #[serde(default = "LogConfig::default_keep_log")] 21 | keep_logs: u32, 22 | } 23 | 24 | impl Default for LogConfig { 25 | fn default() -> Self { 26 | let dir = if Path::new(concat!("C:\\ProgramData\\", env!("CARGO_PKG_NAME"))).exists() { 27 | Some(PathBuf::from(concat!("C:\\ProgramData\\", env!("CARGO_PKG_NAME")))) 28 | } else { 29 | None 30 | }; 31 | 32 | Self { dir, level: LevelFilter::Info, max_size: 10*1024*1024, keep_logs: 10 } 33 | } 34 | } 35 | 36 | impl LogConfig { 37 | pub fn setup(&self, name: &str) -> Handle { 38 | log4rs::init_config(self.as_log4rs_config(name)).unwrap() 39 | } 40 | 41 | pub(self) fn as_log4rs_config(&self, name: &str) -> Config { 42 | let stdout = ConsoleAppender::builder() 43 | .encoder(Box::new(PatternEncoder::new( 44 | "{d(%Y-%m-%d %H:%M:%S)} {h({l})} {t} - {m}{n}", 45 | ))) 46 | .build(); 47 | 48 | let mut config = Config::builder() 49 | .appender(Appender::builder().build("stdout", Box::new(stdout))); 50 | let mut root = Root::builder().appender("stdout"); 51 | 52 | if let Some(dir) = &self.dir { 53 | let policy = CompoundPolicy::new( 54 | Box::new(SizeTrigger::new(self.max_size)), 55 | Box::new( 56 | FixedWindowRoller::builder() 57 | .build(dir.join(format!("{}.{{}}.log", name)).to_str().unwrap(), self.keep_logs) 58 | .unwrap() 59 | ) 60 | ); 61 | 62 | let logfile = RollingFileAppender::builder() 63 | .append(true) 64 | .encoder(Box::new(PatternEncoder::new( 65 | "{d(%Y-%m-%d %H:%M:%S)} {l} {t} - {m}{n}", 66 | ))) 67 | .build( 68 | dir.join(format!("{}.log", name)), 69 | Box::new(policy) 70 | ) 71 | .unwrap(); 72 | config = config.appender(Appender::builder().build("logfile", Box::new(logfile))); 73 | root = root.appender("logfile"); 74 | } 75 | 76 | config.build(root.build(self.level)).unwrap() 77 | } 78 | 79 | fn default_dir() -> Option { 80 | Self::default().dir 81 | } 82 | 83 | fn default_levelfilter() -> LevelFilter { 84 | LevelFilter::Info 85 | } 86 | 87 | fn default_max_size() -> u64 { 88 | 10*1024*1024 89 | } 90 | 91 | fn default_keep_log() -> u32 { 92 | 10 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use super::*; 99 | 100 | #[test] 101 | fn it_parses_log_config() { 102 | let cfg = serde_yaml_ng::from_str::(r#"--- 103 | level: Debug 104 | dir: C:\tmp\ 105 | "#); 106 | dbg!(&cfg); 107 | assert!(cfg.is_ok()); 108 | let cfg = cfg.unwrap(); 109 | assert_eq!(cfg.level, LevelFilter::Debug); 110 | assert_eq!(cfg.dir, Some(PathBuf::from("C:\\tmp\\"))); 111 | } 112 | 113 | #[test] 114 | fn it_parses_log_config_without_dir() { 115 | let cfg = serde_yaml_ng::from_str::(r#"--- 116 | level: Debug 117 | "#); 118 | dbg!(&cfg); 119 | assert!(cfg.is_ok()); 120 | let cfg = cfg.unwrap(); 121 | assert_eq!(cfg.level, LevelFilter::Debug); 122 | assert_eq!(cfg.dir, None); 123 | } 124 | 125 | #[test] 126 | fn as_to_lof4rs_config() { 127 | let cfg = serde_yaml_ng::from_str::(r#"--- 128 | level: Debug 129 | dir: C:\tmp\ 130 | "#).unwrap(); 131 | let cfg = cfg.as_log4rs_config("test"); 132 | dbg!(&cfg); 133 | assert_eq!(cfg.root().appenders(), vec!("stdout", "logfile")); 134 | assert_eq!(cfg.appenders().iter().map(|a| a.name()).collect::>(), vec!("stdout", "logfile")); 135 | } 136 | 137 | #[test] 138 | fn as_to_lof4rs_config_without_dir() { 139 | let cfg = serde_yaml_ng::from_str::(r#"--- 140 | level: Debug 141 | "#).unwrap(); 142 | let cfg = cfg.as_log4rs_config("test"); 143 | dbg!(&cfg); 144 | assert_eq!(cfg.root().appenders(), vec!("stdout")); 145 | assert_eq!(cfg.appenders().iter().map(|a| a.name()).collect::>(), vec!("stdout")); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/sync/netbox/mod.rs: -------------------------------------------------------------------------------- 1 | pub(super) mod model; 2 | 3 | use std::collections::HashMap; 4 | 5 | use chrono::NaiveDate; 6 | use ipnet::Ipv4Net; 7 | use log::debug; 8 | use serde::Deserialize; 9 | use serde_json::json; 10 | use ureq::http::{HeaderValue, Request}; 11 | use ureq::tls::{RootCerts, TlsConfig}; 12 | use ureq::{Agent, Body, SendBody}; 13 | use ureq::{middleware::MiddlewareNext, http::response::Response, Error}; 14 | 15 | pub mod config; 16 | use self::config::SyncNetboxConfig; 17 | use self::model::*; 18 | pub mod prefix; 19 | use prefix::*; 20 | pub mod range; 21 | use range::*; 22 | pub mod address; 23 | use address::*; 24 | 25 | pub struct NetboxApi { 26 | config: SyncNetboxConfig, 27 | client: ureq::Agent, 28 | } 29 | 30 | impl NetboxApi { 31 | pub fn new(config: &SyncNetboxConfig) -> Self { 32 | let config = config.clone(); 33 | 34 | let auth_value = format!("Token {}", config.token()); 35 | 36 | let client = Agent::config_builder() 37 | .tls_config( 38 | TlsConfig::builder() 39 | .root_certs(RootCerts::PlatformVerifier) 40 | .build() 41 | ) 42 | .user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))) 43 | .middleware(move |mut req: Request, next: MiddlewareNext| -> Result, Error> { 44 | req.headers_mut().append("Authorization", HeaderValue::from_str(auth_value.as_str()).unwrap()); 45 | next.handle(req) 46 | }) 47 | .build() 48 | .into(); 49 | 50 | Self { config, client } 51 | } 52 | 53 | pub fn version( 54 | &self, 55 | ) -> Result> { 56 | let url = format!("{}status/", self.config.apiurl()); 57 | 58 | #[derive(Debug, Deserialize)] 59 | struct NetboxStatus { 60 | #[serde(rename = "netbox-version")] 61 | netbox_version: String, 62 | } 63 | 64 | let status: NetboxStatus = self.client.get(&url) 65 | .call()? 66 | .body_mut() 67 | .read_json()?; 68 | 69 | Ok(status.netbox_version) 70 | } 71 | 72 | pub fn get_prefixes(&self) -> Result, ureq::Error> { 73 | self.get_objects("ipam/prefixes/", self.config.prefix_filter()) 74 | } 75 | 76 | pub fn get_ranges(&self) -> Result, ureq::Error> { 77 | self.get_objects("ipam/ip-ranges/", self.config.range_filter()) 78 | } 79 | 80 | pub fn get_reservations(&self) -> Result, ureq::Error> { 81 | self.get_objects("ipam/ip-addresses/", &self.config.reservation_filter(None)) 82 | } 83 | 84 | pub fn get_reservations_for_subnet(&self, subnet: &Ipv4Net) -> Result, ureq::Error> { 85 | self.get_objects("ipam/ip-addresses/", &self.config.reservation_filter(Some(subnet))) 86 | } 87 | 88 | pub fn get_router_for_subnet(&self, subnet: &Ipv4Net) -> Result, ureq::Error> { 89 | self.get_objects("ipam/ip-addresses/", &self.config.router_filter(subnet)) 90 | } 91 | 92 | pub fn set_ip_last_active(&self, ip: &IpAddress, date: &NaiveDate) -> Result<(), ureq::Error> { 93 | let payload = json!({ 94 | "custom_fields": { 95 | "dhcp_reservation_last_active": date, 96 | } 97 | }); 98 | 99 | self.client.patch(ip.url()) 100 | .header("Content-Type", "application/json") 101 | .send(payload.to_string().as_str())?; 102 | 103 | Ok(()) 104 | } 105 | 106 | fn get_objects Deserialize<'a>>( 107 | &self, 108 | path: &str, 109 | filter: &HashMap, 110 | ) -> Result, ureq::Error> { 111 | let url = format!("{}{}", self.config.apiurl(), path); 112 | 113 | let mut query: Vec<(&str, &str)> = vec![]; 114 | for (key, val) in filter.iter() { 115 | query.push((key.as_str(), val.as_str())); 116 | } 117 | 118 | debug!("Fetch {} from {:?}", std::any::type_name::(), url); 119 | let mut page: Pageination = self.client.get(&url) 120 | .query_pairs(query) 121 | .call()? 122 | .body_mut() 123 | .read_json()?; 124 | 125 | let mut objects: Vec = Vec::with_capacity(page.count); 126 | 127 | loop { 128 | objects.append(&mut page.results); 129 | 130 | match page.next { 131 | Some(ref u) => { 132 | debug!("Fetch next page from {:?}", u); 133 | page = self.client.get(u) 134 | .call()? 135 | .body_mut() 136 | .read_json()?; 137 | } 138 | None => { break; } 139 | } 140 | } 141 | 142 | Ok(objects) 143 | } 144 | 145 | pub fn get_object Deserialize<'a>>(&self, url: &str) -> Result { 146 | debug!("Fetch {} from {:?}", std::any::type_name::(), url); 147 | let object: T = self.client.get(url) 148 | .call()? 149 | .body_mut() 150 | .read_json()?; 151 | Ok(object) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/server/sync.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env::{self, consts::EXE_SUFFIX}, 3 | error::Error, 4 | path::PathBuf, 5 | process::Stdio, 6 | time::Duration, 7 | }; 8 | 9 | use chrono::Utc; 10 | use log::{debug, error, info}; 11 | use tokio::{ 12 | process::Command, 13 | sync::broadcast::{self, error::RecvError}, 14 | time::{error::Elapsed, sleep, Instant}, 15 | }; 16 | 17 | use crate::server::shared::SyncStatus; 18 | 19 | use super::{ 20 | config::WebhookConfig, 21 | shared::{Message, SharedServerStatus}, 22 | }; 23 | 24 | fn get_sync_binary() -> Result> { 25 | let sync_exe = env::current_exe()? 26 | .parent().ok_or(SyncError::CommandNotFound)? 27 | .join(format!("netbox-windhcp-sync{}", EXE_SUFFIX)); 28 | if !sync_exe.is_file() { 29 | error!("Sync binary at {} not found.", sync_exe.display()); 30 | return Err(Box::new(SyncError::CommandNotFound)); 31 | } 32 | Ok(sync_exe) 33 | } 34 | 35 | pub async fn worker( 36 | config: &WebhookConfig, 37 | status: &SharedServerStatus, 38 | message_tx: &broadcast::Sender, 39 | mut message_rx: broadcast::Receiver, 40 | ) { 41 | let status: SharedServerStatus = status.clone(); 42 | let sync_standoff_time = config.sync_standoff_time(); 43 | let sync_timeout = config.sync_timeout(); 44 | 45 | let sync_command = match get_sync_binary() { 46 | Ok(bin) => bin, 47 | Err(e) => { 48 | error!("Sync binary not found. {}", e); 49 | message_tx.send(Message::Shutdown).unwrap(); 50 | return; 51 | } 52 | }; 53 | 54 | debug!("Sync Runner Thread Started"); 55 | loop { 56 | match message_rx.recv().await { 57 | Ok(Message::Shutdown) | Err(RecvError::Closed) => { break; }, 58 | Ok(Message::TriggerSync) => { 59 | if !status.lock().await.needs_sync { 60 | debug!("Sync not required"); 61 | continue; 62 | } 63 | info!("Sync Triggerd"); 64 | 65 | sleep(sync_standoff_time).await; 66 | 67 | { 68 | let mut status = status.lock().await; 69 | status.needs_sync = false; 70 | status.syncing = true; 71 | } 72 | 73 | let sync_start = Instant::now(); 74 | let sync_status = run_sync_command(&sync_command, &sync_timeout).await; 75 | 76 | match sync_status { 77 | Ok(_) => { 78 | info!("Sync succeeded ({}s)", sync_start.elapsed().as_secs()); 79 | { 80 | let mut status = status.lock().await; 81 | status.syncing = false; 82 | status.last_sync = Some(Utc::now()); 83 | status.last_sync_status = SyncStatus::SyncOk; 84 | } 85 | } 86 | Err(e) => { 87 | error!("Sync failed: {}", e); 88 | { 89 | let mut status = status.lock().await; 90 | status.syncing = false; 91 | status.last_sync = Some(Utc::now()); 92 | status.last_sync_status = SyncStatus::SyncFailed; 93 | } 94 | } 95 | } 96 | }, 97 | Err(RecvError::Lagged(_)) => {}, 98 | } 99 | } 100 | info!("Sync Thread Ended"); 101 | } 102 | 103 | #[derive(Debug)] 104 | enum SyncError { 105 | CommandNotFound, 106 | IoError(std::io::Error), 107 | Timeout(Elapsed), 108 | CommandStatus(i32), 109 | CommandNoStatus, 110 | } 111 | 112 | impl Error for SyncError {} 113 | impl From for SyncError { 114 | fn from(e: std::io::Error) -> Self { 115 | SyncError::IoError(e) 116 | } 117 | } 118 | impl std::fmt::Display for SyncError { 119 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 120 | match self { 121 | SyncError::CommandNotFound => write!(f, "Sync binary not found"), 122 | SyncError::IoError(e) => write!(f, "Process io::Error {:?}", e), 123 | SyncError::Timeout(_) => write!(f, "Process timed out."), 124 | SyncError::CommandStatus(s) => write!(f, "Process exited with status code {}", s), 125 | SyncError::CommandNoStatus => write!(f, "Process exited without a status"), 126 | } 127 | } 128 | } 129 | 130 | async fn run_sync_command(command: &PathBuf, timeout: &Duration) -> Result<(), SyncError> { 131 | info!("Run Sync Command: {}", &command.display()); 132 | let mut child = Command::new(command) 133 | .stdin(Stdio::null()) 134 | .stdout(Stdio::null()) 135 | .stderr(Stdio::null()) 136 | .spawn()?; 137 | 138 | let status = match tokio::time::timeout(timeout.to_owned(), child.wait()).await { 139 | Ok(s) => s, 140 | Err(e) => { 141 | debug!("Sync Command reached timeout. Terminating process."); 142 | child.kill().await?; 143 | return Err(SyncError::Timeout(e)); 144 | } 145 | }?; 146 | 147 | match status.code() { 148 | Some(0) => Ok(()), 149 | Some(n) => Err(SyncError::CommandStatus(n)), 150 | None => Err(SyncError::CommandNoStatus), 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/sync/windhcp/subnet/elements.rs: -------------------------------------------------------------------------------- 1 | use std::{os::raw::c_void, ptr}; 2 | 3 | use windows::Win32::NetworkManagement::Dhcp::{DhcpRpcFreeMemory, DHCP_BOOTP_IP_RANGE, DHCP_SUBNET_ELEMENT_INFO_ARRAY_V5, DhcpEnumSubnetElementsV5, DhcpIpRangesDhcpBootp, DHCP_SUBNET_ELEMENT_DATA_V5, DHCP_SUBNET_ELEMENT_DATA_V5_0, DhcpAddSubnetElementV5, DhcpReservedIps, DHCP_IP_RESERVATION_V4, DHCP_BINARY_DATA, DhcpRemoveSubnetElementV5, DhcpFullForce}; 4 | 5 | use super::Subnet; 6 | use super::reservation::Reservation; 7 | 8 | pub trait SubnetElements { 9 | fn get_first_element(&self) -> Result, u32> { 10 | todo!() 11 | } 12 | fn get_elements(&self) -> Result, u32> { 13 | todo!() 14 | } 15 | fn add_element(&self, _element: &mut T) -> Result<(), u32> { 16 | todo!() 17 | } 18 | fn remove_element(&self, _element: &mut T) -> Result<(), u32> { 19 | todo!() 20 | } 21 | } 22 | 23 | impl SubnetElements for Subnet { 24 | fn get_first_element(&self) -> Result, u32> { 25 | let mut resumehandle: u32 = 0; 26 | let mut elementsread: u32 = 0; 27 | let mut elementstotal: u32 = 0; 28 | 29 | let mut enumelementinfo: *mut DHCP_SUBNET_ELEMENT_INFO_ARRAY_V5 = ptr::null_mut(); 30 | 31 | match unsafe { 32 | DhcpEnumSubnetElementsV5( 33 | &self.serveripaddress, 34 | self.subnetaddress, 35 | DhcpIpRangesDhcpBootp, 36 | &mut resumehandle, 37 | 0xFFFFFFFF, 38 | &mut enumelementinfo, 39 | &mut elementsread, 40 | &mut elementstotal, 41 | ) 42 | } { 43 | 0 => unsafe { 44 | let range = DHCP_BOOTP_IP_RANGE { ..(*(*(*enumelementinfo).Elements).Element.IpRange) }; 45 | 46 | for idx in 0usize..(*enumelementinfo).NumElements.try_into().unwrap() { 47 | DhcpRpcFreeMemory((*(*enumelementinfo).Elements.offset(idx.try_into().unwrap())).Element.IpRange as *mut c_void); 48 | } 49 | DhcpRpcFreeMemory((*enumelementinfo).Elements as *mut c_void); 50 | DhcpRpcFreeMemory(enumelementinfo as *mut c_void); 51 | 52 | Ok(Some(range)) 53 | }, 54 | //ERROR_NO_MORE_ITEMS 55 | 259 => { 56 | Ok(None) 57 | }, 58 | n => { 59 | Err(n) 60 | } 61 | } 62 | } 63 | 64 | fn add_element(&self, element: &mut DHCP_BOOTP_IP_RANGE) -> Result<(), u32> { 65 | let addelementinfo = DHCP_SUBNET_ELEMENT_DATA_V5 { 66 | ElementType: DhcpIpRangesDhcpBootp, 67 | Element: DHCP_SUBNET_ELEMENT_DATA_V5_0 { 68 | IpRange: element, 69 | }, 70 | }; 71 | 72 | match unsafe { DhcpAddSubnetElementV5(&self.serveripaddress, self.subnetaddress, &addelementinfo) } { 73 | 0 => Ok(()), 74 | n => Err(n), 75 | } 76 | } 77 | } 78 | 79 | 80 | impl SubnetElements for Subnet { 81 | fn get_elements(&self) -> Result, u32> { 82 | let mut resumehandle: u32 = 0; 83 | let mut elementsread: u32 = 0; 84 | let mut elementstotal: u32 = 0; 85 | 86 | let mut enumelementinfo: *mut DHCP_SUBNET_ELEMENT_INFO_ARRAY_V5 = ptr::null_mut(); 87 | 88 | match unsafe { 89 | DhcpEnumSubnetElementsV5( 90 | &self.serveripaddress, 91 | self.subnetaddress, 92 | DhcpReservedIps, 93 | &mut resumehandle, 94 | 0xFFFFFFFF, 95 | &mut enumelementinfo, 96 | &mut elementsread, 97 | &mut elementstotal, 98 | ) 99 | } { 100 | 0 => { 101 | let mut elements = Vec::new(); 102 | 103 | for idx in 0usize..unsafe{ (*enumelementinfo).NumElements.try_into().unwrap() } { 104 | let res = unsafe {*(*(*enumelementinfo).Elements.offset(idx.try_into().unwrap())).Element.ReservedIp}; 105 | elements.insert(idx, Reservation::from(res)); 106 | } 107 | 108 | unsafe { 109 | for idx in 0usize..(*enumelementinfo).NumElements.try_into().unwrap() { 110 | DhcpRpcFreeMemory((*(*enumelementinfo).Elements.offset(idx.try_into().unwrap())).Element.ReservedIp as *mut c_void); 111 | } 112 | DhcpRpcFreeMemory((*enumelementinfo).Elements as *mut c_void); 113 | DhcpRpcFreeMemory(enumelementinfo as *mut c_void); 114 | } 115 | 116 | Ok(elements) 117 | }, 118 | //ERROR_NO_MORE_ITEMS 119 | 259 => { 120 | Ok(vec![]) 121 | } 122 | n => { 123 | Err(n) 124 | } 125 | } 126 | } 127 | 128 | fn add_element(&self, element: &mut Reservation) -> Result<(), u32> { 129 | 130 | let mut for_client = DHCP_BINARY_DATA { 131 | DataLength: element.for_client.len().try_into().unwrap(), 132 | Data: element.for_client[..].as_mut_ptr() 133 | }; 134 | 135 | let mut reserved_ip = DHCP_IP_RESERVATION_V4 { 136 | ReservedIpAddress: element.ip_address.into(), 137 | ReservedForClient: &mut for_client, 138 | bAllowedClientTypes: element.allowed_client_types.into() 139 | }; 140 | 141 | let addelementinfo = DHCP_SUBNET_ELEMENT_DATA_V5 { 142 | ElementType: DhcpReservedIps, 143 | Element: DHCP_SUBNET_ELEMENT_DATA_V5_0 { 144 | ReservedIp: &mut reserved_ip, 145 | }, 146 | }; 147 | 148 | match unsafe { DhcpAddSubnetElementV5(&self.serveripaddress, self.subnetaddress, &addelementinfo) } { 149 | 0 => Ok(()), 150 | n => Err(n), 151 | } 152 | } 153 | 154 | fn remove_element(&self, element: &mut Reservation) -> Result<(), u32> { 155 | 156 | let mut for_client = DHCP_BINARY_DATA { 157 | DataLength: element.for_client.len().try_into().unwrap(), 158 | Data: element.for_client[..].as_mut_ptr() 159 | }; 160 | 161 | let mut reserved_ip = DHCP_IP_RESERVATION_V4 { 162 | ReservedIpAddress: element.ip_address.into(), 163 | ReservedForClient: &mut for_client, 164 | bAllowedClientTypes: element.allowed_client_types.clone().into() 165 | }; 166 | 167 | let removeelementinfo = DHCP_SUBNET_ELEMENT_DATA_V5 { 168 | ElementType: DhcpReservedIps, 169 | Element: DHCP_SUBNET_ELEMENT_DATA_V5_0 { 170 | ReservedIp: &mut reserved_ip, 171 | }, 172 | }; 173 | 174 | match unsafe { DhcpRemoveSubnetElementV5(&self.serveripaddress, self.subnetaddress, &removeelementinfo, DhcpFullForce) } { 175 | 0 => Ok(()), 176 | n => Err(n), 177 | } 178 | } 179 | } -------------------------------------------------------------------------------- /wix/main.wxs: -------------------------------------------------------------------------------- 1 | 2 | 17 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 109 | 110 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /src/server/web.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::Infallible, fmt::Debug, ops::Deref}; 2 | 3 | use log::{debug, warn}; 4 | use serde::Serialize; 5 | use tokio::sync::broadcast; 6 | use warp::{hyper::Uri, reject::Reject, Filter, http::StatusCode}; 7 | use hmac::{Hmac, Mac}; 8 | use sha2::Sha512; 9 | type HmacSha512 = Hmac; 10 | 11 | use super::{ 12 | config::WebhookConfig, 13 | shared::{Message, SharedServerStatus}, 14 | webhook::NetboxWebHook, 15 | }; 16 | 17 | pub async fn server( 18 | config: &WebhookConfig, 19 | status: &SharedServerStatus, 20 | message_tx: &broadcast::Sender, 21 | ) { 22 | let index_route = warp::get() 23 | .and(warp::path::end()) 24 | .map(|| warp::redirect::found(Uri::from_static("/status"))); 25 | 26 | let status_clone = status.clone(); 27 | let status_filter = warp::any().map(move || status_clone.clone()); 28 | let status_route = warp::get() 29 | .and(warp::path("status")).and(warp::path::end()) 30 | .and(status_filter) 31 | .and_then(|status: SharedServerStatus| async move { 32 | let status = status.lock().await; 33 | match serde_json::to_string_pretty(&status.deref()) { 34 | Ok(s) => Ok(s), 35 | Err(_e) => Err(warp::reject()), 36 | } 37 | }) 38 | .map(|reply| 39 | warp::reply::with_header(reply, warp::http::header::REFRESH, "5") 40 | ); 41 | 42 | let status_clone = status.clone(); 43 | let status_filter = warp::any().map(move || status_clone.clone()); 44 | let message_clone = message_tx.clone(); 45 | let message_filter = warp::any().map(move || message_clone.clone()); 46 | let secret_clone = config.secret().map(String::to_owned); 47 | let webhook_route = warp::post() 48 | .and(warp::path("webhook")).and(warp::path::end()) 49 | .and(warp::body::content_length_limit(1024 * 32)) 50 | .and(netbox_webhook_body(secret_clone)) 51 | .and(status_filter).and(message_filter) 52 | .and_then(|body: NetboxWebHook, status: SharedServerStatus, message_tx: broadcast::Sender| async move { 53 | { 54 | let mut status = status.lock().await; 55 | status.needs_sync = true; 56 | } 57 | debug!("Received Webhook: {:?}", body); 58 | match message_tx.send(Message::TriggerSync) { 59 | Ok(_) => Ok(r#"{"info": "Sync triggerd"}"#), 60 | Err(_) => Err(warp::reject()), 61 | } 62 | }); 63 | 64 | let route = warp::any().and( 65 | index_route 66 | .or(status_route) 67 | .or(webhook_route) 68 | ) 69 | .recover(handle_rejection) 70 | .map(|reply| { 71 | warp::reply::with_header(reply, "server", format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))) 72 | }).with(warp::log(module_path!())); 73 | 74 | let message_clone = message_tx.clone(); 75 | 76 | if config.enable_tls() { 77 | let (_addr, server) = warp::serve(route) 78 | .tls() 79 | .cert_path("cert.pem") 80 | .key_path("key.rsa") 81 | .bind_with_graceful_shutdown(config.listen, async move { 82 | let mut message_rx = message_clone.subscribe(); 83 | while let Ok(msg) = message_rx.recv().await { 84 | if msg == Message::Shutdown { 85 | break; 86 | } 87 | } 88 | }); 89 | server.await 90 | } else { 91 | let (_addr, server) = warp::serve(route) 92 | .bind_with_graceful_shutdown(config.listen, async move { 93 | let mut message_rx = message_clone.subscribe(); 94 | while let Ok(msg) = message_rx.recv().await { 95 | if msg == Message::Shutdown { 96 | break; 97 | } 98 | } 99 | }); 100 | server.await 101 | } 102 | } 103 | 104 | 105 | #[derive(Debug, PartialEq, Eq)] 106 | pub enum WebErrors { 107 | MissingSignature, 108 | BadSecret, 109 | BadSignature, 110 | BadFormat, 111 | } 112 | impl Reject for WebErrors {} 113 | 114 | #[derive(Serialize)] 115 | struct ErrorMessage { 116 | code: u16, 117 | message: String, 118 | } 119 | 120 | fn netbox_webhook_body( 121 | secret: Option, 122 | ) -> impl Filter + Clone { 123 | let f = warp::any().map(move || secret.clone()); 124 | 125 | warp::any() 126 | .and(f) 127 | .and(warp::header::optional("X-Hook-Signature")) 128 | .and(warp::body::bytes()) 129 | .and_then(|secret: Option, signature: Option, body: bytes::Bytes| async move { 130 | match secret { 131 | Some(secret) => match signature { 132 | Some(signature) => { 133 | let mut hmac = HmacSha512::new_from_slice(secret.as_bytes()) 134 | .map_err(|_| warp::reject::custom(WebErrors::BadSecret))?; 135 | hmac.update(&body); 136 | let hmac = hmac.finalize(); 137 | let bytes = hmac.into_bytes(); 138 | if format!("{:x}", bytes) == signature { 139 | Ok(body) 140 | } else { 141 | Err(warp::reject::custom(WebErrors::BadSignature)) 142 | } 143 | } 144 | None => Err(warp::reject::custom(WebErrors::MissingSignature)), 145 | }, 146 | None => Ok(body) 147 | } 148 | }) 149 | .and_then(|body: bytes::Bytes| async move { 150 | serde_json::from_slice::(&body).map_err(|_| { 151 | warp::reject::custom(WebErrors::BadFormat) 152 | } 153 | ) 154 | }) 155 | } 156 | 157 | async fn handle_rejection(err: warp::Rejection) -> Result { 158 | let (code, message) = match err.find::() { 159 | Some(WebErrors::BadFormat) => (StatusCode::BAD_REQUEST, "Bad Content"), 160 | Some(WebErrors::BadSecret) => (StatusCode::INTERNAL_SERVER_ERROR, "Bad Secret"), 161 | Some(WebErrors::BadSignature) => (StatusCode::FORBIDDEN, "Bad Signature"), 162 | Some(WebErrors::MissingSignature) => (StatusCode::BAD_REQUEST, "Missing Signature"), 163 | None => (StatusCode::INTERNAL_SERVER_ERROR, "UNHANDLED_REJECTION"), 164 | }; 165 | 166 | let json = warp::reply::json(&ErrorMessage { 167 | code: code.as_u16(), 168 | message: message.into(), 169 | }); 170 | 171 | warn!("Request error {:?} - {}", code, message); 172 | 173 | Ok(warp::reply::with_status(json, code)) 174 | } 175 | 176 | #[cfg(test)] 177 | mod tests { 178 | use chrono::{TimeZone, Utc}; 179 | 180 | use crate::server::webhook::NetboxWebHookEvent; 181 | 182 | use super::*; 183 | 184 | const PAYLOAD: &str = r#"{ 185 | "event": "created", 186 | "timestamp": "2021-03-09 17:55:33+00:00", 187 | "model": "prefix", 188 | "username": "jstretch", 189 | "request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a", 190 | "data": {}, 191 | "snapshots": { 192 | "prechange": null, 193 | "postchange": {} 194 | } 195 | }"#; 196 | 197 | #[tokio::test] 198 | async fn netbox_webhook_body_wo_secret() { 199 | let filter = netbox_webhook_body(None); 200 | 201 | // Execute `sum` and get the `Extract` back. 202 | let res = warp::test::request() 203 | .body(PAYLOAD) 204 | .filter(&filter) 205 | .await; 206 | 207 | assert!(res.is_ok()); 208 | assert_eq!(res.unwrap().username, "jstretch"); 209 | } 210 | 211 | #[tokio::test] 212 | async fn netbox_webhook_body_rejects_wo_signature() { 213 | let filter = netbox_webhook_body(Some(String::from("SECRET"))); 214 | 215 | // Execute `sum` and get the `Extract` back. 216 | let res = warp::test::request() 217 | .body(PAYLOAD) 218 | .filter(&filter) 219 | .await; 220 | 221 | assert!(res.is_err()); 222 | assert_eq!(res.unwrap_err().find::(), Some(&WebErrors::MissingSignature)); 223 | } 224 | 225 | #[tokio::test] 226 | async fn netbox_webhook_body_rejects_w_invalid_signature() { 227 | let filter = netbox_webhook_body(Some(String::from("SECRET"))); 228 | 229 | // Execute `sum` and get the `Extract` back. 230 | let res = warp::test::request() 231 | .header("X-Hook-Signature", "SEGNATURE") 232 | .body(PAYLOAD) 233 | .filter(&filter) 234 | .await; 235 | 236 | assert!(res.is_err()); 237 | assert_eq!(res.unwrap_err().find::(), Some(&WebErrors::BadSignature)); 238 | } 239 | 240 | #[tokio::test] 241 | async fn netbox_webhook_body_rejects_w_valid_signature() { 242 | let filter = netbox_webhook_body(Some(String::from("SECRET"))); 243 | 244 | // Execute `sum` and get the `Extract` back. 245 | let res = warp::test::request() 246 | .method("POST") 247 | .path("/webhook") 248 | .header("X-Hook-Signature", "5ccf9ac371fafa61922c0c5bfbfe6882542eac8ae1b8c26ccf13a3a46108859026cc804c2b211c63c8f9918f9f79f85bbd4fc1f300fc623789264e7650e9d6f2") 249 | .body(PAYLOAD) 250 | .filter(&filter) 251 | .await 252 | .unwrap(); 253 | 254 | assert_eq!(res, NetboxWebHook{ 255 | event: NetboxWebHookEvent::Created, 256 | timestamp: Utc.with_ymd_and_hms(2021, 3, 9, 17, 55, 33).unwrap(), 257 | model: String::from("prefix"), 258 | username: String::from("jstretch"), 259 | request_id: String::from("fdbca812-3142-4783-b364-2e2bd5c16c6a"), 260 | data: serde_json::Map::new() }); 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/sync/windhcp/mod.rs: -------------------------------------------------------------------------------- 1 | use log::debug; 2 | use std::net::Ipv4Addr; 3 | #[cfg(feature = "rpc_free")] 4 | use std::os::raw::c_void; 5 | use std::ptr; 6 | use std::sync::atomic::{AtomicUsize, Ordering}; 7 | use windows::core::{HSTRING, PWSTR}; 8 | use windows::Win32::NetworkManagement::Dhcp::*; 9 | 10 | static GLOBAL_DHCP_INIT_COUNT: AtomicUsize = AtomicUsize::new(0); 11 | 12 | pub mod subnet; 13 | pub use subnet::*; 14 | pub mod error; 15 | pub use error::*; 16 | 17 | #[derive(Debug, PartialEq)] 18 | pub struct WinDhcp { 19 | serveripaddress: HSTRING, 20 | } 21 | 22 | impl WinDhcp { 23 | pub fn new(server: &str) -> Self { 24 | let serveripaddress: HSTRING = HSTRING::from(server); 25 | if GLOBAL_DHCP_INIT_COUNT.fetch_add(1, Ordering::SeqCst) == 0 { 26 | let mut version: u32 = 0; 27 | let ret = unsafe { DhcpCApiInitialize(&mut version) }; 28 | 29 | debug!("Init WinDhcp Api v{} [{}]", version, ret); 30 | } 31 | 32 | WinDhcp { serveripaddress } 33 | } 34 | 35 | pub fn get_version(&self) -> WinDhcpResult<(u32, u32)> { 36 | let mut major: u32 = 0; 37 | let mut minor: u32 = 0; 38 | 39 | match unsafe { DhcpGetVersion(&self.serveripaddress, &mut major, &mut minor) } { 40 | 0 => Ok((major, minor)), 41 | e => Err(WinDhcpError::new("getting version", e)), 42 | } 43 | } 44 | 45 | pub fn get_subnets(&self) -> WinDhcpResult> { 46 | let mut resumehandle: u32 = 0; 47 | let mut elementsread: u32 = 0; 48 | let mut elementstotal: u32 = 0; 49 | 50 | let mut enuminfo: *mut DHCP_IP_ARRAY = ptr::null_mut(); 51 | 52 | match unsafe { 53 | DhcpEnumSubnets( 54 | &self.serveripaddress, 55 | &mut resumehandle, 56 | 0xFFFFFFFF, 57 | &mut enuminfo, 58 | &mut elementsread, 59 | &mut elementstotal, 60 | ) 61 | } { 62 | 0 => (), 63 | //ERROR_NO_MORE_ITEMS 64 | 259 => { 65 | return Ok(Vec::new()); 66 | } 67 | e => { 68 | return Err(WinDhcpError::new("listing subnets", e)); 69 | } 70 | } 71 | 72 | let data: DHCP_IP_ARRAY = unsafe { *enuminfo }; 73 | 74 | let mut subnets = Vec::with_capacity(data.NumElements.try_into().unwrap()); 75 | 76 | for idx in 0..data.NumElements { 77 | subnets.push(Ipv4Addr::from(unsafe { *data.Elements.offset(idx.try_into().unwrap()) })); 78 | } 79 | 80 | #[cfg(feature = "rpc_free")] 81 | unsafe { 82 | DhcpRpcFreeMemory((*enuminfo).Elements as *mut c_void); 83 | DhcpRpcFreeMemory(enuminfo as *mut c_void); 84 | }; 85 | 86 | Ok(subnets) 87 | } 88 | 89 | pub fn get_or_create_subnet( 90 | &self, 91 | subnetaddress: &Ipv4Addr, 92 | subnetmask: &Ipv4Addr, 93 | ) -> Result { 94 | match Subnet::get(&self.serveripaddress, subnetaddress)? { 95 | Some(subnet) => Ok(subnet), 96 | None => Subnet::create(&self.serveripaddress, subnetaddress, subnetmask), 97 | } 98 | } 99 | 100 | pub fn get_subnet( 101 | &self, 102 | subnetaddress: &Ipv4Addr, 103 | ) -> Result, u32> { 104 | Subnet::get(&self.serveripaddress, subnetaddress) 105 | } 106 | 107 | pub fn remove_subnet(&self, subnetaddress: Ipv4Addr) -> WinDhcpResult<()> { 108 | match unsafe { 109 | DhcpDeleteSubnet( 110 | &self.serveripaddress, 111 | u32::from(subnetaddress), 112 | DhcpFullForce, 113 | ) 114 | } { 115 | 0 => Ok(()), 116 | e => Err(WinDhcpError::new("removing subnet", e)), 117 | } 118 | } 119 | 120 | pub fn get_client_name(&self, clientip: Ipv4Addr) -> Result { 121 | let mut clientinfo: *mut DHCP_CLIENT_INFO_V4 = ptr::null_mut(); 122 | 123 | let searchinfo = DHCP_SEARCH_INFO { 124 | SearchType: DHCP_SEARCH_INFO_TYPE(0), 125 | SearchInfo: DHCP_SEARCH_INFO_0 { ClientIpAddress: u32::from(clientip) }, 126 | }; 127 | match unsafe { DhcpGetClientInfoV4(&self.serveripaddress, &searchinfo, &mut clientinfo) } { 128 | 0 => (), 129 | n => { 130 | return Err(n); 131 | } 132 | } 133 | 134 | let info = unsafe { *clientinfo }; 135 | 136 | let name = match info.ClientName.is_null() { 137 | true => String::from(""), 138 | false => unsafe { info.ClientName.to_string() }.unwrap_or_default(), 139 | }; 140 | 141 | #[cfg(feature = "rpc_free")] 142 | unsafe { 143 | DhcpRpcFreeMemory((*clientinfo).ClientName.as_ptr() as *mut c_void); 144 | DhcpRpcFreeMemory((*clientinfo).ClientComment.as_ptr() as *mut c_void); 145 | DhcpRpcFreeMemory((*clientinfo).ClientHardwareAddress.Data as *mut c_void); 146 | DhcpRpcFreeMemory((*clientinfo).OwnerHost.HostName.as_ptr() as *mut c_void); 147 | DhcpRpcFreeMemory((*clientinfo).OwnerHost.NetBiosName.as_ptr() as *mut c_void); 148 | DhcpRpcFreeMemory(clientinfo as *mut c_void); 149 | }; 150 | 151 | Ok(name) 152 | } 153 | 154 | pub fn set_client_name(&self, clientip: Ipv4Addr, name: &str) -> WinDhcpResult<()> { 155 | let mut clientinfo: *mut DHCP_CLIENT_INFO_V4 = ptr::null_mut(); 156 | 157 | let searchinfo = DHCP_SEARCH_INFO { 158 | SearchType: DHCP_SEARCH_INFO_TYPE(0), 159 | SearchInfo: DHCP_SEARCH_INFO_0 { ClientIpAddress: u32::from(clientip) }, 160 | }; 161 | match unsafe { DhcpGetClientInfoV4(&self.serveripaddress, &searchinfo, &mut clientinfo) } { 162 | 0 => (), 163 | e => return Err(WinDhcpError::new("setting client name", e)), 164 | } 165 | 166 | let mut info = unsafe { *clientinfo }; 167 | 168 | let mut wname = name.encode_utf16().chain([0u16]).collect::>(); 169 | info.ClientName = PWSTR(wname.as_mut_ptr()); 170 | 171 | let ret = match unsafe { DhcpSetClientInfoV4(&self.serveripaddress, &info) } { 172 | 0 => Ok(()), 173 | e => Err(WinDhcpError::new("setting client name", e)), 174 | }; 175 | 176 | #[cfg(feature = "rpc_free")] 177 | unsafe { 178 | DhcpRpcFreeMemory((*clientinfo).ClientName.as_ptr() as *mut c_void); 179 | DhcpRpcFreeMemory((*clientinfo).ClientComment.as_ptr() as *mut c_void); 180 | DhcpRpcFreeMemory((*clientinfo).ClientHardwareAddress.Data as *mut c_void); 181 | DhcpRpcFreeMemory((*clientinfo).OwnerHost.HostName.as_ptr() as *mut c_void); 182 | DhcpRpcFreeMemory((*clientinfo).OwnerHost.NetBiosName.as_ptr() as *mut c_void); 183 | DhcpRpcFreeMemory(clientinfo as *mut c_void); 184 | }; 185 | 186 | ret 187 | } 188 | 189 | pub fn get_client_comment(&self, clientip: Ipv4Addr) -> Result { 190 | let mut clientinfo: *mut DHCP_CLIENT_INFO_V4 = ptr::null_mut(); 191 | 192 | let searchinfo = DHCP_SEARCH_INFO { 193 | SearchType: DHCP_SEARCH_INFO_TYPE(0), 194 | SearchInfo: DHCP_SEARCH_INFO_0 { ClientIpAddress: u32::from(clientip) }, 195 | }; 196 | match unsafe { DhcpGetClientInfoV4(&self.serveripaddress, &searchinfo, &mut clientinfo) } { 197 | 0 => (), 198 | n => { 199 | return Err(n); 200 | } 201 | } 202 | 203 | let info = unsafe { *clientinfo }; 204 | 205 | let name = match info.ClientComment.is_null() { 206 | true => String::from(""), 207 | false => unsafe { info.ClientComment.to_string() }.unwrap_or_default(), 208 | }; 209 | 210 | #[cfg(feature = "rpc_free")] 211 | unsafe { 212 | DhcpRpcFreeMemory((*clientinfo).ClientName.as_ptr() as *mut c_void); 213 | DhcpRpcFreeMemory((*clientinfo).ClientComment.as_ptr() as *mut c_void); 214 | DhcpRpcFreeMemory((*clientinfo).ClientHardwareAddress.Data as *mut c_void); 215 | DhcpRpcFreeMemory((*clientinfo).OwnerHost.HostName.as_ptr() as *mut c_void); 216 | DhcpRpcFreeMemory((*clientinfo).OwnerHost.NetBiosName.as_ptr() as *mut c_void); 217 | DhcpRpcFreeMemory(clientinfo as *mut c_void); 218 | }; 219 | 220 | Ok(name) 221 | } 222 | 223 | pub fn set_client_comment(&self, clientip: Ipv4Addr, comment: &str) -> WinDhcpResult<()> { 224 | let mut clientinfo: *mut DHCP_CLIENT_INFO_V4 = ptr::null_mut(); 225 | 226 | let searchinfo = DHCP_SEARCH_INFO { 227 | SearchType: DHCP_SEARCH_INFO_TYPE(0), 228 | SearchInfo: DHCP_SEARCH_INFO_0 { ClientIpAddress: u32::from(clientip) }, 229 | }; 230 | match unsafe { DhcpGetClientInfoV4(&self.serveripaddress, &searchinfo, &mut clientinfo) } { 231 | 0 => (), 232 | e => return Err(WinDhcpError::new("setting client name", e)), 233 | } 234 | 235 | let mut info = unsafe { *clientinfo }; 236 | 237 | let mut wcomment = comment.encode_utf16().chain([0u16]).collect::>(); 238 | info.ClientComment = PWSTR(wcomment.as_mut_ptr()); 239 | 240 | let ret = match unsafe { DhcpSetClientInfoV4(&self.serveripaddress, &info) } { 241 | 0 => Ok(()), 242 | e => Err(WinDhcpError::new("setting client name", e)), 243 | }; 244 | 245 | #[cfg(feature = "rpc_free")] 246 | unsafe { 247 | DhcpRpcFreeMemory((*clientinfo).ClientName.as_ptr() as *mut c_void); 248 | DhcpRpcFreeMemory((*clientinfo).ClientComment.as_ptr() as *mut c_void); 249 | DhcpRpcFreeMemory((*clientinfo).ClientHardwareAddress.Data as *mut c_void); 250 | DhcpRpcFreeMemory((*clientinfo).OwnerHost.HostName.as_ptr() as *mut c_void); 251 | DhcpRpcFreeMemory((*clientinfo).OwnerHost.NetBiosName.as_ptr() as *mut c_void); 252 | DhcpRpcFreeMemory(clientinfo as *mut c_void); 253 | }; 254 | 255 | ret 256 | } 257 | } 258 | 259 | impl Drop for WinDhcp { 260 | fn drop(&mut self) { 261 | if GLOBAL_DHCP_INIT_COUNT.fetch_sub(1, Ordering::SeqCst) == 1 { 262 | unsafe { DhcpCApiCleanup() }; 263 | debug!("DhcpCApiCleanup"); 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/sync/windhcp/subnet/options.rs: -------------------------------------------------------------------------------- 1 | use std::{net::Ipv4Addr, ptr}; 2 | #[cfg(feature = "rpc_free")] 3 | use std::os::raw::c_void; 4 | 5 | use windows::{ 6 | core::{PCWSTR, PWSTR}, 7 | Win32::NetworkManagement::Dhcp::*, 8 | }; 9 | 10 | use crate::sync::windhcp::{WinDhcpError, WinDhcpResult}; 11 | 12 | use super::Subnet; 13 | 14 | pub trait SubnetOptions { 15 | fn get_options(&self, optionid: u32) -> WinDhcpResult>; 16 | fn set_options(&self, optionid: u32, values: &[T]) -> WinDhcpResult<()>; 17 | } 18 | pub trait SubnetOption: SubnetOptions { 19 | fn get_option(&self, optionid: u32) -> WinDhcpResult> { 20 | let mut values: Vec = self.get_options(optionid)?; 21 | match values.len() { 22 | 0 => Ok(None), 23 | 1 => Ok(Some(values.remove(0))), 24 | _ => Err(WinDhcpError::new("multiple values for option only expecting one", 0)), 25 | } 26 | } 27 | 28 | fn set_option(&self, optionid: u32, value: Option<&T>) -> WinDhcpResult<()> { 29 | match value { 30 | Some(value) => { 31 | self.set_options(optionid, &[value.clone()]) 32 | }, 33 | None => self.set_options(optionid, &[]), 34 | } 35 | } 36 | } 37 | 38 | impl SubnetOption for Subnet {} 39 | impl SubnetOptions for Subnet { 40 | fn get_options(&self, optionid: u32) -> WinDhcpResult> { 41 | let mut optionvalue: *mut DHCP_OPTION_VALUE = ptr::null_mut(); 42 | 43 | let mut scopeinfo = DHCP_OPTION_SCOPE_INFO { 44 | ScopeType: DhcpSubnetOptions, 45 | ScopeInfo: DHCP_OPTION_SCOPE_INFO_0 { SubnetScopeInfo: self.subnetaddress }, 46 | }; 47 | 48 | match unsafe { 49 | DhcpGetOptionValueV5( 50 | &self.serveripaddress, 51 | 0x00, 52 | optionid, 53 | PCWSTR::null(), 54 | PCWSTR::null(), 55 | &mut scopeinfo, 56 | &mut optionvalue, 57 | ) 58 | } { 59 | 0 => (), 60 | 2 => return Ok(Vec::new()), 61 | e => return Err(WinDhcpError::new("getting option", e)), 62 | } 63 | 64 | let len = unsafe { (*optionvalue).Value.NumElements }; 65 | 66 | let mut values = Vec::with_capacity(len as usize); 67 | 68 | for idx in 0..len { 69 | let element = unsafe{ (*optionvalue).Value.Elements.offset(idx.try_into().unwrap()) }; 70 | let value = unsafe{ (*element).Element.IpAddressOption }; 71 | values.push(value); 72 | #[cfg(feature = "rpc_free")] 73 | unsafe { DhcpRpcFreeMemory(element as *mut c_void) }; 74 | } 75 | 76 | #[cfg(feature = "rpc_free")] 77 | unsafe { DhcpRpcFreeMemory(optionvalue as *mut c_void) }; 78 | 79 | Ok(values) 80 | } 81 | 82 | fn set_options(&self, optionid: u32, set_values: &[u32]) -> WinDhcpResult<()> { 83 | if set_values.is_empty() { 84 | return self.remove_option(optionid).map_err(|e| 85 | WinDhcpError::new("removing option", e) 86 | ); 87 | } 88 | 89 | let mut scopeinfo = DHCP_OPTION_SCOPE_INFO { 90 | ScopeType: DhcpSubnetOptions, 91 | ScopeInfo: DHCP_OPTION_SCOPE_INFO_0 { SubnetScopeInfo: self.subnetaddress }, 92 | }; 93 | 94 | let mut values = set_values.iter().map( |i| 95 | DHCP_OPTION_DATA_ELEMENT { 96 | OptionType: DhcpDWordOption, 97 | Element: DHCP_OPTION_DATA_ELEMENT_0 { 98 | IpAddressOption: *i, 99 | }, 100 | } 101 | ).collect::>(); 102 | 103 | let mut optionvalue = DHCP_OPTION_DATA { 104 | NumElements: values.len() as u32, 105 | Elements: values.as_mut_ptr(), 106 | }; 107 | 108 | match unsafe { 109 | DhcpSetOptionValueV5( 110 | &self.serveripaddress, 111 | 0x00, 112 | optionid, 113 | PCWSTR::null(), 114 | PCWSTR::null(), 115 | &mut scopeinfo, 116 | &mut optionvalue, 117 | ) 118 | } { 119 | 0 => Ok(()), 120 | e => Err(WinDhcpError::new("setting option", e)), 121 | } 122 | } 123 | } 124 | 125 | impl SubnetOptions for Subnet { 126 | fn get_options(&self, optionid: u32) -> WinDhcpResult> { 127 | let mut optionvalue: *mut DHCP_OPTION_VALUE = ptr::null_mut(); 128 | 129 | let mut scopeinfo = DHCP_OPTION_SCOPE_INFO { 130 | ScopeType: DhcpSubnetOptions, 131 | ScopeInfo: DHCP_OPTION_SCOPE_INFO_0 { SubnetScopeInfo: self.subnetaddress }, 132 | }; 133 | 134 | match unsafe { 135 | DhcpGetOptionValueV5( 136 | &self.serveripaddress, 137 | 0x00, 138 | optionid, 139 | PCWSTR::null(), 140 | PCWSTR::null(), 141 | &mut scopeinfo, 142 | &mut optionvalue, 143 | ) 144 | } { 145 | 0 => (), 146 | 2 => return Ok(Vec::new()), 147 | e => return Err(WinDhcpError::new("getting option", e)), 148 | } 149 | 150 | let len = unsafe { (*optionvalue).Value.NumElements }; 151 | 152 | let mut ips = Vec::with_capacity(len as usize); 153 | 154 | for idx in 0..len { 155 | let element = unsafe{ (*optionvalue).Value.Elements.offset(idx.try_into().unwrap()) }; 156 | let value = unsafe{ (*element).Element.IpAddressOption }; 157 | ips.push(Ipv4Addr::from(value)); 158 | } 159 | 160 | #[cfg(feature = "rpc_free")] 161 | unsafe { DhcpRpcFreeMemory(optionvalue as *mut c_void) }; 162 | 163 | Ok(ips) 164 | } 165 | 166 | fn set_options(&self, optionid: u32, set_values: &[Ipv4Addr]) -> WinDhcpResult<()> { 167 | if set_values.is_empty() { 168 | return self.remove_option(optionid).map_err(|e| 169 | WinDhcpError::new("removing option", e) 170 | ); 171 | } 172 | 173 | let mut scopeinfo = DHCP_OPTION_SCOPE_INFO { 174 | ScopeType: DhcpSubnetOptions, 175 | ScopeInfo: DHCP_OPTION_SCOPE_INFO_0 { SubnetScopeInfo: self.subnetaddress }, 176 | }; 177 | 178 | let mut values = set_values.iter().map( |i| 179 | DHCP_OPTION_DATA_ELEMENT { 180 | OptionType: DhcpIpAddressOption, 181 | Element: DHCP_OPTION_DATA_ELEMENT_0 { 182 | IpAddressOption: u32::from(*i), 183 | }, 184 | } 185 | ).collect::>(); 186 | 187 | let mut optionvalue = DHCP_OPTION_DATA { 188 | NumElements: values.len() as u32, 189 | Elements: values.as_mut_ptr(), 190 | }; 191 | 192 | match unsafe { 193 | DhcpSetOptionValueV5( 194 | &self.serveripaddress, 195 | 0x00, 196 | optionid, 197 | PCWSTR::null(), 198 | PCWSTR::null(), 199 | &mut scopeinfo, 200 | &mut optionvalue, 201 | ) 202 | } { 203 | 0 => Ok(()), 204 | e => Err(WinDhcpError::new("setting option", e)), 205 | } 206 | } 207 | } 208 | 209 | impl SubnetOption for Subnet {} 210 | impl SubnetOptions for Subnet { 211 | fn get_options(&self, optionid: u32) -> WinDhcpResult> { 212 | let mut optionvalue: *mut DHCP_OPTION_VALUE = ptr::null_mut(); 213 | 214 | let mut scopeinfo = DHCP_OPTION_SCOPE_INFO { 215 | ScopeType: DhcpSubnetOptions, 216 | ScopeInfo: DHCP_OPTION_SCOPE_INFO_0 { SubnetScopeInfo: self.subnetaddress }, 217 | }; 218 | 219 | match unsafe { 220 | DhcpGetOptionValueV5( 221 | &self.serveripaddress, 222 | 0x00, 223 | optionid, 224 | PCWSTR::null(), 225 | PCWSTR::null(), 226 | &mut scopeinfo, 227 | &mut optionvalue, 228 | ) 229 | } { 230 | 0 => (), 231 | 2 => return Ok(Vec::new()), 232 | e => return Err(WinDhcpError::new("getting option", e)), 233 | } 234 | 235 | let len = unsafe { (*optionvalue).Value.NumElements }; 236 | 237 | let mut strings = Vec::with_capacity(len as usize); 238 | 239 | for idx in 0..len { 240 | let element = unsafe{ (*optionvalue).Value.Elements.offset(idx.try_into().unwrap()) }; 241 | let value = unsafe{ (*element).Element.StringDataOption.to_string().unwrap_or_default() }.clone(); 242 | strings.push(value); 243 | #[cfg(feature = "rpc_free")] 244 | unsafe { DhcpRpcFreeMemory(element as *mut c_void) }; 245 | } 246 | 247 | #[cfg(feature = "rpc_free")] 248 | unsafe { DhcpRpcFreeMemory(optionvalue as *mut c_void) }; 249 | 250 | Ok(strings) 251 | } 252 | 253 | fn set_options(&self, optionid: u32, set_values: &[String]) -> WinDhcpResult<()> { 254 | if set_values.is_empty() { 255 | return self.remove_option(optionid).map_err(|e| 256 | WinDhcpError::new("removing option", e) 257 | ); 258 | } 259 | 260 | let mut scopeinfo = DHCP_OPTION_SCOPE_INFO { 261 | ScopeType: DhcpSubnetOptions, 262 | ScopeInfo: DHCP_OPTION_SCOPE_INFO_0 { SubnetScopeInfo: self.subnetaddress }, 263 | }; 264 | 265 | let mut set_values_u16 = set_values.iter() 266 | .map(|s|s.encode_utf16().chain([0u16]).collect::>()) 267 | .collect::>>(); 268 | 269 | let mut values = set_values_u16.iter_mut().map( |i| 270 | DHCP_OPTION_DATA_ELEMENT { 271 | OptionType: DhcpStringDataOption, 272 | Element: DHCP_OPTION_DATA_ELEMENT_0 { 273 | StringDataOption: PWSTR(i.as_mut_ptr()), 274 | }, 275 | } 276 | ).collect::>(); 277 | 278 | let mut optionvalue = DHCP_OPTION_DATA { 279 | NumElements: values.len() as u32, 280 | Elements: values.as_mut_ptr(), 281 | }; 282 | 283 | match unsafe { 284 | DhcpSetOptionValueV5( 285 | &self.serveripaddress, 286 | 0x00, 287 | optionid, 288 | PCWSTR::null(), 289 | PCWSTR::null(), 290 | &mut scopeinfo, 291 | &mut optionvalue, 292 | ) 293 | } { 294 | 0 => Ok(()), 295 | e => Err(WinDhcpError::new("setting option", e)), 296 | } 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 Marius Rieder 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /wix/LICENSE.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1348\cocoasubrtf170 2 | {\fonttbl\f0\fmodern\fcharset0 Courier;} 3 | {\colortbl;\red255\green255\blue255;} 4 | \margl1440\margr1440\vieww22940\viewh8400\viewkind0 5 | \deftab720 6 | \pard\pardeftab720 7 | 8 | \f0\fs26 \cf0 \expnd0\expndtw0\kerning0 9 | \ 10 | Apache License\ 11 | Version 2.0, January 2004\ 12 | http://www.apache.org/licenses/\ 13 | \ 14 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\ 15 | \ 16 | 1. Definitions.\ 17 | \ 18 | "License" shall mean the terms and conditions for use, reproduction,\ 19 | and distribution as defined by Sections 1 through 9 of this document.\ 20 | \ 21 | "Licensor" shall mean the copyright owner or entity authorized by\ 22 | the copyright owner that is granting the License.\ 23 | \ 24 | "Legal Entity" shall mean the union of the acting entity and all\ 25 | other entities that control, are controlled by, or are under common\ 26 | control with that entity. For the purposes of this definition,\ 27 | "control" means (i) the power, direct or indirect, to cause the\ 28 | direction or management of such entity, whether by contract or\ 29 | otherwise, or (ii) ownership of fifty percent (50%) or more of the\ 30 | outstanding shares, or (iii) beneficial ownership of such entity.\ 31 | \ 32 | "You" (or "Your") shall mean an individual or Legal Entity\ 33 | exercising permissions granted by this License.\ 34 | \ 35 | "Source" form shall mean the preferred form for making modifications,\ 36 | including but not limited to software source code, documentation\ 37 | source, and configuration files.\ 38 | \ 39 | "Object" form shall mean any form resulting from mechanical\ 40 | transformation or translation of a Source form, including but\ 41 | not limited to compiled object code, generated documentation,\ 42 | and conversions to other media types.\ 43 | \ 44 | "Work" shall mean the work of authorship, whether in Source or\ 45 | Object form, made available under the License, as indicated by a\ 46 | copyright notice that is included in or attached to the work\ 47 | (an example is provided in the Appendix below).\ 48 | \ 49 | "Derivative Works" shall mean any work, whether in Source or Object\ 50 | form, that is based on (or derived from) the Work and for which the\ 51 | editorial revisions, annotations, elaborations, or other modifications\ 52 | represent, as a whole, an original work of authorship. For the purposes\ 53 | of this License, Derivative Works shall not include works that remain\ 54 | separable from, or merely link (or bind by name) to the interfaces of,\ 55 | the Work and Derivative Works thereof.\ 56 | \ 57 | "Contribution" shall mean any work of authorship, including\ 58 | the original version of the Work and any modifications or additions\ 59 | to that Work or Derivative Works thereof, that is intentionally\ 60 | submitted to Licensor for inclusion in the Work by the copyright owner\ 61 | or by an individual or Legal Entity authorized to submit on behalf of\ 62 | the copyright owner. For the purposes of this definition, "submitted"\ 63 | means any form of electronic, verbal, or written communication sent\ 64 | to the Licensor or its representatives, including but not limited to\ 65 | communication on electronic mailing lists, source code control systems,\ 66 | and issue tracking systems that are managed by, or on behalf of, the\ 67 | Licensor for the purpose of discussing and improving the Work, but\ 68 | excluding communication that is conspicuously marked or otherwise\ 69 | designated in writing by the copyright owner as "Not a Contribution."\ 70 | \ 71 | "Contributor" shall mean Licensor and any individual or Legal Entity\ 72 | on behalf of whom a Contribution has been received by Licensor and\ 73 | subsequently incorporated within the Work.\ 74 | \ 75 | 2. Grant of Copyright License. Subject to the terms and conditions of\ 76 | this License, each Contributor hereby grants to You a perpetual,\ 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ 78 | copyright license to reproduce, prepare Derivative Works of,\ 79 | publicly display, publicly perform, sublicense, and distribute the\ 80 | Work and such Derivative Works in Source or Object form.\ 81 | \ 82 | 3. Grant of Patent License. Subject to the terms and conditions of\ 83 | this License, each Contributor hereby grants to You a perpetual,\ 84 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ 85 | (except as stated in this section) patent license to make, have made,\ 86 | use, offer to sell, sell, import, and otherwise transfer the Work,\ 87 | where such license applies only to those patent claims licensable\ 88 | by such Contributor that are necessarily infringed by their\ 89 | Contribution(s) alone or by combination of their Contribution(s)\ 90 | with the Work to which such Contribution(s) was submitted. If You\ 91 | institute patent litigation against any entity (including a\ 92 | cross-claim or counterclaim in a lawsuit) alleging that the Work\ 93 | or a Contribution incorporated within the Work constitutes direct\ 94 | or contributory patent infringement, then any patent licenses\ 95 | granted to You under this License for that Work shall terminate\ 96 | as of the date such litigation is filed.\ 97 | \ 98 | 4. Redistribution. You may reproduce and distribute copies of the\ 99 | Work or Derivative Works thereof in any medium, with or without\ 100 | modifications, and in Source or Object form, provided that You\ 101 | meet the following conditions:\ 102 | \ 103 | (a) You must give any other recipients of the Work or\ 104 | Derivative Works a copy of this License; and\ 105 | \ 106 | (b) You must cause any modified files to carry prominent notices\ 107 | stating that You changed the files; and\ 108 | \ 109 | (c) You must retain, in the Source form of any Derivative Works\ 110 | that You distribute, all copyright, patent, trademark, and\ 111 | attribution notices from the Source form of the Work,\ 112 | excluding those notices that do not pertain to any part of\ 113 | the Derivative Works; and\ 114 | \ 115 | (d) If the Work includes a "NOTICE" text file as part of its\ 116 | distribution, then any Derivative Works that You distribute must\ 117 | include a readable copy of the attribution notices contained\ 118 | within such NOTICE file, excluding those notices that do not\ 119 | pertain to any part of the Derivative Works, in at least one\ 120 | of the following places: within a NOTICE text file distributed\ 121 | as part of the Derivative Works; within the Source form or\ 122 | documentation, if provided along with the Derivative Works; or,\ 123 | within a display generated by the Derivative Works, if and\ 124 | wherever such third-party notices normally appear. The contents\ 125 | of the NOTICE file are for informational purposes only and\ 126 | do not modify the License. You may add Your own attribution\ 127 | notices within Derivative Works that You distribute, alongside\ 128 | or as an addendum to the NOTICE text from the Work, provided\ 129 | that such additional attribution notices cannot be construed\ 130 | as modifying the License.\ 131 | \ 132 | You may add Your own copyright statement to Your modifications and\ 133 | may provide additional or different license terms and conditions\ 134 | for use, reproduction, or distribution of Your modifications, or\ 135 | for any such Derivative Works as a whole, provided Your use,\ 136 | reproduction, and distribution of the Work otherwise complies with\ 137 | the conditions stated in this License.\ 138 | \ 139 | 5. Submission of Contributions. Unless You explicitly state otherwise,\ 140 | any Contribution intentionally submitted for inclusion in the Work\ 141 | by You to the Licensor shall be under the terms and conditions of\ 142 | this License, without any additional terms or conditions.\ 143 | Notwithstanding the above, nothing herein shall supersede or modify\ 144 | the terms of any separate license agreement you may have executed\ 145 | with Licensor regarding such Contributions.\ 146 | \ 147 | 6. Trademarks. This License does not grant permission to use the trade\ 148 | names, trademarks, service marks, or product names of the Licensor,\ 149 | except as required for reasonable and customary use in describing the\ 150 | origin of the Work and reproducing the content of the NOTICE file.\ 151 | \ 152 | 7. Disclaimer of Warranty. Unless required by applicable law or\ 153 | agreed to in writing, Licensor provides the Work (and each\ 154 | Contributor provides its Contributions) on an "AS IS" BASIS,\ 155 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\ 156 | implied, including, without limitation, any warranties or conditions\ 157 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\ 158 | PARTICULAR PURPOSE. You are solely responsible for determining the\ 159 | appropriateness of using or redistributing the Work and assume any\ 160 | risks associated with Your exercise of permissions under this License.\ 161 | \ 162 | 8. Limitation of Liability. In no event and under no legal theory,\ 163 | whether in tort (including negligence), contract, or otherwise,\ 164 | unless required by applicable law (such as deliberate and grossly\ 165 | negligent acts) or agreed to in writing, shall any Contributor be\ 166 | liable to You for damages, including any direct, indirect, special,\ 167 | incidental, or consequential damages of any character arising as a\ 168 | result of this License or out of the use or inability to use the\ 169 | Work (including but not limited to damages for loss of goodwill,\ 170 | work stoppage, computer failure or malfunction, or any and all\ 171 | other commercial damages or losses), even if such Contributor\ 172 | has been advised of the possibility of such damages.\ 173 | \ 174 | 9. Accepting Warranty or Additional Liability. While redistributing\ 175 | the Work or Derivative Works thereof, You may choose to offer,\ 176 | and charge a fee for, acceptance of support, warranty, indemnity,\ 177 | or other liability obligations and/or rights consistent with this\ 178 | License. However, in accepting such obligations, You may act only\ 179 | on Your own behalf and on Your sole responsibility, not on behalf\ 180 | of any other Contributor, and only if You agree to indemnify,\ 181 | defend, and hold each Contributor harmless for any liability\ 182 | incurred by, or claims asserted against, such Contributor by reason\ 183 | of your accepting any such warranty or additional liability.\ 184 | \ 185 | END OF TERMS AND CONDITIONS\ 186 | \ 187 | APPENDIX: How to apply the Apache License to your work.\ 188 | \ 189 | To apply the Apache License to your work, attach the following\ 190 | boilerplate notice, with the fields enclosed by brackets "[]"\ 191 | replaced with your own identifying information. (Don't include\ 192 | the brackets!) The text should be enclosed in the appropriate\ 193 | comment syntax for the file format. We also recommend that a\ 194 | file or class name and description of purpose be included on the\ 195 | same "printed page" as the copyright notice for easier\ 196 | identification within third-party archives.\ 197 | \ 198 | Copyright 2022 Marius Rieder\ 199 | \ 200 | Licensed under the Apache License, Version 2.0 (the "License");\ 201 | you may not use this file except in compliance with the License.\ 202 | You may obtain a copy of the License at\ 203 | \ 204 | http://www.apache.org/licenses/LICENSE-2.0\ 205 | \ 206 | Unless required by applicable law or agreed to in writing, software\ 207 | distributed under the License is distributed on an "AS IS" BASIS,\ 208 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\ 209 | See the License for the specific language governing permissions and\ 210 | limitations under the License.} -------------------------------------------------------------------------------- /src/sync/mod.rs: -------------------------------------------------------------------------------- 1 | use std::net::Ipv4Addr; 2 | 3 | use log::{debug, info, warn}; 4 | 5 | pub mod config; 6 | use self::netbox::address::{AssignedObject, IpAddress}; 7 | use self::netbox::prefix::Prefix; 8 | use self::netbox::range::IpRange; 9 | use self::windhcp::reservation::Reservation; 10 | use self::{config::SyncConfig, netbox::NetboxApi}; 11 | mod mac; 12 | use self::mac::MacAddr; 13 | pub mod netbox; 14 | 15 | mod windhcp; 16 | use self::windhcp::{DnsFlags, Subnet, WinDhcp}; 17 | 18 | pub struct Sync { 19 | config: SyncConfig, 20 | netbox: NetboxApi, 21 | dhcp: WinDhcp, 22 | noop: bool, 23 | scope: Option, 24 | } 25 | 26 | impl Sync { 27 | pub fn new(config: SyncConfig, noop: bool, scope: Option) -> Self { 28 | let netbox = NetboxApi::new(&config.netbox); 29 | let dhcp = WinDhcp::new(config.dhcp.server()); 30 | 31 | Self { config, netbox, dhcp, noop, scope} 32 | } 33 | 34 | pub fn run(&self) -> Result<(), Box> { 35 | info!("Start sync from {} to {} ({} {})", self.config.netbox.apiurl(), self.config.dhcp.server(), env!("CARGO_PKG_NAME"), git_version::git_version!(prefix = "git:", cargo_prefix = "cargo:", fallback = "unknown")); 36 | 37 | let netbox_version = self.netbox.version()?; 38 | debug!("Netbox Version: {}", netbox_version); 39 | let dhcp_version = self.dhcp.get_version()?; 40 | debug!("Windows DHCp Server Version: {}.{}", dhcp_version.0, dhcp_version.1); 41 | 42 | let prefixes = self.netbox.get_prefixes()?; 43 | let ranges = self.netbox.get_ranges()?; 44 | info!("Found {} Prefixes and {} Ranges", prefixes.len(), ranges.len()); 45 | 46 | for prefix in prefixes.iter() { 47 | if let Some(scope) = self.scope { 48 | if !prefix.prefix().contains(&scope) { 49 | debug!("Skip Prefix {} - {}", prefix.prefix(), prefix.description()); 50 | continue; 51 | } 52 | } 53 | 54 | info!("Sync Prefix {} - {}", prefix.prefix(), prefix.description()); 55 | 56 | let range = match ranges.iter().find(|&r| r.is_contained(prefix)) { 57 | Some(r) => r, 58 | None => { 59 | warn!("Skip Prefix {} no range found", prefix.prefix()); 60 | continue; 61 | } 62 | }; 63 | let subnet = self.sync_subnetv4(prefix, range)?; 64 | 65 | /* Update Reservations */ 66 | let mut dhcp_reservations = subnet.get_reservations().unwrap(); 67 | let reservations = self.netbox.get_reservations_for_subnet(&prefix.prefix())?; 68 | info!(" Subnet {}: Found {} reservations", &prefix.addr(), reservations.len()); 69 | 70 | for reservation in reservations.iter() { 71 | self.sync_reservationv4(&subnet, reservation, dhcp_reservations.remove(&reservation.address()))?; 72 | } 73 | 74 | /* Cleanup old Reservations */ 75 | for (reservationaddress, macaddress) in dhcp_reservations { 76 | if !self.noop { subnet.remove_reservation(reservationaddress, &macaddress.for_client)?; } 77 | info!(" Reservation {}: Remove Reservation {}", &reservationaddress, &macaddress.for_client.as_mac()); 78 | } 79 | } 80 | 81 | /* Cleanup old Subnets */ 82 | let prefixes_ip: Vec = prefixes.iter().map(|i| i.addr()).collect(); 83 | for subnet in self.dhcp.get_subnets()? { 84 | if prefixes_ip.contains(&subnet) { 85 | continue; 86 | } 87 | 88 | let dhcp_subnet = self.dhcp.get_subnet(&subnet).unwrap().unwrap(); 89 | let failover = dhcp_subnet.get_failover_relationship().unwrap(); 90 | if let Some(failover) = failover { 91 | info!("Subnet {}: Remove from Failover Relation: {:?}", &subnet, &failover); 92 | if !self.noop { 93 | if let Err(e) = dhcp_subnet.remove_failover_relationship(&failover) { 94 | warn!("Removing {} from {} faild with error code: {}", dhcp_subnet.subnet_mask, &failover, e) 95 | } 96 | } 97 | } 98 | 99 | if !self.noop { self.dhcp.remove_subnet(subnet)?; } 100 | info!("Subnet {}: Removed", &subnet); 101 | } 102 | 103 | Ok(()) 104 | } 105 | 106 | fn sync_subnetv4( 107 | &self, 108 | prefix: &Prefix, 109 | range: &IpRange, 110 | ) -> Result> { 111 | let subnetaddress = &prefix.addr(); 112 | 113 | let subnet = self.dhcp.get_or_create_subnet(subnetaddress, &prefix.netmask()).unwrap(); 114 | debug!("Found: {} - {}", subnetaddress, subnet.subnet_name); 115 | 116 | /* Subnet Netmask */ 117 | if subnet.subnet_mask != prefix.netmask() { 118 | if !self.noop { subnet.set_mask(prefix.netmask())?; } 119 | info!(" Subnet {}: Updated netmask to {}", &subnetaddress, prefix.netmask()); 120 | } 121 | 122 | /* Subnet Name */ 123 | if subnet.subnet_name != prefix.description() { 124 | if !self.noop { subnet.set_name(prefix.description())?; } 125 | info!(" Subnet {}: Updated name to {}", &subnetaddress, prefix.description()); 126 | } 127 | 128 | /* Subnet Comment */ 129 | if subnet.subnet_comment != prefix.description() { 130 | if !self.noop { subnet.set_comment(prefix.description())?; } 131 | info!(" Subnet {}: Updated comment to {}", &subnetaddress, prefix.description()); 132 | } 133 | 134 | /* DHCP Range */ 135 | if (range.start_address(), range.end_address()) != subnet.get_subnet_range()? { 136 | if !self.noop { subnet.set_subnet_range(range.start_address(), range.end_address())?; } 137 | info!(" Subnet {}: Updated range to {}-{}", &subnetaddress, range.start_address(), range.end_address()); 138 | } 139 | 140 | /* Lease Duration */ 141 | let lease_duration = prefix.lease_duration() 142 | .or_else(|| Some(self.config.dhcp.lease_duration())); 143 | if lease_duration != subnet.get_lease_duration()? { 144 | if !self.noop { subnet.set_lease_duration(lease_duration)?; } 145 | info!(" Subnet {}: Updated lease duration to {}", &subnetaddress, lease_duration.unwrap_or_default()); 146 | } 147 | 148 | /* DNS Update */ 149 | let dns_flags = prefix.dns_flags() 150 | .map(DnsFlags::from).or_else(|| self.config.dhcp.default_dns_flags()); 151 | if dns_flags != subnet.get_dns_flags()? { 152 | if !self.noop { subnet.set_dns_flags(dns_flags.as_ref())?; } 153 | info!(" Subnet {}: Updated dns flags to {:?}", &subnetaddress, dns_flags); 154 | } 155 | 156 | /* Router */ 157 | let routers = match prefix.routers() { 158 | Some(ip) => ip, 159 | None => { 160 | let routers = self.netbox.get_router_for_subnet(&prefix.prefix())?; 161 | routers.iter().map(|i| i.address()).collect() 162 | } 163 | }; 164 | if routers != subnet.get_routers()? { 165 | if !self.noop { subnet.set_routers(&routers)?; } 166 | info!(" Subnet {}: Updated routers to {:?}", &subnetaddress, routers); 167 | } 168 | 169 | /* DNS Domain */ 170 | let dns_domain = prefix.dns_domain() 171 | .or_else(|| self.config.dhcp.default_dns_domain()); 172 | if dns_domain != subnet.get_dns_domain()?.as_ref() { 173 | if !self.noop { subnet.set_dns_domain(dns_domain)?; } 174 | info!(" Subnet {}: Updated dns domain to {:?}", &subnetaddress, dns_domain); 175 | } 176 | 177 | /* DNS Server */ 178 | let dns = prefix.dns_servers() 179 | .unwrap_or_else(|| self.config.dhcp.default_dns_servers().to_vec()); 180 | if dns != subnet.get_dns_servers()? { 181 | if !self.noop { subnet.set_dns_servers(&dns)?; } 182 | info!(" Subnet {}: Updated dns to {:?}", &subnetaddress, dns); 183 | } 184 | 185 | /* Failover */ 186 | let expected_failover = prefix.failover_relation() 187 | .or_else(|| self.config.dhcp.default_failover_relation()); 188 | let failover = subnet.get_failover_relationship().unwrap(); 189 | 190 | if failover != expected_failover.cloned() { 191 | 192 | if let Some(failover) = failover { 193 | info!(" Remove from Failover Relation: {:?}", &failover); 194 | if !self.noop { 195 | if let Err(e) = subnet.remove_failover_relationship(&failover) { 196 | warn!("Removing {} from {} faild with error code: {}", subnet.subnet_mask, &failover, e) 197 | } 198 | } 199 | } 200 | if let Some(expected_failover) = expected_failover { 201 | info!(" Add to Failover Relation: {:?}", &expected_failover); 202 | if !self.noop { 203 | if let Err(e) = subnet.add_failover_relationship(&expected_failover) { 204 | warn!("Adding {} to {} faild with error code: {}", subnet.subnet_mask, &expected_failover, e) 205 | } 206 | } 207 | } 208 | } 209 | 210 | Ok(subnet) 211 | } 212 | 213 | fn sync_reservationv4( 214 | &self, 215 | subnet: &Subnet, 216 | reservation: &IpAddress, 217 | dhcp_reservation: Option, 218 | ) -> Result<(), Box> { 219 | let mac = match self.get_macaddress_for_reservation(reservation)? { 220 | Some(mac) => mac, 221 | None => { 222 | warn!("Error no MAC address found for IP {}", &reservation.address()); 223 | return Ok(()); 224 | }, 225 | }; 226 | 227 | /* Reservation */ 228 | if let Some(macaddress) = dhcp_reservation.map(|r| r.for_client) { 229 | if macaddress != mac { 230 | if !self.noop { 231 | subnet.remove_reservation(reservation.address(), &macaddress)?; 232 | subnet.add_reservation(reservation.address(), &mac)?; 233 | } 234 | info!(" Reservation {}: Update Reservation {:?}", &reservation.address(), &mac.as_mac()); 235 | } 236 | } else { 237 | if !self.noop { subnet.add_reservation(reservation.address(), &mac)?; } 238 | 239 | info!(" Reservation {}: Create Reservation {:?}", &reservation.address(), &mac.as_mac()); 240 | } 241 | 242 | /* Client Name */ 243 | let name = self.dhcp.get_client_name(reservation.address()).unwrap_or_default(); 244 | if name != reservation.dns_name() { 245 | if !self.noop { self.dhcp.set_client_name(reservation.address(), reservation.dns_name())?; } 246 | info!(" Reservation {}: Set client name to {}", &reservation.address(), &reservation.dns_name()); 247 | } 248 | 249 | /* Client Comment */ 250 | let comment = self.dhcp.get_client_comment(reservation.address()).unwrap_or_default(); 251 | if comment != reservation.description() { 252 | if !self.noop { self.dhcp.set_client_comment(reservation.address(), reservation.description())?; } 253 | info!(" Reservation {}: Set client comment to {}", &reservation.address(), &reservation.description()); 254 | } 255 | 256 | Ok(()) 257 | } 258 | 259 | fn get_macaddress_for_reservation( 260 | &self, 261 | reservation: &IpAddress, 262 | ) -> Result>, Box> { 263 | let mac = match reservation.reservation_mac() { 264 | Some(mac) => Some(mac.clone()), 265 | None => match reservation.assigned_object_url() { 266 | Some(url) => self.get_macaddress_for_reservation_from_assigned_object(url)?, 267 | None => None, 268 | }, 269 | }; 270 | 271 | Ok(mac.map(|m| Vec::::from_mac(&m))) 272 | } 273 | 274 | fn get_macaddress_for_reservation_from_assigned_object(&self, url: &str) -> Result, Box> { 275 | Ok(self.netbox.get_object::(url)?.mac_address().cloned()) 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/sync/windhcp/subnet/mod.rs: -------------------------------------------------------------------------------- 1 | use ipnet::Ipv4Net; 2 | use log::{info, trace}; 3 | use serde::Deserialize; 4 | use std::{collections::HashMap, fmt, net::Ipv4Addr, ptr}; 5 | use windows::{ 6 | core::{HSTRING, PCWSTR, PWSTR}, 7 | Win32::NetworkManagement::Dhcp::*, 8 | }; 9 | 10 | use super::{WinDhcpError, WinDhcpResult}; 11 | 12 | mod options; 13 | use self::options::*; 14 | mod elements; 15 | use self::elements::*; 16 | pub mod reservation; 17 | use self::reservation::*; 18 | 19 | #[derive(Debug)] 20 | pub struct Subnet { 21 | serveripaddress: HSTRING, 22 | pub subnetaddress: u32, 23 | pub subnet_mask: Ipv4Addr, 24 | pub subnet_name: String, 25 | pub subnet_comment: String, 26 | } 27 | 28 | impl Subnet { 29 | 30 | fn get_subnet_info(&self) -> Result { 31 | let mut subnetinfo: *mut DHCP_SUBNET_INFO = ptr::null_mut(); 32 | 33 | match unsafe { 34 | trace!("Call DhcpGetSubnetInfo({}, {}, ptr)", &self.serveripaddress, self.subnetaddress); 35 | DhcpGetSubnetInfo(&self.serveripaddress, self.subnetaddress, &mut subnetinfo) 36 | } { 37 | 0 => Ok(unsafe{*subnetinfo}), 38 | n => Err(n), 39 | } 40 | } 41 | 42 | fn set_subnet_info(&self, subnetinfo: DHCP_SUBNET_INFO) -> Result<(), u32> { 43 | match unsafe { DhcpSetSubnetInfo(&self.serveripaddress, self.subnetaddress, &subnetinfo) } { 44 | 0 => Ok(()), 45 | n => Err(n), 46 | } 47 | } 48 | 49 | fn remove_option(&self, optionid: u32) -> Result<(), u32> { 50 | let mut scopeinfo = DHCP_OPTION_SCOPE_INFO { 51 | ScopeType: DhcpSubnetOptions, 52 | ScopeInfo: DHCP_OPTION_SCOPE_INFO_0 { SubnetScopeInfo: self.subnetaddress }, 53 | }; 54 | 55 | match unsafe { 56 | DhcpRemoveOptionValueV5( 57 | &self.serveripaddress, 58 | 0x00, 59 | optionid, 60 | PCWSTR::null(), 61 | PCWSTR::null(), 62 | &mut scopeinfo, 63 | ) 64 | } { 65 | 0 => Ok(()), 66 | n => Err(n), 67 | } 68 | } 69 | 70 | pub fn get(serveripaddress: &HSTRING, subnetaddress: &Ipv4Addr) -> Result, u32> { 71 | let mut subnetinfo: *mut DHCP_SUBNET_INFO = std::ptr::null_mut(); 72 | let subnetaddress = u32::from(*subnetaddress); 73 | 74 | let ret = match unsafe { DhcpGetSubnetInfo(serveripaddress, subnetaddress, &mut subnetinfo) } { 75 | 0 => { 76 | let subnet_name = match unsafe { (*subnetinfo).SubnetName }.is_null() { 77 | true => String::default(), 78 | false => unsafe { (*subnetinfo).SubnetName.to_string() }.unwrap_or_default(), 79 | }; 80 | let subnet_comment = match unsafe { (*subnetinfo).SubnetComment }.is_null() { 81 | true => String::default(), 82 | false => unsafe { (*subnetinfo).SubnetComment.to_string() }.unwrap_or_default(), 83 | }; 84 | 85 | Ok(Some(Self { 86 | serveripaddress: serveripaddress.clone(), 87 | subnetaddress, 88 | subnet_mask: Ipv4Addr::from(unsafe { *subnetinfo }.SubnetMask), 89 | subnet_name, 90 | subnet_comment, 91 | })) 92 | }, 93 | ERROR_DHCP_SUBNET_NOT_PRESENT => Ok(None), 94 | n => Err(n), 95 | }; 96 | 97 | #[cfg(feature = "rpc_free")] 98 | unsafe { DhcpRpcFreeMemory(subnetinfo as *mut c_void) }; 99 | 100 | ret 101 | } 102 | 103 | pub fn create( 104 | serveripaddress: &HSTRING, 105 | subnetaddress: &Ipv4Addr, 106 | subnetmask: &Ipv4Addr, 107 | ) -> Result { 108 | let subnetinfo = DHCP_SUBNET_INFO { 109 | SubnetAddress: u32::from(*subnetaddress), 110 | SubnetMask: u32::from(*subnetmask), 111 | SubnetName: PWSTR::null(), 112 | SubnetComment: PWSTR::null(), 113 | PrimaryHost: DHCP_HOST_INFO::default(), 114 | SubnetState: DhcpSubnetEnabled, 115 | }; 116 | 117 | match unsafe { DhcpCreateSubnet(serveripaddress, u32::from(*subnetaddress), &subnetinfo) } { 118 | 0 => match Self::get(serveripaddress, subnetaddress)? { 119 | Some(subnet) => Ok(subnet), 120 | None => Err(0) 121 | }, 122 | n => Err(n), 123 | } 124 | } 125 | 126 | pub fn set_mask(&self, subnetmask: Ipv4Addr) -> WinDhcpResult<()> { 127 | let mut subnetinfo = self.get_subnet_info() 128 | .map_err(|e| WinDhcpError::new("setting subnet mask", e))?; 129 | 130 | subnetinfo.SubnetMask = u32::from(subnetmask); 131 | self.set_subnet_info(subnetinfo) 132 | .map_err(|e| WinDhcpError::new("setting subnet mask", e))?; 133 | Ok(()) 134 | } 135 | 136 | pub fn set_name(&self, name: &str) -> WinDhcpResult<()> { 137 | let mut subnetinfo = self.get_subnet_info() 138 | .map_err(|e| WinDhcpError::new("setting subnet name", e))?; 139 | 140 | let mut wname = name.encode_utf16().chain([0u16]).collect::>(); 141 | subnetinfo.SubnetName = PWSTR(wname.as_mut_ptr()); 142 | 143 | self.set_subnet_info(subnetinfo) 144 | .map_err(|e| WinDhcpError::new("setting subnet name", e)) 145 | } 146 | 147 | pub fn set_comment(&self, comment: &str) -> WinDhcpResult<()> { 148 | let mut subnetinfo = self.get_subnet_info() 149 | .map_err(|e| WinDhcpError::new("setting subnet comment", e))?; 150 | 151 | let mut wcomment = comment.encode_utf16().chain([0u16]).collect::>(); 152 | subnetinfo.SubnetComment = PWSTR(wcomment.as_mut_ptr()); 153 | self.set_subnet_info(subnetinfo) 154 | .map_err(|e| WinDhcpError::new("setting subnet comment", e)) 155 | } 156 | 157 | pub fn get_subnet_range(&self) -> WinDhcpResult<(Ipv4Addr, Ipv4Addr)> { 158 | match SubnetElements::::get_first_element(self) { 159 | Ok(Some(range)) => Ok((Ipv4Addr::from(range.StartAddress), Ipv4Addr::from(range.EndAddress))), 160 | Ok(None) => Ok((Ipv4Addr::from(0), Ipv4Addr::from(0))), 161 | Err(e) => Err(WinDhcpError::new("getting subnet range", e)), 162 | } 163 | } 164 | 165 | pub fn set_subnet_range( 166 | &self, 167 | start_address: Ipv4Addr, 168 | end_address: Ipv4Addr, 169 | ) -> WinDhcpResult<()> { 170 | let start_address = u32::from(start_address); 171 | let end_address = u32::from(end_address); 172 | 173 | let mut range = match SubnetElements::::get_first_element(self) { 174 | Ok(Some(range)) => range, 175 | Ok(None) => DHCP_BOOTP_IP_RANGE { 176 | StartAddress: std::u32::MAX, 177 | EndAddress: 0u32, 178 | BootpAllocated: 0u32, 179 | MaxBootpAllowed: 0u32, 180 | }, 181 | Err(e) => return Err(WinDhcpError::new("getting subnet range", e)), 182 | }; 183 | 184 | range.StartAddress = std::cmp::max( 185 | std::cmp::min(range.StartAddress, start_address), 186 | self.get_range_min() 187 | ); 188 | 189 | range.EndAddress = std::cmp::min( 190 | std::cmp::max(range.EndAddress, end_address), 191 | self.get_range_max() 192 | ); 193 | info!("Set range to {} - {}", Ipv4Addr::from(range.StartAddress), Ipv4Addr::from(range.EndAddress)); 194 | 195 | self.add_element(&mut range) 196 | .map_err(|e| WinDhcpError::new("setting subnet range to ", e))?; 197 | 198 | range.StartAddress = start_address; 199 | range.EndAddress = end_address; 200 | info!("Set range to {} - {}", Ipv4Addr::from(range.StartAddress), Ipv4Addr::from(range.EndAddress)); 201 | 202 | self.add_element(&mut range) 203 | .map_err(|e| WinDhcpError::new("setting subnet range2", e))?; 204 | 205 | //unsafe { DhcpRpcFreeMemory(data as *mut c_void) }; 206 | 207 | Ok(()) 208 | } 209 | 210 | pub fn get_lease_duration(&self) -> WinDhcpResult> { 211 | self.get_option(OPTION_LEASE_TIME) 212 | } 213 | 214 | pub fn set_lease_duration(&self, lease_duration: Option) -> WinDhcpResult<()> { 215 | self.set_option(OPTION_LEASE_TIME, lease_duration.as_ref()) 216 | } 217 | 218 | pub fn get_dns_flags(&self) -> WinDhcpResult> { 219 | #[allow(clippy::redundant_closure)] 220 | Ok(self.get_option(81)?.map(|f: u32| DnsFlags::from(f))) 221 | } 222 | 223 | pub fn set_dns_flags(&self, dns_flags: Option<&DnsFlags>) -> WinDhcpResult<()> { 224 | self.set_option(81, dns_flags.map(u32::from).as_ref()) 225 | } 226 | 227 | pub fn get_routers(&self) -> WinDhcpResult> { 228 | self.get_options(OPTION_ROUTER_ADDRESS) 229 | } 230 | 231 | pub fn set_routers(&self, routers: &[Ipv4Addr]) -> WinDhcpResult<()> { 232 | self.set_options(OPTION_ROUTER_ADDRESS, routers) 233 | } 234 | 235 | pub fn get_dns_domain(&self) -> WinDhcpResult> { 236 | self.get_option(OPTION_DOMAIN_NAME) 237 | } 238 | 239 | pub fn set_dns_domain(&self, domain: Option<&String>) -> WinDhcpResult<()> { 240 | self.set_option(OPTION_DOMAIN_NAME, domain) 241 | } 242 | 243 | pub fn get_dns_servers(&self) -> WinDhcpResult> { 244 | self.get_options(OPTION_DOMAIN_NAME_SERVERS) 245 | } 246 | 247 | pub fn set_dns_servers(&self, servers: &[Ipv4Addr]) -> WinDhcpResult<()> { 248 | self.set_options(OPTION_DOMAIN_NAME_SERVERS, servers) 249 | } 250 | 251 | pub fn get_reservations(&self) -> Result, u32> { 252 | let reservations: Vec = self.get_elements()?; 253 | 254 | if reservations.is_empty() { return Ok(HashMap::new()); } 255 | 256 | let mut ret = HashMap::with_capacity(reservations.len()); 257 | 258 | for reservation in reservations.into_iter() { 259 | ret.insert(reservation.ip_address, reservation); 260 | } 261 | 262 | Ok(ret) 263 | } 264 | 265 | pub fn add_reservation( 266 | &self, 267 | reservationaddress: Ipv4Addr, 268 | macaddress: &[u8], 269 | ) -> WinDhcpResult<()> { 270 | let mut reservation = Reservation { 271 | ip_address: reservationaddress, 272 | for_client: macaddress.to_owned(), 273 | allowed_client_types: ReservationClientTypes::Both, 274 | }; 275 | match self.add_element(&mut reservation) { 276 | Ok(_) => Ok(()), 277 | Err(e) => Err(WinDhcpError::new("adding reservation", e)), 278 | } 279 | } 280 | 281 | pub fn remove_reservation(&self, reservationaddress: Ipv4Addr, macaddress: &[u8]) -> WinDhcpResult<()> { 282 | let mut reservation = Reservation { 283 | ip_address: reservationaddress, 284 | for_client: macaddress.to_owned(), 285 | allowed_client_types: ReservationClientTypes::Both, 286 | }; 287 | 288 | match self.remove_element(&mut reservation) { 289 | Ok(_) => Ok(()), 290 | Err(e) => Err(WinDhcpError::new("adding reservation", e)), 291 | } 292 | } 293 | 294 | fn get_range_min(&self) -> u32 { 295 | self.subnetaddress + 1 296 | } 297 | 298 | fn get_range_max(&self) -> u32 { 299 | let net = Ipv4Net::with_netmask(Ipv4Addr::from(self.subnetaddress), self.subnet_mask).expect("Unable to create net"); 300 | net.broadcast().into() 301 | } 302 | 303 | pub fn get_failover_relationship(&self) -> Result, u32> { 304 | let mut prelationship: *mut DHCP_FAILOVER_RELATIONSHIP = ptr::null_mut(); 305 | 306 | match unsafe { 307 | trace!("Call DhcpV4FailoverGetScopeRelationship({}, {}, ptr)", &self.serveripaddress, self.subnetaddress); 308 | DhcpV4FailoverGetScopeRelationship(&self.serveripaddress, self.subnetaddress, &mut prelationship) 309 | } { 310 | 0 => { 311 | let name : String = unsafe{(*prelationship).RelationshipName.to_string().unwrap()}; 312 | Ok(Some(name)) 313 | }, 314 | ERROR_DHCP_FO_SCOPE_NOT_IN_RELATIONSHIP => Ok(None), 315 | n => Err(n), 316 | } 317 | } 318 | 319 | pub fn add_failover_relationship(&self, name: &str) -> Result<(), u32> { 320 | let name: Vec = name.encode_utf16().chain([0u16]).collect::>(); 321 | 322 | let prelationship = DHCP_FAILOVER_RELATIONSHIP { 323 | PrimaryServer: 0, 324 | SecondaryServer: 0, 325 | Mode: windows::Win32::NetworkManagement::Dhcp::DHCP_FAILOVER_MODE(0), 326 | ServerType: windows::Win32::NetworkManagement::Dhcp::DHCP_FAILOVER_SERVER(0), 327 | State: windows::Win32::NetworkManagement::Dhcp::FSM_STATE(0), 328 | PrevState: windows::Win32::NetworkManagement::Dhcp::FSM_STATE(0), 329 | Mclt: 0, 330 | SafePeriod: 0, 331 | RelationshipName: PWSTR::from_raw(name.as_ptr() as *mut _), 332 | PrimaryServerName: PWSTR::null(), 333 | SecondaryServerName: PWSTR::null(), 334 | pScopes: &mut DHCP_IP_ARRAY { 335 | NumElements: 1, 336 | Elements: &mut self.subnetaddress.clone(), 337 | }, 338 | Percentage: 0, 339 | SharedSecret: PWSTR::null(), 340 | }; 341 | 342 | match unsafe { 343 | trace!("Call DhcpV4FailoverAddScopeToRelationship({}, {:?})", &self.serveripaddress, prelationship); 344 | DhcpV4FailoverAddScopeToRelationship( 345 | &self.serveripaddress, 346 | &prelationship 347 | ) 348 | } { 349 | 0 => { 350 | Ok(()) 351 | }, 352 | n => Err(n), 353 | } 354 | } 355 | 356 | pub fn remove_failover_relationship(&self, name: &str) -> Result<(), u32> { 357 | let name: Vec = name.encode_utf16().chain([0u16]).collect::>(); 358 | 359 | let prelationship = DHCP_FAILOVER_RELATIONSHIP { 360 | PrimaryServer: 0, 361 | SecondaryServer: 0, 362 | Mode: windows::Win32::NetworkManagement::Dhcp::DHCP_FAILOVER_MODE(0), 363 | ServerType: windows::Win32::NetworkManagement::Dhcp::DHCP_FAILOVER_SERVER(0), 364 | State: windows::Win32::NetworkManagement::Dhcp::FSM_STATE(0), 365 | PrevState: windows::Win32::NetworkManagement::Dhcp::FSM_STATE(0), 366 | Mclt: 0, 367 | SafePeriod: 0, 368 | RelationshipName: PWSTR::from_raw(name.as_ptr() as *mut _), 369 | PrimaryServerName: PWSTR::null(), 370 | SecondaryServerName: PWSTR::null(), 371 | pScopes: &mut DHCP_IP_ARRAY { 372 | NumElements: 1, 373 | Elements: &mut self.subnetaddress.clone(), 374 | }, 375 | Percentage: 0, 376 | SharedSecret: PWSTR::null(), 377 | }; 378 | 379 | match unsafe { 380 | trace!("Call DhcpV4FailoverDeleteScopeFromRelationship({}, {:?})", &self.serveripaddress, prelationship); 381 | DhcpV4FailoverDeleteScopeFromRelationship( 382 | &self.serveripaddress, 383 | &prelationship 384 | ) 385 | } { 386 | 0 => { 387 | Ok(()) 388 | }, 389 | n => Err(n), 390 | } 391 | } 392 | } 393 | 394 | #[derive(Debug, Default, Deserialize, Clone, PartialEq, Eq)] 395 | pub struct DnsFlags { 396 | #[serde(default)] 397 | pub enabled: bool, 398 | #[serde(default)] 399 | pub update_downlevel: bool, 400 | #[serde(default)] 401 | pub cleanup_expired: bool, 402 | #[serde(default)] 403 | pub update_both_always: bool, 404 | #[serde(default)] 405 | pub update_dhcid: bool, 406 | #[serde(default)] 407 | pub disable_ptr_update: bool, 408 | } 409 | 410 | impl fmt::Display for DnsFlags { 411 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 412 | if self.enabled { write!(f, "Enabled")? } else { write!(f, "Disabled")? } 413 | if self.update_downlevel { write!(f, ", Update Downlevel")? } 414 | if self.cleanup_expired { write!(f, ", Cleanup Expired")? } 415 | if self.update_both_always { write!(f, ", Update Both always")? } 416 | if self.update_dhcid { write!(f, ", Update DHCID")? } 417 | if self.disable_ptr_update { write!(f, ", Disable PTR update")? } 418 | Ok(()) 419 | } 420 | } 421 | 422 | impl From<&Vec> for DnsFlags { 423 | fn from(flags: &Vec) -> Self { 424 | Self { 425 | enabled: flags.contains(&String::from("enabled")), 426 | update_downlevel: flags.contains(&String::from("update_downlevel")), 427 | cleanup_expired: flags.contains(&String::from("cleanup_expired")), 428 | update_both_always: flags.contains(&String::from("update_both_always")), 429 | update_dhcid: flags.contains(&String::from("update_dhcid")), 430 | disable_ptr_update: flags.contains(&String::from("disable_ptr_update")), 431 | } 432 | } 433 | } 434 | 435 | impl From for DnsFlags { 436 | fn from(flags: u32) -> Self { 437 | Self { 438 | enabled: flags & DNS_FLAG_ENABLED == DNS_FLAG_ENABLED, 439 | update_downlevel: flags & DNS_FLAG_UPDATE_DOWNLEVEL == DNS_FLAG_UPDATE_DOWNLEVEL, 440 | cleanup_expired: flags & DNS_FLAG_CLEANUP_EXPIRED == DNS_FLAG_CLEANUP_EXPIRED, 441 | update_both_always: flags & DNS_FLAG_UPDATE_BOTH_ALWAYS == DNS_FLAG_UPDATE_BOTH_ALWAYS, 442 | update_dhcid: flags & DNS_FLAG_UPDATE_DHCID == DNS_FLAG_UPDATE_DHCID, 443 | disable_ptr_update: flags & DNS_FLAG_DISABLE_PTR_UPDATE == DNS_FLAG_DISABLE_PTR_UPDATE, 444 | } 445 | } 446 | } 447 | 448 | impl From<&DnsFlags> for u32 { 449 | fn from(flags: &DnsFlags) -> Self { 450 | let mut f = 0; 451 | 452 | if flags.enabled { f += DNS_FLAG_ENABLED; } 453 | if flags.update_downlevel { f += DNS_FLAG_UPDATE_DOWNLEVEL; } 454 | if flags.cleanup_expired { f += DNS_FLAG_CLEANUP_EXPIRED; } 455 | if flags.update_both_always { f += DNS_FLAG_UPDATE_BOTH_ALWAYS; } 456 | if flags.update_dhcid { f += DNS_FLAG_UPDATE_DHCID; } 457 | if flags.disable_ptr_update { f += DNS_FLAG_DISABLE_PTR_UPDATE; } 458 | 459 | f 460 | } 461 | } --------------------------------------------------------------------------------