实现一个ss客户端

一、协议

Shadowsocks is a secure split proxy loosely based on SOCKS5

ss是建立在socks5协议上的一种代理协议,它的职责是建立两个通道,一个用于把客户端数据传输到服务端,一个用于把服务端的数据传输到客户端

ss协议可以分为socks5部分和shadowsocks部分,其数据传输流向可以表示为

1
client <---> ss-local <--[encrypted]--> ss-remote <---> target

ss-local扮演了一个传统socks5代理的角色,为客户端请求提供代理,它将会加密并转发来自客户端的包到ss-remotess-remote收到数据后会进行解密并向client请求的目标地址发起真正的请求,来自目标的回复也同样将被加密,并由ss-remote转发回ss-local,后者解密并最终返回给原始客户端

  1. socks5协议

    SOCKS is an Internet protocol that exchanges network packets between a client and server through a proxy server

    socks5协议包括握手阶段和请求阶段

    在握手阶段,客户端将往ss-local发送如下格式的报文

    +----+----------+----------+
    |VER | NMETHODS | METHODS  |
    +----+----------+----------+
    | 1  |    1     | 1 to 255 |
    +----+----------+----------+
    

    ss-local收到了客户端的请求之后,响应报文的格式为

     +----+--------+
     |VER | METHOD |
     +----+--------+
     | 1  |   1    |
     +----+--------+
    

    代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    fn handshake(stream: TcpStream) -> Result<(), Error> {
    let mut buf_reader = BufReader::new(stream);
    let ver = buf_reader.read_u8()?;
    if ver != SOCKS5_VER {
    return Err(Error::new(ErrorKind::Other, "not supported ver"));
    }
    let methods = buf_reader.read_u8()?;
    let mut buf = vec![0; methods as usize];
    buf_reader.read_exact(&mut buf[..])?;

    // 简单起见,这里METHOD字段的值为0x00,表示不需要验证
    buf_reader.get_mut().write([SOCKS5_VER, 0].as_ref())?;
    Ok(())
    }

    socks5客户端和服务端进行握手&授权验证通过之后,就可以开始建立连接。连接由客户端发起,告诉socks5服务端客户端需要访问的目标地址,其中包含远程服务器的地址和端口,地址可以是ipv4/ipv6/domain

    1
    2
    3
    4
    5
    +----+-----+-------+------+----------+----------+
    |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
    +----+-----+-------+------+----------+----------+
    | 1 | 1 | X'00' | 1 | Variable | 2 |
    +----+-----+-------+------+----------+----------+

    socks5服务端返回的报文格式如下

    1
    2
    3
    4
    5
    +----+-----+-------+------+----------+----------+
    |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
    +----+-----+-------+------+----------+----------+
    | 1 | 1 | X'00' | 1 | Variable | 2 |
    +----+-----+-------+------+----------+----------+

    代码实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    fn connect(mut stream: TcpStream, cfg: Cfg) -> Result<(), Error> {
    // 读取socks5客户端请求
    let mut reader = BufReader::new(&stream);
    let mut buf = [0; 4];
    reader.read_exact(&mut buf)?;
    let (ver, cmd, atype) = (buf[0], buf[1], buf[3]);
    if ver != SOCKS5_VER {
    return Err(Error::new(ErrorKind::Other, "not supported ver"));
    }
    if cmd != CMD_BIND {
    return Err(Error::new(ErrorKind::Other, "not supported cmd"));
    }

    let mut raw_addr = vec![atype];
    let addr = match atype {
    ATYP_IPV4 => {
    reader.read_exact(&mut buf)?;
    let ipv4 = IpAddr::V4(Ipv4Addr::new(buf[0], buf[1], buf[2], buf[3]));
    raw_addr.append(&mut buf[..].to_owned());
    let mut buf = [0; 2];
    reader.read_exact(&mut buf)?;
    raw_addr.append(&mut buf[..].to_owned());
    let port = Cursor::new(&buf).read_u16::<BigEndian>()?;
    Ok(SocketAddr::new(ipv4, port))
    }
    ATYPE_IPV6 => {
    let mut buf = [0; 18];
    reader.read_exact(&mut buf)?;
    raw_addr.append(&mut buf[..].to_owned());
    let ipv6 = IpAddr::V6(Ipv6Addr::new(
    Cursor::new(&buf[0..2]).read_u16::<BigEndian>()?,
    Cursor::new(&buf[2..4]).read_u16::<BigEndian>()?,
    Cursor::new(&buf[4..6]).read_u16::<BigEndian>()?,
    Cursor::new(&buf[6..8]).read_u16::<BigEndian>()?,
    Cursor::new(&buf[8..10]).read_u16::<BigEndian>()?,
    Cursor::new(&buf[10..12]).read_u16::<BigEndian>()?,
    Cursor::new(&buf[12..14]).read_u16::<BigEndian>()?,
    Cursor::new(&buf[14..16]).read_u16::<BigEndian>()?,
    ));
    let port = Cursor::new(&buf[16..]).read_u16::<BigEndian>()?;
    Ok(SocketAddr::new(ipv6, port))
    }
    ATYPE_HOST => {
    let host_byte = reader.read_u8()?;
    raw_addr.push(host_byte);
    let mut buf = vec![0; host_byte as usize];
    reader.read_exact(&mut buf)?;
    raw_addr.append(&mut buf[..].to_owned());
    Ok(String::from_utf8_lossy(&buf[..])
    .to_socket_addrs()?
    .next()
    .unwrap())
    }
    _ => Err(Error::new(ErrorKind::Other, "not supported atype")),
    };

    // 写入响应
    // +------+-------+---------+--------+-------------+-------------+
    // |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
    // +------+-------+---------+--------+-------------+-------------+
    // | 0x5 | 0x0 | 0x0 | 0x1 | 0x00000000 | 0x00 |
    // +------+-------+---------+--------+-------------+-------------+
    stream.write(&[SOCKS5_VER, 0, 0, 1, 0, 0, 0, 0, 0, 0])?;

    // start proxying
    }
  2. ss协议
    ss支持tcp/udp包的转发,这篇文章只讨论tcp协议下ss的报文结构
    从ss-local或ss-server发送shadowsocks tcp连接的第一个数据包必须包含随机生成的用于加密的IV。

    1
    2
    3
    4
    5
    +-------+----------+
    | IV | Payload |
    +-------+----------+
    | Fixed | Variable |
    +-------+----------+

    iv 表示一个随机生成的向量
    payload是加密传输的数据,可以是target addressuser datatarget address会被加在第一个user data的前面

    一旦收到这个数据包,就能根据ivpassword进行解密出原始数据。对于ss-server,数据随后将被转发到目标地址。对于ss-local,数据被转发到客户端。之后进入tcp stream传输阶段,在这个阶段,数据被用相同的iv加密,并直接传输,不需要再额外添加iv。tcp stream可以表示为:

    1
    [iv][target address][user data][user data][user data].....

二、RC4-MD5加密算法

本文选用rc4-md5加密算法对ss流量进行加密,需要注意的是RC4的加密算法已不再安全,如果自己使用的话最好选择AEAD加密方式

RC4-MD5本质上是RC4的扩展。它对key进行MD5运算,得到RC4的key,最终被RC4算法用于生成密钥流。用户的密码会经过下面步骤转换为RC4的密钥流

  1. k1 = bytes(password)
  2. k2 = generate_key(k1)
  3. rc4-md5-key = md5(k2,iv)

生成key的算法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
fn generate_key(data: &[u8], key_len: usize) -> Vec<u8> {
let count = (key_len as f32 / MD5_LENGTH as f32).ceil() as u32;
let mut key = Vec::from(&compute(data)[..]);
let mut start = 0;
for _ in 1..count {
start += MD5_LENGTH;
let mut d = Vec::from(&key[(start - MD5_LENGTH) as usize..start as usize]);
d.extend_from_slice(data);
let d = compute(d.as_slice());
key.extend_from_slice(&*d);
}
key
}

生成rc4-md5-key的算法如下

1
2
3
4
5
6
7
8
fn generate_rc4_key(password: &[u8], iv: &[u8]) -> Vec<u8> {
let mut hasher = Md5::new();
let password = generate_key(password, KEY_LEN);
hasher.update(&password);
hasher.update(iv);
let key = hasher.finalize();
key.to_vec()
}

为了对tcp steam进行rc4加密,需要增加两个struct:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
pub struct CryptoRead<R: Read> {
conn_r: R,
dec: Box<Rc4>,
}

impl<R: Read> CryptoRead<R> {
pub fn new(conn_r: R, dec: Box<Rc4>) -> Self {
Self { conn_r, dec }
}
}

impl<R: Read> Deref for CryptoRead<R> {
type Target = R;

fn deref(&self) -> &Self::Target {
&self.conn_r
}
}

impl<R: Read> Read for CryptoRead<R> {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
println!("read: {:?}", buf);
if !self.dec.is_init() {
println!("init dec");
let mut iv = [0; IV_LEN];
self.conn_r.read_exact(&mut iv)?;
println!("read iv: {:?}", iv);
self.dec.init(&iv[..]);
}
let n = self.conn_r.read(buf)?;
if n > 0 {
// 解密
self.dec.crypt_inplace(buf[..n].as_mut())
}
println!("read {} byte", n);
Ok(n)
}
}

pub struct CryptoWrite<W: Write> {
conn_w: W,
enc: Box<Rc4>,
}

impl<W: Write> CryptoWrite<W> {
pub fn new(conn_w: W, enc: Box<Rc4>) -> Self {
Self { conn_w, enc }
}
}

impl<W: Write> Deref for CryptoWrite<W> {
type Target = W;

fn deref(&self) -> &Self::Target {
&self.conn_w
}
}

impl<W: Write> Write for CryptoWrite<W> {
fn write(&mut self, buf: &[u8]) -> Result<usize, std::io::Error> {
println!("write: {:?}", buf);
let mut iv_o: Option<Vec<u8>> = None;
if !self.enc.is_init() {
println!("init enc");
let iv = generate_iv();
iv_o = Some(iv.clone());
self.enc.init(&iv[..]);
}
let mut data: Vec<u8> = Vec::new();
if let Some(mut iv) = iv_o {
println!("append iv {:?}", iv);
data.append(&mut iv);
}
let mut buf = buf.to_vec();
// 加密
self.enc.crypt_inplace(&mut buf[..]);
data.append(&mut buf);
println!("write data {:?}", data);
let n = self.conn_w.write(data.as_mut_slice())?;
Ok(n)
}

fn flush(&mut self) -> Result<(), std::io::Error> {
self.conn_w.flush()
}
}

在了解加解密过程之后就可以继续完成之前的代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
fn connect(mut stream: TcpStream, cfg: Cfg) -> Result<(), Error> {
// --snip--


let mut stream_c = stream.try_clone()?;
println!(
"raw addr {:?}, proxy addr: {} by {}",
raw_addr, addr?, cfg.server_addr
);

let dest = TcpStream::connect(cfg.server_addr)?;
let dest_c = dest.try_clone()?;
let mut thread_vec: Vec<thread::JoinHandle<()>> = Vec::new();

let mut conn_r = CryptoRead::new(dest, Box::new(Rc4::new(&cfg.password.as_bytes())));
let mut conn_w = CryptoWrite::new(dest_c, Box::new(Rc4::new(&cfg.password.as_bytes())));
// 发送第一个报文,格式为
// +------+----------+----------+
// | ATYP | DST.ADDR | DST.PORT |
// +------+----------+----------+
// | 1 | Variable | 2 |
// +------+----------+----------+
conn_w.write(&raw_addr)?;

// 启动两个线程进行双向流量复制
let handle = thread::spawn(move || match io::copy(&mut stream, &mut conn_w) {
Ok(u) => println!("reader: stream, writer conn. copy {}", u),
Err(e) => {
println!("reader: stream, writer conn. err {}", e);
conn_w.shutdown(Shutdown::Both).unwrap();
stream.shutdown(Shutdown::Both).unwrap();
drop(stream);
drop(conn_w);
}
});
thread_vec.push(handle);

let handle = thread::spawn(move || match io::copy(&mut conn_r, &mut stream_c) {
Ok(u) => println!("reader conn, writer stream. copy {}", u),
Err(e) => {
println!("reader conn, writer stream. err {}", e);
conn_r.shutdown(Shutdown::Both).unwrap();
stream_c.shutdown(Shutdown::Both).unwrap();
drop(stream_c);
drop(conn_r);
}
});
thread_vec.push(handle);

for handle in thread_vec {
handle.join().unwrap();
}
Ok(())
}

三、总结

本文介绍了ss协议,并用rust实现了一个简单的ss客户端,算是自己对rust学习过程的一个记录吧,项目总共耗时大约三个晚上,其中大量时间花在了和编辑器斗智斗勇上,对rust还是不够熟练。文中的完整代码可以参考ss