自己动手用 Golang 写 Socks5 代理,让其支持 Auth 和白名单

因为访问网络需求,需要使用 Socks5 代理,用 Golang 可以很方便的写出一个。

自己动手用 Golang 写 Socks5 代理,让其支持 Auth 和白名单

需求

  • 为安全考虑,支持 Auth 用户名/密码验证
  • 有些客户端不支持 Auth 验证,比如 Chrome 或系统代理,则需要 ip 白名单过滤
  • 可选择是否开启匿名模式

Socks5 流程

首先看一个简单的 Socks5 流程,然后在此基础上添加上面功能。

Go: 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 的结构

Go: 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 里处理

Go: 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 可以在刚进入时获取

Go: get clientIp
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 客户端请求

Go: http socks5 client
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 客户端请求

Go: fasthttp socks5 client
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 字符集可以更简单点

Go: fasthttp proxy
1
FastHttpClient.Dial = fasthttpproxy.FasthttpSocksDialer(`socks5://username:userpassword@127.0.0.1:1080`)

建议使用 openssl 来生成随机安全密码

Bash: openssl 生成密码
1
2
openssl rand -base64 16
openssl rand -base64 32

结语

现成的工具很多,但太复杂,自己动手撸一个符合自己简单需求,而且性能很好。