Use an async context for module and refactor
This commit is contained in:
parent
96663abaef
commit
516bf4000c
|
@ -18,17 +18,17 @@ bindgen = "0.60"
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
rusty-hook = "0.11"
|
rusty-hook = "0.11"
|
||||||
|
rusty-forkfork = "0.4"
|
||||||
test-cdylib = "1.1"
|
test-cdylib = "1.1"
|
||||||
tokio = { version = "1", features = ["rt", "sync", "macros", "time"] }
|
tokio = { version = "1", features = ["rt", "sync", "macros", "time"] }
|
||||||
zbus = { version = "3.0", default-features = false, features = ["tokio"] }
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
const_format = "0.2"
|
const_format = "0.2"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
once_cell = "1.13"
|
once_cell = "1.13"
|
||||||
gio = "0.15"
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
nix = { version = "0.25", features = ["socket", "user", "sched"] }
|
nix = { version = "0.25", features = ["socket", "user", "sched"] }
|
||||||
tokio = { version = "1", features = ["rt"] }
|
tokio = { version = "1", features = ["rt"] }
|
||||||
trust-dns-resolver = "0.21"
|
trust-dns-resolver = "0.21"
|
||||||
|
zbus = { version = "3.0", default-features = false, features = ["tokio"] }
|
||||||
|
|
8
build.rs
8
build.rs
|
@ -4,17 +4,11 @@
|
||||||
use {std::env, std::path::PathBuf};
|
use {std::env, std::path::PathBuf};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
println!("cargo:rerun-if-changed=wrapper.hpp");
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed=wrapper.h");
|
|
||||||
|
|
||||||
// Required by NSS 2
|
// Required by NSS 2
|
||||||
println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,libnss_malcontent.so.2");
|
println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,libnss_malcontent.so.2");
|
||||||
|
|
||||||
// Enable dynamic loading of NSS module from OUT_DIR
|
|
||||||
// (see https://doc.rust-lang.org/cargo/reference/environment-variables.html#dynamic-library-paths)
|
|
||||||
println!("cargo:rustc-link-search={}", &out_dir);
|
|
||||||
|
|
||||||
let bindings = bindgen::Builder::default()
|
let bindings = bindgen::Builder::default()
|
||||||
.header("wrapper.hpp")
|
.header("wrapper.hpp")
|
||||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
|
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
use {
|
||||||
|
crate::nss_bindings::{nss_status, HErrno},
|
||||||
|
libc::{hostent, size_t, socklen_t},
|
||||||
|
nix::errno::Errno,
|
||||||
|
std::os::raw::{c_char, c_int, c_void},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async unsafe fn gethostbyaddr2_r(
|
||||||
|
_addr: *const c_void,
|
||||||
|
_len: socklen_t,
|
||||||
|
_af: c_int,
|
||||||
|
_host: *mut hostent,
|
||||||
|
_buffer: *mut c_char,
|
||||||
|
_buflen: size_t,
|
||||||
|
_errnop: *mut Errno,
|
||||||
|
_h_errnop: *mut HErrno,
|
||||||
|
_ttlp: *mut i32,
|
||||||
|
) -> nss_status {
|
||||||
|
todo!()
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
use {
|
||||||
|
crate::helpers::{ips_to_gaih_addr, set_if_valid},
|
||||||
|
crate::nss_bindings::{gaih_addrtuple, nss_status, HErrno},
|
||||||
|
crate::policy_checker::{PolicyChecker as _, POLICY_CHECKER},
|
||||||
|
libc::{hostent, size_t},
|
||||||
|
nix::errno::Errno,
|
||||||
|
std::ffi::CStr,
|
||||||
|
std::os::raw::{c_char, c_int},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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() {
|
||||||
|
Ok(name) => name,
|
||||||
|
Err(_) => {
|
||||||
|
set_if_valid(errnop, Errno::EINVAL);
|
||||||
|
set_if_valid(h_errnop, HErrno::Internal);
|
||||||
|
return nss_status::NSS_STATUS_TRYAGAIN;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// disable application-based DNS for those applications
|
||||||
|
// (notably, Firefox) that support it
|
||||||
|
if name == CANARY_HOSTNAME {
|
||||||
|
set_if_valid(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
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("{}", err);
|
||||||
|
nss_status::NSS_STATUS_UNAVAIL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("{}", err);
|
||||||
|
nss_status::NSS_STATUS_UNAVAIL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
) -> nss_status {
|
||||||
|
todo!()
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
use {
|
||||||
|
crate::nss_bindings::gaih_addrtuple,
|
||||||
|
anyhow::{bail, Result},
|
||||||
|
libc::{AF_INET, AF_INET6},
|
||||||
|
nix::errno::Errno,
|
||||||
|
once_cell::sync::Lazy,
|
||||||
|
std::ffi::CString,
|
||||||
|
std::mem::{align_of, size_of},
|
||||||
|
std::net::IpAddr,
|
||||||
|
trust_dns_resolver::lookup_ip::LookupIp,
|
||||||
|
};
|
||||||
|
|
||||||
|
static RUNTIME: Lazy<std::io::Result<tokio::runtime::Handle>> = 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())
|
||||||
|
});
|
||||||
|
|
||||||
|
pub unsafe fn ips_to_gaih_addr(
|
||||||
|
ips: LookupIp,
|
||||||
|
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 {
|
||||||
|
// First add the name to the buffer
|
||||||
|
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);
|
||||||
|
|
||||||
|
let l = name_src.len();
|
||||||
|
if buf.len() < offset + l {
|
||||||
|
return Err(Errno::ERANGE.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ptr::copy_nonoverlapping(name_src.as_ptr(), name_dest, l);
|
||||||
|
buf = &mut buf[(offset + l)..];
|
||||||
|
|
||||||
|
// Then add the tuple with the address
|
||||||
|
let offset = buf.as_ptr().align_offset(align_of::<gaih_addrtuple>());
|
||||||
|
let l = GAIH_ADDRTUPLE_SZ;
|
||||||
|
if buf.len() < offset + l {
|
||||||
|
return Err(Errno::ERANGE.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let tuple = &mut *(buf.as_mut_ptr().add(offset) as *mut gaih_addrtuple);
|
||||||
|
tuple.next = std::ptr::null_mut();
|
||||||
|
tuple.name = name_dest as *mut i8;
|
||||||
|
tuple.scopeid = 0; // how to set tuple.scopeid ????
|
||||||
|
set_if_valid(prev_link, &mut *tuple); // link from previous tuple to this tuple
|
||||||
|
prev_link = &mut (*tuple).next;
|
||||||
|
|
||||||
|
match addr {
|
||||||
|
IpAddr::V4(addr) => {
|
||||||
|
tuple.family = AF_INET;
|
||||||
|
tuple.addr[0] = std::mem::transmute_copy(&addr.octets());
|
||||||
|
}
|
||||||
|
IpAddr::V6(addr) => {
|
||||||
|
tuple.family = AF_INET6;
|
||||||
|
tuple.addr = std::mem::transmute_copy(&addr.octets());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ret == std::ptr::null_mut() {
|
||||||
|
ret = tuple;
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = &mut buf[(offset + l)..];
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_if_valid<T>(ptr: *mut T, val: T) {
|
||||||
|
if !ptr.is_null() {
|
||||||
|
unsafe { *ptr = val };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn block_on<F>(f: F) -> Result<F::Output>
|
||||||
|
where
|
||||||
|
F: std::future::Future,
|
||||||
|
{
|
||||||
|
use std::ops::Deref;
|
||||||
|
match RUNTIME.deref() {
|
||||||
|
Ok(rt_handle) => Ok(rt_handle.block_on(async { f.await })),
|
||||||
|
Err(e) => bail!("Unable to start tokio runtime: {}", e),
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,9 @@
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
mod constants;
|
mod constants;
|
||||||
|
mod gethostbyaddr;
|
||||||
|
mod gethostbyname;
|
||||||
|
mod helpers;
|
||||||
pub mod nss_api;
|
pub mod nss_api;
|
||||||
|
mod nss_bindings;
|
||||||
mod policy_checker;
|
mod policy_checker;
|
||||||
mod utils;
|
|
||||||
|
|
118
src/nss_api.rs
118
src/nss_api.rs
|
@ -1,75 +1,73 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#![allow(dead_code)]
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
|
||||||
|
|
||||||
use {
|
use {
|
||||||
crate::policy_checker::{MalcontentPolicyChecker, PolicyChecker as _},
|
crate::gethostbyaddr::gethostbyaddr2_r,
|
||||||
crate::utils::set_if_valid,
|
crate::gethostbyname::{gethostbyname3_r, gethostbyname4_r},
|
||||||
|
crate::helpers::{block_on, set_if_valid},
|
||||||
|
crate::nss_bindings::{gaih_addrtuple, nss_status, HErrno},
|
||||||
libc::{hostent, size_t, socklen_t, AF_INET},
|
libc::{hostent, size_t, socklen_t, AF_INET},
|
||||||
nix::errno,
|
nix::errno,
|
||||||
nix::errno::Errno,
|
nix::errno::Errno,
|
||||||
std::ffi::CStr,
|
|
||||||
std::os::raw::{c_char, c_int, c_void},
|
std::os::raw::{c_char, c_int, c_void},
|
||||||
std::ptr,
|
std::ptr,
|
||||||
};
|
};
|
||||||
|
|
||||||
// See https://support.mozilla.org/en-US/kb/configuring-networks-disable-dns-over-https
|
|
||||||
const CANARY_HOSTNAME: &[u8] = b"use-application-dns.net\0";
|
|
||||||
|
|
||||||
// -------------- by host ---------------
|
// -------------- by host ---------------
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn _nss_malcontent_gethostbyname4_r(
|
pub unsafe extern "C" fn _nss_malcontent_gethostbyname4_r(
|
||||||
name: *const c_char,
|
name: *const c_char,
|
||||||
_pat: *mut *mut gaih_addrtuple,
|
pat: *mut *mut gaih_addrtuple,
|
||||||
_buffer: *mut c_char,
|
buffer: *mut c_char,
|
||||||
_buflen: size_t,
|
buflen: size_t,
|
||||||
errnop: *mut Errno,
|
errnop: *mut Errno,
|
||||||
h_errnop: *mut HErrno,
|
h_errnop: *mut HErrno,
|
||||||
_ttlp: *mut i32,
|
ttlp: *mut i32,
|
||||||
) -> nss_status {
|
) -> nss_status {
|
||||||
let name = CStr::from_ptr(name);
|
|
||||||
let policy_checker = MalcontentPolicyChecker::new(); // TODO: make LazySync, use RefCell for interior mutability
|
|
||||||
match policy_checker.resolver(None) {
|
|
||||||
Ok(None) => nss_status::NSS_STATUS_NOTFOUND,
|
|
||||||
Ok(Some(resolver)) => {
|
|
||||||
if name == CStr::from_bytes_with_nul_unchecked(CANARY_HOSTNAME) {
|
|
||||||
set_if_valid(errnop, errno::from_i32(0));
|
set_if_valid(errnop, errno::from_i32(0));
|
||||||
set_if_valid(h_errnop, HErrno::HostNotFound);
|
set_if_valid(h_errnop, HErrno::Success);
|
||||||
return nss_status::NSS_STATUS_SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
match resolver.lookup_ip(name.to_str().unwrap()) {
|
match block_on(async {
|
||||||
Ok(_result) => {
|
gethostbyname4_r(name, pat, buffer, buflen, errnop, h_errnop, ttlp).await
|
||||||
// TODO
|
}) {
|
||||||
// https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/posix/getaddrinfo.c;h=fed2d3bf130b6fc88e5c6840804bbadb8fe9a98b;hb=4ab2ab03d4351914ee53248dc5aef4a8c88ff8b9
|
Ok(status) => status,
|
||||||
// https://chromium.googlesource.com/chromiumos/third_party/glibc/+/cvs/fedora-glibc-2_8_90-1/nss/nss_files/files-hosts.c
|
Err(runtime_error) => {
|
||||||
// https://github.com/systemd/systemd/blob/d5548eb618d426a3952746eb3a0e7d95d186ea7e/src/nss-resolve/nss-resolve.c
|
log::error!("gethostbyname4_r: {}", runtime_error);
|
||||||
|
set_if_valid(h_errnop, HErrno::Internal);
|
||||||
nss_status::NSS_STATUS_SUCCESS
|
nss_status::NSS_STATUS_TRYAGAIN
|
||||||
}
|
}
|
||||||
Err(_) => nss_status::NSS_STATUS_UNAVAIL,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_err) => nss_status::NSS_STATUS_UNAVAIL,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn _nss_malcontent_gethostbyname3_r(
|
pub unsafe extern "C" fn _nss_malcontent_gethostbyname3_r(
|
||||||
_name: *const c_char,
|
name: *const c_char,
|
||||||
_af: c_int,
|
af: c_int,
|
||||||
_host: *mut hostent,
|
host: *mut hostent,
|
||||||
_buffer: *mut c_char,
|
buffer: *mut c_char,
|
||||||
_buflen: size_t,
|
buflen: size_t,
|
||||||
_errnop: *mut Errno,
|
errnop: *mut Errno,
|
||||||
_h_errnop: *mut HErrno,
|
h_errnop: *mut HErrno,
|
||||||
_ttlp: *mut i32,
|
ttlp: *mut i32,
|
||||||
_canonp: *mut *mut char,
|
canonp: *mut *mut char,
|
||||||
) -> nss_status {
|
) -> nss_status {
|
||||||
todo!()
|
set_if_valid(errnop, errno::from_i32(0));
|
||||||
|
set_if_valid(h_errnop, HErrno::Success);
|
||||||
|
|
||||||
|
match block_on(async {
|
||||||
|
gethostbyname3_r(
|
||||||
|
name, af, host, buffer, buflen, errnop, h_errnop, ttlp, canonp,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}) {
|
||||||
|
Ok(status) => status,
|
||||||
|
Err(runtime_error) => {
|
||||||
|
log::error!("gethostbyname3_r: {}", runtime_error);
|
||||||
|
set_if_valid(h_errnop, HErrno::Internal);
|
||||||
|
nss_status::NSS_STATUS_TRYAGAIN
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
@ -121,17 +119,29 @@ pub unsafe extern "C" fn _nss_malcontent_gethostbyname_r(
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn _nss_malcontent_gethostbyaddr2_r(
|
pub unsafe extern "C" fn _nss_malcontent_gethostbyaddr2_r(
|
||||||
_addr: *const c_void,
|
addr: *const c_void,
|
||||||
_len: socklen_t,
|
len: socklen_t,
|
||||||
_af: c_int,
|
af: c_int,
|
||||||
_host: *mut hostent,
|
host: *mut hostent,
|
||||||
_buffer: *mut c_char,
|
buffer: *mut c_char,
|
||||||
_buflen: size_t,
|
buflen: size_t,
|
||||||
_errnop: *mut Errno,
|
errnop: *mut Errno,
|
||||||
_h_errnop: *mut HErrno,
|
h_errnop: *mut HErrno,
|
||||||
_ttlp: *mut i32,
|
ttlp: *mut i32,
|
||||||
) -> nss_status {
|
) -> nss_status {
|
||||||
todo!()
|
set_if_valid(errnop, errno::from_i32(0));
|
||||||
|
set_if_valid(h_errnop, HErrno::Success);
|
||||||
|
|
||||||
|
match block_on(async {
|
||||||
|
gethostbyaddr2_r(addr, len, af, host, buffer, buflen, errnop, h_errnop, ttlp).await
|
||||||
|
}) {
|
||||||
|
Ok(status) => status,
|
||||||
|
Err(runtime_error) => {
|
||||||
|
log::error!("gethostbyaddr2_r: {}", runtime_error);
|
||||||
|
set_if_valid(h_errnop, HErrno::Internal);
|
||||||
|
nss_status::NSS_STATUS_TRYAGAIN
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
pub fn set_if_valid<T>(ptr: *mut T, val: T) {
|
#![allow(dead_code)]
|
||||||
if !ptr.is_null() {
|
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
||||||
unsafe { *ptr = val };
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,15 +9,18 @@ use {
|
||||||
std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||||
std::sync::{Arc, RwLock},
|
std::sync::{Arc, RwLock},
|
||||||
trust_dns_resolver::config as dns_config,
|
trust_dns_resolver::config as dns_config,
|
||||||
trust_dns_resolver::Resolver,
|
trust_dns_resolver::TokioAsyncResolver,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type Restrictions<'a> = &'a [IpAddr];
|
pub type Restrictions<'a> = &'a [IpAddr];
|
||||||
|
|
||||||
pub trait PolicyChecker {
|
pub trait PolicyChecker {
|
||||||
fn resolver(&self, user: Option<Uid>) -> Result<Option<Arc<Resolver>>>;
|
fn resolver(&self, user: Option<Uid>) -> Result<Option<Arc<TokioAsyncResolver>>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub static POLICY_CHECKER: Lazy<MalcontentPolicyChecker> =
|
||||||
|
Lazy::new(|| MalcontentPolicyChecker::new());
|
||||||
|
|
||||||
static CLOUDFLARE_PARENTALCONTROL_ADDRS: Lazy<Vec<IpAddr>> = Lazy::new(|| {
|
static CLOUDFLARE_PARENTALCONTROL_ADDRS: Lazy<Vec<IpAddr>> = Lazy::new(|| {
|
||||||
vec![
|
vec![
|
||||||
IpAddr::V4(Ipv4Addr::new(1, 1, 1, 3)),
|
IpAddr::V4(Ipv4Addr::new(1, 1, 1, 3)),
|
||||||
|
@ -27,10 +30,9 @@ static CLOUDFLARE_PARENTALCONTROL_ADDRS: Lazy<Vec<IpAddr>> = Lazy::new(|| {
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: make unique per process
|
|
||||||
// TODO: accept notifications about config changes
|
// TODO: accept notifications about config changes
|
||||||
pub struct MalcontentPolicyChecker {
|
pub struct MalcontentPolicyChecker {
|
||||||
resolvers: RwLock<HashMap<Uid, Option<Arc<Resolver>>>>,
|
resolvers: RwLock<HashMap<Uid, Option<Arc<TokioAsyncResolver>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MalcontentPolicyChecker {
|
impl MalcontentPolicyChecker {
|
||||||
|
@ -53,7 +55,7 @@ impl MalcontentPolicyChecker {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PolicyChecker for MalcontentPolicyChecker {
|
impl PolicyChecker for MalcontentPolicyChecker {
|
||||||
fn resolver(&self, user: Option<Uid>) -> Result<Option<Arc<Resolver>>> {
|
fn resolver(&self, user: Option<Uid>) -> Result<Option<Arc<TokioAsyncResolver>>> {
|
||||||
let user = user.unwrap_or_else(|| getuid());
|
let user = user.unwrap_or_else(|| getuid());
|
||||||
{
|
{
|
||||||
let ro_resolvers = self.resolvers.read().unwrap();
|
let ro_resolvers = self.resolvers.read().unwrap();
|
||||||
|
@ -84,8 +86,10 @@ impl PolicyChecker for MalcontentPolicyChecker {
|
||||||
config
|
config
|
||||||
});
|
});
|
||||||
|
|
||||||
let resolver =
|
let resolver = TokioAsyncResolver::tokio(
|
||||||
Resolver::new(resolver_config, dns_config::ResolverOpts::default())?;
|
resolver_config,
|
||||||
|
dns_config::ResolverOpts::default(),
|
||||||
|
)?;
|
||||||
Some(Arc::new(resolver))
|
Some(Arc::new(resolver))
|
||||||
}
|
}
|
||||||
None => None,
|
None => None,
|
||||||
|
|
131
tests/common.rs
131
tests/common.rs
|
@ -27,38 +27,98 @@ use {
|
||||||
tokio::task::JoinHandle,
|
tokio::task::JoinHandle,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type Eai = EaiRetcode;
|
// Adapted from rusty_forkfork (which inherits it from rusty_fork)
|
||||||
|
// to allow a custom pre-fork function
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! fork_test {
|
||||||
|
(#![rusty_fork(timeout_ms = $timeout:expr)]
|
||||||
|
$(
|
||||||
|
$(#[$meta:meta])*
|
||||||
|
fn $test_name:ident() $(-> $ret:ty)? $body:block
|
||||||
|
)*) => { $(
|
||||||
|
$(#[$meta])*
|
||||||
|
fn $test_name() {
|
||||||
|
// Eagerly convert everything to function pointers so that all
|
||||||
|
// tests use the same instantiation of `fork`.
|
||||||
|
fn body_fn() $(-> $ret)? $body
|
||||||
|
let body: fn () $(-> $ret)? = body_fn;
|
||||||
|
|
||||||
static SETUP: Once = Once::new();
|
fn supervise_fn(child: &mut rusty_forkfork::ChildWrapper,
|
||||||
|
_file: &mut ::std::fs::File) {
|
||||||
|
rusty_forkfork::fork_test::supervise_child(child, $timeout)
|
||||||
|
}
|
||||||
|
let supervise:
|
||||||
|
fn (&mut rusty_forkfork::ChildWrapper, &mut ::std::fs::File) =
|
||||||
|
supervise_fn;
|
||||||
|
|
||||||
pub fn setup() -> Result<()> {
|
rusty_forkfork::fork(
|
||||||
|
rusty_forkfork::rusty_fork_test_name!($test_name),
|
||||||
|
rusty_forkfork::rusty_fork_id!(),
|
||||||
|
$crate::common::prefork_setup,
|
||||||
|
supervise, body).expect("forking test failed");
|
||||||
|
}
|
||||||
|
)* };
|
||||||
|
|
||||||
|
($(
|
||||||
|
$(#[$meta:meta])*
|
||||||
|
fn $test_name:ident() $(-> $ret:ty)? $body:block
|
||||||
|
)*) => {
|
||||||
|
fork_test! {
|
||||||
|
#![rusty_fork(timeout_ms = 0)]
|
||||||
|
|
||||||
|
$($(#[$meta])* fn $test_name() $(-> $ret)? $body)*
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prefork_setup(_child: &mut Command) {
|
||||||
let out_dir = PathBuf::from(env!("OUT_DIR"));
|
let out_dir = PathBuf::from(env!("OUT_DIR"));
|
||||||
let nss_config_status = unsafe {
|
|
||||||
SETUP.call_once(|| {
|
PREFORK.call_once(|| {
|
||||||
|
std::env::set_var("LD_LIBRARY_PATH", &out_dir);
|
||||||
|
|
||||||
let library_path = test_cdylib::build_current_project();
|
let library_path = test_cdylib::build_current_project();
|
||||||
let mut library_filename = library_path.file_name().unwrap().to_owned();
|
let mut library_filename = library_path.file_name().unwrap().to_owned();
|
||||||
library_filename.push(".2"); // required for NSS modules
|
library_filename.push(".2"); // required for NSS modules
|
||||||
|
|
||||||
let dest = out_dir.join(library_filename);
|
let dest = out_dir.join(library_filename);
|
||||||
std::fs::copy(library_path, dest).unwrap();
|
std::fs::copy(library_path, &dest).unwrap();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Eai = EaiRetcode;
|
||||||
|
|
||||||
|
static PREFORK: Once = Once::new();
|
||||||
|
static SETUP: Once = Once::new();
|
||||||
|
|
||||||
|
pub fn setup() -> Result<()> {
|
||||||
|
SETUP.call_once(|| {
|
||||||
|
let nss_config_status = unsafe {
|
||||||
let db = CString::new("hosts").unwrap();
|
let db = CString::new("hosts").unwrap();
|
||||||
let resolvers = CString::new("malcontent dns").unwrap();
|
let resolvers = CString::new("malcontent dns").unwrap();
|
||||||
__nss_configure_lookup(db.as_ptr(), resolvers.as_ptr())
|
__nss_configure_lookup(db.as_ptr(), resolvers.as_ptr())
|
||||||
};
|
};
|
||||||
|
|
||||||
ensure!(
|
if nss_config_status != 0 {
|
||||||
nss_config_status == 0,
|
panic!(
|
||||||
"Unable to configure NSS to load module: __nss_configure_lookup() returned {}",
|
"Unable to configure NSS to load module: __nss_configure_lookup() returned {}",
|
||||||
nss_config_status
|
nss_config_status
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn system_resolve(host: &str) -> Result<IpAddr> {
|
pub fn resolve_with_system(family: libc::c_int, host: &str) -> Result<IpAddr> {
|
||||||
let process = Command::new("getent").arg("hosts").arg(host).output()?;
|
let process = Command::new("getent")
|
||||||
|
.arg(match family {
|
||||||
|
libc::AF_INET => "ahostsv4",
|
||||||
|
libc::AF_INET6 => "ahostsv6",
|
||||||
|
_ => panic!("INET family must be either IPv4 or IPv6"),
|
||||||
|
})
|
||||||
|
.arg(host)
|
||||||
|
.output()?;
|
||||||
ensure!(
|
ensure!(
|
||||||
process.status.success(),
|
process.status.success(),
|
||||||
"Failed to run getent to check host IP"
|
"Failed to run getent to check host IP"
|
||||||
|
@ -72,29 +132,26 @@ pub fn system_resolve(host: &str) -> Result<IpAddr> {
|
||||||
Ok(IpAddr::from_str(&addr_string)?)
|
Ok(IpAddr::from_str(&addr_string)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_addrinfo(sa: &SockaddrStorage) -> Result<IpAddr> {
|
pub fn resolve_with_module(family: libc::c_int, hostname: &str) -> Result<IpAddr> {
|
||||||
if let Some(addr) = sa.as_sockaddr_in() {
|
assert!(
|
||||||
Ok(IpAddr::V4(Ipv4Addr::from(addr.ip())))
|
SETUP.is_completed(),
|
||||||
} else if let Some(addr) = sa.as_sockaddr_in6() {
|
"Forgot to call common::setup() at beginning of test?"
|
||||||
Ok(IpAddr::V6(Ipv6Addr::from(addr.ip())))
|
);
|
||||||
} else {
|
|
||||||
bail!("addrinfo is not either an IPv4 or IPv6 address")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resolve_system_and_us(hostname: &str) -> Result<(IpAddr, IpAddr)> {
|
|
||||||
setup()?;
|
|
||||||
let ip_from_system = system_resolve(hostname)?;
|
|
||||||
|
|
||||||
let c_hostname = CString::new(hostname).unwrap();
|
let c_hostname = CString::new(hostname).unwrap();
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut addr = std::ptr::null_mut();
|
let mut addr = std::ptr::null_mut();
|
||||||
let getaddrinfo_status = getaddrinfo(
|
let hints = libc::addrinfo {
|
||||||
c_hostname.as_ptr(),
|
ai_family: family,
|
||||||
std::ptr::null(),
|
ai_flags: 0,
|
||||||
std::ptr::null(),
|
ai_socktype: 0,
|
||||||
&mut addr,
|
ai_protocol: 0,
|
||||||
);
|
ai_addrlen: 0,
|
||||||
|
ai_addr: std::ptr::null_mut(),
|
||||||
|
ai_canonname: std::ptr::null_mut(),
|
||||||
|
ai_next: std::ptr::null_mut(),
|
||||||
|
};
|
||||||
|
let getaddrinfo_status =
|
||||||
|
getaddrinfo(c_hostname.as_ptr(), std::ptr::null(), &hints, &mut addr);
|
||||||
|
|
||||||
let error = CStr::from_ptr(gai_strerror(getaddrinfo_status));
|
let error = CStr::from_ptr(gai_strerror(getaddrinfo_status));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -106,9 +163,19 @@ pub fn resolve_system_and_us(hostname: &str) -> Result<(IpAddr, IpAddr)> {
|
||||||
);
|
);
|
||||||
let addr_storage = SockaddrStorage::from_raw((*addr).ai_addr, Some((*addr).ai_addrlen))
|
let addr_storage = SockaddrStorage::from_raw((*addr).ai_addr, Some((*addr).ai_addrlen))
|
||||||
.ok_or(anyhow!("Garbled addrinfo from getaddrinfo()"))?;
|
.ok_or(anyhow!("Garbled addrinfo from getaddrinfo()"))?;
|
||||||
let ip_from_us = convert_addrinfo(&addr_storage)?;
|
let ip = convert_addrinfo(&addr_storage)?;
|
||||||
freeaddrinfo(addr);
|
freeaddrinfo(addr);
|
||||||
Ok((ip_from_system, ip_from_us))
|
Ok(ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_addrinfo(sa: &SockaddrStorage) -> Result<IpAddr> {
|
||||||
|
if let Some(addr) = sa.as_sockaddr_in() {
|
||||||
|
Ok(IpAddr::V4(Ipv4Addr::from(addr.ip())))
|
||||||
|
} else if let Some(addr) = sa.as_sockaddr_in6() {
|
||||||
|
Ok(IpAddr::V6(Ipv6Addr::from(addr.ip())))
|
||||||
|
} else {
|
||||||
|
bail!("addrinfo is not either an IPv4 or IPv6 address")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,43 +24,23 @@ static CLOUDFLARE_PARENTALCONTROL_ADDRS: Lazy<Vec<IpAddr>> = Lazy::new(|| {
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
#[test]
|
fork_test! {
|
||||||
#[ignore]
|
#[test]
|
||||||
fn nss_module_is_loaded() -> Result<()> {
|
fn nss_module_is_loaded() -> Result<()> {
|
||||||
common::setup()?;
|
common::setup()?;
|
||||||
|
common::resolve_with_module(libc::AF_INET, "gnome.org")?;
|
||||||
let hostname = std::ffi::CString::new("gnome.org").unwrap();
|
|
||||||
unsafe {
|
|
||||||
let mut addr = std::ptr::null_mut();
|
|
||||||
let getaddrinfo_status = getaddrinfo(
|
|
||||||
hostname.as_ptr(),
|
|
||||||
std::ptr::null(),
|
|
||||||
std::ptr::null(),
|
|
||||||
&mut addr,
|
|
||||||
);
|
|
||||||
|
|
||||||
let error = std::ffi::CStr::from_ptr(gai_strerror(getaddrinfo_status));
|
|
||||||
assert_eq!(
|
|
||||||
getaddrinfo_status,
|
|
||||||
0,
|
|
||||||
"Unable to resolve hostname, getaddrinfo failed: {}",
|
|
||||||
error.to_str().unwrap()
|
|
||||||
);
|
|
||||||
freeaddrinfo(addr);
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[test]
|
||||||
#[ignore]
|
fn application_dns_is_nxdomain() -> Result<()> {
|
||||||
async 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(),
|
getuid(),
|
||||||
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
|
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
|
||||||
)]));
|
)]));
|
||||||
|
|
||||||
common::setup()?;
|
|
||||||
let hostname = std::ffi::CString::new("use-application-dns.net").unwrap();
|
let hostname = std::ffi::CString::new("use-application-dns.net").unwrap();
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut addr = std::ptr::null_mut();
|
let mut addr = std::ptr::null_mut();
|
||||||
|
@ -82,41 +62,73 @@ async fn application_dns_is_nxdomain() -> Result<()> {
|
||||||
};
|
};
|
||||||
|
|
||||||
timeout(Duration::from_secs(1), dbus).await??
|
timeout(Duration::from_secs(1), dbus).await??
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[test]
|
||||||
#[ignore]
|
fn wikipedia_is_unrestricted() -> Result<()> {
|
||||||
async fn wikipedia_is_unrestricted() -> 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(),
|
getuid(),
|
||||||
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
|
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
|
||||||
)]));
|
)]));
|
||||||
|
|
||||||
let (system_addr, our_addr) = common::resolve_system_and_us("wikipedia.org")?;
|
const HOSTNAME: &str = "wikipedia.org";
|
||||||
|
|
||||||
|
for family in [libc::AF_INET, libc::AF_INET6] {
|
||||||
|
let system_addr = common::resolve_with_system(family, HOSTNAME)?;
|
||||||
|
let our_addr = common::resolve_with_module(family, HOSTNAME)?;
|
||||||
assert_eq!(system_addr, our_addr);
|
assert_eq!(system_addr, our_addr);
|
||||||
timeout(Duration::from_secs(1), dbus).await??
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
timeout(Duration::from_secs(1), dbus).await??
|
||||||
#[ignore]
|
})
|
||||||
async fn adultsite_is_restricted() -> Result<()> {
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adultsite_is_restricted() -> 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(),
|
getuid(),
|
||||||
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
|
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
|
||||||
)]));
|
)]));
|
||||||
|
|
||||||
let (system_addr, our_addr) = common::resolve_system_and_us("pornhub.com")?;
|
const HOSTNAME: &str = "pornhub.com";
|
||||||
|
let system_addr = common::resolve_with_system(libc::AF_INET, HOSTNAME)?;
|
||||||
|
let our_addr = common::resolve_with_module(libc::AF_INET, HOSTNAME)?;
|
||||||
assert_ne!(system_addr, our_addr);
|
assert_ne!(system_addr, our_addr);
|
||||||
assert_eq!(our_addr, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)));
|
assert_eq!(our_addr, IpAddr::V4(Ipv4Addr::UNSPECIFIED));
|
||||||
timeout(Duration::from_secs(1), dbus).await??
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
let system_addr = common::resolve_with_system(libc::AF_INET6, HOSTNAME)?;
|
||||||
#[ignore]
|
let our_addr = common::resolve_with_module(libc::AF_INET6, HOSTNAME)?;
|
||||||
async fn privileged_user_bypasses_restrictions() -> Result<()> {
|
assert_ne!(system_addr, our_addr);
|
||||||
|
assert_eq!(our_addr, IpAddr::V6(Ipv6Addr::UNSPECIFIED));
|
||||||
|
|
||||||
|
timeout(Duration::from_secs(1), dbus).await??
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn privileged_user_bypasses_restrictions() -> Result<()> {
|
||||||
|
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 */])]));
|
||||||
|
|
||||||
let (system_addr, our_addr) = common::resolve_system_and_us("pornhub.com")?;
|
const HOSTNAME: &str = "pornhub.com";
|
||||||
|
|
||||||
|
for family in [libc::AF_INET, libc::AF_INET6] {
|
||||||
|
let system_addr = common::resolve_with_system(family, HOSTNAME)?;
|
||||||
|
let our_addr = common::resolve_with_module(family, HOSTNAME)?;
|
||||||
assert_eq!(system_addr, our_addr);
|
assert_eq!(system_addr, our_addr);
|
||||||
|
}
|
||||||
|
|
||||||
timeout(Duration::from_secs(1), dbus).await??
|
timeout(Duration::from_secs(1), dbus).await??
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,9 @@ enum class HErrno {
|
||||||
TryAgain = TRY_AGAIN,
|
TryAgain = TRY_AGAIN,
|
||||||
NoRecovery = NO_RECOVERY,
|
NoRecovery = NO_RECOVERY,
|
||||||
NoData = NO_DATA,
|
NoData = NO_DATA,
|
||||||
|
#ifdef __USE_MISC
|
||||||
|
Internal = NETDB_INTERNAL,
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class EaiRetcode {
|
enum class EaiRetcode {
|
||||||
|
|
Loading…
Reference in New Issue