Go 语言的实战案例 SOCKS5 代理 | 青训营

摘要
Title: Go 语言的实战案例 SOCKS5 代理 | 青训营
Tag: ByteDance、Go、后端

Powered by:NEFU AB-IN

Go 语言的实战案例 SOCKS5 代理 | 青训营

  • GO语言工程实践课后作业:实现思路、代码以及路径记录

引入

代理是指在计算机网络中,代理服务器充当客户端和目标服务器之间的中介。它接收来自客户端的请求,然后将请求转发给目标服务器,再将目标服务器的响应返回给客户端。

用途

  • 匿名浏览:Socks5代理可以隐藏客户端的真实IP地址,使得客户端在访问目标服务器时更加匿名。可以代表客户端向其他服务器发送请求

    • 在网络爬虫中,使用代理服务器可以解决IP访问频率限制问题,因为它们可以隐藏真实的客户端IP地址并提供新的IP地址来进行请求,从而减轻对单个IP的访问频率限制。这样,爬虫就可以通过多个代理IP来轮流发送请求,避免被目标服务器封禁。
  • 数据加密:Socks5代理支持对数据流进行加密传输,提供更高的数据安全性。

  • 跨协议代理:Socks5代理不仅支持HTTP协议,还支持包括FTP、SMTP等在内的多种协议,实现了更广泛的应用场景。

  • 穿越防火墙:由于Socks5代理更通用,不受网络防火墙的限制,可以更容易地穿越网络防火墙,访问被封锁或限制的资源。

Socks5代理是一种代理协议,用于在网络上进行数据传输。Socks5代理服务器可以支持多种协议的代理,包括HTTP、FTP、SMTP等。与HTTP代理相比,Socks5代理更加灵活和通用。

主要区别如下:

  • HTTP代理:主要用于HTTP请求,支持浏览器的代理设置,透明代理
  • Socks5代理:支持HTTP以外的其他协议的代理,不支持浏览器的代理设置,需要在应用程序中设置代理,Socks5代理的主要特点是它可以在客户端和代理服务器之间建立一个 TCP 或 UDP 连接,并直接将数据转发给目标服务器,而不需要解析或修改数据包。这种特性使得SOCKS5代理适用于很多不同类型的网络应用,如 P2P 文件共享、实时视频传输等。

代理过程通常如下:

  1. 客户端向代理服务器发送连接请求,并指定目标服务器的地址。
  2. 代理服务器接收到请求后,解析目标服务器地址,并建立与目标服务器的连接。
  3. 代理服务器将客户端的请求转发给目标服务器。
  4. 目标服务器接收到请求后,处理请求并生成响应。
  5. 代理服务器将目标服务器的响应转发给客户端。
  6. 客户端接收到代理服务器返回的响应,并进行处理。

image.png
总的来说,代理服务器充当了客户端和目标服务器之间的中转站,使得客户端能间接与目标服务器进行通信,提供了更多的隐私保护和功能扩展。

TCP echo server

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
/*
* @Author: NEFU AB-IN
* @Date: 2023-08-23 22:36:26
* @FilePath: \GoTest\6\6.go
* @LastEditTime: 2023-08-23 22:43:57
*/
package main

import (
"bufio"
"log"
"net"
)

// TCP echo server

func main() {
server, err := net.Listen("tcp", "127.0.0.1:1080") // 监听端口,返回一个server
if err != nil {
panic(err)
}
for {
client, err := server.Accept() // 在死循环里,用这个方法接收一个请求
if err != nil {
log.Printf("Accept failed %v", err)
continue
}
go process(client) // 处理这个链接 go关键字 启动子线程去处理
// 如果接受连接时出现错误,服务器会记录错误信息,然后继续等待下一个客户端连接
// 否则为每个客户端连接启动一个独立的process协程来处理客户端请求,这样可以并发处理多个客户端连接。
}
}

func process(conn net.Conn) {
defer conn.Close() // 最后将连接关掉
reader := bufio.NewReader(conn) // 基于这个链接,创造一个只读的带缓冲的流
for {
b, err := reader.ReadByte() // 每次读一个字节
if err != nil {
break
}
_, err = conn.Write([]byte{b}) // 将读取的字节回显给客户端,直接写回到连接中
if err != nil {
break
}
}
}

image.png

auth 认证

认证阶段:实现auth函数,更改process函数,里面用到auth函数

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
87
88
89
90
91
92
93
94
95
/*
* @Author: NEFU AB-IN
* @Date: 2023-08-23 22:50:05
* @FilePath: \GoTest\7\7.go
* @LastEditTime: 2023-08-23 22:50:11
*/
package main

import (
"bufio"
"fmt"
"io"
"log"
"net"
)
// 一些常量定义,用于表示Socks5协议中的一些数值,例如协议版本、命令类型和地址类型等。
const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

func main() {
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", err)
continue
}
go process(client)
}
}

func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
err := auth(reader, conn)
if err != nil {
log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
return
}
log.Println("auth success")
}

func auth(reader *bufio.Reader, conn net.Conn) (err error) {

// 第一步:浏览器向代理服务器发送报文,三个字段
// +----+----------+----------+
// |VER | NMETHODS | METHODS |
// +----+----------+----------+
// | 1 | 1 | 1 to 255 |
// +----+----------+----------+
// VER: 协议版本,socks5为0x05
// NMETHODS: 支持认证的方法数量
// METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。
// RFC预定义了一些值的含义,内容如下:
// X’00’ NO AUTHENTICATION REQUIRED
// X’02’ USERNAME/PASSWORD


// 前两个字段都是单字节,所以从流中单独读出
ver, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read ver failed:%w", err)
}
if ver != socks5Ver {
return fmt.Errorf("not supported ver:%v", ver)
}
methodSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read methodSize failed:%w", err)
}
method := make([]byte, methodSize) // 创建缓冲区
_, err = io.ReadFull(reader, method) // 它会从r中连续读取数据,直到buf被填满,或者遇到了一个错误。如果读取完整的buf之前发生错误,函数会返回读取的字节数n和错误信息err;如果成功读取了整个buf,函数会返回buf的长度和nil。
// 这个函数在需要确保读取完整数据包时很有用,比如读取固定长度的消息、解码二进制数据等。
if err != nil {
return fmt.Errorf("read method failed:%w", err)
}
log.Println("ver", ver, "method", method)
// +----+--------+
// |VER | METHOD |
// +----+--------+
// | 1 | 1 |
// +----+--------+
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}

请求阶段

请求阶段:实现connect函数,签名一致,在process中调用

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package main

import (
"bufio"
"encoding/binary"
"errors"
"fmt"
"io"
"log"
"net"
)

const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

func main() {
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", err)
continue
}
go process(client)
}
}

func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
err := auth(reader, conn)
if err != nil {
log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
return
}
err = connect(reader, conn)
if err != nil {
log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
return
}
}

func auth(reader *bufio.Reader, conn net.Conn) (err error) {
// +----+----------+----------+
// |VER | NMETHODS | METHODS |
// +----+----------+----------+
// | 1 | 1 | 1 to 255 |
// +----+----------+----------+
// VER: 协议版本,socks5为0x05
// NMETHODS: 支持认证的方法数量
// METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
// X’00’ NO AUTHENTICATION REQUIRED
// X’02’ USERNAME/PASSWORD

ver, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read ver failed:%w", err)
}
if ver != socks5Ver {
return fmt.Errorf("not supported ver:%v", ver)
}
methodSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read methodSize failed:%w", err)
}
method := make([]byte, methodSize)
_, err = io.ReadFull(reader, method)
if err != nil {
return fmt.Errorf("read method failed:%w", err)
}

// +----+--------+
// |VER | METHOD |
// +----+--------+
// | 1 | 1 |
// +----+--------+
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}

func connect(reader *bufio.Reader, conn net.Conn) (err error) {
// 第二步:浏览器发送报文,包含六个字段
// +----+-----+-------+------+----------+----------+
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
// +----+-----+-------+------+----------+----------+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
// +----+-----+-------+------+----------+----------+
// VER 版本号,socks5的值为0x05
// CMD 0x01表示CONNECT请求
// RSV 保留字段,值为0x00
// ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
// 0x01表示IPv4地址,DST.ADDR为4个字节
// 0x03表示域名,DST.ADDR是一个可变长度的域名
// DST.ADDR 一个可变长度的值
// DST.PORT 目标端口,固定2个字节


// !!!! 可以看到前四个字段都是单字节,可以采用之前的方法一个个读出来,这里直接构造一个4字节的缓冲区,用readfull全读出来
buf := make([]byte, 4)
_, err = io.ReadFull(reader, buf)
if err != nil {
return fmt.Errorf("read header failed:%w", err)
}
ver, cmd, atyp := buf[0], buf[1], buf[3]
if ver != socks5Ver {
return fmt.Errorf("not supported ver:%v", ver)
}
if cmd != cmdBind {
return fmt.Errorf("not supported cmd:%v", cmd)
}
addr := ""
switch atyp {
case atypeIPV4: // 若是IPV4 读四个字节
_, err = io.ReadFull(reader, buf)
if err != nil {
return fmt.Errorf("read atyp failed:%w", err)
}
addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
case atypeHOST: // 若是域名,先读长度,构造缓冲区,最后转换为字符串
hostSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read hostSize failed:%w", err)
}
host := make([]byte, hostSize)
_, err = io.ReadFull(reader, host)
if err != nil {
return fmt.Errorf("read host failed:%w", err)
}
addr = string(host)
case atypeIPV6: // 先不实现IPV6
return errors.New("IPv6: no supported yet")
default:
return errors.New("invalid atyp")
}
_, err = io.ReadFull(reader, buf[:2]) // 这里复用之前的缓冲区,切片效果,只用前两位,填充满
if err != nil {
return fmt.Errorf("read port failed:%w", err)
}
port := binary.BigEndian.Uint16(buf[:2]) // 用于将 2 字节的大端序字节序列解码为一个无符号 16 位整数(uint16)。

log.Println("dial", addr, port)

// 给予回报
// +----+-----+-------+------+----------+----------+
// |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
// +----+-----+-------+------+----------+----------+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
// +----+-----+-------+------+----------+----------+
// VER socks版本,这里为0x05
// REP Relay field,内容取值如下 X’00’ succeeded
// RSV 保留字段
// ATYPE 地址类型
// BND.ADDR 服务绑定的地址
// BND.PORT 服务绑定的端口DST.PORT
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0}) // 拼成byte数组
if err != nil {
return fmt.Errorf("write failed: %w", err)
}
return nil
}

relay阶段

relay阶段:最后一步,真正和服务器建立TCP连接,在 connect 函数中,增加功能

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
package main

import (
"bufio"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"log"
"net"
)

const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

func main() {
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", err)
continue
}
go process(client)
}
}

func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
err := auth(reader, conn)
if err != nil {
log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
return
}
err = connect(reader, conn)
if err != nil {
log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
return
}
}

func auth(reader *bufio.Reader, conn net.Conn) (err error) {
// +----+----------+----------+
// |VER | NMETHODS | METHODS |
// +----+----------+----------+
// | 1 | 1 | 1 to 255 |
// +----+----------+----------+
// VER: 协议版本,socks5为0x05
// NMETHODS: 支持认证的方法数量
// METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
// X’00’ NO AUTHENTICATION REQUIRED
// X’02’ USERNAME/PASSWORD

ver, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read ver failed:%w", err)
}
if ver != socks5Ver {
return fmt.Errorf("not supported ver:%v", ver)
}
methodSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read methodSize failed:%w", err)
}
method := make([]byte, methodSize)
_, err = io.ReadFull(reader, method)
if err != nil {
return fmt.Errorf("read method failed:%w", err)
}

// +----+--------+
// |VER | METHOD |
// +----+--------+
// | 1 | 1 |
// +----+--------+
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}

func connect(reader *bufio.Reader, conn net.Conn) (err error) {
// +----+-----+-------+------+----------+----------+
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
// +----+-----+-------+------+----------+----------+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
// +----+-----+-------+------+----------+----------+
// VER 版本号,socks5的值为0x05
// CMD 0x01表示CONNECT请求
// RSV 保留字段,值为0x00
// ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
// 0x01表示IPv4地址,DST.ADDR为4个字节
// 0x03表示域名,DST.ADDR是一个可变长度的域名
// DST.ADDR 一个可变长度的值
// DST.PORT 目标端口,固定2个字节

buf := make([]byte, 4)
_, err = io.ReadFull(reader, buf)
if err != nil {
return fmt.Errorf("read header failed:%w", err)
}
ver, cmd, atyp := buf[0], buf[1], buf[3]
if ver != socks5Ver {
return fmt.Errorf("not supported ver:%v", ver)
}
if cmd != cmdBind {
return fmt.Errorf("not supported cmd:%v", cmd)
}
addr := ""
switch atyp {
case atypeIPV4:
_, err = io.ReadFull(reader, buf)
if err != nil {
return fmt.Errorf("read atyp failed:%w", err)
}
addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
case atypeHOST:
hostSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read hostSize failed:%w", err)
}
host := make([]byte, hostSize)
_, err = io.ReadFull(reader, host)
if err != nil {
return fmt.Errorf("read host failed:%w", err)
}
addr = string(host)
case atypeIPV6:
return errors.New("IPv6: no supported yet")
default:
return errors.New("invalid atyp")
}
_, err = io.ReadFull(reader, buf[:2])
if err != nil {
return fmt.Errorf("read port failed:%w", err)
}
port := binary.BigEndian.Uint16(buf[:2])

// 代理服务器与下游服务器(远端)建立tcp连接,使用 `net.Dial` 函数连接到目标服务器 `dest`,即客户端所请求的远程服务器。这样就建立了代理服务器与目标服务器之间的连接。
dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
if err != nil {
return fmt.Errorf("dial dst failed:%w", err)
}
defer dest.Close()
log.Println("dial", addr, port)

// +----+-----+-------+------+----------+----------+
// |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
// +----+-----+-------+------+----------+----------+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
// +----+-----+-------+------+----------+----------+
// VER socks版本,这里为0x05
// REP Relay field,内容取值如下 X’00’ succeeded
// RSV 保留字段
// ATYPE 地址类型
// BND.ADDR 服务绑定的地址
// BND.PORT 服务绑定的端口DST.PORT

// 返回连接成功的报文
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
if err != nil {
return fmt.Errorf("write failed: %w", err)
}


// 这段代码的主要功能是实现两个goroutine并行执行io.Copy操作,并在其中一个goroutine完成后取消另一个goroutine的执行。
//- 第一行创建一个新的上下文对象ctx,并使用context.Background()作为根上下文对象。这个上下文对象用于在不同goroutine之间传递取消信号。
//- 第二行定义了一个cancel函数,用于取消上下文对象ctx。defer关键字用于延迟执行cancel函数,在函数返回时自动调用。
//- 第四行开启了一个goroutine,该goroutine负责将读取到的数据从reader复制到dest,并在复制完成后调用cancel函数取消上下文对象的执行。这样做是为了在复制完成后,能正确地通知另一个goroutine停止执行。
//- 第六行开启了第二个goroutine,该goroutine负责将dest的数据复制到conn,并在复制完成后也调用cancel函数取消上下文对象的执行。
//- 第九行使用<-ctx.Done()语法,表示等待上下文对象ctx被取消。一旦ctx被取消,即ctx.Done()返回,程序会继续往下执行。

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

go func() {
_, _ = io.Copy(dest, reader)
cancel()
}()
go func() {
_, _ = io.Copy(conn, dest)
cancel()
}()

<-ctx.Done() // 阻塞等待,直到两个数据转发的Go协程都完成后,函数返回。执行时机,也就是cancel的时间。
return nil
}

ctx, cancel := context.WithCancel(context.Background())解释

  • 上下文对象是 Go 语言中用于跨 goroutine 传递请求的值、截止时间以及取消信号的一种机制。它可以用来解决在并发环境下协调多个 goroutine 之间的操作和通信的问题。

  • 上下文对象通常用于以下几种情况:

    1. 传递请求的相关值:例如请求ID、用户信息等,在多个 goroutine 之间共享。
    2. 设置截止时间:对某些操作设置一个超时时间,用于控制操作的执行时间。
    3. 取消信号:当一个操作出现问题或者不再需要执行时,可以通过取消上下文对象来通知相关的 goroutine 停止执行。
  • context.WithCancel(context.Background()) 这句代码的意思是创建一个具有取消功能的上下文对象(WithCancel()函数接受一个 Context 并返回其子Context和取消函数cancel)。context.Background() 创建一个根上下文对象,而 context.WithCancel 则基于根上下文对象创建一个新的上下文对象,该上下文对象具有取消功能。返回的 ctx 和 cancel 是用于取消该上下文对象的函数。

  • 所以这句代码的作用就是创建一个新的上下文对象 ctx,并使用 context.WithCancel 函数将其绑定到一个取消函数 cancel 上。这样可以通过调用 cancel 函数来取消该上下文对象的执行。该上下文对象可以被传递给其他需要依赖上下文的 goroutine 来实现协作和取消的功能。

image.png

使用搜索:谷歌必应百度