diff --git a/src/bin/ssurl.rs b/src/bin/ssurl.rs index d79114ac..dfb7b68e 100644 --- a/src/bin/ssurl.rs +++ b/src/bin/ssurl.rs @@ -13,6 +13,7 @@ use clap::{App, Arg}; use qrcode::QrCode; +use shadowsocks::VERSION; use shadowsocks::config::{Config, ConfigType, ServerConfig}; const BLACK: &'static str = "\x1b[40m \x1b[0m"; @@ -103,6 +104,7 @@ fn main() { let app = App::new("ssurl") .author("Y. T. Chung ") .about("Encode and decode ShadowSocks URL") + .version(VERSION) .arg(Arg::with_name("ENCODE") .short("e") .long("encode") diff --git a/src/config.rs b/src/config.rs index bca5749a..2776365b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -133,6 +133,7 @@ impl ServerAddr { } } +/// Parse ServerAddr error #[derive(Debug)] pub struct ServerAddrError; @@ -169,13 +170,18 @@ impl Display for ServerAddr { /// Configuration for a server #[derive(Clone, Debug)] pub struct ServerConfig { + /// Server address pub addr: ServerAddr, + /// Encryption password (key) pub password: String, + /// Encryption type (method) pub method: CipherType, + /// Connection timeout pub timeout: Option, } impl ServerConfig { + /// Create a basic config pub fn basic(addr: SocketAddr, password: String, method: CipherType) -> ServerConfig { ServerConfig { addr: ServerAddr::SocketAddr(addr), @@ -206,9 +212,14 @@ impl json::ToJson for ServerConfig { /// Listening address pub type ClientConfig = SocketAddr; +/// Server config type #[derive(Clone, Copy)] pub enum ConfigType { + /// Config for local + /// + /// Requires `local` configuration Local, + /// Config for server Server, } @@ -230,6 +241,7 @@ impl Default for Config { } } +/// Configuration parsing error kind #[derive(Copy, Clone)] pub enum ErrorKind { MissingField, diff --git a/src/lib.rs b/src/lib.rs index d2d6b36a..de7924af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,6 +44,7 @@ extern crate tokio_core; extern crate libc; +/// ShadowSocks version pub const VERSION: &'static str = env!("CARGO_PKG_VERSION"); pub mod config; diff --git a/src/relay/local.rs b/src/relay/local.rs index ea0ff353..25d73ee8 100644 --- a/src/relay/local.rs +++ b/src/relay/local.rs @@ -37,8 +37,6 @@ use config::Config; /// Relay server running under local environment. /// /// ```no_run -/// use std::net::SocketAddr; -/// /// use shadowsocks::relay::RelayLocal; /// use shadowsocks::config::{Config, ServerConfig, ServerAddr}; /// use shadowsocks::crypto::CipherType; @@ -51,7 +49,7 @@ use config::Config; /// method: CipherType::Aes256Cfb, /// timeout: None, /// }]; -/// RelayLocal::run(config); +/// RelayLocal::run(config).unwrap(); /// ``` #[derive(Clone)] pub struct RelayLocal; diff --git a/src/relay/server.rs b/src/relay/server.rs index 4571c39e..af3ebbee 100644 --- a/src/relay/server.rs +++ b/src/relay/server.rs @@ -34,8 +34,6 @@ use config::Config; /// Relay server running on server side. /// /// ```no_run -/// use std::net::SocketAddr; -/// /// use shadowsocks::relay::RelayServer; /// use shadowsocks::config::{Config, ServerConfig, ServerAddr}; /// use shadowsocks::crypto::CipherType; @@ -47,7 +45,7 @@ use config::Config; /// method: CipherType::Aes256Cfb, /// timeout: None, /// }]; -/// RelayServer::run(config); +/// RelayServer::run(config).unwrap(); /// ``` /// #[derive(Clone)] diff --git a/src/relay/socks5.rs b/src/relay/socks5.rs index 5d98218e..cf24fb78 100644 --- a/src/relay/socks5.rs +++ b/src/relay/socks5.rs @@ -19,6 +19,10 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +//! Socks5 protocol definition (RFC1928) +//! +//! Implements [SOCKS Protocol Version 5](https://www.ietf.org/rfc/rfc1928.txt) proxy protocol + #![allow(dead_code)] use std::fmt::{self, Debug, Formatter}; @@ -61,11 +65,14 @@ const SOCKS5_REPLY_TTL_EXPIRED: u8 = 0x06; const SOCKS5_REPLY_COMMAND_NOT_SUPPORTED: u8 = 0x07; const SOCKS5_REPLY_ADDRESS_TYPE_NOT_SUPPORTED: u8 = 0x08; -#[allow(dead_code)] +/// SOCKS5 command #[derive(Clone, Debug, Copy)] pub enum Command { + /// CONNECT command (TCP tunnel) TcpConnect, + /// BIND command (Not supported in ShadowSocks) TcpBind, + /// UDP ASSOCIATE command UdpAssociate, } @@ -90,6 +97,7 @@ impl Command { } } +/// SOCKS5 reply code #[derive(Clone, Debug, Copy)] pub enum Reply { Succeeded, @@ -139,9 +147,29 @@ impl Reply { } } +impl fmt::Display for Reply { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Reply::Succeeded => write!(f, "Succeeded"), + Reply::AddressTypeNotSupported => write!(f, "Address type not supported"), + Reply::CommandNotSupported => write!(f, "Command not supported"), + Reply::ConnectionNotAllowed => write!(f, "Connection not allowed"), + Reply::ConnectionRefused => write!(f, "Connection refused"), + Reply::GeneralFailure => write!(f, "General failure"), + Reply::HostUnreachable => write!(f, "Host unreachable"), + Reply::NetworkUnreachable => write!(f, "Network unreachable"), + Reply::OtherReply(u) => write!(f, "Other reply ({})", u), + Reply::TtlExpired => write!(f, "TTL expired"), + } + } +} + +/// SOCKS5 protocol error #[derive(Clone)] pub struct Error { + /// Reply code pub reply: Reply, + /// Error message pub message: String, } @@ -191,9 +219,12 @@ impl From for io::Error { } } +/// SOCKS5 address type #[derive(Clone, PartialEq, Eq, Hash)] pub enum Address { + /// Socket address (IP Address) SocketAddress(SocketAddr), + /// Domain name address DomainNameAddress(String, u16), } @@ -250,13 +281,23 @@ impl From for Address { } } +impl From<(String, u16)> for Address { + fn from((dn, port): (String, u16)) -> Address { + Address::DomainNameAddress(dn, port) + } +} + +/// TCP request header after handshake #[derive(Clone, Debug)] pub struct TcpRequestHeader { + /// SOCKS5 command pub command: Command, + /// Remote address pub address: Address, } impl TcpRequestHeader { + /// Creates a request header pub fn new(cmd: Command, addr: Address) -> TcpRequestHeader { TcpRequestHeader { command: cmd, @@ -264,6 +305,7 @@ impl TcpRequestHeader { } } + /// Read from a reader pub fn read_from(r: R) -> Box> { let fut = read_exact(r, [0u8; 3]) .map_err(From::from) @@ -295,6 +337,7 @@ impl TcpRequestHeader { Box::new(fut) } + /// Write data into a writer pub fn write_to(&self, w: W) -> BoxIoFuture { let addr = self.address.clone(); @@ -304,19 +347,24 @@ impl TcpRequestHeader { Box::new(fut) } + /// Length in bytes #[inline] pub fn len(&self) -> usize { self.address.len() + 3 } } +/// TCP response header #[derive(Clone, Debug)] pub struct TcpResponseHeader { + /// SOCKS5 reply pub reply: Reply, + /// Reply address pub address: Address, } impl TcpResponseHeader { + /// Creates a response header pub fn new(reply: Reply, address: Address) -> TcpResponseHeader { TcpResponseHeader { reply: reply, @@ -324,6 +372,7 @@ impl TcpResponseHeader { } } + /// Read from a reader pub fn read_from(r: R) -> Box> { let fut = read_exact(r, [0u8; 3]) .map_err(From::from) @@ -351,6 +400,7 @@ impl TcpResponseHeader { Box::new(fut) } + /// Write to a writer pub fn write_to(&self, w: W) -> BoxIoFuture { let addr = self.address.clone(); let fut = write_all(w, [SOCKS5_VERSION, self.reply.as_u8(), 0x00]) @@ -360,6 +410,7 @@ impl TcpResponseHeader { Box::new(fut) } + /// Length in bytes #[inline] pub fn len(&self) -> usize { self.address.len() + 3 @@ -507,21 +558,25 @@ fn get_addr_len(atyp: &Address) -> usize { } } -// +----+----------+----------+ -// |VER | NMETHODS | METHODS | -// +----+----------+----------+ -// | 5 | 1 | 1 to 255 | -// +----+----------+----------| +/// SOCKS5 handshake request packet +/// +/// +----+----------+----------+ +/// |VER | NMETHODS | METHODS | +/// +----+----------+----------+ +/// | 5 | 1 | 1 to 255 | +/// +----+----------+----------| #[derive(Clone, Debug)] pub struct HandshakeRequest { pub methods: Vec, } impl HandshakeRequest { + /// Creates a handshake request pub fn new(methods: Vec) -> HandshakeRequest { HandshakeRequest { methods: methods } } + /// Read from a reader pub fn read_from(r: R) -> BoxIoFuture<(R, HandshakeRequest)> { let fut = read_exact(r, [0u8, 0u8]) .and_then(|(r, buf)| { @@ -541,6 +596,7 @@ impl HandshakeRequest { Box::new(fut) } + /// Write to a writer pub fn write_to(self, w: W) -> BoxIoFuture { let fut = write_all(w, [SOCKS5_VERSION, self.methods.len() as u8]) .and_then(move |(w, _)| write_all(w, self.methods)) @@ -550,21 +606,25 @@ impl HandshakeRequest { } } -// +----+--------+ -// |VER | METHOD | -// +----+--------+ -// | 1 | 1 | -// +----+--------+ +/// SOCKS5 handshake response packet +/// +/// +----+--------+ +/// |VER | METHOD | +/// +----+--------+ +/// | 1 | 1 | +/// +----+--------+ #[derive(Clone, Debug, Copy)] pub struct HandshakeResponse { pub chosen_method: u8, } impl HandshakeResponse { + /// Creates a handshake response pub fn new(cm: u8) -> HandshakeResponse { HandshakeResponse { chosen_method: cm } } + /// Read from a reader pub fn read_from(r: R) -> BoxIoFuture<(R, HandshakeResponse)> { let fut = read_exact(r, [0u8, 0u8]).and_then(|(r, buf)| { let ver = buf[0]; @@ -579,18 +639,25 @@ impl HandshakeResponse { Box::new(fut) } + /// Write to a writer pub fn write_to(self, w: W) -> BoxIoFuture { Box::new(write_all(w, [SOCKS5_VERSION, self.chosen_method]).map(|(w, _)| w)) } } +/// UDP ASSOCIATE request header #[derive(Clone, Debug)] pub struct UdpAssociateHeader { + /// Fragment + /// + /// ShadowSocks does not support fragment, so this frag must be 0x00 pub frag: u8, + /// Remote address pub address: Address, } impl UdpAssociateHeader { + /// Creates a header pub fn new(frag: u8, address: Address) -> UdpAssociateHeader { UdpAssociateHeader { frag: frag, @@ -598,6 +665,7 @@ impl UdpAssociateHeader { } } + /// Read from a reader pub fn read_from(r: R) -> Box> { let fut = read_exact(r, [0u8; 3]) .map_err(From::from) @@ -611,6 +679,7 @@ impl UdpAssociateHeader { Box::new(fut) } + /// Write to a writer pub fn write_to(&self, w: W) -> BoxIoFuture { let addr = self.address.clone(); let fut = write_all(w, [0x00, 0x00, self.frag]) @@ -619,6 +688,8 @@ impl UdpAssociateHeader { Box::new(fut) } + /// Length in bytes + #[inline] pub fn len(&self) -> usize { 3 + self.address.len() } diff --git a/src/relay/tcprelay/client.rs b/src/relay/tcprelay/client.rs new file mode 100644 index 00000000..826309cf --- /dev/null +++ b/src/relay/tcprelay/client.rs @@ -0,0 +1,97 @@ +// The MIT License (MIT) + +// Copyright (c) 2014 Y. T. CHUNG + +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +//! TCP relay client implementation + +use std::io::{self, Read, Write}; +use std::net::SocketAddr; + +use tokio_core::reactor::Handle; +use tokio_core::net::TcpStream; + +use futures::{self, Future}; + +use relay::socks5::{self, HandshakeRequest, HandshakeResponse, Address, TcpRequestHeader, TcpResponseHeader, Command, + Reply}; +use relay::{BoxIoFuture, boxed_future}; + +/// Socks5 proxy client +pub struct Socks5Client { + stream: TcpStream, +} + +impl Socks5Client { + pub fn connect(addr: A, proxy: SocketAddr, handle: Handle) -> BoxIoFuture + where Address: From, + A: 'static + { + let fut = futures::lazy(move || TcpStream::connect(&proxy, &handle)) + .and_then(move |s| { + // 1. Handshake + let hs = HandshakeRequest::new(vec![socks5::SOCKS5_AUTH_METHOD_NONE]); + trace!("Client connected, going to send handshake: {:?}", hs); + + hs.write_to(s) + .and_then(|s| HandshakeResponse::read_from(s)) + .and_then(|(s, rsp)| { + trace!("Got handshake response: {:?}", rsp); + assert_eq!(rsp.chosen_method, socks5::SOCKS5_AUTH_METHOD_NONE); + Ok(s) + }) + }) + .and_then(move |s| { + // 2. Send request header + let h = TcpRequestHeader::new(Command::TcpConnect, From::from(addr)); + trace!("Going to connect, req: {:?}", h); + h.write_to(s) + .and_then(|s| TcpResponseHeader::read_from(s).map_err(From::from)) + .and_then(|(s, rsp)| { + trace!("Got response: {:?}", rsp); + match rsp.reply { + Reply::Succeeded => Ok(s), + r => { + let err = io::Error::new(io::ErrorKind::Other, format!("{}", r)); + Err(err) + } + } + }) + }) + .map(|s| Socks5Client { stream: s }); + + boxed_future(fut) + } +} + +impl Read for Socks5Client { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.stream.read(buf) + } +} + +impl Write for Socks5Client { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.stream.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.stream.flush() + } +} \ No newline at end of file diff --git a/src/relay/tcprelay/mod.rs b/src/relay/tcprelay/mod.rs index 07141da1..96a6443e 100644 --- a/src/relay/tcprelay/mod.rs +++ b/src/relay/tcprelay/mod.rs @@ -50,17 +50,25 @@ pub mod local; pub mod server; mod stream; mod http; +pub mod client; +/// Directions in the tunnel #[derive(Debug, Copy, Clone)] pub enum TunnelDirection { + /// Client -> Server Client2Server, + /// Client <- Server Server2Client, } +/// ReadHalf of TcpStream with decryption pub type DecryptedHalf = DecryptedReader>; +/// WriteHalf of TcpStream with encryption pub type EncryptedHalf = EncryptedWriter>; +/// Boxed future of DecryptedHalf pub type DecryptedHalfFut = BoxIoFuture; +/// Boxed future of EncryptedHalf pub type EncryptedHalfFut = BoxIoFuture; fn connect_proxy_server(handle: &Handle, @@ -302,7 +310,18 @@ pub fn tunnel(addr: Address, c2s: CF, s2c: SF) -> BoxIoFuture<()> } }); - Box::new(c2s.join(s2c).map(|_| ())) + let fut = c2s.select(s2c) + .and_then(|(dir, _)| { + match dir { + TunnelDirection::Server2Client => trace!("client <- server is closed, abort connection"), + TunnelDirection::Client2Server => trace!("server -> client is closed, abort connection"), + } + + Ok(()) + }) + .map_err(|(err, _)| err); + + boxed_future(fut) } /// Read until EOF, and ignore diff --git a/src/relay/udprelay/mod.rs b/src/relay/udprelay/mod.rs index 56158a5d..3a08fee1 100644 --- a/src/relay/udprelay/mod.rs +++ b/src/relay/udprelay/mod.rs @@ -19,6 +19,8 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +//! UdpRelay implementation + pub mod local; pub mod server; diff --git a/tests/test.rs b/tests/test.rs new file mode 100644 index 00000000..9bb08637 --- /dev/null +++ b/tests/test.rs @@ -0,0 +1,93 @@ +extern crate shadowsocks; +extern crate tokio_core; +extern crate futures; +#[macro_use] +extern crate log; +extern crate env_logger; + +use std::thread; +use std::net::SocketAddr; +use std::sync::{Arc, Barrier}; +use std::time::Duration; + +use tokio_core::reactor::Core; +use tokio_core::io::{read_to_end, write_all, flush}; +use futures::Future; + +use shadowsocks::relay::tcprelay::client::Socks5Client; +use shadowsocks::config::{Config, ServerConfig}; +use shadowsocks::crypto::CipherType; +use shadowsocks::relay::{RelayLocal, RelayServer}; +use shadowsocks::relay::socks5::Address; + +const SERVER_ADDR: &'static str = "127.0.0.1:8096"; +const LOCAL_ADDR: &'static str = "127.0.0.1:8008"; + +const PASSWORD: &'static str = "test-password"; +const METHOD: CipherType = CipherType::Aes128Cfb; + +fn get_config() -> Config { + let mut cfg = Config::new(); + cfg.local = Some(LOCAL_ADDR.parse().unwrap()); + cfg.server = vec![ServerConfig { + addr: SERVER_ADDR.parse().unwrap(), + password: PASSWORD.to_owned(), + method: METHOD, + timeout: None, + }]; + cfg +} + +fn get_client_addr() -> SocketAddr { + LOCAL_ADDR.parse().unwrap() +} + +fn start_server(bar: Arc) { + thread::spawn(move || { + drop(env_logger::init()); + bar.wait(); + RelayServer::run(get_config()).unwrap(); + }); +} + +fn start_local(bar: Arc) { + thread::spawn(move || { + drop(env_logger::init()); + bar.wait(); + RelayLocal::run(get_config()).unwrap(); + }); +} + +#[test] +fn socks5_relay() { + drop(env_logger::init()); + + let bar = Arc::new(Barrier::new(3)); + + start_server(bar.clone()); + start_local(bar.clone()); + + bar.wait(); + + // Wait until all server starts + thread::sleep(Duration::from_secs(1)); + + let mut lp = Core::new().unwrap(); + let handle = lp.handle(); + + let c = Socks5Client::connect(Address::DomainNameAddress("www.example.com".to_owned(), 80), + get_client_addr(), + handle); + let fut = c.and_then(|c| { + let req = b"GET / HTTP/1.0\r\nHost: www.example.com\r\nAccept: */*\r\n\r\n"; + write_all(c, req.to_vec()) + .and_then(|(c, _)| flush(c)) + .and_then(|c| read_to_end(c, Vec::new())) + .map(|(_, buf)| { + println!("Got reply from server: {}", + unsafe { String::from_utf8_unchecked(buf) }); + }) + }); + + lp.run(fut).unwrap(); +} \ No newline at end of file