From 4e9a8b8ad8eec96901ad6740c359d2e1e62f27c8 Mon Sep 17 00:00:00 2001 From: Eric Rodrigues Pires Date: Mon, 30 Dec 2024 13:44:25 -0300 Subject: [PATCH] Add profanity filters --- CHANGELOG.md | 2 + Cargo.lock | 58 ++++- Cargo.toml | 1 + book/src/cli.md | 8 + book/src/compiling_from_source.md | 7 +- src/addressing.rs | 338 ++++++++++++++++++++---------- src/config.rs | 10 + src/lib.rs | 24 ++- src/ssh.rs | 2 +- 9 files changed, 333 insertions(+), 117 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67eb73d..4be576f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ - Add `--disable-http` CLI flag. - Add `--disable-tcp` CLI flag. +- Add `--random-subdomain-filter-profanities` CLI flag. +- Add `--requested-domain-filter-profanities` CLI flag. ### Changed diff --git a/Cargo.lock b/Cargo.lock index eff6d03..45ca903 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -678,7 +678,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim", + "strsim 0.11.1", ] [[package]] @@ -954,7 +954,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.11.1", "syn", ] @@ -1083,6 +1083,12 @@ dependencies = [ "syn", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "downcast" version = "0.11.0" @@ -1282,6 +1288,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "finl_unicode" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94c970b525906eb37d3940083aa65b95e481fc1857d467d13374e1d925cfc163" + [[package]] name = "flate2" version = "1.0.35" @@ -2046,6 +2058,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.12.1" @@ -3347,6 +3368,23 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "rustrict" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a566beb65e3d86654ff5cc7091d947589ef06f4bc6ba946455c2851ab1193914" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "doc-comment", + "finl_unicode", + "itertools 0.10.5", + "lazy_static", + "rustc-hash", + "strsim 0.10.0", + "unicode-normalization", +] + [[package]] name = "rustversion" version = "1.0.18" @@ -3417,6 +3455,7 @@ dependencies = [ "rustls-acme", "rustls-platform-verifier", "rustls-webpki", + "rustrict", "serde", "serde_json", "ssh-key", @@ -3740,6 +3779,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strsim" version = "0.11.1" @@ -4147,6 +4192,15 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.12.0" diff --git a/Cargo.toml b/Cargo.toml index e66d984..aedb1f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ rustls = "0.23.16" rustls-acme = "0.12.1" rustls-platform-verifier = "0.4.0" rustls-webpki = "0.102.8" +rustrict = { version = "0.7.33", features = ["customize"] } serde = "1.0.215" serde_json = "1.0.133" ssh-key = "0.6.7" diff --git a/book/src/cli.md b/book/src/cli.md index 74e2eb5..5a5ad60 100644 --- a/book/src/cli.md +++ b/book/src/cli.md @@ -202,6 +202,14 @@ Expose HTTP/SSH/TCP services through SSH port forwarding. [default: 6] + --random-subdomain-filter-profanities + Prevents random subdomains from containing profanities + + --requested-domain-filter-profanities + Prevents user-requested domains/subdomains from containing profanities. + + Beware that this can lead to false positives being blocked! + --idle-connection-timeout <DURATION> Grace period for dangling/unauthenticated SSH connections before they are forcefully disconnected. diff --git a/book/src/compiling_from_source.md b/book/src/compiling_from_source.md index 085fe2d..03be510 100644 --- a/book/src/compiling_from_source.md +++ b/book/src/compiling_from_source.md @@ -8,7 +8,7 @@ If you're compiling from a separate workstation than the one that will be runnin git clone https://github.com/EpicEric/sandhole cd sandhole cargo build --release -scp target/release/sandhole you@server.com:/usr/local/bin/sandhole +scp target/release/sandhole user@server.com:/usr/local/bin/sandhole ``` If you're compiling on the machine that's running Sandhole, you can install it directly with `cargo install`. This should also add `sandhole` to your `PATH`: @@ -19,7 +19,7 @@ cargo install --git https://github.com/EpicEric/sandhole git clone https://github.com/EpicEric/sandhole cargo install --path sandhole # -- OR -- -cargo install sandhole # Installs from sources uploaded to crates.io +cargo install sandhole # Installs from latest release sources uploaded to crates.io ``` Once this is all done, you can start running Sandhole! Just make sure that it points to your own domain: @@ -31,11 +31,12 @@ sandhole --domain server.com By default, this will expose ports 80 (for HTTP), 443 (for HTTPS), and 2222 (for SSH). If it all succeeds, you should see the following: ```log +[2024-11-23T13:10:51Z INFO sandhole] Starting Sandhole... [2024-11-23T13:10:51Z INFO sandhole] Key file not found. Creating... [2024-11-23T13:10:51Z INFO sandhole] Listening for HTTP connections on port 80. [2024-11-23T13:10:51Z INFO sandhole] Listening for HTTPS connections on port 443. [2024-11-23T13:10:51Z INFO sandhole] Listening for SSH connections on port 2222. -[2024-11-23T13:10:51Z INFO sandhole] sandhole is now running. +[2024-11-23T13:10:51Z INFO sandhole] Sandhole is now running. ``` Now you're ready to dig sandholes like a crab! diff --git a/src/addressing.rs b/src/addressing.rs index 0649208..fcb5d40 100644 --- a/src/addressing.rs +++ b/src/addressing.rs @@ -10,6 +10,7 @@ use mockall::automock; use rand::{seq::SliceRandom, thread_rng, Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; use rand_seeder::SipHasher; +use rustrict::{Censor, CensorStr, Type}; use ssh_key::Fingerprint; use webpki::types::DnsName; @@ -109,20 +110,24 @@ pub(crate) struct AddressDelegator { txt_record_prefix: String, // Root domain for Sandhole. root_domain: String, + // Policy on how to allow binding hostnames. + bind_hostnames: BindHostnames, + // Whether subdomains should be random or not. + force_random_subdomains: bool, + // Policy for generating random subdomains. + random_subdomain_seed: Option, + // The length of the string appended to the start of random subdomains. + random_subdomain_length: usize, + // Whether profanities should be filtered out from random subdomain addressing. + random_subdomain_filter_profanities: bool, + // Trie to optionally verify for profanities in requested domains/subdomains. + requested_domain_filter: Option<&'static rustrict::Trie>, // Random seed for generating consistent yet secure random values. seed: u64, // Mapping between numbers and IDs. block_id: BlockId, // Counter to generate random IDs with the block ID. block_rng: Mutex, - // Policy for generating random subdomains. - random_subdomain_seed: Option, - // Policy on how to allow binding hostnames. - bind_hostnames: BindHostnames, - // Whether subdomains should be random or not. - force_random_subdomains: bool, - // The length of the string appended to the start of random subdomains. - random_subdomain_length: NonZero, } pub(crate) struct AddressDelegatorData { @@ -133,6 +138,8 @@ pub(crate) struct AddressDelegatorData { pub(crate) force_random_subdomains: bool, pub(crate) random_subdomain_seed: Option, pub(crate) random_subdomain_length: NonZero, + pub(crate) random_subdomain_filter_profanities: bool, + pub(crate) requested_domain_filter: Option<&'static rustrict::Trie>, } impl AddressDelegator { @@ -142,10 +149,12 @@ impl AddressDelegator { resolver, txt_record_prefix, root_domain, - random_subdomain_seed, bind_hostnames, force_random_subdomains, + random_subdomain_seed, random_subdomain_length, + random_subdomain_filter_profanities, + requested_domain_filter, } = data; debug_assert!(!txt_record_prefix.is_empty()); debug_assert!(!root_domain.is_empty()); @@ -157,7 +166,9 @@ impl AddressDelegator { bind_hostnames, force_random_subdomains, random_subdomain_seed, - random_subdomain_length, + random_subdomain_length: ::from(::from(random_subdomain_length)), + random_subdomain_filter_profanities, + requested_domain_filter, seed: rng.gen(), block_id: BlockId::new( Alphabet::lowercase_alphanumeric(), @@ -168,8 +179,8 @@ impl AddressDelegator { } } - // Assign an address given the current configuration - pub(crate) async fn get_address( + // Assign an HTTP address given the current configuration + pub(crate) async fn get_http_address( &self, requested_address: &str, user: &Option, @@ -178,50 +189,63 @@ impl AddressDelegator { ) -> String { // Only consider valid DNS addresses if DnsName::try_from(requested_address).is_ok() { - // If we bind all hostnames, return the provided address - if matches!(self.bind_hostnames, BindHostnames::All) { - return requested_address.to_string(); - } - // If we bind by CNAME records, check that this address points to Sandhole's root domain - if matches!(self.bind_hostnames, BindHostnames::Cname) - && requested_address != self.root_domain - && self - .resolver - .has_cname_record_for_domain(requested_address, &self.root_domain) - .await - { - return requested_address.to_string(); - } - // If we bind by TXT or CNAME records, check that the public key's fingerprint is among the TXT records. - if matches!( - self.bind_hostnames, - BindHostnames::Cname | BindHostnames::Txt - ) { - if let Some(fingerprint) = fingerprint { - if self + if self.requested_domain_filter.as_ref().is_some_and(|trie| { + Censor::from_str(requested_address) + .with_trie(trie) + .analyze() + .is(Type::INAPPROPRIATE) + }) { + warn!( + "Profane address requested ({}), defaulting to random", + requested_address + ); + } else { + // If we bind all hostnames, return the provided address + if matches!(self.bind_hostnames, BindHostnames::All) { + return requested_address.to_string(); + } + // If we bind by CNAME records, check that this address points to Sandhole's root domain + if matches!(self.bind_hostnames, BindHostnames::Cname) + && requested_address != self.root_domain + && self .resolver - .has_txt_record_for_fingerprint( - &self.txt_record_prefix, - requested_address, - fingerprint, - ) + .has_cname_record_for_domain(requested_address, &self.root_domain) .await - { - return requested_address.to_string(); + { + return requested_address.to_string(); + } + // If we bind by TXT or CNAME records, check that the public key's fingerprint is among the TXT records. + if matches!( + self.bind_hostnames, + BindHostnames::Cname | BindHostnames::Txt + ) { + if let Some(fingerprint) = fingerprint { + if self + .resolver + .has_txt_record_for_fingerprint( + &self.txt_record_prefix, + requested_address, + fingerprint, + ) + .await + { + return requested_address.to_string(); + } } } - } - // If subdomains aren't random, check if user provided a valid one - if !self.force_random_subdomains { - // Assign specified subdomain under the root domain - let address = requested_address.trim_end_matches(&format!(".{}", self.root_domain)); - if !address.is_empty() && !address.contains('.') { - return format!("{}.{}", address, self.root_domain); - } else { - warn!( - "Invalid address requested ({}), defaulting to random", - requested_address - ); + // If subdomains aren't random, check if user provided a valid one + if !self.force_random_subdomains { + // Assign specified subdomain under the root domain + let address = + requested_address.trim_end_matches(&format!(".{}", self.root_domain)); + if !address.is_empty() && !address.contains('.') { + return format!("{}.{}", address, self.root_domain); + } else { + warn!( + "Invalid address requested ({}), defaulting to random", + requested_address + ); + } } } } else { @@ -303,27 +327,38 @@ impl AddressDelegator { } if hash_initialized { // Generate random subdomain from hashed state - let mut seed: ::Seed = Default::default(); - hasher.into_rng().fill(&mut seed); - let mut rng = ChaCha20Rng::from_seed(seed); - String::from_utf8( - (0..self.random_subdomain_length.into()) - .flat_map(|_| { - b"0123456789abcdefghijklmnopqrstuvwxyz" - .choose(&mut rng) - .copied() - }) - .collect(), - ) - .unwrap() + let mut hasher_rng = hasher.into_rng(); + loop { + let mut seed: ::Seed = Default::default(); + hasher_rng.fill(&mut seed); + let mut rng = ChaCha20Rng::from_seed(seed); + let result = String::from_utf8( + (0..self.random_subdomain_length) + .flat_map(|_| { + b"0123456789abcdefghijklmnopqrstuvwxyz" + .choose(&mut rng) + .copied() + }) + .collect(), + ) + .unwrap(); + if !self.random_subdomain_filter_profanities || !result.is_inappropriate() { + break result; + } + } } else { // Hash hasn't been initialized properly, use block ID to generate a random string let mut block_rng = self.block_rng.lock().unwrap(); - let mut string = self.block_id.encode_string(*block_rng).unwrap(); - *block_rng = block_rng.wrapping_add(1); + let mut result = loop { + let result = self.block_id.encode_string(*block_rng).unwrap(); + *block_rng = block_rng.wrapping_add(1); + if !self.random_subdomain_filter_profanities || !result.is_inappropriate() { + break result; + } + }; drop(block_rng); - string.drain(::from(::from(self.random_subdomain_length))..); - string + result.drain(self.random_subdomain_length..); + result } } } @@ -353,9 +388,11 @@ mod address_delegator_tests { force_random_subdomains: false, random_subdomain_seed: None, random_subdomain_length: 6.try_into().unwrap(), + random_subdomain_filter_profanities: false, + requested_domain_filter: None, }); let address = delegator - .get_address( + .get_http_address( "some.address", &None, &None, @@ -382,9 +419,11 @@ mod address_delegator_tests { force_random_subdomains: false, random_subdomain_seed: None, random_subdomain_length: 6.try_into().unwrap(), + random_subdomain_filter_profanities: false, + requested_domain_filter: None, }); let address = delegator - .get_address( + .get_http_address( "root.tld", &None, &None, @@ -414,9 +453,11 @@ mod address_delegator_tests { force_random_subdomains: false, random_subdomain_seed: None, random_subdomain_length: 6.try_into().unwrap(), + random_subdomain_filter_profanities: false, + requested_domain_filter: None, }); let address = delegator - .get_address( + .get_http_address( "some.address", &None, &None, @@ -456,9 +497,11 @@ mod address_delegator_tests { force_random_subdomains: false, random_subdomain_seed: None, random_subdomain_length: 6.try_into().unwrap(), + random_subdomain_filter_profanities: false, + requested_domain_filter: None, }); let address = delegator - .get_address( + .get_http_address( "some.address", &None, &Some(fingerprint), @@ -495,9 +538,11 @@ mod address_delegator_tests { force_random_subdomains: false, random_subdomain_seed: None, random_subdomain_length: 6.try_into().unwrap(), + random_subdomain_filter_profanities: false, + requested_domain_filter: None, }); let address = delegator - .get_address( + .get_http_address( "some.address", &None, &Some(fingerprint), @@ -526,9 +571,11 @@ mod address_delegator_tests { force_random_subdomains: false, random_subdomain_seed: None, random_subdomain_length: 6.try_into().unwrap(), + random_subdomain_filter_profanities: false, + requested_domain_filter: None, }); let address = delegator - .get_address( + .get_http_address( "subdomain", &None, &None, @@ -561,9 +608,11 @@ mod address_delegator_tests { force_random_subdomains: false, random_subdomain_seed: None, random_subdomain_length: 6.try_into().unwrap(), + random_subdomain_filter_profanities: false, + requested_domain_filter: None, }); let address = delegator - .get_address( + .get_http_address( "something", &None, &Some(fingerprint), @@ -590,9 +639,11 @@ mod address_delegator_tests { force_random_subdomains: false, random_subdomain_seed: None, random_subdomain_length: 6.try_into().unwrap(), + random_subdomain_filter_profanities: false, + requested_domain_filter: None, }); let address = delegator - .get_address( + .get_http_address( "prefix.root.tld", &None, &None, @@ -625,9 +676,11 @@ mod address_delegator_tests { force_random_subdomains: false, random_subdomain_seed: None, random_subdomain_length: 6.try_into().unwrap(), + random_subdomain_filter_profanities: false, + requested_domain_filter: None, }); let address = delegator - .get_address( + .get_http_address( "root.tld", &None, &Some(fingerprint), @@ -660,9 +713,11 @@ mod address_delegator_tests { force_random_subdomains: false, random_subdomain_seed: None, random_subdomain_length: 6.try_into().unwrap(), + random_subdomain_filter_profanities: false, + requested_domain_filter: None, }); let address = delegator - .get_address( + .get_http_address( "root.tld", &None, &Some(fingerprint), @@ -701,9 +756,11 @@ mod address_delegator_tests { force_random_subdomains: false, random_subdomain_seed: None, random_subdomain_length: 6.try_into().unwrap(), + random_subdomain_filter_profanities: false, + requested_domain_filter: None, }); let address = delegator - .get_address( + .get_http_address( "we.are.root.tld", &None, &Some(fingerprint), @@ -740,9 +797,11 @@ mod address_delegator_tests { force_random_subdomains: false, random_subdomain_seed: None, random_subdomain_length: 6.try_into().unwrap(), + random_subdomain_filter_profanities: false, + requested_domain_filter: None, }); let address = delegator - .get_address( + .get_http_address( ".", &None, &Some(fingerprint), @@ -779,13 +838,15 @@ mod address_delegator_tests { force_random_subdomains: true, random_subdomain_seed: None, random_subdomain_length: 6.try_into().unwrap(), + random_subdomain_filter_profanities: false, + requested_domain_filter: None, }); let mut set = std::collections::HashSet::with_capacity(200_000); let regex = Regex::new(r"^[0-9a-z]{6}\.root\.tld$").unwrap(); // 99.99% chance of collision with naïve implementation for _ in 0..200_000 { let address = delegator - .get_address( + .get_http_address( "some.address", &None, &Some(fingerprint), @@ -808,7 +869,7 @@ mod address_delegator_tests { } #[tokio::test] - async fn returns_unique_random_subdomains_with_different_size() { + async fn returns_unique_random_subdomains_with_different_size_and_no_profanities() { let fingerprint = russh_keys::PrivateKey::random(&mut OsRng, russh_keys::Algorithm::Ed25519) .unwrap() @@ -823,11 +884,15 @@ mod address_delegator_tests { force_random_subdomains: true, random_subdomain_seed: None, random_subdomain_length: 4.try_into().unwrap(), + random_subdomain_filter_profanities: true, + requested_domain_filter: None, }); + let mut set = std::collections::HashSet::with_capacity(5_600); let regex = Regex::new(r"^[0-9a-z]{4}\.root\.tld$").unwrap(); - for _ in 0..10_000 { + // 99.99% chance of collision with naïve implementation + for _ in 0..5_600 { let address = delegator - .get_address( + .get_http_address( "some.address", &None, &Some(fingerprint), @@ -840,9 +905,56 @@ mod address_delegator_tests { "non DNS-compatible address {}", address ); + assert!( + !set.contains(&address), + "generated non-unique address: {}", + address + ); + set.insert(address); } } + #[tokio::test] + async fn returns_random_subdomain_if_requested_subdomain_contains_profanity() { + let mut mock = MockResolver::new(); + mock.expect_has_txt_record_for_fingerprint().never(); + let delegator = AddressDelegator::new(AddressDelegatorData { + resolver: mock, + txt_record_prefix: "_some_prefix".into(), + root_domain: "root.tld".into(), + bind_hostnames: BindHostnames::All, + force_random_subdomains: false, + random_subdomain_seed: None, + random_subdomain_length: 8.try_into().unwrap(), + random_subdomain_filter_profanities: false, + requested_domain_filter: Some(Box::leak(Box::new(rustrict::Trie::default()))), + }); + let address = delegator + .get_http_address( + "fuck.root.tld", + &None, + &None, + &"127.0.0.1:12345".parse::().unwrap(), + ) + .await; + assert!( + Regex::new(r"^[0-9a-z]{8}\.root\.tld$") + .unwrap() + .is_match(&address), + "invalid address {}", + address + ); + assert!( + !address.contains("fuck"), + "address contains user-provided profanity" + ); + assert!( + DnsName::try_from(address.clone()).is_ok(), + "non DNS-compatible address {}", + address + ); + } + #[tokio::test] async fn returns_unique_random_subdomains_per_user_and_address_if_forced() { let mut mock = MockResolver::new(); @@ -855,9 +967,11 @@ mod address_delegator_tests { force_random_subdomains: true, random_subdomain_seed: Some(RandomSubdomainSeed::User), random_subdomain_length: 6.try_into().unwrap(), + random_subdomain_filter_profanities: false, + requested_domain_filter: None, }); let address1_u1_a1 = delegator - .get_address( + .get_http_address( "a1", &Some("u1".into()), &None, @@ -865,7 +979,7 @@ mod address_delegator_tests { ) .await; let address2_u1_a1 = delegator - .get_address( + .get_http_address( "a1", &Some("u1".into()), &None, @@ -873,7 +987,7 @@ mod address_delegator_tests { ) .await; let address3_u2_a1 = delegator - .get_address( + .get_http_address( "a1", &Some("u2".into()), &None, @@ -881,7 +995,7 @@ mod address_delegator_tests { ) .await; let address4_u1_a2 = delegator - .get_address( + .get_http_address( "a2", &Some("u1".into()), &None, @@ -932,9 +1046,11 @@ mod address_delegator_tests { force_random_subdomains: true, random_subdomain_seed: Some(RandomSubdomainSeed::Fingerprint), random_subdomain_length: 6.try_into().unwrap(), + random_subdomain_filter_profanities: false, + requested_domain_filter: None, }); let address1_f1_a1_u0 = delegator - .get_address( + .get_http_address( "a1", &None, &Some(f1.clone()), @@ -942,7 +1058,7 @@ mod address_delegator_tests { ) .await; let address2_f1_a1_u0 = delegator - .get_address( + .get_http_address( "a1", &None, &Some(f1.clone()), @@ -950,7 +1066,7 @@ mod address_delegator_tests { ) .await; let address3_f2_a1_u0 = delegator - .get_address( + .get_http_address( "a1", &None, &Some(f2.clone()), @@ -958,7 +1074,7 @@ mod address_delegator_tests { ) .await; let address4_f1_a2_u0 = delegator - .get_address( + .get_http_address( "a2", &None, &Some(f1.clone()), @@ -966,7 +1082,7 @@ mod address_delegator_tests { ) .await; let address5_f1_a1_u1 = delegator - .get_address( + .get_http_address( "a1", &Some("u1".into()), &Some(f1.clone()), @@ -974,7 +1090,7 @@ mod address_delegator_tests { ) .await; let address6_f1_a1_u1 = delegator - .get_address( + .get_http_address( "a1", &Some("u1".into()), &Some(f1.clone()), @@ -982,7 +1098,7 @@ mod address_delegator_tests { ) .await; let address7_f2_a1_u1 = delegator - .get_address( + .get_http_address( "a1", &Some("u1".into()), &Some(f2.clone()), @@ -990,7 +1106,7 @@ mod address_delegator_tests { ) .await; let address8_f1_a2_u1 = delegator - .get_address( + .get_http_address( "a2", &Some("u1".into()), &Some(f1.clone()), @@ -998,7 +1114,7 @@ mod address_delegator_tests { ) .await; let address9_f1_a1_u2 = delegator - .get_address( + .get_http_address( "a1", &Some("u2".into()), &Some(f1.clone()), @@ -1006,7 +1122,7 @@ mod address_delegator_tests { ) .await; let address10_f1_a1_u2 = delegator - .get_address( + .get_http_address( "a1", &Some("u2".into()), &Some(f1.clone()), @@ -1014,7 +1130,7 @@ mod address_delegator_tests { ) .await; let address11_f2_a1_u2 = delegator - .get_address( + .get_http_address( "a1", &Some("u2".into()), &Some(f2.clone()), @@ -1022,7 +1138,7 @@ mod address_delegator_tests { ) .await; let address12_f1_a2_u2 = delegator - .get_address( + .get_http_address( "a2", &Some("u2".into()), &Some(f1.clone()), @@ -1123,9 +1239,11 @@ mod address_delegator_tests { force_random_subdomains: true, random_subdomain_seed: Some(RandomSubdomainSeed::IpAndUser), random_subdomain_length: 6.try_into().unwrap(), + random_subdomain_filter_profanities: false, + requested_domain_filter: None, }); let address1_u1_i1 = delegator - .get_address( + .get_http_address( "a1", &Some("user1".into()), &None, @@ -1133,7 +1251,7 @@ mod address_delegator_tests { ) .await; let address2_u1_i1 = delegator - .get_address( + .get_http_address( "a1", &Some("user1".into()), &None, @@ -1141,7 +1259,7 @@ mod address_delegator_tests { ) .await; let address3_u2_i1 = delegator - .get_address( + .get_http_address( "a1", &Some("user2".into()), &None, @@ -1149,7 +1267,7 @@ mod address_delegator_tests { ) .await; let address4_u1_i2 = delegator - .get_address( + .get_http_address( "a1", &Some("user1".into()), &None, @@ -1194,9 +1312,11 @@ mod address_delegator_tests { force_random_subdomains: true, random_subdomain_seed: Some(RandomSubdomainSeed::Address), random_subdomain_length: 6.try_into().unwrap(), + random_subdomain_filter_profanities: false, + requested_domain_filter: None, }); let address1_s1_a1 = delegator - .get_address( + .get_http_address( "a1", &None, &None, @@ -1204,7 +1324,7 @@ mod address_delegator_tests { ) .await; let address2_s1_a1 = delegator - .get_address( + .get_http_address( "a1", &None, &None, @@ -1212,7 +1332,7 @@ mod address_delegator_tests { ) .await; let address3_s2_a1 = delegator - .get_address( + .get_http_address( "a1", &None, &None, @@ -1220,7 +1340,7 @@ mod address_delegator_tests { ) .await; let address4_s1_a2 = delegator - .get_address( + .get_http_address( "a2", &None, &None, diff --git a/src/config.rs b/src/config.rs index d04ddef..a831823 100644 --- a/src/config.rs +++ b/src/config.rs @@ -269,6 +269,16 @@ pub struct ApplicationConfig { )] pub random_subdomain_length: NonZero, + /// Prevents random subdomains from containing profanities. + #[arg(long, default_value_t = false)] + pub random_subdomain_filter_profanities: bool, + + /// Prevents user-requested domains/subdomains from containing profanities. + /// + /// Beware that this can lead to false positives being blocked! + #[arg(long, default_value_t = false)] + pub requested_domain_filter_profanities: bool, + /// Grace period for dangling/unauthenticated SSH connections before they are forcefully disconnected. /// /// A low value may cause valid proxy/tunnel connections to be erroneously removed. diff --git a/src/lib.rs b/src/lib.rs index 852ef53..a436306 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,7 @@ use russh::{ use russh_keys::decode_secret_key; use rustls::ServerConfig; use rustls_acme::is_tls_alpn_challenge; +use rustrict::CensorStr; use ssh_key::Fingerprint; use sysinfo::{CpuRefreshKind, MemoryRefreshKind, Networks, RefreshKind, System}; use tcp::TcpHandler; @@ -183,6 +184,8 @@ impl SandholeServer { #[doc(hidden)] // Main entrypoint of the application. pub async fn entrypoint(config: ApplicationConfig) -> anyhow::Result<()> { + info!("Starting Sandhole..."); + // Check configuration flags for issues or other operations if config.disable_http && config.disable_tcp && config.disable_aliasing { return Err(ServerError::InvalidConfig( "One of HTTP, TCP, or aliasing must be enabled".into(), @@ -316,6 +319,21 @@ pub async fn entrypoint(config: ApplicationConfig) -> anyhow::Result<()> { )); // Add TCP handler service as a listener for TCP port updates. tcp_connections.update_reactor(Some(Arc::clone(&tcp_handler))); + // Add addressing service with optional profanity filtering + let requested_domain_filter: Option<&'static rustrict::Trie> = + if config.requested_domain_filter_profanities { + let mut trie = rustrict::Trie::default(); + if config.domain.is_inappropriate() { + warn!( + "Domain \"{}\" is considered a profanity; adding to safe word list", + config.domain + ); + trie.set(&config.domain, rustrict::Type::SAFE); + } + Some(Box::leak(Box::new(trie))) + } else { + None + }; let addressing = Arc::new(AddressDelegator::new(AddressDelegatorData { resolver: DnsResolver::new(), txt_record_prefix: config.txt_record_prefix.trim_matches('.').to_string(), @@ -324,6 +342,8 @@ pub async fn entrypoint(config: ApplicationConfig) -> anyhow::Result<()> { force_random_subdomains: !config.allow_requested_subdomains, random_subdomain_seed: config.random_subdomain_seed, random_subdomain_length: config.random_subdomain_length, + random_subdomain_filter_profanities: config.random_subdomain_filter_profanities, + requested_domain_filter, })); // Configure the default domain redirect for Sandhole. let domain_redirect = Arc::new(DomainRedirect { @@ -653,7 +673,7 @@ pub async fn entrypoint(config: ApplicationConfig) -> anyhow::Result<()> { .await .with_context(|| "Error listening to SSH port")?; info!("Listening for SSH connections on port {}.", config.ssh_port); - info!("sandhole is now running."); + info!("Sandhole is now running."); // Add OS signal handlers for termination. let signal_handler = wait_for_signal(); pin!(signal_handler); @@ -680,7 +700,7 @@ pub async fn entrypoint(config: ApplicationConfig) -> anyhow::Result<()> { } } } - info!("sandhole is shutting down."); + info!("Sandhole is shutting down."); join_handle_http.abort(); join_handle_https.abort(); Ok(()) diff --git a/src/ssh.rs b/src/ssh.rs index 4857356..ab7cc1a 100644 --- a/src/ssh.rs +++ b/src/ssh.rs @@ -955,7 +955,7 @@ impl Handler for ServerHandler { let assigned_host = self .server .address_delegator - .get_address(address, &self.user, &self.key_fingerprint, &self.peer) + .get_http_address(address, &self.user, &self.key_fingerprint, &self.peer) .await; // Add handler to HTTP connection map if let Err(err) = self.server.http.insert( -- 2.51.2