// SPDX-FileCopyrightText: Matteo Settenvini // 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 = 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 = 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 = 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::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)); } } }