Socks5协议

具体协议过程参考理解socks5协议的工作过程和协议细节。这篇文章主要是通过代码去理解socks5协议。

实验条件介绍

命令行设置proxy

export https_proxy=socks5://127.0.0.1:7891 http_proxy=socks5://127.0.0.1:7891

之后通过curl发起http请求。

curl www.baidu.com

这时候,curl这个命令就会走socks协议,并封装socks协议数据发送给socks5://127.0.0.1:7891

协商阶段

根据参考资料,握手阶段-协商阶段发送的数据格式如下:

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

#上方的数字表示字节数,下面的表格同理,不再赘述

VER: 协议版本,socks5为0x05

NMETHODS: 支持认证的方法数量

METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:

  • X’00’ NO AUTHENTICATION REQUIRED
  • X’01’ GSSAPI
  • X’02’ USERNAME/PASSWORD
  • X’03’ to X’7F’ IANA ASSIGNED
  • X’80’ to X’FE’ RESERVED FOR PRIVATE METHODS
  • X’FF’ NO ACCEPTABLE METHODS

那么socks服务端接受到请求时,需要解析内容,选中一个METHOD返回给客户端,格式如下:

+----+--------+
|VER | METHOD |
+----+--------+
| 1  |   1    |
+----+--------+
func ServerHandshake(rw net.Conn, authenticator auth.Authenticator) (addr Addr, command Command, user string, err error) {
	...

	if _, err = io.ReadFull(rw, buf[:2]); err != nil {
		return
	}

	nmethods := buf[1]
	if _, err = io.ReadFull(rw, buf[:nmethods]); err != nil {
		return
	}

	// 返回信息,需要认证
	if authenticator != nil {
		// 版本和认证方法
		if _, err = rw.Write([]byte{5, 2}); err != nil {
			return
		}
	}

  ...
}

上面代码逻辑就是处理这个过程。代码中固定返回的是X’02’ USERNAME/PASSWORD这种认证方式。

认证阶段

认证阶段是一个可选的阶段,如果不需要认证,可以直接跳过。

如果需要认证,客户端向socks5服务器发起一个认证请求,这里以0x02的认证方式举例:

|VER | ULEN |  UNAME   | PLEN |  PASSWD  |
+----+------+----------+------+----------+
| 1  |  1   | 1 to 255 |  1   | 1 to 255 |
+----+------+----------+------+----------+

VER: 版本,**通常为`0x01`**

ULEN: 用户名长度

UNAME: 对应用户名的字节数据

PLEN: 密码长度

PASSWD: 密码对应的数据

socks5服务器收到客户端的认证请求后,解析内容,验证信息是否合法,然后给客户端响应结果。响应格式如下:

+----+--------+
|VER | STATUS |
+----+--------+
| 1  |   1    |
+----+--------+

注意这里的VER并不少socks的版本


func ServerHandshake(rw net.Conn, authenticator auth.Authenticator) (addr Addr, command Command, user string, err error) {
  ...

	// 返回信息,需要认证
	if authenticator != nil {
		// 版本和认证方法
		if _, err = rw.Write([]byte{5, 2}); err != nil {
			return
		}

		// 认证阶段
		header := make([]byte, 2)
		if _, err = io.ReadFull(rw, header); err != nil {
			return
		}

		authBuf := make([]byte, MaxAuthLen)
		// 读取username
		userLen := int(header[1])
		if userLen <= 0 {
			// 返回认证失败
			rw.Write([]byte{1, 1})
			err = ErrAuth
			return
		}
		if _, err = io.ReadFull(rw, authBuf[:userLen]); err != nil {
			return
		}
		user = string(authBuf[:userLen])

		// 读取pass
		if _, err = rw.Read(header[:1]); err != nil {
			return
		}
		passLen := int(header[0])
		if passLen <= 0 {
			// 返回认证失败
			rw.Write([]byte{1, 1})
			err = ErrAuth
			return
		}
		if _, err = io.ReadFull(rw, authBuf[:userLen]); err != nil {
			return
		}
		pass := string(authBuf[:userLen])

		if ok := authenticator.Verify(user, pass); !ok {
			// 返回认证失败
			rw.Write([]byte{1, 1})
			err = ErrAuth
			return
		}

		// 返回认成功
		if _, err = rw.Write([]byte{1, 0}); err != nil {
			return
		}
	} else {
		// 告诉客户端不需要验证
		if _, err = rw.Write([]byte{5, 0}); err != nil {
			return
		}
	}

	return
}

...

请求阶段

顺利通过协商阶段后,客户端向socks5服务器发起请求细节,格式如下:

+----+-----+-------+------+----------+----------+
|VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
+----+-----+-------+------+----------+----------+
| 1  |  1  | X'00' |  1   | Variable |    2     |
+----+-----+-------+------+----------+----------+
  • VER 版本号,socks5的值为0x05
  • CMD
    • 0x01表示CONNECT请求
    • 0x02表示BIND请求
    • 0x03表示UDP转发
  • RSV 保留字段,值为0x00
  • ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
    • 0x01表示IPv4地址,DST.ADDR为4个字节
    • 0x03表示域名,DST.ADDR是一个可变长度的域名
    • 0x04表示IPv6地址,DST.ADDR为16个字节长度
  • DST.ADDR 一个可变长度的值
  • DST.PORT 目标端口,固定2个字节

上面的值中,DST.ADDR是一个变长的数据,它的数据长度根据ATYP的类型决定。分为下面3种情况:

  • X’01’
    • 一个4字节的ipv4地址
  • X’03’
    • 一个可变长度的域名,这种情况下DST.ADDR的第一个字节表示域名长度,剩下部分是域名内容。
  • X’04’
    • 一个16字节的ipv6地址

socks5服务器收到客户端的请求后,需要返回一个响应,结构如下

+----+-----+-------+------+----------+----------+
|VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
+----+-----+-------+------+----------+----------+
| 1  |  1  | X'00' |  1   | Variable |    2     |
+----+-----+-------+------+----------+----------+
  • VER socks版本,这里为0x05
  • REP Relay field,内容取值如下
  • X’00’ succeeded
    • X’01’ general SOCKS server failure
    • X’02’ connection not allowed by ruleset
    • X’03’ Network unreachable
    • X’04’ Host unreachable
    • X’05’ Connection refused
    • X’06’ TTL expired
    • X’07’ Command not supported
    • X’08’ Address type not supported
    • X’09’ to X’FF’ unassigned
  • RSV 保留字段
  • ATYPE 同请求的ATYPE
  • BND.ADDR 服务绑定的地址
  • BND.PORT 服务绑定的端口DST.PORT
// 请求阶段
if _, err = io.ReadFull(rw, buf[:3]); err != nil {
  return
}
command = buf[1]
addr, err = ReadAddr(rw, buf)
if err != nil {
  return
}

switch command {
	case CmdConnect, CmdUDPAssociate:
		localAddr := ParseAddr(rw.LocalAddr().String())
		if localAddr == nil {
			err = ErrAddressNotSupported
		} else {
			// 返回响应
			_, err = rw.Write(bytes.Join([][]byte{{5, 0, 0}, localAddr}, []byte{}))
		}
  ...
}
func ReadAddr(r io.Reader, b []byte) (Addr, error) {
	_, err := io.ReadFull(r, b[:1])
	if err != nil {
		return nil, err
	}

	switch b[0] {
	case AtypDomainName:
		_, err = io.ReadFull(r, b[1:2])
		if err != nil {
			return nil, err
		}
		domainLength := uint16(b[1])
		_, err = io.ReadFull(r, b[2:2+domainLength+2])
		return b[:1+1+domainLength+2], err
	case AtypIPv4:
		_, err = io.ReadFull(r, b[1:1+net.IPv4len+2])
		return b[:1+net.IPv4len+2], err
	case AtypIPv6:
		_, err = io.ReadFull(r, b[1:1+net.IPv6len+2])
		return b[:1+net.IPv6len+2], err
	}

	return nil, ErrAddressNotSupported
}

Relay阶段

socks5服务器收到请求后,解析内容。如果是UDP请求,服务器直接转发; 如果是TCP请求,服务器向目标服务器建立TCP连接,后续负责把客户端的所有数据转发到目标服务。

Trojan协议

连接Trojan服务端需要TLS握手,后续的流量就是TLS保护的了。

trojan客户端与trojan服务端之间使用自定义的协议,官方文档上已经有相应的说明:

+-----------------------+---------+----------------+---------+----------+
| hex(SHA224(password)) |  CRLF   | Trojan Request |  CRLF   | Payload  |
+-----------------------+---------+----------------+---------+----------+
|          56           | X'0D0A' |    Variable    | X'0D0A' | Variable |
+-----------------------+---------+----------------+---------+----------+

where Trojan Request is a SOCKS5-like request:

+-----+------+----------+----------+
| CMD | ATYP | DST.ADDR | DST.PORT |
+-----+------+----------+----------+
|  1  |  1   | Variable |    2     |
+-----+------+----------+----------+

where:

o  CMD
		o  CONNECT X'01'
		o  UDP ASSOCIATE X'03'
o  ATYP address type of following address
		o  IP V4 address: X'01'
		o  DOMAINNAME: X'03'
		o  IP V6 address: X'04'
o  DST.ADDR desired destination address
o  DST.PORT desired destination port in network octet order

trojan客户端发起的数据包中,头部都是上面结构所示的那样:

  1. 头部最开始是56字节的hash,这个hash是配置中设置的字符串密码(实现商量好的)计算出来的hash值。
  2. hash之后跟CRLF换行符。
  3. CRLF后是TrojanRequest的结构。
  4. TrojanRequest后又是一个CRLF换行符。
  5. CRLF之后便是真正的数据。

参考资料