From aea926391f29f77884f18f72fdf4ac2d6b81e01a Mon Sep 17 00:00:00 2001 From: David Mulder Date: Wed, 10 Jul 2024 14:04:28 -0600 Subject: [PATCH] Add the Azure Entra Id Daemon Signed-off-by: David Mulder Reviewed-by: Alexander Bokovoy --- himmelblaud/Cargo.toml | 17 +- himmelblaud/build.rs | 18 ++ himmelblaud/sock/Cargo.toml | 12 ++ himmelblaud/sock/src/lib.rs | 57 +++++ himmelblaud/sock/src/proto.rs | 115 ++++++++++ himmelblaud/src/constants.rs | 1 + himmelblaud/src/himmelblaud.rs | 153 +++++++++++++ himmelblaud/src/main.rs | 383 ++++++++++++++++++++++++++++++++- himmelblaud/wscript_build | 4 + wscript_build | 1 + 10 files changed, 759 insertions(+), 2 deletions(-) create mode 100644 himmelblaud/build.rs create mode 100644 himmelblaud/sock/Cargo.toml create mode 100644 himmelblaud/sock/src/lib.rs create mode 100644 himmelblaud/sock/src/proto.rs create mode 100644 himmelblaud/src/constants.rs create mode 100644 himmelblaud/src/himmelblaud.rs create mode 100644 himmelblaud/wscript_build diff --git a/himmelblaud/Cargo.toml b/himmelblaud/Cargo.toml index bce2de8fe08..0ba4178a70e 100644 --- a/himmelblaud/Cargo.toml +++ b/himmelblaud/Cargo.toml @@ -12,21 +12,36 @@ homepage.workspace = true version.workspace = true [dependencies] +libhimmelblau = "0.2.7" +ntstatus_gen.workspace = true +param = { workspace = true } +sock = { workspace = true } tdb = { workspace = true } dbg = { workspace = true } +chelps.workspace = true +clap = "4.5.9" +kanidm-hsm-crypto = "0.2.0" +serde_json = "1.0.120" +tokio = { version = "1.38.1", features = [ "rt", "macros" ] } +tokio-util = { version = "0.7.11", features = ["codec"] } +bytes = "1.6.1" +futures = "0.3.30" +serde = "1.0.204" +idmap = { workspace = true } libc = "0.2.155" [workspace] members = [ "chelps", "dbg", "idmap", "ntstatus_gen", - "param", "tdb", + "param", "sock", "tdb", ] [workspace.dependencies] param = { path = "param" } dbg = { path = "dbg" } chelps = { path = "chelps" } +sock = { path = "sock" } ntstatus_gen = { path = "ntstatus_gen" } tdb = { path = "tdb" } idmap = { path = "idmap" } diff --git a/himmelblaud/build.rs b/himmelblaud/build.rs new file mode 100644 index 00000000000..1b5015098f3 --- /dev/null +++ b/himmelblaud/build.rs @@ -0,0 +1,18 @@ +use std::env; + +fn main() { + // Re-export the Target OS, so that Himmelblaud has access to this at + // runtime. + if &env::var("CARGO_CFG_TARGET_OS").unwrap() != "none" { + println!( + "cargo:rustc-env=TARGET_OS={}", + &env::var("CARGO_CFG_TARGET_OS").unwrap() + ); + } else { + println!( + "cargo:rustc-env=TARGET_OS={}", + &env::var("CARGO_CFG_TARGET_FAMILY").unwrap() + ); + } + println!("cargo:rerun-if-changed-env=TARGET"); +} diff --git a/himmelblaud/sock/Cargo.toml b/himmelblaud/sock/Cargo.toml new file mode 100644 index 00000000000..675e520f94a --- /dev/null +++ b/himmelblaud/sock/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "sock" +edition.workspace = true +license.workspace = true +homepage.workspace = true +version.workspace = true + +[dependencies] +libc = "0.2.155" +libnss = "0.8.0" +serde = { version = "1.0.204", features = ["derive"] } +serde_json = "1.0.120" diff --git a/himmelblaud/sock/src/lib.rs b/himmelblaud/sock/src/lib.rs new file mode 100644 index 00000000000..e52ad39ff37 --- /dev/null +++ b/himmelblaud/sock/src/lib.rs @@ -0,0 +1,57 @@ +/* + Unix SMB/CIFS implementation. + + Unix socket communication for the Himmelblau daemon + + Copyright (C) David Mulder 2024 + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +mod proto; +pub use proto::*; + +use serde_json::{from_slice as json_from_slice, to_vec as json_to_vec}; +use std::error::Error; +use std::io::{Read, Write}; +use std::os::unix::net::UnixStream; +use std::time::Duration; + +pub struct ClientStream { + stream: UnixStream, +} + +impl ClientStream { + pub fn new(path: &str) -> Result> { + Ok(ClientStream { + stream: UnixStream::connect(path)?, + }) + } + + pub fn send( + &mut self, + req: &Request, + timeout: u64, + ) -> Result> { + let timeout = Duration::from_secs(timeout); + self.stream.set_read_timeout(Some(timeout))?; + self.stream.set_write_timeout(Some(timeout))?; + let req_bytes = json_to_vec(req)?; + self.stream.write_all(&req_bytes)?; + let mut buf = Vec::new(); + self.stream.read_to_end(&mut buf)?; + let resp: Response = json_from_slice(&buf)?; + Ok(resp) + } +} diff --git a/himmelblaud/sock/src/proto.rs b/himmelblaud/sock/src/proto.rs new file mode 100644 index 00000000000..ad40c50486f --- /dev/null +++ b/himmelblaud/sock/src/proto.rs @@ -0,0 +1,115 @@ +/* + Unix SMB/CIFS implementation. + + Unix socket communication for the Himmelblau daemon + + Copyright (C) David Mulder 2024 + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +use libc::uid_t; +use libnss::group::Group as NssGroup; +use libnss::passwd::Passwd as NssPasswd; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Passwd { + pub name: String, + pub passwd: String, + pub uid: u32, + pub gid: u32, + pub gecos: String, + pub dir: String, + pub shell: String, +} + +impl From for NssPasswd { + fn from(val: Passwd) -> Self { + NssPasswd { + name: val.name, + passwd: val.passwd, + uid: val.uid, + gid: val.gid, + gecos: val.gecos, + dir: val.dir, + shell: val.shell, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Group { + pub name: String, + pub passwd: String, + pub gid: u32, + pub members: Vec, +} + +impl From for NssGroup { + fn from(val: Group) -> Self { + NssGroup { + name: val.name, + passwd: val.passwd, + gid: val.gid, + members: val.members, + } + } +} + +#[derive(Serialize, Deserialize)] +pub enum PamAuthRequest { + Password { cred: String }, + MFACode { cred: String }, + MFAPoll { poll_attempt: u32 }, + SetupPin { pin: String }, + Pin { pin: String }, +} + +#[derive(Serialize, Deserialize)] +pub enum Request { + NssAccounts, + NssAccountByUid(uid_t), + NssAccountByName(String), + NssGroups, + NssGroupByGid(uid_t), + NssGroupByName(String), + PamAuthenticateInit(String), + PamAuthenticateStep(PamAuthRequest), + PamAccountAllowed(String), + PamAccountBeginSession(String), +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum PamAuthResponse { + Unknown, + Success, + Denied, + Password, + MFACode { msg: String }, + MFAPoll { msg: String, polling_interval: u32 }, + SetupPin { msg: String }, + Pin, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum Response { + NssAccounts(Vec), + NssAccount(Option), + NssGroups(Vec), + NssGroup(Option), + PamStatus(Option), + PamAuthStepResponse(PamAuthResponse), + Success, + Error, +} diff --git a/himmelblaud/src/constants.rs b/himmelblaud/src/constants.rs new file mode 100644 index 00000000000..e2a36473c6d --- /dev/null +++ b/himmelblaud/src/constants.rs @@ -0,0 +1 @@ +pub const DEFAULT_ODC_PROVIDER: &str = "odc.officeapps.live.com"; diff --git a/himmelblaud/src/himmelblaud.rs b/himmelblaud/src/himmelblaud.rs new file mode 100644 index 00000000000..b3ea0d45090 --- /dev/null +++ b/himmelblaud/src/himmelblaud.rs @@ -0,0 +1,153 @@ +/* + Unix SMB/CIFS implementation. + + Himmelblau daemon + + Copyright (C) David Mulder 2024 + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +use crate::cache::{GroupCache, PrivateCache, UserCache}; +use bytes::{BufMut, BytesMut}; +use dbg::{DBG_DEBUG, DBG_ERR}; +use futures::{SinkExt, StreamExt}; +use himmelblau::graph::Graph; +use himmelblau::BrokerClientApplication; +use idmap::Idmap; +use kanidm_hsm_crypto::{BoxedDynTpm, MachineKey}; +use param::LoadParm; +use sock::{Request, Response}; +use std::error::Error; +use std::io; +use std::io::{Error as IoError, ErrorKind}; +use std::sync::Arc; +use tokio::net::UnixStream; +use tokio::sync::Mutex; +use tokio_util::codec::{Decoder, Encoder, Framed}; + +pub(crate) struct Resolver { + realm: String, + tenant_id: String, + lp: LoadParm, + idmap: Idmap, + graph: Graph, + pcache: PrivateCache, + user_cache: UserCache, + group_cache: GroupCache, + hsm: Mutex, + machine_key: MachineKey, + client: Arc>, +} + +impl Resolver { + pub(crate) fn new( + realm: &str, + tenant_id: &str, + lp: LoadParm, + idmap: Idmap, + graph: Graph, + pcache: PrivateCache, + user_cache: UserCache, + group_cache: GroupCache, + hsm: BoxedDynTpm, + machine_key: MachineKey, + client: BrokerClientApplication, + ) -> Self { + Resolver { + realm: realm.to_string(), + tenant_id: tenant_id.to_string(), + lp, + idmap, + graph, + pcache, + user_cache, + group_cache, + hsm: Mutex::new(hsm), + machine_key, + client: Arc::new(Mutex::new(client)), + } + } +} + +struct ClientCodec; + +impl Decoder for ClientCodec { + type Error = io::Error; + type Item = Request; + + fn decode( + &mut self, + src: &mut BytesMut, + ) -> Result, Self::Error> { + match serde_json::from_slice::(src) { + Ok(msg) => { + src.clear(); + Ok(Some(msg)) + } + _ => Ok(None), + } + } +} + +impl Encoder for ClientCodec { + type Error = io::Error; + + fn encode( + &mut self, + msg: Response, + dst: &mut BytesMut, + ) -> Result<(), Self::Error> { + DBG_DEBUG!("Attempting to send response -> {:?} ...", msg); + let data = serde_json::to_vec(&msg).map_err(|e| { + DBG_ERR!("socket encoding error -> {:?}", e); + io::Error::new(ErrorKind::Other, "JSON encode error") + })?; + dst.put(data.as_slice()); + Ok(()) + } +} + +impl ClientCodec { + fn new() -> Self { + ClientCodec + } +} + +pub(crate) async fn handle_client( + stream: UnixStream, + resolver: Arc>, +) -> Result<(), Box> { + DBG_DEBUG!("Accepted connection"); + + let Ok(_ucred) = stream.peer_cred() else { + return Err(Box::new(IoError::new( + ErrorKind::Other, + "Unable to verify peer credentials.", + ))); + }; + + let mut reqs = Framed::new(stream, ClientCodec::new()); + + while let Some(Ok(req)) = reqs.next().await { + let resp = match req { + _ => todo!(), + }; + reqs.send(resp).await?; + reqs.flush().await?; + DBG_DEBUG!("flushed response!"); + } + + DBG_DEBUG!("Disconnecting client ..."); + Ok(()) +} diff --git a/himmelblaud/src/main.rs b/himmelblaud/src/main.rs index f328e4d9d04..74997fc0194 100644 --- a/himmelblaud/src/main.rs +++ b/himmelblaud/src/main.rs @@ -1 +1,382 @@ -fn main() {} +/* + Unix SMB/CIFS implementation. + + Himmelblau daemon + + Copyright (C) David Mulder 2024 + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +use clap::{Arg, ArgAction, Command}; +use dbg::*; +use himmelblau::graph::Graph; +use himmelblau::BrokerClientApplication; +use idmap::Idmap; +use kanidm_hsm_crypto::soft::SoftTpm; +use kanidm_hsm_crypto::{BoxedDynTpm, Tpm}; +use param::LoadParm; +use std::path::{Path, PathBuf}; +use std::process::ExitCode; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use tokio::net::UnixListener; +use tokio::signal::unix::{signal, SignalKind}; +use tokio::sync::Mutex; + +mod constants; +use constants::DEFAULT_ODC_PROVIDER; +mod cache; +mod himmelblaud; +use cache::{GroupCache, PrivateCache, UserCache}; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> ExitCode { + let clap_args = Command::new("himmelblaud") + .version(env!("CARGO_PKG_VERSION")) + .about("Samba Himmelblau Authentication Daemon") + .arg( + Arg::new("debuglevel") + .help("Set debug level") + .short('d') + .long("debuglevel") + .value_parser( + clap::value_parser!(u16).range(0..(MAX_DEBUG_LEVEL as i64)), + ) + .action(ArgAction::Set), + ) + .arg( + Arg::new("debug-stdout") + .help("Send debug output to standard output") + .long("debug-stdout") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("configfile") + .help("Use alternative configuration file") + .short('s') + .long("configfile") + .action(ArgAction::Set), + ) + .get_matches(); + + let stop_now = Arc::new(AtomicBool::new(false)); + let terminate_now = Arc::clone(&stop_now); + let quit_now = Arc::clone(&stop_now); + let interrupt_now = Arc::clone(&stop_now); + + async { + // Set the command line debug level + if let Some(debuglevel) = clap_args.get_one::("debuglevel") { + debuglevel_set!(*debuglevel); + } + + // Initialize the LoadParm from the command line specified config file + let lp = match LoadParm::new( + clap_args + .get_one::("configfile") + .map(|x| x.as_str()), + ) { + Ok(lp) => lp, + Err(e) => { + eprintln!("Failed loading smb.conf: {:?}", e); + return ExitCode::FAILURE; + } + }; + + // Check that the realm is configured. This the bare minimum for + // himmelblaud, since a device join can happen at authentication time, + // but we need to know the permitted enrollment domain. + let realm = match lp.realm() { + Ok(Some(realm)) => realm, + _ => { + eprintln!( + "The realm MUST be set in the \ + smb.conf to start himmelblaud" + ); + return ExitCode::FAILURE; + } + }; + + // Setup logging, either to the configured logfile, or to stdout, depending + // on what is specified on the command line. + match lp.logfile() { + Ok(Some(logfile)) => debug_set_logfile(&logfile), + _ => { + eprintln!("Failed to determine logfile name"); + return ExitCode::FAILURE; + } + } + match clap_args.get_flag("debug-stdout") { + true => setup_logging(env!("CARGO_PKG_NAME"), DEBUG_STDOUT), + false => setup_logging(env!("CARGO_PKG_NAME"), DEBUG_FILE), + } + + // Determine the unix socket path + let sock_dir_str = match lp.winbindd_socket_directory() { + Ok(Some(sock_dir)) => sock_dir, + _ => return ExitCode::FAILURE, + }; + let sock_dir = Path::new(&sock_dir_str); + let mut sock_path = PathBuf::from(sock_dir); + sock_path.push("hb_pipe"); + let sock_path = match sock_path.to_str() { + Some(sock_path) => sock_path, + None => return ExitCode::FAILURE, + }; + + // Initialize the Himmelblau cache + let private_cache_path = match lp.private_path("himmelblau.tdb") { + Ok(Some(private_cache_path)) => private_cache_path, + _ => { + DBG_ERR!("Failed to determine private cache path"); + return ExitCode::FAILURE; + } + }; + let mut pcache = match PrivateCache::new(&private_cache_path) { + Ok(cache) => cache, + Err(e) => { + DBG_ERR!( + "Failed to open the himmelblau private cache: {:?}", + e + ); + return ExitCode::FAILURE; + } + }; + + let cache_dir = match lp.cache_directory() { + Ok(Some(cache_dir)) => cache_dir, + _ => { + DBG_ERR!("Failed to determine cache directory"); + return ExitCode::FAILURE; + } + }; + + let user_cache_path = Path::new(&cache_dir) + .join("himmelblau_users.tdb") + .display() + .to_string(); + let user_cache = match UserCache::new(&user_cache_path) { + Ok(cache) => cache, + Err(e) => { + DBG_ERR!("Failed to open the himmelblau user cache: {:?}", e); + return ExitCode::FAILURE; + } + }; + + let group_cache_path = Path::new(&cache_dir) + .join("himmelblau_groups.tdb") + .display() + .to_string(); + let group_cache = match GroupCache::new(&group_cache_path) { + Ok(cache) => cache, + Err(e) => { + DBG_ERR!("Failed to open the himmelblau group cache: {:?}", e); + return ExitCode::FAILURE; + } + }; + + // Check for and create the hsm pin if required. + let auth_value = match pcache.hsm_pin_fetch_or_create() { + Ok(auth_value) => auth_value, + Err(e) => { + DBG_ERR!("{:?}", e); + return ExitCode::FAILURE; + } + }; + + // Setup the HSM and its machine key + let mut hsm: BoxedDynTpm = BoxedDynTpm::new(SoftTpm::new()); + + let loadable_machine_key = match pcache + .loadable_machine_key_fetch_or_create(&mut hsm, &auth_value) + { + Ok(lmk) => lmk, + Err(e) => { + DBG_ERR!("{:?}", e); + return ExitCode::FAILURE; + } + }; + + let res = hsm.machine_key_load(&auth_value, &loadable_machine_key); + let machine_key = match res { + Ok(machine_key) => machine_key, + Err(e) => { + DBG_ERR!("Unable to load machine root key: {:?}", e); + DBG_INFO!("This can occur if you have changed your HSM pin."); + DBG_INFO!( + "To proceed, run `tdbtool erase {}`", + private_cache_path + ); + DBG_INFO!("The host will forget domain enrollments."); + return ExitCode::FAILURE; + } + }; + + // Get the transport key for a joined domain + let loadable_transport_key = + pcache.loadable_transport_key_fetch(&realm); + + // Get the certificate key for a joined domain + let loadable_cert_key = pcache.loadable_cert_key_fetch(&realm); + + // Contact the odc provider to get the authority host and tenant id + let graph = match Graph::new(DEFAULT_ODC_PROVIDER, &realm).await { + Ok(graph) => graph, + Err(e) => { + DBG_ERR!("Failed initializing the graph: {:?}", e); + return ExitCode::FAILURE; + } + }; + let authority_host = graph.authority_host(); + let tenant_id = graph.tenant_id(); + let authority = format!("https://{}/{}", authority_host, tenant_id); + + let client = match BrokerClientApplication::new( + Some(&authority), + loadable_transport_key, + loadable_cert_key, + ) { + Ok(client) => client, + Err(e) => { + DBG_ERR!("Failed initializing the broker: {:?}", e); + return ExitCode::FAILURE; + } + }; + + let mut idmap = match Idmap::new() { + Ok(idmap) => idmap, + Err(e) => { + DBG_ERR!("Failed initializing the idmapper: {:?}", e); + return ExitCode::FAILURE; + } + }; + // Configure the idmap range + let (low, high) = match lp.idmap_range(&realm) { + Ok(res) => res, + Err(e) => { + DBG_ERR!("Failed fetching idmap range: {:?}", e); + return ExitCode::FAILURE; + } + }; + if let Err(e) = idmap.add_gen_domain(&realm, &tenant_id, (low, high)) { + DBG_ERR!("Failed adding the domain idmap range: {:?}", e); + return ExitCode::FAILURE; + } + + let resolver = Arc::new(Mutex::new(himmelblaud::Resolver::new( + &realm, + &tenant_id, + lp, + idmap, + graph, + pcache, + user_cache, + group_cache, + hsm, + machine_key, + client, + ))); + + // Listen for incomming requests from PAM and NSS + let listener = match UnixListener::bind(sock_path) { + Ok(listener) => listener, + Err(e) => { + DBG_ERR!("Failed setting up the socket listener: {:?}", e); + return ExitCode::FAILURE; + } + }; + + let server = tokio::spawn(async move { + while !stop_now.load(Ordering::Relaxed) { + let resolver_ref = resolver.clone(); + match listener.accept().await { + Ok((socket, _addr)) => { + tokio::spawn(async move { + if let Err(e) = himmelblaud::handle_client( + socket, + resolver_ref.clone(), + ) + .await + { + DBG_ERR!( + "handle_client error occurred: {:?}", + e + ); + } + }); + } + Err(e) => { + DBG_ERR!("Error while handling connection: {:?}", e); + } + } + } + }); + + let terminate_task = tokio::spawn(async move { + match signal(SignalKind::terminate()) { + Ok(mut stream) => { + stream.recv().await; + terminate_now.store(true, Ordering::Relaxed); + } + Err(e) => { + DBG_ERR!("Failed registering terminate signal: {}", e); + } + }; + }); + + let quit_task = tokio::spawn(async move { + match signal(SignalKind::quit()) { + Ok(mut stream) => { + stream.recv().await; + quit_now.store(true, Ordering::Relaxed); + } + Err(e) => { + DBG_ERR!("Failed registering quit signal: {}", e); + } + }; + }); + + let interrupt_task = tokio::spawn(async move { + match signal(SignalKind::interrupt()) { + Ok(mut stream) => { + stream.recv().await; + interrupt_now.store(true, Ordering::Relaxed); + } + Err(e) => { + DBG_ERR!("Failed registering interrupt signal: {}", e); + } + }; + }); + + DBG_INFO!("Server started ..."); + + tokio::select! { + _ = server => { + DBG_DEBUG!("Main listener task is terminating"); + }, + _ = terminate_task => { + DBG_DEBUG!("Received signal to terminate"); + }, + _ = quit_task => { + DBG_DEBUG!("Received signal to quit"); + }, + _ = interrupt_task => { + DBG_DEBUG!("Received signal to interrupt"); + } + } + + ExitCode::SUCCESS + } + .await +} diff --git a/himmelblaud/wscript_build b/himmelblaud/wscript_build new file mode 100644 index 00000000000..f302ab1e6c8 --- /dev/null +++ b/himmelblaud/wscript_build @@ -0,0 +1,4 @@ +#!/usr/bin/env python + +bld.SAMBA_RUST_BINARY('himmelblaud', + source='src/main.rs param/src/lib.rs chelps/src/lib.rs dbg/src/lib.rs ntstatus_gen/src/lib.rs sock/src/lib.rs tdb/src/lib.rs version/src/lib.rs') diff --git a/wscript_build b/wscript_build index 3ee2cc5243e..d2b10a45134 100644 --- a/wscript_build +++ b/wscript_build @@ -155,6 +155,7 @@ bld.RECURSE('dfs_server') bld.RECURSE('file_server') bld.RECURSE('lib/krb5_wrap') bld.RECURSE('packaging') +bld.RECURSE('himmelblaud') bld.RECURSE('testsuite/headers')