Add Socks5Client wrap, fulfill more documents, add tcp connect test

This commit is contained in:
Y. T. Chung
2016-10-30 16:44:05 +08:00
parent 510c0068a7
commit 6b1344ca91
10 changed files with 311 additions and 18 deletions

View File

@@ -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 <zonyitoo@gmail.com>")
.about("Encode and decode ShadowSocks URL")
.version(VERSION)
.arg(Arg::with_name("ENCODE")
.short("e")
.long("encode")

View File

@@ -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<Duration>,
}
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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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)]

View File

@@ -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<Error> 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<SocketAddr> 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: Read + 'static>(r: R) -> Box<Future<Item = (R, TcpRequestHeader), Error = Error>> {
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<W: Write + 'static>(&self, w: W) -> BoxIoFuture<W> {
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: Read + 'static>(r: R) -> Box<Future<Item = (R, TcpResponseHeader), Error = Error>> {
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<W: Write + 'static>(&self, w: W) -> BoxIoFuture<W> {
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<u8>,
}
impl HandshakeRequest {
/// Creates a handshake request
pub fn new(methods: Vec<u8>) -> HandshakeRequest {
HandshakeRequest { methods: methods }
}
/// Read from a reader
pub fn read_from<R: Read + 'static>(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<W: Write + 'static>(self, w: W) -> BoxIoFuture<W> {
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: Read + 'static>(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<W: Write + 'static>(self, w: W) -> BoxIoFuture<W> {
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: Read + 'static>(r: R) -> Box<Future<Item = (R, UdpAssociateHeader), Error = Error>> {
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<W: Write + 'static>(&self, w: W) -> BoxIoFuture<W> {
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()
}

View File

@@ -0,0 +1,97 @@
// The MIT License (MIT)
// Copyright (c) 2014 Y. T. CHUNG <zonyitoo@gmail.com>
// 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<A>(addr: A, proxy: SocketAddr, handle: Handle) -> BoxIoFuture<Socks5Client>
where Address: From<A>,
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<usize> {
self.stream.read(buf)
}
}
impl Write for Socks5Client {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.stream.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.stream.flush()
}
}

View File

@@ -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<ReadHalf<TcpStream>>;
/// WriteHalf of TcpStream with encryption
pub type EncryptedHalf = EncryptedWriter<WriteHalf<TcpStream>>;
/// Boxed future of DecryptedHalf
pub type DecryptedHalfFut = BoxIoFuture<DecryptedHalf>;
/// Boxed future of EncryptedHalf
pub type EncryptedHalfFut = BoxIoFuture<EncryptedHalf>;
fn connect_proxy_server(handle: &Handle,
@@ -302,7 +310,18 @@ pub fn tunnel<CF, SF>(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

View File

@@ -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;

93
tests/test.rs Normal file
View File

@@ -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<Barrier>) {
thread::spawn(move || {
drop(env_logger::init());
bar.wait();
RelayServer::run(get_config()).unwrap();
});
}
fn start_local(bar: Arc<Barrier>) {
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();
}