dns: NSS module implementation
This commit is contained in:
parent
3beee1cf84
commit
03fbdb2d26
|
@ -1,6 +1,6 @@
|
|||
project('malcontent', 'c',
|
||||
version : '0.12.0',
|
||||
meson_version : '>= 0.59.0',
|
||||
meson_version : '>= 0.60.0',
|
||||
license: ['LGPL-2.1-or-later', 'GPL-2.0-or-later'],
|
||||
default_options : [
|
||||
'buildtype=debugoptimized',
|
||||
|
@ -154,3 +154,7 @@ if get_option('ui').enabled()
|
|||
endif
|
||||
subdir('pam')
|
||||
subdir('po')
|
||||
|
||||
if get_option('dns').enabled()
|
||||
subdir('nss')
|
||||
endif
|
||||
|
|
|
@ -9,6 +9,12 @@ option(
|
|||
type: 'string',
|
||||
description: 'directory for PAM modules'
|
||||
)
|
||||
option(
|
||||
'dns',
|
||||
type: 'feature',
|
||||
value: 'enabled',
|
||||
description: 'enable NSS module support (for parental DNS controls)'
|
||||
)
|
||||
option(
|
||||
'ui',
|
||||
type: 'feature',
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
# SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
*.orig
|
||||
*~
|
||||
|
||||
.gdbinit
|
||||
.lldbinit
|
||||
|
||||
/target
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Cargo integration_test",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--test=integration_test"
|
||||
]
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"RUST_BACKTRACE": "full",
|
||||
"NSS_MALCONTENT_LOG": "trace",
|
||||
},
|
||||
"preRunCommands": [
|
||||
"settings set target.process.follow-fork-mode child",
|
||||
"settings set target.process.stop-on-exec false"
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
SPDX-License-Identifier: CC0-1.0
|
|
@ -0,0 +1,54 @@
|
|||
# SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
[package]
|
||||
name = "malcontent-nss"
|
||||
version = "0.1.0"
|
||||
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 = "unwind" # We rely on this
|
||||
|
||||
[features]
|
||||
integration_test = ["dep:zbus_names"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
name = "nss_malcontent"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
gethostname = "0.4"
|
||||
libc = "0.2"
|
||||
once_cell = "1.13"
|
||||
ld_preload = "0.1"
|
||||
log = "0.4"
|
||||
nix = { version = "0.27", features = ["socket", "user", "net", "sched"] }
|
||||
serde = "1.0"
|
||||
syslog = "6.1"
|
||||
tokio = { version = "1", features = ["rt"] }
|
||||
hickory-resolver = { version = "0.24", features = ["dns-over-native-tls"] }
|
||||
hickory-proto = "0.24"
|
||||
zbus = { version = "3.0", default-features = false, features = ["tokio"] }
|
||||
zvariant = "3.6"
|
||||
zbus_names = { version = "2.2", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
elf = "0.7"
|
||||
malcontent-nss = { path = ".", features = ["integration_test"] }
|
||||
env_logger = "0.10"
|
||||
event-listener = "4.0"
|
||||
rusty-fork = "*"
|
||||
futures-util = "0.3"
|
||||
static_init = "1.0"
|
||||
test-cdylib = "1.1"
|
||||
tokio = { version = "1", features = ["rt", "sync", "macros", "net", "time"] }
|
||||
tokio-util = { version = "0.7" }
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.69"
|
|
@ -0,0 +1,15 @@
|
|||
# Malcontent NSS Module
|
||||
|
||||
This is a NSS module allowing to perform parental controls
|
||||
when querying the system interfaces for hostname resolution (DNS).
|
||||
|
||||
A system administrator would install the module and then add it to the
|
||||
`hosts` entry in `/etc/nsswitch.conf`. For instance:
|
||||
|
||||
```
|
||||
hosts: files myhostname malcontent resolve [!UNAVAIL=return] dns
|
||||
```
|
||||
|
||||
Note how `malcontent` precedes both systemd's `resolve` and the libc `dns` modules.
|
||||
|
||||
Parental control configuration would happen through the Malcontent client or GUI.
|
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
@ -0,0 +1,58 @@
|
|||
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use {std::env, std::io::Error, std::io::ErrorKind, std::path::PathBuf};
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=wrapper.hpp");
|
||||
|
||||
// Required by NSS 2
|
||||
println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,libnss_malcontent.so.2");
|
||||
|
||||
// Pass down the path to the output folder to the integration test binary
|
||||
let cdylib_dir = get_cargo_target_dir().unwrap();
|
||||
println!("cargo:rustc-env=CDYLIB_OUT_DIR={}", cdylib_dir.display());
|
||||
// Set the DT_RPATH of the test binary to search in its own folder first
|
||||
println!("cargo:rustc-link-arg-tests=-Wl,--disable-new-dtags");
|
||||
println!("cargo:rustc-link-arg-tests=-Wl,--rpath=$ORIGIN");
|
||||
|
||||
let bindings = bindgen::Builder::default()
|
||||
.header("wrapper.hpp")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||
.newtype_enum("nss_status")
|
||||
.allowlist_type("nss_status")
|
||||
.newtype_enum("HErrno")
|
||||
.allowlist_type("HErrno")
|
||||
.newtype_enum("EaiRetcode")
|
||||
.allowlist_type("EaiRetcode")
|
||||
.allowlist_type("gaih_addrtuple")
|
||||
.allowlist_function("__nss_configure_lookup")
|
||||
.generate()
|
||||
.expect("Unable to generate bindings");
|
||||
|
||||
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
bindings
|
||||
.write_to_file(out_path.join("bindings.rs"))
|
||||
.expect("Couldn't write bindings!");
|
||||
}
|
||||
|
||||
fn get_cargo_target_dir() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
|
||||
let profile = std::env::var("PROFILE")?;
|
||||
|
||||
let mut path = out_dir.as_path();
|
||||
loop {
|
||||
if let Some(parent) = path.parent() {
|
||||
path = parent;
|
||||
if parent.ends_with(&profile) {
|
||||
return Ok(path.to_path_buf());
|
||||
}
|
||||
} else {
|
||||
return Err(Box::new(Error::new(
|
||||
ErrorKind::NotFound,
|
||||
"Unable to determine cargo build target directory",
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
# SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
if get_option('buildtype') == 'debug'
|
||||
profile = 'dev'
|
||||
target_subdir = 'debug'
|
||||
else
|
||||
profile = 'release'
|
||||
target_subdir = 'release'
|
||||
endif
|
||||
|
||||
dbus_iface_file = meson.project_source_root() / 'accounts-service/com.endlessm.ParentalControls.Dns.xml'
|
||||
env = environment({
|
||||
'DBUS_IFACE': dbus_iface_file,
|
||||
})
|
||||
|
||||
cargo = find_program('cargo')
|
||||
cargo_common_opts = [
|
||||
'--manifest-path', meson.current_source_dir() / 'Cargo.toml',
|
||||
'--workspace',
|
||||
'--profile', profile,
|
||||
'--target-dir', meson.current_build_dir() / 'target',
|
||||
]
|
||||
|
||||
library = 'libnss_malcontent.so'
|
||||
|
||||
build_library = custom_target('cargo-nss_malcontent',
|
||||
output: 'target' / target_subdir / library,
|
||||
build_always_stale: true,
|
||||
command: [cargo, 'build'] + cargo_common_opts,
|
||||
console: true,
|
||||
env: env,
|
||||
)
|
||||
|
||||
custom_target('libnss_malcontent',
|
||||
input: build_library,
|
||||
output: library + '.2', # Required by NSS 3
|
||||
build_by_default: true,
|
||||
command: ['ln', '-f', '@INPUT@', '@OUTPUT@'],
|
||||
env: env,
|
||||
install: true,
|
||||
install_dir: get_option('libdir'),
|
||||
)
|
||||
|
||||
build_tests = custom_target('nss_malcontent-tests',
|
||||
output: 'something-something-something',
|
||||
build_always_stale: true,
|
||||
build_by_default: true,
|
||||
command: [cargo, 'test'] + cargo_common_opts + '--no-run',
|
||||
console: true,
|
||||
env: env
|
||||
)
|
||||
|
||||
test('dns', cargo,
|
||||
args: ['test'] + cargo_common_opts + ['--no-fail-fast', '--message-format', 'json'],
|
||||
env: env,
|
||||
depends: [build_tests],
|
||||
protocol: 'rust',
|
||||
)
|
|
@ -0,0 +1,33 @@
|
|||
// SPDX-FileCopyrightText: 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 with(
|
||||
_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 {
|
||||
// At the moment, we are not handling this function
|
||||
// in our module.
|
||||
//
|
||||
// We assume it is fine to go to the next module
|
||||
// in the nsswitch.conf list to get an authoritative
|
||||
// answer.
|
||||
//
|
||||
// The use case for reverse IP lookup
|
||||
// should not impact parental controls.
|
||||
|
||||
nss_status::NSS_STATUS_UNAVAIL
|
||||
}
|
|
@ -0,0 +1,337 @@
|
|||
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use hickory_resolver::error::ResolveErrorKind;
|
||||
|
||||
use {
|
||||
crate::helpers::{set_if_valid, write_record_name_to_buf, write_vector_to_buf},
|
||||
crate::nss_bindings::{gaih_addrtuple, nss_status, HErrno},
|
||||
crate::policy_checker::POLICY_CHECKER,
|
||||
hickory_proto::rr::record_type::RecordType,
|
||||
hickory_proto::rr::{RData, Record},
|
||||
hickory_resolver::TokioAsyncResolver,
|
||||
hickory_resolver::{lookup::Lookup, lookup_ip::LookupIp},
|
||||
libc::{c_char, c_int, hostent, size_t, AF_INET, AF_INET6},
|
||||
nix::errno,
|
||||
nix::errno::Errno,
|
||||
std::ffi::CStr,
|
||||
std::mem::{align_of, discriminant, size_of},
|
||||
std::sync::Arc,
|
||||
};
|
||||
|
||||
// See https://support.mozilla.org/en-US/kb/configuring-networks-disable-dns-over-https
|
||||
const CANARY_HOSTNAME: &str = "use-application-dns.net";
|
||||
|
||||
#[derive(Debug)]
|
||||
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,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Result {
|
||||
V3(*mut hostent),
|
||||
V4(*mut *mut gaih_addrtuple),
|
||||
}
|
||||
|
||||
pub enum DnsResult {
|
||||
NxDomain,
|
||||
Found,
|
||||
}
|
||||
|
||||
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().await {
|
||||
Ok(None) => {
|
||||
// no restrictions for user, the next NSS module will decide
|
||||
nss_status::NSS_STATUS_NOTFOUND
|
||||
}
|
||||
Ok(Some(resolver)) => resolve_hostname_through(resolver, args).await,
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
nss_status::NSS_STATUS_UNAVAIL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async unsafe fn resolve_hostname_through(
|
||||
resolver: Arc<TokioAsyncResolver>,
|
||||
args: &mut Args,
|
||||
) -> nss_status {
|
||||
let name = match CStr::from_ptr(args.name).to_str() {
|
||||
Ok(name) => name,
|
||||
Err(_) => {
|
||||
set_if_valid(args.errnop, Errno::EINVAL);
|
||||
set_if_valid(args.h_errnop, HErrno::Internal);
|
||||
return nss_status::NSS_STATUS_TRYAGAIN;
|
||||
}
|
||||
};
|
||||
|
||||
// Disable DoH in Firefox unless user forced it manually
|
||||
if name == CANARY_HOSTNAME {
|
||||
set_if_valid(args.h_errnop, HErrno::HostNotFound);
|
||||
return 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).await,
|
||||
libc::AF_INET6 => resolver.lookup(name, RecordType::AAAA).await,
|
||||
_ => return nss_status::NSS_STATUS_NOTFOUND,
|
||||
};
|
||||
|
||||
match lookup {
|
||||
Ok(result) => {
|
||||
log::trace!("lookup: {} => {:?}", name, result);
|
||||
prepare_response(args, result)
|
||||
}
|
||||
Err(err) => match err.kind() {
|
||||
ResolveErrorKind::NoRecordsFound {
|
||||
query: _,
|
||||
soa: _,
|
||||
negative_ttl: _,
|
||||
response_code: _,
|
||||
trusted: _,
|
||||
} => {
|
||||
log::trace!("lookup: {} => NXDOMAIN", name);
|
||||
set_if_valid(args.h_errnop, HErrno::HostNotFound);
|
||||
nss_status::NSS_STATUS_SUCCESS
|
||||
}
|
||||
_ => {
|
||||
log::warn!("{}", err);
|
||||
nss_status::NSS_STATUS_UNAVAIL
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn prepare_response(
|
||||
args: &mut Args,
|
||||
lookup: hickory_resolver::lookup::Lookup,
|
||||
) -> nss_status {
|
||||
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.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(), &mut **hostent, buf) {
|
||||
Ok(DnsResult::Found) => nss_status::NSS_STATUS_SUCCESS,
|
||||
Ok(DnsResult::NxDomain) => {
|
||||
set_if_valid(args.h_errnop, HErrno::HostNotFound);
|
||||
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
|
||||
}
|
||||
|
||||
// TODO: refactor to be less ugly
|
||||
pub unsafe fn records_to_hostent(
|
||||
lookup: Lookup,
|
||||
host: &mut hostent,
|
||||
buf: &mut [u8],
|
||||
) -> std::io::Result<DnsResult> {
|
||||
// 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.
|
||||
|
||||
let first_record = match lookup.record_iter().peekable().peek() {
|
||||
Some(record) => *record,
|
||||
None => return Ok(DnsResult::NxDomain),
|
||||
};
|
||||
|
||||
let first_rdata = match first_record.data() {
|
||||
Some(rdata) => rdata,
|
||||
None => return Err(Errno::ENODATA.into()),
|
||||
};
|
||||
|
||||
// First put the name in the buffer
|
||||
let (buf, name_dest) = write_record_name_to_buf(first_record, buf)?;
|
||||
|
||||
// Then the empty aliases array (just a null pointer)
|
||||
let (mut buf, aliases_ptr) = write_vector_to_buf(buf, vec![std::ptr::null_mut()])?;
|
||||
|
||||
// Then the address list
|
||||
let offset = buf.as_ptr().align_offset(align_of::<*mut c_char>());
|
||||
buf = &mut buf[offset..];
|
||||
|
||||
let records = lookup
|
||||
.record_iter()
|
||||
.flat_map(Record::data)
|
||||
.filter(|r| discriminant(*r) == discriminant(first_rdata));
|
||||
|
||||
let mut addresses = vec![];
|
||||
for record in records {
|
||||
let offset = buf.as_ptr().align_offset(align_of::<*mut c_char>());
|
||||
buf = &mut buf[offset..];
|
||||
addresses.push(buf.as_mut_ptr());
|
||||
|
||||
let l = match record {
|
||||
RData::A(addr) => {
|
||||
let octets = addr.octets();
|
||||
let l = octets.len();
|
||||
if buf.len() < l {
|
||||
return Err(Errno::ERANGE.into());
|
||||
}
|
||||
buf[..l].copy_from_slice(&octets);
|
||||
l
|
||||
}
|
||||
RData::AAAA(addr) => {
|
||||
let octets = addr.octets();
|
||||
let l = octets.len();
|
||||
if buf.len() < l {
|
||||
return Err(Errno::ERANGE.into());
|
||||
}
|
||||
buf[..l].copy_from_slice(&octets);
|
||||
l
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
buf = &mut buf[l..];
|
||||
}
|
||||
|
||||
addresses.push(std::ptr::null_mut());
|
||||
let (_, addr_list) = write_vector_to_buf(buf, addresses)?;
|
||||
|
||||
// Finally, populate the hostent structure
|
||||
host.h_name = name_dest;
|
||||
host.h_aliases = aliases_ptr;
|
||||
host.h_addr_list = addr_list as *mut *mut c_char;
|
||||
match first_rdata {
|
||||
RData::A(_) => {
|
||||
host.h_addrtype = AF_INET;
|
||||
host.h_length = 4;
|
||||
}
|
||||
RData::AAAA(_) => {
|
||||
host.h_addrtype = AF_INET6;
|
||||
host.h_length = 16;
|
||||
}
|
||||
_ => return Err(Errno::EBADMSG.into()),
|
||||
}
|
||||
|
||||
Ok(DnsResult::Found)
|
||||
}
|
||||
|
||||
// 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 mut prev_link: *mut *mut gaih_addrtuple = std::ptr::null_mut();
|
||||
|
||||
let mut name_dest;
|
||||
for record in lookup.record_iter() {
|
||||
// First add the name to the buffer
|
||||
(buf, name_dest) = write_record_name_to_buf(record, buf)?;
|
||||
|
||||
// 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;
|
||||
tuple.scopeid = 0; // how to set tuple.scopeid correctly??? Use a custom trust_dns_resolver::ConnectionProvider?
|
||||
set_if_valid(prev_link, &mut *tuple); // link from previous tuple to this tuple
|
||||
prev_link = &mut (*tuple).next;
|
||||
|
||||
match record.data() {
|
||||
Some(RData::A(addr)) => {
|
||||
tuple.family = AF_INET;
|
||||
tuple.addr[0] = std::mem::transmute_copy(&addr.octets());
|
||||
}
|
||||
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() {
|
||||
ret = tuple;
|
||||
}
|
||||
|
||||
buf = &mut buf[(offset + l)..];
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use {
|
||||
hickory_proto::rr::resource::Record,
|
||||
libc::c_char,
|
||||
nix::errno::Errno,
|
||||
once_cell::sync::Lazy,
|
||||
std::ffi::CString,
|
||||
std::mem::{align_of, size_of},
|
||||
};
|
||||
|
||||
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()
|
||||
.enable_time()
|
||||
.enable_io()
|
||||
.build()?;
|
||||
Ok(rt)
|
||||
});
|
||||
|
||||
pub fn write_record_name_to_buf<'a>(
|
||||
record: &Record,
|
||||
buf: &'a mut [u8],
|
||||
) -> std::io::Result<(&'a mut [u8], *mut c_char)> {
|
||||
use std::io::{Error, ErrorKind};
|
||||
let name = CString::new(record.name().to_utf8())
|
||||
.map_err(|e| Error::new(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 l = name_src.len();
|
||||
if buf.len() < offset + l {
|
||||
return Err(Errno::ERANGE.into());
|
||||
}
|
||||
|
||||
let name_dest = unsafe { buf.as_mut_ptr().add(offset) };
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(name_src.as_ptr(), name_dest, l);
|
||||
}
|
||||
Ok((&mut buf[(offset + l)..], name_dest as *mut c_char))
|
||||
}
|
||||
|
||||
pub fn write_vector_to_buf<'a, T>(
|
||||
buf: &'a mut [u8],
|
||||
v: Vec<T>,
|
||||
) -> std::io::Result<(&'a mut [u8], *mut T)> {
|
||||
let offset = buf.as_ptr().align_offset(align_of::<*mut T>());
|
||||
let l = size_of::<*mut c_char>() * v.len();
|
||||
if buf.len() < offset + l {
|
||||
return Err(Errno::ERANGE.into());
|
||||
}
|
||||
|
||||
let buf = &mut buf[offset..];
|
||||
let dest = buf.as_mut_ptr().cast::<T>();
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(v.as_ptr() as *const T, dest, l);
|
||||
}
|
||||
Ok((&mut buf[l..], dest))
|
||||
}
|
||||
|
||||
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) -> anyhow::Result<F::Output>
|
||||
where
|
||||
F: std::future::Future,
|
||||
{
|
||||
// TODO: use std::panic::catch_unwind() to be resilient
|
||||
// to OOM problems
|
||||
|
||||
use std::ops::Deref;
|
||||
match RUNTIME.deref() {
|
||||
Ok(rt) => Ok(rt.block_on(async { f.await })),
|
||||
Err(e) => anyhow::bail!("Unable to start tokio runtime: {}", e),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
mod gethostbyaddr;
|
||||
mod gethostbyname;
|
||||
mod helpers;
|
||||
pub mod nss_api;
|
||||
mod nss_bindings;
|
||||
mod policy_checker;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
struct LevelFilter(log::LevelFilter);
|
||||
|
||||
impl FromStr for LevelFilter {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let lv = match s.to_lowercase().as_str() {
|
||||
"off" => log::LevelFilter::Off,
|
||||
"error" => log::LevelFilter::Error,
|
||||
"warn" => log::LevelFilter::Warn,
|
||||
"info" => log::LevelFilter::Info,
|
||||
"debug" => log::LevelFilter::Debug,
|
||||
"trace" => log::LevelFilter::Trace,
|
||||
_ => return Err(()),
|
||||
};
|
||||
Ok(Self(lv))
|
||||
}
|
||||
}
|
||||
|
||||
ld_preload::ld_preload_init! {
|
||||
{
|
||||
let level = std::env::var("NSS_MALCONTENT_LOG")
|
||||
.ok()
|
||||
.and_then(|level| { LevelFilter::from_str(&level).ok() })
|
||||
.map(|x| x.0)
|
||||
.unwrap_or(log::LevelFilter::Warn);
|
||||
|
||||
let _ = syslog::init(syslog::Facility::LOG_AUTHPRIV, level, Some("nss_malcontent"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use {
|
||||
crate::helpers::{block_on, set_if_valid},
|
||||
crate::nss_bindings::{gaih_addrtuple, nss_status, HErrno},
|
||||
crate::{gethostbyaddr, gethostbyname},
|
||||
libc::{hostent, size_t, socklen_t, AF_INET},
|
||||
nix::errno::Errno,
|
||||
std::os::raw::{c_char, c_int, c_void},
|
||||
std::ptr,
|
||||
};
|
||||
|
||||
// -------------- by host ---------------
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn _nss_malcontent_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 {
|
||||
log::trace!("--------------- _nss_malcontent_gethostbyname4_r ---------------------");
|
||||
|
||||
let mut args = gethostbyname::Args {
|
||||
name,
|
||||
family: 0,
|
||||
result: gethostbyname::Result::V4(pat),
|
||||
buffer,
|
||||
buflen,
|
||||
errnop,
|
||||
h_errnop,
|
||||
ttlp,
|
||||
};
|
||||
|
||||
match block_on(async { gethostbyname::with(&mut args).await }) {
|
||||
Ok(status) => status,
|
||||
Err(runtime_error) => {
|
||||
log::error!("gethostbyname4_r: {}", runtime_error);
|
||||
set_if_valid(args.h_errnop, HErrno::Internal);
|
||||
nss_status::NSS_STATUS_TRYAGAIN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn _nss_malcontent_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 {
|
||||
log::trace!("--------------- _nss_malcontent_gethostbyname3_r ---------------------");
|
||||
|
||||
let mut args = gethostbyname::Args {
|
||||
name,
|
||||
family: af,
|
||||
result: gethostbyname::Result::V3(host),
|
||||
buffer,
|
||||
buflen,
|
||||
errnop,
|
||||
h_errnop,
|
||||
ttlp,
|
||||
};
|
||||
|
||||
match block_on(async { gethostbyname::with(&mut args).await }) {
|
||||
Ok(status) => {
|
||||
if !host.is_null() {
|
||||
set_if_valid(canonp, (*host).h_name as *mut char);
|
||||
}
|
||||
status
|
||||
}
|
||||
Err(runtime_error) => {
|
||||
log::error!("gethostbyname3_r: {}", runtime_error);
|
||||
set_if_valid(h_errnop, HErrno::Internal);
|
||||
nss_status::NSS_STATUS_TRYAGAIN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn _nss_malcontent_gethostbyname2_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,
|
||||
) -> nss_status {
|
||||
log::trace!("--------------- _nss_malcontent_gethostbyname2_r ---------------------");
|
||||
|
||||
_nss_malcontent_gethostbyname3_r(
|
||||
name,
|
||||
af,
|
||||
host,
|
||||
buffer,
|
||||
buflen,
|
||||
errnop,
|
||||
h_errnop,
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn _nss_malcontent_gethostbyname_r(
|
||||
name: *const c_char,
|
||||
host: *mut hostent,
|
||||
buffer: *mut c_char,
|
||||
buflen: size_t,
|
||||
errnop: *mut Errno,
|
||||
h_errnop: *mut HErrno,
|
||||
) -> nss_status {
|
||||
log::trace!("--------------- _nss_malcontent_gethostbyname_r ---------------------");
|
||||
|
||||
_nss_malcontent_gethostbyname3_r(
|
||||
name,
|
||||
AF_INET,
|
||||
host,
|
||||
buffer,
|
||||
buflen,
|
||||
errnop,
|
||||
h_errnop,
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
)
|
||||
}
|
||||
|
||||
// ----------------- by addr -----------------
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn _nss_malcontent_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 {
|
||||
log::trace!("--------------- _nss_malcontent_gethostbyaddr2_r ---------------------");
|
||||
|
||||
set_if_valid(errnop, nix::errno::from_i32(0));
|
||||
set_if_valid(h_errnop, HErrno::Success);
|
||||
|
||||
match block_on(async {
|
||||
gethostbyaddr::with(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]
|
||||
pub unsafe extern "C" fn _nss_malcontent_gethostbyaddr_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,
|
||||
) -> nss_status {
|
||||
log::trace!("--------------- _nss_malcontent_gethostbyaddr_r ---------------------");
|
||||
|
||||
_nss_malcontent_gethostbyaddr2_r(
|
||||
addr,
|
||||
len,
|
||||
af,
|
||||
host,
|
||||
buffer,
|
||||
buflen,
|
||||
errnop,
|
||||
h_errnop,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#![allow(dead_code)]
|
||||
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
|
@ -0,0 +1,66 @@
|
|||
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use {
|
||||
serde::{Deserialize, Serialize},
|
||||
std::net::IpAddr,
|
||||
zbus::{dbus_proxy, Result},
|
||||
};
|
||||
|
||||
#[dbus_proxy(
|
||||
default_service = "com.endlessm.ParentalControls",
|
||||
interface = "com.endlessm.ParentalControls.Dns",
|
||||
default_path = "/com/endlessm/ParentalControls/Dns",
|
||||
gen_blocking = false
|
||||
)]
|
||||
trait MalcontentDns {
|
||||
fn get_dns(&self) -> Result<Restrictions>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, zvariant::Type)]
|
||||
pub struct Restriction {
|
||||
pub ip: IpAddr,
|
||||
pub hostname: String,
|
||||
}
|
||||
|
||||
pub type Restrictions = Vec<Restriction>;
|
||||
|
||||
pub async fn restrictions() -> anyhow::Result<Vec<Restriction>, anyhow::Error> {
|
||||
#[cfg(not(feature = "integration_test"))]
|
||||
let connection = {
|
||||
// This is the normal behavior
|
||||
zbus::Connection::session().await?
|
||||
};
|
||||
|
||||
#[cfg(feature = "integration_test")]
|
||||
let connection = {
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
// During integration testing, we want to connect to a private
|
||||
// bus name to avoid clashes with existing system services.
|
||||
let socketaddr = std::env::var("TEST_DBUS_SOCKET")
|
||||
.expect("The test has not set the TEST_DBUS_SOCKET environment variable");
|
||||
|
||||
let socket = loop {
|
||||
match TcpStream::connect(&socketaddr).await {
|
||||
Ok(stream) => break stream,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::ConnectionRefused => {
|
||||
tokio::task::yield_now().await;
|
||||
continue;
|
||||
}
|
||||
Err(e) => anyhow::bail!(e),
|
||||
}
|
||||
};
|
||||
|
||||
log::trace!("connecting to DBus on {}", socket.local_addr()?);
|
||||
zbus::ConnectionBuilder::tcp_stream(socket)
|
||||
.p2p()
|
||||
.build()
|
||||
.await?
|
||||
};
|
||||
|
||||
let proxy = MalcontentDnsProxy::builder(&connection).build().await?;
|
||||
let restrictions = proxy.get_dns().await;
|
||||
log::trace!("malcontent-nss: user restrictions are {:?}", &restrictions);
|
||||
Ok(restrictions?)
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
mod dbus;
|
||||
|
||||
use {
|
||||
anyhow::Result,
|
||||
hickory_proto::rr::domain::Name as DomainName,
|
||||
hickory_resolver::config as dns_config,
|
||||
hickory_resolver::TokioAsyncResolver,
|
||||
nix::unistd::{getuid, Uid},
|
||||
once_cell::sync::Lazy,
|
||||
std::collections::HashMap,
|
||||
std::net::{SocketAddr, TcpStream},
|
||||
std::sync::{Arc, RwLock},
|
||||
std::time::Duration,
|
||||
};
|
||||
|
||||
pub use self::dbus::{Restriction, Restrictions};
|
||||
|
||||
const DNS_UDP_PORT: u16 = 53;
|
||||
const DNS_TLS_PORT: u16 = 853;
|
||||
|
||||
pub static POLICY_CHECKER: Lazy<PolicyChecker> = Lazy::new(|| PolicyChecker::new());
|
||||
|
||||
// TODO: accept notifications about config changes
|
||||
pub struct PolicyChecker {
|
||||
resolvers: RwLock<HashMap<Uid, Option<Arc<TokioAsyncResolver>>>>,
|
||||
}
|
||||
|
||||
impl PolicyChecker {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
resolvers: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn restrictions(&self, user: Uid) -> Result<Restrictions> {
|
||||
if user.is_root() {
|
||||
return Ok(vec![]);
|
||||
};
|
||||
|
||||
dbus::restrictions().await
|
||||
}
|
||||
|
||||
pub async fn resolver(&self) -> Result<Option<Arc<TokioAsyncResolver>>> {
|
||||
let user = getuid(); // account for processes changing user during execution
|
||||
|
||||
log::trace!("querying PolicyChecker::resolver() for user {}", user);
|
||||
// Check if already cached
|
||||
{
|
||||
let ro_resolvers = self.resolvers.read().unwrap();
|
||||
if let Some(resolver) = ro_resolvers.get(&user) {
|
||||
return Ok(resolver.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Else, initialize and cache it
|
||||
{
|
||||
let mut rw_resolvers = self.resolvers.write().unwrap();
|
||||
if let Some(resolver) = rw_resolvers.get(&user) {
|
||||
return Ok(resolver.clone());
|
||||
}
|
||||
|
||||
// try first to prime resolver with DoT implementation,
|
||||
// fallback to unencrypted only if not available.
|
||||
let restrictions = self.restrictions(user).await?;
|
||||
let resolver = if !restrictions.is_empty() {
|
||||
let resolver = TokioAsyncResolver::tokio(
|
||||
resolver_config_for(restrictions),
|
||||
dns_config::ResolverOpts::default(),
|
||||
);
|
||||
Some(Arc::new(resolver))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
rw_resolvers.insert(user, resolver.clone());
|
||||
Ok(resolver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolver_config_for(restrictions: Vec<Restriction>) -> dns_config::ResolverConfig {
|
||||
use dns_config::NameServerConfigGroup as NsConfig;
|
||||
|
||||
let resolver_config_group =
|
||||
restrictions
|
||||
.into_iter()
|
||||
.fold(NsConfig::new(), |mut config, restr| {
|
||||
let supports_tls = TcpStream::connect_timeout(
|
||||
&SocketAddr::new(restr.ip, DNS_TLS_PORT),
|
||||
Duration::from_secs(1),
|
||||
)
|
||||
.is_ok();
|
||||
|
||||
let new_config = if supports_tls {
|
||||
NsConfig::from_ips_tls(&[restr.ip], DNS_TLS_PORT, restr.hostname, true)
|
||||
} else {
|
||||
NsConfig::from_ips_clear(&[restr.ip], DNS_UDP_PORT, true)
|
||||
};
|
||||
|
||||
config.merge(new_config);
|
||||
config
|
||||
});
|
||||
|
||||
let basename = gethostname::gethostname()
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.map(|hn| DomainName::from_labels(hn.split('.')).ok())
|
||||
.flatten()
|
||||
.map(|hn| hn.base_name());
|
||||
|
||||
dns_config::ResolverConfig::from_parts(basename, vec![], resolver_config_group)
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
include!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/src/policy_checker/dbus.rs"
|
||||
));
|
||||
|
||||
use {
|
||||
std::sync::atomic::{AtomicUsize, Ordering},
|
||||
tokio_util::sync::CancellationToken,
|
||||
zbus::dbus_interface,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MalcontentDBusMock {
|
||||
responses: Vec<Restrictions>,
|
||||
invocations_left: AtomicUsize,
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "com.endlessm.ParentalControls.Dns")]
|
||||
impl MalcontentDBusMock {
|
||||
fn get_dns(&mut self) -> Restrictions {
|
||||
let restrictions = self
|
||||
.responses
|
||||
.pop()
|
||||
.expect("MockError: DBus mock is saturated");
|
||||
self.invocations_left.fetch_sub(1, Ordering::SeqCst);
|
||||
restrictions
|
||||
}
|
||||
}
|
||||
|
||||
impl MalcontentDBusMock {
|
||||
pub fn new(mut responses: Vec<Restrictions>) -> Self {
|
||||
responses.reverse(); // We pop responses from the back, so...
|
||||
Self {
|
||||
invocations_left: AtomicUsize::new(responses.len()),
|
||||
responses,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MalcontentDBusMock {
|
||||
fn drop(&mut self) {
|
||||
let invocations_left = self.invocations_left.load(Ordering::Acquire);
|
||||
assert_eq!(
|
||||
invocations_left, 0,
|
||||
"MockError: During teardown, {} invocations are still left on the mock object",
|
||||
invocations_left
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DBusMockServer {
|
||||
handle: std::thread::JoinHandle<Result<()>>,
|
||||
cancellation: CancellationToken,
|
||||
}
|
||||
|
||||
impl DBusMockServer {
|
||||
pub fn new(responses: Vec<Restrictions>) -> Result<Self> {
|
||||
let token = CancellationToken::new();
|
||||
let cloned_token = token.clone();
|
||||
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
|
||||
std::env::set_var("TEST_DBUS_SOCKET", format!("{}", listener.local_addr()?));
|
||||
|
||||
let handle = std::thread::spawn(move || {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(Self::spawn_async(responses, listener, cloned_token))
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
handle: handle,
|
||||
cancellation: token,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn stop(self) -> Result<()> {
|
||||
self.cancellation.cancel();
|
||||
self.handle.join().unwrap()
|
||||
}
|
||||
|
||||
async fn spawn_async(
|
||||
responses: Vec<Restrictions>,
|
||||
listener: std::net::TcpListener,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<()> {
|
||||
listener.set_nonblocking(true)?;
|
||||
let listener = tokio::net::TcpListener::from_std(listener)?;
|
||||
|
||||
let guid = zbus::Guid::generate();
|
||||
let mock = MalcontentDBusMock::new(responses);
|
||||
let (stream, _) = listener
|
||||
.accept()
|
||||
.await
|
||||
.expect("Server socket closed unexpectedly");
|
||||
|
||||
log::trace!("dbus mock server accepted client connection");
|
||||
let _connection = zbus::ConnectionBuilder::tcp_stream(stream)
|
||||
.server(&guid)
|
||||
.p2p()
|
||||
.auth_mechanisms(&[zbus::AuthMechanism::Anonymous])
|
||||
.name("com.endlessm.ParentalControls")
|
||||
.expect("Unable to serve given dbus name")
|
||||
.serve_at("/com/endlessm/ParentalControls/Dns", mock)
|
||||
.expect("Unable to server malcontent dbus mock object")
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
tokio::select! {
|
||||
_ = cancellation_token.cancelled() => {
|
||||
Ok(())
|
||||
}
|
||||
_ = std::future::pending::<()>() => { unreachable!() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DBusMockServerGuard {
|
||||
mock: Option<DBusMockServer>,
|
||||
}
|
||||
|
||||
impl DBusMockServerGuard {
|
||||
pub fn new(responses: Vec<Restrictions>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
mock: Some(DBusMockServer::new(responses)?),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DBusMockServerGuard {
|
||||
fn drop(&mut self) {
|
||||
self.mock
|
||||
.take()
|
||||
.unwrap()
|
||||
.stop()
|
||||
.expect("cannot stop dbus server mock");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
||||
|
||||
mod dbus;
|
||||
|
||||
use {
|
||||
anyhow::{anyhow, bail, ensure, Result},
|
||||
libc::{freeaddrinfo, gai_strerror, getaddrinfo},
|
||||
nix::sys::socket::{SockaddrLike as _, SockaddrStorage},
|
||||
std::env,
|
||||
std::ffi::{CStr, CString},
|
||||
std::net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
std::path::PathBuf,
|
||||
std::process::Command,
|
||||
std::str::FromStr,
|
||||
};
|
||||
|
||||
pub use self::dbus::{DBusMockServerGuard, Restriction, Restrictions};
|
||||
|
||||
pub type Eai = EaiRetcode;
|
||||
|
||||
#[static_init::dynamic]
|
||||
static GLOBAL_SETUP: () = {
|
||||
let cdylib_path = test_cdylib::build_current_project();
|
||||
let file_name = cdylib_path.file_name().unwrap();
|
||||
|
||||
let mut versioned_file_name = file_name.to_owned();
|
||||
versioned_file_name.push(".2"); // required for NSS 3 modules
|
||||
|
||||
let dest = PathBuf::from(std::env::current_exe().unwrap()).with_file_name(versioned_file_name);
|
||||
let _ = std::fs::remove_file(&dest);
|
||||
std::fs::hard_link(&cdylib_path, &dest).expect(&format!(
|
||||
"Cannot hard link {} to {}",
|
||||
cdylib_path.display(),
|
||||
dest.display()
|
||||
));
|
||||
};
|
||||
|
||||
/// Note that this should be called once per process; this implies that
|
||||
/// each test calling this function is in a separate process too.
|
||||
pub fn setup() -> Result<()> {
|
||||
let _ = env_logger::builder()
|
||||
.is_test(true)
|
||||
.filter_level(log::LevelFilter::max())
|
||||
.try_init();
|
||||
|
||||
std::env::set_var("NSS_MALCONTENT_LOG", "trace");
|
||||
|
||||
// setup NSS to load our cdylib, now versioned
|
||||
let db = CString::new("hosts").unwrap();
|
||||
let resolvers = CString::new("malcontent dns").unwrap();
|
||||
let nss_config_status = unsafe { __nss_configure_lookup(db.as_ptr(), resolvers.as_ptr()) };
|
||||
|
||||
if nss_config_status != 0 {
|
||||
panic!(
|
||||
"Unable to configure NSS to load module: __nss_configure_lookup() returned {}",
|
||||
nss_config_status
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn resolve_with_system(family: libc::c_int, host: &str) -> Result<IpAddr> {
|
||||
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!(
|
||||
process.status.success(),
|
||||
"Failed to run getent to check host IP"
|
||||
);
|
||||
let output = String::from_utf8(process.stdout)?;
|
||||
log::trace!("system getent reports: {}", &output);
|
||||
let addr_string = output
|
||||
.as_str()
|
||||
.split(' ')
|
||||
.next()
|
||||
.ok_or(anyhow!("Unparseable output from getent"))?;
|
||||
Ok(IpAddr::from_str(&addr_string)?)
|
||||
}
|
||||
|
||||
pub fn resolve_with_module(family: libc::c_int, hostname: &str) -> Result<Vec<IpAddr>> {
|
||||
let c_hostname = CString::new(hostname).unwrap();
|
||||
unsafe {
|
||||
let mut addr = std::ptr::null_mut();
|
||||
let hints = libc::addrinfo {
|
||||
ai_family: family,
|
||||
ai_flags: 0,
|
||||
ai_socktype: 0,
|
||||
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));
|
||||
assert_eq!(
|
||||
getaddrinfo_status,
|
||||
Eai::Success.0,
|
||||
"Should have gotten hostname for {}, instead got {}",
|
||||
hostname,
|
||||
error.to_str().unwrap()
|
||||
);
|
||||
|
||||
let ips = convert_addrinfo(addr)?;
|
||||
freeaddrinfo(addr);
|
||||
Ok(ips)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn convert_addrinfo(addrs: *const libc::addrinfo) -> Result<Vec<IpAddr>> {
|
||||
let mut ips = vec![];
|
||||
|
||||
let mut addr = addrs;
|
||||
while addr != std::ptr::null() {
|
||||
let addr_storage = SockaddrStorage::from_raw((*addr).ai_addr, Some((*addr).ai_addrlen))
|
||||
.ok_or(anyhow!("Garbled addrinfo from getaddrinfo()"))?;
|
||||
|
||||
if let Some(addr) = addr_storage.as_sockaddr_in() {
|
||||
ips.push(IpAddr::V4(Ipv4Addr::from(addr.ip())));
|
||||
} else if let Some(addr) = addr_storage.as_sockaddr_in6() {
|
||||
ips.push(IpAddr::V6(Ipv6Addr::from(addr.ip())));
|
||||
} else {
|
||||
bail!("addrinfo is not either an IPv4 or IPv6 address")
|
||||
}
|
||||
|
||||
addr = (*addr).ai_next;
|
||||
}
|
||||
|
||||
ips.sort();
|
||||
ips.dedup();
|
||||
Ok(ips)
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
mod common;
|
||||
|
||||
use {
|
||||
crate::common::{Eai, Restriction, Restrictions},
|
||||
anyhow::Result,
|
||||
elf::ElfStream,
|
||||
libc::{freeaddrinfo, gai_strerror, getaddrinfo},
|
||||
once_cell::sync::Lazy,
|
||||
rusty_fork::rusty_fork_test,
|
||||
std::net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
std::os::unix::fs::FileExt,
|
||||
std::path::PathBuf,
|
||||
};
|
||||
|
||||
static DNS0_PARENTALCONTROL_ADDRS: Lazy<Restrictions> = Lazy::new(|| {
|
||||
vec![
|
||||
Restriction {
|
||||
ip: IpAddr::V4(Ipv4Addr::new(193, 110, 81, 1)),
|
||||
hostname: "kids.dns0.eu".into(),
|
||||
},
|
||||
Restriction {
|
||||
ip: IpAddr::V4(Ipv4Addr::new(185, 253, 5, 1)),
|
||||
hostname: "kids.dns0.eu".into(),
|
||||
},
|
||||
Restriction {
|
||||
ip: IpAddr::V6(Ipv6Addr::new(0x2a0f, 0xfc80, 0, 0, 0, 0, 0, 0x1)),
|
||||
hostname: "kids.dns0.eu".into(),
|
||||
},
|
||||
Restriction {
|
||||
ip: IpAddr::V6(Ipv6Addr::new(0x2a0f, 0xfc81, 0, 0, 0, 0, 0, 0x1)),
|
||||
hostname: "kids.dns0.eu".into(),
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
static CLOUDFLARE_PARENTALCONTROL_ADDRS: Lazy<Restrictions> = Lazy::new(|| {
|
||||
vec![
|
||||
Restriction {
|
||||
ip: IpAddr::V4(Ipv4Addr::new(1, 1, 1, 3)),
|
||||
hostname: "family.cloudflare-dns.com".into(),
|
||||
},
|
||||
Restriction {
|
||||
ip: IpAddr::V4(Ipv4Addr::new(1, 0, 0, 3)),
|
||||
hostname: "family.cloudflare-dns.com".into(),
|
||||
},
|
||||
Restriction {
|
||||
ip: IpAddr::V6(Ipv6Addr::new(0x2606, 0x4700, 0x4700, 0, 0, 0, 0, 0x1113)),
|
||||
hostname: "family.cloudflare-dns.com".into(),
|
||||
},
|
||||
Restriction {
|
||||
ip: IpAddr::V6(Ipv6Addr::new(0x2606, 0x4700, 0x4700, 0, 0, 0, 0, 0x1003)),
|
||||
hostname: "family.cloudflare-dns.com".into(),
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
static NO_RESTRICTIONS: Lazy<Restrictions> = Lazy::new(|| vec![]);
|
||||
|
||||
#[test]
|
||||
fn sanity_check() -> Result<()> {
|
||||
let current_exe = PathBuf::from(std::env::current_exe().unwrap());
|
||||
let myself = std::fs::File::open(¤t_exe)?;
|
||||
let mut elf_stream: ElfStream<elf::endian::AnyEndian, _> =
|
||||
elf::ElfStream::open_stream(&myself)?;
|
||||
let dynamic_section = elf_stream.dynamic()?.expect("No dynamic table");
|
||||
let rpath_off = dynamic_section
|
||||
.iter()
|
||||
.find(|x| x.d_tag == elf::abi::DT_RPATH)
|
||||
.map(|x| x.d_val())
|
||||
.expect("No DT_RPATH found in test executable ELF dyn section");
|
||||
let strtable = dynamic_section
|
||||
.into_iter()
|
||||
.find(|x| x.d_tag == elf::abi::DT_STRTAB)
|
||||
.map(|x| x.d_ptr())
|
||||
.expect("No DT_STRTAB found in test executable ELF dyn section");
|
||||
|
||||
let mut buf = [0u8; 1024];
|
||||
let _ = myself.read_at(buf.as_mut_slice(), strtable + rpath_off)?;
|
||||
let end = buf
|
||||
.iter()
|
||||
.position(|c| *c == 0u8)
|
||||
.expect(&format!("RPATH is bigger than {} bytes", buf.len()));
|
||||
let rpath = std::str::from_utf8(&buf[..end])?;
|
||||
assert_eq!(rpath, "$ORIGIN");
|
||||
|
||||
common::setup()?;
|
||||
let lib = current_exe.parent().unwrap().join("libnss_malcontent.so.2");
|
||||
assert!(lib.exists());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
rusty_fork_test! {
|
||||
#[test]
|
||||
fn application_dns_is_nodata() {
|
||||
common::setup().unwrap();
|
||||
|
||||
let _dbus = common::DBusMockServerGuard::new(vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()]).unwrap();
|
||||
|
||||
let hostname = std::ffi::CString::new("use-application-dns.net").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,
|
||||
Eai::NoData.0,
|
||||
"Should have gotten no data (NODATA), instead got {}",
|
||||
error.to_str().unwrap()
|
||||
);
|
||||
freeaddrinfo(addr);
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn getaddrinfo_resolution() {
|
||||
common::setup().unwrap();
|
||||
|
||||
const HOSTNAME: &str = "gnome.org";
|
||||
|
||||
let _dbus = common::DBusMockServerGuard::new(vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()]).unwrap();
|
||||
|
||||
unsafe {
|
||||
let mut addr = std::ptr::null_mut();
|
||||
let hostname = std::ffi::CString::new(HOSTNAME).unwrap();
|
||||
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,
|
||||
Eai::Success.0,
|
||||
"Should have gotten an hostname, instead got {}",
|
||||
error.to_str().unwrap()
|
||||
);
|
||||
freeaddrinfo(addr);
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wikipedia_is_unrestricted() {
|
||||
common::setup().unwrap();
|
||||
|
||||
const HOSTNAME: &str = "wikipedia.org";
|
||||
|
||||
let _dbus = common::DBusMockServerGuard::new(vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()]).unwrap();
|
||||
|
||||
for family in [libc::AF_INET, libc::AF_INET6] {
|
||||
let system_addr = common::resolve_with_system(family, HOSTNAME).unwrap();
|
||||
let our_addrs = common::resolve_with_module(family, HOSTNAME).unwrap();
|
||||
assert!(our_addrs.contains(&system_addr));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dns0_adultsites_are_restricted() {
|
||||
common::setup().unwrap();
|
||||
|
||||
const HOSTNAME: &str = "pornhub.com";
|
||||
|
||||
let _dbus = common::DBusMockServerGuard::new(vec![DNS0_PARENTALCONTROL_ADDRS.clone()]).unwrap();
|
||||
|
||||
let our_addrs_ipv4 = common::resolve_with_module(libc::AF_INET, HOSTNAME).unwrap();
|
||||
let our_addrs_ipv6 = common::resolve_with_module(libc::AF_INET, HOSTNAME).unwrap();
|
||||
assert!(
|
||||
our_addrs_ipv4.is_empty(),
|
||||
"Resolver answered with {:?}, should be empty",
|
||||
our_addrs_ipv4
|
||||
);
|
||||
assert!(
|
||||
our_addrs_ipv6.is_empty(),
|
||||
"Resolver answered with {:?}, should be empty",
|
||||
our_addrs_ipv6
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cloudflare_adultsite_is_restricted_ipv4() {
|
||||
common::setup().unwrap();
|
||||
|
||||
const HOSTNAME: &str = "nudity.testcategory.com";
|
||||
|
||||
let _dbus = common::DBusMockServerGuard::new(vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()]).unwrap();
|
||||
|
||||
let system_addr = common::resolve_with_system(libc::AF_INET, HOSTNAME).unwrap();
|
||||
let our_addrs = common::resolve_with_module(libc::AF_INET, HOSTNAME).unwrap();
|
||||
assert!(
|
||||
!our_addrs.contains(&system_addr),
|
||||
"Resolver answered with {:?}, should not contain {}",
|
||||
our_addrs,
|
||||
system_addr
|
||||
);
|
||||
assert_eq!(our_addrs, [IpAddr::V4(Ipv4Addr::UNSPECIFIED)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cloudflare_adultsite_is_restricted_ipv6() {
|
||||
common::setup().unwrap();
|
||||
|
||||
const HOSTNAME: &str = "nudity.testcategory.com";
|
||||
|
||||
let _dbus = common::DBusMockServerGuard::new(vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()]).unwrap();
|
||||
|
||||
let system_addr = common::resolve_with_system(libc::AF_INET6, HOSTNAME).unwrap();
|
||||
let our_addrs = common::resolve_with_module(libc::AF_INET6, HOSTNAME).unwrap();
|
||||
assert!(
|
||||
!our_addrs.contains(&system_addr),
|
||||
"Resolver answered with {:?}, should not contain {}",
|
||||
our_addrs,
|
||||
system_addr
|
||||
);
|
||||
assert_eq!(our_addrs, [IpAddr::V6(Ipv6Addr::UNSPECIFIED)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn privileged_user_bypasses_restrictions() {
|
||||
common::setup().unwrap();
|
||||
|
||||
const HOSTNAME: &str = "malware.testcategory.com";
|
||||
|
||||
let _dbus = common::DBusMockServerGuard::new(vec![NO_RESTRICTIONS.clone()]).unwrap();
|
||||
|
||||
for family in [libc::AF_INET, libc::AF_INET6] {
|
||||
let system_addr = common::resolve_with_system(family, HOSTNAME).unwrap();
|
||||
let our_addrs = common::resolve_with_module(family, HOSTNAME).unwrap();
|
||||
assert!(our_addrs.contains(&system_addr));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#include <nss.h>
|
||||
#include <netdb.h>
|
||||
|
||||
// Work around enums in netdb.h defined as macros instead :-p
|
||||
|
||||
enum class HErrno : int {
|
||||
Success = 0,
|
||||
HostNotFound = HOST_NOT_FOUND,
|
||||
TryAgain = TRY_AGAIN,
|
||||
NoRecovery = NO_RECOVERY,
|
||||
NoData = NO_DATA,
|
||||
#ifdef __USE_MISC
|
||||
Internal = NETDB_INTERNAL,
|
||||
#endif
|
||||
};
|
||||
|
||||
enum class EaiRetcode : int {
|
||||
Success = 0,
|
||||
BadFlags = EAI_BADFLAGS,
|
||||
NoName = EAI_NONAME,
|
||||
Fail = EAI_FAIL,
|
||||
Family = EAI_FAMILY,
|
||||
SockType = EAI_SOCKTYPE,
|
||||
Service = EAI_SERVICE,
|
||||
Memory = EAI_MEMORY,
|
||||
System = EAI_SYSTEM,
|
||||
Overflow = EAI_OVERFLOW,
|
||||
#ifdef __USE_GNU
|
||||
NoData = EAI_NODATA,
|
||||
AddrFamily = EAI_ADDRFAMILY,
|
||||
InProgress = EAI_INPROGRESS,
|
||||
Canceled = EAI_CANCELED,
|
||||
NotCanceled = EAI_NOTCANCELED,
|
||||
AllDone = EAI_ALLDONE,
|
||||
Interrupted = EAI_INTR,
|
||||
IdnEncode = EAI_IDN_ENCODE,
|
||||
#endif /* __USE_GNU */
|
||||
};
|
Loading…
Reference in New Issue