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(