Refactor code to reduce duplication among gethostbyname(3|4)_r
This commit is contained in:
parent
516bf4000c
commit
8082509da0
|
@ -8,6 +8,12 @@ edition = "2021"
|
|||
authors = ["Matteo Settenvini <matteo.settenvini@montecristosoftware.eu"]
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
name = "nss_malcontent"
|
||||
|
@ -31,4 +37,5 @@ log = "0.4"
|
|||
nix = { version = "0.25", features = ["socket", "user", "sched"] }
|
||||
tokio = { version = "1", features = ["rt"] }
|
||||
trust-dns-resolver = "0.21"
|
||||
trust-dns-proto = "0.21"
|
||||
zbus = { version = "3.0", default-features = false, features = ["tokio"] }
|
||||
|
|
|
@ -2,38 +2,57 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use {
|
||||
crate::helpers::{ips_to_gaih_addr, set_if_valid},
|
||||
crate::helpers::{records_to_gaih_addr, records_to_hostent, set_if_valid},
|
||||
crate::nss_bindings::{gaih_addrtuple, nss_status, HErrno},
|
||||
crate::policy_checker::{PolicyChecker as _, POLICY_CHECKER},
|
||||
libc::{hostent, size_t},
|
||||
libc::{c_char, c_int, hostent, size_t},
|
||||
nix::errno,
|
||||
nix::errno::Errno,
|
||||
std::ffi::CStr,
|
||||
std::os::raw::{c_char, c_int},
|
||||
trust_dns_proto::rr::record_type::RecordType,
|
||||
trust_dns_proto::xfer::dns_request::DnsRequestOptions,
|
||||
trust_dns_resolver::{lookup::Lookup, lookup_ip::LookupIp},
|
||||
};
|
||||
|
||||
// See https://support.mozilla.org/en-US/kb/configuring-networks-disable-dns-over-https
|
||||
const CANARY_HOSTNAME: &str = "use-application-dns.net";
|
||||
|
||||
pub async unsafe fn gethostbyname4_r(
|
||||
name: *const c_char,
|
||||
pat: *mut *mut gaih_addrtuple,
|
||||
buffer: *mut c_char,
|
||||
buflen: size_t,
|
||||
errnop: *mut Errno,
|
||||
h_errnop: *mut HErrno,
|
||||
ttlp: *mut i32,
|
||||
) -> nss_status {
|
||||
pub struct Args {
|
||||
pub name: *const c_char,
|
||||
pub family: c_int,
|
||||
pub buffer: *mut c_char,
|
||||
pub buflen: size_t,
|
||||
pub errnop: *mut Errno,
|
||||
pub h_errnop: *mut HErrno,
|
||||
pub ttlp: *mut i32,
|
||||
pub result: Result,
|
||||
}
|
||||
|
||||
pub enum Result {
|
||||
V3(HostEnt),
|
||||
V4(*mut *mut gaih_addrtuple),
|
||||
}
|
||||
|
||||
pub struct HostEnt {
|
||||
pub host: *mut hostent,
|
||||
pub canonp: *mut *mut char,
|
||||
}
|
||||
|
||||
pub async unsafe fn with(args: &mut Args) -> nss_status {
|
||||
set_if_valid(args.errnop, errno::from_i32(0));
|
||||
set_if_valid(args.h_errnop, HErrno::Success);
|
||||
|
||||
match POLICY_CHECKER.resolver(None) {
|
||||
Ok(None) => {
|
||||
// no restrictions for user, the next NSS module will decide
|
||||
nss_status::NSS_STATUS_NOTFOUND
|
||||
}
|
||||
Ok(Some(resolver)) => {
|
||||
let name = match CStr::from_ptr(name).to_str() {
|
||||
let name = match CStr::from_ptr(args.name).to_str() {
|
||||
Ok(name) => name,
|
||||
Err(_) => {
|
||||
set_if_valid(errnop, Errno::EINVAL);
|
||||
set_if_valid(h_errnop, HErrno::Internal);
|
||||
set_if_valid(args.errnop, Errno::EINVAL);
|
||||
set_if_valid(args.h_errnop, HErrno::Internal);
|
||||
return nss_status::NSS_STATUS_TRYAGAIN;
|
||||
}
|
||||
};
|
||||
|
@ -41,54 +60,27 @@ pub async unsafe fn gethostbyname4_r(
|
|||
// disable application-based DNS for those applications
|
||||
// (notably, Firefox) that support it
|
||||
if name == CANARY_HOSTNAME {
|
||||
set_if_valid(h_errnop, HErrno::HostNotFound);
|
||||
set_if_valid(args.h_errnop, HErrno::HostNotFound);
|
||||
return nss_status::NSS_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
match resolver.lookup_ip(name).await {
|
||||
Ok(result) if result.iter().peekable().peek().is_none() => {
|
||||
set_if_valid(h_errnop, HErrno::HostNotFound);
|
||||
nss_status::NSS_STATUS_SUCCESS
|
||||
let lookup: std::result::Result<Lookup, _> = match args.family {
|
||||
libc::AF_UNSPEC => resolver.lookup_ip(name).await.map(LookupIp::into),
|
||||
libc::AF_INET => {
|
||||
resolver
|
||||
.lookup(name, RecordType::A, DnsRequestOptions::default())
|
||||
.await
|
||||
}
|
||||
Ok(result) => {
|
||||
if pat == std::ptr::null_mut() {
|
||||
set_if_valid(errnop, Errno::EINVAL);
|
||||
set_if_valid(h_errnop, HErrno::Internal);
|
||||
return nss_status::NSS_STATUS_TRYAGAIN;
|
||||
}
|
||||
|
||||
let ttl = result
|
||||
.valid_until()
|
||||
.duration_since(std::time::Instant::now())
|
||||
.as_secs();
|
||||
set_if_valid(
|
||||
ttlp,
|
||||
if ttl < (i32::MAX as u64) {
|
||||
ttl as i32
|
||||
} else {
|
||||
i32::MAX
|
||||
},
|
||||
);
|
||||
|
||||
let buf = std::slice::from_raw_parts_mut(buffer as *mut u8, buflen);
|
||||
match ips_to_gaih_addr(result, buf) {
|
||||
Ok(addrs) => {
|
||||
// DEBUG: eprintln!("{:?} => {:?}", addrs, *addrs);
|
||||
*pat = addrs;
|
||||
nss_status::NSS_STATUS_SUCCESS
|
||||
}
|
||||
Err(err) => {
|
||||
set_if_valid(
|
||||
errnop,
|
||||
err.raw_os_error()
|
||||
.map(Errno::from_i32)
|
||||
.unwrap_or(Errno::EAGAIN),
|
||||
);
|
||||
set_if_valid(h_errnop, HErrno::Internal);
|
||||
nss_status::NSS_STATUS_TRYAGAIN
|
||||
}
|
||||
}
|
||||
libc::AF_INET6 => {
|
||||
resolver
|
||||
.lookup(name, RecordType::AAAA, DnsRequestOptions::default())
|
||||
.await
|
||||
}
|
||||
_ => return nss_status::NSS_STATUS_NOTFOUND,
|
||||
};
|
||||
|
||||
match lookup {
|
||||
Ok(result) => prepare_response(args, result),
|
||||
Err(err) => {
|
||||
log::warn!("{}", err);
|
||||
nss_status::NSS_STATUS_UNAVAIL
|
||||
|
@ -102,16 +94,77 @@ pub async unsafe fn gethostbyname4_r(
|
|||
}
|
||||
}
|
||||
|
||||
pub async unsafe fn gethostbyname3_r(
|
||||
_name: *const c_char,
|
||||
_af: c_int,
|
||||
_host: *mut hostent,
|
||||
_buffer: *mut c_char,
|
||||
_buflen: size_t,
|
||||
_errnop: *mut Errno,
|
||||
_h_errnop: *mut HErrno,
|
||||
_ttlp: *mut i32,
|
||||
_canonp: *mut *mut char,
|
||||
unsafe fn prepare_response(
|
||||
args: &mut Args,
|
||||
lookup: trust_dns_resolver::lookup::Lookup,
|
||||
) -> nss_status {
|
||||
todo!()
|
||||
if lookup.iter().peekable().peek().is_none() {
|
||||
set_if_valid(args.h_errnop, HErrno::HostNotFound);
|
||||
return nss_status::NSS_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
let ttl = lookup
|
||||
.valid_until()
|
||||
.duration_since(std::time::Instant::now())
|
||||
.as_secs();
|
||||
set_if_valid(
|
||||
args.ttlp,
|
||||
if ttl < (i32::MAX as u64) {
|
||||
ttl as i32
|
||||
} else {
|
||||
i32::MAX
|
||||
},
|
||||
);
|
||||
|
||||
let buf = std::slice::from_raw_parts_mut(args.buffer as *mut u8, args.buflen);
|
||||
let ret = match &mut args.result {
|
||||
Result::V3(hostent) => {
|
||||
if hostent.host.is_null() {
|
||||
set_if_valid(args.errnop, Errno::EINVAL);
|
||||
set_if_valid(args.h_errnop, HErrno::Internal);
|
||||
return nss_status::NSS_STATUS_TRYAGAIN;
|
||||
}
|
||||
|
||||
match records_to_hostent(lookup.into(), hostent, buf) {
|
||||
Ok(_) => nss_status::NSS_STATUS_SUCCESS,
|
||||
Err(err) => {
|
||||
set_if_valid(
|
||||
args.errnop,
|
||||
err.raw_os_error()
|
||||
.map(Errno::from_i32)
|
||||
.unwrap_or(Errno::EAGAIN),
|
||||
);
|
||||
set_if_valid(args.h_errnop, HErrno::Internal);
|
||||
nss_status::NSS_STATUS_TRYAGAIN
|
||||
}
|
||||
}
|
||||
}
|
||||
Result::V4(pat) => {
|
||||
if pat.is_null() {
|
||||
set_if_valid(args.errnop, Errno::EINVAL);
|
||||
set_if_valid(args.h_errnop, HErrno::Internal);
|
||||
return nss_status::NSS_STATUS_TRYAGAIN;
|
||||
}
|
||||
|
||||
match records_to_gaih_addr(lookup.into(), buf) {
|
||||
Ok(addrs) => {
|
||||
// DEBUG: eprintln!("{:?} => {:?}", addrs, *addrs);
|
||||
**pat = addrs;
|
||||
nss_status::NSS_STATUS_SUCCESS
|
||||
}
|
||||
Err(err) => {
|
||||
set_if_valid(
|
||||
args.errnop,
|
||||
err.raw_os_error()
|
||||
.map(Errno::from_i32)
|
||||
.unwrap_or(Errno::EAGAIN),
|
||||
);
|
||||
set_if_valid(args.h_errnop, HErrno::Internal);
|
||||
nss_status::NSS_STATUS_TRYAGAIN
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ret
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use {
|
||||
crate::gethostbyname::HostEnt,
|
||||
crate::nss_bindings::gaih_addrtuple,
|
||||
anyhow::{bail, Result},
|
||||
libc::{AF_INET, AF_INET6},
|
||||
|
@ -9,31 +10,35 @@ use {
|
|||
once_cell::sync::Lazy,
|
||||
std::ffi::CString,
|
||||
std::mem::{align_of, size_of},
|
||||
std::net::IpAddr,
|
||||
trust_dns_resolver::lookup_ip::LookupIp,
|
||||
trust_dns_proto::rr::RData,
|
||||
trust_dns_resolver::lookup::Lookup,
|
||||
};
|
||||
|
||||
static RUNTIME: Lazy<std::io::Result<tokio::runtime::Handle>> = Lazy::new(|| {
|
||||
static RUNTIME: Lazy<std::io::Result<tokio::runtime::Runtime>> = Lazy::new(|| {
|
||||
// The runtime should remain single-threaded, some
|
||||
// programs depend on it (e.g. programs calling unshare())
|
||||
let rt = tokio::runtime::Builder::new_current_thread().build()?;
|
||||
Ok(rt.handle().clone())
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_time()
|
||||
.enable_io()
|
||||
.build()?;
|
||||
Ok(rt)
|
||||
});
|
||||
|
||||
pub unsafe fn ips_to_gaih_addr(
|
||||
ips: LookupIp,
|
||||
// TODO: error handling codes chosen a bit sloppily
|
||||
pub unsafe fn records_to_gaih_addr(
|
||||
lookup: Lookup,
|
||||
mut buf: &mut [u8],
|
||||
) -> std::io::Result<*mut gaih_addrtuple> {
|
||||
const GAIH_ADDRTUPLE_SZ: usize = size_of::<gaih_addrtuple>();
|
||||
|
||||
let mut ret = std::ptr::null_mut();
|
||||
let query = ips.query();
|
||||
|
||||
let name = CString::new(query.name().to_utf8()).unwrap(); // TODO: .map_err() and fail more graciously
|
||||
let mut prev_link: *mut *mut gaih_addrtuple = std::ptr::null_mut();
|
||||
|
||||
for addr in ips {
|
||||
for record in lookup.record_iter() {
|
||||
// First add the name to the buffer
|
||||
let name = CString::new(record.name().to_utf8())
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
|
||||
|
||||
let offset = buf.as_ptr().align_offset(align_of::<libc::c_char>());
|
||||
let name_src = name.as_bytes_with_nul();
|
||||
let name_dest = buf.as_mut_ptr().add(offset);
|
||||
|
@ -60,15 +65,17 @@ pub unsafe fn ips_to_gaih_addr(
|
|||
set_if_valid(prev_link, &mut *tuple); // link from previous tuple to this tuple
|
||||
prev_link = &mut (*tuple).next;
|
||||
|
||||
match addr {
|
||||
IpAddr::V4(addr) => {
|
||||
match record.data() {
|
||||
Some(RData::A(addr)) => {
|
||||
tuple.family = AF_INET;
|
||||
tuple.addr[0] = std::mem::transmute_copy(&addr.octets());
|
||||
}
|
||||
IpAddr::V6(addr) => {
|
||||
Some(RData::AAAA(addr)) => {
|
||||
tuple.family = AF_INET6;
|
||||
tuple.addr = std::mem::transmute_copy(&addr.octets());
|
||||
}
|
||||
Some(_) => return Err(Errno::EBADMSG.into()),
|
||||
None => return Err(Errno::ENODATA.into()),
|
||||
}
|
||||
|
||||
if ret == std::ptr::null_mut() {
|
||||
|
@ -81,6 +88,37 @@ pub unsafe fn ips_to_gaih_addr(
|
|||
Ok(ret)
|
||||
}
|
||||
|
||||
pub unsafe fn records_to_hostent(
|
||||
lookup: Lookup,
|
||||
_hostent: &mut HostEnt,
|
||||
mut _buf: &mut [u8],
|
||||
) -> std::io::Result<()> {
|
||||
|
||||
// char *h_name Official name of the host.
|
||||
// char **h_aliases A pointer to an array of pointers to
|
||||
// alternative host names, terminated by a
|
||||
// null pointer.
|
||||
// int h_addrtype Address type.
|
||||
// int h_length The length, in bytes, of the address.
|
||||
// char **h_addr_list A pointer to an array of pointers to network
|
||||
// addresses (in network byte order) for the host,
|
||||
// terminated by a null pointer.
|
||||
|
||||
// In C struct hostent:
|
||||
//
|
||||
// - for the type of queries we perform, we can assume h_aliases
|
||||
// is an empty array.
|
||||
// - hostent is limited to just one address type for all addresses
|
||||
// in the list. We pick the type of first result, and only
|
||||
// append addresses of the same type.
|
||||
|
||||
for record in lookup.record_iter() {
|
||||
todo!();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_if_valid<T>(ptr: *mut T, val: T) {
|
||||
if !ptr.is_null() {
|
||||
unsafe { *ptr = val };
|
||||
|
@ -93,7 +131,7 @@ where
|
|||
{
|
||||
use std::ops::Deref;
|
||||
match RUNTIME.deref() {
|
||||
Ok(rt_handle) => Ok(rt_handle.block_on(async { f.await })),
|
||||
Ok(rt) => Ok(rt.block_on(async { f.await })),
|
||||
Err(e) => bail!("Unable to start tokio runtime: {}", e),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,10 @@
|
|||
|
||||
use {
|
||||
crate::gethostbyaddr::gethostbyaddr2_r,
|
||||
crate::gethostbyname::{gethostbyname3_r, gethostbyname4_r},
|
||||
crate::gethostbyname,
|
||||
crate::helpers::{block_on, set_if_valid},
|
||||
crate::nss_bindings::{gaih_addrtuple, nss_status, HErrno},
|
||||
libc::{hostent, size_t, socklen_t, AF_INET},
|
||||
nix::errno,
|
||||
nix::errno::Errno,
|
||||
std::os::raw::{c_char, c_int, c_void},
|
||||
std::ptr,
|
||||
|
@ -25,16 +24,22 @@ pub unsafe extern "C" fn _nss_malcontent_gethostbyname4_r(
|
|||
h_errnop: *mut HErrno,
|
||||
ttlp: *mut i32,
|
||||
) -> nss_status {
|
||||
set_if_valid(errnop, errno::from_i32(0));
|
||||
set_if_valid(h_errnop, HErrno::Success);
|
||||
let mut args = gethostbyname::Args {
|
||||
name,
|
||||
family: 0,
|
||||
result: gethostbyname::Result::V4(pat),
|
||||
buffer,
|
||||
buflen,
|
||||
errnop,
|
||||
h_errnop,
|
||||
ttlp,
|
||||
};
|
||||
|
||||
match block_on(async {
|
||||
gethostbyname4_r(name, pat, buffer, buflen, errnop, h_errnop, ttlp).await
|
||||
}) {
|
||||
match block_on(async { gethostbyname::with(&mut args).await }) {
|
||||
Ok(status) => status,
|
||||
Err(runtime_error) => {
|
||||
log::error!("gethostbyname4_r: {}", runtime_error);
|
||||
set_if_valid(h_errnop, HErrno::Internal);
|
||||
set_if_valid(args.h_errnop, HErrno::Internal);
|
||||
nss_status::NSS_STATUS_TRYAGAIN
|
||||
}
|
||||
}
|
||||
|
@ -52,15 +57,20 @@ pub unsafe extern "C" fn _nss_malcontent_gethostbyname3_r(
|
|||
ttlp: *mut i32,
|
||||
canonp: *mut *mut char,
|
||||
) -> nss_status {
|
||||
set_if_valid(errnop, errno::from_i32(0));
|
||||
set_if_valid(h_errnop, HErrno::Success);
|
||||
let result = gethostbyname::HostEnt { host, canonp };
|
||||
|
||||
match block_on(async {
|
||||
gethostbyname3_r(
|
||||
name, af, host, buffer, buflen, errnop, h_errnop, ttlp, canonp,
|
||||
)
|
||||
.await
|
||||
}) {
|
||||
let mut args = gethostbyname::Args {
|
||||
name,
|
||||
family: af,
|
||||
result: gethostbyname::Result::V3(result),
|
||||
buffer,
|
||||
buflen,
|
||||
errnop,
|
||||
h_errnop,
|
||||
ttlp,
|
||||
};
|
||||
|
||||
match block_on(async { gethostbyname::with(&mut args).await }) {
|
||||
Ok(status) => status,
|
||||
Err(runtime_error) => {
|
||||
log::error!("gethostbyname3_r: {}", runtime_error);
|
||||
|
@ -129,7 +139,7 @@ pub unsafe extern "C" fn _nss_malcontent_gethostbyaddr2_r(
|
|||
h_errnop: *mut HErrno,
|
||||
ttlp: *mut i32,
|
||||
) -> nss_status {
|
||||
set_if_valid(errnop, errno::from_i32(0));
|
||||
set_if_valid(errnop, nix::errno::from_i32(0));
|
||||
set_if_valid(h_errnop, HErrno::Success);
|
||||
|
||||
match block_on(async {
|
||||
|
|
|
@ -11,8 +11,8 @@ use {
|
|||
once_cell::sync::Lazy,
|
||||
std::collections::HashMap,
|
||||
std::net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
std::time::Duration,
|
||||
tokio::time::timeout,
|
||||
//std::time::Duration,
|
||||
//tokio::time::timeout,
|
||||
};
|
||||
|
||||
static CLOUDFLARE_PARENTALCONTROL_ADDRS: Lazy<Vec<IpAddr>> = Lazy::new(|| {
|
||||
|
@ -36,7 +36,7 @@ fork_test! {
|
|||
fn application_dns_is_nxdomain() -> Result<()> {
|
||||
common::setup()?;
|
||||
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
let dbus = common::mock_dbus(HashMap::from([(
|
||||
let _dbus = common::mock_dbus(HashMap::from([(
|
||||
getuid(),
|
||||
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
|
||||
)]));
|
||||
|
@ -61,7 +61,8 @@ fork_test! {
|
|||
freeaddrinfo(addr);
|
||||
};
|
||||
|
||||
timeout(Duration::from_secs(1), dbus).await??
|
||||
//timeout(Duration::from_secs(1), dbus).await??
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -70,7 +71,7 @@ fork_test! {
|
|||
common::setup()?;
|
||||
|
||||
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
let dbus = common::mock_dbus(HashMap::from([(
|
||||
let _dbus = common::mock_dbus(HashMap::from([(
|
||||
getuid(),
|
||||
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
|
||||
)]));
|
||||
|
@ -83,7 +84,8 @@ fork_test! {
|
|||
assert_eq!(system_addr, our_addr);
|
||||
}
|
||||
|
||||
timeout(Duration::from_secs(1), dbus).await??
|
||||
//timeout(Duration::from_secs(1), dbus).await??
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -92,7 +94,7 @@ fork_test! {
|
|||
common::setup()?;
|
||||
|
||||
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
let dbus = common::mock_dbus(HashMap::from([(
|
||||
let _dbus = common::mock_dbus(HashMap::from([(
|
||||
getuid(),
|
||||
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
|
||||
)]));
|
||||
|
@ -108,7 +110,8 @@ fork_test! {
|
|||
assert_ne!(system_addr, our_addr);
|
||||
assert_eq!(our_addr, IpAddr::V6(Ipv6Addr::UNSPECIFIED));
|
||||
|
||||
timeout(Duration::from_secs(1), dbus).await??
|
||||
//timeout(Duration::from_secs(1), dbus).await??
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -117,7 +120,7 @@ fork_test! {
|
|||
common::setup()?;
|
||||
|
||||
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
let dbus = common::mock_dbus(HashMap::from([(getuid(), vec![ /* no restriction */])]));
|
||||
let _dbus = common::mock_dbus(HashMap::from([(getuid(), vec![ /* no restriction */])]));
|
||||
|
||||
const HOSTNAME: &str = "pornhub.com";
|
||||
|
||||
|
@ -127,7 +130,8 @@ fork_test! {
|
|||
assert_eq!(system_addr, our_addr);
|
||||
}
|
||||
|
||||
timeout(Duration::from_secs(1), dbus).await??
|
||||
//timeout(Duration::from_secs(1), dbus).await??
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue