go实践 socks5代理
Socks5协议是一款广泛使用的代理协议。Socks5代理服务器通过将客户端发来的请求转发给真正的目标服务器,模拟了一个客户端请求操作。在这里,客户端和SOCKS5代理服务器之间也是通过TCP/IP协议进行通讯,客户端将原本要发送给真正服务器的请求先发送给SOCKS5服务器,然后SOCKS5服务器再将请求转发给真正的服务器。
socks5交互流程
socks5默认监听1080接口,如果连接成功,客户端需要与服务端协商认证方式并完成认证,之后便可以发送中继请求。SOCKS 服务端会执行请求,要么建立起合适的连接,要么拒绝请求。 首先是浏览器和 socks5 代理建立 TCP 连接,代理再和真正的服务器建立 TCP 连接。这里可以分成四个阶段,握手阶段、认证阶段、请求阶段、 relay 阶段。 第一个握手阶段,浏览器会向 socks5 代理发送请求,包的内容包括一个协议的版本号,还有支持的认证的种类,socks5 服务器会选中一个认证方式,返回给浏览器。如果返回的是 00 的话就代表不需要认证,返回其他类型的话会开始认证流程。 第三个阶段是请求阶段,认证通过之后浏览器会 socks5 服务器发起请求。主要信息包括 版本号,请求的类型,一般主要是 connection 请求,就代表代理服务器要和某个域名或者某个 IP 地址某个端口建立 TCP 连接。代理服务器收到响应之后,会真正和后端服务器建立连接,然后返回一个响应。 第四个阶段是 relay 阶段。此时浏览器会发送 正常发送请求,然后代理服务器接收到请求之后,会直接把请求转换到真正的服务器上。然后如果真正的服务器以后返回响应的话,那么也会把请求转发到浏览器这边。然后实际上 代理服务器并不关心流量的细节,可以是 HTTP流量,也可以是其它 TCP 流量。 这个就是 socks5 协议的工作原理,接下来我们就会试图去简单地实现它。
认证
客户端向代理服务器发送代理请求,其中包含了代理的版本和认证方式:
| VER | NMETHODS | METHODS |
|---|---|---|
| 1 | 1 | 1 to 255 |
注意:
- ver: 版本号。
X'04':Socks4协议。X'05':Socks5协议。 - nmethods: 认证的方法数目。
- method: 每个methods的编码,取值如下。
代理服务器从给定的方法列表中选择一个方法并返回选择报文:
| ver | method |
|---|---|
| 1 | 1 |
注意:
- 如果 METHOD(方法)字段为
X'FF', 表示方法列表中的所有方法均不可用,客户端收到此信息必须关闭连接。
目前已定义method如下:
X'00':无需认证X'01':GSSAPIX'02':用户名/密码X'03':到 X'7F' IANA 指定X'80':到 X'FE'为私有方法保留X'FF':无可接受的方法
请求
SOCKS5请求格式如下:
| ver | cmd | rsv | atyp | dst.addr | dst.port |
|---|---|---|---|---|---|
| 1 | 1 | X'00' | 1 | variable | 2 |
注意:
- ver: 协议版本:X'05'
- CMD:命令。CONNECT:连接,X'01';BIND,监听X'02';UDP ASSOCIATE UDP关联 X'03'
- RSV:保留字段
- ATYP:地址类型。X'01': 表明地址字段为一个 IPV4 地址,长度为 4 个字节。X'03' :表明地址字段为一个(合法的)域名,且第一个字节为域名长度标识,(显然)其不以 NULL 作为结束标识。X'04' :表明地址字段为一个 IPV6 地址,长度为 16 个字节
- DST.ADDR:目标地址
- DST.PORT目标端口 (网络字节序)
返回报文格式:
| ver | rep | rsv | atyp | bnd.addr | dst.port |
|---|---|---|---|---|---|
| 1 | 1 | X'00' | 1 | variable | 2 |
注意:
- VER协议版本: X'05'
- REP 回复字段(回复类型):X'00':成功。X'01':常规 SOCKS 服务故障。X'02':规则不允许的连接。X'03':网络不可达。X'04':主机无法访问。X'05':拒绝连接。X'06':连接超时。X'07':不支持的命令。X'08':不支持的地址类型。从X'09'到X'FF'未定义。
- RSV 保留字段
- ATYP 地址类型:IPV4 X'01'。域名 X'03'。IPV6 X'04'。
- BND.ADDR 服务端绑定地址
- BND.PORT 服务端绑定端口 (网络字节序) 其中,标记为保留字段( RSV )的值必须设定为 X'00' 。
通信
当连接建立后,客户端就可以和正常一样访问服务端通信了,此时通信的数据除了目的地址是发往代理程序以外,所有内容都是和普通连接一模一样。对代理程序而言,后面所有收到的来自客户端的数据都会原样转发到服务读端。
示例代码:
const (
socks5Ver = 0x05 // version
cmdBind = 0x01
atypIPV4 = 0x01 // ipv4
atypHOST = 0x03 // 域名
atypIPV6 = 0x04 // ipv6
)
func main() {
// 3. socks5 代理
server, err := net.Listen("tcp", "127.0.0.1:1080")
if err != nil {
panic(err)
}
for {
client, err := server.Accept()
if err != nil {
log.Printf("Accept failed, %v\n", err)
continue
}
go process(client)
}
}
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
// 1. 认证
err := auth(reader, conn)
if err != nil {
log.Printf("client %v auth failed, %v\n", conn.RemoteAddr().String(), err)
return
}
err = connect(reader, conn)
if err != nil {
log.Printf("client %v connect failed: %v\n", conn.RemoteAddr().String(), err)
return
}
}
// auth 认证
func auth(reader *bufio.Reader, conn net.Conn) error {
ver, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read ver failed: %v\n", err)
}
if ver != socks5Ver {
return fmt.Errorf("not supported version\n")
}
methodSize, err := reader.ReadByte()
if err != nil {
return errors.New("read methodSize failed")
}
method := make([]byte, methodSize)
_, err = io.ReadFull(reader, method)
if err != nil {
return fmt.Errorf("read method failed: %v\n", err)
}
log.Println("ver:", ver, "method:", method)
// 认证的返回
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%v\n", err)
}
return nil
}
// connect 请求
func connect(reader *bufio.Reader, conn net.Conn) error {
buf := make([]byte, 4)
_, err := io.ReadFull(reader, buf)
if err != nil {
return fmt.Errorf("read header failed")
}
ver, cmd, atyp := buf[0], buf[1], buf[3]
if ver != socks5Ver {
return fmt.Errorf("not supported version")
}
if cmd != cmdBind {
return fmt.Errorf("not supported cmd")
}
var addr string
switch atyp {
case atypIPV4:
_, err = io.ReadFull(reader, buf)
if err != nil {
return fmt.Errorf("read atyp failed")
}
addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
case atypHOST:
hostSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read hostsize failed")
}
host := make([]byte, hostSize)
_, err = io.ReadFull(reader, host)
if err != nil {
return fmt.Errorf("read host failed")
}
addr = string(host)
case atypIPV6:
return fmt.Errorf("ipv6 not supported")
default:
return errors.New("invalid atyp")
}
_, err = io.ReadFull(reader, buf[:2]) // 只使用2个大小的slice
if err != nil {
return fmt.Errorf("read port failed")
}
port := binary.BigEndian.Uint16(buf[:2])
log.Println("dial", addr, port)
dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
if err != nil {
return fmt.Errorf("dial dst failed: %v", err)
}
defer dest.Close()
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
if err != nil {
return fmt.Errorf("write failed")
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
io.Copy(dest, reader)
cancel()
}()
go func() {
io.Copy(conn, dest)
cancel()
}()
<-ctx.Done()
return nil
}
打开终端执行如下命令查看结果:
curl --socks5 127.0.0.1:1080 -v http://www.qq.com