WIP: add DNS parental controls (Rust version) #2
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
|
||||||
|
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||||
|
<node>
|
||||||
|
<interface name="com.endlessm.ParentalControls.Dns">
|
||||||
|
<annotation name="org.freedesktop.Accounts.VendorExtension" value="true"/>
|
||||||
|
|
||||||
|
<annotation name="org.freedesktop.Accounts.Authentication.ChangeOwn"
|
||||||
|
value="com.endlessm.ParentalControls.SessionLimits.ChangeOwn"/>
|
||||||
|
<annotation name="org.freedesktop.Accounts.Authentication.ReadOwn"
|
||||||
|
value="com.endlessm.ParentalControls.SessionLimits.ReadOwn"/>
|
||||||
|
<annotation name="org.freedesktop.Accounts.Authentication.ChangeAny"
|
||||||
|
value="com.endlessm.ParentalControls.SessionLimits.ChangeAny"/>
|
||||||
|
<annotation name="org.freedesktop.Accounts.Authentication.ReadAny"
|
||||||
|
value="com.endlessm.ParentalControls.SessionLimits.ReadAny"/>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Dns:
|
||||||
|
|
||||||
|
A list of DNS server IP addresses to use for this user, in order of preference.
|
||||||
|
|
||||||
|
Each can be optionally qualified by a hashtag and the corresponding
|
||||||
|
hostname (if a TLS version is supported).
|
||||||
|
|
||||||
|
For instance: using "dns0.eu Kids" (https://www.dns0.eu/kids),
|
||||||
|
which offers a good number of blocked domains, would entail:
|
||||||
|
|
||||||
|
- `193.110.81.1#kids.dns0.eu`
|
||||||
|
- `2a0f:fc80::1#kids.dns0.eu`
|
||||||
|
- `185.253.5.1#kids.dns0.eu`
|
||||||
|
- `2a0f:fc81::1#kids.dns0.eu`
|
||||||
|
|
||||||
|
If the array is left empty, the globally-set system resolver
|
||||||
|
is used instead.
|
||||||
|
-->
|
||||||
|
<property name="Dns" type="as" access="readwrite">
|
||||||
|
<annotation name="org.freedesktop.Accounts.DefaultValue" value=""/>
|
||||||
|
</property>
|
||||||
|
|
||||||
|
</interface>
|
||||||
|
</node>
|
|
@ -40,6 +40,46 @@
|
||||||
</defaults>
|
</defaults>
|
||||||
</action>
|
</action>
|
||||||
|
|
||||||
|
<action id="com.endlessm.ParentalControls.Dns.ChangeOwn">
|
||||||
|
<description>Change your own DNS servers</description>
|
||||||
|
<message>Authentication is required to change your DNS servers.</message>
|
||||||
|
<defaults>
|
||||||
|
<allow_any>auth_admin_keep</allow_any>
|
||||||
|
<allow_inactive>auth_admin_keep</allow_inactive>
|
||||||
|
<allow_active>auth_admin_keep</allow_active>
|
||||||
|
</defaults>
|
||||||
|
</action>
|
||||||
|
|
||||||
|
<action id="com.endlessm.ParentalControls.Dns.ReadOwn">
|
||||||
|
<description>Read your own DNS servers</description>
|
||||||
|
<message>Authentication is required to read your DNS servers.</message>
|
||||||
|
<defaults>
|
||||||
|
<allow_any>yes</allow_any>
|
||||||
|
<allow_inactive>yes</allow_inactive>
|
||||||
|
<allow_active>yes</allow_active>
|
||||||
|
</defaults>
|
||||||
|
</action>
|
||||||
|
|
||||||
|
<action id="com.endlessm.ParentalControls.Dns.ChangeAny">
|
||||||
|
<description>Change another user’s DNS servers</description>
|
||||||
|
<message>Authentication is required to change another user’s DNS servers.</message>
|
||||||
|
<defaults>
|
||||||
|
<allow_any>auth_admin_keep</allow_any>
|
||||||
|
<allow_inactive>auth_admin_keep</allow_inactive>
|
||||||
|
<allow_active>auth_admin_keep</allow_active>
|
||||||
|
</defaults>
|
||||||
|
</action>
|
||||||
|
|
||||||
|
<action id="com.endlessm.ParentalControls.Dns.ReadAny">
|
||||||
|
<description>Read another user’s DNS servers</description>
|
||||||
|
<message>Authentication is required to read another user’s DNS servers.</message>
|
||||||
|
<defaults>
|
||||||
|
<allow_any>auth_admin_keep</allow_any>
|
||||||
|
<allow_inactive>auth_admin_keep</allow_inactive>
|
||||||
|
<allow_active>auth_admin_keep</allow_active>
|
||||||
|
</defaults>
|
||||||
|
</action>
|
||||||
|
|
||||||
<action id="com.endlessm.ParentalControls.SessionLimits.ChangeOwn">
|
<action id="com.endlessm.ParentalControls.SessionLimits.ChangeOwn">
|
||||||
<description>Change your own session limits</description>
|
<description>Change your own session limits</description>
|
||||||
<message>Authentication is required to change your session limits.</message>
|
<message>Authentication is required to change your session limits.</message>
|
||||||
|
|
|
@ -24,6 +24,8 @@ polkit.addRule(function(action, subject) {
|
||||||
* needing an additional polkit authorisation dialogue. */
|
* needing an additional polkit authorisation dialogue. */
|
||||||
if ((action.id == "com.endlessm.ParentalControls.AppFilter.ReadOwn" ||
|
if ((action.id == "com.endlessm.ParentalControls.AppFilter.ReadOwn" ||
|
||||||
action.id == "com.endlessm.ParentalControls.AppFilter.ReadAny" ||
|
action.id == "com.endlessm.ParentalControls.AppFilter.ReadAny" ||
|
||||||
|
action.id == "com.endlessm.ParentalControls.Dns.ReadOwn" ||
|
||||||
|
action.id == "com.endlessm.ParentalControls.Dns.ReadAny" ||
|
||||||
action.id == "com.endlessm.ParentalControls.SessionLimits.ReadOwn" ||
|
action.id == "com.endlessm.ParentalControls.SessionLimits.ReadOwn" ||
|
||||||
action.id == "com.endlessm.ParentalControls.SessionLimits.ReadAny") &&
|
action.id == "com.endlessm.ParentalControls.SessionLimits.ReadAny") &&
|
||||||
subject.active && subject.local &&
|
subject.active && subject.local &&
|
||||||
|
|
|
@ -9,6 +9,7 @@ i18n.merge_file(
|
||||||
dbus_interfaces = [
|
dbus_interfaces = [
|
||||||
'com.endlessm.ParentalControls.AccountInfo',
|
'com.endlessm.ParentalControls.AccountInfo',
|
||||||
'com.endlessm.ParentalControls.AppFilter',
|
'com.endlessm.ParentalControls.AppFilter',
|
||||||
|
'com.endlessm.ParentalControls.Dns',
|
||||||
'com.endlessm.ParentalControls.SessionLimits',
|
'com.endlessm.ParentalControls.SessionLimits',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
project('malcontent', 'c',
|
project('malcontent', 'c',
|
||||||
version : '0.12.0',
|
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'],
|
license: ['LGPL-2.1-or-later', 'GPL-2.0-or-later'],
|
||||||
default_options : [
|
default_options : [
|
||||||
'buildtype=debugoptimized',
|
'buildtype=debugoptimized',
|
||||||
|
@ -154,3 +154,7 @@ if get_option('ui').enabled()
|
||||||
endif
|
endif
|
||||||
subdir('pam')
|
subdir('pam')
|
||||||
subdir('po')
|
subdir('po')
|
||||||
|
|
||||||
|
if get_option('dns').enabled()
|
||||||
|
subdir('nss')
|
||||||
|
endif
|
||||||
|
|
|
@ -9,6 +9,12 @@ option(
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'directory for PAM modules'
|
description: 'directory for PAM modules'
|
||||||
)
|
)
|
||||||
|
option(
|
||||||
|
'dns',
|
||||||
|
type: 'feature',
|
||||||
|
value: 'enabled',
|
||||||
|
description: 'enable NSS module support (for parental DNS controls)'
|
||||||
|
)
|
||||||
option(
|
option(
|
||||||
'ui',
|
'ui',
|
||||||
type: 'feature',
|
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