Add Functionality for Tor Runner / Torrc Builder

This commit is contained in:
Sarah Jamie Lewis 2021-01-13 16:54:54 -08:00
parent dec3660f46
commit 178f426c7e
9 changed files with 263 additions and 10 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
/target
Cargo.lock
.idea/*
examples/client_data_dir
examples/server_data_dir
*_rc

View File

@ -1,14 +1,16 @@
[package]
name = "tapir-cwtch"
version = "0.1.7"
version = "0.1.8"
authors = ["Sarah Jamie Lewis <sarah@openprivacy.ca>"]
edition = "2018"
license = "MIT"
description = "Tapir is a small library for building p2p applications over anonymous communication systems"
repository = "https://git.openprivacy.ca/sarah/tapir-rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
# Compile with support for launching v3 onion services
onionv3 = []
[[test]]
@ -23,6 +25,7 @@ merlin = "2.0.0"
hex = "0.4.2"
base32 = "0.4.0"
base64 = "0.13.0"
sha1 = "0.6.0"
sha3 = "0.9.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.61"
@ -32,3 +35,4 @@ integer-encoding = "2.1.1"
secretbox = "0.1.2"
subtle = "2.3.0"
hashbrown = "0.9.1"
which = "4.0.2"

View File

@ -1,4 +1,7 @@
use rand::{thread_rng, Rng};
use std::sync::Arc;
use tapir_cwtch::acns::tor::run::TorRunner;
use tapir_cwtch::acns::tor::torrc::TorrcGenerator;
use tapir_cwtch::applications::authentication_app::{AuthenicationApp, AUTHENTICATION_CAPABILITY};
use tapir_cwtch::connections::service::Service;
use tapir_cwtch::connections::{Connection, ConnectionInterface, OutboundConnection};
@ -9,7 +12,19 @@ fn main() {
let identity = Arc::new(Identity::initialize_ephemeral_identity());
println!("Setup: {}", identity.hostname());
let mut service = Service::init(identity.clone());
let tor_path = which::which("tor");
let mut rng = thread_rng();
let socks_port = rng.gen_range(9052, 9100);
let mut tor_runner = TorRunner::run(
TorrcGenerator::new().with_socks_port(socks_port),
"./example_client_rc",
tor_path.unwrap().to_str().unwrap(),
"./examples/client_data_dir/",
)
.unwrap();
tor_runner.wait_until_bootstrapped();
let mut service = Service::init(identity.clone(), socks_port);
let identity = identity.clone();
let outbound_identity = identity.clone();
let outbound_service = |conn: Connection<OutboundConnection>| {
@ -24,7 +39,7 @@ fn main() {
}
}
};
match service.connect("qz4o3mqhzw6ye3jjadbi3l7g2rldbnkidf255iwsl4vfvrzsl26drfad", outbound_service.clone()) {
match service.connect("srg3w256xrpcs6o25i5qlo6iviwsybnfw66nwelrbah7c5e6knb32xyd", outbound_service.clone()) {
_ => {}
}
loop {}

View File

@ -1,5 +1,8 @@
use rand::{thread_rng, Rng};
use std::sync::Arc;
use tapir_cwtch::acns::tor::authentication::HashedPassword;
use tapir_cwtch::acns::tor::run::TorRunner;
use tapir_cwtch::acns::tor::torrc::TorrcGenerator;
use tapir_cwtch::acns::tor::TorProcess;
use tapir_cwtch::applications::authentication_app::AuthenicationApp;
use tapir_cwtch::connections::service::Service;
@ -8,7 +11,23 @@ use tapir_cwtch::primitives::identity::Identity;
use tapir_cwtch::primitives::transcript::Transcript;
fn main() {
let mut auth_control_port = TorProcess::connect(9051)
let tor_path = which::which("tor");
let mut rng = thread_rng();
let socks_port = rng.gen_range(10052, 11100);
let control_port = rng.gen_range(9052, 9100);
let mut tor_runner = TorRunner::run(
TorrcGenerator::new()
.with_socks_port(socks_port)
.with_control_port(control_port)
.with_hashed_control_password("examplehashedpassword"),
"./example_server_rc",
tor_path.unwrap().to_str().unwrap(),
"./examples/server_data_dir/",
)
.unwrap();
tor_runner.wait_until_bootstrapped();
let mut auth_control_port = TorProcess::connect(control_port)
.unwrap()
.authenticate(Box::new(HashedPassword::new(String::from("examplehashedpassword"))))
.unwrap();
@ -20,7 +39,7 @@ fn main() {
println!("Service Id: {}", service_id);
println!("Setup: {}", identity.hostname());
let service = Service::init(identity.clone());
let service = Service::init(identity.clone(), 0);
let inbound_service = |conn: Connection<InboundConnection>| {
let mut transcript = Transcript::new_transcript("tapir-transcript");

View File

@ -1,7 +1,9 @@
#[cfg(any(feature = "onionv3"))]
pub mod tor;
#[derive(Debug)]
pub enum ACNError {
ConfigurationError(String),
AuthenticationError(String),
ServiceSetupError(String),
}

View File

@ -7,6 +7,8 @@ use std::io::{BufRead, BufReader, Error};
use std::net::TcpStream;
pub mod authentication;
pub mod run;
pub mod torrc;
#[derive(Debug)]
pub struct TorDisconnected(());

75
src/acns/tor/run.rs Normal file
View File

@ -0,0 +1,75 @@
use crate::acns::tor::torrc::TorrcGenerator;
use crate::acns::ACNError;
use std::io::{BufRead, BufReader};
use std::process::{Child, Command, Stdio};
#[derive(Debug)]
pub struct TorRunner {
process: Box<Child>,
}
impl TorRunner {
/// run Tor at the given tor_path with the given torrc
pub fn run(torrc: TorrcGenerator, torrc_path: &str, tor_path: &str, tor_data_dir: &str) -> Result<TorRunner, ACNError> {
match torrc.build(torrc_path) {
Ok(()) => {
let child = Command::new(tor_path)
.arg("-f")
.arg(torrc_path)
.arg("--DataDirectory")
.arg(tor_data_dir)
.stdout(Stdio::piped())
.spawn()
.unwrap();
Ok(TorRunner { process: Box::new(child) })
}
Err(e) => Err(e),
}
}
pub fn wait_until_bootstrapped(&mut self) {
let child_stdout = self.process.stdout.as_mut().unwrap();
let mut reader = BufReader::new(child_stdout);
loop {
let mut line = String::new();
reader.read_line(&mut line);
if line.is_empty() == false {
if line.contains("Bootstrapped 100% (done): Done") {
return;
}
}
}
}
}
/// Kill Tor when the TorRunner drops out of scope
impl Drop for TorRunner {
fn drop(&mut self) {
match self.process.kill() {
Ok(()) => eprintln!("Killing Tor...{:?}", self.process.wait()),
Err(_err) => eprintln!("Could not Kill Tor..."),
}
}
}
#[cfg(test)]
mod tests {
use crate::acns::tor::run::TorRunner;
use crate::acns::tor::torrc::TorrcGenerator;
use std::fs::remove_file;
use std::thread::sleep;
use std::time::Duration;
#[test]
fn test_run_tor() {
match TorRunner::run(TorrcGenerator::new().with_socks_port(9055), "./torrc", "/usr/sbin/tor") {
Ok(runner) => {
sleep(Duration::new(5, 0));
assert!(remove_file("./torrc").is_ok());
println!("Runner {:?}", runner)
}
_ => panic!("tor did not run"),
}
sleep(Duration::new(10, 0));
}
}

130
src/acns/tor/torrc.rs Normal file
View File

@ -0,0 +1,130 @@
use crate::acns::ACNError;
use crate::acns::ACNError::ConfigurationError;
use std::fs::File;
use std::io::Write;
/// Build a Torrc File with a set of configured options. Note: This is not a complete Torrc builder
/// although it might one day grow to be one. As it stands it only allows the configuration options
/// most necessary for Tor v3 Onion Service clients/servers as they related to Tapir/Cwtch.
/// We use the Builder patter to allow easy programmatic construction for different Tor configurations.
pub struct TorrcGenerator {
socks_port: u16,
control_port: u16,
hashed_password: String,
}
impl TorrcGenerator {
/// Generate a new, empty Torrc Generator.
pub fn new() -> TorrcGenerator {
TorrcGenerator {
socks_port: 0,
control_port: 0,
hashed_password: String::new(),
}
}
/// Configure Tor to open a port to connect to external servers over Tor via SOCKS5
pub fn with_socks_port(mut self, socks_port: u16) -> Self {
self.socks_port = socks_port;
self
}
/// Configure Tor to have an open Control Port for configuration.
pub fn with_control_port(mut self, control_port: u16) -> Self {
self.control_port = control_port;
self
}
/// Force the tor control port to accept a hashed password as authentication, the given password
/// is hashed with a random salt
/// From the Tor Docs: If the 'HashedControlPassword' option is set, it must contain the salted
/// hash of a secret password. The salted hash is computed according to the
/// S2K algorithm in RFC 2440 (OpenPGP), and prefixed with the s2k specifier.
/// This is then encoded in hexadecimal, prefixed by the indicator sequence
/// "16:".
pub fn with_hashed_control_password(mut self, password: &str) -> Self {
let salt: [u8; 8] = rand::random();
self.hashed_password = TorrcGenerator::generate_hashed_password(&salt, password);
self
}
/// generate_hashed_password calculates a hash in the same way tha tor --hash-password does
/// this function takes a salt as input which is not great from an api-misuse perspective, but
/// we make it private.
fn generate_hashed_password(salt: &[u8; 8], password: &str) -> String {
let c = 96;
let mut count = (16 + (c & 15)) << ((c >> 4) + 6);
let mut tmp = vec![];
tmp.extend_from_slice(salt);
tmp.extend_from_slice(password.as_bytes());
let slen = tmp.len();
let mut hash = sha1::Sha1::new();
while count != 0 {
if count > slen {
hash.update(tmp.as_slice());
count -= slen
} else {
let tmp_slice = tmp.as_slice();
hash.update(tmp_slice.split_at(count).0);
count = 0
}
}
let hashed = hash.digest().bytes();
return format!("16:{}{:X}{}", hex::encode_upper(salt), c, hex::encode_upper(hashed));
}
/// Compile the final torrc file and write it to the file system.
pub fn build(&self, file_name: &str) -> Result<(), ACNError> {
let mut file = match File::create(file_name) {
Err(why) => return Err(ConfigurationError(why.to_string())),
Ok(file) => file,
};
let mut lines = vec![];
if self.socks_port != 0 {
lines.push(format!("SocksPort {}", self.socks_port));
}
if self.control_port != 0 {
lines.push(format!("ControlPort {}", self.control_port));
}
if self.hashed_password.is_empty() == false {
lines.push(format!("HashedControlPassword {}", self.hashed_password));
}
match file.write_all(lines.join("\n").as_bytes()) {
Ok(_) => Ok(()),
Err(why) => Err(ConfigurationError(why.to_string())),
}
}
}
#[cfg(test)]
mod tests {
use crate::acns::tor::torrc::TorrcGenerator;
use std::fs::{read_to_string, remove_file};
use std::path::Path;
#[test]
fn generate_hashed_password() {
let salt: [u8; 8] = [0xC1, 0x53, 0x05, 0xF9, 0x77, 0x89, 0x41, 0x4B];
assert_eq!(
String::from("16:C15305F97789414B601259E3EC5E76B8E55FC56A9F562B713F3D2BA257"),
TorrcGenerator::generate_hashed_password(&salt, "examplehashedpassword")
);
}
#[test]
fn generate_torrc() {
let gen = TorrcGenerator::new().with_control_port(9051).with_hashed_control_password("examplehashedpassword");
match gen.build("./torrc") {
Ok(()) => {
assert!(Path::new("./torrc").exists());
println!("{}", read_to_string("./torrc").unwrap());
assert!(remove_file("./torrc").is_ok());
}
_ => assert!(false),
}
}
}

View File

@ -13,6 +13,7 @@ pub struct ApplicationListenService(());
pub struct Service<ListenService> {
identity: Arc<Identity>,
socks_port: u16,
listen_service: ListenService,
}
@ -21,7 +22,7 @@ impl<ListenService> Service<ListenService> {
where
F: FnOnce(Connection<OutboundConnection>) + Send + Clone + 'static,
{
let conn = Socks5Stream::connect(format!("127.0.0.1:9050"), Domain(format!("{}.onion", hostname), 9878));
let conn = Socks5Stream::connect(format!("127.0.0.1:{}", self.socks_port), Domain(format!("{}.onion", hostname), 9878));
match conn {
Ok(conn) => {
let application = application.clone();
@ -34,9 +35,10 @@ impl<ListenService> Service<ListenService> {
}
impl Service<NoListenService> {
pub fn init(identity: Arc<Identity>) -> Service<NoListenService> {
pub fn init(identity: Arc<Identity>, socks_port: u16) -> Service<NoListenService> {
Service {
identity,
socks_port,
listen_service: NoListenService(()),
}
}
@ -66,6 +68,7 @@ impl Service<NoListenService> {
Ok(Service {
identity: self.identity,
socks_port: self.socks_port,
listen_service: jh,
})
}
@ -94,7 +97,7 @@ mod tests {
let keypair = ed25519_dalek::Keypair::generate(&mut csprng);
let _secret_key = SecretKey::from_bytes(&keypair.secret.to_bytes());
let identity = Identity::initialize(keypair);
let _service = Service::init(identity);
let _service = Service::init(identity, 9051);
//let mut listen_service = service.listen(10000, TranscriptApp::new_instance()).unwrap_or_else(|_| panic!());
// this will not compile! wish we could test that service.connect(Hostname{},TranscriptApp::new_instance());
}
@ -105,7 +108,7 @@ mod tests {
let keypair = ed25519_dalek::Keypair::generate(&mut csprng);
let _secret_key = SecretKey::from_bytes(&keypair.secret.to_bytes());
let identity = Identity::initialize(keypair);
let _service = Service::init(identity);
let _service = Service::init(identity, 9051);
//let listen_service = service.listen(1000, TranscriptApp::new_instance()).unwrap_or_else(|_| panic!());
// TODO use trybuild to test that this fails: service.connect(Hostname{},TranscriptApp::new_instance());
}