diff --git a/Cargo.toml b/Cargo.toml index a12f9832..8ebb36d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,6 @@ path = "src/bin/local.rs" name = "ssserver" path = "src/bin/server.rs" -[dependencies.rust-crypto] +# [dependencies.rust-crypto] -git = "https://github.com/DaGenix/rust-crypto.git" +# git = "https://github.com/DaGenix/rust-crypto.git" diff --git a/src/bin/local.rs b/src/bin/local.rs index f2fbf5ab..7cd3b971 100644 --- a/src/bin/local.rs +++ b/src/bin/local.rs @@ -23,7 +23,6 @@ fn main() { optopt("p", "server-port", "server port", ""), optopt("l", "local-port", "local socks5 proxy port", ""), optopt("m", "encrypt-method", "entryption method", "aes-256-cfb"), - optflag("d", "debug", "print debug message"), ]; let matches = getopts(os::args().tail(), opts).unwrap(); @@ -39,14 +38,20 @@ fn main() { return; } - let mut config: Config; - - if matches.opt_present("c") { - config = Config::load_from_file(matches.opt_str("c") - .unwrap().as_slice()) + let mut config = if matches.opt_present("c") { + Config::load_from_file(matches.opt_str("c") + .unwrap().as_slice()).unwrap() } else { - config = Config::new() - } + match Config::load_from_file("config.json") { + Some(c) => c, + None => { + error!("Cannot find any `config.json` under current directory"); + println!("{}", usage(format!("Usage: {} [options]", os::args()[0]).as_slice(), + opts)); + return; + } + } + }; if matches.opt_present("s") { let server_ip = matches.opt_str("s").unwrap(); @@ -79,8 +84,7 @@ fn main() { info!("ShadowSocks {}", shadowsocks::VERSION); - println!("{}", config) + debug!("Config: {}", config) - let mut tcp_server = TcpRelayLocal::new(&config); - tcp_server.run(); + TcpRelayLocal::new(&config).run(); } diff --git a/src/bin/server.rs b/src/bin/server.rs index 07413774..f42a5668 100644 --- a/src/bin/server.rs +++ b/src/bin/server.rs @@ -9,6 +9,8 @@ use getopts::{optopt, optflag, getopts, usage}; use std::os; use shadowsocks::config::Config; +use shadowsocks::tcprelay::TcpRelayServer; +use shadowsocks::relay::Relay; fn main() { let opts = [ @@ -21,7 +23,6 @@ fn main() { optopt("p", "server-port", "server port", ""), optopt("l", "local-port", "local socks5 proxy port", ""), optopt("m", "encrypt-method", "entryption method", "aes-256-cfb"), - optflag("d", "debug", "print debug message"), ]; let matches = getopts(os::args().tail(), opts).unwrap(); @@ -37,14 +38,18 @@ fn main() { return; } - let mut config: Config; - - if matches.opt_present("c") { - config = Config::load_from_file(matches.opt_str("c") - .unwrap().as_slice()) + let mut config = if matches.opt_present("c") { + Config::load_from_file(matches.opt_str("c") + .unwrap().as_slice()).unwrap() } else { - config = Config::new() - } + match Config::load_from_file("config.json") { + Some(c) => c, + None => { + error!("Cannot find any `config.json` under current directory"); + return; + } + } + }; if matches.opt_present("s") { let server_ip = matches.opt_str("s").unwrap(); @@ -77,5 +82,7 @@ fn main() { info!("ShadowSocks {}", shadowsocks::VERSION); - println!("{}", config) + debug!("Config: {}", config) + + TcpRelayServer::new(&config).run(); } diff --git a/src/config.rs b/src/config.rs index 6a8ae898..1d4e5669 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ extern crate serialize; -use serialize::{Decodable, Encodable}; +use serialize::Encodable; use serialize::json; use std::io::{File, Read, Open}; @@ -29,56 +29,98 @@ impl Config { server_port: 8000, local_port: 8000, password: "".to_string(), - method: "aes-256-cfb".to_string(), + method: "aes-128-cfb".to_string(), timeout: None, fast_open: false, } } - fn parse_json_object(o: &json::JsonObject) -> Config { + fn parse_json_object(o: &json::JsonObject) -> Option { let mut config = Config::new(); for (key, value) in o.iter() { match key.as_slice() { "server" => { - config.server = value.as_string().unwrap().to_string(); + config.server = match value.as_string() { + Some(v) => v.to_string(), + None => return None, + }; }, "server_port" => { - config.server_port = value.as_i64().unwrap() as u16; + config.server_port = match value.as_i64() { + Some(v) => v as u16, + None => return None, + }; }, "local_port" => { - config.local_port = value.as_i64().unwrap() as u16; + config.local_port = match value.as_i64() { + Some(v) => v as u16, + None => return None, + }; }, "password" => { - config.password = value.as_string().unwrap().to_string(); + config.password = match value.as_string() { + Some(v) => v.to_string(), + None => return None, + }; }, "method" => { - config.method = value.as_string().unwrap().to_string(); + config.method = match value.as_string() { + Some(v) => v.to_string(), + None => return None, + }; }, "timeout" => { - config.timeout = Some(value.as_i64().unwrap() as u64); + config.timeout = match value.as_i64() { + Some(v) => Some(v as u64), + None => return None, + }; }, "fast_open" => { - config.fast_open = value.as_boolean().unwrap(); + config.fast_open = match value.as_boolean() { + Some(v) => v, + None => return None, + } }, _ => (), } } - config + Some(config) } - pub fn load_from_str(s: &str) -> Config { - let object = json::from_str(s).unwrap(); - let json_object = object.as_object().unwrap(); + pub fn load_from_str(s: &str) -> Option { + let object = match json::from_str(s) { + Ok(obj) => { obj }, + Err(e) => return None, + }; + + let json_object = match object.as_object() { + Some(obj) => { obj }, + None => return None, + }; + Config::parse_json_object(json_object) } - pub fn load_from_file(filename: &str) -> Config { - let reader = &mut File::open_mode(&Path::new(filename), Open, Read).unwrap(); + pub fn load_from_file(filename: &str) -> Option { + let mut readeropt = File::open_mode(&Path::new(filename), Open, Read); + + let mut reader = match readeropt { + Ok(ref mut r) => r, + Err(..) => return None, + }; + + let object = match json::from_reader(reader) { + Ok(obj) => { obj }, + Err(..) => return None, + }; + + let json_object = match object.as_object() { + Some(obj) => obj, + None => return None, + }; - let object = json::from_reader(reader).unwrap(); - let json_object = object.as_object().unwrap(); Config::parse_json_object(json_object) } } diff --git a/src/crypto.rs b/src/crypto.rs deleted file mode 100644 index 286240cb..00000000 --- a/src/crypto.rs +++ /dev/null @@ -1,4 +0,0 @@ -extern crate "rust-crypto" as crypto; - -pub const AES_128_CFB: &'static str = "aes-128-cfb"; -pub const AES_256_CFB: &'static str = "aes-256-cfb"; diff --git a/src/crypto/cipher.rs b/src/crypto/cipher.rs new file mode 100644 index 00000000..c41afd81 --- /dev/null +++ b/src/crypto/cipher.rs @@ -0,0 +1,5 @@ + +pub trait Cipher { + fn encrypt(&self, data: &[u8]) -> Vec; + fn decrypt(&self, data: &[u8]) -> Vec; +} diff --git a/src/crypto/crypto.rs b/src/crypto/crypto.rs new file mode 100644 index 00000000..7f395ed2 --- /dev/null +++ b/src/crypto/crypto.rs @@ -0,0 +1,120 @@ +extern crate libc; + +use std::vec::Vec; + +pub const CIPHER_AES_128_CFB: &'static str = "aes-128-cfb1"; +pub const CIPHER_AES_256_CFB: &'static str = "aes-256-cfb"; + +struct CipherDef(&'static str, int, int); + +const Ciphers: [CipherDef, .. 2] = [ + CipherDef(CIPHER_AES_128_CFB, 16, 16), + CipherDef(CIPHER_AES_256_CFB, 32, 16), +]; + +pub enum CipherType { + CipherAes128Cfb, + CipherAes256Cfb, +} + +#[allow(non_camel_case_types)] +type EVP_CIPHER_CTX = *mut libc::c_void; +#[allow(non_camel_case_types)] +type EVP_CIPHER = *mut libc::c_void; + +pub const CIPHER_MODE_ENCRYPT: libc::c_int = 1; +pub const CIPHER_MODE_DECRYPT: libc::c_int = 0; + +pub enum CipherMode { + CipherModeEncrypt, + CipherModeDecrypt, +} + + + +pub fn cipher_type_from_str(type_string: &str) -> CipherType { + match type_string { + CIPHER_AES_128_CFB => CipherAes128Cfb, + CIPHER_AES_256_CFB => CipherAes256Cfb, + _ => fail!("Unknown cipher type"), + } +} + +pub struct Crypto { + cipher: EVP_CIPHER, + cipher_ctx: EVP_CIPHER_CTX, +} + +impl Crypto { + pub fn new(method: CipherType, key: &str, iv: &str, mode: CipherMode) -> Crypto { + let cipher = unsafe { + match method { + CipherAes128Cfb => EVP_aes_128_cfb128(), + CipherAes256Cfb => EVP_aes_256_cfb(), + } + }; + assert!(!cipher.is_null()); + let cipher_ctx = unsafe { EVP_CIPHER_CTX_new() }; + + let op = match mode { + CipherModeEncrypt => 1, + CipherModeDecrypt => 0 + }; + + unsafe { + EVP_CipherInit(cipher_ctx, cipher, + key.to_c_str().as_ptr(), iv.to_c_str().as_ptr(), op); + } + + Crypto { + cipher: cipher, + cipher_ctx: cipher_ctx, + } + } + + pub fn update(&self, data: &[u8]) -> Vec { + let pdata: *const u8 = data.as_ptr(); + let datalen: u32 = data.len() as u32; + let mut reslen: u32 = datalen + (16 as u32); + let preslen: *mut u32 = &mut reslen; + let mut res = Vec::from_elem(reslen as uint, 0u8); + let pres: *mut libc::c_uchar = res.as_mut_ptr(); + + unsafe { + EVP_CipherUpdate(self.cipher_ctx, pres, preslen, pdata, datalen); + } + + res + } + + pub fn drop(&mut self) { + unsafe { + EVP_CIPHER_CTX_cleanup(self.cipher_ctx); + EVP_CIPHER_CTX_free(self.cipher_ctx); + } + } +} + +pub struct Cipher { + encryptor: Crypto, + decryptor: Crypto, +} + +impl Cipher { + fn new(method: CipherType, key: &str) -> Cipher { + + } + + pub fn encrypt(&self, data: &[u8]) -> Vec { + + } + + pub fn decrypt(&self, data: &[u8]) -> Vec { + + } +} + +#[test] +fn test_encrypt_decrypt() { + +} diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs new file mode 100644 index 00000000..7ba682ce --- /dev/null +++ b/src/crypto/mod.rs @@ -0,0 +1,5 @@ +extern crate libc; + +pub mod cipher; +pub mod openssl; + diff --git a/src/crypto/openssl.rs b/src/crypto/openssl.rs new file mode 100644 index 00000000..d67d5b05 --- /dev/null +++ b/src/crypto/openssl.rs @@ -0,0 +1,198 @@ +extern crate libc; +extern crate log; + +use crypto::cipher::Cipher; + +use std::str; +use std::ptr; + +#[allow(non_camel_case_types)] +type EVP_CIPHER_CTX = *const libc::c_void; +#[allow(non_camel_case_types)] +type EVP_CIPHER = *mut libc::c_void; +#[allow(non_camel_case_types)] +type EVP_MD = *const libc::c_void; + +const CIPHER_MODE_ENCRYPT: libc::c_int = 1; +const CIPHER_MODE_DECRYPT: libc::c_int = 0; + +#[allow(dead_code)] +#[link(name="crypto")] +extern { + fn EVP_get_cipherbyname(name: *const libc::c_char) -> EVP_CIPHER; + fn EVP_CIPHER_CTX_new() -> EVP_CIPHER_CTX; + fn EVP_CIPHER_CTX_cleanup(ctx: EVP_CIPHER_CTX); + fn EVP_CIPHER_CTX_free(ctx: EVP_CIPHER_CTX); + + fn EVP_CipherInit(ctx: EVP_CIPHER_CTX, evp: EVP_CIPHER, + key: *const libc::c_uchar, iv: *const libc::c_uchar, mode: libc::c_int) -> libc::c_int; + fn EVP_CipherUpdate(ctx: EVP_CIPHER_CTX, + outbuf: *mut libc::c_uchar, outlen: *mut libc::c_int, + inbuf: *const libc::c_uchar, inlen: libc::c_int) -> libc::c_int; + fn EVP_CipherFinal(ctx: EVP_CIPHER_CTX, res: *mut libc::c_uchar, len: *mut libc::c_int) -> libc::c_int; + + fn EVP_BytesToKey(cipher: EVP_CIPHER, md: EVP_MD, + salt: *const libc::c_uchar, data: *const libc::c_uchar, datal: libc::c_int, + count: libc::c_int, key: *mut libc::c_uchar, iv: *mut libc::c_uchar) -> libc::c_int; + + // Ciphers + fn EVP_aes_128_cfb128() -> EVP_CIPHER; + fn EVP_aes_192_cfb() -> EVP_CIPHER; + fn EVP_aes_256_cfb() -> EVP_CIPHER; + + // MD + fn EVP_md5() -> EVP_MD; + fn EVP_sha() -> EVP_MD; + fn EVP_sha1() -> EVP_MD; + + // Errors + fn ERR_print_errors_fp(fp: libc::c_int); +} + +enum CryptoMode { + CryptoModeDecrypt, + CryptoModeEncrypt, +} + +enum CipherType { + CipherTypeAes128Cfb, + CipherTypeAes192Cfb, + CipherTypeAes256Cfb, +} + +struct OpenSSLCrypto { + evp_ctx: EVP_CIPHER_CTX, + block_size: uint, + // key_size: uint, +} + +impl OpenSSLCrypto { + pub fn new(cipher_type: CipherType, key: &[u8], mode: CryptoMode) -> OpenSSLCrypto { + let (ctx, _, block_size) = unsafe { + let (cipher, key_size, block_size) = match cipher_type { + CipherTypeAes128Cfb => (EVP_aes_128_cfb128(), 16, 16), + CipherTypeAes192Cfb => (EVP_aes_192_cfb() , 24, 16), + CipherTypeAes256Cfb => (EVP_aes_256_cfb() , 32, 16), + }; + + let evp_ctx = EVP_CIPHER_CTX_new(); + + let mut pad_key: Vec = Vec::with_capacity(key_size); + let mut pad_iv: Vec = Vec::with_capacity(block_size); + + EVP_BytesToKey(cipher, EVP_md5(), ptr::null(), key.as_ptr(), key.len() as libc::c_int, + 1, pad_key.as_mut_ptr(), pad_iv.as_mut_ptr()); + + let op = match mode { + CryptoModeEncrypt => 1 as libc::c_int, + CryptoModeDecrypt => 0 as libc::c_int, + }; + + if EVP_CipherInit(evp_ctx, cipher, pad_key.as_slice().as_ptr(), + pad_iv.as_slice().as_ptr(), op) != 1 as libc::c_int { + EVP_CIPHER_CTX_free(evp_ctx); + fail!("EVP_CipherInit error"); + } + + (evp_ctx, key_size, block_size) + }; + + OpenSSLCrypto { + evp_ctx: ctx, + block_size: block_size, + // key_size: key_size, + } + } + + pub fn cipher(&self, data: &[u8]) -> Vec { + unsafe { + let pdata: *const u8 = data.as_ptr(); + let datalen: libc::c_int = data.len() as libc::c_int; + + let reslen: uint = datalen as uint + self.block_size; + let mut res = Vec::from_elem(reslen, 0u8); + + let mut len: libc::c_int = 0; + let pres: *mut u8 = res.as_mut_ptr(); + + if EVP_CipherUpdate(self.evp_ctx, + pres, &mut len, + pdata, datalen) != 1 { + drop(self); + fail!("Failed on EVP_CipherUpdate"); + } + + let mut total_length = len; + if EVP_CipherFinal(self.evp_ctx, pres.offset(len as int), &mut len) != 1 { + drop(self); + fail!("Failed on EVP_CipherFinal"); + } + + total_length += len; + + res.truncate(total_length as uint); + + res + } + } + + +} + + +#[unsafe_destructor] +impl Drop for OpenSSLCrypto { + fn drop(&mut self) { + unsafe { + EVP_CIPHER_CTX_cleanup(self.evp_ctx); + EVP_CIPHER_CTX_free(self.evp_ctx); + } + } +} + +struct OpenSSLCipher { + encryptor: OpenSSLCrypto, + decryptor: OpenSSLCrypto, +} + +impl OpenSSLCipher { + pub fn new(cipher_type: CipherType, key: &[u8]) -> OpenSSLCipher { + let enc = OpenSSLCrypto::new(cipher_type, key, CryptoModeEncrypt); + let dec = OpenSSLCrypto::new(cipher_type, key, CryptoModeDecrypt); + + OpenSSLCipher { + encryptor: enc, + decryptor: dec, + } + } +} + +impl Cipher for OpenSSLCipher { + fn encrypt(&self, data: &[u8]) -> Vec { + self.encryptor.cipher(data) + } + + fn decrypt(&self, data: &[u8]) -> Vec { + self.decryptor.cipher(data) + } +} + +#[test] +fn test_aes() { + let message = "hello world"; + let key = "passwordhaha"; + + let types = [CipherTypeAes128Cfb, CipherTypeAes192Cfb, CipherTypeAes256Cfb]; + + for t in types.iter() { + let cipher = OpenSSLCipher::new(*t, key.as_bytes()); + + let encrypted_msg = cipher.encrypt(message.as_bytes()); + debug!("ENC {}", encrypted_msg); + + let decrypted_msg = cipher.decrypt(encrypted_msg.as_slice()); + debug!("DEC {}", str::from_utf8(decrypted_msg.as_slice()).unwrap()); + + assert!(message == str::from_utf8(decrypted_msg.as_slice()).unwrap()); + } +} diff --git a/src/lib.rs b/src/lib.rs index d6e66cae..7c389754 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ #![crate_type="lib"] #![crate_name="shadowsocks"] -#![feature(phase)] +#![feature(phase, unsafe_destructor)] extern crate serialize; #[phase(plugin, link)] @@ -12,4 +12,4 @@ pub mod config; pub mod relay; pub mod tcprelay; pub mod udprelay; -pub mod crypto; +mod crypto; diff --git a/src/relay.rs b/src/relay.rs index 12c287e9..3dbcc4cb 100644 --- a/src/relay.rs +++ b/src/relay.rs @@ -11,3 +11,13 @@ pub enum Stage { StageReply, StageStream, } + +pub const SOCK5_VERSION : u8 = 5; + +pub const SOCK5_CMD_TCP_CONNECT : u8 = 1; +pub const SOCK5_CMD_TCP_BIND : u8 = 2; +pub const SOCK5_CMD_UDP_ASSOCIATE : u8 = 3; + +pub const SOCK5_ADDR_MODE_IPV4 : u8 = 0x01; +pub const SOCK5_ADDR_MODE_DOMAIN_NAME : u8 = 0x03; +pub const SOCK5_ADDR_MODE_IPV6 : u8 = 0x04; diff --git a/src/tcprelay.rs b/src/tcprelay.rs index 98ef9ce6..a0a399ca 100644 --- a/src/tcprelay.rs +++ b/src/tcprelay.rs @@ -4,28 +4,121 @@ extern crate log; use relay::Relay; use relay::Stage; use relay::{StageInit, StageHello, StageUdpAssoc, StageDns, StageReply, StageStream}; +use relay; // mod config; use config::Config; -use std::io::TcpListener; +use std::io::{TcpListener}; use std::io::{Acceptor, Listener}; -use std::io::net::tcp::TcpAcceptor; +use std::io::net::tcp::{TcpAcceptor, TcpStream}; use std::io::{TimedOut, EndOfFile}; - use std::str; +use std::sync::Arc; pub struct TcpRelayLocal { + acceptor: TcpAcceptor, + stage: Stage, + config: Config, +} + +impl TcpRelayLocal { + pub fn new(c: &Config) -> TcpRelayLocal { + let acceptor = TcpListener::bind(c.local.as_slice(), c.local_port).unwrap().listen().unwrap(); + + TcpRelayLocal { + acceptor: acceptor, + stage: StageInit, + config: c.clone(), + } + } + + fn handle_hello(remote_stream: &mut TcpStream) { + let buf = [relay::SOCK5_VERSION, 1, 1]; + debug!("Sent {} to server", buf.to_vec()); + remote_stream.write(buf); + + let reply = remote_stream.read_exact(2).unwrap(); + + debug!("Recv {} from server", reply); + + if reply[0] != relay::SOCK5_VERSION { + fail!("Invalid sock5 version") + } + + let method_num = reply[1]; + + if method_num == 0xff { + fail!("Server does not support the encrypt method"); + } + } + + fn handle_auth(remote_stream: &mut TcpStream) { + + } +} + +impl Relay for TcpRelayLocal { + fn run(&mut self) { + let server_str_arc = Arc::new(self.config.server.clone()); + let server_port_arc = Arc::new(self.config.server_port.clone()); + let encrypt_password = Arc::new(self.config.password.clone()); + + loop { + let server_str = server_str_arc.clone(); + let server_port = server_port_arc.clone(); + + match self.acceptor.accept() { + Ok(mut stream) => spawn(proc() { + info!("Client {} connected", stream.peer_name().unwrap()); + + let server = server_str.as_slice(); + + let mut remote_stream = TcpStream::connect(server, *server_port.deref()).unwrap(); + TcpRelayLocal::handle_hello(&mut remote_stream); + TcpRelayLocal::handle_auth(&mut remote_stream); + + loop { + let mut buf = [0u8, .. 10240]; + match stream.read(buf) { + Ok(len) => { + let s = buf.slice_to(len); + + remote_stream.write(s); + }, + Err(err) => { + if err.kind == EndOfFile { + break + } + error!("Err: {}", err); + break + } + } + } + + info!("Client {} disconnected", stream.peer_name().unwrap()); + + drop(stream) + }), + Err(e) => { + fail!(e) + } + } + } + } +} + +pub struct TcpRelayServer { acceptor: TcpAcceptor, stage: Stage, timeout: Option, } -impl TcpRelayLocal { - pub fn new(c: &Config) -> TcpRelayLocal { - let mut acceptor = TcpListener::bind(c.local.as_slice(), c.local_port).unwrap().listen().unwrap(); +impl TcpRelayServer { + pub fn new(c: &Config) -> TcpRelayServer { + let acceptor = TcpListener::bind(c.server.as_slice(), c.server_port).unwrap().listen().unwrap(); - TcpRelayLocal { + TcpRelayServer { acceptor: acceptor, stage: StageInit, timeout: c.timeout, @@ -36,16 +129,16 @@ impl TcpRelayLocal { loop { match self.acceptor.accept() { Ok(mut stream) => spawn(proc() { - info!("Client {} connected", stream.socket_name().unwrap()); + info!("Client {} connected", stream.peer_name().unwrap()); + + TcpRelayServer::handle_hello(&mut stream); loop { let mut buf = [0u8, .. 10240]; match stream.read(buf) { Ok(len) => { - println!("Len: {}", len) - let s = buf.slice_to(len); - println!("Received: {}", str::from_utf8(s).unwrap()); + debug!("{} Received: {}", stream.peer_name().unwrap(), s); stream.write(s).unwrap() }, Err(err) => { @@ -58,7 +151,7 @@ impl TcpRelayLocal { } } - info!("Client {} disconnected", stream.socket_name().unwrap()); + info!("Client {} disconnected", stream.peer_name().unwrap()); drop(stream) }), @@ -68,19 +161,31 @@ impl TcpRelayLocal { } } } + + fn handle_hello(stream: &mut TcpStream) { + let first_two_bytes = stream.read_exact(2).unwrap(); + + if first_two_bytes[0] != relay::SOCK5_VERSION { + fail!("Invalid sock5 version"); + } else if first_two_bytes[1] == 0 { + fail!("Invalid sock5 method number"); + } + + let methods = stream.read_exact(first_two_bytes[1] as uint); + + for m in methods.iter() { + // Choose + } + + let chosen_method = 1u8; + + let buf = [relay::SOCK5_VERSION, chosen_method]; + stream.write(buf); + } } -impl Relay for TcpRelayLocal { +impl Relay for TcpRelayServer { fn run(&mut self) { self.accept_loop() } } - -pub struct TcpRelayServer; - -impl TcpRelayServer { - pub fn new() -> TcpRelayServer { - TcpRelayServer - } - -}