用 Go 语言实现一个 Socks5 代理

Socks5

Socks 是什么 Wiki 上有很经典的介绍。简而言之,SOCKS 是一种网络传输协议,主要用于客户端与外网服务器之间通讯的中间传递。SOCKS 是 “SOCKetS” 的缩写。而 Socks5 是 SOCKS 的最新版本协议。

Socks5 工作流程

  1. 客户端与服务端建立连接
  2. 客户端请求协商「认证方式」
  3. 如果服务端存在认证,服务端将回应「认证方式」,否则回应「无需认证」
  4. 如果客户端收到认证,客户端发出「身份信息」,服务端回应「认证状态」
  5. 认证成功或者无需认证,客户端会发来「请求信息」,否则关闭连接
  6. 服务端根据「请求信息」与目标主机建立连接
  7. 服务端回应「请求状态」,成功则转发流量

实现一个 Socks5 服务端

这里我用 Go 语言实现了一个最基本的 TCP 协议 Socks5 服务端,可以直接用 Chrome 浏览器扩展 Proxy SwitchyOmega 刷网页测试。

package main

import (
	"net"
	"log"
	"io"
	"encoding/binary"
	"strconv"
)

const (
	VER_SOCKS5  = 0x05
	NORMAL      = 0x00
	INDEX_ATYP  = 3
	ATYP_IPV4   = 0x01
	ATYP_DOMAIN = 0x03
)

func main() {
	listener, err := net.Listen("tcp", ":1081")
	CheckErr(err)
	for {
		conn, err := listener.Accept()
		CheckErr(err)
		go HandleClientRequest(conn)
	}

}

func HandleClientRequest(client net.Conn) {
	if client == nil {
		return
	}

	var buf [1024]byte
	_, err := client.Read(buf[:])
	CheckErr(err)
	if buf[0] == VER_SOCKS5 {
		client.Write([]byte{VER_SOCKS5, NORMAL})
		n, err := client.Read(buf[:])
		CheckErr(err)
		var host, port string
		switch buf[INDEX_ATYP] {
		case ATYP_IPV4:
			// 截取 ipv4 地址
			host = net.IPv4(buf[INDEX_ATYP+1], buf[INDEX_ATYP+2],
				buf[INDEX_ATYP+3], buf[INDEX_ATYP+4]).String()
			log.Printf("Client requests ipv4:%s", host)
		case ATYP_DOMAIN:
			// 截取完整域名
			// 此处也可以利用 buf[4] (表示域名的长度)截取:
			// begin, end := INDEX_ATYP+2, INDEX_ATYP+2+buf[INDEX_ATYP+1]
			begin, end := INDEX_ATYP+2, n-2
			host = string(buf[begin: end])
			log.Printf("Client requests domian name:%s", host)
		}
		port = strconv.Itoa(int(binary.BigEndian.Uint16(buf[n-2:n])))
		server, err := net.Dial("tcp", net.JoinHostPort(host, port))
		if err != nil {
			log.Println(err)
			return
		}
		// 响应客户端连接成功
		client.Write([]byte{0x05, NORMAL, NORMAL, 0x01, NORMAL,
				    NORMAL, NORMAL, NORMAL, NORMAL, NORMAL})
		defer server.Close()
		// 进行转发
		go io.Copy(client, server)
		io.Copy(server, client)
	}

}

解析代码中的步骤

首先,创建一组会用到的语义化的常量:

const (
	VER_SOCKS5 = 0x05
	NORMAL = 0x00
	INDEX_ATYP  = 3
	ATYP_IPV4   = 0x01
	ATYP_DOMAIN = 0x03
)

监听服务,然后对每一个客户端连接都开启一个 coroutine 进一步回应(除非你想让连接排队响应,否则请让所有建立的连接并行)。

listener, err := net.Listen("tcp", ":1081")
for {
	conn, err := listener.Accept()
	go HandleClientRequest(conn)
}

将客户端发来的第一段数据包读取到一个 1024 字节的 buffer 中(通常协议请求不会超过这个容量)。

var buf [1024]byte
_, err := client.Read(buf[:])

这里服务端不进行认证,所以仅判断协议版本(VER,位置 1),然后直接回应无认证:

if buf[0] == VER_SOCKS5 {
      client.Write([]byte{VER_SOCKS5, NORMAL})
      // ...
}

接着读取客户端发来的请求信息,根据地址类型(ATYP,位置 4)分别做数据提取(目标地址和端口):

n, err := client.Read(buf[:])
switch buf[INDEX_ATYP] {
case ATYP_IPV4:
	// ipv4 handle
case ATYP_DOMAIN:
	// domain handle
}

ipv4 是在 ATYP 后面占用了四个字节,可以通过下标分别访问然后用 net.IPv4 方法组合起来再 toString:

host = net.IPv4(buf[INDEX_ATYP+1], buf[INDEX_ATYP+2], buf[INDEX_ATYP+3], buf[INDEX_ATYP+4]).String()

域名数据是 ATYP 隔一个位置再往后算起始 倒数第三字节结尾,占用长度不固定。所以可以这样:

begin, end := INDEX_ATYP+2, n-2
host = string(buf[begin: end])

而域名和 ATYP 之间相隔的一个字节储存的是域名的长度,所以在不知道有效数据长度(上面的 n)的情况下,也可以根据域名长度来截取完整域名:

begin, end := INDEX_ATYP+2, INDEX_ATYP+2+buf[INDEX_ATYP+1]

得到了地址我们要获取端口,因为端口是以大端字节序(目的是跨平台以及硬件?)的形式储存在最后两个字节中的。所以用内置库直接转换就可以得到真实端口了:

port = strconv.Itoa(int(binary.BigEndian.Uint16(buf[n-2:n])))

但是我在开源代码中发现了一种左移 + 位运算直接获取的方法(其实跟 binary.BigEndian.Uint16 实现思路一样):

int(buf[n-2])<<8 | int(buf[n-1])

有了端口我们需要和目标主机建立连接,并回应请求状态(除了位置 1 的 VER 和位置 4 的 ATYP,其余的都填充 0x00,因为返回的 ATYP 是 ipv4 类型所以字节数是 10):

server, err := net.Dial("tcp", net.JoinHostPort(host, port))
client.Write([]byte{0x05, NORMAL, NORMAL, 0x01, NORMAL, NORMAL, NORMAL, NORMAL, NORMAL, NORMAL})

最后一步,转发流量:

defer server.Close()
go io.Copy(server, client)
io.Copy(client, server)

io.Copy 方法是将来源 Reader 读取到的数据往目标 Writer 里边写,由于 Client 和 Server 建立连接的读写是双向的,所以需要让一个以 coroutine 并行运行,不然任何一方阻塞都是不行的。

这样就实现了一个最基本的仅支持 TCP 的无认证 Socks5 协议的服务端代理程序。

其他

  • 由于博客 Markdown 编辑器的原因,并没有用表格解释每一个 Socks5 中协议定义字段的具体含义。但是如果想用表格一览每一个协议字段的话,看 Wiki 就够了,我也没必要照搬。
  • ATYP 还有一种类型是 ipv6,跟 ipv4 截取方式一样。不过 ipv6 占用 16 个字节长度。
  • 也许过不久我会发一篇 Shadowsocks 的基本实现
  • Socks5 的官方协议说明(RFC 标准)可以看 rfc1928