// SPDX-FileCopyrightText: 2022 Matteo Settenvini // SPDX-License-Identifier: GPL-3.0-or-later mod common; use { crate::common::{Eai, Restriction, Restrictions}, anyhow::Result, libc::{freeaddrinfo, gai_strerror, getaddrinfo}, nix::unistd::getuid, once_cell::sync::Lazy, std::collections::HashMap, std::net::{IpAddr, Ipv4Addr, Ipv6Addr}, }; 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![]); fork_test! { #[test] fn nss_module_is_loaded() -> Result<()> { common::setup()?; tokio::runtime::Runtime::new().unwrap().block_on(async { let _dbus = common::mock_dbus(HashMap::from([(getuid(), vec![NO_RESTRICTIONS.clone()])])).await?; common::resolve_with_module(libc::AF_INET, "gnome.org")?; Ok(()) }) } #[test] fn application_dns_is_nodata() -> Result<()> { common::setup()?; tokio::runtime::Runtime::new().unwrap().block_on(async { let _dbus = common::mock_dbus(HashMap::from([( getuid(), vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()], )])).await?; 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); }; Ok(()) }) } #[test] fn getaddrinfo_resolution() -> Result<()> { common::setup()?; const HOSTNAME: &str = "gnome.org"; tokio::runtime::Runtime::new().unwrap().block_on(async { let _dbus = common::mock_dbus(HashMap::from([( getuid(), vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()], )])).await?; unsafe { let mut addr = std::ptr::null_mut(); let getaddrinfo_status = getaddrinfo( std::ffi::CString::new(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); }; Ok(()) }) } #[test] fn wikipedia_is_unrestricted() -> Result<()> { common::setup()?; const HOSTNAME: &str = "wikipedia.org"; tokio::runtime::Runtime::new().unwrap().block_on(async { let _dbus = common::mock_dbus(HashMap::from([( getuid(), vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()], )])).await?; for family in [libc::AF_INET, libc::AF_INET6] { let system_addr = common::resolve_with_system(family, HOSTNAME)?; let our_addrs = common::resolve_with_module(family, HOSTNAME)?; assert!(our_addrs.contains(&system_addr)); } Ok(()) }) } #[test] fn adultsite_is_restricted_ipv4() -> Result<()> { common::setup()?; const HOSTNAME: &str = "nudity.testcategory.com"; tokio::runtime::Runtime::new().unwrap().block_on(async { let _dbus = common::mock_dbus(HashMap::from([( getuid(), vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()], )])).await?; let system_addr = common::resolve_with_system(libc::AF_INET, HOSTNAME)?; let our_addrs = common::resolve_with_module(libc::AF_INET, HOSTNAME)?; assert!(!our_addrs.contains(&system_addr), "Resolver answered with {:?}, should not contain {}", our_addrs, system_addr); assert_eq!(our_addrs, [IpAddr::V4(Ipv4Addr::UNSPECIFIED)]); Ok(()) }) } #[test] fn adultsite_is_restricted_ipv6() -> Result<()> { common::setup()?; const HOSTNAME: &str = "nudity.testcategory.com"; tokio::runtime::Runtime::new().unwrap().block_on(async { let _dbus = common::mock_dbus(HashMap::from([( getuid(), vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()], )])).await; let system_addr = common::resolve_with_system(libc::AF_INET6, HOSTNAME)?; let our_addrs = common::resolve_with_module(libc::AF_INET6, HOSTNAME)?; assert!(!our_addrs.contains(&system_addr), "Resolver answered with {:?}, should not contain {}", our_addrs, system_addr); assert_eq!(our_addrs, [IpAddr::V6(Ipv6Addr::UNSPECIFIED)]); Ok(()) }) } #[test] fn privileged_user_bypasses_restrictions() -> Result<()> { common::setup()?; const HOSTNAME: &str = "malware.testcategory.com"; tokio::runtime::Runtime::new().unwrap().block_on(async { let _dbus = common::mock_dbus(HashMap::from([(getuid(), vec![NO_RESTRICTIONS.clone()])])).await?; for family in [libc::AF_INET, libc::AF_INET6] { let system_addr = common::resolve_with_system(family, HOSTNAME)?; let our_addrs = common::resolve_with_module(family, HOSTNAME)?; assert!(our_addrs.contains(&system_addr)); } Ok(()) }) } }