因为访问网络需求,需要使用 Socks5 代理,用 Golang 可以很方便的写出一个。
需求
- 为安全考虑,支持 Auth 用户名/密码验证
- 有些客户端不支持 Auth 验证,比如 Chrome 或系统代理,则需要 ip 白名单过滤
- 可选择是否开启匿名模式
Socks5 流程
首先看一个简单的 Socks5 流程,然后在此基础上添加上面功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
ln, err := net.Listen("tcp", ":1080")
if err != nil {
panic(err)
return
}
for {
var conn net.Conn
conn, err = ln.Accept() // 监听请求
if err != nil {
log.Println(err)
return
}
go handleConnection(conn) // 启动一个 goroutine 来处理它,主进程再回到上面监听等待
}
}
函数 handleConnection
的结构
1
2
3
4
5
6
7
8
9
10
11
12
13
func handleConnection(conn net.Conn) {
defer conn.Close()
if err := handShake(conn); err != nil {
log.Println("socks handshake:", err)
return
}
addr, err := parseTarget(conn)
if err != nil {
log.Println("socks consult transfer mode or parse target :", err)
return
}
pipeWhenClose(conn, addr)
}
支持 Auth 和白名单
要实现 支持 Auth 和白名单就需要在 handShake
里处理
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
func handShake(conn net.Conn) (err error) {
const (
idVer = 0
idNmethod = 1
)
buf := make([]byte, 258)
var n int
// make sure we get the nmethod field
if n, err = io.ReadAtLeast(conn, buf, idNmethod+1); err != nil {
return
}
if buf[idVer] != socksVer5 {
return errVer
}
nmethod := int(buf[idNmethod]) // client support auth mode
msgLen := nmethod + 2 // auth msg length
if n == msgLen { // handshake done, common case
// do nothing, jump directly to send confirmation
} else if n < msgLen { // has more methods to read, rare case
if _, err = io.ReadFull(conn, buf[n:msgLen]); err != nil {
return
}
} else { // error, should not get extra data
return errAuthExtraData
}
/*
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
*/
if nmethod == 1 {
// 无密码登录,需要验证白名单
// if in list client.Write([]byte{0x05, 0x00})
// else client.Write([]byte{0x05, 0xff}) or client.Write([]byte{0x05, 0x02})
if AllowNobody {
_, err = conn.Write([]byte{socksVer5, 0x00}) //无需认证
return
}
_, _ = conn.Write([]byte{socksVer5, 0xff})
err = errors.New(" not Allow Nobody")
return
} else if nmethod == 2 {
// 用户名/ 密码登录
_, err = conn.Write([]byte{socksVer5, 0x02})
if err != nil {
return
}
} else {
_, err = conn.Write([]byte{socksVer5, 0xff})
if err != nil {
return
}
err = errors.New("method forbidden")
return
}
// 检测用户/密码
_, err = conn.Read(buf[0:])
if err != nil {
return
}
b0 := buf[0]
nameLens := int(buf[1])
uName := string(buf[2 : 2+nameLens])
passLens := int(buf[2+nameLens])
uPass := string(buf[2+nameLens+1 : 2+nameLens+1+passLens])
if uName != userName || uPass != userPass {
_, _ = conn.Write([]byte{b0, 0xff})
err = errors.New("authentication failed")
// 可以对 clientIp 处理,如出错次数,防止被穷取破解
return
}
// send confirmation: version 5, no authentication required
_, err = conn.Write([]byte{b0, 0x00})
return
}
客户端请求ip 可以在刚进入时获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for {
var conn net.Conn
conn, err = ln.Accept() // 监听请求
if err != nil {
log.Println(err)
return
}
// 获取客户端 ip
clientIp := strings.Split(conn.RemoteAddr().String(), ":")[0]
// 对 clientIp 处理,如果被限制就直接退出
if forbidden {
conn.Close()
return
}
go handleConnection(conn) // 启动一个 goroutine 来处理它,主进程再回到上面监听等待
}
客户端
http 客户端请求
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
var tr = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
dialer, err := proxy.SOCKS5("tcp", "127.0.0.1:1080",
&proxy.Auth{User:"username", Password:"password"},
&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
},
)
if err == nil {
tr.DialContext = func(ctx context.Context, network, addr string) (conn net.Conn, e error) {
c, e := dialer.Dial(network, addr)
return c, e
}
}
var httpClient = http.Client{
Timeout: time.Second * 30,
Transport: tr,
}
req, _ := http.NewRequest("GET","https://httpbin.org/get", nil)
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)")
resp, err := httpClient.Do(req)
fasthttp 客户端请求
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
FastHttpClient := &fasthttp.Client{
TLSConfig: &tls.Config{InsecureSkipVerify: true},
NoDefaultUserAgentHeader: true,
MaxConnsPerHost: 12000,
ReadBufferSize: 4096,
WriteBufferSize: 4096,
ReadTimeout: time.Minute,
WriteTimeout: time.Minute,
MaxIdleConnDuration: time.Minute,
DisableHeaderNamesNormalizing: true,
}
dialer, err := proxy.SOCKS5("tcp", "127.0.0.1:1080",
&proxy.Auth{User:"username", Password:"password"},
&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
},
)
if err == nil {
FastHttpClient.Dial = func(addr string) (net.Conn, error) {
if err != nil {
return nil, err
}
return dialer.Dial("tcp", addr)
}
}
req := fasthttp.AcquireRequest()
res := fasthttp.AcquireResponse()
defer func() {
fasthttp.ReleaseRequest(req)
fasthttp.ReleaseResponse(res)
}()
req.Header.SetMethod("GET")
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)")
req.SetRequestURI("https://httpbin.org/get")
err = FastHttpClient.DoRedirects(req, res, 5)
if err != nil {
log.Println(err)
return
}
如果用户名/密码是 Base64 字符集可以更简单点,也可以用 URL-based encoding 转换特殊字符。
1
FastHttpClient.Dial = fasthttpproxy.FasthttpSocksDialer(`socks5://username:userpassword@127.0.0.1:1080`)
建议使用 openssl
来生成随机安全密码
1
2
openssl rand -base64 16
openssl rand -base64 32
结语
现成的工具很多,但太复杂,自己动手撸一个符合自己简单需求,而且性能很好。
参考
- 「Golang实现简单的Socks5代理」 https://golangnote.com/topic/258.html
本文网址: https://golangnote.com/topic/297.html 转摘请注明来源