blob: 6f2876d2cfe30dc0795198b26ab936bfd3e060c8 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#![deny(warnings)]
use {
anyhow::{Context as _, Error},
argh::FromArgs,
dhcp::{
configuration,
protocol::{Message, SERVER_PORT},
server::{Server, ServerAction, ServerDispatcher, DEFAULT_STASH_ID, DEFAULT_STASH_PREFIX},
},
fuchsia_async::{self as fasync, net::UdpSocket, Interval},
fuchsia_component::server::ServiceFs,
fuchsia_syslog::{self as fx_syslog, fx_log_err, fx_log_info},
fuchsia_zircon::DurationNum,
futures::{Future, FutureExt, StreamExt, TryFutureExt, TryStreamExt},
net2::unix::UnixUdpBuilderExt,
std::{
cell::RefCell,
net::{IpAddr, Ipv4Addr},
os::unix::io::AsRawFd,
},
void::Void,
};
/// A buffer size in excess of the maximum allowable DHCP message size.
const BUF_SZ: usize = 1024;
const DEFAULT_CONFIG_PATH: &str = "/pkg/data/config.json";
/// The rate in seconds at which expiration DHCP leases are recycled back into the managed address
/// pool. The current value of 5 is meant to facilitate manual testing.
// TODO(atait): Replace with Duration type after it has been updated to const fn.
const EXPIRATION_INTERVAL_SECS: i64 = 5;
enum IncomingService {
Server(fidl_fuchsia_net_dhcp::Server_RequestStream),
}
/// The Fuchsia DHCP server.
#[derive(Debug, FromArgs)]
#[argh(name = "dhcpd")]
pub struct Args {
/// the identifier used to access fuchsia.stash.Store. dhcpd will attempt to access its
/// configuration parameters, saved DHCP option values, and saved leases from the
/// fuchsia.stash.Store instance specified by this identifier. If there are no configuration
/// parameters etc. at the specified fuchsia.stash.Store instance, then dhcpd will fallback to
/// parameters stored in the default configuration file.
#[argh(option, default = "DEFAULT_STASH_ID.to_string()")]
pub stash: String,
/// the path to the default configuration file consumed by dhcpd if it was unable to access a
/// fuchsia.stash.Store instance.
#[argh(option, default = "DEFAULT_CONFIG_PATH.to_string()")]
pub config: String,
}
#[fasync::run_singlethreaded]
async fn main() -> Result<(), Error> {
fx_syslog::init_with_tags(&["dhcpd"])?;
let Args { config, stash } = argh::from_env();
let stash = dhcp::stash::Stash::new(&stash, DEFAULT_STASH_PREFIX)
.context("failed to instantiate stash")?;
let params = stash.load_parameters().await.or_else(|e| {
fx_syslog::fx_log_warn!("failed to load parameters from stash: {}", e);
configuration::load_server_params_from_file(&config)
.context("failed to load server parameters from configuration file")
})?;
let socks = if params.bound_device_names.len() > 0 {
params.bound_device_names.iter().map(String::as_str).try_fold::<_, _, Result<_, Error>>(
Vec::new(),
|mut acc, name| {
let sock = create_socket(Some(name))?;
let () = acc.push(sock);
Ok(acc)
},
)?
} else {
vec![create_socket(None)?]
};
if socks.len() == 0 {
return Err(anyhow::format_err!("no valid sockets to receive messages from"));
}
let options = stash.load_options().await.unwrap_or_else(|e| {
fx_syslog::fx_log_warn!("failed to load options from stash: {}", e);
std::collections::HashMap::new()
});
let cache = stash.load_client_configs().await.unwrap_or_else(|e| {
fx_syslog::fx_log_warn!("failed to load cached client config from stash: {}", e);
std::collections::HashMap::new()
});
let server = RefCell::new(Server::new(stash, params, options, cache));
let mut fs = ServiceFs::new_local();
fs.dir("svc").add_fidl_service(IncomingService::Server);
fs.take_and_serve_directory_handle()?;
let admin_fut =
fs.then(futures::future::ok).try_for_each_concurrent(None, |incoming_service| async {
match incoming_service {
IncomingService::Server(stream) => {
run_server(stream, &server).inspect_err(|e| log::info!("{:?}", e)).await?;
Ok(())
}
}
});
if !server.borrow().is_serving() {
fx_log_info!("starting server in configuration only mode");
let () = admin_fut.await?;
} else {
let msg_loops = socks
.into_iter()
.map(|sock| define_msg_handling_loop_future(sock, &server).boxed_local());
let lease_expiration_handler = define_lease_expiration_handler_future(&server);
fx_log_info!("starting server");
let (_void, (), ()) = futures::try_join!(
futures::future::select_ok(msg_loops),
admin_fut,
lease_expiration_handler
)?;
}
Ok(())
}
fn create_socket(name: Option<&str>) -> Result<UdpSocket, Error> {
let sock = net2::UdpBuilder::new_v4()?;
// Since dhcpd may listen to multiple interfaces, we must enable
// SO_REUSEPORT so that binding the same (address, port) pair to each
// interface can still succeed.
let sock = sock.reuse_port(true)?;
if let Some(name) = name {
// There are currently no safe Rust interfaces to set SO_BINDTODEVICE,
// so we must set it through libc.
if unsafe {
libc::setsockopt(
sock.as_raw_fd(),
libc::SOL_SOCKET,
libc::SO_BINDTODEVICE,
name.as_ptr() as *const libc::c_void,
name.len() as libc::socklen_t,
)
} == -1
{
return Err(anyhow::format_err!(
"setsockopt(SO_BINDTODEVICE) failed for {}: {}",
name,
std::io::Error::last_os_error()
));
}
}
let sock = sock.bind((Ipv4Addr::UNSPECIFIED, SERVER_PORT))?;
let () = sock.set_broadcast(true)?;
Ok(UdpSocket::from_socket(sock)?)
}
async fn define_msg_handling_loop_future(
sock: UdpSocket,
server: &RefCell<Server>,
) -> Result<Void, Error> {
let mut buf = vec![0u8; BUF_SZ];
loop {
let (received, mut sender) =
sock.recv_from(&mut buf).await.context("failed to read from socket")?;
fx_log_info!("received message from: {}", sender);
let msg = Message::from_buffer(&buf[..received])?;
fx_log_info!("parsed message: {:?}", msg);
// This call should not block because the server is single-threaded.
let result = server.borrow_mut().dispatch(msg);
match result {
Err(e) => fx_log_err!("error processing client message: {}", e),
Ok(ServerAction::AddressRelease(addr)) => fx_log_info!("released address: {}", addr),
Ok(ServerAction::AddressDecline(addr)) => fx_log_info!("allocated address: {}", addr),
Ok(ServerAction::SendResponse(message, dest)) => {
fx_log_info!("generated response: {:?}", message);
// Check if server returned an explicit destination ip.
if let Some(addr) = dest {
sender.set_ip(IpAddr::V4(addr));
}
let response_buffer = message.serialize();
sock.send_to(&response_buffer, sender).await.context("unable to send response")?;
fx_log_info!("response sent to: {}", sender);
}
}
}
}
fn define_lease_expiration_handler_future<'a>(
server: &'a RefCell<Server>,
) -> impl Future<Output = Result<(), Error>> + 'a {
let expiration_interval = Interval::new(EXPIRATION_INTERVAL_SECS.seconds());
expiration_interval
.map(move |()| server.borrow_mut().release_expired_leases())
.map(|_| Ok(()))
.try_collect::<()>()
}
async fn run_server<S: ServerDispatcher>(
stream: fidl_fuchsia_net_dhcp::Server_RequestStream,
server: &RefCell<S>,
) -> Result<(), fidl::Error> {
stream
.try_for_each(|request| async {
match request {
fidl_fuchsia_net_dhcp::Server_Request::GetOption { code: c, responder: r } => {
r.send(&mut server.borrow().dispatch_get_option(c).map_err(|e| e.into_raw()))
}
fidl_fuchsia_net_dhcp::Server_Request::GetParameter { name: n, responder: r } => {
r.send(&mut server.borrow().dispatch_get_parameter(n).map_err(|e| e.into_raw()))
}
fidl_fuchsia_net_dhcp::Server_Request::SetOption { value: v, responder: r } => r
.send(
&mut server.borrow_mut().dispatch_set_option(v).map_err(|e| e.into_raw()),
),
fidl_fuchsia_net_dhcp::Server_Request::SetParameter { value: v, responder: r } => r
.send(
&mut server
.borrow_mut()
.dispatch_set_parameter(v)
.map_err(|e| e.into_raw()),
),
fidl_fuchsia_net_dhcp::Server_Request::ListOptions { responder: r } => {
r.send(&mut server.borrow().dispatch_list_options().map_err(|e| e.into_raw()))
}
fidl_fuchsia_net_dhcp::Server_Request::ListParameters { responder: r } => r.send(
&mut server.borrow().dispatch_list_parameters().map_err(|e| e.into_raw()),
),
}
})
.await
}
#[cfg(test)]
mod tests {
use super::*;
struct CannedDispatcher {}
impl ServerDispatcher for CannedDispatcher {
fn dispatch_get_option(
&self,
_code: fidl_fuchsia_net_dhcp::OptionCode,
) -> Result<fidl_fuchsia_net_dhcp::Option_, fuchsia_zircon::Status> {
Ok(fidl_fuchsia_net_dhcp::Option_::SubnetMask(fidl_fuchsia_net::Ipv4Address {
addr: [0, 0, 0, 0],
}))
}
fn dispatch_get_parameter(
&self,
_name: fidl_fuchsia_net_dhcp::ParameterName,
) -> Result<fidl_fuchsia_net_dhcp::Parameter, fuchsia_zircon::Status> {
Ok(fidl_fuchsia_net_dhcp::Parameter::Lease(fidl_fuchsia_net_dhcp::LeaseLength {
default: None,
max: None,
}))
}
fn dispatch_set_option(
&mut self,
_value: fidl_fuchsia_net_dhcp::Option_,
) -> Result<(), fuchsia_zircon::Status> {
Ok(())
}
fn dispatch_set_parameter(
&mut self,
_value: fidl_fuchsia_net_dhcp::Parameter,
) -> Result<(), fuchsia_zircon::Status> {
Ok(())
}
fn dispatch_list_options(
&self,
) -> Result<Vec<fidl_fuchsia_net_dhcp::Option_>, fuchsia_zircon::Status> {
Ok(vec![])
}
fn dispatch_list_parameters(
&self,
) -> Result<Vec<fidl_fuchsia_net_dhcp::Parameter>, fuchsia_zircon::Status> {
Ok(vec![])
}
}
#[fasync::run_singlethreaded(test)]
async fn get_option_with_subnet_mask_returns_subnet_mask() -> Result<(), Error> {
let (proxy, stream) =
fidl::endpoints::create_proxy_and_stream::<fidl_fuchsia_net_dhcp::Server_Marker>()?;
let server = RefCell::new(CannedDispatcher {});
let res = proxy.get_option(fidl_fuchsia_net_dhcp::OptionCode::SubnetMask);
fasync::spawn_local(async move {
let () = run_server(stream, &server).await.unwrap_or(());
});
let res = res.await?;
let expected_result =
Ok(fidl_fuchsia_net_dhcp::Option_::SubnetMask(fidl_fuchsia_net::Ipv4Address {
addr: [0, 0, 0, 0],
}));
assert_eq!(res, expected_result);
Ok(())
}
#[fasync::run_until_stalled(test)]
async fn get_parameter_with_lease_length_returns_lease_length() -> Result<(), Error> {
let (proxy, stream) =
fidl::endpoints::create_proxy_and_stream::<fidl_fuchsia_net_dhcp::Server_Marker>()?;
let server = RefCell::new(CannedDispatcher {});
let res = proxy.get_parameter(fidl_fuchsia_net_dhcp::ParameterName::LeaseLength);
fasync::spawn_local(async move {
let () = run_server(stream, &server).await.unwrap_or(());
});
let res = res.await?;
let expected_result =
Ok(fidl_fuchsia_net_dhcp::Parameter::Lease(fidl_fuchsia_net_dhcp::LeaseLength {
default: None,
max: None,
}));
assert_eq!(res, expected_result);
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn set_option_with_subnet_mask_returns_unit() -> Result<(), Error> {
let (proxy, stream) =
fidl::endpoints::create_proxy_and_stream::<fidl_fuchsia_net_dhcp::Server_Marker>()?;
let server = RefCell::new(CannedDispatcher {});
let res = proxy.set_option(&mut fidl_fuchsia_net_dhcp::Option_::SubnetMask(
fidl_fuchsia_net::Ipv4Address { addr: [0, 0, 0, 0] },
));
fasync::spawn_local(async move {
let () = run_server(stream, &server).await.unwrap_or(());
});
let res = res.await?;
assert_eq!(res, Ok(()));
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn set_parameter_with_lease_length_returns_unit() -> Result<(), Error> {
let (proxy, stream) =
fidl::endpoints::create_proxy_and_stream::<fidl_fuchsia_net_dhcp::Server_Marker>()?;
let server = RefCell::new(CannedDispatcher {});
let res = proxy.set_parameter(&mut fidl_fuchsia_net_dhcp::Parameter::Lease(
fidl_fuchsia_net_dhcp::LeaseLength { default: None, max: None },
));
fasync::spawn_local(async move {
let () = run_server(stream, &server).await.unwrap_or(());
});
let res = res.await?;
assert_eq!(res, Ok(()));
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn list_options_returns_empty_vec() -> Result<(), Error> {
let (proxy, stream) =
fidl::endpoints::create_proxy_and_stream::<fidl_fuchsia_net_dhcp::Server_Marker>()?;
let server = RefCell::new(CannedDispatcher {});
let res = proxy.list_options();
fasync::spawn_local(async move {
let () = run_server(stream, &server).await.unwrap_or(());
});
let res = res.await?;
assert_eq!(res, Ok(vec![]));
Ok(())
}
#[fasync::run_singlethreaded(test)]
async fn list_parameters_returns_empty_vec() -> Result<(), Error> {
let (proxy, stream) =
fidl::endpoints::create_proxy_and_stream::<fidl_fuchsia_net_dhcp::Server_Marker>()?;
let server = RefCell::new(CannedDispatcher {});
let res = proxy.list_parameters();
fasync::spawn_local(async move {
let () = run_server(stream, &server).await.unwrap_or(());
});
let res = res.await?;
assert_eq!(res, Ok(vec![]));
Ok(())
}
}